Cloud Messaging Integration in iOS and Android->
In mobile applications, sending and receiving notifications is a crucial feature for enhancing user engagement. React Native provides multiple tools and libraries to handle push notifications efficiently. In this blog, we’ll explore how to handle push notifications in a React Native app using Firebase Cloud Messaging (FCM) and notifee
for both foreground and background notifications, manage badge counts, and navigate based on user interactions.
Here’s the standardized JSON structure for sending push notifications to both Android and iOS using Firebase Cloud Messaging (FCM).
It ensures compatibility with both platforms by properly utilizing the data field for Android and the notification and apns fields for iOS.
Note: For android hide
notification
attribute as it leads to dual notification. ForiOS
,notification
anddata
(optional) both can be used.
{
"tokens": [
"f2LCHErZRBqgeyG8I_K5hg:APA91bHpSIOGdgBlqUup04kBf...",
"ekYNX6PEqUv6t950IFiDiS:APA91bFxq9_6K55wQX5r..."
],
"appName": "your-app-name",
"data": {
"title": "You've been mentioned in a post!",
"body": "Check the post for more details.",
"feedId": "66ff93b861f9f839e714d496",
"notificationId": "6715dab22af6594ff2cc2cac",
"badge": "17",
"commentId": ""
},
"notification": {
"title": "You've been mentioned in a post!",
"body": "Check the post for more details."
},
"apns": {
"payload": {
"aps": {
"sound": "default",
"badge": 17
}
}
}
}
This is sample consoles that I gathered from clicking notification in every scenario(just for ref.):
// iOS FOREGROUND CLICK:
LOG onForegroundEventCLICKFG✨ ios {
"type": 1,
"detail": {
"pressAction": {
"id": "default"
},
"notification": {
"body": "this is a post for demo ajmal .A hasan\r\n#app #testing",
"data": {
"notificationId": "6715dab22af6594ff2cc2cac",
"feedId": "66ff93b861f9f839e714d496",
"badge": "17",
"title": "You've been mentioned in a post!",
"commentId": "",
"body": "this is a post for demo ajmal .A hasan\r\n#app #testing"
},
"title": "You've been mentioned in a post!",
"id": "X82DMjiHzxgPedVpRNEo",
"ios": {
"foregroundPresentationOptions": {
"sound": true,
"list": true,
"alert": true,
"badge": true,
"banner": true
},
"categoryId": "1729494940166589"
}
}
}
}
// iOS BACKGROUND CLICK:
LOG NotificationClicked_Linking {
"messageId": "1729494968462956",
"data": {
"badge": "17",
"title": "You've been mentioned in a post!",
"commentId": "",
"feedId": "66ff93b861f9f839e714d496",
"notificationId": "6715dab22af6594ff2cc2cac",
"body": "this is a post for demo ajmal .A hasan\r\n#app #testing"
},
"notification": {
"ios": {
"badge": 17
},
"title": "You've been mentioned in a post!",
"sound": "default",
"body": "this is a post for demo ajmal .A hasan\r\n#app #testing"
},
"from": "493029575220"
}
// iOS KILL MODE CLICK:
LOG NotificationClicked_Linking_Initial {
"messageId": "1729495095731738",
"data": {
"notificationId": "6715dab22af6594ff2cc2cac",
"feedId": "66ff93b861f9f839e714d496",
"badge": "17",
"title": "You've been mentioned in a post!",
"commentId": "",
"body": "this is a post for demo ajmal .A hasan\r\n#app #testing"
},
"notification": {
"ios": {
"badge": 17
},
"title": "You've been mentioned in a post!",
"sound": "default",
"body": "this is a post for demo ajmal .A hasan\r\n#app #testing"
},
"from": "493029575220"
}
// Android FOREGROUND CLICK:
LOG onForegroundEventCLICKFG✨ android {
"type": 1,
"detail": {
"pressAction": {
"launchActivity": "default",
"id": "default"
},
"notification": {
"title": "You've been mentioned in a post!",
"data": {
"title": "You've been mentioned in a post!",
"badge": "17",
"notificationId": "6715dab22af6594ff2cc2cac",
"feedId": "66ff93b861f9f839e714d496",
"body": "this is a post for demo ajmal .A hasan\r\n#app #testing",
"commentId": ""
},
"id": "coC7k8CGOcWtFqvdoNuJ",
"body": "this is a post for demo ajmal .A hasan\r\n#app #testing",
"android": {
"importance": 3,
"groupSummary": false,
"colorized": false,
"pressAction": {
"launchActivity": "default",
"id": "default"
},
"lightUpScreen": false,
"loopSound": false,
"visibility": 0,
"circularLargeIcon": false,
"asForegroundService": false,
"ongoing": false,
"showTimestamp": false,
"badgeIconType": 2,
"groupAlertBehavior": 0,
"onlyAlertOnce": true,
"showChronometer": false,
"channelId": "default",
"autoCancel": true,
"localOnly": false,
"defaults": [
-1
],
"chronometerDirection": "up",
"smallIcon": "ic_launcher"
}
}
}
}
// Android BACKGROUND CLICK:
LOG onForegroundEventCLICKBG✨ android {
"headless": true,
"detail": {
"pressAction": {
"launchActivity": "default",
"id": "default"
},
"notification": {
"title": "You've been mentioned in a post!",
"data": {
"title": "You've been mentioned in a post!",
"badge": "17",
"notificationId": "6715dab22af6594ff2cc2cac",
"feedId": "66ff93b861f9f839e714d496",
"body": "this is a post for demo ajmal .A hasan\r\n#app #testing",
"commentId": ""
},
"id": "5A2O5VvXKxUK6oXoiUsG",
"body": "this is a post for demo ajmal .A hasan\r\n#app #testing",
"android": {
"importance": 3,
"groupSummary": false,
"colorized": false,
"pressAction": {
"launchActivity": "default",
"id": "default"
},
"lightUpScreen": false,
"loopSound": false,
"visibility": 0,
"circularLargeIcon": false,
"asForegroundService": false,
"ongoing": false,
"showTimestamp": false,
"badgeIconType": 2,
"groupAlertBehavior": 0,
"onlyAlertOnce": true,
"showChronometer": false,
"channelId": "default",
"autoCancel": true,
"localOnly": false,
"defaults": [
-1
],
"chronometerDirection": "up",
"smallIcon": "ic_launcher"
}
}
},
"type": 1
}
// Android KILL MODE CLICK:
LOG onForegroundEventCLICKFG✨ android {
"type": 1,
"detail": {
"pressAction": {
"launchActivity": "default",
"id": "default"
},
"notification": {
"title": "You've been mentioned in a post!",
"data": {
"title": "You've been mentioned in a post!",
"badge": "17",
"notificationId": "6715dab22af6594ff2cc2cac",
"feedId": "66ff93b861f9f839e714d496",
"body": "this is a post for demo ajmal .A hasan\r\n#app #testing",
"commentId": ""
},
"id": "RHUcpFIT7I44eQEob3GH",
"body": "this is a post for demo ajmal .A hasan\r\n#app #testing",
"android": {
"importance": 3,
"groupSummary": false,
"colorized": false,
"pressAction": {
"launchActivity": "default",
"id": "default"
},
"lightUpScreen": false,
"loopSound": false,
"visibility": 0,
"circularLargeIcon": false,
"asForegroundService": false,
"ongoing": false,
"showTimestamp": false,
"badgeIconType": 2,
"groupAlertBehavior": 0,
"onlyAlertOnce": true,
"showChronometer": false,
"channelId": "default",
"autoCancel": true,
"localOnly": false,
"defaults": [
-1
],
"chronometerDirection": "up",
"smallIcon": "ic_launcher"
}
}
}
}
Project Setup
To start, we’ll be utilizing the following libraries:
-
@react-native-firebase/messaging
for handling Firebase push notifications. -
notifee
for displaying local notifications and managing badge counts.
Let’s break down the implementation, step by step.
1. Setting Up Notification Listeners in index.js
In your index.js
, you’ll set up listeners to handle incoming push notifications, both in the foreground and background. This is how your app will react when a notification is received.
import messaging from '@react-native-firebase/messaging';
import './src/helpers/notification/notificationListener'; // Import notification listener
import { addBadgeCount, displayNotification } from './src/helpers/notification/notificationInitial'; // Helper functions
import { MMKV } from 'react-native-mmkv' // For local storage
export const storage = new MMKV()
async function onMessageReceived(message) {
console.log('onMessageReceived_🙌' + Platform.OS, message);
// Extract notification details
const { title, body } = message.data?.title ? message.data : message.notification;
const categoryId = `${message?.messageId}`;
// Check if the notification has already been received
const categoryIdStored = storage.getString('categoryId');
if (categoryIdStored === categoryId) {
return;
}
storage.set('categoryId', `${categoryId}`); // Store the message ID
// Display the notification
await displayNotification(title, body, `${message?.messageId}`, message?.data);
// Manage badge count (for both Android and iOS)
const badgeCount = Platform.OS === 'ios' ? message.notification?.ios?.badge : message.data?.badge;
await addBadgeCount(badgeCount);
}
// Set up notification listeners
messaging().onMessage(onMessageReceived); // For foreground
messaging().setBackgroundMessageHandler(onMessageReceived); // For background
Key Highlights:
- onMessageReceived: This function is triggered when a push notification is received. It handles both displaying the notification and updating the badge count.
- Message Storage: To avoid displaying duplicate notifications, the message ID is stored and checked before showing the notification.
2. Handling Foreground and Background Events with Notifee
Notifee
is used to manage notifications within the app, especially when the app is in the foreground or background. We listen to notification press events and navigate the user to the appropriate screen.
import notifee, { EventType } from '@notifee/react-native';
import { Platform } from 'react-native';
import { NOTIFICATION_SCREEN } from '../../routes/navigationConstants'; // Screen to navigate to
import { navigate } from '../NavigationService'; // Navigation helper
import { getDataNotificationData } from '../helper'; // Helper function
// Foreground event handler
notifee.onForegroundEvent((message) => {
switch (message?.type) {
case EventType.PRESS:
if (navigate) {
console.log('onForegroundEventCLICKFG✨', Platform.OS, getDataNotificationData(message));
navigate(NOTIFICATION_SCREEN, {}); // Navigate to the notification screen
}
break;
default:
break;
}
});
// Background event handler
notifee.onBackgroundEvent(async (message) => {
if (message?.type == EventType.PRESS) {
if (navigate) {
console.log('onForegroundEventCLICKBG✨', Platform.OS, getDataNotificationData(message));
navigate(NOTIFICATION_SCREEN); // Navigate to the notification screen
}
}
});
Key Highlights:
-
Foreground and Background Handling:
notifee
handles events when a user clicks on the notification. Depending on the state of the app (foreground/background), it navigates to the appropriate screen.
3. Helper Function to Extract Notification Data
The getDataNotificationData
function helps us extract the necessary data from a notification event. It’s crucial for ensuring the notification payload is parsed correctly, whether it's from Android or iOS.
const getDataNotificationData = (event) => {
if (event?.type && event?.detail?.notification?.data) {
return event.detail.notification.data;
}
if (event?.messageId && event?.data) {
return event.data;
}
if (event?.detail?.notification?.data) {
return event.detail.notification.data;
}
return null;
};
Key Highlights:
- Cross-Platform Data Extraction: This function works for both Android and iOS, ensuring the correct notification data is extracted no matter how the event is triggered.
4. Displaying Notifications and Managing Badge Counts
In notificationInitial.js
, we define functions for displaying notifications and managing badge counts using notifee
.
import notifee from '@notifee/react-native';
// will not work in kill/background mode, for them from remote server we have to send badge
export const addBadgeCount = async (count = 1) => {
notifee.setBadgeCount(count).then(() => console.log('Badge Count Updated'));
};
export const removeBadgeCount = async (count = 0) => {
notifee.setBadgeCount(count).then(() => console.log('Badge count removed!'));
};
export const displayNotification = async (title, body, categoryId, data) => {
const channelId = await notifee.createChannel({
id: 'default',
name: 'Default Channel',
});
await notifee.displayNotification({
title,
body,
data,
android: {
channelId,
onlyAlertOnce: true,
smallIcon: 'ic_stat_name', // Ensure the icon exists in your project
pressAction: { id: 'default' },
},
ios: {
categoryId,
foregroundPresentationOptions: {
badge: true,
sound: true,
banner: true,
list: true,
},
},
});
};
Key Highlights:
-
Badge Management: The badge count is updated using
notifee.setBadgeCount()
, ensuring the correct badge is displayed. It will not work in kill/background mode, for them from remote server we have to send badge. -
Notification Display:
notifee.displayNotification()
handles the display of foreground notifications on both Android and iOS.
5. Permission and Token:
In notificationPermission.js
:
import notifee from '@notifee/react-native';
import { Alert } from 'react-native';
import messaging from '@react-native-firebase/messaging';
import Config from 'react-native-config';
import { StorageMMKV } from '../MMKVStorage';
import { KEY_APP_TOKEN } from '../Constants';
export const requestPermission = async () => {
await notifee.requestPermission();
await notifee.setBadgeCount(0);
return getFCMToken();
};
const subscribeToTopic = async () => {
await messaging().subscribeToTopic(Config.topicFCM);
};
const unsubscribeFromTopic = async () => {
await messaging().unsubscribeFromTopic(Config.topicFCM);
};
export const getFCMToken = async () => {
const fcmToken = await StorageMMKV.getUserPreferences(KEY_APP_TOKEN);
console.log('TOKEN>>>', fcmToken);
try {
if (!fcmToken) {
const token = await messaging().getToken();
StorageMMKV.setUserPreferences(KEY_APP_TOKEN, token);
subscribeToTopic();
console.log('TOKEN>>>>IF', token);
return token;
}
console.log('TOKEN>>>>ELSE', fcmToken);
return fcmToken;
} catch ({ message }) {
console.log('getFCMToken', message);
return fcmToken;
}
};
export const powerManagerCheck = async () => {
const powerManagerInfo = await notifee.getPowerManagerInfo();
if (powerManagerInfo.activity) {
Alert.alert(
'Restrictions Detected',
'To ensure notifications are delivered, please adjust your settings to prevent the app from being killed',
[
{
text: 'OK, open settings',
onPress: async () => await notifee.openPowerManagerSettings(),
},
{
text: 'Cancel',
onPress: () => console.log('Cancel Pressed'),
style: 'cancel',
},
],
{ cancelable: false }
);
}
};
export const batteryOptimizationCheck = async () => {
const batteryOptimizationEnabled = await notifee.isBatteryOptimizationEnabled();
if (batteryOptimizationEnabled) {
Alert.alert(
'Restrictions Detected',
'To ensure notifications are delivered, please disable battery optimization for the app.',
[
{
text: 'OK, open settings',
onPress: async () => await notifee.openBatteryOptimizationSettings(),
},
{
text: 'Cancel',
onPress: () => console.log('Cancel Pressed'),
style: 'cancel',
},
],
{ cancelable: false }
);
}
};
Key Highlights:
- Request Permissions: Requests notification access and resets badge count.
- FCM Token: Retrieves, stores, and subscribes to FCM token.
- Topic Subscription: Manages notification topic subscription.
- Power Management: Detects restrictions, prompts settings adjustment.
- Battery Optimization: Alerts users to disable battery optimization for notifications.
- Settings Alerts: Guides users to adjust power/battery settings.
6. Handling Initial Notifications in Routes
When the app is opened from a killed/ background state, notifications can be handled using like below.
routes.jsx
useEffect(() => {
permissionChecksNotification()
}, []);
const permissionChecksNotification = async () => {
const deviceToken = await requestPermission();
// dispatch(saveFCMToken(deviceToken));
// await removeBadgeCount();
// getInitialNotification: When the application is opened from killed mode this is called.
try {
const message = await messaging().getInitialNotification();
if (message && message?.messageId) {
console.log(
'NotificationClicked_Linking_Initial',
JSON.stringify(message, null, 2),
'👉',
getDataNotificationData(message)
);
navigate(NOTIFICATION_SCREEN);
}
} catch (error) {
console.log('Error getting initial notification:', error);
}
// use for battery optimisation popup alert
// if (isAndroid()) {
// batteryOptimizationCheck();
// powerManagerCheck();
// }
};
// onNotificationOpenedApp: When the application is running, but in the background.
const linking = {
subscribe(listener) {
const unsubscribe = messaging().onNotificationOpenedApp((remoteMessage) => {
console.log('NotificationClicked_Linking', getDataNotificationData(remoteMessage));
if (remoteMessage && remoteMessage?.messageId) {
navigate(NOTIFICATION_SCREEN);
}
});
return () => {
unsubscribe();
};
},
};
return(
<NavigationContainer
linking={linking}>
...
</NavigationContainer>)
Key Highlights:
-
Handling Notification Clicks: Notifications clicked from the background or killed state are handled with
onNotificationOpenedApp
andgetInitialNotification
, ensuring users are taken to the correct screen.
Conclusion
Managing notifications in a React Native app requires careful handling of both foreground and background states, ensuring users receive important messages regardless of the app’s state. With Firebase Cloud Messaging and Notifee, you can effectively handle notifications, manage badge counts, and navigate users based on their interactions with notifications.
This blog provided a comprehensive guide to implementing notifications in React Native, including displaying notifications, managing badge counts, and handling notification events on both iOS and Android.
By following this approach, you can ensure your app's notification system is robust, user-friendly, and efficient across platforms.