How we achieved both foreground and background notification using Angular Progressive Web App and Firebase Cloud Messaging at Kabbage

Xin Zhang
10 min readNov 16, 2019

--

We wanted to build an internal monitoring app with alert notification features based on our existing technical stack:

  1. Angular + Typescript
  2. AngularFire
  3. Firebase cloud messaging (FCM)

We wanted to build a Progressive Web App (PWA), taking into consideration the technical cost, which will provide us the same feeling as a native app: installation, upgrade, in app/off app notification, and notification click events. We decided to reuse the default Angular service worker because it is a proven, working JavaScript (js) file that is leveraged by many users and provides a lot of features compatible with the Angular framework, such as SwPush and SwUpdate. Also, we didn’t want to spend too much effort maintaining a service worker written by us (for example, Angular service worker is 2.75K lines of code). However, we experienced two major difficulties:

  1. AngularFireMessaging is not compatible with the Angular Service Worker, see this discussion.
  2. The default Angular service worker has no plan to implement the background-app notification click event in the near future (still valid as of this writing: Oct 2019)

We eventually worked around these two issues. In this article, I will reproduce the steps we took to build our internal monitoring tool. The end product will be a very lightweight angular PWA app with minimum code, to provide foreground and background notification abilities. This article will build this component app, step by step using an agile methodology, to create a working app at the end of each stage (sprint in my preferred way):

  1. Build a hello world PWA.
  2. Hook up the AngularFire and FCM with Angular service worker, to achieve messaging consumption, foreground notification, and foreground notification click event (no back-ground event handling yet)
  3. Customize Angular service worker to be able to provide background notification click behavior and demonstrate some mobile tests, too.

Our final code is here, and eventually, our app will be able to accomplish something like the below picture on Android: Once clicking the notification, it will open our app:

What is the app looks like on Pixel 2

Sprint 1: Build a hello world PWA

I will keep these well known steps simple, just follow any online articles. I will outline only the basic stuff needed here:

First, install Angular cli if you haven’t, npm install @angular/cli -g

Create new project, let’s call it PushDemo: ng new PushDemo. Using the most simple options; we don’t even need routing for now. I used Less for styling, but it doesn’t matter, we are not going to use it. Once that is done, navigate to this directory directly using cd PushDemo.

Register a Firebase application on https://console.firebase.google.com/. Then you will need to add a web app in your project configuration page. The minimal project setup is fine, which is just to have a name. Copy the information Firebase console provided to you to set up your environment.ts and environment.prod.ts:

export const environment = {
production: false,
firebase: {
apiKey: '<your-key>',
authDomain: '<your-project-authdomain>',
databaseURL: '<your-database-URL>',
projectId: '<your-project-id>',
storageBucket: '<your-storage-bucket>',
messagingSenderId: '<your-messaging-sender-id>'
}
};

Run ng serve. You should have a basic webapp running!

Next, enable Angular PWA to have the default service worker by running ng add @angular/pwa --project PushDemo. This will add a bunch of png files, modify and add some json file, everything is fine and expected. Particularly, this will register a service worker when in production mode, which added a line at the end of imports on app.module.ts: ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production }).

That is all for Sprint 1 development work. You can check out the Sprint1 on github (Note I didn’t commit the change I made for environment.ts and environment.prod.ts for security reasons). For now, your app.module.ts file should look like below:

app.module.ts

The Angular service worker only works in prod mode. Starting from now, we lose the ability to use ng-serve to debug service worker stuff. We can still build and debug though:

  1. Install http-server-spa globally: npm install -g http-server-spa@1.3.0, you will only need to run this once.
  2. Build production module with source map: ng build --prod --source-map
  3. Serve your application using http-server-spa dist/PushDemo/ index.html 18080 (I used 18080 port, you can use any free port you are using), visit http://localhost:18080/. Now you have a beautiful PWA running, which, will be teared down soon.

