Implement simple web push notifications as one of the channels to engage users.

Mohammad Hasan Muktasyim Billah
Blibli.com Tech Blog
8 min readJan 3, 2023
Photo by Volodymyr Hryshchenko on Unsplash

A notification contains a small piece of information from a website. By using web notifications, we can display important information at a certain time. The appearance of notifications depends on the platform and browser used.

Push notification is a combination of using push message API and notification API. With web push notifications we can send information even when the user does not open the website so this will increase user engagement for certain important information or pages.

Things needed to implement web push notifications are:

  • a website that implements service workers to listen to any push messages and manage the subscription.
  • a notification sender server. initially, chrome and Mozilla have different push services. so to support both browsers, we need to implement VAPID in the server.
notification example in google chrome

Asking for Permission and Managing Subscription

We will be using Notification API to display a notification. But first, To be able to receive Web notifications the user must agree and grant permission. We can use Notification.requestPermission() to ask the user for permission. Initially, the permission value is “default”. if the user clicks “Allow” then the permission status will change to “granted”. but when the user refuses, the status will change to “denied”.

example of browser permission prompt

This process is very easy to implement, but when should we ask for user permissions?

Timing is one of the important factors that determine permission approval. In general, the user will refuse the permission request if it is requested during the first time load without knowing the context of its use.

For example, the user would be more likely to grant permission to calendar web notifications when the user adds a new event. Likewise in the maps application, users will tend to give location permissions when users need route information to a location.

So we come to the first rule about permission requests, it’s better to ask it at the right time and we shouldn’t spam users with it.

Currently in most browsers, once the user refuses to enable notifications, we are unable to show the prompt again. In this case, Permissions can only be changed via Browser settings (which will be very rarely done by the user). Therefore, we can make a custom permission prompt request before displaying the browser prompt. This way we have more control and can show the custom prompt multiple times even if the user declines the prompt the first time it’s asked. But remember, we shouldn’t spam users, so we need to add a delay before showing this custom prompt for the second time.

example of a custom permission prompt

When the user approves the permissions, we can use the Push API to create a subscription object. This object contains URL information from the push service to send notifications to the browser. This push service URL is specific to each browser (for example, Mozilla and Chrome have different push services) and browser sessions (different chrome profiles will use different subscriptions objects). We can save the data on a server.

If permission has been previously approved, we can also update subscription data. This is necessary because subscription data changes may occur in the push service.

navigator.serviceWorker.ready.then(reg => {
reg.pushManager.getSubscription().then(function (sub) {
// check if subscription is not exist and the process is for initial generation
if (!sub && !onlyRefreshToken) {
// ask for permission
Notification.requestPermission()

//generate subscription object
reg.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: applicationServerKey
}).then(function (subscribe) {
// send subscription to server to be saved
// parse string version of the json to get the expected object structure
sendSubscriptionFunction(JSON.parse(JSON.stringify(subscribe)))
}).catch(e => {
console.log(e)
})
}
// check if subscription is not exist but the permission already granted
else if (!sub && onlyRefreshToken && isPermissionGranted) {
//re-generate subscription object
reg.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: applicationServerKey
}).then(function (subscribe) {
// send subscription to server to be saved
// parse string version of the json to get the expected object structure
sendSubscriptionFunction(JSON.parse(JSON.stringify(subscribe)))
}).catch(e => {
console.log(e)
})
}
// check if subscription is already exist
else if (!!sub) {
// send subscription to server to be saved
// parse string version of the json to get the expected object structure
sendSubscriptionFunction(JSON.parse(JSON.stringify(sub)))
}
})
})

This process can be executed when the user grants the permissions on our custom request as well as the first time the app is run (onlyRefreshToken is true). While creating the subscription object we also need the applicationServerKey which is the public key of our server which will send notifications. This key will be discussed in the next section.

Publish the Notification

To be able to send notifications to different push services, we need a server that has implemented VAPID (Voluntary Application Server Identification). VAPID is a method that allows push services to identify whether notifications received are sent from authorized servers or not.

To make it easier to implement VAPID and send web push notifications, we can use this WebPush(JAVA) library (in this example we will use JAVA based server). The web push library is also available in several languages :

After installing the dependency library, we need to add BouncyCastle to the security provider and then we can create the PushService instance or bean.

