Part 2: Push Notifications in React Native 2024

Varun Kukade
14 min readSep 29, 2024

--

In this article series, we are completely building a push notification service on the front end using Firebase. I have divided the explanation into two articles. This is the second part.

Here’s is first part: https://medium.com/@varunkukade999/part-1-push-notifications-in-react-native-2024-db5f7200288f

Here are the steps covered in this article:

Step 8: Get notifications on the foreground state
Step 9: Notifee installation, create a notification channel on Android
Step 10: Show Local notifications on the foreground state using notifee
Step 11:
Handle Local notification click on foreground state using notifee
Step 12:
Get notifications on the app background state and quit state
Step 13: Handle notifications click on the app background state and quit the state
Step 14: Breaking change on IOS when using Firebase + notifee
Step 15: Handle badge count on IOS
Step 16: Using react-native-notifications
Step 17: Testing notifications.

Foreground state:

As we discussed when a notification is received if the app is in a foreground state and its payload contains a notification object, the notification won’t be displayed. If you want to display this notification, you need to get a payload and display it by yourself. In this case, firebase can’t help us. Here type of notification that we are talking about is called “Local Notification”. Local notification is displayed by the app.

Here’s how you can get the notification payload once you receive it in the foreground.

https://rnfirebase.io/messaging/usage#foreground-state-messages: Register onMessage handler and you will be able to access the notification payload in the passed callback.

notificationsHelper.ts

import React, { useEffect } from 'react';
import { Alert } from 'react-native';
import messaging from '@react-native-firebase/messaging';

export const setNotificationsHandler = async () => {
let granted = await checkNotificationPermissionStatus();
if(!granted) return;
await messaging().registerDeviceForRemoteMessages();
const token = await messaging().getToken();
await passTokenToBackend(token);

messaging().onTokenRefresh((token) => {
//call api and pass the token
passTokenToBackend(token)
});

messaging().onMessage(async remoteMessage => {
Alert.alert('A new FCM message arrived in foreground!', JSON.stringify(remoteMessage));
});
}

In the callback, you can do any tasks like updating the UI, async tasks, etc.

Show Local notification on foreground state using notifee:

Notifee: https://notifee.app/react-native/docs/overview.
Notifee helps the app to display the notification in the notification drawer. Right now latest version as of Sept 2024 is 9.0.0. Although to be honest doc is not up to date as per the latest version. I think it’s updated till 7.0.1. But apart from one breaking change (which was introduced in 7.0.0 and which we will cover later in the article) which is related to Firebase, right now I don’t think things will break. I have tested 9.0.0 and it’s working fine. I researched various libraries and decided to go with notifee because it provides lot of features for local notification whereas no other library supports this much features right now. In addition to that other libraries are somewhat old and have not been updated for years.

Installation:
Go ahead and install the library https://notifee.app/react-native/docs/installation
Check environmental support also and check if it’s good for your app versions. https://notifee.app/react-native/docs/environment-support

Creating a channel:
For Android, notify requires us to create a notification channel before displaying the notification. Starting in Android 8.0 (API level 26), all notifications must be assigned to a channel or they don’t appear.

This is how channels like like:

In the categories section, as you can see “firing alarms & timers”, “Missed Alarms”, and “Stopwatch” are all channels. This lets users disable specific notification channels for your app instead of disabling all your notifications. Users can control the visual and auditory options for each channel from the Android system settings,

On devices running Android 7.1 (API level 25) and lower, users can manage notifications on a per-app basis only. Each app effectively has only one channel on Android 7.1 and lower.

An app can have separate channels for each type of notification the app issues.

Now check out this section https://notifee.app/react-native/docs/displaying-a-notification#notification-title--body. Notifee provides createChannel function to create a channel. You can use the following helper code to create a channel if not been created already.

notificationsHelper.ts

import React, { useEffect } from 'react';
import { Alert } from 'react-native';
import messaging from '@react-native-firebase/messaging';
import notifee from '@notifee/react-native';

export const channelId = 'missedAlarm';
export const channelName = 'Missed Alarms';