Sprint 2: Hook up AngularFire and FCM with Angular service worker

First install Firebase and AngularFire: npm install firebase @angular/fire — save

Navigate to app.component.ts and add these important changes:

Import two things import * as firebase from 'firebase/app'; and import 'firebase/messaging';

Define a property displayToken: string;, then modify the constructor to initialize Firebase app and told Firebase messaging to use the service worker that Angular provided. Code snippet below:

if (!firebase.apps.length) {
firebase.initializeApp(environment.firebase);
navigator.serviceWorker.getRegistration().then(swr => firebase.messaging().useServiceWorker(swr));
}

The code initializes a Firebase app if necessary using the environment settings from previous step. Then the most important part is it looks for registered service worker, and use that service worker registration, to hook up with Firebase by using the useServiceWorker() method.

Next, we create a method, called permitToNotify, this method asks a browser for permissions to be able to get a notification. We will also save the granted permission token to be used later. Below are code snippet, pretty standard promise style:

permitToNotify() {
const messaging = firebase.messaging();
messaging.requestPermission()
.then(() => this._messaging.getToken().then(token => this.displayToken = token))
.catch(err => {
console.log('Unable to get permission to notify.', err);
});
}

Then we modify our component template to consume this method, super simple and no css styling:

@Component({
selector: 'app-root',
template: `
<button (click)="permitToNotify()">
Click button to Click Notifiy
</button>
<br/>
Device token: {{displayToken}}
`
})

Delete app.component.less(or the styling based on your choice), app.component.html and app.component.spec.ts, as I said we want to keep this minimal, but ugly…

Lastly, we inject SwUpdate and SwPush for demo and debugging purpose. Note these two work only in the foreground app. We will cover background in the next stage. The way I use them is just for logging purposes, I used SwUpdate to make the app upgrade itself so that you don’t need to hard refresh every time because of caching of serviceworker. Snippet:

