Implementing Web Push Notifications

Long gone are the days of push notifications being an exclusive feature of native mobile applications. With the advent of the Push API, web browsers now have the ability to receive notifications as well. Users can even receive notifications when they don’t have your website open. We have recently implemented support on meetme.com.

What is the current browser support?

Web push works on both desktop and mobile; however, not all browsers support the standard yet.

  • Chome 50+ (both desktop and mobile Android)
  • Chrome 44 introduced partial support but notification payloads were not supported
  • Firefox 44+ desktop, Firefox 48+ mobile Android
  • Safari does NOT support web push (although the desktop version supports Apple’s own proprietary protocol for push)
  • Coming soon to Microsoft Edge

High level overview

From the perspective of a user, web push notifications work largely the same as on native mobile. However, before a user can start receiving push notifications they must accept permissions for your website. When a user arrives at a website that supports web push (and they are using a browser that supports it), the website can then prompt the user to enable notifications:

Chrome Desktop
Chrome desktop notifications prompt

From this point forward, whenever the website has a notification it will make a REST call to the push provider (more on this below), which will in turn generate the notification on the user’s device. Note that the user does not need to have the website open in order to receive notifications.

Notification
Chrome desktop notification

Behind the scenes the website needs to implement a service worker which will handle receiving the push event and generating a notification UI element.

Components of Web Push

Service Workers

A service worker runs in the background separately from your web application and is required in order to receive push notifications. It has no direct access to the DOM and any communication with your web application must occur using postMessage() to send and receive messages. Some other aspects of service workers include:

  • Can be stopped/restarted by the browser as necessary (you cannot depend on global state)
  • Has access to the IndexedDB API
  • Does NOT have access to LocalStorage or cookies
  • Receives the actual push notification message
  • Generates the visible notification message and handles the click

Use of Promises

The API for service workers relies heavily on promises, so you will need to have some familiarity with them. In a nutshell, promises are designed to make working with asynchronous code easier by wrapping success and error callbacks in chainable functions to allow for greater readability. Although this article is not going to give a tutorial on promises, some good documentation can be found here. One thing that should be noted, however, is that if you are accustomed to using promises in jQuery or Q, the interface for JavaScript ES6 Promises is slightly different. For example, here is how you might use a promise in jQuery:

The preceding code will wait for 5 seconds and then print “Wait Complete!” to the JavaScript console. Let’s look at the equivalent using ES6 Promises:

As you can see, the concept is the same but the syntax is slightly different. It should also be noted that, unlike jQuery, an ES6 Promise does not have a done() method.

How is a service worker used?

A service worker needs to be registered; however, we must first check for browser support.

Once our service worker is registered we will need to subscribe via the PushManager in order to start receiving push notifications.

For users who have not previously enabled notifications a prompt will appear.

Chrome android prompt
Chrome Android notifications prompt

Note that if you want to check for the existence of a push subscription prior to invoking the prompt, you can do so by calling getSubscription() on the PushManager instance instead. If the Promise it returns resolves to a null value then there is no existing subscription. Calling getSubscription() first can be useful if you want to display your own custom message to better inform the user of the enable notifications prompt they are about to see.

It’s possible to determine if the user has pressed cancel on the “enable notifications” prompt if you call subscribe(); the error thrown will have a “name” property of “PermissionDeniedError”.

You may have noticed that when we called subscribe() a “userVisibleOnly” property was specified as true. This is currently required in Chrome. The option indicates that for every push notification received we will show a notification. If you were to not generate a visible notification when a push message was received then the browser will generate a default notification with the text “This site has been updated in the background.” This can also occur if you attempt to perform an asynchronous operation prior to showing the notification without returning a promise.

In our handlePushSubscription() function, we will receive a single argument of type PushSubscription. This contains the data that is required in order for our backend to generate push notifications. The data that PushSubscription contains is a bit different from what you may be used to if you’ve worked with native mobile push notifications (which generate a token hash string). Here is an example of how it is formatted if we stringify the PushSubscription object:

The “keys” portion is what is used to encrypt the payload of the push message. There are libraries that can handle this portion for you (we’ll show an example later on).

The “endpoint” property is unique and is used to identify your browser. Endpoints take the following form (where “unique-key” is a key that identifies the user’s specific browser):

Chrome
https://android.googleapis.com/gcm/send/unique-key

FireFox
https://updates.push.services.mozilla.com/wpush/v1/unique-key

Now let’s define our “handlePushSubscription” function. Its job is going to be informing our backend of the push subscription (which can then be used to generate notifications to the browser itself).

The URL endpoint used above will need to store our web push subscription data (and associate it with the current user).

Generating Push Notifications on our backend

When we have a notification that we would like to send to our user, we will need to encrypt the notification’s payload and make a POST request to the endpoint that we received. To accomplish this, we’re going to use Node.js and the excellent web-push package.

Chrome requires some additional setup steps such as defining an application manifest with a “gcm_sender_id” (for more details see here).

Handling Push Notifications in our Service Worker

We’re now going to setup the “serviceWorker.js” that we referenced earlier. This script is going to run in the background listening for notifications.

There are other useful events that we can listen for in a service worker (see a more thorough guide here). The “push” event is fired when a push notification is received by the browser. You do not need to have a tab open (of the web application) to receive a notification. Let’s define our event handlers:

Note that the notification click handler above will open a new browser tab regardless of whether or not one is already open (there are ways around this by finding an existing client tab using clients.matchAll(), sending it a message via postMessage(), and then handling the message in the web application).

Conclusion

Browser adoption of the push standard has taken an important step towards moving the web platform forward. Although the discussion of service workers in this article was limited to push notifications, service workers can be used for other purposes such as adding offline functionality. A full example of the code used here has been posted on Github.

Open Talk: Making Sense of JavaScript Using Backbone & RequireJS, May 27th

Making Sense of JavaScript Using Backbone & Requirejs

JavaScript can often feel like a free-for-all. The past few years have brought us amazing tools such as jQuery, Backbone, Requirejs, and more, which we can leverage to bring some sanity to our front-end applications.

This talk will set you on a path you may not have thought possible with JavaScript – clean, well-organized code, separated into views, data models, and other modules where dependencies are clearly defined and laid out in front of you. You don’t need to be a JavaScript “ninja” to start kicking serious butt on the front-end. Just an hour or two of dedicated training.”

About the presenters

Corky Brown – Corky has been doing web development for nearly a decade and is closing in on his eighth year at MeetMe. During his career, he’s worked on both the front and back-ends of web applications, but has in recent years focused primarily on building JavaScript applications using tools such as Backbone, RequireJS, and Bootstrap.

RSVP here.