export const setNotificationsHandler = async () => {
let granted = await checkNotificationPermissionStatus();
if(!granted) return;
await messaging().registerDeviceForRemoteMessages();
const token = await messaging().getToken();
await passTokenToBackend(token);

messaging().onTokenRefresh((token) => {
//call api and pass the token
passTokenToBackend(token)
});

notifee.isChannelCreated(channelId).then(isCreated => {
if (!isCreated) {
notifee.createChannel({
id: channelId,
name: channelName,
sound: 'default',
});
}
});

messaging().onMessage(async remoteMessage => {
Alert.alert('A new FCM message arrived in foreground!', JSON.stringify(remoteMessage));
});
}

On the iOS platform, the call createChannel resolves instantly & gracefully (iOS has no concept of a channel)

Now once foreground notification is received, you can display the notification using the notification payload.

notificationsHelper.ts

import React, { useEffect } from 'react';
import { Alert } from 'react-native';
import messaging from '@react-native-firebase/messaging';
import notifee, {
AndroidImportance
} from '@notifee/react-native';

export const channelId = 'missedAlarm';
export const channelName = 'Missed Alarms';

const showForegroundNotification = async (message: any) => {
if (!message || !message?.notification) {
return;
}
const { title, body } = message.notification;
notifee.displayNotification({
title,
body,
android: {
channelId: channelId, //pass the same channel id for which channel is created
importance: AndroidImportance.HIGH,
pressAction: {
id: 'default',
},
},
});
};

export const setNotificationsHandler = async () => {
let granted = await checkNotificationPermissionStatus();
if (!granted) {
return;
}
await messaging().registerDeviceForRemoteMessages();
const token = await messaging().getToken();
await passTokenToBackend(token);

messaging().onTokenRefresh(token => {
//call api and pass the token
passTokenToBackend(token);
});

notifee.isChannelCreated(channelId).then(isCreated => {
if (!isCreated) {
notifee.createChannel({
id: channelId,
name: channelName,
sound: 'default',
});
}
});

messaging().onMessage(showForegroundNotification);
};

This way notifications will be displayed even in the foreground.

Handle Local notification click on foreground state using notifee:

By default, if you click a notification in the app foreground state, nothing happens on the app UI. But notification goes away from the notification drawer.

In case you want to handle a notification tap and navigate the user to a certain screen in the app UI/ perform some async task, etc you can use Notifee’s onForegroundEvent.

Let’s say the backend sent you some type in the notification data, you can pass the same type to your displayed notification. And you can utilize onForegroundEvent and listen to the tap events. Whenever you see the tap event check the type passed with the notification, and based on the type, you can do redirection to a specific screen.

notificationsHelper.ts

import React, { useEffect } from 'react';
import { Alert } from 'react-native';
import messaging from '@react-native-firebase/messaging';
import notifee, { AndroidImportance, EventType } from '@notifee/react-native';

export const channelId = 'missedAlarm';
export const channelName = 'Missed Alarms';

const showForegroundNotification = async (message: any) => {
if (!message || !message?.notification) {
return;
}
const { title, body } = message.notification;
const { type } = message?.data;

let obj = {
title,
body,
android: {
channelId: channelId, //pass the same channel id for which channel is created
importance: AndroidImportance.HIGH,
pressAction: {
id: 'default',
},
},
};
if (type) {
//if type exist, pass it to the notification object
obj.data = { type };
}
notifee.displayNotification(obj);
};

export const setNotificationsHandler = async () => {
let granted = await checkNotificationPermissionStatus();
if (!granted) {
return;
}
await messaging().registerDeviceForRemoteMessages();
const token = await messaging().getToken();
await passTokenToBackend(token);

messaging().onTokenRefresh(token => {
//call api and pass the token
passTokenToBackend(token);
});

notifee.isChannelCreated(channelId).then(isCreated => {
if (!isCreated) {
notifee.createChannel({
id: channelId,
name: channelName,
sound: 'default',
});
}
});

notifee.onForegroundEvent(({ type, detail }) => {
switch (type) {
case EventType.DISMISSED: {
//User dismiss notification that received in foreground
console.log('Notification dismissed');
break;
}
case EventType.PRESS: {
console.log('Notification pressed');
const { type } = detail?.notification?.data;
switch (type) {
case 'missedAlarams':
//navigate user to any screen on UI
break;
default:
break;
}
break;
}
}
});

messaging().onMessage(showForegroundNotification);
};

In onForegroundEvent, you get type and detail.

detail includes the notification object that you passed while displaying notification.

Possible values of type are

