Push notification workflow
Implement Web Push Notifications using the service worker and Firebase Messaging
These are the exact same native notifications received on mobile phones, home screen, or desktop but they are triggered via a web application. Your application must have the Service Worker installed to implement push notification. See the PWA workflow guide for implementing the Service Worker.
Update the User interface with a tokens property
File: libs\models\src\lib\user\user.interface.ts
export interface User {
...
fcmTokens?: { [token: string]: true };
...
}
In order to send push notifications, it is required to have a User
document available in our Firestore database that matches the User's uid
.
When a User subscribes to push notifications, the User document will be updated with the token.
Update the project's Web Push certificates
Visit the Project Settings
for your project in the Firebase Console: https://console.firebase.google.com/
Select the Cloud Messaging
tab.

Generate a key pair.

The public key is used as a unique server identifier for subscribing the user to notifications sent by that server.
The private key needs to be kept secret (unlike the public key) and its used by the application server to sign messages, before sending them to the Push Service for delivery.
Once we have the keys generated we can now use the public key to subscribe to Push Notifications using the Service Worker.
Update app manifest
The application manifest file needs to be updated with the new messaging id.
Manifest file: apps\{{project}}\src\manifest.webmanifest
Update to include the following:
{
"name": "{{project}}",
...
"gcm_sender_id": "103953800507" // Add this exact ID
...
}
The gcm_sender_id
is used by some browsers to enable push notifications. It is the same for all projects, this is not your project's sender ID. Reference
Add the Firebase Messaging service worker
The Firebase Messaging service worker listens for messages to act upon.
Create the file: apps\{{project}}\src\firebase-messaging-sw.js
Note: the file must be named exactly as above.
Add the following code to the file:
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable no-undef */
// Give the service worker access to Firebase Messaging.
// Note that you can only use Firebase Messaging here, other Firebase libraries
// are not available in the service worker.
importScripts('https://www.gstatic.com/firebasejs/9.2.0/firebase-app-compat.js');
importScripts('https://www.gstatic.com/firebasejs/9.2.0/firebase-messaging-compat.js');
// Initialize the Firebase app in the service worker by passing in the messagingSenderId.
firebase.initializeApp({
'apiKey': '{{project api key}}',
'appId': '{{project app id}}',
'projectId': '{{project id}}',
'messagingSenderId': '{{project sender id}}'
});
// Retrieve an instance of Firebase Messaging so that it can handle background messages.
const messaging = firebase.messaging();
Verify the latest firebase package versions from the official example.
Update messagingSenderId
with the project's Sender ID, found in the Firebase Console project setting's Cloud Messaging
tab.

