Forward third-party Push Notifications to React Native Firebase Messaging
Sometimes your React Native app has multiple sources for push notifications. In this article I will explain how to forward push notifications received on the native side from a third-party like Klaviyo to React Native Firebase Messaging library. This is really useful because you can re-use one set of callbacks on the JS side for multiple native “clients”, in this case Firebase Cloud Messaging and Klaviyo.
iOS
Include these imports in your *-Bridging-Header.h
so we can use them in Swift. If you don’t have a bridging header in place, please follow this article.
#import <RNFBApp/RNFBRCTEventEmitter.h>
#import "RNFBMessagingSerializer.h"
Now it’s time to implement UNUserNotificationCenterDelegate
in your AppDelegate.swift
file.
import RNFBMessaging
import KlaviyoSwift
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {
var window: UIWindow?
var bridge: RCTBridge!
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
FirebaseApp.configure()
UNUserNotificationCenter.current().delegate = self
//[...]
}
}
Let’s implement some methods from that protocol. This one can be useful if you want to display push notifications while your app is in foreground.
extension AppDelegate {
func userNotificationCenter(
_ center: UNUserNotificationCenter,
willPresent notification: UNNotification
) async -> UNNotificationPresentationOptions {
if #available(iOS 14.0, *) {
return [.banner]
} else {
return [.alert]
}
}
In the next method, we will receive the notification object, then we will convert it to a dictionary using some functions from RNFBMessaging
. Once the dictionary data is ready we will use the sendEvent()
function and send it to the JS realm. This event will trigger the callback passed to messaging().onNotificationOpenedApp(callback)
.
The last part is also handling the case when the app is opened from scratch, and on the JS side messaging().getInitialNotification()
is expected to return the push notification.
func userNotificationCenter(
_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse
) async {
// This will check if the notification is from Klaviyo
let handled = KlaviyoSDK().handle(notificationResponse: response) { }
if(!handled) {
return
}
// https://github.com/invertase/react-native-firebase/blob/9b5c405b2933c84daad561117a3eebacc65cbb7e/packages/messaging/ios/RNFBMessaging/RNFBMessaging%2BUNUserNotificationCenter.m#L151
let userInfo = response.notification.request.content.userInfo
let dict = RNFBMessagingSerializer.notification(toDict: response.notification)
RNFBRCTEventEmitter.shared().sendEvent(withName: "messaging_notification_opened", body: dict)
// Making sure the getInitialNotification() func will return something on app cold start.
// https://github.com/invertase/react-native-firebase/blob/9b5c405b2933c84daad561117a3eebacc65cbb7e/packages/messaging/lib/modular/index.d.ts#L104
RNFBMessagingUNUserNotificationCenter.sharedInstance().initialNotification = dict
}
Android
On Android we need to patch the library in order to expose a private field. Here is a @react-native-firebase+messaging+20.4.0.patch
file that is only adding the public
keyword for ReadableMap initialNotification
from ReactNativeFirebaseMessagingModule.java
. More details on how to patch a library can be found here.
diff --git a/node_modules/@react-native-firebase/messaging/android/src/main/java/io/invertase/firebase/messaging/ReactNativeFirebaseMessagingModule.java b/node_modules/@react-native-firebase/messaging/android/src/main/java/io/invertase/firebase/messaging/ReactNativeFirebaseMessagingModule.java
index 3b89817..e5d4c64 100644
--- a/node_modules/@react-native-firebase/messaging/android/src/main/java/io/invertase/firebase/messaging/ReactNativeFirebaseMessagingModule.java
+++ b/node_modules/@react-native-firebase/messaging/android/src/main/java/io/invertase/firebase/messaging/ReactNativeFirebaseMessagingModule.java
@@ -40,7 +40,7 @@ import java.util.Map;
public class ReactNativeFirebaseMessagingModule extends ReactNativeFirebaseModule
implements ActivityEventListener {
private static final String TAG = "Messaging";
- ReadableMap initialNotification = null;
+ public ReadableMap initialNotification = null;
private HashMap<String, Boolean> initialNotificationMap = new HashMap<>();
ReactNativeFirebaseMessagingModule(ReactApplicationContext reactContext) {
In this case, Klaviyo already has a service to create and display the push notification on Android, if your push provider doesn’t offer this, feel free to check out their implementation. Don’t forget to also register that service in your AndroidManifest.xml
.
The following must be added inside <application> [...] </application>
.
<service android:name="com.klaviyo.pushFcm.KlaviyoPushService" android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
To make use of that patch, when the app is started, in MainActivity.kt
we check for a notification bundle in the intent. If we find something, we are sending the bundle to a helper function to convert it to a WritableMap
. This dictionary is set to initialNotification
, this field is part of the already running instance of a native module called ReactNativeFirebaseMessagingModule
. On the JS side messaging().getInitialNotification()
will return this push notification.
import com.facebook.react.bridge.WritableNativeMap
import com.klaviyo.analytics.Klaviyo
import com.klaviyo.analytics.Klaviyo.isKlaviyoIntent
import io.invertase.firebase.common.ReactNativeFirebaseEventEmitter
import io.invertase.firebase.messaging.ReactNativeFirebaseMessagingModule
// [...]
override fun onCreate(savedInstanceState: Bundle?) {
// https://github.com/software-mansion/react-native-screens/issues/17#issuecomment-424704067
super.onCreate(null)
if(intent.isKlaviyoIntent) {
// Only for Analytics
Klaviyo.handlePush(intent)
// Cold app start after tapping on a push notification from Klaviyo
val remoteMessageMap =
ReactNativeFirebaseMessagingSerializer.klaviyoExtrasToWritableMap(intent.extras)
val newInitialNotification = WritableNativeMap()
newInitialNotification.merge(remoteMessageMap!!)
val modules = this.reactInstanceManager.currentReactContext?.nativeModules
val messagingModule = modules?.first { it.name == "RNFBMessagingModule" } as ReactNativeFirebaseMessagingModule
messagingModule.initialNotification = newInitialNotification
}
}
If the app is already running in background or in foreground, the onNewIntent()
method will be called on notification tap. Here we will convert again the bundle extras to a dictionary and send it as an event. On the JS side, this event will trigger the callback passed inside messaging().onNotificationOpenedApp(callback)
.
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
if (intent?.isKlaviyoIntent != true) {
return
}
// Only for Analytics
Klaviyo.handlePush(intent)
val remoteMessageMap =
ReactNativeFirebaseMessagingSerializer.klaviyoExtrasToWritableMap(intent.extras)
val emitter = ReactNativeFirebaseEventEmitter.getSharedInstance()
emitter.sendEvent(
io.invertase.firebase.messaging.ReactNativeFirebaseMessagingSerializer.remoteMessageMapToEvent(
remoteMessageMap, true
)
)
}
That was it
Now your push notifications from third-party providers will show up on the JS side when using @react-native-firebase/messaging
.
import messaging from '@react-native-firebase/messaging';
const unsubscribe = messaging().onNotificationOpenedApp(callback);
const notification = await messaging().getInitialNotification();
Feel free to write your question in the comments section bellow and let’s connect on LinkedIn.
Comments