Web Push Notifications, a Proof-of-Concept

By Doug Kumagai

Doug Kumagai
Antique luggages with labels
Photo by Nick Fewings on Unsplash

In response to COVID-19 work on traveler notifications, Egencia® setup an internal proof-of-concept on web push notifications.

Image for post

Push Notifications on the Web

You may have seen this functionality on websites even if you never used it:

A notification permission prompt
Please don’t ambush the user on page load

Keep in mind Safari® does not support web push notifications but all other browsers do.

Showing a notification

The first step requires that a site:

1. Has HTTPS

2. Has a service worker

3. Has been granted permissions by the user

Note that in many browsers you only get one shot to prompt for permission. If you prompt and the user says no, you don’t get to prompt again. Furthermore, for browsers like Chrome™️, if too many users say no you may get a slightly different UX that is less attention catching. Notifications can be sent without a push server, for example if you background poll or use a timer you can send a notification. This could be useful if the user has another tab in front of yours or for an alarm style notification.

Getting a subscription

The second step involves requesting a subscription from the browser using a protocol called VAPID (Voluntary Application Server Identification). This is like setting up a mailbox with the browser vendor for this website. You need to generate a public/private key pair and then call subscribe on the service worker’s push manager:

const subscription = await serviceWorker.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: applicationPushPublicKey
});

userVisibleOnly must always be true. It means that the user must get a visible alert when you’ve pushed something. There is no support for non-visible notifications.

The subscription object contains a url (this is what makes it browser agnostic) and a key. You then store this key in your backend somewhere as this will be what you use to send the push notification.

An example subscription object:

{
"endpoint": "https://fcm.googleapis.com/fcm/send/dKT-qrQhi1g:APA91bGLPAc2ZGrZlh0Sb5whT5P8Pap-qwCLc_n2c0SAZMNhOn8vXVcDZN02Y4DW_bUwfnWYFaYAZgSuH1zqdx9IKC7PKUZh2QWWtgvIX3NYIpnNY0JCQA7JGahTtYsIDdzDJgz3RjDl",
"expirationTime": null,
"keys": {
"p256dh": "BCDDjqcwtlKlG0HNpgcVrXy4Fka1z69FxqMyGoGcaCg_6BuOCPrjnGFrGRGyJ5SEZU-fJ-sPp3XHzMfXa-qxaNA",
"auth": "34v4Zx_xTnpRde0jPAGTVQ"
}
}

There are multiple libraries to do web push notifications, in our case we used the web-push package for node. It is used to send small data payloads to the browser push service which then relays them to the user’s browser. The details are a lot of elliptic curve cryptography to assure that messages cannot be eavesdropped (they are encrypted with a unique key given by the browser) or forged (signed with the application private key, the counterpart to the public key passed to subscribe). We don’t need to worry about all that though, we just need the payload and the subscription object and let the library do the rest.

await webpush.sendNotification(subscription, message);

Registering a push handler

Once sent, the webpage needs to know what to do with it. The push triggers a “notification” event in the service worker, to which you can subscribe.

//service-worker.jsself.addEventListener('push', event => {
event.waitUntil(
self.registration.showNotification("Egencia Says:", {
body: event.data.text(),
icon: "images/icon-180x180.png",
badge: "images/badge-egencia.png",
actions: [{
action: "show",
title: "Show Me More"
}]
})
);
});

From here, you can send a notification, but web push has many other use cases. You could make an ajax request or trigger a background download. Since this happens in the service worker, the user doesn’t need to have the site or browser open, it’ll appear like a normal notification on their lock screen when they get it. We can set action buttons on the notification to do different things like take the user to a specific url or perform an action on the site. These are differentiated with the “action” tag.

A notification from localhost with the text “Hello!”
A notification from Chrome on macos
Android top bar with an icon in the left shaped like the Egencia bird
Notification in the Android notification tray
Android notification pulldown view with message from Egencia saying “A travel warning has been issued for your location”
A notification on Android with branded icon

We can also add branding elements such as the icon that appears with the push notification, badges (the grey mark shown here and in the notification tray), or images to accompany the icon. You can even control the vibration pattern just like a native app!

In order to respond to the action we listen to the “notificationclick” event:

//service-worker.jsself.addEventListener('notificationclick', event => {
event.notification.close();
switch (event.action) {
case "show": {
const url = "https://www.egencia.com/help-center/article/123";
const promise = clients.matchAll({
type: 'window',
includeUncontrolled: true
})
.then(windowClients => {
const matchingClient = windowClients.find(wc =>
wc.url == urlToOpen);

return matchingClient
? matchingClient.focus();
: clients.openWindow(urlToOpen);
});
event.waitUntil(promise);
}
//other actions ...
}
});

In this snippet we respond to the “show” action by looking up all windows (only ones matching our domain will be found) including ones the service worker does not control. If the user was already browsing our article, we simply focus on that window, if not then we open a new window to that article page.

Image for post

What next?

This is a proof-of-concept and very basic. An actual implementation would require integration with other notifications types like SMS, email and native app push. It’s best practice to give users a choice of how to consume their notifications.

A screenshot from Facebook showing different types of notifications (activity, reminders etc) set to SMS, Push or both.
Facebook Notification Settings

Sites like Facebook™️ give users options for where notifications go. This can help the user avoid duplicate notifications when subscribed to other channels.

When to ask for permission would also require thoughtful design for it to have the best contextual relevance to users. Users should understand why they would like push notifications and how they benefit them. Another common tactic is to use a fake prompt, this insulates against the one-prompt limit by allowing users to say “no” to in-app UX before it’s turned off for good.

A screenshot of Business Insider using an fake notification permission prompt with some context text.
A fake notification prompt that provides more context and the ability to reprompt later

By the way, if you want a spammy website to stop asking for notifications (especially on page load), try clicking allow on the fake prompt and then disallow on the native prompt.

Image for post

Do end users need web push notifications?

Web push is a big opportunity for folks who have not installed our native app. They might never install it due to space requirements, infrequent use or lack of compatibility. Web push provides a viable channel for alerting them. By giving users more choices we allow them to get more use out of our product.

If you’d like to know more, the web push book is a fantastic resource that goes into much greater depth: https://web-push-book.gauntface.com/