DEV Community

Cover image for Sending Notifications In Your Web Apps
Oluwatobi Adedeji
Oluwatobi Adedeji

Posted on

Sending Notifications In Your Web Apps

Ever wondered how YouTube,x, or any of your favorite apps you use on your browser sends you notifications? Well it's not magic, there are ways to get it done and in this article, I will be illustrating one of the ways.

In the world of web development, with the introduction of some Web APIs, it's now much easier to get things done that may take having to use Mobile Apps to achieve one of them which is sending notifications. Sending notifications on the Web Browser is now much easier than ever with the Notification API and Push API using service workers.

In this article, I will be explaining how to send notifications in your website/web application and I will be doing this using Javascript. Let's go

Notifications can be of two types:

  1. Local Notification: This is generated by your app itself.

  2. Push Notification: This is generated by a server(backend) via a push event, an example of a push notification you get is when your favorite YouTuber just posted a video and you get notified even when you are not currently on the YouTube web app and when your internet comes up you get notified.

I will be covering each of these in the course of this article. What do we need to know before getting started?

Components needed for Sending Notification:
⦁ Service Workers: Service workers essentially act as proxy servers that sit between web applications, the browser, and the network (when available). They are intended, among other things, to enable the creation of effective offline experiences, intercept network requests, and take appropriate action based on whether the network is available. They will also allow access to push notifications and background sync APIs. A good explanation of a service worker at work is when you are offline but push notifications are being sent to your browser from a YouTube notification server, you do not get notifications until you are connected back to the internet.

⦁ Notification API: This API is used to show a notification prompt to the user just like in the case of mobile devices directly from your web app, for instance when a user clicks a button or performs an action in your app.

⦁ Push API: This API is used to get the push message from the server.

Sending Local Notifications

There are 3 steps we will be taking in sending local notifications in your web app.
⦁ We need to ask for the user's permission to send notifications via the Notification API using the requestPermission method.

⦁ If permission is granted, our Service Worker now listens for push events. On arrival of a push event, the service worker awakens and uses the information from the message to show a notification using notification API.

⦁ If permission is not granted you should also handle that properly in your code

Let's start by writing sample code for a simple page that implements local notifications on clicking the "Notify Me" button

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <button class="notify">Notify Me</button>
</body>
<script src="./script.js"></script>
</html>
Enter fullscreen mode Exit fullscreen mode

Now let's have our Javascript code...

const Notifybtn = document.querySelector(".notify");
const sendNotification = ()=>{
    if(!("Notification" in window)){
        throw new Error("Your browser does not support push notification");
    }
    Notification.requestPermission().then((Permission)=>{
        const notificationOptions = {
            body:"Welcome to Javascript Push Notification",
            icon:"./image.png"
        }
        new Notification("Push Notification",notificationOptions);
    })
};
Notifybtn.addEventListener("click", sendNotification);
Enter fullscreen mode Exit fullscreen mode

In the code above, we have the sendNotification function where we are first checking if the user's browser supports notification, then we request permission from the user to send notification, if the permission is not granted, we will be unable to send notification.
To send the notification, we use the Notification constructor passing the notification title as the first argument and the notification options as the second argument. Running this you get something like this

Basic Local Notification
Let's step it up a little bit, we want to send a more interactive notification where users have a button to click on or whatever kind of interaction we want to have. Let's try to modify our code to fit this need.

const sendNotification = ()=>{
    if(!("Notification" in window)){
        throw new Error("Your browser does not support push notification");
    }
    Notification.requestPermission().then((Permission)=>{
        const notificationOptions = {
            body:"Welcome to Javascript Push Notification",
            icon:"./image.png",
            actions: [
      {
        action: "thanks",
        title: "Thanks",
      },
      {
        action: "view_profile",
        title: "View Profile",
      },
    ],
        }
        new Notification("Push Notification",notificationOptions);
    })
};
Enter fullscreen mode Exit fullscreen mode

Here we added actions property to the notification options where users have buttons on the notification to interact with.. When you run this, you get an error like this :

We get an error when we try to create interactable actions with Notification constructor
Quite an explicit error message, actions are only supported while using service workers, how do we do this?

