Firebase Notification + React

RK
11 min readFeb 5, 2023

--

So, I'm gonna make this a story rather than an article on how we can integrate Firebase notification to your react or your react PWA application.

So it all started way back in '97 when I noticed a ray of light as I was born. Nah don't worry, I'm kidding. So a few weeks back, I was tasked with creating a notification feature for a React PWA application.

Although I really don't prefer making PWA applications for mobile, I had no choice in the matter. So I put on my 3-day-old blue shorts and started where every developer would start on building a new feature, Google the hell out of the technology. That's exactly what I did.

The one thing that popped out from all the search result is that almost every documentation or tutorial on medium or Youtube were based on using Firebase as a service for sending out notifications. I have nothing against Firebase but why is every tutorial on sending push notifications based on Firebase?

I got super curious, as every developer who hasn't showered for the past 3 days should be (not me 😅). Turns out there was no point in installing multiple firebase modules just to get notifications in your PWA application. The only module that you would need is the web-push module, which can help you send out notifications from your server to your front end by making use of a subscription token. Now, this part is really important.

Flow without using Firebase

The fancy diagram shows you the flow that I had before I implemented Firebase. All was going well, so I slept early and in peace (which is super rare).

But when I tried it the next day, I was not getting notified. Turns out the subscription details that I store in the database for each user, will get expired after a few hours and I have to update that using a service worker method. But here's the catch, the latest version of Chrome does not support that feature.

So, I started panicking as the work that I did so far is going straight to the trash can. So after self-loathing for an entire day, I started to go back to the board (by the board, I mean back to googling solutions).

Firebase to the rescue?

I had to give in to using firebase at last, so how does the entire flow look after implementing firebase?

Turns out it's very similar to what we did so far, but the only change is that we let firebase handle the heavy lifting (ie: sending notifications).

Initial setup

The first step would be to install firebase in your react application by using the npm command.

npm install firebase

In your backend application (In my case, I'm using Node), install the following firebase modules. The node-cron is used to set up a cron job that can run for scheduled specified times.

npm install firebase firebase-admin firebase-functions node-cron

Setting up firebase project

We need to do is to set up a Firebase project. FCM is a service and as such, we’ll be needing some API keys. This step requires that you have a Google account. Create one if you don’t already have one. You can click here to get started.

After setting up your Google account, head on to the Firebase console.

Click on add project. Enter a name for your project and click on continue. On the next screen, you may choose to turn off analytics. You can always turn it on later from the Analytics menu of your project page. Click continue and wait a few minutes for the project to be created. It’s usually under a minute. Then click on continue to open your project page.

Once we’ve successfully set up a project, the next step is to get the necessary keys to work with our project.

On the project page, click on the add project icon to add Firebase to your web app.

How about giving your app a fun and quirky name like “CodeCrusher” or “AppAttack”? And don’t worry about setting up Firebase hosting, we don’t want it to feel like homework. Just hit that “Register app” button and give it a moment to finish the setup. Trust me, your app will be grateful for the break. And once it’s ready, grab those app credentials and keep them safe and sound, like a squirrel storing away its acorns for the winter. Or you could just leave this window open and come back to it when you’re ready, like a cat taking a nap in the sun. (Sorry, I got sidetracked, back to the application.)

We’ll be needing the configuration object later. Click on continue to console to return to your console.

Backend setup

We need a service account credential to connect with our Firebase project from the backend. On your project page, click on the gear icon next to Project Overview to create a service account for use with our Express backend. Refer to the below screenshot. Also, be sure to create a service and keep your service account file in a safe place.

Actual Implementation Now

Gritty work on the backend

If you're working with Node and Mongo Db, I created a separate schema for storing subscription-related details. (I just realized that there's currently no application to visualize a large MongoDB database! Hmm could work on that.)

Anyhow, this is a collection structure that I have in my database.

Subscription schema model

The idea is to save the subscription token for each user and then have separate objects to store the type of subscription.

The next step is to configure firebase for our backend. So go ahead and create a javascript file on the root of your project called firebaseInit.js

We get details for configuration when we create our project in Firebase way back. Also make sure that all the cofigurtion details on firebase is stored in your .env file.

/**
* This file sets up and initializes Firebase using the provided
* configuration from environment variables.
* It exports the messaging instance for use in other parts of the application.
*/
import admin from "firebase-admin";

let messaging;

