Setup Push Notification For PWA

February 13, 2018

Push notification allow your PWA web app to send notification to user on Desktop (Chrome Browser) or Android devices (probably not supported on iOS yet).

Below is the flow to enable Push Notification.

It would be nice if you have a basic understanding of PWA setup before going through this tutorial.

Register a Service Worker

Edit your index.html to include the following code (original code from Google Developers).

<script>
if ('serviceWorker' in navigator && 'PushManager' in window) {
  console.log('Service Worker and Push is supported');

  navigator.serviceWorker.register('/sw-push-notification.js')
      .then(function(registration) {
        console.log('Service Worker is registered', registration);

        // our PushManager helper methods
        window.lua.pwa.checkSubscription(registration);
      })
      .catch(function(error) {
        console.error('Service Worker Error', error);
      });
} else {
  console.warn('Push messaging is not supported');
}
</script>

NOTE: refer to more service worker registration code from sw-cache or vuejs-templates/pwa.

NOTE: if you already have a service worker generated using webpack’s SWPrecacheWebpackPlugin, you can use importScripts to include sw-push-notification.js.

Handle PushEvent and NotificationEvent

Code for sw-push-notification.js.

// if you send a push notification from server, it might be instant or delay up 10 minutes
// https://developer.mozilla.org/en-US/docs/Web/API/PushEvent
self.addEventListener('push', function(event) {
  console.log('[Service Worker] Push Received.');
  // push notification can send event.data.json() as well
  console.log(`[Service Worker] Push had this data: "${event.data.text()}"`);

  // https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/showNotification
  const title = 'MyApp Alert';
  const options = {
    body: event.data.text(),
    icon: '/static/img/icon-512.png',
    badge: '/static/img/icon-96.png',
    tag: 'alert'
  };

  event.waitUntil(self.registration.showNotification(title, options));
});


self.addEventListener('notificationclick', function(event) {
  // can handle different type of notification based on event.notification.tag
  console.log(`[Service Worker] Notification click Received: ${event.notification.tag}`);

  event.notification.close();

  // Modify code from https://developer.mozilla.org/en-US/docs/Web/API/WindowClient/focus
  // find existing "/notification" window to focus on, or open a new one if not available
  event.waitUntil(clients.matchAll({
    type: "window"
  }).then(function(clientList) {
    const client = clientList.find(function(c) {
      new URL(c.url).pathname === '/notification'
    });
    if (client !== undefined) {
      return client.focus();
    }

    return clients.openWindow('/notification');
  }));
});

PushManager helper methods

I am using namespace window.lua.pwa to store all my pwa variable and methods. I put in as part of my webpack bundle, or you can use put the code in a simple javascript file (e.g. app.js).

import axios from 'axios'

window.lua.pwa = {
  // public key of the push notification server. 
  // if you are using the Test Push Notification Server, 
  // get public key from https://web-push-codelab.glitch.me/
  applicationServerPublicKey: 'xxx',
  registration: null,
  isSubscribed: false,
  // render a button UI to subscribe/unsubscribe push notification
  // I am using Vue.js for the UI, though you can onvert it to use plain javascript or jQuery
  initUi(options) {
    if ($('#lua-home-actionbar').length) {
      new Vue({
        el: '#lua-home-actionbar',
        data: {
          isSubscribed: options.isSubscribed
        },
        template: '<HomeActionBar :isSubscribedProp="isSubscribed" />',
        components: { HomeActionBar },
      })
    }
  },
  // call when service worker is registered
  checkSubscription(registration) {
    registration.pushManager.getSubscription()
      .then((subscription) => {
        this.isSubscribed = !(subscription === null)

        // send subscription info to server (the server will send push notification to this subscription)
        this.updateSubscriptionOnServer({ subscription: subscription, is_active: this.isSubscribed })

        this.registration = registration

        // update UI to indicate Push Notification is subscribed or not
        this.initUi({ isSubscribed: this.isSubscribed })
      })
  },
  // subscribe push notification
  subscribe() {
    const applicationServerKey = this.urlB64ToUint8Array(this.applicationServerPublicKey)

    return this.registration.pushManager.subscribe({
      // https://developers.google.com/web/fundamentals/push-notifications/subscribing-a-user#uservisibleonly_options
      // symbolic agreement with the browser that the web app will show 
      // a notification every time a push is received (i.e. no silent push).
      userVisibleOnly: true,
      applicationServerKey
    })
      .then((subscription) => {
        // subscription successful, send subscription info to server
        this.updateSubscriptionOnServer({ subscription, is_active: true })
        this.isSubscribed = true
        return true
      })
  },
  // unsubscribe push notification
  unsubscribe() {
    return this.registration.pushManager.getSubscription()
      .then((subscription) => {
        if (subscription) {
          // unsubscribe successful, update server
          this.updateSubscriptionOnServer({ subscription, is_active: false })
          this.isSubscribed = false
          return subscription.unsubscribe()
        }
        return false
      })
  },
  // send subscription info to server
  updateSubscriptionOnServer({ subscription, is_active }) {
    // if you are using https://web-push-codelab.glitch.me/ as a Test Push Notification Server
    // you need to copy and paste this string
    console.log(JSON.stringify(subscription))

    // if you implemented your own Push Notification Server
    /*
    axios({
      method: 'post',
      url: '/_intents/subscribe',
      data: {
        subscription_info: JSON.stringify(subscription),
        is_active: is_active
      }
    })
      .then((response) => {
        console.log(response.data)
      })
      .catch((error) => {
        console.log(error)
      })
      */
  },
  // convert applicationServerPublicKey
  urlB64ToUint8Array(base64String) {
    const padding = '='.repeat((4 - base64String.length % 4) % 4)
    const base64 = (base64String + padding)
      .replace(/\-/g, '+')
      .replace(/_/g, '/')

    const rawData = window.atob(base64)
    const outputArray = new Uint8Array(rawData.length)

    for (let i = 0; i < rawData.length; ++i) {
      outputArray[i] = rawData.charCodeAt(i)
    }
    return outputArray
  }
}