@Bean
public PushService pushService() {
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
Security.addProvider(new BouncyCastleProvider());
}
try {
return new PushService(publicKey, privateKey, notificationSubject);
} catch (Exception e) {
e.printStackTrace();
}
return new PushService();
}

This code will create a simple bean of the PushService. When creating instance of the PushService we include the public and private keys of the server as the server self-identification signature. This public key will also be used on the website when creating the subscription object. the notificationSubject is a contact URI for the server as either “mailto:”(email) or an “https:” Uri. You can generate the key using the provided CLI or use this website.

public HttpResponse sendPushMessage(Subscription subscription, String payload) {
try {
HttpResponse response =
pushService.send(new Notification(subscription, payload), Encoding.AES128GCM);
log.warn("code=send-web-push-notification | endpoint={} | payload={} | status={}",
subscription.endpoint, payload, response.getStatusLine().getStatusCode());
return response;
} catch (IOException | JoseException | ExecutionException | InterruptedException e) {
log.warn("code=send-web-push-notification-failed | endpoint={} | payload={} | error={}",
subscription.endpoint, payload, e);
}
return null;
}

This method is a simple method to publish messages to the push service. We will use the subscription data we got from the web (that contains the endpoint, p256dh key, and auth key). We can send simple strings such as “Hallo” or even a string JSON. I prefer to send a JSON string payload so that I can customize all the notification part such as title, body, and button action. This is an example of the payload structure we can use

{
"title" : "title",
"body" : "body",
"tag" : "123",
"clickUrl" : "https://dasdsad"
}

Displays Notifications

To receive notifications we need to listen to push events on the service worker.

//listen to any push event
self.addEventListener('push', function (event) {
let payload
try {
// parse JSON string to an object
payload = JSON.parse(event.data.text())
} catch (err) {
console.log(err)
// if error in parsing we can set default value to the payload
payload = {
title: 'New Updates Arrives!',
body: 'We Got Something for You!',
clickUrl: ''
}
}
const title = payload.title
// this option is used to modify the notification
const options = {
// this is the notification body
body: payload.body || 'New Updates Arrives!',
// notification icon
icon: '/favicon.png',
// badge icon
badge: '/favicon.png',
//custom data used when handling the event in the notification (click or others)
data: {
tag: payload.tag, // allows us to identify notification
clickActionUrl: self.location.origin + (payload.clickUrl ? payload.clickUrl : '/announcements'),
},
// notification action button
actions: [
{
//action label
action: 'explore',
// action title
title: 'Go to the site'
},
{
//action label
action: 'close',
// action title
title: 'No thank you'
}
],
// tag used to replace any simmilar notif
tag: payload.tag? payload.tag: null
}

// wait until notification displayed
event.waitUntil(self.registration.showNotification(title, options))
})

With the code above, the service worker will listen to every push message event sent by the push service. To display notifications we can use the showNotification method on the service worker by including the notification title and several options to customize the notification that will be displayed.

In the example above, if the JSON string parse process fails, we will be able to display a notification with the default template so that there is still engagement for the user to open the announcement page (which will contain the entire list of existing announcements). The tag on the options object is used to prevent displaying pop-ups with the same tag code. If a notification with the same tag is sent to the same client for some reason, the notification will be updated and not display a pop-up for the second time if the previous notification is not closed via an action or close icon in the OS notification center. We can adjust the code above to suit the needs and use cases of our website.

notification example in Mozilla

As can be seen in the options object there are also actions that contain buttons that will be displayed in the notification (may not be displayed depending on the browser used). In order to process the click from the action, we need to modify the service worker and add a handler for the click event.

//listen to any notification click
self.addEventListener('notificationclick', function (event) {
var notification = event.notification
var action = event.action
//handle if action is close
if (action === 'close') {
notification.close()
}
//handle for other action
else {
self.clients.openWindow(event.notification.data.clickActionUrl)
}
})

the code above will add a new event listener for all notificationClick. If the received event is a close action then we need to close the notification and handle other actions to open new windows.

Finally, our simple notification is ready to be used. Here are some of the things you need to keep in mind while using web push notifications.

  • Make sure you ask the user permission at the right time
  • Avoid spamming users with notifications or permission prompts.
  • Use tags in the notification
  • Keep the notification body to be as simple and as clear as possible since the char number will be limited in some browsers

--

--