Codelab: Build a push notification client

Kate Jeffreys
Kate Jeffreys

This codelab shows you, step-by-step, how to build a push notification client. By the end of the codelab you'll have a client that:

  • Subscribes the user to push notifications.
  • Receives push messages and displays them as notifications.
  • Unsubscribes the user from push notifications.

This codelab is focused on helping you learn by doing and doesn't talk about concepts much. Check out How do push notifications work? to learn about push notification concepts.

The server code of this codelab is already complete. You'll only be implementing the client in this codelab. To learn how to implement a push notification server, check out Codelab: Build a push notification server.

Check out push-notifications-client-codelab-complete (source) to see the complete code.

Browser compatibility

This codelab is known to work with the following operating system and browser combinations:

  • Windows: Chrome, Edge
  • macOS: Chrome, Firefox
  • Android: Chrome, Firefox

This codelab is known to not work with the following operating systems (or operating system and browser combinations):

  • macOS: Brave, Edge, Safari
  • iOS

Setup

Get an editable copy of the code

The code editor that you see to the right of these instructions will be called the Glitch UI throughout this codelab.

  1. Click Remix to Edit to make the project editable.

Set up authentication

Before you can get push notifications working, you need to set up your server and client with authentication keys. See Sign your web push protocol requests to learn why.

  1. In the Glitch UI click Tools and then click Terminal to open the Glitch Terminal.
  2. In the Glitch Terminal, run npx web-push generate-vapid-keys. Copy the private key and public key values.
  3. In the Glitch UI open .env and update VAPID_PUBLIC_KEY and VAPID_PRIVATE_KEY. Set VAPID_SUBJECT to mailto:test@test.test. All of these values should be wrapped in double quotes. After making your updates, your .env file should look similar to this:
VAPID_PUBLIC_KEY="BKiwTvD9HA…"
VAPID_PRIVATE_KEY="4mXG9jBUaU…"
VAPID_SUBJECT="mailto:test@test.test"
  1. Close the Glitch Terminal.
  1. Open public/index.js.
  2. Replace VAPID_PUBLIC_KEY_VALUE_HERE with the value of your public key.

Register a service worker

Your client will eventually need a service worker to receive and display notifications. It's best to register the service worker as early as possible. See Receive and display the pushed messages as notifications for more context.

  1. Replace the // TODO add startup logic here comment with the following code:
// TODO add startup logic here
if ('serviceWorker' in navigator && 'PushManager' in window) {
  navigator.serviceWorker.register('./service-worker.js').then(serviceWorkerRegistration => {
    console.info('Service worker was registered.');
    console.info({serviceWorkerRegistration});
  }).catch(error => {
    console.error('An error occurred while registering the service worker.');
    console.error(error);
  });
  subscribeButton.disabled = false;
} else {
  console.error('Browser does not support service workers or push messages.');
}

subscribeButton.addEventListener('click', subscribeButtonHandler);
unsubscribeButton.addEventListener('click', unsubscribeButtonHandler);
  1. To preview the site, press View App. Then press Fullscreen fullscreen.
  1. Press `Control+Shift+J` (or `Command+Option+J` on Mac) to open DevTools.
  2. Click the Console tab. You should see the message Service worker was registered. logged to the Console.

Request push notification permission

You should never request permission to send push notifications on page load. Instead, your UI should ask the user if they want to receive push notifications. Once they explicitly confirm (with a button click, for example) then you can start the formal process for getting push notification permission from the browser.

  1. In the Glitch UI click View Source to return to your code.
  2. In public/index.js replace the // TODO comment in subscribeButtonHandler() with the following code:
// TODO
// Prevent the user from clicking the subscribe button multiple times.
subscribeButton.disabled = true;
const result = await Notification.requestPermission();
if (result === 'denied') {
  console.error('The user explicitly denied the permission request.');
  return;
}
if (result === 'granted') {
  console.info('The user accepted the permission request.');
}
  1. Go back to the app tab and click Subscribe to push. Your browser or operating system will probably ask you if you want to let the website send you push notifications. Click Allow (or whatever equivalent phrase your browser/OS uses). In the Console you should see a message indicating whether the request was accepted or denied.

