DEV Community

Ajmal Hasan
Ajmal Hasan

Posted on • Edited on

Handling Notifee Push Notifications in React Native (Display & Click)

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. For iOS, notification and data(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
      }
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

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"
        }
      }
    }
  }
Enter fullscreen mode Exit fullscreen mode

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.

Image description

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
Enter fullscreen mode Exit fullscreen mode

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

export const storage = new MMKV({
  id: `engage-mmkv-storage`,
  //  encryptionKey: Config.env
});

// 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) => {
  console.log('onForegroundEventCLICKBACKGROUND✨', Platform.OS, message);

// on kill mode redirection to specific page was not happeing so did this workaround
  storage.delete('notificationData');
  storage.set(
    'notificationData',
    JSON.stringify({ ...getDataNotificationData(message), type: message?.type })
  );

  if (message?.type == EventType.PRESS) {
    if (navigate) {
      console.log('onForegroundEventCLICKBG✨', Platform.OS, getDataNotificationData(message));
      navigate(NOTIFICATION_SCREEN); // Navigate to the notification screen
    }
  }
});
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

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';

// Remove badge count function
export const removeBadgeCount = async () => {
  await notifee.setBadgeCount(0);
  await notifee.cancelAllNotifications();
  console.log('Badge count removed!');
};

// Add badge count function
export const addBadgeCount = async (count = 1) => {
  await notifee.setBadgeCount(count);
  console.log(`Badge count set to ${count}`);
};

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,
      },
    },
  });
};
Enter fullscreen mode Exit fullscreen mode

AndroidManifest.xml
For notification add like this

...
    <meta-data android:name="com.google.firebase.messaging.default_notification_icon" android:resource="@drawable/ic_stat_name" />
    <meta-data android:name="com.google.firebase.messaging.default_notification_color" android:resource="@color/primary_dark"  tools:replace="android:resource"/>

  </application>
</manifest>
Enter fullscreen mode Exit fullscreen mode

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);
  await notifee.cancelAllNotifications();
  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 }
    );
  }
};

Enter fullscreen mode Exit fullscreen mode

Key Highlights:

  1. Request Permissions: Requests notification access and resets badge count.
  2. FCM Token: Retrieves, stores, and subscribes to FCM token.
  3. Topic Subscription: Manages notification topic subscription.
  4. Power Management: Detects restrictions, prompts settings adjustment.
  5. Battery Optimization: Alerts users to disable battery optimization for notifications.
  6. 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

import messaging from '@react-native-firebase/messaging';
import { requestPermission } from '../helpers/notification/notificationPermission';
import { getDataNotificationData } from '../helpers/helper';

useEffect(() => {
    permissionChecksNotification()
  }, []);

  const permissionChecksNotification = async () => {
    const deviceToken = await requestPermission();
    // dispatch(saveFCMToken(deviceToken));

    // await removeBadgeCount();
    // 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();
    };
  },
};

  const onReady = async () => {
    // onReady: When the application is opened from kill mode.
    try {
      const message = JSON.parse(storage.getString('notificationData'));
      console.log('onReady_MESSAGE', message);
      if (message?.type === EventType.PRESS) {
        navigate(NOTIFICATION_SCREEN);
      }
      storage.delete('notificationData');
    } catch (error) {
      console.log('Error getting initial notification:', error);
    }
  };

return(
    <NavigationContainer
      linking={linking}
onReady={onReady}

>
...
</NavigationContainer>)
Enter fullscreen mode Exit fullscreen mode

Key Highlights:

  • Handling Notification Clicks: Notifications clicked from the background or killed state are handled with onNotificationOpenedApp and getInitialNotification, 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.

Top comments (1)

Collapse
 
ritu_bhagwasiya_ebe9b17b4 profile image
Ritu Bhagwasiya

Can you please add import statements to section 6, Handling Initial Notifications in Routes?