Implementing Push Notifications: Setting Up & Firebase

You know those the little notification windows that pop up in the top right (Mac) or bottom right (Windows) corner when, for example, a new article on our favorite blog or a new video on YouTube was uploaded? Those are push notifications.

Part of the magic of these notifications is that they can appear even when we're not currently on that website to give us that information (after you've approved it). On mobile devices, where supported, you can even close the browser and still get them.

Article Series:

  1. Setting Up & Firebase (You are here!)
  2. The Back End
Notification on Mac via Chrome
Push notification on a Mac in Chrome

A notification consists of the browser logo so the user knows from which software it comes from, a title, the website URL it was sent from, a short description, and a custom icon.

We are going to explore how to implement push notifications. Since it relies on Service Workers, check out these starting points if you are not familiar with it or the general functionality of the Push API:

What we are going to create

Preview of the our push notification demo website

To test out our notifications system, we are going to create a page with:

  • a subscribe button
  • a form to add posts
  • a list of all the previously published posts

A repo on Github with the complete code can be found here and a preview of the project:

View Demo Site

And a video of it working:

Gathering all the tools

You are free to choose the back-end system which suits you best. I went with Firebase since it offers a special API which makes implementing a push notification service relatively easy.

We need:

In this part, we'll only focus on the front end, including the Service Worker and manifest, but to use Firebase, you will also need to register and create a new project.

Implementing Subscription Logic

HTML

We have a button to subscribe which gets enabled if 'serviceWorker' in navigator. Below that, a simple form and a list of posts:

<button id="push-button" disabled>Subscribe</button>

<form action="#">
  <input id="input-title">
  <label for="input-title">Post Title</label>
  <button type="submit" id="add-post">Add Post</button>
</form>

<ul id="list"></ul>

Implementing Firebase

To make use of Firebase, we need to implement some scripts.

<script src="https://www.gstatic.com/firebasejs/4.1.3/firebase-app.js"></script>
<script src="https://www.gstatic.com/firebasejs/4.1.3/firebase-database.js"></script>
<script src="https://www.gstatic.com/firebasejs/4.1.3/firebase-messaging.js"></script>

Now we can initialize Firebase using the credentials given under Project Settings → General. The sender ID can be found under Project Settings → Cloud Messaging. The settings are hidden behind the cog icon in the top left corner.

firebase.initializeApp({
    apiKey: '<API KEY>',
    authDomain: '<PROJECT ID>.firebaseapp.com',
    databaseURL: 'https://<PROJECT ID>.firebaseio.com',
    projectId: '<PROJECT ID>',
    storageBucket: '<PROJECT ID>.appspot.com',
    messagingSenderId: '<SENDER ID>'
})

Service Worker Registration

Firebase offers its own service worker setup by creating a file called `firebase-messaging-sw.js` which holds all the functionality to handle push notifications. But usually, you need your Service Worker to do more than just that. So with the useServiceWorker method we can tell Firebase to use our own `service-worker.js` file as well.

Now we can create a userToken and a isSubscribed variable which will be used later on.

const messaging = firebase.messaging(),
      database  = firebase.database(),
      pushBtn   = document.getElementById('push-button')

let userToken    = null,
    isSubscribed = false

window.addEventListener('load', () => {

    if ('serviceWorker' in navigator) {

        navigator.serviceWorker.register('https://cdn.css-tricks.com/service-worker.js')
            .then(registration => {

                messaging.useServiceWorker(registration)

                initializePush()
            })
            .catch(err => console.log('Service Worker Error', err))

    } else {
        pushBtn.textContent = 'Push not supported.'
    }

})

Initialize Push Setup

Notice the function initializePush() after the Service Worker registration. It checks if the current user is already subscribed by looking up a token in localStorage. If there is a token, it changes the button text and saves the token in a variable.

function initializePush() {

    userToken = localStorage.getItem('pushToken')

    isSubscribed = userToken !== null
    updateBtn()

    pushBtn.addEventListener('click', () => {
        pushBtn.disabled = true

        if (isSubscribed) return unsubscribeUser()

        return subscribeUser()
    })
}

Here we also handle the click event on the subscription button. We disable the button on click to avoid multiple triggers of it.

Update the Subscription Button

To reflect the current subscription state, we need to adjust the button's text and style. We can also check if the user did not allow push notifications when prompted.

function updateBtn() {

    if (Notification.permission === 'denied') {
        pushBtn.textContent = 'Subscription blocked'
        return
    }

    pushBtn.textContent = isSubscribed ? 'Unsubscribe' : 'Subscribe'
    pushBtn.disabled = false
}

Subscribe User

Let's say the user visits us for the first time in a modern browser, so he is not yet subscribed. Plus, Service Workers and Push API are supported. When he clicks the button, the subscribeUser() function is fired.

