Search Lessons, Code Snippets, and Videos
search by algolia
X
#native_cta# #native_desc# Sponsored by #native_company#

Topic-Based Web Notifications With Ionic4 and Cloud Messaging

Episode 137 written by Jeff Delaney
full courses for pro members

Health Check: This lesson was last reviewed on and tested with these packages:

  • Angular v6
  • RxJS v6.2
  • @angular/fire v5.0.0
  • Ionic v4

Find an issue? Let's fix it

Broadcasting push notifications to millions of users simultaneously is amazingly simple when you combine Firebase Cloud Messaging (FCM) with Callable Cloud Functions. Throw Ionic 4 into the mix you get beautiful UI elements out of the box to handle messages in the app. The following lesson is a comprehensive guide for building topic-based web notifications.

The Notification Process

There are several steps in the web push notification process. Here’s the rundown.

(1) Get permission from the user. FCM will return a device token (just a string) if the user clicks allow.

Get permission to send notifications on FCM

(2) Subscribe the device token to a topic using the Admin SDK in a Callable Cloud Function

(3) Broadcast messages using the using the Admin SDK on a Firestore Cloud Function

(4a) Handle the notification in the app foreground with an Ionic component.

handled in Ionic

(4b) Handle the notification in the app backgorund on the browser or native device

Web notification on Android Chrome

Web notifications work almost everywhere, including Android Chrome, but the main holdout is iOS Safari, so they will not currently work for iPhone users.

Initial Setup

This lesson is focused on web notifications only. If you need to send notifications on native mobile devices, like iOS and Android, enroll in the Full Ionic Courses.

We have a few initial setup chores needed to get started. First, create a new file for the service worker firebase-messaging-sw.js and add the following code to it:

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

firebase.initializeApp({
'messagingSenderId': 'YOUR_SENDER_ID'
});

const messaging = firebase.messaging();

We need to ensure this file is included in the production build by registering it in the angular.json

// ...omitted
"assets": [
"src/firebase-messaging-sw.js", // <-- here
{
"glob": "**/*",
"input": "src/assets",
"output": "assets"
},
],
// ...omitted

Cloud Functions for Firebase Messaging

At this point, we need a backend that uses the Firebase Admin SDK to subscribe to topics and send messages.

firebase init functions

Subscribe and Unsubscribe to Topics

Callable Cloud Functions for Firebase will allow us to write backend code that can be called directly. They are similar to HTTP functions, but more streamlined by bypassing the things like HTTP clients and auth middleware. We will send a token and topic from our frontend to subscribe to topics.

export const subscribeToTopic = functions.https.onCall(
async (data, context) => {
await admin.messaging().subscribeToTopic(data.token, data.topic);

return `subscribed to ${data.topic}`;
}
);

export const unsubscribeFromTopic = functions.https.onCall(
async (data, context) => {
await admin.messaging().unsubscribeFromTopic(data.token, data.topic);

return `unsubscribed from ${data.topic}`;
}
);

Send a Notification on Firestore Document Create

Another function is needed to send the actual message. In this demo, the notification is broadcast to all subscribed devices when a new document is created in the discounts collection.

The web, Android, and iOS all have different notification APIs, which can be customized in the notification payload.

export const sendOnFirestoreCreate = functions.firestore
.document('discounts/{discountId}')
.onCreate(async snapshot => {
const discount = snapshot.data();

const notification: admin.messaging.Notification = {
title: 'New Discount Available!',
body: discount.headline
};

const payload: admin.messaging.Message = {
notification,
topic: 'discounts'
};

return admin.messaging().send(payload);
});

Customizing the Message

Every platform (web, iOS, and Android) has its own spec for notification formatting. They share common elements - like a title and body - but differ from there. You can customize the message payload passing an object with settings for each platform.

const payload: admin.messaging.Message = {
notification,
webpush: {
notification: {
vibrate: [200, 100, 200],
icon: 'https://angularfirebase.com/images/logo.png',
actions: [
{
action: 'like',
title: '👍 Yaaay!'
},
{
action: 'dislike',
title: 'Boooo!'
}
]
}
},
topic: 'discounts'
};