Using Service Workers To Send Notifications

First, we need to have our service worker file and register the service worker. Let's have our serviceWorker.js file with just a simple console.log there.

//serviceWorker.js
console.log("Hello, welcome to service worker")
Enter fullscreen mode Exit fullscreen mode

We can now go ahead to register our service worker.

const registerServiceWorker = async () => {
  if ("serviceWorker" in navigator) {
    try {
      const registration = await navigator.serviceWorker.register(
        "serviceWorker.js",
        {
          scope: "./",
        }
      );
      return registration;
    } catch (error) {
      console.error(`Registration failed with ${error}`);
    }
  }
};
Enter fullscreen mode Exit fullscreen mode

So it's time to create a notification that has interactive actions, by modifying our sendNotification function...

// script.js
const sendNotification = async () => {
  let notificationOptions = {
    body: "Elon Musk sent you a friend request",
    icon: "./image.png",
   data: {
      requestId: "1234",
      username: "elonmusk"
    },
    actions: [
      {
        action: "accept",
        title: "Accept",
      },
      {
        action: "view_profile",
        title: "View Profile",
      },
    ],
    tag: "friend_request",
  };
  const sw = await registerServiceWorker();
  sw.showNotification("Friend Request", notificationOptions);
};
Enter fullscreen mode Exit fullscreen mode

And here you have it

When you click nothing seems to be happening :) Let's step further by listening to the notificationclick event in our serviceWorker.

// serviceWorker.js

self.addEventListener("notificationclick", (event) => {
  event.notification.close();

  switch (event.notification.tag) {
  case "friend_request":{
    switch (event.action) {
      case "accept": {
        console.log("accept request API call.. with ", event.notification.data);
      }

      case "view_profile":
        {
          // direct to profile page
          event.waitUntil(
            clients
              .matchAll({
                type: "window",
                includeUncontrolled: true,
              })
              .then((windowClients) => {
                const matchingClient = windowClients.find(
                  (wc) => wc.url === urlToOpen
                );

                if (matchingClient) {
                  return matchingClient.focus();
                } else {
                  return clients.openWindow(event.notification.data.username);
                }
              })
          );
        }

        break;
      // Handle other actions ...
    }
  }
  }
});
Enter fullscreen mode Exit fullscreen mode

Here you see we have two actions Accept Request and View Profile, and we perform that when the tag for the notification is "friend_request", and we are basically passing data around in our notification which is an important thing to take note of.

Sending Push Notification

So far we have been able to send Local notifications within our web app, which is pretty cool right? But most times when building a real-world application what you will need is push notifications; notifications that are generated from a server. In this section, we are going to have our backend server in ExpressJS.. Let's set up our server.

// app.js

const express = require("express");
const cors = require("cors");
require("dotenv").config();

const { API_PREFIX } = process.env;
const app = express();
const { models } = require("./config/db");
const { sendNotification } = require("./utils/helper");
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(
  cors({
    origin: "*",
  })
);
app.disable("x-powered-by");
module.exports = app
Enter fullscreen mode Exit fullscreen mode
// index.js
const app = require("./app");
const { PORT } = process.env;
const connection = require("./config/db");
(async () => {
    await connection
      .sync()
      .then(() => {
        console.log("Database successfully connected");
      })
      .catch((err) => {
        console.log(err.message);
        return process.exit(1);
      });
    app.listen(PORT, () => {
        console.log(
          `<<<<<<<<<<<<<<<< Yeeeep,Server running on port ${PORT}..>>>>>>>>>>>`
        );
    });
  })();

Enter fullscreen mode Exit fullscreen mode