const initializeFirebase = async () => {
const firebaseConfig = {
type: process.env.FIREBASE_TYPE,
project_id: process.env.FIREBASE_PROJECT_ID,
private_key_id: process.env.FIREBASE_PRIVATE_KEY_ID,
private_key: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, "\n"),
client_email: process.env.FIREBASE_CLIENT_EMAIL,
client_id: process.env.FIREBASE_CLIENT_ID,
auth_uri: process.env.FIREBASE_AUTH_URI,
token_uri: process.env.FIREBASE_TOKEN_URI,
auth_provider_x509_cert_url:
process.env.FIREBASE_AUTH_PROVIDER_X509_CERT_URL,
client_x509_cert_url: process.env.FIREBASE_X509_CERT_URL,
};

await admin.initializeApp({
credential: admin.credential.cert(firebaseConfig),
});
messaging = admin.messaging();
return messaging;
};

export default initializeFirebase;
export { messaging };

After creating the file, go into the jaascript file that starts your application. In most cases it should be server.js and call the method initializeFirebase();

Creating the APIs

It better to have API that we can use to create and update the subscription for a user. So we can go ahead and create the following APIs in Node.

  • POST /subscription — To create a new subscription
  • PUT /subscription/appointment — To enable subscription for appointmenet for a user
  • PUT /subscription/journal — To enable subscription for journal.
  • GET /subscription — To get subscription details for a user.
// @route   GET /subscription
// @desc sends push notification to subscriber id.
const getSubscriptionForUser = asyncHandler(async (req, res) => {
// Get user's subscription data
const subscription = await Subscription.findOne({ user: req.user._id });

// Return the subscription data
res.status(200).json(subscription);
});

The subscription is the mongodb schema that I talked about earlier. The following is the code to update an existing subsription or create a new subscription and update them as well. You can do the same for any other subscripton that you might have in the future.

// @route    PUT /subscribe/appointment
// @desc updates currently logged in users data
const subscribeToAppointment = asyncHandler(async (req, res) => {
// Get the token and status of the subscription
const { token, isEnabled } = req.body;

// If a token is present, create a new subscription for the user
if (token) {
const user = await User.findById(req.user._id);
await Subscription.create({
"appointmentNotification.isEnabled": req.body.isEnabled,
token: token,
user: user,
});
} else {
// If a token is not present, update the user's existing subscription
await Subscription.updateOne(
{ user: req.user._id },
{
$set: {
"appointmentNotification.isEnabled": isEnabled,
},
}
);
}

res.status(200).json({ token });
});

We are almost done guy, The only thing that's left out is to create a cron job to run for every five minutes and trigger notification to Firebase.

Creating Cron Job

In your node application, create a new file called cron.js and paste in the following code.

/**
* Sends reminders every 5 minutes.
* - Fetches reminder times using `getTimeForNotification` and `getTimeForApointment`
* - Fetches journal and appointment reminders using `getJournalReminders` and `getAppointmentReminders`
* - Sends all notifications using `sendNotifications`
*/
cron.schedule(
"* * * * *",
async () => {
console.log("---NOTIFYING USERS---");
const { start, end } = await getTimeForNotification(5, "m");
const { appointmentStart, appointmentEnd } = await getTimeForApointment(
65,
"minutes"
);

// Getting notifications to send.
const journalReminders = await getJournalReminders(start, end);
const appointmentReminders = await getAppointmentReminders(
appointmentStart,
appointmentEnd
);

// Sending all notifications.
sendNotifications([...journalReminders, ...appointmentReminders]);
},
{
scheduled: true,
timezone: "America/Edmonton",
}
);

const sendNotifications = async (notifications) => {
notifications.forEach((notification) => {
sendNotificationToClient(notification);
});
};

The methods,

  • getJournalReminders and getAppointmentReminders are very similar. They just get the details of the notificaiton that we are supposed to send to Firebase.

The only method that we really need to keep in mind is the sendNotifications method. The method is used to send out notification to Firebase. Make sure you create a seperate file called notify.js to include this method.

import admin from "firebase-admin";

/**
* Send a message to the devices corresponding to the provided
* registration tokens.
*
* @param {*} data
*/
export const sendNotificationToClient = (data) => {
admin
.messaging()
.send(data)
.then((response) => {
console.log(response);
})
.catch((error) => {
console.log("Error sending message:", error);
});
};

When we invokde the sendNotificationToClient method, firebase expects th data to be in the following format.

{
"data":{
"title":"Apointment Notification",
"body":"How was your appointment"
}
"token":"<subscription.token>"
}

Now on the frontend

Creating service worker file

Create a new file called firebase-messagin-sw.js in the public directory of your react application and add the following code.

/**
* This script sets up Firebase messaging in a Service Worker.
*
* The script starts by importing the necessary Firebase scripts and initializing
* the Firebase app with the given configuration.
* It then retrieves the Firebase messaging instance and sets up
* event listeners for background messages.
*
* The `onBackgroundMessage` function displays a notification with the title
* and body specified in the payload, as well as custom options like
* the notification icon and vibration pattern.
* The `notificationclick` event opens a new window to the "/journal"
* route when the notification is clicked.
*/
importScripts(
"https://www.gstatic.com/firebasejs/9.16.0/firebase-app-compat.js"
);
importScripts(
"https://www.gstatic.com/firebasejs/9.16.0/firebase-messaging-compat.js"
);

