Browser push notifications using JavaScript

%image_alt%

In this article, we will discuss about sending browser push notifications using JavaScript. The image given below is a sample browser push notification.

There are so many third party vendors who are providing this service with basic subscription costing around 25–30$/month

Some of them are: moengage, pushcrew, pushengage, izooto, more…

So if you are a JavaScript developer, you will be able to do this without any third party subscription. Let me explain it in detail.

What is a Service Worker?

It is a script that runs silently in the background of a browser which is registered against an origin and path. A registered service worker doesn’t need the webpage to be opened in order to work. It runs in a separate thread which is non-blocking in manner and is designed to be asynchronous hence we cannot use APIs such as synchronous XHR and local storage inside a service worker.

Service workers are so powerful. If you are a black hat, you can use it to hijack network connections and reconstruct their response. For such security reasons, they are allowed to be run only over HTTPS. For development, you can use localhost which is considered as a secure origin.

Support

Service workers are currently supported in Chrome (version 42 and above), Firefox (version 44 and above),and opera(version 27 and above) on desktop. On mobiles, it is available on Android (version 4 and above) default browser and on Android Chrome. For Edge,this technology is under development and for Safari it is under consideration.

To check for installed service workers on your browser for different domains;

For Chrome:
chrome://serviceworker-internals

%image_alt%

For Firefox:
about:serviceworkers

%image_alt%

And in your js script, you can use below expression to check for the support of service worker for any browser.

Service Worker Life Cycle

%image_alt%

Now lets start building the first Web push Notification application with payload.

  • Create a project in Google Developer Console and generate a GCM browser API key which you will be using in your node script later.

  • Project Structure:

%image_alt%

  • Implementation:

First we will create a file called service-worker.js which will add event listeners to the service worker workspace up on registration.

self.addEventListener('install', function(event) {
  event.waitUntil(self.skipWaiting()); //will install the serviceworker
});

self.addEventListener('activate', function(event) {
  event.waitUntil(self.clients.claim()); //will activate the serviceworker
});

// Register event listener for the 'push' event.
self.addEventListener('push', function(event) {

  // Retrieve the textual payload from event.data (a PushMessageData object).
  var payload = JSON.parse(event.data.text());
  var clickUrl = payload.url;

  // Keep the service worker alive until the web push notification is created.
  event.waitUntil(
    // Show a notification
    self.registration.showNotification(payload.title, {
      body: payload.body,
      icon: payload.icon
    })
  );
});

// Register event listener for the 'notificationclick' event.
self.addEventListener('notificationclick', function(event) {
  event.waitUntil(
    // Retrieve a list of the clients of this service worker.
    self.clients.matchAll().then(function(clientList) {
      // If there is at least one client, focus it.
      if (clientList.length > 0) {
        return clientList[0].focus();

      }

      // Otherwise, open a new page.
      return self.clients.openWindow(clickUrl);
    })
  );
});

Now we will create index.js file where service worker registration is initiated.

var endpoint;
var key;
var authSecret;

// Register a Service Worker.
navigator.serviceWorker.register('service-worker.js')
  .then(function(registration) {
    // Use the PushManager to get the user's subscription to the push service.

    //service worker.ready will return the promise once the service worker is registered. This can help to get rid of
    //errors that occur while fetching subscription information before registration of the service worker

    return navigator.serviceWorker.ready.then(function(serviceWorkerRegistration) {
      return serviceWorkerRegistration.pushManager.getSubscription()
        .then(function(subscription) {

          // If a subscription was found, return it.
          if (subscription) {
            return subscription;
          }

          // Otherwise, subscribe the user (userVisibleOnly allows to specify that we don't plan to
          // send browser push notifications that don't have a visible effect for the user).
          return serviceWorkerRegistration.pushManager.subscribe({
            userVisibleOnly: true
          });
        });

    });

  }).then(function(subscription) { //chaining the subscription promise object

    // Retrieve the user's public key.
    var rawKey = subscription.getKey ? subscription.getKey('p256dh') : '';
    key = rawKey ?
      btoa(String.fromCharCode.apply(null, new Uint8Array(rawKey))) :
      '';
    var rawAuthSecret = subscription.getKey ? subscription.getKey('auth') : '';
    authSecret = rawAuthSecret ?
      btoa(String.fromCharCode.apply(null, new Uint8Array(rawAuthSecret))) :
      '';

    endpoint = subscription.endpoint;

    // Send the subscription details to the server using the Fetch API.

    fetch('/register', {
      method: 'post',
      headers: {
        'Content-type': 'application/json'
      },
      body: JSON.stringify({
        endpoint: subscription.endpoint,
        key: key,
        authSecret: authSecret,
      }),
    });

  });

// Ask the server to send the client a notification (for testing purposes, in real
// applications the notification will be generated by some event on the server).
document.getElementById('doIt').addEventListener('click', function() {

  fetch('/sendNotification', {
    method: 'post',
    headers: {
      'Content-type': 'application/json'
    },
    body: JSON.stringify({
      endpoint: endpoint,
      key: key,
      authSecret: authSecret,
      title: document.getElementById("notificationTitle").value,
      body: document.getElementById("notificationBody").value,
      icon: document.getElementById("notificationIcon").value,
      link: document.getElementById("notificationLink").value
    }),
  });
});