constructor(updates: SwUpdate, push: SwPush) {
updates.available.subscribe(_ => updates.activateUpdate().then(() => {
console.log('reload for update');
document.location.reload();
}));
push.messages.subscribe(msg => console.log('push message', msg));
push.notificationClicks.subscribe(click => console.log('notification click', click));
// .... The rest Firebase initialization stuff in step 2 above.

This is the final app.component.ts:

app.component.ts

Let’s test sprint 2 for now. Note: the message is not the real Firebase message yet… we will get it later.

Build and re-serve using ng build --prod --source-map and http-server-spa dist/PushDemo/ index.html 18080

Hard refresh http://localhost:18080/ (for the 1st time only because the old code does not do auto-reload for upgrade. For future changes, you just need to reload as normal, and wait for Angular to pick up the changes and reload itself. There will have a log in the console to tell you update is finished), click the only button on the page, Chrome will ask you if you grant permission. Click “Accept”, you should see something like cY4hsxFuLhSuXg0P_LU2mW:APA91bGBqBs1rDjzueRkXmKuClsal_Hide_For_Security on your page, you will need this in the future, please remember this token:

What the app displays after you click accept.

Ensure you have system notification enabled. For example, on Windows 10, enable the “get notifications from apps and other senders” and give Google Chrome permission to send notification:

Windows 10 system notification setting
Windows 10 system notification setting for chrome

Open Developer tools, click Application tab of the dev tools, click the Service Workers under Application on the left, you should see service worker listed for http://localhost:18080/. In the push field, paste this payload { "notification": {"body": "Kabbage Demo", "icon": "https://www.kabbage.com/favicon.ico", "title": "Demo"}}, and click the “Push” button:

You should see a notification tab:

In developer console, you should also see push message payload as well, note this is generated by our foreground message handler that subscribed to SwPush. Test again and this time, click the notification tab quickly before it disappear, you will also see a foreground click event body logged in developer console, which is sent by the Angular service worker. Below are the logging for these two steps:

It is good for now, but this is NOT real FCM message yet. We need to add this arbitrary string to your manifest.webmanifest: "gcm_sender_id": "103953800507",, it is the same stuff no matter what your messaging app is, what your Firebase account looks like. Go do it!

"gcm_sender_id": "103953800507" pasted in manifest.webmanifest

Rebuild, rerun.

Now go to the Firebase console, click Cloud messaging under Grow tab, and select your application, then click the “Send your first message” button:

Click the “Send your first message” on the right middle

Just fill in title and text, and click “Send test message” button, you can add more attribute later by yourself programmatically.

The real FCM test message we are going to send

Copy the token in on your webpage. As an example, mine looks like: cY4hsxFuLhSuXg0P_LU2mW:APA91bGBqBs1rDjzueRkXmKuClsal_Hide_For_Security. Then, add this token in the “Test on device” pop up, check it, then “Test”:

You should get a message notification and some console output like before; you can play around with it for fun. The notification tile doesn’t look very much like the previous, as it is a different payload request we are using. Google FCM UI page is limited for the payload contract, in reality we used api to compose the message so it looks a little bit different. For now this is good enough for demo purpose. And that is all for sprint 2. You can find Sprint 2 on Github, A screenshot with my example:

The notification made by real FCM message

As of now, your app should be able to handle the push message pretty well. And if you install it on an Android device (will cover more on installation on the next sprint), it will also show you the notification like other apps. However, on both Windows and Android, clicking the notification would not do anything. Or, more specifically, when your app is in the foreground, meaning the app is open, clicking the notification will actually do some logging as in this example. But when your close the app, meaning app running in the background. Sadly, Angular has no short-term plan (true as of Oct 2019) to implement this feature. There are many customization options available online to achieve this but, in the following Sprint 3, I will introduce the way we did it and how we think it is manageable.

Sprint 3: Customize Angular service worker in a manageable way

Many of the customization examples online modify the Angular service worker — we do the same. After you build in prod mode using ng build --prod, you will find a ngsw-worker.js file under dist\PushDemo folder. We want to reuse this file because it integrates with Angular very well and we also don’t want to maintain 2.75K lines of Javascript. We also want it source controlled and to be easier for future upgrade.

We found the ngsw-worker.js under dist\PushDemo is always the same for the same version, not uglified nor minimized. So we made a copy this file, named it as ngsw-worker-custom-{{angular-version}}.js(in my case, ngsw-worker-custom-8.3.12.js), and put it into our source-controlled directory.

Edit the ngsw-worker-custom-8.3.12.js, put the following code in handleClick function before broadcast NOTIFICATION_CLICK event.

yield clients.matchAll().then(matchedClients => {
const url = new URL('/', location).href;
for (let matchClient of matchedClients) {
if (matchClient.url.startsWith(url)) {
return matchClient.focus();
}
}
return clients.openWindow(url);
})
Customized ngsw-worker-custom-8.3.12.js

By doing so we are able to track what customization we did on which version using our source control system and in future upgrades we can carry over our customization or remove it.

Each time you rebuild, remember to copy this file to dist\PushDemo folder to replace the original ngsw-worker.js. Then, serve the application again. For our own monitor system, we used docker file to make this automatic: RUN cp ./src/ngsw-worker-custom-7.2.14.js ./dist/ngsw-worker.js (just used as FYI purpose, not included in this tutorial)

That is all for sprint 3. You can reference the Sprint 3 code here.

To test, Chrome has safety restrictions about service worker, for example must valid https. Chrome did whitelist the localhost domain. So we can still test chrome just using your laptop. To test the android device, I followed chrome remote devices, and made the screen recording at the beginning of this article.

To summarize, we used Angular Firebase with its core Javascript library to work with the Angular default service worker to achieve our customized angular app with foreground and background notification abilities. However, this works only for chrome based environment: windows chrome. Android devices. It not working for iPhone simply because iOS web push has not implemented yet, which is totally another story.

--

--