Update project.json
Update apps\{{project}}\project.json
to include the new service worker file asset.
"assets": [
...
"apps/{{project}}/src/firebase-messaging-sw.js",
...
],
Create the Firebase Messaging service
Run the below command to create the Firebase Messaging service library:
nx g lib --name=firebase-messaging --directory=services
The following files will be generated:
libs
- services
...
- firebase-messaging
- src
- lib
- services-firebase-messaging.module.ts
- index.ts
- test-setup.ts
- .eslintrc.json
- jest.config.ts
- project.json
- README.md
- tsconfig.json|lib|spec
Delete
the services-firebase-messaging.module.ts
file.
Update
the index.ts
file to remove the module export.
Run the below command to create the Firebase Messaging Service
nx g service --name=firebase-messaging --path=libs/services/firebase-messaging/src/lib --project=services-firebase-messaging
Update
the index.ts
file to export the firebase messaging service file.
export * from './lib/firebase-messaging.service';
Update
the service with the following code:
import { Injectable } from '@angular/core';
import { AngularFireMessaging } from '@angular/fire/compat/messaging';
import { AccountFacade } from '@full-stack-typescript/features/account';
import { getMessaging } from 'firebase/messaging';
import { BehaviorSubject } from 'rxjs';
/* eslint-disable @typescript-eslint/no-non-null-assertion */
@Injectable({
providedIn: 'root'
})
export class FirebaseMessagingService {
private messaging = getMessaging();
currentMessage = new BehaviorSubject<any>(null);
constructor(
private accountFacade: AccountFacade,
private angularFireMessaging: AngularFireMessaging
) {}
// receiveMessage listens for new messages and updates the currentMessage
// with the newest notification received
receiveMessage () {
this.angularFireMessaging.messages.subscribe((message: any) => {
console.log('New Message Received:::', message);
this.currentMessage.next(message);
});
}
// requestPermission provides the latest token the user has subscribed to
// and dispatches an action to update the user's account with the token
requestPermission () {
this.angularFireMessaging.requestToken.subscribe((token) => {
console.log('TOKEN:::', token);
this.accountFacade.updateAccountTokens(token!);
}),
(error: any) => {
console.log('Request Permission ERROR:::', error);
}
}
}
Prepare the Account state for managing the tokens
User tokens will be managed on the User
document in the Firestore database. Therefore, we will need to update the AccountState
to handle the new token management process.
Update Account actions
File: libs\features\account\src\lib+state\account.actions.ts
New actions:
export const updateAccountTokens = createAction(
'[Account] Update account tokens',
props<{ token: string }>()
);
export const updateAccountTokensFailure = createAction(
'[Account] Update account tokens Failure',
props<{ error: any }>()
);
export const updateAccountTokensSuccess = createAction(
'[Account] Update account tokens Success'
);
Update Account reducer
File: libs\features\account\src\lib+state\account.reducer.ts
Add the updateAccountTokensFailure
on(
accountActions.loadAccountFailure,
accountActions.updateAccountFailure,
accountActions.updateAccountTokensFailure, // New
(state, { error }) => ({
...state,
error,
isLoaded: false,
isLoading: false
})),
Update Account selectors
File: libs\features\account\src\lib+state\account.selectors.ts
New selector:
export const getAccountTokens = createSelector(
getAccountState,
(state: AccountState) => state.fcmTokens
);
Update Account facade
File: libs\features\account\src\lib+state\account.facade.ts
tokens$ = this.store.pipe(select(accountSelectors.getAccountTokens));
...
updateAccountTokens(token: string) {
this.store.dispatch(updateAccountTokens({ token }));
}
Update Account effects
File: libs\features\account\src\lib+state\account.effects.ts
New effect:
updateAccountTokens$ = createEffect(() =>
this.actions$.pipe(
ofType(accountActions.updateAccountTokens),
concatMap((action) =>
of(action).pipe(withLatestFrom(this.account.pipe(select(getAccountTokens))))
),
exhaustMap(([{ token }, accountTokens = {}]) => {
const currentTokens = Object.keys(accountTokens);
const tokenExists = currentTokens.includes(token);
if (tokenExists) {
return of(accountActions.updateAccountTokensSuccess());
} else {
return this.accountService.updateAccountToken(token).pipe(
map(() => {
return accountActions.updateAccountTokensSuccess();
}),
catchError((error) => of(accountActions.updateAccountTokensFailure({ error })))
);
}
})
)
);
The updateAccountTokens$
effect manages whether or not we should send a request to the backend to update the User's tokens.
If the token already exists in the User document, a request will not be sent. However, if the User has created a new subscription (could be a new device, refreshed token subscription, or they uninstalled/reinstalled the app) a request will be made to the backend to update the User document.
This is an important step to reduce costs.
Update Account service
File: libs\services\account\src\lib\account.service.ts
Add the new method that will interact with the Cloud Function that updates the User document with the latest tokens. Interacting with a Cloud Function, rather than allowing the client to update documents in the database directly, is the preferred and secure way to interact with the database.
updateAccountToken(token: string): Observable<string> {
const callable = this.fns.httpsCallable('onUpdateAccountTokens');
const result = callable(token);
return result as Observable<string>;
}
Create Cloud Function to manage new tokens
Since the tokens are stored in the User
document, a new Account
Cloud Function is needed to update the document.
In the account functions folder, create a on-update-account-tokens.ts
file.
Location: apps\full-stack-typescript-api\src\functions\account
import * as admin from 'firebase-admin';
import { getDocData } from '../../utils/get-doc-data';
const firestoreInstance = admin.firestore();
export async function onUpdateAccountTokens(data: string, context) {
const batch = firestoreInstance.batch(); // Get a new write batch
const serverTimestamp = admin.firestore.Timestamp.now();
const token = data;
const uid = context.auth.uid;
const userDoc = firestoreInstance.collection('users').doc(uid);
const userDocData = await getDocData(userDoc);
const currentTokens = userDocData.fcmTokens || {};
const updatedTokens = { ...currentTokens, [token]: true };
batch.set(userDoc, {
...userDocData,
fcmTokens: updatedTokens,
updatedAt: serverTimestamp
}, { merge: true });
return batch.commit();
};
Update
the index.ts
file to export the new function.
File: apps\full-stack-typescript-api\src\functions\account\index.ts
export * from './on-update-account';
export * from './on-update-account-tokens';
Update
the main.ts
file to include the new function.
File: apps\full-stack-typescript-api\src\main.ts
...
export const onUpdateAccountTokens = functions.https.onCall((data, context) => {
return accountFunctions.onUpdateAccountTokens(data, context);
});
Decide when to request and manage tokens
Push notifications are only sent when we have an Authenticated
user and that user's document (account details) has been loaded. There are many strategies to decide on when to manage the user tokens.
The Full Stack Typescript repository uses the app-toolbar
as a strategy to help manage this process. The app-toolbar
receives authentication
and account
inputs to emit output events for loading and managing account information. It is achieved by the following:
File: libs\components\app-toolbar\src\lib\app-toolbar.component.ts
export class AppToolbarComponent implements OnChanges {
@Input() isAccountLoaded: boolean | null = false;
@Input() isAuthenticated: boolean | null = false;
@Output() isAuthed = new EventEmitter();
@Output() isAccountDetailsLoaded = new EventEmitter();
ngOnChanges (changes: SimpleChanges) {
const authChanges = changes['isAuthenticated'];
const accountLoadedChanges = changes['isAccountLoaded'];
// If there are authentication changes, check to see if the user
// has just logged in
if (authChanges) {
this.authenticated(authChanges);
}
// If there are account loaded changes, check to see if the user
// has just loaded their account
if (accountLoadedChanges) {
this.accountLoaded(accountLoadedChanges)
}
}
authenticated (changes: SimpleChange) {
const currentValue = changes.currentValue;
const previousValue = !changes.previousValue;
// If the user was not authenticated and has now become
// authenticated, emit the isAuthed event
if (previousValue && currentValue) {
this.isAuthed.emit();
}
}
accountLoaded (changes: SimpleChange) {
const currentValue = changes.currentValue;
const previousValue = !changes.previousValue;
// If the account details were not loaded and have not become
// loaded, emit the isAccountDetailsLoaded event
if (previousValue && currentValue) {
this.isAccountDetailsLoaded.emit();
}
}
}
Update the App module
The account
feature should be moved to an eagerly loaded strategy in order to access the state immediately.
File: apps\full-stack-typescript\src\app\app.module.ts
Update
the imports to include the AccountModule
imports: [
...
AccountModule,
...
],
Update the App component
The app.component
manages the dispatching of actions and retrieving information from the store to communicate with the app-toolbar
File: apps\full-stack-typescript\src\app\app.component.ts
Update
to include:
isAccountLoaded$ = this.accountFacade.isLoaded$;
isAuthenticated$ = this.authFacade.isAuthenticated$;
constructor (
private accountFacade: AccountFacade,
private authFacade: AuthFacade,
private firebaseMessagingService: FirebaseMessagingService
) {};
// When the user's account details are loaded (user document from the database
// the app-toolbar emits an event to execute the isAccountDetailsLoaded
// method which dispatches service calls to prompt the user to subscribe
// to push notifications, retrieve the user's subscription token,
// listen for new messages (notifications), and retrieve the current message
isAccountDetailsLoaded () {
this.firebaseMessagingService.requestPermission();
this.firebaseMessagingService.receiveMessage();
this.message = this.firebaseMessagingService.currentMessage;
}
// When a user becomes authenticated, the app-toolbar emits an event to
// execute the isAuthed method which then dispatches an action to load
// the user account details (User document from the database)
isAuthed () {
this.accountFacade.loadAccount();
}
Update the App template
The app-toolbar
emits events to communicate to the parent component when to dispatch actions such as loading the user account details, prompting the user to subscribe to notifications, and loading statuses.
File: apps\full-stack-typescript\src\app\app.component.html
<full-stack-typescript-app-toolbar
(isAccountDetailsLoaded)="isAccountDetailsLoaded()"
(isAuthed)="isAuthed()"
[isAccountLoaded]="isAccountLoaded$ | async"
[isAuthenticated]="isAuthenticated$ | async"
></full-stack-typescript-app-toolbar>
Deploy latest changes
Push notifications can be tested through localhost
. Build the latest Cloud Function updates and deploy to Firebase to test the push notifications.
Run: nx build {{api project}}
Example: nx build full-stack-typescript-api
After a successful build, run:
firebase deploy --only functions
Subscribe to push notifications
Run: nx serve {{project}}
Example: nx serve full-stack-typescript
Navigate
to http://localhost:4200
Login
as a user
When prompted click Allow
to subscribe to push notifications
Test push notifications
Navigate
to Messaging
in the Firebase cloud console:
Firebase console: https://console.firebase.google.com/

Click
Create your first campaign

Select
Firebase Notification messages and Click
Create

Enter
a Notification title and Notification text then Click
Send test message

Add
your subscription token and click Test

You should receive a push notification on the device you subscribed with.
Last updated