export declare enum EventType {
/**
* An unknown event was received.
*
* This event type is a failsafe to catch any unknown events from the device. Please
* report an issue with a reproduction so it can be correctly handled.
*/
UNKNOWN = -1,
/**
* Event type is sent when the user dismisses a notification. This is triggered via the user swiping
* the notification from the notification shade or performing "Clear all" notifications.
*
* This event is **not** sent when a notification is cancelled or times out.
*
* @platform android Android
*/
DISMISSED = 0,
/**
* Event type is sent when a notification has been pressed by the user.
*
* On Android, notifications must include an `android.pressAction` property for this event to trigger.
*
* On iOS, this event is always sent when the user presses a notification.
*/
PRESS = 1,
/**
* Event type is sent when a user presses a notification action.
*/
ACTION_PRESS = 2,
/**
* Event type sent when a notification has been delivered to the device. For trigger notifications,
* this event is sent at the point when the trigger executes, not when a the trigger notification is created.
*
* It's important to note even though a notification has been delivered, it may not be shown to the
* user. For example, they may have notifications disabled on the device/channel/app.
*/
DELIVERED = 3,
/**
* Event is sent when the user changes the notification blocked state for the entire application or
* when the user opens the application settings.
*
* @platform android API Level >= 28
*/
APP_BLOCKED = 4,
/**
* Event type is sent when the user changes the notification blocked state for a channel in the application.
*
* @platform android API Level >= 28
*/
CHANNEL_BLOCKED = 5,
/**
* Event type is sent when the user changes the notification blocked state for a channel group in the application.
*
* @platform android API Level >= 28
*/
CHANNEL_GROUP_BLOCKED = 6,
/**
* Event type is sent when a notification trigger is created.
*/
TRIGGER_NOTIFICATION_CREATED = 7
}

Now, in case you are wondering how to navigate to a particular screen from onForegroundEvent, that’s a valid question. Generally navigation object is passed through navigation screens and also you can access it through the useNavigation hook. But setNotificationsHandler is placed outside of the react component. Then how can we access the navigation object inside it?
The answer is through navigation ref.

Check this out: https://reactnavigation.org/docs/navigating-without-navigation-prop/

The following code fetches the navigation ref and defines the navigate function which navigates to some screen when the navigation ref is ready. Navigation ref is considered to be ready when the navigation container and all its children are mounted.

Also, pass this ref to NavigationContainer.

AppNavigation.ts

import { createNavigationContainerRef } from '@react-navigation/native';
import { NavigationContainer } from '@react-navigation/native';

export const navigationRef = createNavigationContainerRef();

export function navigate(name, params) {
if (navigationRef.isReady()) {
navigationRef.navigate(name, params);
}
}

export default function AppNavigation() {
return (
<NavigationContainer ref={navigationRef}>{/* ... */}</NavigationContainer>
);
}

In our case, we needed to navigate the user to some screen when the user clicked on a notification when the app was in the foreground state. In the foreground state, the navigation container and its children are already mounted. Hence it's safe now to directly use the navigate function as follows:

import {navigate} from './AppNavigation'

notifee.onForegroundEvent(({ type, detail }) => {
switch (type) {
case EventType.DISMISSED: {
//User dismiss notification that received in foreground
console.log('Notification dismissed');
break;
}
case EventType.PRESS: {
console.log('Notification pressed');
const { type } = detail?.notification?.data;
switch (type) {
case 'missedAlarams':
navigate("MissedAlarms")
break;
default:
break;
}
break;
}
}
});

Background state and Quit state:

After handling the foreground state, we will handle the app background and quit state events and notification tap.

To handle notification messages when notification is displayed to the user in case of app background or quit, we need to add a background message handler outside of the react context.
Check this: https://rnfirebase.io/messaging/usage#background--quit-state-messages

To set a background handler, call the setBackgroundMessageHandler outside of your application logic as early as possible:

// index.js
import { AppRegistry } from 'react-native';
import messaging from '@react-native-firebase/messaging';
import App from './App';

// Register background handler
messaging().setBackgroundMessageHandler(async remoteMessage => {
console.log('Message handled in the background!', remoteMessage);
});

AppRegistry.registerComponent('app', () => App);

Mistake from my side: I mistakenly wrote the above handler as follows as I didn’t have any task to do here and background/quit notifications suddenly stopped.

