Read More on Telerik Blogs
August 19, 2022 Web, React
Get A Free Trial

 

We will examine and implement push notifications using the web push protocol in a mobile app, giving us customization control.

Since the advent of PWAs (progressive web applications) and service workers, native mobile application features do not feel like a far cry from what the web offers. In this article, we will be examining and implementing one of these native mobile application features: push notifications, which provide an elegant way to engage users with personalized content. Without further ado, let’s dive in.

How Push Notifications Work

The diagram below shows how push notifications work.

First, it registers a service worker on the client application and then gains permission from the client to enable notifications. Then, it creates some subscription information unique to the client, which is then shipped to a web server via HTTP. The server receives this subscription information that is then saved in a database and used to communicate with the push service with whatever message (string) is to be sent to the client. The push server receives the message and forwards it to the appropriate client.

If the client’s device is offline when the message arrives, the message may be dropped and not sent or preserved and then sent later when the device comes online, all based on the specifications of the server. Once the message has gotten to the user’s device, it is passed to the service worker via a push event which finally displays the notification on the user’s device.

Prerequisites

Basic knowledge about the below is necessary to follow along with this post:

  • Service workers
  • Building servers with Express and Node.js
  • CRUD operations with databases
  • React

Project Setup

First, paste the following code into your terminal to set up the required folders.

mkdir pushNotifications
cd pushNotifications
mkdir frontend backend

The above command creates a project directory and then creates two subfolders. Now we need to configure the frontend folder and install the necessary dependencies; insert the following code into your terminal.

cd frontend
npx create-react-app .
npm i --save axios

The above code bootstraps a React app. Next, we install Axios, the HTTP client to be used to communicate with our backend. Lastly, to set up the backend folder, open your terminal and insert the following commands.

cd backend
npm init --y
npm i express web-push mongoose
touch server.js subscriptionModel.js .env

The code above first initializes our project and installs necessary dependencies—one of which is web push, used on the server to communicate with the push service to send notifications—followed by creating the necessary files. The server.js file is where our server code will live, and the subscriptionModel.js file will hold the model, which will be used to store subscription information in our MongoDB database.

If done correctly, the frontend folder structure will look similar to the image below.

Setting Up Subscriptions

To clearly understand the process, we will divide the steps performed on the client and server sides, respectively.

Note: Each time we discuss the client side, it is assumed that we are in the “frontend” directory, and for server side, the “backend” directory

Here are the steps for the server side:

  • Generate VAPID (voluntary application server identification) keys.
  • Set up an endpoint to handle subscriptions from the client and save to the database.

Here are the steps for the client side:

  • Register service worker.
  • Generate subscription information and send it to the endpoint defined on the server.

Generate VAPID Keys

We want to send notifications to the client and ensure that it comes from our server, not some random server that somehow managed to access our client’s subscription information.

VAPID helps to identify our server as the initiator/creator of any message sent to the push service to be shipped to the client’s device and also helps the push service to notify the server owner via emails whenever there are issues with a push message.

Although it’s not compulsory to generate VAPID keys, it’s good practice as it regulates the traffic coming to clients and can reduce the chances of an attack.

To generate VAPID keys, we will use one of the scripts by the web-push module we installed earlier. Update the package.json file in the backend directory by adding the following to the script object.

"gen_vapid_keys": " web-push generate-vapid-keys [--json]"

Next, run the following command in your terminal to generate VAPID keys.

npm run gen_vapid_keys

If it runs successfully, a safe public and private key will be generated. The private key lives on the server, while the public key will be shared with the client to generate a subscription later on. Copy the public and private key pair and store them in environment variables on the .env file created earlier in the backend directory.

Set Up an Endpoint

Before we work in our server file, let’s first define the subscription schema that will be used to store subscription information in the database. Open the subscriptionSchema.js file and insert the following:

const mongoose = require ('mongoose');
const Schema = mongoose.Schema;
const Subscription = new Schema ({
  endpoint: String,
  expirationTime: Number,
  keys: {
    p256dh: String,
    auth: String,
  },
});
module.exports = mongoose.model ('subscription', Subscription);

The above code first imports the Mongoose ORM/ODM. It defines all the fields and their types required to store a single subscription to the database. It then finally exports a Model that maps to a “subscriptions” collection in our database, which will be used later in our server file to create this collection and store some subscription information.

Next, we set up our server.

require ('dotenv').config ();
const express = require ('express');
const webPush = require ('web-push');
const SubscriptionModel = require ('./subscriptionSchema');
const mongoose = require ('mongoose');
const app = express ();
const port = 9000;
const DatabaseName = 'pushDb';
const DatabaseURI = `mongodb://localhost:27017/${DatabaseName}`;
app.use (express.json ());
app.use (express.urlencoded ({extended: false}));

//...middlewares will be added in a moment

mongoose
  .connect (DatabaseURI, {
    useNewUrlParser: true,
    useUnifiedTopology: true,
  })
  .then (db => {
    app.listen (port, () => console.log (`app running live on ${port}`));
  })
  .catch (err => console.log (err.message));

