DEV Community

ajay upreti
ajay upreti

Posted on

How to use Push Notifications in Angular ?

In this post, we are going to go through a complete example of how to implement Web Push Notifications in an Angular Application using the Angular Service Worker.

Note that these are the exact same native notifications that we receive for example on our mobile phones home screen or desktop, but they are triggered via a web application instead of a native app.

These notifications can even be displayed to the user if all application tabs are closed, thanks to Service Workers! When well used, Push notifications are a great way of having our users re-engage with our application.

This is a step-by-step tutorial, so I invite you to code along as we implement Push Notifications in an existing application.

We will also learn along the way how Push Notifications work in general, as we follow and explain the complete flow that a given notification follows.

The Angular PWA series
Note that this post is part of the Angular PWA Series, here is the complete series:

Service Workers - Practical Guided Introduction (several examples)
Angular App Shell - Boosting Application Startup Performance
Angular Service Worker - Step-By-Step Guide for turning your Application into a PWA
Angular Push Notifications - Step-by-Step Guide

Table of Contents

In this post, we will cover the following topics:

  • How do Push Notifications work?
  • Browser Push Service Providers
  • What is a VAPID key pair?
  • generating a VAPID key pair using node webpush
  • Subscribing to Push Notifications
  • showing again the Allow/Deny Notifications popup
  • Understanding the PushSubscription object and its properties
  • How to use the Push Subscription?
  • Sending Push Notifications from a Node backend
  • Push Notifications In Action - Demo
  • Source Code + Github Running Example (complete Angular PWA) So without further ado, let's get started learning how to handle Push Notifications with the Angular Service Worker.

Introduction to Push Notifications

What we know as Web Push Notifications are actually based on two separate browser standards:

