Push notifications to mobile devices using browser APIs

Tom-Einar Sørensen

This proof of concept walks you through how to send free messages from your server to the browser on both desktop and mobile, without having to install anything on the end user device or computer. It also offers tips and tricks for some common scenarios.

The motivation for doing this

The use case here is that we have a web application and we want to show notification messages to the end user. To address this, we implement a solution where we utilize the Notification API to give the user a richer experience, but if the web app isn’t running in a tab in the browser, there is no connection to the user, and we can’t send messages.

What if there was a way to send messages to users even if the tab with our app is closed? We wanted this to run in the browser only, so we couldn’t use native code or anything that required the user to install anything on their device. It needed to work on both mobile and desktop, and our previous attempts with the Notification API didn’t work too well with mobile devices.

Then a solution appeared. The Push API. Fortunately, most of the registered users were using Chrome, making the following a plausible solution.

What APIs and technologies do we need to utilize

These spesifications vary in maturity and not all browsers support them, so use a service like http://caniuse.com/ to see what your browser/version supports.

We'll be using Google Cloud Messaging (GCM) in this proof of concept because "...it's completely free"

Overview

To sum things up, I've made this diagram that shows the flow of events. It consists of two parts:
1. (a-e) Registration of the service worker, subscribing with GCM and the subsequent storing of registration_id needed to notify GCM.
2. (a-d) The proccess of sending a message to a number of users through GCM, based on previously stored registration ids. This proccess will probably be initiated by some event in your system that needs to notify the users.
I'm using Draw.io for the diagram

How to get this up and running

  1. First off, we need a simple HTML-page:
  2. <html>  
      <head>
        <title>Push demo</title>
        <script src="./register.js"></script>
      </head>
      <body>
        <h1>Push notification Demo</h1>
      </body>
    </html>  
    
  3. That page has a reference to a javascript file that registers a service worker, register.js:
  4. window.addEventListener('load', function () {  
      if ('serviceWorker' in navigator) {
        navigator.serviceWorker.register('./service-worker-demo.js');
      } else {
        console.log('Service workers aren\'t supported in this browser.');
      }
    });
    
  5. The service worker, service-worker-demo.js, has an event listener attached that will execute when the push event is fired. Even though we won’t be sending any messages from an external source just yet, we can use this event handler to verify that the service worker is registered and that we are wired up correctly and able to use the push notification API to send messages to the user:
    'use strict';
    
    self.addEventListener('push', function(event) {  
      console.log('Received a push message', event);
    
      var notificationOptions = {
        body: 'Hello everybody!',
        icon: './images/hipstercat.jpg',
        tag: 'simple-push-demo-notification'
      };    
    
      return self.registration.showNotification('Important message', notificationOptions);
    }); 
    


  6. The ServiceWorker debug screen in Chrome lets us see all the registered service workers in the browser. You can see the newly registered service worker at the top of this list: We can simulate the push event from the browser by clicking the "Push" button. The push event is published and the handler we registered in our service worker is invoked. So far, so good:



  7. We need to setup an account with Google Developer Console and enable the Messaging API (GCM – google cloud messaging). Follow this procedure to do this: https://developers.google.com/web/fundamentals/getting-started/push-notifications/step-04?hl=en.
    1. You need to make a note of the project number, as this goes into the manifest file as the value of the property gcm_sender_id
    2. Make a note of the API key, as this is used to send messages to GCM, so that GCM can verify that you have access to send messages to the registered endpoints.
  8. Add a manifest.json file to the project. This file needs to include the gcm_sender_id from step 5.:
    {
      "name": "Push Demo",
      "short_name": "Push Demo",
      "icons": [{
            "src": "images/hipstercat.jpg",
            "sizes": "225x225",
            "type": "image/jpg"
          }],
      "start_url": "./index.html",
      "display": "standalone",
      "gcm_sender_id": "<your project number goes here>"
    }
    
  9. Reference this file in the HTML:
    <link rel="manifest" href="./manifest.json">  
    
  10. Add the subscription code for the push manager. This code will give us the details of the subscription, including the registration id we need to call the GCM service. Theoretically (and possibly sometime in the future), this URL could be the URL we can use to get GCM to push messages for us. Unfortunately, we have to dismantle it for ourselves in order to get what we need. I'm running the latest version of chrome, but your users might not, so check for features before subscribing:
    function subscribe() {  
        if (!('showNotification' in ServiceWorkerRegistration.prototype)) {
            console.log('Notifications aren\'t supported.');
            return;
        }
    
        if (Notification.permission === 'denied') {
            console.log('The user has blocked notifications.');
            return;
        }
    
        if (!('PushManager' in window)) {
            console.log('Push messaging isn\'t supported.');
            return;
        }
        navigator.serviceWorker.ready.then(function(serviceWorkerRegistration) {
            serviceWorkerRegistration.pushManager.subscribe({userVisibleOnly: true})
                .then(function(subscription) {
                    if(subscription.endpoint.startsWith("https://android.googleapis.com/gcm/send")){
                        var parts = subscription.endpoint.split("/");
                        var registrationId = parts[parts.length -1];
                        console.log("RegistrationId:")
                        console.log(registrationId);
                    }
                })
                .catch(function(e) {
                    console.log('Something unfortunate happened: ' + e);
                });
        });
    }
    
  11. Running this now should yield the following result:

  12. This is where one typically would send the registration id to the server for storage, so we can use it when we need to push messages to this client. In this example, we will use curl to do that for us. Note that the authorization key in the header is the API key from step 5. The registration_ids array should contain the last part of the URL from step 8:
    curl --header "Authorization: key=<your API key goes here>" --header "Content-Type: application/json" https://android.googleapis.com/gcm/send -d "{\"registration_ids\":[\"d3dECu6n_G0:APA91bErlguPkeQiCEcCutdphhBt8mKmONNQJ4zXkMs8KZQnmFI40laYQq6Qc0-BxFr07Wl5Lf-3rYHAascsuW7YQ7zcDbJrsYciQI6ePqfLJ87k9nuzDgM2u4lMh_hJ6vFHwjuiPwDw\"]}"  
    
  13. Run the command:
    This command won't run if you try, the project used has been deactivated

  14. A notification appears on the screen:

  15. Now I want to verify that this works on my mobile device, so I publish it as a Web App to Azure. This will fail if we don't add a web.config and the following section:
    <system.webServer>  
        <staticContent>
          <mimeMap fileExtension=".json" mimeType="application/json" />
        </staticContent>
      </system.webServer>
    
    The first time I load the website, I get asked if I want to allow notifications:
    It looks something like this on your mobile device:

  16. If we want to be able to repeat the steps to get the registration id, we need to enable remote debugging for android devices: https://developers.google.com/web/tools/chrome-devtools/debug/remote-debugging/remote-debugging?hl=en I had to install Chrome Canary to get access to my device.
  17. chrome://inspect/#devices gives me a list of my devices in Chrome Canary. Click inspect to open Developer Tools:
  18. Repeat steps 9 through 11 and observe that a notification appears:
  19. Chrome doesn't have to be the active app on your mobile and your web application doesn't need to be running in a tab for this to work, but the browser must be running for the service worker to be able to process the incoming push message.