messaging().setBackgroundMessageHandler();

Note that even if you don’t have any task to do here, don’t skip this handler, and also don’t forget to add the async function as a callback as it expects the promise. If you remove this function then background/quit notifications may not come.

This handler must not attempt to update any UI (e.g. via state) — you can however perform network requests, update local storage etc.

Handle the click of notification in case of app background/quit state:

By default, if you click a notification in the app background and quit state, the application is launched. Also, a notification goes away from the notification drawer.

Firebase provides us with the onNotificationOpenedApp ( background state notification message handler) and getInitialNotification (quit state notification message handler) methods to handle the messages.

notificationsHelper.ts

import React, { useEffect } from 'react';
import { Alert } from 'react-native';
import messaging from '@react-native-firebase/messaging';
import notifee, { AndroidImportance, EventType } from '@notifee/react-native';
import {navigate} from './AppNavigation'

export const channelId = 'missedAlarm';
export const channelName = 'Missed Alarms';

const showForegroundNotification = async (message: any) => {
if (!message || !message?.notification) {
return;
}
const { title, body } = message.notification;
const { type } = message?.data;

let obj = {
title,
body,
android: {
channelId: channelId, //pass the same channel id for which channel is created
importance: AndroidImportance.HIGH,
pressAction: {
id: 'default',
},
},
};
if (type) {
//if type exist, pass it to the notification object
obj.data = { type };
}
notifee.displayNotification(obj);
};

export const setNotificationsHandler = async () => {
let granted = await checkNotificationPermissionStatus();
if (!granted) {
return;
}
await messaging().registerDeviceForRemoteMessages();
const token = await messaging().getToken();
await passTokenToBackend(token);

messaging().onTokenRefresh(token => {
//call api and pass the token
passTokenToBackend(token);
});

notifee.isChannelCreated(channelId).then(isCreated => {
if (!isCreated) {
notifee.createChannel({
id: channelId,
name: channelName,
sound: 'default',
});
}
});

notifee.onForegroundEvent(({ type, detail }) => {
switch (type) {
case EventType.DISMISSED: {
//User dismiss notification that received in foreground
console.log('Notification dismissed');
break;
}
case EventType.PRESS: {
console.log('Notification pressed');
const { type } = detail?.notification?.data;
switch (type) {
case 'missedAlarams':
navigate("MissedAlarms")
break;
default:
break;
}
break;
}
}
});

messaging().onMessage(showForegroundNotification);
messaging().onNotificationOpenedApp(remoteMessage => {
console.log('Background state: Notification tapped:');
if (remoteMessage?.notification?.data) {
const { type } = remoteMessage?.notification?.data;
switch (type) {
case 'missedAlarms':
navigate("MissedAlarms")
break;
default:
break;
}
}
});
};

In the setNotificationsHandler function, I added an onNotificationOpenedApp handler, which will be invoked when app notification is tapped in case of app background state.

In the case of the app background state also, the NavigationContainer and all its children are already mounted. Hence it's safe to call the navigate function which relies on navigationRef.

Mistake with getInitialNotification : For app quit state notification tap handling, I tried to add getInitialNotification just below the onNotificationOpenedApp handler inside the setNotificationsHandler function. But this event handler was never called when I tapped the notification. Then I added it to the home screen code of the app and it worked successfully. The home screen is the first screen I show when the app is launched. Hence you need to note that if adding it outside of the react component/context doesn't work for you, try to add it inside the first screen in the navigation hierarchy. The first screen should be the screen that will be displayed the first time the app is launched from the notification tap.

homescreen.tsx

const navigation = useNavigation();

useEffect(() => {
//Killed state: Notification tapped
messaging()
.getInitialNotification()
.then(async remoteMessage => {
if (!remoteMessage) {
return;
}
const { type } = remoteMessage?.data;
switch (type) {
case 'missedAlarms':
navigation.navigate('MissedAlarmScreen');
break;
default:
break;
}
});
}, []);

Breaking change:

This is only applicable to you if
1. You are using the Notifee package along with with Firebase messaging package.
2. You are using the Notifee package with version > 7.0.0