The above code starts by bringing in the required dependencies followed by our MongoDB model, which we defined earlier, and then defines our database name and connection URL. We then use the express module to configure our server. Finally, we connect to the database, which, when done successfully, will listen for requests on port 9000.

Next, we define the endpoint that stores subscription information in our database and sends push notifications later. Let’s now update our server.js file with the following code.

app.use (express.urlencoded ({extended: false}));

app.post ('/subscribe', async (req, res, next) => {
  const newSubscription = await SubscriptionModel.create ({...req.body});
  //.....
});

mongoose
  .connect(......)

The above code defines the middleware function that handles a POST request to the “/subscribe” endpoint, which expects some subscription information contained in the request body that is saved to the database when parsed successfully.

Next, let’s focus on the frontend folder.

Register Service Worker

Before registering a service worker, we have to create one. Open your terminal and insert the following command.

cd public
touch sw.js

The above command creates a service worker file in the public directory. I will go over why we place it there when we register it. Update the sw.js file with the following code.

this.addEventListener ('activate', function (event) {
  console.log ('service worker activated');
});
this.addEventListener ('push', async function (event) {
  console.log ("notifications will be displayed here");
});

Since service workers are event-driven, we defined some events it will respond to, one of which is the activate event; this is called when the service worker has been activated. The push event is used to display the push notification when it comes on the push service to the client’s device. For now, we just added some logs, but later on, they will be updated with the logic required to display notifications.

Generate Subscription Information and Send It to Endpoint

We now define helper methods we will use later in our React components to register our service worker file and generate subscription information using the server’s public VAPID key we created earlier.

To create the helper file, open your terminal and insert the following command.

cd src
touch helper.js

Next, insert the following code to define the required functions.

import axios from 'axios';
async function regSw () {
  if ('serviceWorker' in navigator) {
    let url = process.env.PUBLIC_URL + '/sw.js';
    const reg = await navigator.serviceWorker.register (url, {scope: '/'});
    console.log ('service config is', {reg});
    return reg;
  }
  throw Error ('serviceworker not supported');
}

Above, we first check support for service workers on the client’s browser, and, if it exists, we create a URL that points to the service worker file we created earlier in the public directory.

Finally, we register the service worker and return it.

async function subscribe (serviceWorkerReg) {
  let subscription = await serviceWorkerReg.pushManager.getSubscription ();
  console.log ({subscription});
  if (subscription === null) {
    subscription = await serviceWorkerReg.pushManager.subscribe ({
      userVisibleOnly: true,
      applicationServerKey: 'BKemtwM7irZVq7QiMjpIvx_pioe-DDN-T2mdceu_bE57MjttTD_BPmZYrnUfyNaQsOJ28oub9l_-UW8yqBDo',
    });
  }
}

This method expects a service worker registration which it then uses to check if a subscription exists. If not, it uses the push API to create one by calling the subscribe method and passing appropriate options which do the following:

  • userVisibleOnly: True means that any push message that comes to the client’s device will display a notification.
  • applicationServerkey: This is a string whose value is the public VAPID key we created earlier on the server; it is used to associate the subscription to the server. The key will be used to decrypt the message on the push service, which will be sent by our server later.

For more information about options, please see this page.

Later on, we will call this function, but let’s quickly see the implications of each function altogether with the actions performed for us by the browser.

The subscribe call first displays a pop-up requesting permission from the user to receive notifications.

Suppose the user grants access. Under the hood, it makes an HTTP request to the push service (every browser chooses a push service of its choice) and then registers the server public key (application server key) and creates the subscription information, which takes the form below.

{
  endpoint: 'https://fcm.googleapis.com/fcm/send/eI_J9PnhptA:APA91bGWynL1Lu6AuKrV2l7tmfLboBvlRdeoboA6n1vbMy7EEa02WUTSuQx1wIH3xL8kZpGVhhIk0h-7cIFrgZBX4ANdxJWLRFWu',
  expirationTime: null,
  keys: {
    p256dh: 'BI11ZwAW0PtbarMUF15iVt0wKC8TGaVR_GhtHTQftXd60MtLtYfo8JXGgkX2y4Ejkx90Flj3vlokQ65l
    auth: 'CfovVtVP_wZOEYjHkZLpmw'
  }
}

The endpoint key is a unique URL that is used to send a push message to that device. Keys hold information that will be used to encrypt the push message that the server will send to the push service that will arrive at the client device.

The server’s private key is used to encrypt the request that will be later verified by the push service using the server’s public key, while the keys generated above from the call to subscribe() are used to encrypt the actual message. The encryption is done because the endpoint may refer to some random push service that the browser chooses, and it can’t be trusted with the client’s information.

Finally, we export the defined methods.

export {regSw, subscribe};

Send the Push Notification

Like we did earlier with the subscription, we break down the things to be done on the client and server sides.

Here are the steps for the client side:

  • Send subscription information to the server.
  • Set up a React app to trigger service worker registration and subscription.

Here are the steps for the server side:

  • Use subscription information to send push messages to push service.
  • Send subscription information to the server.

Send Subscription Information

