Skip to content

Commit

Permalink
feat: push permission prompt (#101)
Browse files Browse the repository at this point in the history
Co-authored-by: Rehan <[email protected]>
Co-authored-by: Levi Bostian <[email protected]>
  • Loading branch information
3 people authored Mar 3, 2023
1 parent d394da5 commit 1abe9b3
Show file tree
Hide file tree
Showing 12 changed files with 289 additions and 5 deletions.
2 changes: 2 additions & 0 deletions android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="io.customer.reactnative.sdk">

<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

</manifest>
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
package io.customer.reactnative.sdk

import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.bridge.*
import io.customer.reactnative.sdk.extension.toMap
import io.customer.reactnative.sdk.messagingpush.RNCIOPushMessaging
import io.customer.sdk.CustomerIO
import io.customer.sdk.CustomerIOShared
import io.customer.sdk.util.Logger

class CustomerIOReactNativeModule(
reactContext: ReactApplicationContext,
private val pushMessagingModule: RNCIOPushMessaging,
private val inAppMessagingModule: RNCIOInAppMessaging,
) : ReactContextBaseJavaModule(reactContext) {
private val logger: Logger
Expand Down Expand Up @@ -111,6 +110,16 @@ class CustomerIOReactNativeModule(
customerIO.registerDeviceToken(token)
}

@ReactMethod
fun getPushPermissionStatus(promise: Promise) {
pushMessagingModule.getPushPermissionStatus(promise)
}

@ReactMethod
fun showPromptForPushNotifications(pushConfigurationOptions: ReadableMap?, promise: Promise) {
pushMessagingModule.showPromptForPushNotifications(pushConfigurationOptions, promise)
}

companion object {
internal const val MODULE_NAME = "CustomerioReactnative"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,17 @@ import com.facebook.react.ReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.uimanager.ViewManager
import io.customer.reactnative.sdk.messagingpush.RNCIOPushMessaging

class CustomerIOReactNativePackage : ReactPackage {
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
val pushMessagingModule = RNCIOPushMessaging(reactContext)
val inAppMessagingModule = RNCIOInAppMessaging(reactContext)
return listOf(
inAppMessagingModule,
CustomerIOReactNativeModule(
reactContext = reactContext,
pushMessagingModule = pushMessagingModule,
inAppMessagingModule = inAppMessagingModule,
),
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package io.customer.reactnative.sdk.messagingpush

/**
* Enum class for wrapping status of Android app permissions.
*/
enum class PermissionStatus {
Granted,
Denied,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package io.customer.reactnative.sdk.messagingpush

import android.Manifest
import android.content.pm.PackageManager
import android.os.Build
import androidx.core.content.ContextCompat
import com.facebook.react.bridge.*
import com.facebook.react.modules.core.PermissionAwareActivity
import com.facebook.react.modules.core.PermissionListener

/**
* ReactNative module to hold push messages features in a single place to bridge with native code.
*/
class RNCIOPushMessaging(
private val reactContext: ReactApplicationContext,
) : ReactContextBaseJavaModule(reactContext), PermissionListener {
/**
* Temporarily holds reference for notification request as the request is dependent on Android
* lifecycle and cannot be completed instantly.
*/
private var notificationRequestPromise: Promise? = null

@ReactMethod
fun getPushPermissionStatus(promise: Promise) {
promise.resolve(checkPushPermissionStatus().toReactNativeResult)
}

/**
* To request push notification permissions using native apis. Push notifications doesn't
* require permissions for Android versions older than 13, so the results are returned instantly.
* For newer versions, the permission is requested and the promise is resolved after the request
* has been completed.
*
* @param pushConfigurationOptions configurations options for push notifications, required for
* iOS only, unused on Android.
* @param promise to resolve and return the results.
*/
@ReactMethod
fun showPromptForPushNotifications(pushConfigurationOptions: ReadableMap?, promise: Promise) {
// Skip requesting permissions when already granted
if (checkPushPermissionStatus() == PermissionStatus.Granted) {
promise.resolve(PermissionStatus.Granted.toReactNativeResult)
return
}

try {
val activity = currentActivity
val permissionAwareActivity = activity as? PermissionAwareActivity
if (permissionAwareActivity == null) {
promise.reject(
"E_ACTIVITY_DOES_NOT_EXIST",
"Permission cannot be requested because activity doesn't exist. Please make sure to request permission from UI components only"
)
return
}

notificationRequestPromise = promise
permissionAwareActivity.requestPermissions(
arrayOf(POST_NOTIFICATIONS_PERMISSION_NAME),
POST_NOTIFICATIONS_PERMISSION_REQUEST,
this,
)
} catch (ex: Throwable) {
promise.reject(ex)
notificationRequestPromise = null
}
}

/**
* Checks current permission of push notification permission
*/
private fun checkPushPermissionStatus(): PermissionStatus =
// Skip requesting permissions for older versions where not required
if (Build.VERSION.SDK_INT < BUILD_VERSION_CODE_TIRAMISU || ContextCompat.checkSelfPermission(
reactContext, POST_NOTIFICATIONS_PERMISSION_NAME,
) == PackageManager.PERMISSION_GRANTED
) PermissionStatus.Granted
else PermissionStatus.Denied

/**
* Resolves and clears promise with the provided permission status
*/
private fun resolvePermissionPromise(status: PermissionStatus) {
notificationRequestPromise?.resolve(status.toReactNativeResult)
notificationRequestPromise = null
}

override fun onRequestPermissionsResult(
requestCode: Int, permissions: Array<String>, grantResults: IntArray,
): Boolean = when (requestCode) {
POST_NOTIFICATIONS_PERMISSION_REQUEST -> {
// If request is cancelled, the result arrays are empty.
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
resolvePermissionPromise(PermissionStatus.Granted)
} else {
resolvePermissionPromise(PermissionStatus.Denied)
}
true // as this permission listener can be removed now
}
else -> false // desired permission not yet granted, so we will keep the listener
}

override fun getName(): String = "CustomerioPushMessaging"

/**
* Maps native class to react native supported type so the result can be passed on to JS/TS classes.
*/
private val PermissionStatus.toReactNativeResult: Any
get() = this.name

companion object {
/**
* Copying value os [Manifest.permission.POST_NOTIFICATIONS] as constant so we don't have to
* force newer compile sdk versions
*/
private const val POST_NOTIFICATIONS_PERMISSION_NAME =
"android.permission.POST_NOTIFICATIONS"
private const val BUILD_VERSION_CODE_TIRAMISU = 33
private const val POST_NOTIFICATIONS_PERMISSION_REQUEST = 24676
}
}
5 changes: 5 additions & 0 deletions ios/CustomerioReactnative.m
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,10 @@ @interface RCT_EXTERN_MODULE(CustomerioReactnative, NSObject)

RCT_EXTERN_METHOD(registerDeviceToken : (nonnull NSString *) token)

RCT_EXTERN_METHOD(showPromptForPushNotifications: (NSDictionary *) options
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)

RCT_EXTERN_METHOD(getPushPermissionStatus: (RCTPromiseResolveBlock) resolver
rejecter:(RCTPromiseRejectBlock)rejecter)
@end
91 changes: 90 additions & 1 deletion ios/CustomerioReactnative.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@ import Foundation
import CioTracking
import Common
import CioMessagingInApp
import UserNotifications

enum PushPermissionStatus: String, CaseIterable {
case denied
case notDetermined
case granted

var value: String {
return rawValue.capitalized
}
}

@objc(CustomerioReactnative)
class CustomerioReactnative: NSObject {
Expand Down Expand Up @@ -43,6 +54,11 @@ class CustomerioReactnative: NSObject {
if let isEnableInApp = configData["enableInApp"] as? Bool, isEnableInApp {
initializeInApp()
}

// Register app for push notifications if not done already
DispatchQueue.main.async {
UIApplication.shared.registerForRemoteNotifications()
}
}

/**
Expand Down Expand Up @@ -120,6 +136,79 @@ class CustomerioReactnative: NSObject {
func registerDeviceToken(token: String) -> Void {
CustomerIO.shared.registerDeviceToken(token)
}

/**
To show push notification prompt if current authorization status is not determined
*/
@objc(showPromptForPushNotifications:resolver:rejecter:)
func showPromptForPushNotifications(options : Dictionary<String, AnyHashable>, resolver resolve: @escaping(RCTPromiseResolveBlock), rejecter reject: @escaping(RCTPromiseRejectBlock)) -> Void {

// Show prompt if status is not determined
getPushNotificationPermissionStatus { status in
if status == .notDetermined {
self.requestPushAuthorization(options: options) { permissionStatus in

guard let status = permissionStatus as? Bool else {
reject("[CIO]", "Error requesting push notification permission.", permissionStatus as? Error)
return
}
resolve(status ? PushPermissionStatus.granted.value : PushPermissionStatus.denied.value)
}
} else {
resolve(status.value)
}
}
}

/**
This functions gets the current status of push notification permission and returns as a promise
*/
@objc(getPushPermissionStatus:rejecter:)
func getPushPermissionStatus(resolver resolve: @escaping(RCTPromiseResolveBlock), rejecter reject: @escaping(RCTPromiseRejectBlock)) -> Void {
getPushNotificationPermissionStatus { status in
resolve(status.value)
}
}

private func requestPushAuthorization(options: [String: Any], onComplete : @escaping(Any) -> Void
) {
let current = UNUserNotificationCenter.current()
var notificationOptions : UNAuthorizationOptions = [.alert]
if let ios = options["ios"] as? [String: Any] {

if let soundOption = ios["sound"] as? Bool, soundOption {
notificationOptions.insert(.sound)
}
if let bagdeOption = ios["badge"] as? Bool, bagdeOption {
notificationOptions.insert(.badge)
}
}
current.requestAuthorization(options: notificationOptions) { isGranted, error in
if let error = error {
onComplete(error)
return
}
onComplete(isGranted)
}
}

private func getPushNotificationPermissionStatus(completionHandler: @escaping(PushPermissionStatus) -> Void) {
var status = PushPermissionStatus.notDetermined
let current = UNUserNotificationCenter.current()
current.getNotificationSettings(completionHandler: { permission in
switch permission.authorizationStatus {
case .authorized, .provisional, .ephemeral:
status = .granted
case .denied:
status = .denied
default:
status = .notDetermined
}
completionHandler(status)
})
}

// MARK: - Push Notifications - End
/**
Initialize in-app using customerio package
*/
Expand All @@ -144,7 +233,7 @@ extension CustomerioReactnative: InAppEventListener {
body["actionName"] = actionName
}
CustomerioInAppMessaging.shared?.sendEvent(
withName: "InAppEventListener",
withName: "InAppEventListener",
body: body
)
}
Expand Down
31 changes: 31 additions & 0 deletions src/CustomerioTracking.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
} from './CustomerioConfig';
import { Region } from './CustomerioEnum';
import { CustomerIOInAppMessaging } from './CustomerIOInAppMessaging';
import type { PushPermissionStatus, PushPermissionOptions } from './types';
var pjson = require("customerio-reactnative/package.json");

const LINKING_ERROR =
Expand Down Expand Up @@ -143,6 +144,36 @@ class CustomerIO {
}
CustomerioReactnative.registerDeviceToken(token)
}

/**
* Request to show prompt for push notification permissions.
* Prompt will only be shown if the current status is - not determined.
* In other cases, this function will return current status of permission.
* @param options
* @returns Success & Failure promises
*/
static async showPromptForPushNotifications(
options?: PushPermissionOptions
): Promise<PushPermissionStatus> {
let defaultOptions: PushPermissionOptions = {
ios: {
badge: true,
sound: true,
},
};

return CustomerioReactnative.showPromptForPushNotifications(
options || defaultOptions
);
}

/**
* Get status of push permission for the app
* @returns Promise with status of push permission as a string
*/
static getPushPermissionStatus() : Promise<PushPermissionStatus> {
return CustomerioReactnative.getPushPermissionStatus()
}
}

export { CustomerIO, Region };
2 changes: 2 additions & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,5 @@ export {
CustomerIOEnv,
CioLogLevel
};

export * from "./types";
6 changes: 6 additions & 0 deletions src/types/PushPermissionOptions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export interface PushPermissionOptions {
ios?: {
badge: boolean;
sound: boolean;
};
}
5 changes: 5 additions & 0 deletions src/types/PushPermissionStatus.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export enum PushPermissionStatus {
Granted = "Granted",
Denied = "Denied",
NotDetermined = "NotDetermined"
}
2 changes: 2 additions & 0 deletions src/types/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './PushPermissionOptions'
export * from './PushPermissionStatus'

0 comments on commit 1abe9b3

Please sign in to comment.