Implementing Firebase Cloud Messaging in a React iOS / Android PWA : A Comprehensive Guide

Sahil AhmedSahil Ahmed
8 min read

1. Introduction

Firebase Cloud Messaging (FCM) is a cross-platform messaging solution that lets you reliably send messages at no cost. Using FCM, you can send notification messages to drive user re-engagement and retention.
In this article, we'll cover how to implement FCM in a React application, focusing specifically on a React Progressive Web App (PWA) for both iOS and Android. This is important because you'll encounter some poorly documented bugs and strange behavior of push notifications while developing for these platforms.
I'll guide you through setting up Firebase and handling notifications in both the foreground and background.

2. Setting up Firebase in your React application

Creating a Firebase project

  1. Go to the Firebase Console

  2. Click "Add project" and follow the setup steps

  3. Once your project is created, click "Add app" and choose the web platform

  4. Follow the instructions to register your app

  5. In Project Settings > Cloud Messaging, ensure Cloud Messaging is enabled

Installing necessary dependencies

In your React project directory, run:

npm install firebase

Initializing Firebase in your app

Create a src/firebase.ts file.
In Android Chrome, the background service worker are supported, but the foreground new Notification() API isn't supported. So, we need to use registration.showNotification() after checking the user agent in case we are not using the onMessage method from firebase/messaging directly in our component.

import { initializeApp } from "firebase/app";
import { getMessaging, onMessage } from "firebase/messaging";

const firebaseConfig = {
  apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
  authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
  databaseURL: import.meta.env.VITE_FIREBASE_DATABASE_URL,
  projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
  storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID,
  appId: import.meta.env.VITE_FIREBASE_APP_ID,
  measurementId: import.meta.env.VITE_FIREBASE_MEASUREMENT_ID,
};

import { initializeApp } from 'firebase/app';
import { getMessaging, onMessage } from 'firebase/messaging';

const app = initializeApp(firebaseConfig);
const messaging = getMessaging(app);

type notificationOptions = { body: string,
    icon: "./icon-192x192.png",
    requireInteraction: boolean,
    tag: "renotify",
    data: {
      time: string,
      message: "new order",
    },}

// Usage in your onMessage handler
onMessage(messaging, (payload) => {

    showNotification(payload.notification?.title as string, {
      body: payload.notification?.body as string,
      icon: "./icon-192x192.png",
      requireInteraction: true,
      tag: "renotify",
      data: {
        time: new Date(Date.now()).toString(),
        message: "new order",
      },
    });
  });


function showNotification(title: string, options: notificationOptions) {
    const platform = detectPlatform();

    switch(platform) {
      case 'iOS':
        // On iOS, we'll use a custom alert or UI element
        showIOSAlert(title, options.body);
        break;
      case 'Android':
        if ('serviceWorker' in navigator && 'PushManager' in window) {
          navigator.serviceWorker.ready.then(function(registration) {
            registration.showNotification(title, options);
          });
        } else {
          console.warn("Service Worker or Push API not supported on this browser");
        }
        break;
         default:
    // For other platforms, use the Notification API
    Notification.requestPermission().then(function (permission) {
        if (permission === "granted") {
          new Notification(title, options);
        }
      });
    }
  }

function showIOSAlert(title: string, body: string) {
    // Implement a custom alert for iOS
    alert(`${title}\n\n${body}`);
  }

function detectPlatform() {
    const userAgent = navigator.userAgent || navigator.vendor

    if (/android/i.test(userAgent)) {
      return 'Android';
    }

    if (/iPad|iPhone|iPod/.test(userAgent) && !('MSStream' in window)) {
      return 'iOS';
    }

    if (/chrome/i.test(userAgent) && /android/i.test(userAgent)) {
      return 'ChromeAndroid';
    }

    return 'Other';
}
export { app, messaging };

3. Implementing the Firebase messaging service worker

Handling Background Notifications

Create a public/firebase-messaging-sw.js file:

importScripts("https://www.gstatic.com/firebasejs/9.9.4/firebase-app-compat.js");
importScripts("https://www.gstatic.com/firebasejs/9.9.4/firebase-messaging-compat.js");

const firebaseConfig = {
  // Your Firebase config here
};

firebase.initializeApp(firebaseConfig);
const messaging = firebase.messaging();

//This is an event listener that listens for incoming messages when the 
//application is not in the foreground (in the background or terminated)
messaging.onBackgroundMessage((payload) => {
  console.log("background message received sw => ", payload);

  const notificationOptions = {
    body: "Please check the app to start delivery",
    icon: "../../public/icons/maskable_icon_x48.png",
    badge: "../../public/logo.svg",
    image: "../../public/logo.svg",
    tag: "renotify",
    renotify: true,
    requireInteraction: true,
    timestamp: Date.parse(new Date()),
    actions: [
      {
        action: "openApp",
        type: "button",
        title: "Order",
        icon: "../src/assets/icon.svg",
      },
    ],
    data: {
      time: new Date(Date.now()).toString(),
      message: "going to dashboard ...",
      click_action: "/dashboard",
    },
  };
  const notificationTitle = "New Order";

  self.registration.showNotification(notificationTitle, notificationOptions);
});

4. Setting up FCM utility functions in your React application

Create a src/utils/general.ts file. Add functions to request or check notification permissions, get the messaging token, etc:

import { getToken } from "firebase/messaging";
import { messaging } from "../firebase";

export const getNotificationPermission = async () => {
  Notification.requestPermission().then(async (permission) => {
    if (permission === "granted") {
      console.log("Notification permission has been granted.");

      const messagingToken = await getMessagingToken();

      console.log("messagingToken =>", messagingToken);
    } else {
      alert("Please allow notifications or enable them from device settings");
    }
  });

  try {
    Notification.requestPermission().then((permission) => {
      checkNotificationPermissionGranted(permission);
    });
  } catch (error) {
    if (error instanceof TypeError) {
      Notification.requestPermission((permission) => {
        checkNotificationPermissionGranted(permission);
      });
    } else {
      throw error;
    }
  }
};

async function checkNotificationPermissionGranted(permission: string) {
  if (permission === "granted") {
    console.log("Notification permission has been granted.");

    const messagingToken = await getMessagingToken();

    if (messagingToken) {
      console.log("messagingToken to be send to server=>", messagingToken);
      localStorage.setItem("fcm_token", JSON.stringify(messagingToken));

      sendTokenToServer(messagingToken as string);
    }
  } else {
    if (
      navigator.platform.includes("Mac") ||
      navigator.platform.includes("iPad") ||
      navigator.platform.includes("iPhone")
    ) {
      Notification.requestPermission((permission) => {
        checkNotificationPermissionGranted(permission);
      });
    } else {
      alert("Please allow notifications or enable them from device settings");
    }
  }
}

function sendTokenToServer(token: string) {
  console.log("Sending token to server ...", token);
  // Implement your API call to send token to server here , where it is 
 //stored securely and used for sending notifications to correct client
}

export const getMessagingToken = async () => {
  try {
    const messagingToken = await getToken(messaging, {
      vapidKey: import.meta.env.VITE_FIREBASE_VAPID_KEY,
    });

    if (messagingToken) {
      return messagingToken;
    } else {
      alert("No registration token available. Request permission to generate one.");
    }
  } catch (err) {
    throw new Error("An error occurred while retrieving token. ", {
      cause: err,
    });
  }
};

5. Handling receiving notifications in your React App with Notification Modal Component

Create a new file called NotificationModal.tsx with the following content:


const NotificationModal= ({ notification }: { notification: { image?: string; title: string; body: string } }) => {
  return (
    <>
      <div id="notificationHeader">
        {/* image is optional */}
        {notification.image && (
          <div id="imageContainer">
            <img src={notification.image} width={100} alt="" />
          </div>
        )}
        <span>{notification.title}</span>
      </div>
      <div id="notificationBody">{notification.body}</div>
    </>
  );
};

export default NotificationModal;

Then, in your main App component:

import React, { useEffect } from 'react';
import { getNotificationPermission } from './utils/general';
import NotificationModal from "./components/NotificationModal";
import "react-toastify/dist/ReactToastify.css";
import { toast, ToastContainer } from "react-toastify";
import { onMessage } from 'firebase/messaging';
import {messaging} from './firebase';

