Enhance User Engagement: Web Push Notifications with Firebase Cloud Messaging (FCM)
Intro
Web push notifications have become a key component in enhancing user engagement on modern web applications. Firebase Cloud Messaging (FCM) provides a robust and easy-to-use solution for implementing push notifications across various platforms, including the web. Here, we will explore integrating FCM into a NextJs application to enable web push notifications for browsers. This blog covers sending background notifications to the browser.
Preliminary Step
Before jumping on the NextJs code, we need to configure the Firebase project and add a web app to our project to obtain the Firebase configuration object.
Also, we need to install FIrebase in our NextJs App using npm install firebase
Getting Started
There are a few steps that need to be done in the NextJs App before sending out our first push notification
Initialize Firebase app
Request for notification permission
Sending firebase config to the service worker
Handle Incoming Messages through service worker
When it comes to the art of sending notifications, Firebase Cloud Messaging (FCM) bestows two distinct methods upon developers —
Firebase Messaging Console
FCM V1 API
In this article, we'll navigate through these essential steps, ensuring your Next.js app is primed for delivering engaging and timely push notifications
In the realm of implementing web push notifications through Firebase Cloud Messaging (FCM), certain nuanced aspects arise, especially when utilizing the FCM API for notification dispatch. While the default messaging.onBackgroundMessage()
adeptly manages notifications from the Firebase console, a unique challenge surfaces when images are involved, requiring the explicit use of showNotification
.
Notably, this issue doesn't persist when leveraging the FCM v1 API. In this article, we'll explore the nuances of handling web push notifications in React/Next.js, shedding light on effective practices and workarounds. Additionally, we'll delve into the FCM v1 API for server-side notification dispatch and provide insights on optimizing the notification handling process.
Initialize Firebase app and messaging
//File: lib/authHook/firebase
//Initialise firebase app
import { initializeApp } from "firebase/app";
import { getMessaging } from "firebase/messaging";
export const firebaseConfig = {
apiKey: "API_KEY",
authDomain: "PROJECT_ID.firebaseapp.com",
databaseURL: "https://DATABASE_NAME.firebaseio.com",
projectId: "PROJECT_ID",
storageBucket: "PROJECT_ID.appspot.com",
messagingSenderId: "SENDER_ID",
appId: "APP_ID",
measurementId: "G-MEASUREMENT_ID",
};
export const app = initializeApp(firebaseConfig);
let messaging
if (typeof window !== 'undefined'){
messaging = getMessaging(app);
}
export {messaging}
Seeking notification permission
To enable the delivery of notifications, a crucial step involves seeking the user's consent, a common experience encountered when visiting websites. This permission is typically obtained through the Notification API, allowing developers to seamlessly set up the necessary permissions for notification delivery.
// File: _app.js
import { messaging } from "@/lib/authHook/firebase";
import { useEffect } from "react";
function MyApp({ Component, pageProps }) {
async function requestPermission() {
const permission = await Notification.requestPermission();
if (permission === "granted") {
// Generate Device Token for notification
const token = await getToken(messaging, {
vapidKey: process.env.NEXT_PUBLIC_FIREBASE_VAPID_KEY,
});
console.log("Token Gen", token);
} else if (permission === "denied") {
console.log("Denied for the notification");
}
}
useEffect(() => {
requestPermission()
}, []);
return
<Component {...pageProps} />
}
Handle incoming messages in the Service Worker
To facilitate the seamless handling of notifications in your web application, it's essential to create a dedicated service worker, often named 'firebase-messaging-sw.js.' This service worker plays a pivotal role in managing the background processes associated with Firebase Cloud Messaging, ensuring efficient handling of notifications and ensuring a seamless user experience.
// File: firebase-messaging-sw.js
importScripts("https://www.gstatic.com/firebasejs/8.10.0/firebase-app.js");
importScripts("https://www.gstatic.com/firebasejs/8.10.0/firebase-messaging.js");
// Set Firebase configuration, once available
self.addEventListener('fetch', () => {
try {
const urlParams = new URLSearchParams(location.search);
self.firebaseConfig = Object.fromEntries(urlParams);
} catch (err) {
console.error('Failed to add event listener', err);
}
});
// "Default" Firebase configuration (prevents errors)
const defaultConfig = {
apiKey: true,
projectId: true,
messagingSenderId: true,
appId: true,
};
// Initialize Firebase app
firebase.initializeApp(self.firebaseConfig || defaultConfig);
let messaging;
try {
messaging = firebase.messaging.isSupported() ? firebase.messaging() : null
} catch (err) {
console.error('Failed to initialize Firebase Messaging', err);
}
// To dispaly background notifications
if (messaging) {
try {
messaging.onBackgroundMessage((payload) => {
console.log('Received background message: ', payload);
const notificationTitle = payload.notification.title;
const notificationOptions = {
body: payload.notification.body,
tag: notificationTitle, // tag is added to ovverride the notification with latest update
icon: payload.notification?.image || data.image,
data: {
url: payload?.data?.openUrl,// This should contain the URL you want to open
},
}
// Optional
/*
* This condition is added because notification triggers from firebase messaging console doesn't handle image by default.
* collapseKey comes only when the notification is triggered from firebase messaging console and not from hitting fcm google api.
*/
if (payload?.collapseKey && notification?.image) {
self.registration.showNotification(notificationTitle, notificationOptions);
} else {
// Skipping the event handling for notification
return new Promise(function(resolve, reject) {});
}
});
} catch (err) {
console.log(err);
}
}
The inclusion of the optional section becomes particularly relevant when utilizing the Firebase Cloud Messaging (FCM) API to send notifications. It's noteworthy that when notifications are dispatched through the Firebase console, the messaging.onBackgroundMessage()
function seamlessly manages the notification handling process on its own.
Notifications initiated from the Firebase Console lack automatic handling of images, necessitating the explicit use of showNotification
to manage image display. However, this approach introduces a challenge, causing notifications to be triggered twice – once from the default messaging.onBackgroundMessage()
and the second from self.registration.showNotification()
. Addressing this duality becomes a key consideration when refining the notification handling process.
This is not the case when notification is triggered through FCM v1 API.
Request:** If you know how to overcome this do mention it in the comments, it will help me too :)
Use the FCM v1 API to send notifications from your server. Ensure your server sends a valid notification payload with the necessary data.
https://fcm.googleapis.com/v1/projects/${projectName}/messages:send
For handling the notification click in the service worker for opening the action URL we need to add an event listener in the same service worker file.
// File: firebase-messaging-sw.js
// Handling Notification click
self.addEventListener('notificationclick', (event) => {
event.notification.close(); // CLosing the notification when clicked
const urlToOpen = event?.notification?.data?.url || 'https://www.test.com/';
// Open the URL in the default browser.
event.waitUntil(
clients.matchAll({
type: 'window',
})
.then((windowClients) => {
// Check if there is already a window/tab open with the target URL
for (const client of windowClients) {
if (client.url === urlToOpen && 'focus' in client) {
return client.focus();
}
}
// If not, open a new window/tab with the target URL
if (clients.openWindow) {
return clients.openWindow(urlToOpen);
}
})
);
});
Secure firebase config in the service worker
As we cannot use env variables in the service worker one way to use the Firebase config is to send the Firebase config to the service worker file through URLSearchParams as it is good to hide the keys from the public file.
const UrlFirebaseConfig = new URLSearchParams(
{
apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
databaseURL: process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL ,
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID ,
storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET ,
messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID ,
appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID ,
measurementId: process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID ,
}.toString()
);
const swUrl = `${process.env.NEXT_PUBLIC_SERVER_URL}/firebase-messaging-sw.js?${UrlFirebaseConfig}`;
Conclusion
By following the insights shared in this article, you can seamlessly establish web push notifications via Firebase Cloud Messaging (FCM) in your React or Next.js application. While certain drawbacks have been discussed earlier, the inclusion of effective workarounds ensures a smooth initiation into the world of web push notifications. Reflecting on my own experience implementing this setup, I've found that despite the challenges, the enhanced user engagement and communication capabilities offered by FCM make it a valuable addition to any web development toolkit.
Subscribe to my newsletter
Read articles from Deepak Yadav directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Deepak Yadav
Deepak Yadav
Developer, exploring frontend technologies and building India great. Frontend Engineer @ Melorra