Building a PWA in Vanilla JavaScript - Part 2: Push API

Jack Misteli

This is the second part of a three-part series about creating an app that leverages the Web Push API and cron-schedule.

In this installment, we’ll cover the Web Push API aspect of the app.

What are we Building

We are going to build a simple progressive web app (PWA) which will remind me to take my pills every day 💊💊💊.

Our app will have a web server powered by Express.js. Express will push notifications to the clients which subscribed to the push notifications. It will also serve the frontend.

What We’ve Built So Far

So far, we created an installable Progressive Web App (PWA) and an Express.js Server to serve our static files on http://localhost:3000. You can head over to Part 1 here to read all about how we got there.

How the Push API works

This part can be a bit scary so you can skip it if you’re just looking for some simple code snippets. Here’s a little chronology (you don’t have to follow these steps in this exact order).

  1. We ask our user for her permission to send her some notifications.
  2. The browser registers a service worker.
  3. The browser fetches a public key from our Express server.
  4. The service worker has an interface called pushManager. We pass the public key to pushManager's subscribe function which creates a new subscription object.

Note: The subscription object has an endpoint key which is the URL from which the push notification will be sent. Only the browser can generate these URLs. It also contains some authentication information.

{
  "endpoint": "https://fcm.googleapis.com/fcm/send/cyMiHqakiZE:APA91bFUIYbrvtnSGw0dw ...."
  "expirationTime": null
  "keys":{
    // Receive key
    "p256dh":"BJG5rNdalnpu6yuRSuly3H221ljDVYRvDk...",
    // Auth Key
    "auth": "zG9-yhkAzIdknhMW0d89Aw"
  }
}
  1. We send our subscription object to our Express server and store it.
  2. Now that we have the key on our server we do some very interesting encryption magic with our server’s private key (the one which matches the public key in step 3). Then we send our encrypted message to the endpoint.

If you want to learn more about the encryption process I highly recommend reading about it in this MDN article.

  1. The push server (managed by Apple, Mozilla, Google,…), will analyze your request, and if it’s valid send the push notification to our application.
  2. Our service worker will catch the notification in the 'push' event listener. And then we can do whatever we want with that information.

Phewww finally done. This is all we need to know for the purpose of this article. Let's code now!

Subscribing to a Push Notification in Javascript

Here’s how we subscribe on the client:

(async () => {
  if('serviceWorker' in navigator) {
    // We first get the registration
    const registration = await navigator.serviceWorker.ready;
    // Asking for the subscription object
    let subscription = await registration.pushManager.getSubscription();

    // If we don't have a subscription we have to create and register it!
    if (!subscription) {
      subscription = await subscribe(registration);
    }
    // Implementing an unsubscribe button
    document.getElementById('unsubscribe').onclick = () => unsubscribe();
  }
})()

// We use this function to subscribe to our push notifications
// As soon as you run this code once, it shouldn't run again if the initial subscription went well
// Except if you clear your storage
const subscribe = async (registration) => {
  // First get a public key from our Express server
  const response = await fetch('/vapid-public-key');
  const body = await response.json();
  const publicKey = body.publicKey;

  // this is an annoying part of the process we have to turn our public key
  // into a Uint8Array
  const Uint8ArrayPublicKey = urlBase64ToUint8Array(publicKey);

  // registering a new subscription to our service worker's Push manager
  const subscription = await registration.pushManager.subscribe({
    // don't worry about the userVisible only atm
    userVisibleOnly: true,
    applicationServerKey: Uint8ArrayPublicKey
  });

  // Sending the subscription object to our Express server
  await fetch('/subscribe',
    {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(subscription.toJSON())
    }
  );
  return subscription;
};

// Let's create an unsubscribe function as well
const unsubscribe = async () => {
  const registration = await navigator.serviceWorker.ready;
  const subscription = await registration.pushManager.getSubscription();

  // This tells our browser that we want to unsubscribe
  await subscription.unsubscribe();

  // This tells our Express server that we want to unsubscribe
  await fetch("/unsubscribe", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(subscription.toJSON())
  });
  writeSubscriptionStatus("Unsubscribed");
};

// This simply shows our user that they are unsubscribed
const writeSubscriptionStatus = subscriptionStatus => {
  document.getElementById("status").innerHTML = subscriptionStatus;
};