your manifest.json

{
  "name": "Push Notifications",
  "short_name": "push Notifications",
  "start_url": "./index.html",
  "display": "standalone",
  "gcm_sender_id": "xxxxxxxxxxx",
  "gcm_user_visible_only": true
}

Include the script in your index.html file

<!doctype html>
<html>

<head>
    <meta charset="utf-8">
    <title>Service Workers -Push Notifications</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="manifest" href="manifest.json">
</head>

<body>
    <span> Title</span>
    <input type="text" id="notificationTitle" />
    <br/>
    <span> body</span>
    <input type="text" id="notificationBody" />
    <br/>
    <span> Icon Url</span>
 <input type="url" id="notificationIcon" />
 <br/>
 <span> Link</span>
 <input type="url" id="notificationLink" />
 <br/>
 <button id="doIt">Send notification</button>
 <script src="./index.js"></script>
</body>

</html>

your server side script will be in server.js

// Use the web-push library to hide the implementation details of the communication
// between the application server and the push service.
// For details, see https://tools.ietf.org/html/draft-ietf-webpush-protocol and
// https://tools.ietf.org/html/draft-ietf-webpush-encryption.

var express = require('express');
var app = express();
var webPush = require('web-push');
var bodyParser = require('body-parser')

webPush.setGCMAPIKey(process.env.GCM_API_KEY);

app.set('port', 5000);
app.use(express.static(__dirname + '/'));

app.use(bodyParser.json())

webPush.setGCMAPIKey(process.env.GCM_API_KEY);

app.post('/register', function(req, res) {
  // A real world application would store the subscription info.
  res.sendStatus(201);
});

app.post('/sendNotification', function(req, res) {

webPush.sendNotification(req.body.endpoint, {
  TTL: 4000,
  payload: JSON.stringify({
    'title': req.body.title,
    'icon': req.body.icon,
    'body': req.body.body,
    url: req.body.link
  }),
  userPublicKey: req.body.key,
  userAuth: req.body.authSecret,
})

ndStatus(201);
}, function(err) {
console.log(err);
});
});


app.listen(app.get('port'), function() {
  console.log('Node app is running on port', app.get('port'));
});


.then(function() {
res.sendStatus(201);
}, function(err) {
console.log(err);
});
});


app.listen(app.get('port'), function() {
  console.log('Node app is running on port', app.get('port'));
});

**Note: **Hope the code is self explanatory as we have put comments everywhere.

Running your project:

Install the node dependencies and start your server using command “node serve_r_” which will serve the static index.html at its base.

Now open http://localhost:5000 on your browser

%image_alt%

Click “Allow” to let the browser allow notifications from the domain “http://localhost:5000

%image_alt%

Fill the form and send notification

%image_alt%

YAY! GOT IT

%image_alt%

What if your production site doesn’t use HTTP’s.

You can go with a third party library where you need to buy a basic subscription that costs around 25–30$/month.
Let me explain what do they do to your HTTP site. When user wants to activate browser push notifications on your http site, they will open a popup window in HTTP’s having their domain name and with a sub domain(usually your domain name) of your choice. So the moment a user registers, a service worker will be registered under this domain name.

An example would be
e.g. https://cronj.notificationcrew.com

where ‘notificationcrew’ is the third party vendors domain name. There is no workaround without a HTTP’s site. So if you are having some other HTTP’s site, you can create a sub domain having name same as your HTTP domain name. Let’s say http://cronj.com is your HTTP site and you have some other HTTP’s site which is as https://subscribe.com . Your subdomain would be something like https://cronj.subscribe.com

Now here you can gather browser endpoint data by opening a popup window on your HTTP site (http://cronj.com).The popup window will be served over HTTP’s which will hold all your service worker scripts. e.g: https://cronj.subscribe.com/notificationsubscribe.html will be opened in a popup that can have markup and service worker scripts included.
You need to “Allow” notifications on the browser default popup which means browser will allow notifications that are registered under this domain name.Service workers will now be registered in your browser and you will be saving the browser subscription info in your application database by making an ajax call.
As now you have the browser endpoints data with you,your server can request GCM to fire a notification to these endpoints.

At this point,what if the user wants to unsubscribe from the notifications.
You have notifications activated, but you cannot have access to the service worker subscription data from a HTTP domain. It is not recommended to open a popup on HTTP’s every time the user wants to unsubscribe/resubscribe to notifications.

Here Comes the browser fingerprinting,
Fingerprinting is a technique which is outlined in the research by Electronic Frontier Foundation, of anonymously identifying a web browser with accuracy of up to 94%. As they explained, “Browser is queried its agent string, screen resolution and color depth, installed plugins with supported mime types, timezone offset and other capabilities, such as local storage and session storage. Then these values are passed through a hashing function to produce a fingerprint that gives weak guarantees of uniqueness”.

Since the accuracy is 94%, we will use fingerprint + ip address for 100% accuracy.

We will store this unique identification (fingerprint + ip address) of the browser along with the push subscription data in the database. When user visits your HTTP site, you can generate the fingerprint again and check for the push subscription details in the database. You can even have a flag like DND(Do Not Disturb) which you could reset every time the user unsubscribe/re-subscribe from the HTTP domain.