Enhance User Engagement: Web Push Notifications with Firebase Cloud Messaging (FCM)

Deepak YadavDeepak Yadav
6 min read

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 —

  1. Firebase Messaging Console

  2. 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.

0
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