Push API - this is an API that allows messages to be pushed from a server to a browser (even when the site isn't focused or the browser is closed)

Notifications API: displays native system notifications to the user

The Push API is what allows the message to be pushed from a server to the browser, and the Notifications API is what allows the message to be displayed, once it gets to the browser.

But notice that we can't push notifications from our server directly to the user's browser. Instead, only certain servers that browser development companies (like Google, Mozilla, etc.) specifically choose will be able to push notifications to a given browser.

These servers are known as a Browser Push Service. Note that for example, the Push Service used by Chrome is different than the one used by Firefox, and each Push Service is under control of the corresponding browser company.

Browser Push Service Providers

As we sometimes see online, Push Notifications can be very disruptive to the user, and browser implementers want to ensure that browser users have a good online experience at all times.

This means that browser providers want to be able to block out certain notifications from being displayed to the user, for example if the notifications are too frequent.

The way that browsers like Chrome or Firefox ensure that Push messages don't cause user experience issues is to funnel all push messages over servers under their control.

For example, in the case of the Chrome browser, all Push messages come to the browser via the Firebase Cloud Messaging service and NOT directly from our application server.

Firebase Cloud Messaging acts in this case as the Chrome Browser Push Service. The Push Service that each browser uses cannot be changed, and is determined by the browser provider.

In order to be able to deliver a message to a given user and only to that user, the Push Service identifies the user in an anonymous way, ensuring the user privacy. Also, the Push Service does not know the content of the messages, as they are encrypted.

Let's then go through the whole lifecycle of a particular message, to understand in detail how everything works. We will start by uniquely identifying our server, and learn why that's important.

Why identify our server as a Push source?

The first thing that we should do is to uniquely identify our server to the several Browser Push Services available.

Each Push Service will analyze the behavioral patterns of the messages being sent in order to avoid disruptive experiences, so identifying our server and using the push messages correctly over time will increase our odds that the Push Service will deliver our messages in a timely manner.

We will then start by uniquely identifying our Application server using a VAPID key pair.

What is a VAPID key pair?
VAPID stands for Voluntary Application Server Identification for Web Push protocol. A VAPID key pair is a cryptographic public/private key pair that is used in the following way:

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.

Generating a VAPID key pair using node web-push

Let's start by generating a VAPID key using the node webpush library. We will first install the webpush library globally, as a command line tool:

npm install web-push -g
Enter fullscreen mode Exit fullscreen mode

We can then generate a VAPID key pair with the following command:

web-push generate-vapid-keys --json
Enter fullscreen mode Exit fullscreen mode

Using this command, here is what a VAPID key pair looks like:

{
  "publicKey":"BLBx-hf2WrL2qEa0qKb-aCJbcxEvyn62GDTyyP9KTS5K7ZL0K7TfmOKSPqp8vQF0DaG8hpSBknz_x3qf5F4iEFo",
  "privateKey":"PkVHOUKgY29NM7myQXXoGbp_bH_9j-cxW5cO-fGcSsA"
}
Enter fullscreen mode Exit fullscreen mode

Subscribing to Push Notifications

The first thing that we will need is the Angular Service Worker, and for that here is a guide for how to add it to an existing Angular application.

Once we have the Angular Service Worker installed, we can now request the user permission for sending Push Notifications:

@Component({
    selector: 'app-root',
    template: `
        <button class="button button-primary" (click)="subscribeToNotifications()">
          Subscribe
        </button>
`})
export class AppComponent {

    readonly VAPID_PUBLIC_KEY = "BLBx-hf2WrL2qEa0qKb-aCJbcxEvyn62GDTyyP9KTS5K7ZL0K7TfmOKSPqp8vQF0DaG8hpSBknz_x3qf5F4iEFo";

    constructor(
        private swPush: SwPush,
        private newsletterService: NewsletterService) {}

    subscribeToNotifications() {

        this.swPush.requestSubscription({
            serverPublicKey: this.VAPID_PUBLIC_KEY
        })
        .then(sub => this.newsletterService.addPushSubscriber(sub).subscribe())
        .catch(err => console.error("Could not subscribe to notifications", err));
    }
}
Enter fullscreen mode Exit fullscreen mode

Let's break down what is going on in this code sample:

  • the user clicks on the Subscribe button and the subscribeToNotifications() method gets executed

  • using the swPush service, we are going to ask the user if he allows our server (identified by the VAPID public key) to send him Web Push messages

  • the requestSubscription() method returns a Promise which emits the push subscription object, in case the user allows notifications

  • The user is then going to see a browser popup asking him to allow or deny the request:

  • Push Notifications Popup

  • if the user accepts the request, the Promise returned by requestSubscription() is going to be evaluated successfully, and a push subscription object is going to be passed to .then()

Showing again the Allow/Deny Notifications Popup

While testing this on localhost, you might accidentally hit the wrong button in the popup. The next time that you click subscribe, the popup will not be displayed.

Instead, the Promise is going to be rejected and the catch block in our code sample above is going to be triggered.

Here is what we need to do in order to have the popup displayed again:

  • go to chrome://settings/content/notifications
  • scroll down the Block list, containing all the websites that are blocked from emitting push notifications
  • delete localhost from the Block list
  • Click the Subscribe button again
  • The popup should now appear again, and if we click on the Allow option, a Push Subscription object will be generated.

The PushSubscription object
Here is what the push subscription object looks like, as we receive it in the then() clause:

{
  "endpoint": "https://fcm.googleapis.com/fcm/send/cbx2QC6AGbY:APA91bEjTzUxaBU7j-YN7ReiXV-MD-bmk2pGsp9ZVq4Jj0yuBOhFRrUS9pjz5FMnIvUenVqNpALTh5Hng7HRQpcUNQMFblTLTF7aw-yu1dGqhBOJ-U3IBfnw3hz9hq-TJ4K5f9fHLvjY",
  "expirationTime": null,
  "keys": {
    "p256dh": "BOXYnlKnMkzlMc6xlIjD8OmqVh-YqswZdut2M7zoAspl1UkFeQgSLYZ7eKqKcx6xMsGK7aAguQbcG9FMmlDrDIA=",
    "auth": "if-YFywyb4g-bFB1hO9WMw=="
  }
}
Enter fullscreen mode Exit fullscreen mode

view raw03.ts hosted with ❤ by GitHub
Let's now break down the content of the subscription object, as that will help us to understand better how Push Notifications work in general:

  • endpoint: This contains a unique URL to a Firebase Cloud Messaging endpoint. This url is a public but unguessable endpoint to the Browser Push Service used by the application server to send push notifications to this subscription
  • expirationTime: some messages are time sensitive and don't need to be sent if a certain time interval has passed. This is useful in certain cases, for example, if a message might contain an authentication code that expires after 1 minute
  • p256dh: this is an encryption key that our server will use to encrypt the message, before sending it to the Push Service
  • auth: this is an authentication secret, which is one of the inputs of the message content encryption process All the information present in the subscription object is necessary to be able to send push notifications to this user.

Latest comments (4)

Collapse
 
omamaaslam profile image
Omama Aslam

But in my case didn't work, I setup complete MEAN app here is my code :
*Server Side Code *

const webpush = require("web-push");
const express = require("express");
const app = express();
const bodyparser = require("body-parser");
const cors = require("cors");
const port = 3000;
const mongoose = require("mongoose");
mongoose.set("strictQuery", true);

const vapidKeys = {
  publicKey:
    "BE1DOuy5qzVVanYhcD8cnd7NIMuv5PDinH6YO-rwaEGzTax4WWck_CZ9tFPi_zsBeDs5r6IYAP8K1XZ8x8_KW5o",
  privateKey: "SooeMFMBiJ3NTTWCY0NOzCuvuT6-n-Rs8q3vk1_txX8",
};  

webpush.setVapidDetails(
  "mailto:example@yourdomain.org",
  vapidKeys.publicKey,
  vapidKeys.privateKey
);

// Connect to Database.
mongoose.connect(
  "*********************************************************************************",
  { useNewUrlParser: true, useUnifiedTopology: true }
);

// Mongoose Schema
const subscriptionSchema = new mongoose.Schema({
  endpoint: String,
  expirationTime: String,
  keys: {
    auth: String,
    p256dh: String,
  },
});

const Subscription = mongoose.model('Subscription', subscriptionSchema);

app.use(express.json());
app.use(bodyparser.json());
app.use(bodyparser.urlencoded({ extended: false }));
app.use(cors());

// Create Routes....

app.post('/api/subscribe', async (req, res) => {
  const subscription = req.body;
  try {
    const newSubscription = new Subscription(subscription);
    await newSubscription.save();

    // Send a welcome notification to the newly subscribed client
    const notificationPayload = {
      notification: {
        title: 'Welcome!',
        body: 'Thank you for subscribing to our notifications.',
        icon: 'icons/icon-72x72.png',
      }
    };
    sendNotificationToSubscriber(subscription, notificationPayload);
    res.status(201).json({ message: 'Subscription successful' });
  } catch (error) {
    console.error('Error saving subscription:', error);
    res.sendStatus(500);
  }
});

app.get('/api/subscriber', async (req, res) => {
  try {
    const subscriber = await Subscription.find();
    res.send(subscriber);
  } catch (error) {
    console.log(error);
  }
});

function sendNotificationToSubscriber(subscription, notificationPayload) {
  webpush.sendNotification(subscription, JSON.stringify(notificationPayload))
    .then(() => {
      console.log('Push notification sent successfully');
    })
    .catch((error) => {
      console.error('Error sending push notification:', error);
    });
}

app.listen(port, () => console.log("Server Started...."));

Enter fullscreen mode Exit fullscreen mode

Client Side Code (In Angular)

import { NgModule, isDevMode } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { environment } from 'src/environments/environment.production';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { ServiceWorkerModule } from '@angular/service-worker';
import { PushNotificationService } from './service/push-notification.service';
import { HttpClientModule } from '@angular/common/http';
import { MyDirectiveDirective } from './my-directive.directive';
@NgModule({
  declarations: [AppComponent, MyDirectiveDirective],
  imports: [
    BrowserModule,
    AppRoutingModule,
    HttpClientModule,
    ServiceWorkerModule.register('ngsw-worker.js', {
      enabled: environment.production,
    }),
  ],
  providers: [PushNotificationService],
  bootstrap: [AppComponent],
})
export class AppModule {}

**Service file Code**

export class PushNotificationService {
  endPoint: string = 'http://localhost:3000/api/subscribe';
  endPoint1: string = 'http://localhost:3000/api/subscriber';
  constructor(private http: HttpClient) {}

  public sendSubscriptionToTheServer(subscription: PushSubscription) {
    return this.http.post(this.endPoint, subscription);
  }

  public getSubscriberFromDatabase() {
    return this.http.get(this.endPoint1);
  }
}


**app.component.ts code**
export class AppComponent implements OnInit {
  title = 'pwa-frontend';
  AllSubscriber: any = [];
  private publicKey =
    'BE1DOuy5qzVVanYhcD8cnd7NIMuv5PDinH6YO-rwaEGzTax4WWck_CZ9tFPi_zsBeDs5r6IYAP8K1XZ8x8_KW5o';
  constructor(
    private swPush: SwPush,
    private service: PushNotificationService
  ) {}

  pushSubscription() {
    if (!this.swPush.isEnabled) {
      console.log('Notifications is not enabled.');
      return;
    }
    this.swPush
      .requestSubscription({ serverPublicKey: this.publicKey })
      .then((subscription) => {
        // send subscription to the server
        this.service.sendSubscriptionToTheServer(subscription).subscribe();
        console.log(JSON.stringify(subscription));
      })
      .catch((error) => console.log(error));
  }

  getAllSubscriber() {
    this.service.getSubscriberFromDatabase().subscribe((res) => {
      this.AllSubscriber = res;
      console.log("AllSubscriber Object  : " , this.AllSubscriber);
    });
  }

  ngOnInit(): void {}
}

**app.component.html**
<div appMyDirective>
  <button class="btn btn-danger" (click)="pushSubscription()">subscribe</button>
  <button class="btn btn-success" (click)="getAllSubscriber()">Button</button>
</div>

Enter fullscreen mode Exit fullscreen mode
Collapse
 
omamaaslam profile image
Omama Aslam • Edited

In my case the code is exactly same but still not work the subscription endpiont is store inside my mongoDB document but I am not getting the notifications inside my browser.

Collapse
 
kolkov profile image
Andrey Kolkov

But nothing so that we put the secret keys in the user's browser? ;)

Collapse
 
ezequieltejada profile image
Ezequiel E. Tejada

Thank you Ajay! Do you have a link for the rest of the series?