Subscribe to push notifications

The subscription process involves interacting with a web service controlled by the browser vendor that's called a push service. Once you get the push notification subscription information you need to send it to a server and have the server store it in a database long-term. See Subscribe the client to push notifications for more context about the subscription process.

  1. Add the following highlighted code to subscribeButtonHandler():
subscribeButton.disabled = true;
const result = await Notification.requestPermission();
if (result === 'denied') {
  console.error('The user explicitly denied the permission request.');
  return;
}
if (result === 'granted') {
  console.info('The user accepted the permission request.');
}
const registration = await navigator.serviceWorker.getRegistration();
const subscribed = await registration.pushManager.getSubscription();
if (subscribed) {
  console.info('User is already subscribed.');
  notifyMeButton.disabled = false;
  unsubscribeButton.disabled = false;
  return;
}
const subscription = await registration.pushManager.subscribe({
  userVisibleOnly: true,
  applicationServerKey: urlB64ToUint8Array(VAPID_PUBLIC_KEY)
});
notifyMeButton.disabled = false;
fetch('/add-subscription', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify(subscription)
});

The userVisibleOnly option must be true. It may one day be possible to push messages without displaying user-visible notifications (silent pushes) but browsers currently don't allow that capability because of privacy concerns.

The applicationServerKey value relies on a utility function that converts a base64 string to a Uint8Array. This value is used for authentication between your server and the push service.

Unsubscribe from push notifications

After a user has subscribed to push notifications, your UI needs to provide a way to unsubscribe in case the user changes their mind and no longer wants to receive push notifications.

  1. Replace the // TODO comment in unsubscribeButtonHandler() with the following code:
// TODO
const registration = await navigator.serviceWorker.getRegistration();
const subscription = await registration.pushManager.getSubscription();
fetch('/remove-subscription', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({endpoint: subscription.endpoint})
});
const unsubscribed = await subscription.unsubscribe();
if (unsubscribed) {
  console.info('Successfully unsubscribed from push notifications.');
  unsubscribeButton.disabled = true;
  subscribeButton.disabled = false;
  notifyMeButton.disabled = true;
}

Receive a push message and display it as a notification

As mentioned before, you need a service worker to handle the receiving and displaying of messages that were pushed to the client from your server. See Receive and display the pushed messages as notifications for more detail.

  1. Open public/service-worker.js and replace the // TODO comment in the service worker's push event handler with the following code:
// TODO
let data = event.data.json();
const image = 'https://cdn.glitch.com/614286c9-b4fc-4303-a6a9-a4cef0601b74%2Flogo.png?v=1605150951230';
const options = {
  body: data.options.body,
  icon: image
}
self.registration.showNotification(
  data.title, 
  options
);
  1. Go back to the app tab.
  2. Click Notify me. You should receive a push notification.
  3. Try opening the URL of your app tab on other browsers (or even other devices), going through the subscription workflow, and then clicking Notify all. You should receive the same push notification on all of the browsers that you subscribed. Refer back to Browser compatibility to see a list of browser/OS combinations that are known to work or not work.

You can customize the notification in lots of ways. See the parameters of ServiceWorkerRegistration.showNotification() to learn more.

Open a URL when a user clicks a notification

In the real-world, you'll probably use the notification as a way to re-engage your user and prompt them to visit your site. To do that, you need to configure your service worker a bit more.

  1. Replace the // TODO comment in the service worker's notificationclick event handler with the following code:
// TODO
event.notification.close();
event.waitUntil(self.clients.openWindow('https://web.dev'));
  1. Go back to the app tab, send yourself another notification, and then click the notification. Your browser should open a new tab and load https://web.dev.

Next steps