// Initialize the Firebase app in the service worker by passing the generated config
var firebaseConfig = {
apiKey: "",
authDomain: "",
projectId: "",
storageBucket: "",
messagingSenderId: "",
appId: "",
};

firebase.initializeApp(firebaseConfig);

// Retrieve firebase messaging
const messaging = firebase.messaging();

messaging.onBackgroundMessage(function (payload) {
console.log("Received background message ", payload);

const notificationTitle = payload.data.title;
const notificationOptions = {
body: payload.data.body,
icon: "./images/icons/Logo.png",
vibrate: [200, 100, 200, 100, 200, 100, 200],
tag: "",
data: {
url: "https://web.dev/push-notifications-overview/",
},
};

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

// Code for adding event on click of notification
self.addEventListener("notificationclick", (event) => {
// Hide the notification
event.notification.close();

clients.openWindow("/journal");
});

The onBackgroundMessage recieves all the messages that are received when the application is in the background and sends notifications using the web API registration.showNotification()

Now create anther file called firebase.js in the src direcotry of your react application and include the following code.

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

// Firebase configurations.
var firebaseConfig = {
apiKey: process.env.REACT_APP_API_KEY,
authDomain: process.env.REACT_APP_AUTH_DOMAIN,
projectId: process.env.REACT_APP_PROJECT_ID,
storageBucket: process.env.REACT_APP_STORAGE_BUCKET,
messagingSenderId: process.env.REACT_APP_MESSAGING_SENDER_ID,
appId: process.env.REACT_APP_APP_ID,
};

/**
* This function requests permission for notifications from Firebase messaging API.
* It returns a Promise that resolves with the Firebase notification
* token if permission is granted,
* or rejects with an error if the request fails or is denied.
*/
export const requestFirebaseNotificationPermission = () =>
new Promise((resolve, reject) => {
messaging
.requestPermission()
.then(() => messaging.getToken())
.then((firebaseToken) => {
resolve(firebaseToken);
})
.catch((err) => {
reject(err);
});
});

/**
* This function returns a subscription token from Firebase which
* is used to identify a unique system.
*
* If the token is successfully retrieved, it will be logged and returned.
* If not, the function will request notification permission
* and log the result of the request.
*/
export const getTokenFromFirebase = () => {
return getToken(messaging, {
vapidKey:
"<The ceritificate key that we generated in fireabase>",
})
.then((currentToken) => {
if (currentToken) {
console.log("current token for client: ", currentToken);
return currentToken;
} else {
console.log(
"No registration token available. Request permission to generate one."
);
Notification.requestPermission().then((permission) => {
console.log("Access for notification is ", permission);
});
}
})
.catch((err) => {
console.log("An error occurred while retrieving token. ", err);
});
};

/**
* The onMessage function from the Firebase Messaging API
* is used to listen for incoming messages.
*/
export const onMessageListener = () =>
new Promise((resolve) => {
onMessage(messaging, (payload) => {
resolve(payload);
});
});

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

The getTokenFromFirebase will install the service-worker and request permission for notification from the user. The token that is received from this method can be saved in the database and used to send notiication from the backend.

The onMessageListener method listens to messages that comes in when the application is opened in the users browser.

Inside your App.js file you can create a state to reive the notifaciaiton and show them to the user.

import {
onMessageListener
} from "./firebase";

// State for on app notification.
const [notificationState, setNotificationState] = useState({
open: false,
message: "",
});

const {
open,
message
} = notificationState;

const handleNotificationClick = () => {
navigate("/journal");
setNotificationState({
...notificationState,
open: false
});
};

const handleNotificationClose = () => {
setNotificationState({
...notificationState,
open: false
});
};

/**
* Method that listens to firebase FCM notification.
* Messages that are sent when the app is in use
* trigger this method.
*/
onMessageListener()
.then((payload) =>
setNotificationState({
open: true,
message: `🗓 ${payload.data.body}`,
})
)
.catch((err) => {
console.log(`An error occured when showing notif ${err}`);
});

Notification Screen

The way that I handled creating / enabling notification is to use a toggle. So when the user toggles a notification, the subscription model is updated such that isEnabled is made to be true.

The flow of the application is this, So once the cron job becomes active and it sends out notification to firebase, the user will receive push notification on his system.

--

--

RK

Software Engineer | Procaffinator ☕ | A dev and a little brown dude trying to make it big !