Skip to content

Commit

Permalink
feat: process push notifications received outside CIO SDK (#117)
Browse files Browse the repository at this point in the history
  • Loading branch information
mrehan27 authored Apr 4, 2023
1 parent f35df6d commit 458472d
Show file tree
Hide file tree
Showing 5 changed files with 158 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class CustomerIOReactNativePackage : ReactPackage {
val inAppMessagingModule = RNCIOInAppMessaging(reactContext)
return listOf(
inAppMessagingModule,
pushMessagingModule,
CustomerIOReactNativeModule(
reactContext = reactContext,
pushMessagingModule = pushMessagingModule,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.customer.reactnative.sdk.extension

import com.facebook.react.bridge.ReadableMap
import com.google.firebase.messaging.RemoteMessage
import io.customer.sdk.data.model.Region
import io.customer.sdk.util.CioLogLevel

Expand All @@ -20,3 +21,50 @@ internal fun Double?.toCIOLogLevel(fallback: CioLogLevel = CioLogLevel.NONE): Ci
return if (this == null) fallback
else CioLogLevel.values().getOrNull(index = toInt() - 1) ?: fallback
}

/**
* Safely transforms any value to string
*/
private fun Any.toStringOrNull(): String? = try {
toString()
} catch (ex: Exception) {
// We don't need to print any error here as this is expected for some values and doesn't
// break anything
null
}

/**
* Extension function to build FCM [RemoteMessage] using RN map. This should be independent from
* the sender source and should be able to build a valid [RemoteMessage] for our native SDK.
*
* @param destination receiver of the message. It is mainly required for sending upstream messages,
* since we are using RemoteMessage only for broadcasting messages locally, we can use any non-empty
* string for it.
*/
internal fun ReadableMap.toFCMRemoteMessage(destination: String): RemoteMessage {
val messageParams = buildMap {
putAll(getMap("notification").toMap())
// Adding `data` after `notification` so `data` params take more value as we mainly use
// `data` in rich push
putAll(getMap("data").toMap())
}
return with(RemoteMessage.Builder(destination)) {
messageParams.let { params ->
val paramsIterator = params.iterator()
while (paramsIterator.hasNext()) {
val (key, value) = paramsIterator.next()
// Some values in notification object can be another object and may not support
// mapping to string values, transforming these values in a try-catch so the code
// doesn't break due to one bad value
value.toStringOrNull()?.let { v -> addData(key, v) }
}
}
getString("messageId")?.let { id -> setMessageId(id) }
getString("messageType")?.let { type -> setMessageType(type) }
getString("collapseKey")?.let { key -> setCollapseKey(key) }
if (hasKey("ttl")) {
ttl = getInt("ttl")
}
build()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,22 @@ import androidx.core.content.ContextCompat
import com.facebook.react.bridge.*
import com.facebook.react.modules.core.PermissionAwareActivity
import com.facebook.react.modules.core.PermissionListener
import io.customer.messagingpush.CustomerIOFirebaseMessagingService
import io.customer.reactnative.sdk.extension.takeIfNotBlank
import io.customer.reactnative.sdk.extension.toFCMRemoteMessage
import io.customer.sdk.CustomerIOShared
import io.customer.sdk.util.Logger
import java.util.*

/**
* 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 {
private val logger: Logger
get() = CustomerIOShared.instance().diStaticGraph.logger

/**
* Temporarily holds reference for notification request as the request is dependent on Android
* lifecycle and cannot be completed instantly.
Expand Down Expand Up @@ -66,6 +75,36 @@ class RNCIOPushMessaging(
}
}

/**
* Handles push notification received. This is helpful in processing push notifications
* received outside the CIO SDK.
*
* @param message push payload received from FCM.
* @param handleNotificationTrigger indicating if the local notification should be triggered.
*/
@ReactMethod
fun handleMessage(message: ReadableMap?, handleNotificationTrigger: Boolean, promise: Promise) {
try {
if (message == null) {
promise.reject(IllegalArgumentException("Remote message cannot be null"))
return
}

// Generate destination string, see docs on receiver method for more details
val destination =
message.getString("to")?.takeIfNotBlank() ?: UUID.randomUUID().toString()
val isNotificationHandled = CustomerIOFirebaseMessagingService.onMessageReceived(
context = reactContext,
remoteMessage = message.toFCMRemoteMessage(destination = destination),
handleNotificationTrigger = handleNotificationTrigger,
)
promise.resolve(isNotificationHandled)
} catch (ex: Throwable) {
logger.error("Unable to handle push notification, reason: ${ex.message}")
promise.reject(ex)
}
}

/**
* Checks current permission of push notification permission
*/
Expand Down
65 changes: 65 additions & 0 deletions src/CustomerIOPushMessaging.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { NativeModules, Platform } from 'react-native';

const LINKING_ERROR =
`The package 'customerio-reactnative' doesn't seem to be linked. Make sure: \n\n` +
Platform.select({ ios: "- You have run 'pod install'\n", default: '' }) +
'- You rebuilt the app after installing the package\n' +
'- You are not using Expo managed workflow\n';

/**
* Get CustomerIOPushMessaging native module
*/
const PushMessagingNative = NativeModules.CustomerioPushMessaging
? NativeModules.CustomerioPushMessaging
: new Proxy(
{},
{
get() {
throw new Error(LINKING_ERROR);
},
}
);

class CustomerIOPushMessaging {
/**
* Processes push notification received outside the CIO SDK. The method displays notification on
* device and tracks CIO metrics for push notification.
*
* @param message push payload received from FCM. The payload must contain data payload received in push
* notification.
* @param handleNotificationTrigger indicates whether it should display the notification or not.
* true (default): The SDK will display the notification and track associated metrics.
* false: The SDK will only process the notification to track metrics but will not display any notification.
* @return promise that resolves to boolean indicating if the notification was handled by the SDK or not.
*/
onMessageReceived(
message: any,
handleNotificationTrigger: boolean = true
): Promise<boolean> {
if (Platform.OS === 'ios') {
// Since push notifications on iOS work fine with multiple notification services,
// We don't need to process them on iOS for now.
// Resolving promise to true makes it easier for callers to avoid adding
// unnecessary platform specific checks.
return Promise.resolve(true);
} else {
return PushMessagingNative.handleMessage(
message,
handleNotificationTrigger
);
}
}

/**
* Handles push notification received when app is background. Since FCM itself displays the notification
* when app is background, this method makes it easier to determine whether the notification should be
* displayed or not.
*
* @see [onMessageReceived] for more details
*/
onBackgroundMessageReceived(message: any): Promise<boolean> {
return this.onMessageReceived(message, !message.notification);
}
}

export { CustomerIOPushMessaging };
5 changes: 5 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 { CustomerIOPushMessaging } from './CustomerIOPushMessaging';
import type { PushPermissionStatus, PushPermissionOptions } from './types';
var pjson = require('customerio-reactnative/package.json');

Expand Down Expand Up @@ -136,6 +137,10 @@ class CustomerIO {
return new CustomerIOInAppMessaging();
}

static pushMessaging(): CustomerIOPushMessaging {
return new CustomerIOPushMessaging();
}

/**
* Register a device with respect to a profile.
* If no profile is identified, no device will be registered.
Expand Down

0 comments on commit 458472d

Please sign in to comment.