In case you are in the above category, you need to know that the above 2 handlers that you added getInitialNotification and onNotificationOpenedApp will only work for Android and not IOS. As soon as Notifee released version 7.0.0 these handlers were depreciated for IOS.
Check this out: https://github.com/invertase/notifee/blob/main/docs-react-native/react-native/docs/release-notes.md#700

But you don’t need to worry about IOS in this case. The existing event that we already added in this article notifee.onForegroundEvent will be handling notification tap in case of IOS background and quit state. Names are confusing. Correct? But that’s what I found while searching for the answer.
Check this out:
https://github.com/invertase/notifee/issues/616#issuecomment-1380264271
https://notifee.app/react-native/docs/events#app-open-events

getInitialNotification is deprecated on iOS in favour of the PRESS event received by the onForegroundEvent event handler

Handle badge count on IOS:

Use the notifee badge count functionality to show badge count. https://notifee.app/react-native/docs/ios/badges

You don’t need to show the badge count if the app is in the foreground.

When the app is in the background and you receive the notification, you can increment the badge count inside messaging().setBackgroundMessageHandler function in index.js file.

messaging().setBackgroundMessageHandler(async remoteMessage => {
console.log('Message handled in the background!', remoteMessage);
// Increment the count by 1
await notifee.incrementBadgeCount();
});

When the app is launched and mounted, you can safely make the badge count to 0.

app.tsx

useEffect(() => {
notifee.setBadgeCount(0).then(() => console.log('Badge count removed'));
}, []);

As per the breaking change, when notification is tapped/dismissed for the IOS app killed and background state notifee.onForegroundEvent will be called. In this case, we can decrease the badge count by 1.

notifee.onForegroundEvent(({ type, detail }) => {
switch (type) {
case EventType.DISMISSED: {
//User dismiss notification that received in foreground
console.log('Notification dismissed');
// Decrement the count by 1
await notifee.decrementBadgeCount();
break;
}
case EventType.PRESS: {
console.log('Notification pressed');
// Decrement the count by 1
await notifee.decrementBadgeCount();
const { type } = detail?.notification?.data;
switch (type) {
case 'missedAlarms':
navigate("MissedAlarms")
break;
default:
break;
}
break;
}
}
});

Using react-native-notifications:

Mostly firebase + notifee will work for all scenarios of notification. But somehow if you see issues with the Firebase Module for displaying notifications/tapping them, you can make use of react-native-notifications. I have tested this completely and this is working fine for all cases foreground, background, and quit state.

Note: This library has the registerRemoteNotificationsRegistered method which gives us the FCM token on Android and the APN token on IOS. Check compatibility with backend what service they are using for IOS. If backend is using APN then you can go ahead and use registerRemoteNotificationsRegistered to get tokens and pass them to the backend.
If the backend only uses FCM for IOS also, you need to only use the FCM token. Just for token purposes, you can try using the messaging().getToken() function from Firebase to get FCM token and pass to backend. You can continue using react-native-notifications functions for showing and handling tap on notifications. Check this https://github.com/wix/react-native-notifications/issues/1036

For showing local notifications, you can also try integrating notifee with react-native-notifications. I haven’t tried this yet. May or may not work. Need to check.

Set badge count: You can use https://wix.github.io/react-native-notifications/docs/advanced-ios#get-and-set-application-icon-badges-count-ios-only

Testing

I found this while testing:
Android: Notifications work on Android debug and release builds (both simulator and real device).
IOS: Notifications only work on release builds on real devices.

To test if a notification is coming or not, you can try a Postman API call. I have found a wonderful blog that explains how to do that.
Check it out: https://apoorv487.medium.com/testing-fcm-push-notification-http-v1-through-oauth-2-0-playground-postman-terminal-part-2-7d7a6a0e2fa0

That’s it for this article. I hope you are now clear on Notifications in react native. Let me know if you have any doubts in the comments. If you face any specific problem, let everyone know in the comments.

If you found this tutorial helpful, don’t forget to give this post 50 claps👏 and follow 🚀 if you enjoyed this post and want to see more. Your enthusiasm and support fuel my passion for sharing knowledge in the tech community.

I create various tech blogs in my leisure time. You can find more of such articles on my profile -> https://medium.com/@varunkukade999

--

--

Varun Kukade

React Native Engineer 🚀 Transitioning into a Full-Fledged Mobile Engineer (Hybrid & Native) 📱💡 https://github.com/varunkukade varunkukade999@gmail.com