# Push notification workflow

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](/full-stack-typescript/workflows/pwa-workflow.md) for implementing the Service Worker.

* [Update the User interface with a tokens property](#update-the-user-interface-with-a-tokens-property)
* [Update the project's Web Push certificates](#update-the-projects-web-push-certificates)
* [Update app manifest](#update-app-manifest)
* [Add the Firebase Messaging service worker](#add-the-firebase-messaging-service-worker)
* [Update project.json](#update-project.json)
* [Prepare the Account state for managing the tokens](#prepare-the-account-state-for-managing-the-tokens)
  * [Update Account actions](#update-account-actions)
  * [Update Account reducer](#update-account-reducer)
  * [Update Account selectors](#update-account-selectors)
  * [Update Account facade](#update-account-facade)
  * [Update Account effects](#update-account-effects)
* [Update Account service](#update-account-service)
* [Create Cloud Function to manage new tokens](#create-cloud-function-to-manage-new-tokens)
* [Decide when to request and manage tokens](#decide-when-to-request-and-manage-tokens)
  * [Update the App module](#update-the-app-module)
  * [Update the App component](#update-the-app-component)
  * [Update the App template](#update-the-app-template)
* [Subscribe to push notifications](#subscribe-to-push-notifications)
* [Test push notifications](#test-push-notifications)

## Update the User interface with a tokens property

File: `libs\models\src\lib\user\user.interface.ts`

```typescript
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.

<figure><img src="/files/EdvYVaYtESt5S82ktXBX" alt=""><figcaption></figcaption></figure>

Generate a key pair.

<figure><img src="/files/3VnYAhvJO5WEbpGZ7j8d" alt=""><figcaption></figcaption></figure>

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:

```json
{
  "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](https://github.com/firebase/quickstart-js/blob/master/messaging/manifest.json)

## 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`&#x20;

**Note:** the file must be named exactly as above.

Add the following code to the file:

```javascript
/* 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](https://github.com/firebase/quickstart-js/blob/master/messaging/firebase-messaging-sw.js).

Update `messagingSenderId` with the project's Sender ID, found in the Firebase Console project setting's `Cloud Messaging` tab.

<figure><img src="/files/R5bSpr8cnuUnWH0UrsbZ" alt=""><figcaption></figcaption></figure>

## Update project.json

Update `apps\{{project}}\project.json` to include the new service worker file asset.

```json
        "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.

```typescript
export * from './lib/firebase-messaging.service';
```

`Update` the service with the following code:

```typescript
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:

```typescript
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`

```typescript
    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:

```typescript
export const getAccountTokens = createSelector(
    getAccountState,
    (state: AccountState) => state.fcmTokens
);
```

### Update Account facade

File: `libs\features\account\src\lib+state\account.facade.ts`

```typescript
    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:

```typescript
	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.

```typescript
	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`

```typescript
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`

```typescript
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`

```typescript
...
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`

```typescript
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`

```typescript
  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:

```typescript
	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`

```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/>

<figure><img src="/files/dbDPbfYIZikyDFXn2AUc" alt=""><figcaption></figcaption></figure>

`Click` Create your first campaign

<figure><img src="/files/b1AYmSAtMfOZjePFdrB3" alt=""><figcaption></figcaption></figure>

`Select` Firebase Notification messages and `Click` Create

<figure><img src="/files/bv35ZUDg5ePuVlMjNP57" alt=""><figcaption></figcaption></figure>

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

<figure><img src="/files/i8jFqzlyzAiJCweWvISt" alt=""><figcaption></figcaption></figure>

`Add` your subscription token and click Test

<figure><img src="/files/50EVWlV0jDFDvIrnIIUe" alt=""><figcaption></figcaption></figure>

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


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://flignats.gitbook.io/full-stack-typescript/workflows/push-notification-workflow.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