FCM Service

In Ionic or Angular, I recommend setting up a dedicated messaging service because you will likely need it in multiple components.

ionic g service fcm

At the time of this article there is a bug in AngularFireMessaging. See the comments in the code below to fix it.

import { Injectable } from '@angular/core';
import { AngularFireMessaging } from '@angular/fire/messaging';
import { AngularFireFunctions } from '@angular/fire/functions';
import { ToastController } from '@ionic/angular';
import { tap } from 'rxjs/operators';

// Import firebase to fix temporary bug in AngularFire
import * as app from 'firebase';

@Injectable({
providedIn: 'root'
})
export class FcmService {
token;

constructor(
private afMessaging: AngularFireMessaging,
private fun: AngularFireFunctions,
private toastController: ToastController
) {

// Bind methods to fix temporary bug in AngularFire
try {
const _messaging = app.messaging();
_messaging.onTokenRefresh = _messaging.onTokenRefresh.bind(_messaging);
_messaging.onMessage = _messaging.onMessage.bind(_messaging);
} catch(e)

}


/// METHODS GO HERE ///

}

Show Toast Messages in Ionic

When the app is running we need a good-looking UI element to display the message. An Ionic toast is the perfect tool for this use-case.

async makeToast(message) {
const toast = await this.toastController.create({
message,
duration: 5000,
position: 'top',
showCloseButton: true,
closeButtonText: 'dismiss'
});
toast.present();
}

Get Notification Permission and Show Messages

We need to get permission to send messages for this device, then maintain the token as a property on this service. The second method handles a stream of messages and displays a toast whenever a new notification is emitted.

getPermission(): Observable<any>  {
return this.afMessaging.requestToken.pipe(
tap(token => (this.token = token))
)
}

showMessages(): Observable<any> {
return this.afMessaging.messages.pipe(
tap(msg => {
const body: any = (msg as any).notification.body;
this.makeToast(body);
})
);
}

Use the Callable Functions

We can subscribe the token to a topic by calling our callable functions.

sub(topic) {
this.fun
.httpsCallable('subscribeToTopic')({ topic, token: this.token })
.pipe(tap(_ => this.makeToast(`subscribed to ${topic}`)))
.subscribe();
}

unsub(topic) {
this.fun
.httpsCallable('unsubscribeFromTopic')({ topic, token: this.token })
.pipe(tap(_ => this.makeToast(`unsubscribed from ${topic}`)))
.subscribe();
}

Receiving Messages

The final step is to put our service to use in a component. I would recommend subscribing to notifications in the root app.component.ts to ensure that notifications are always handled in the foreground when the app is being actively used.

@Component({
selector: 'app-root',
templateUrl: 'app.component.html'
})
export class AppComponent {
constructor(
private platform: Platform,
private splashScreen: SplashScreen,
private statusBar: StatusBar,
private fcm: FcmService
) {
this.initializeApp();
}

initializeApp() {
this.platform.ready().then(() => {
this.statusBar.styleDefault();
this.splashScreen.hide();
this.fcm.showMessages().subscribe();
});
}
}

The other methods can be used in the UI component responsible for handling topic subscriptions. Just inject the service publicly and bind the methods to HTML buttons.

import { FcmService } from '../services/fcm.service';

@Component({
selector: 'app-fcm',
templateUrl: './fcm.page.html',
styleUrls: ['./fcm.page.scss']
})
export class FcmPage {
constructor(public fcm: FcmService) {}

getPermission() {
this.fcm.getPermission().subscribe();
}

}
<ion-button (click)="getPermission()">Grant Permission</ion-button>
<ion-button (click)="fcm.sub('discounts')">Subscribe to Topic</ion-button>

Tip: Revoke Notification Permission in Chrome

When developing, it is useful to revoke permission to send messages to ensure the grant permission process is working properly. You can do this by navigating to chrome://settings/content/notifications and removing token for localhost:8100 or whatever URL you’re hosting locally.