// I have found this code (or variations of) from; multiple sources
// but I could not find the original author
// here's one such source:
// https://stackoverflow.com/questions/42362235/web-pushnotification-unauthorizedregistration-or-gone-or-unauthorized-sub
const urlBase64ToUint8Array = (base64String) => {
  const padding = '='.repeat((4 - base64String.length % 4) % 4);
  const base64 = (base64String + padding)
    .replace(/\-/g, '+')
    .replace(/_/g, '/');

  const rawData = window.atob(base64);
  const outputArray = new Uint8Array(rawData.length);

  for (let i = 0; i < rawData.length; ++i) {
    outputArray[i] = rawData.charCodeAt(i);
  }
  return outputArray;
};

Setting up Push Notifications on our Express Server

There is a lot of complex encryption going on our server. So we are going to use a library called web-push to take care of it for us:

$ npm install --save web-push
# or:
$ yarn add web-push

Module: app.js

// ... See previous article for the rest of the code
const bodyParser = require('body-parser');

const webPush = require('web-push');

// for this application we use allSubscription object to manage our subscriptions
// this is NOT GOOD for production since it makes our application stateful
const allSubscriptions = {};

// We use webpush to generate our public and private keys
const { publicKey, privateKey } = webPush.generateVAPIDKeys();

// We are giving webpush the required information to encrypt our data
webPush.setVapidDetails(
  'https://jmisteli.com',
  publicKey,
  privateKey
);

// bodyParse.json() allows us to easily read json bodies
app.use(bodyParser.json());

// Send our public key to the client
app.get('/vapid-public-key', (req, res) => res.send({ publicKey }));

// Allows our client to subscribe
app.post('/subscribe', (req, res) => {
  const subscription = req.body;
  registerTasks(subscription);
  res.send('subscribed!');
});

const registerTasks = (subscription) => {
  const endpoint = subscription.endpoint;

  // the endpoints are the keys of our subscriptions object
  // Every 3 seconds we will send a notification with the message 'hey this is a push!'
  const intervalID = setInterval(()=>{
      sendNotification(subscription, 'Hey this is a push!')
  }, 3000);
  allSubscriptions[endpoint] = intervalID;
}

// Allows our client to unsubscribe
app.post('/unsubscribe', (req, res) => {
  const endpoint = req.body.endpoint;
  // We find the client's endpoint and clear the interval associated with it
  const intervalID = allSubscriptions[endpoint];
  clearInterval(intervalID);
  // We delete the key
  delete allSubscriptions[endpoint];
});

// This function takes a subscription object and a payload as an argument
// It will try to encrypt the payload
// then attempt to send a notification via the subscription's endpoint
const sendNotification = async (subscription, payload) => {
  // This means we won't resend a notification if the client is offline
  const options = {
    TTL: 0
  };

  if (!subscription.keys) {
    payload = payload || null;
  }

  // web-push's sendNotification function does all the work for us
  try {
    const res = await webPush.sendNotification(subscription, payload, options);
    console.log(res, 'sent!');
  } catch (e) {
    console.log('error sending', e);
  }
}

Push Notification Service worker

And finally here’s how we listen for the push even in the service worker on the client:

Module: /public/sw.js

self.addEventListener('push', function (e) {
  const message = e.data;

  // The notificationOptions will shape the look and behavior of our notification
  const notificationOptions = {
    body: `Time is the message: ${message}`,
    // we use the images from the PWA generator we made
    icon: '/images/icons/icon-512x512.png',
    vibrate: [100, 50, 100],
    data: {
      dateOfArrival: Date.now(),
      primaryKey: '2'
    },
    actions: [
      {
        action: 'close',
        title: 'Close'
      }
    ]
  };
  e.waitUntil(
    // We use the service worker's registration `showNotification` function to display the Notification
    self.registration.showNotification('💊💊 You got notified! 💊💊', notificationOptions)
  );
});

ScreenShot of successful notification

For more details about how the Notifications API work check out Going Native with the Web Notifications API.

We are finally done for this section. I realize this is a big one so if you have any questions or comments please send us a message on Twitter!

  Tweet It

🕵 Search Results

🔎 Searching...