Overview

This codelab walks through building a complete push notification client. When you finish, your client will be able to subscribe a user to push notifications, receive and display pushed messages, and unsubscribe the user when they no longer want notifications.

The server side is already implemented for you. Your work is confined to the client code. If you need to build the server side too, work through Codelab: Build a push notification server first. For concepts, see How push works.

Tested environments

The codelab is verified against these platforms:

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

It does not work with the following:

  • macOS: Brave, Edge, Safari
  • iOS

Project setup

First, click Remix to Edit to make the project editable.

Authentication keys

Servers and clients authenticate with each other using VAPID keys. The rationale is explained in Sign your web push protocol requests. Store secrets in a .env file like this:

VAPID_PUBLIC_KEY="BKiwTvD9HA…"
VAPID_PRIVATE_KEY="4mXG9jBUaU…"
VAPID_SUBJECT="mailto:[email protected]"

Then, open public/index.js and replace VAPID_PUBLIC_KEY_VALUE_HERE with your actual public key.

Registering a service worker

A service worker is mandatory for receiving and showing notifications. Registering it should happen as early as possible.

Replace the // TODO add startup logic here comment with:

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);

Open the DevTools Console in Chrome and confirm you see Service worker was registered.

Requesting permission

Do not request push notification permission when the page loads. Instead, your interface should give users an explicit control, like a button. When they click it, you begin the formal browser permission flow.

In subscribeButtonHandler(), replace the // TODO comment with:

// 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.');
}

Click Subscribe to push in the app tab. Your browser (or OS) will prompt for permission. Accept the prompt. The Console logs whether your request was granted or denied.

Subscribing

Subscription means interacting with a push service—a browser-vendor-controlled web service. Once the browser gives you subscription information, you send it to your server for long-term database storage.

Add the code below (also marked visually in the source listing) 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)
});

Set userVisibleOnly to true. Silent pushes (messages with no visible notification) may be possible one day, but browsers do not permit them now for privacy reasons. The applicationServerKey option is a base64 string converted to a Uint8Array by the urlBase64ToUint8Array utility, and it authenticates your server with the push service.

Unsubscribing

Your UI must also let users opt out later. Address the // TODO comment in unsubscribeButtonHandler():

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;
}

Receiving and displaying messages

Your service worker handles incoming messages and turns them into notifications you can see.

In public/service-worker.js, replace the // TODO comment inside the push event handler:

let data = event.data.json();
const image = 'logo.png';
const options = {
  body: data.options.body,
  icon: image
}
self.registration.showNotification(
  data.title,
  options
);

Return to the app tab and click Notify me to trigger a notification. To verify receiving in other supported browsers, repeat the subscription flow in each and click Notify all. All should display the same push.

Find all visual customization options in the documentation on ServiceWorkerRegistration.showNotification()

.

Opening a URL from a notification click

Notifications become useful when the click re-engages the user by opening your site. That requires further service worker configuration.

Deal with the // TODO comment in the service worker's notificationclick event handler:

event.notification.close();
event.waitUntil(self.clients.openWindow('https://web.dev'));

Test with a fresh notification in the app tab—clicking it should open https://web.dev in a new tab.

Going further