Notifications

Like I mentioned earlier, this is the minimum amount of code you need to get this running. You can customize your notification to close after a period of time or add a click handler. The tag option that we actually have implemented in this example, will prevent the user from seeing multiple notifications. If one notification with a given tag is present, a new notification with the same tag will just replace it instead of stacking on top of it. This can be quite irritating, especially on a mobile device.

In general, I would use caution when displaying notifications. There is a fine line between being informative and being annoying. To illustrate, I have removed the tag on the notification options:

First the user gets notified that something has happened to the application in the background (after I publish the changes):

Then we start flooding GCM with messages (each of these come with a sound):

This will probably lead to the user turning off notifications and it will all have been for nothing.

Tips and troubleshooting

If you want to try this out, here are a few pointers that might help:

  • chrome://serviceworker-internals/ gives you access to all the Service Workers in your browser.
  • If you need to debug the service worker, click the "Inspect" button in the list of service workers and this will open Developer Tools for your service worker.
  • Uncaught (in promise) DOMException: Only secure origins are allowed (see: https://goo.gl/Y0ZkNV). You can only register service workers using https or if you host name is localhost.
  • Uncaught (in promise) TypeError: No notification permission has been granted for this origin. If you haven’t asked the user for permission to display Notifications, you need to do so:
Notification.requestPermission().then(function(result) {  
    //do your notification magic
});
  • AbortError: Registration failed - no sender id provided. You need to add the manifest file or your gcm_sender_id is incorrect
  • Chrome currently only supports the Push API for subscriptions that will result in user-visible messages. You can indicate this by calling pushManager.subscribe({userVisibleOnly: true}) instead. See https://goo.gl/yqv4Q4 for more details. This error message should be self-explanatory. E-mail me if you for some reason can't figure it out :)
  • If you don’t have curl in your command line, you can use Invoke-WebRequest in Powershell. I got my curl from my git installation:


Disclaimer

The code you see in this post will be the minimum amount of code required to get things up and running, a proof of concept if you will. This POC will only work with the Google Chrome browser. I have successfully tested it on Windows and Android platforms. Mozilla has its own push message service that pushes messages to the Firefox browser.

When I first made my proof of concept, some time ago, I relied quite heavily on google's Push Notifications on the open web. They have a more elaborate example than the one I used. You might find it useful.