Sample Vue.js UI for subscribe/unsunscribe button

The UI logic is as per the following:

  • Upon registration of Server Worker, we call window.lua.pwa.checkSubscription to determine our Push Notification subsription status (is subscribed or not).
  • If not subscribed yet, we click on Subscribe Button to call window.lua.pwa.subscribe.
  • If already subscribed, we can click on Unsubsribe Button by calliing window.lua.pwa.unsubscribe.
  • User can block Notification as well, so need to check for Notification.permission === 'denied'
<template>
  <div>
    <button type="button" class="btn btn-primary" @click.prevent="subscribe">{{ buttonText }}</button>
  </div>
</template>

<script>
import axios from 'axios'

export default {
  name: 'HomeActionBar',
  props: ['isSubscribedProp'],
  data() {
    return {
      isSubscribed: this.isSubscribedProp
    }
  },
  computed: {
    buttonText() {
      if (Notification.permission === 'denied') {
        window.lua.pwa.updateSubscriptionOnServer(null)
        return 'Notification Blocked'
      }

      if (!this.isSubscribed) {
        return 'Enable Push Notification'
      }
      return 'Disable Push Notification'
    },
  },
  methods: {
    subscribe() {
      if (Notification.permission === 'denied') {
        console.log('Notification is Blocked')
        return
      }

      if (this.isSubscribed) {
        window.lua.pwa.unsubscribe()
          .then((result) => {
            if (result) {
              console.log('Unsubscribe successful')
              this.isSubscribed = false
            }
          })
          .catch((error) => {
            console.log(error.message)
          })
      } else {
        window.lua.pwa.subscribe()
          .then((result) => {
            if (result) {
              console.log('Subscribe successful')
              this.isSubscribed = true
            }
          })
          .catch((error) => {
            // use null to trigger update if user block notification
            this.isSubscribed = null
            console.log(error.message)
          })
      }
    },
  }
}
</script>

Test Push Notification

Use Chrome DevTools

The easiest way yo test Push Notification without a server is using Chrome DevTools -> Application -> Service Workers -> Push. This would trigger the code in self.addEventListener('push' ... and show a notification.

Use Test Push Notification Server

Before you build your own push notification server, you can use https://web-push-codelab.glitch.me/ for testing purpose.

Set window.lua.pwa.applicationServerPublicKey to the Public Key provided on the website.

After you subscribed to puch notification by calling window.lua.pwa.subscribe, window.lua.pwa.updateSubscriptionOnServer will print subsciption info (json string which starts with {"endpoint":"https://fcm.googleapis.com/fcm/...) through console.log.

Copy the subsciption info and paste it into Subscription to Send To on the website, key in any Text to Send and click Send Push Message.

Build your own Push Notification Server

You can build your own Push Notification Server using web push libraries. Refer to Develop Web Push Notification Server With Python.

References:

This work is licensed under a
Creative Commons Attribution-NonCommercial 4.0 International License.