function subscribeUser() {

    messaging.requestPermission()
        .then(() => messaging.getToken())
        .then(token => {

            updateSubscriptionOnServer(token)
            isSubscribed = true
            userToken = token
            localStorage.setItem('pushToken', token)
            updateBtn()
        })
        .catch(err => console.log('Denied', err))

}

Here we ask permission to send push notifications to the user by writing messaging.requestPermission().

The browser asking permission to send push notifications.

If the user blocks this request, the button is adjusted the way we implemented it in the updateBtn() function. If the user allows this request, a new token is generated, saved in a variable as well as in localStorage. The token is being saved in our database by updateSubscriptionOnServer().

Save Subscription in our Database

If the user was already subscribed, we target the right database reference where we saved the tokens (in this case device_ids), look for the token the user already has provided before, and remove it.

Otherwise, we want to save the token. With .once('value'), we receive the key values and can check if the token is already there. This serves as second protection to the lookup in localStorage in initializePush() since the token might get deleted from there due to various reasons. We don't want the user to receive multiple notifications with the same content.

function updateSubscriptionOnServer(token) {

    if (isSubscribed) {
        return database.ref('device_ids')
                .equalTo(token)
                .on('child_added', snapshot => snapshot.ref.remove())
    }

    database.ref('device_ids').once('value')
        .then(snapshots => {
            let deviceExists = false

            snapshots.forEach(childSnapshot => {
                if (childSnapshot.val() === token) {
                    deviceExists = true
                    return console.log('Device already registered.');
                }

            })

            if (!deviceExists) {
                console.log('Device subscribed');
                return database.ref('device_ids').push(token)
            }
        })
}

Unsubscribe User

If the user clicks the button after subscribing again, their token gets deleted. We reset our userToken and isSubscribed variables as well as remove the token from localStorage and update our button again.

function unsubscribeUser() {

    messaging.deleteToken(userToken)
        .then(() => {
            updateSubscriptionOnServer(userToken)
            isSubscribed = false
            userToken = null
            localStorage.removeItem('pushToken')
            updateBtn()
        })
        .catch(err => console.log('Error unsubscribing', err))
}

To let the Service Worker know we use Firebase, we import the scripts into `service-worker.js` before anything else.

importScripts('https://www.gstatic.com/firebasejs/4.1.3/firebase-app.js')
importScripts('https://www.gstatic.com/firebasejs/4.1.3/firebase-database.js')
importScripts('https://www.gstatic.com/firebasejs/4.1.3/firebase-messaging.js')

We need to initialize Firebase again since the Service Worker cannot access the data inside our `main.js` file.

firebase.initializeApp({
    apiKey: "<API KEY>",
    authDomain: "<PROJECT ID>.firebaseapp.com",
    databaseURL: "https://<PROJECT ID>.firebaseio.com",
    projectId: "<PROJECT ID>",
    storageBucket: "<PROJECT ID>.appspot.com",
    messagingSenderId: "<SENDER ID>"
})

Below that we add all events around handling the notification window. In this example, we close the notification and open a website after clicking on it.

self.addEventListener('notificationclick', event => {
    event.notification.close()

    event.waitUntil(
        self.clients.openWindow('https://artofmyself.com')
    )
})

Another example would be synchronizing data in the background. Read Google's article about that.

Show Messages when on Site

When we are subscribed to notifications of new posts but are already visiting the blog at the same moment a new post is published, we don't receive a notification.

A way to solve this is by showing a different kind of message on the site itself like a little snackbar at the bottom.

To intercept the payload of the message, we call the onMessage method on Firebase Messaging.

The styling in this example uses Material Design Lite.

<div id="snackbar" class="mdl-js-snackbar mdl-snackbar">
  <div class="mdl-snackbar__text"></div>
  <button class="mdl-snackbar__action" type="button"></button>
</div>
import 'material-design-lite'

messaging.onMessage(payload => {

    const snackbarContainer = document.querySelector('#snackbar')

    let data = {
        message: payload.notification.title,
        timeout: 5000,
        actionHandler() {
            location.reload()
        },
        actionText: 'Reload'
    }
    snackbarContainer.MaterialSnackbar.showSnackbar(data)
})

Adding a Manifest

The last step for this part of the series is adding the Google Cloud Messaging Sender ID to the `manifest.json` file. This ID makes sure Firebase is allowed to send messages to our app. If you don't already have a manifest, create one and add the following. Do not change the value.

{
  "gcm_sender_id": "103953800507"
}

Now we are all set up on the front end. What's left is creating our actual database and the functions to watch database changes in the next article.

Article Series:

  1. Setting Up & Firebase (You are here!)
  2. The Back End