Zum Inhalt springen

A quick guide to push notifications with expo-notifications and Firebase

These days I had to implement push notifications, and since I really like the direction Expo is going, I decided to lean more into its ecosystem, so I chose to use expo-notifications to implement the feature. To my surprise, I couldn’t find any good guides on how to do it. Expo has a guide on how to set it up through their proprietary services, which I decided not to use to avoid the feeling of being platform-locked. Personally, the biggest benefit of Expo is keeping the manual implementation and management of native code to a minimum, as the longer a project lives, the harder it becomes to keep track of changes in different files for numerous libraries. So we will leverage Expo’s strengths using expo-notifications to set up pretty much all the native code and handle push notifications in the foreground while using Firebase Messaging to listen to notification events coming from Firebase and handle notifications in the background.

npx expo install expo-notifications @react-native-firebase/app @react-native-firebase/messaging

Here we configure Google Services and push notifications.
The path to the files and defaultChannel are my preference and can be changed.
If enableBackgroundRemoteNotifications is set to true, the needed permissions to handle background messages (when the app is open but not in focus or the app is closed/killed) will be set.

app.json

{
  "expo": {
    "ios": {
      "googleServicesFile": "./assets/GoogleService-Info.plist"
    }
    "android": {
      "googleServicesFile": "./assets/google-services.json"
    }
  }
  "plugins": [
    [
      "expo-notifications",
      {
        "icon": "./assets/icon.png",
        "color": "#002d2d",
        "defaultChannel": "default",
        "enableBackgroundRemoteNotifications": true
      }
    ],
    "@react-native-firebase/app",
    "@react-native-firebase/messaging",
  ]
}
 npx expo prebuild

You can see that all the Android adaptive icons for notifications have been created, and the required native implementations and permissions are set.
But nothing is perfect. In this case, you’re probably going to get an error if you try to build. Both expo-notifications and @react-native-firebase/messaging are trying to set the Android values for notifications, and we’ll have to resolve it manually by overriding the values of com.google.firebase.messaging.

android/app/src/main/AndroidManifest.xml

- <meta-data android:name="com.google.firebase.messaging.default_notification_channel_id" android:value="default" />
+ <meta-data android:name="com.google.firebase.messaging.default_notification_channel_id" android:value="default" tools:replace="android:value"/>

- <meta-data android:name="com.google.firebase.messaging.default_notification_color" android:resource="@color/notification_icon_color" />
+ <meta-data android:name="com.google.firebase.messaging.default_notification_color" android:resource="@color/notification_icon_color" tools:replace="android:resource"/>

- <meta-data android:name="com.google.firebase.messaging.default_notification_icon" android:resource="@drawable/notification_icon" />
+ <meta-data android:name="com.google.firebase.messaging.default_notification_icon" android:resource="@drawable/notification_icon" tools:replace="android:resource"/>

Now we can build and run the app.

 npm run android

We will now configure our app to start listening to the messages sent from Firebase. When in the background (app not in focus or closed), the OS will automatically handle the notifications for us, so we need to grant the necessary permissions to allow the OS to display push notifications.

import { getMessaging } from "@react-native-firebase/messaging";
import { PermissionsAndroid } from "react-native";

// Get a Firebase Messaging service instance
const messaging = getMessaging();

//IOS Permission
await messaging.requestPermission();
//Android Permission
await PermissionsAndroid.request(
  PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS
);

// Subscribe to receive events from Firebase messaging service
await messaging.registerDeviceForRemoteMessages();
// Get device a FCM token for the device
const pushToken = await messaging.getToken();
// You probably would like save the token in a database
console.log("Firebase Token:", pushToken);

You can send messages from the Firebase console at
Execute > Messaging > New Campaign > Notifications > Send a test message

We already have a basic notification system working, but as a design choice, Firebase Messaging does not automatically handle messages when the app is in the foreground (open, being used), since we wouldn’t necessarily want to show a push notification if the user is already using the app. In my use case, I would like to show the push notification, so we can create an event listener to handle messages when the app is in the foreground and use expo-notifications to trigger the OS to display the push notification.

import { getMessaging } from "@react-native-firebase/messaging";
import * as Notifications from "expo-notifications";

// Configure push notification behavior when app is in foreground
Notifications.setNotificationHandler({
  handleNotification: async () => ({
    shouldShowAlert: true,
    shouldPlaySound: true,
    shouldSetBadge: false,
    shouldShowBanner: true,
    shouldShowList: true,
  }),
});