Without the subscription information from the client, the server doesn’t know whom to send push messages, so we have to ship this information to the server by making an HTTP request. Open the sw.js file and insert the following code in the subscribe function we defined earlier.

async function subscribe (serviceWorkerReg) {
  let subscription = await serviceWorkerReg.pushManager.getSubscription ();
  if (subscription === null) {
    subscription = await serviceWorkerReg.pushManager.subscribe ({
      userVisibleOnly: true,
      applicationServerKey: 'BKemtwM7irZVq7QiMjpIvx_.....',
    });
    axios.post ('/subscribe', subscription);
}

The above code adds the logic required to make a POST request to our server’s /subscribe endpoint.

Just a tiny tip here: Ensure you have included the proxy key in your package.json file to avoid any CORS errors when making the request.

Set Up a React App To Trigger Service Worker

We don’t want to manually call the functions to register and subscribe on the client machine. We want it to be a result of a user’s action. Here it will be done when the user clicks a button. We now add the code to display the button. Open the App.js file and insert the following code.

import logo from './logo.svg';
import './App.css';
import {regSw, subscribe} from './registerSW';
function App () {
  async function registerAndSubscribe () {
    try {
      const serviceWorkerReg = await regSw ();
      await subscribe (serviceWorkerReg);
    } catch (error) {
      console.log (error);
    }
  }
  return (
    <div className="App">
      <button onClick={registerAndSubscribe}>
        subscribe for push notifications
      </button>
    </div>
  );
}
export default App;

We don’t want to manually call the functions to register and subscribe on the client machine. We want it to be a result of a user’s action. Here it will be done when the user clicks a button. We now add the code to display the button. Open the App.js file and insert the following code.

import logo from './logo.svg';
import './App.css';
import {regSw, subscribe} from './registerSW';
function App () {
  async function registerAndSubscribe () {
    try {
      const serviceWorkerReg = await regSw ();
      await subscribe (serviceWorkerReg);
    } catch (error) {
      console.log (error);
    }
  }
  return (
    <div className="App">
      <button onClick={registerAndSubscribe}>
        subscribe for push notifications
      </button>
    </div>
  );
}
export default App;

First we import the helper methods we defined earlier. Then we added some CSS to place the button in the center of the screen. In the App component, we defined a function bound to the button’s click event, which first calls the function to register the service worker, then uses the registration to create a subscription. Finally, it passes the subscription to the server as defined earlier. Open your terminal and run the react app.

npm start

We don’t see any notifications now because our server is yet to send one. We now go to our server and receive the subscription and send a push message.

Use Subscription Information To Send Push Messages

We need to update our server file with the logic to handle the subscription details and send a push notification. Now insert the following code.

app.post ('/subscribe', async (req, res, next) => {
  const newSubscription = await SubscriptionModel.create ({...req.body});
  // return res.send ('hallo');
  const options = {
    vapidDetails: {
      subject: 'mailto:myemail@example.com',
      publicKey: process.env.PUBLIC_KEY,
      privateKey: process.env.PRIVATE_KEY,
    },
  };
  try {
    const res2 = await webPush.sendNotification (
      newSubscription,
      JSON.stringify ({
        title: 'Hello from server',
        description: 'this message is coming from the server',
        image: 'https://cdn2.vectorstock.com/i/thumb-large/94/66/emoji-smile-icon-symbol-smiley-face-vector-26119466.jpg',
      }),
      options
    );
    res.sendStatus(200)
  } catch (error) {
    console.log (error);
    res.sendStatus (500);
  }
});

The above code first takes the subscription information contained in the request body, stores it in the database and feeds it to the send notification in the web-push library. This method takes three parameters. The subscription is followed by the message: a string (here, we passed an object that has been converted to a string using the JSON.stringify global method) followed by the options object.

Here’s what’s happening under the hood.

  1. The sendNotification method encrypts the message (string) we passed in using the subscription keys.
  2. It then creates and signs a JWT using the VAPID private key and places the JWT in the authorization header.
  3. It then adds the message as the payload.
  4. And lastly, it forwards it to push service, which then delivers it to the client device.

Displaying the Notification

We now head over to the client side to display the notification on the user’s device. Open the sw.js file we created earlier and update it with the following code.

this.addEventListener ('activate', function (event) {
  ................
});
this.addEventListener ('push', async function (event) {
  const message = await event.data.json ();
  let {title, description, image} = message;
  console.log ({message});
  await event.waitUntil (
    this.registration.showNotification (title, {
      body: description,
      icon: image,
      actions: [
        {
          title: 'say hi',
        },
      ],
    })
  );
});

In the above code, we first get the push message from the event object, store it in the message variable, then get its contents by destructuring—finally displaying the notification using the showNotification method. In this method, we describe how the notification will look by defining the title and the contents. See more customizations for notifications.

Conclusion

In this article, we went over the core fundamentals required before a push notification can be sent using the web push protocol. Hopefully, this provides you with a solid basis to implement it in future projects.


Next up: Learn more about JWTs and how to implement them with Node.js.


About the Author

Chinedu Imoh

Chinedu is a tech enthusiast focused on full-stack JavaScript and Infrastructure engineering.

Related Posts