With this, our backend server is up and running(P.S: this code snippet won't work properly as it ought to because of some imports, at the end of this article, you have a link to the GitHub repository for all the codes at the end of the article"

Let's move fallback to the front end again... For us to be able to send push notifications, we need to have the following:
⦁ VAPID keys
⦁ The web app must subscribe to the push service through the service worker

VAPID: Voluntary Application Server Identification for Web Push is a spec that allows a backend server to identify itself to the Push Service(browser-specific service). It is a security measure that prevents anyone else from sending messages to an application’s users.

How do VAPID keys enable security?
In short:
You generate a set of private and public keys (VAPID keys) at the application server.
A public key will be sent to the push service when the frontend app tries to subscribe to the push service. Now the push service knows the public key from your application server(backend). The subscribe method will return a unique endpoint for your frontend web app. You will store this unique endpoint at the backend application server.
From the application server, you will hit an API request to the unique push service endpoint that you just saved. In this API request, you will sign some information in the Authorization header with the private key. This allows the push service to verify that the request came from the correct application server.
After validating the header, the push service will send the message to the frontend web app.

Let's go ahead to generate our VAPID keys, we need to install the "web-push" dendency on your backend or globally...

npm i web-push
Enter fullscreen mode Exit fullscreen mode

After installing you can run

npx web-push generate-vapid-keys
Enter fullscreen mode Exit fullscreen mode

And you have something like this

Public Key:
<YOUR_PUBLIC_KEY>

Private Key:
<YOUR_PRIVATE_KEY>
Enter fullscreen mode Exit fullscreen mode

You need to only generate this once. You have to Keep your private key safe.

Now let's fall to our serviceWorker.js file
We need to subscribe to the push service when the service worker registers...

//serviceWorker.js
const urlB64ToUint8Array = (base64String) => {
  const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
  const base64 = (base64String + padding)
    .replace(/\-/g, "+")
    .replace(/_/g, "/");
  const rawData = atob(base64);
  const outputArray = new Uint8Array(rawData.length);
  for (let i = 0; i < rawData.length; ++i) {
    outputArray[i] = rawData.charCodeAt(i);
  }
  return outputArray;
};

self.addEventListener("activate", async () => {
  try {
    const applicationServerKey = urlB64ToUint8Array(
      "<YOUR_PUBLIC_KEY>"
    );
    const options = { applicationServerKey, userVisibleOnly: true };
    const subscription = await self.registration.pushManager.subscribe(options);
    console.log(JSON.stringify(subscription))
  } catch (err) {
    console.log("Error", err);
  }
});
Enter fullscreen mode Exit fullscreen mode

Here we are listening to when the service worker registers and activates, we make use of the VAPID Public Key, converting it from a base64 string into to Array buffer which is needed by the subscription option, and we also have the userVisibleOnly in the property which is set to true. You need to send userVisibleOnly as true always. This parameter restricts the developer to use push messages for notifications. That is the developer will not be able to use the push messages to send data to the server silently without showing a notification. Currently, it has to be set to true otherwise you get a permission denied error. In essence, silent push messages are not supported at the moment due to privacy and security concerns.

When this runs successfully you have something like this:

{
"endpoint":"https://fcm.googleapis.com/fcm/send/<some_strange_id>",
"expirationTime":null,
"keys":{"p256dh":"<some_key>","auth":"<some_id>"}
}
Enter fullscreen mode Exit fullscreen mode

This response we got, we ought to save in a database on our backend, so let's create two endpoints on our backend, one to save the subscription and another to send notifications.

//app.js

app.post(`/${API_PREFIX}save-subscription`, async (req, res) => {

  try {
    const subscription = JSON.stringify(req.body);
    console.log(subscription);
    const sub = await models.subscriptions.create({ subsription:subscription });
    res.status(201).json({ message: "Subscription Successful" });
  } catch (error) {
    res.status(500).json({ message: error.message });
  }
});

app.post(`/${API_PREFIX}send-notification`, async (req, res) => {
  try {
    const { id } = req.body;
    const sub = await models.subscriptions.findOne({where:{id}});
    const message = {
      body: "Elon Musk sent you a friend request",
      icon: "https://media.npr.org/assets/img/2022/06/01/ap22146727679490-6b4aeaa7fd9c9b23d41bbdf9711ba54ba1e7b3ae-s800-c85.webp",
      data: {
        requestId: "1234",
        username: "elonmusk"
      },
      actions: [
        {
          action: "accept",
          title: "Accept",
        },
        {
          action: "view_profile",
          title: "View Profile",
        },
      ],
      tag: "friend_request",
    };
    await sendNotification(sub.subsription, message);
    res.json({ message: "message sent" });    
  } catch (error) {
    res.status(500).json({ message: error.message });

  }

});
Enter fullscreen mode Exit fullscreen mode

We also need to create the import utilily function that helps to send the push notification.

// utils/helper.js
const webpush = require("web-push");
const { VAPID_PRIVATE_KEY, VAPID_PUBLIC_KEY } = process.env;
//setting our previously generated VAPID keys
webpush.setVapidDetails(
  "mailto:<your_email>",
  VAPID_PUBLIC_KEY,
  VAPID_PRIVATE_KEY
);
//function to send the notification to the subscribed device
const sendNotification = async (subscription, dataToSend) => {
  try {
   await webpush.sendNotification(subscription, JSON.stringify(dataToSend)); //string or Node Buffer
  } catch (error) {
    console.log(error); 
    throw new Error(error.message);
  }
};
module.exports = { sendNotification };
Enter fullscreen mode Exit fullscreen mode

We have an endpoint to save the subscriptions and also another to send notifications (P.S: this is just a sample/ demo project, in real life scenarios you are most likely not going to have an endpoint to send notifications, rather it will be a utility service (class or function that can be triggered when certain actions occur).
Now let's go back to our front end to sync things up, we start by saving the push service subscription to the database by calling the endpoint.

   // serviceWorker.js
const saveSubscription = async (subscription) => {
  const SERVER_URL = "http://localhost:5005/api/save-subscription";
  const response = await fetch(SERVER_URL, {
    method: "post",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify(subscription),
  });
  return response.json();
};
self.addEventListener("activate", async () => {
  try {
    const applicationServerKey = urlB64ToUint8Array(
      "<YOUR_VAPID_PUBLIC_KEY>"
    );
    const options = { applicationServerKey, userVisibleOnly: true };
    const subscription = await self.registration.pushManager.subscribe(options);
    const response = await saveSubscription(subscription);
  } catch (err) {
    console.log("Error", err);
  }
});
Enter fullscreen mode Exit fullscreen mode

We are just a few steps away!! Now let's have the showNotification function that displays the push notification when it's received

// serviceWorker.js
const showLocalNotification = (title, data, swRegistration) => {
  swRegistration.showNotification(title, data);
};
Enter fullscreen mode Exit fullscreen mode

The function takes in three arguments; the title of the push notification, the notificationOption, and the service worker registration. Finally, we need to listen to the push event, when a push notification is made.

 // serviceWorker.js
self.addEventListener("push", function (event) {
  if (event.data) {
    console.log("Push event!! ", JSON.parse(event.data.text()));
    showLocalNotification(
      "Notification ",
      JSON.parse(event.data.text()),
      self.registration
    );
  } else {
    console.log("Push event but no data");
  }
});
Enter fullscreen mode Exit fullscreen mode

Take a look at what we have:

Sending push notification from the backend

We must also take note that when the page or browser is refreshed/restarted the service worker still remains registered, and you may need to change some things in the service worker file, you can invoke this function


const unregisterServiceWorkers = ()=>{
  if (window.navigator && navigator.serviceWorker) {
    navigator.serviceWorker.getRegistrations().then(function (registrations) {
      for (let registration of registrations) {
        registration.unregister();
      }
    });
  }
}

Enter fullscreen mode Exit fullscreen mode

Whenever we hit the send-notification endpoint with the ID generated by the db, we get the push notification on your device, isn't that cool?? If you are looking to build a mobile app because of push notifications, you could consider building a web app instead, if there are no "trade-offs"...

Thanks for reading through this article, I really hope you have learned one thing or the other, I have pushed the code samples of this article to this repo, enjoy!!

Top comments (5)

Collapse
 
michaelmakzo profile image
Michael Joseph

Thanks for writing and sharing this helpful content!

Collapse
 
shifi profile image
Shifa Ur Rehman

Omg thank you so much ❤️

Collapse
 
oluwatobi_ profile image
Oluwatobi Adedeji

Hi Hirak. I don't really get your concern.
Do you mean, how do we integrate this in WordPress?

Collapse
 
dotenv profile image
Dotenv

💛🌴

Collapse
 
niteshdaga profile image
Nitesh Daga

How to make it work if the frontend/backend do not have internet access, But they can talk to each other on local network ?