// Trigger notification at same moment
async function schedulePushNotification({
  title = "New menage",
  body = "You have a new message!",
  data = {},
}) {
  await Notifications.scheduleNotificationAsync({
    content: {
      title,
      body,
      data,
    },
    trigger: null,
  });
}

// Get a Firebase Messaging service instance
const messaging = getMessaging();

useEffect(() => {
  // Handle foreground messages and returns the cleanup function
  const unsubscribeForeground = messaging.onMessage(async (remoteMessage) => {
    schedulePushNotification({
      title: remoteMessage?.notification?.title,
      body: remoteMessage?.notification?.body,
      data: remoteMessage?.data || {},
    });
  });

  return () => {
    // Clean the resources used for the listener
    unsubscribeForeground();
  };
}, []);

If you want to control how notifications interact with your app when opened by clicking on them, for example redirecting to a specific screen, you can create a listener for this type of event as well.

useEffect(() => {
  // Handle app opening from a background messages and returns the cleanup function
  const unsubscribeBackground = messaging.onNotificationOpenedApp(
    // Handle navigation or app state changes
    (remoteMessage) => {
      console.log("Background message opened:", remoteMessage);
    }
  );

  return () => {
    // Clean the resources used for the listener
    unsubscribeBackground();
  };
}, []);

So, for the complete implementation, I created a hook in a separate file to keep the code clean and call it to register the device and save the token to a DB. I save the token on the back end along with the device ID to update the token when it is refreshed or when the app is reinstalled on the same device. Since data is being saved to the DB, for security reasons, I also use a bearer token as a form of authentication.

import { getMessaging } from "@react-native-firebase/messaging";
import * as Notifications from "expo-notifications";
import { useEffect } from "react";
import { PermissionsAndroid, Platform } from "react-native";
import { getUniqueIdSync } from "react-native-device-info";

import { postPushNotification } from "../services/services";

// Configure push notification behavior when app is in foreground
Notifications.setNotificationHandler({
  handleNotification: async () => ({
    shouldShowAlert: true,
    shouldPlaySound: true,
    shouldSetBadge: false,
    shouldShowBanner: true,
    shouldShowList: true,
  }),
});

// Trigger notification
async function schedulePushNotification({
  title = "Nova mensagem",
  body = "Você tem uma nova mensagem!",
  data = {},
}) {
  await Notifications.scheduleNotificationAsync({
    content: {
      title,
      body,
      data,
    },
    trigger: null,
  });
}

// Configure push notification
export function useConfigPushNotification() {
  // Firebase messaging service instance
  const messaging = getMessaging();
  const deviceId = getUniqueIdSync();
  const platform = Platform.OS;

  /**
   * Handle permissions, register device in Firebase and save token in back end
   * @param {string} bearerToken
   */
  async function register(bearerToken) {
    // IOS permission
    await messaging.requestPermission();
    // Android permission
    await PermissionsAndroid.request(
      PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS
    );

    // Register device
    await messaging.registerDeviceForRemoteMessages();
    const pushToken = await messaging.getToken();

    // Save token in back end
    try {
      await postPushNotification(bearerToken, platform, pushToken, deviceId);
    } catch (err) {
      console.log(err?.response);
    }
  }

  useEffect(() => {
    // Handle foreground messages and returns the cleanup function
    const unsubscribeForeground = messaging.onMessage(async (remoteMessage) => {
      schedulePushNotification({
        title: remoteMessage?.notification?.title,
        body: remoteMessage?.notification?.body,
        data: remoteMessage?.data || {},
      });
    });

    // Handle app opening from a background messages and returns the cleanup function
    const unsubscribeBackground = messaging.onNotificationOpenedApp(
      // Background message pop up is handled automatically
      // Handle navigation or app state changes
      (remoteMessage) => {
        console.log("Background message opened:", remoteMessage);
      }
    );

    return () => {
      unsubscribeForeground();
      unsubscribeBackground();
    };
  }, []);

  return { register };
}

And finally, call the hook to handle the configurations.

const { register } = useConfigPushNotification();

// Config Push Notifications
try {
  await register("bearer_token");
} catch (err) {
  console.log(err);
}

I hope this guide helps you (and my future self) set up push notifications using Expo and Firebase!
Let me know if you have any questions or if there’s anything that could be improved.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert