Implementing Firebase Cloud Messaging in a React iOS / Android PWA : A Comprehensive Guide
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
Go to the Firebase Console
Click "Add project" and follow the setup steps
Once your project is created, click "Add app" and choose the web platform
Follow the instructions to register your app
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
Go to Cloud Messaging: In the Firebase Console, navigate to Cloud Messaging.
Compose a Message: Click "Send your first message" and fill in the details.
Target Users: Choose your web app and specify the users or topics.
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
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
Error handling:
Implement proper error handling for token retrieval and notification display
Provide fallback mechanisms for devices that don't support notifications
User experience:
Allow users to manage their notification preferences
Don't overwhelm users with too many notifications
Ensure notifications are relevant and timely
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 runsOnly
onBackgroundMessage
event runs, even when app is open/in foregroundService 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.
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.