function App() {
  useEffect(() => {
    getNotificationPermission();
  }, []);

  onMessage(messaging, (payload) => {
    if (payload.notification) {
      toast(<NotificationModal notification={{
        title: payload.notification.title || '',
        body: payload.notification.body || '',
        image: payload.notification.image
      }} />);
    }
  });

  return (
    <div className="App">
       <ToastContainer />
      {/* Rest of your app */}
    </div>
  );
}

export default App;

6. Sending Notifications with FCM

Using Firebase Console

  1. Go to Cloud Messaging: In the Firebase Console, navigate to Cloud Messaging.

  2. Compose a Message: Click "Send your first message" and fill in the details.

  3. Target Users: Choose your web app and specify the users or topics.

  4. Send the Message: Click "Send message."

Sending Notifications Programmatically

To send notifications programmatically without using the console, use the Firebase Admin SDK with Node.js. Check out the article I have written on it.

7. Best practices for FCM implementation

  1. Token management:

    • Store the FCM token securely (e.g., in your backend database)

    • Implement a mechanism to update the token when it changes

    • Associate the token with the user's account for targeted notifications

  2. Error handling:

    • Implement proper error handling for token retrieval and notification display

    • Provide fallback mechanisms for devices that don't support notifications

  3. User experience:

    • Allow users to manage their notification preferences

    • Don't overwhelm users with too many notifications

    • Ensure notifications are relevant and timely

  4. Testing:

    • Test your notification system thoroughly on different devices and browsers

    • Implement proper logging to track notification delivery and user interactions

    • Simulate different scenarios (foreground, background, app closed)

By following these guidelines and implementing FCM correctly, you can create a robust notification system for your React application that enhances user engagement and provides timely updates to your users.

8. Troubleshooting Notification Delivery Issues

Foreground Notifications

  • In iOS PWAs, if a foreground notification is not interacted with:

    • Subsequent foreground notifications won't pop up

    • The first notification remains visible

    • Second notification updates the existing one if payload differs

  • Solution: Use a custom notification toast for each notification

  • Canceling/removing notification programmatically doesn't count as interaction

  • Only removing from notification tray allows similar notifications to appear

  • This behavior doesn't occur with background notifications

  • Desktop Chrome PWA doesn't exhibit this behavior

Token Management

  • Token may change randomly without warning, especially in iOS

  • Recommended to send token to backend every time it changes

  • Token changes when:

    • App is reinstalled after uninstalling

    • User returns to the app after a few days

iOS-Specific Considerations

  • Notification permission request must be triggered by explicit user action (e.g., button click)

  • Notifications can only be requested when running as a PWA (added to home screen)

  • Current bug: Foreground notification not showing, onMessage event never runs

  • Only onBackgroundMessage event runs, even when app is open/in foreground

  • Service worker isn't active immediately after phone restart

  • Notifications appear when PWA is open or service worker becomes active

  • iOS notification sound is overridden by natively set device notification sound

General FCM Behavior

  • FCM notification permission doesn't work in incognito mode

  • Users may not get notifications immediately or at all due to:

    • Device on Do Not Disturb (DND) mode

    • Notifications blocked unintentionally

    • Notification tray being full

    • Poor internet connectivity

Service Worker Behavior

  • After phone restart, service worker isn't immediately active

  • Notifications appear when:

    • PWA is open

    • Service worker becomes active on its own

  • If another notification is sent, both notifications are shown

    Some of these reasons are covered in detail here.

9. Conclusion

Implementing Firebase Cloud Messaging in your React application can greatly enhance user engagement and provide real-time updates to your users. By following this guide, you've learned how to set up FCM, handle notifications in both the foreground and background, and implement best practices for token management and error handling.

Remember always to keep security in mind when dealing with FCM tokens, and continually test your implementation to ensure it works across different devices and scenarios.

0
Subscribe to my newsletter

Read articles from Sahil Ahmed directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Sahil Ahmed
Sahil Ahmed

I'm Sahil Ahmed, an experienced software engineer who wants to be a digital polymath. My journey in software development is driven by a curiosity to create efficient, user-centric solutions for real-world problems across web and mobile platforms while writing clean code. I also love to read classic literature and play football.