DEV Community

Smruti Ranjan Rana
Smruti Ranjan Rana

Posted on • Updated on

Web Push Notification with web-push | Angular & Node JS

Push notifications are a compelling way to engage users.

Push technology, or server push, is a style of Internet-based communication where the request for a given transaction is initiated by the publisher or central server. - Wikipedia

In this article, we will learn how to quickly add push notification in our Angular application with Service Worker.

Service Worker 👷‍♂️

A service worker is a script that your browser runs in the background, separate from a web page, opening the door to features that don't need a web page or user interaction.
Angular Service Worker

Prerequisites 📝

Basic knowledge of Angular & Node JS

So, If you are ready, Let's get started 🚀🚀🚀

Let's Start

Let’s Begin 🏁

Step 1 : Create a server

Let's create a server directory inside our root directory.

.
└───server
Enter fullscreen mode Exit fullscreen mode

Inside /server, run below command to initialize npm.

npm init -y
Enter fullscreen mode Exit fullscreen mode

A package.json file will be generated for you.

{
  "name": "server",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}
Enter fullscreen mode Exit fullscreen mode

Now let's install web-push by running following command.

npm i web-push
Enter fullscreen mode Exit fullscreen mode

Updated package.json

{
  "name": "server",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "web-push": "^3.4.5"
  }
}

Enter fullscreen mode Exit fullscreen mode

Amazing 👏👏👏

Let's create our server file server/index.js

server
    └───index.js
    └───package-lock.json
    └───package.json
    └───node_modules
Enter fullscreen mode Exit fullscreen mode

Import web-push as below

const webpush = require('web-push'); // new
Enter fullscreen mode Exit fullscreen mode

To subscribe push messages, we need to pass VAPID keys.
We can generate VAPID keys as below.

const webpush = require('web-push');

console.log(webpush.generateVAPIDKeys()); // new
Enter fullscreen mode Exit fullscreen mode

Learn more about web push protocol here.

Let's run our server. It will print our keys in the console.

node .
Enter fullscreen mode Exit fullscreen mode

Output

{
  publicKey: '<YOUR_PUBLIC_KEY>',
  privateKey: '<YOUR_PRIVATE_KEY>'
}
Enter fullscreen mode Exit fullscreen mode

Now copy and put these keys in a variable. And remove console.log for generating keys.

const webpush = require('web-push');

const vapidKeys = { // new
  publicKey: '<YOUR_PUBLIC_KEY>', // new
  privateKey: '<YOUR_PRIVATE_KEY>' // new
}; // new
Enter fullscreen mode Exit fullscreen mode

Then create a variable called subscription as below.

// get client subscription config from db
const subscription = {
    endpoint: '',
    expirationTime: null,
    keys: {
        auth: '',
        p256dh: '',
    },
};
Enter fullscreen mode Exit fullscreen mode

endpoint: This contains a unique URL to a messaging server 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, 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.

We'll get the subscription details from client. You can store that subscription config in DB and fetch the details here.

Now let's create payload for the notification.

const payload = {
    notification: {
        title: 'Title',
        body: 'This is my body',
        icon: 'assets/icons/icon-384x384.png',
        actions: [
            { action: 'bar', title: 'Focus last' },
            { action: 'baz', title: 'Navigate last' },
        ],
        data: {
            onActionClick: {
                default: { operation: 'openWindow' },
                bar: {
                    operation: 'focusLastFocusedOrOpen',
                    url: '/signin',
                },
                baz: {
                    operation: 'navigateLastFocusedOrOpen',
                    url: '/signin',
                },
            },
        },
    },
};
Enter fullscreen mode Exit fullscreen mode

The Angular service worker supports the following operations:

openWindow : Opens a new tab at the specified URL, which is resolved relative to the service worker scope.

focusLastFocusedOrOpen : Focuses the last focused client. If there is no client open, then it opens a new tab at the specified URL, which is resolved relative to the service worker scope.

navigateLastFocusedOrOpen : Focuses the last focused client and navigates it to the specified URL, which is resolved relative to the service worker scope. If there is no client open, then it opens a new tab at the specified URL.

Check different payloads here.
You can play with different types of notification here.

Now add our third variable options.

const options = {
    vapidDetails: {
        subject: 'mailto:example_email@example.com',
        publicKey: vapidKeys.publicKey,
        privateKey: vapidKeys.privateKey,
    },
    TTL: 60,
};
Enter fullscreen mode Exit fullscreen mode

At last call sendNotification() method to send notification as below.

// send notification
webpush.sendNotification(subscription, JSON.stringify(payload), options)
    .then((_) => {
        console.log('SENT!!!');
        console.log(_);
    })
    .catch((_) => {
        console.log(_);
    });
Enter fullscreen mode Exit fullscreen mode

Here is our final code looks like.

const webpush = require('web-push');

const vapidKeys = {
  publicKey: '<YOUR_PUBLIC_KEY>',
  privateKey: '<YOUR_PRIVATE_KEY>'
};

// get client subscription config from db
const subscription = {
    endpoint: '',
    expirationTime: null,
    keys: {
        auth: '',
        p256dh: '',
    },
};

const payload = {
    notification: {
        title: 'Title',
        body: 'This is my body',
        icon: 'assets/icons/icon-384x384.png',
        actions: [
            { action: 'bar', title: 'Focus last' },
            { action: 'baz', title: 'Navigate last' },
        ],
        data: {
            onActionClick: {
                default: { operation: 'openWindow' },
                bar: {
                    operation: 'focusLastFocusedOrOpen',
                    url: '/signin',
                },
                baz: {
                    operation: 'navigateLastFocusedOrOpen',
                    url: '/signin',
                },
            },
        },
    },
};

const options = {
    vapidDetails: {
        subject: 'mailto:example_email@example.com',
        publicKey: vapidKeys.publicKey,
        privateKey: vapidKeys.privateKey,
    },
    TTL: 60,
};

// send notification
webpush.sendNotification(subscription, JSON.stringify(payload), options)
    .then((_) => {
        console.log('SENT!!!');
        console.log(_);
    })
    .catch((_) => {
        console.log(_);
    });

Enter fullscreen mode Exit fullscreen mode

Great work so far 💪💪💪

Great Work

Keep this server code as it is for now.
Let's create our fronted.

Step 2 : Create the client

Let's come back to our root directory and run below command to create an angular project client.

ng new client
Enter fullscreen mode Exit fullscreen mode

Now inside client, run the below command to add all the necessary configurations for PWA in our app.

ng add @angular/pwa
Enter fullscreen mode Exit fullscreen mode

Go to app.component.ts and add ngOnInit() method as below.

export class AppComponent implements OnInit {
  title = 'client';

  ngOnInit() {}
}
Enter fullscreen mode Exit fullscreen mode

Import SwPush from @angular/service-worker and add to the constructor.

import { SwPush } from "@angular/service-worker";

export class AppComponent implements OnInit{
  title = 'client';

  constructor(private _swPush: SwPush) {}

  ngOnInit() {}
}
Enter fullscreen mode Exit fullscreen mode

Then create a method requestSubscription() which will request for notification permission and will give us the subscription object.

 requestSubscription = () => {
    if (!this._swPush.isEnabled) {
      console.log("Notification is not enabled.");
      return;
    }

    this._swPush.requestSubscription({
      serverPublicKey: '<VAPID_PUBLIC_KEY_FROM_BACKEND>'
    }).then((_) => {
      console.log(JSON.stringify(_));
    }).catch((_) => console.log);
  };
Enter fullscreen mode Exit fullscreen mode

Call requestSubscription() method in ngOnInit()

  ngOnInit() {
    this.requestSubscription();
  }
Enter fullscreen mode Exit fullscreen mode

Let's build our app to run our application with the Service Worker.

ng build
Enter fullscreen mode Exit fullscreen mode

After build complete, go to dist/client, you will find a file named ngsw-worker.js. That's our service worker.

Now install http-server globally in your machine.

npm i -g http-server
Enter fullscreen mode Exit fullscreen mode

After that go to dist/client in your terminal and run

http-server -p 8000
Enter fullscreen mode Exit fullscreen mode

Now our project is running at localhost:8000.

When we'll open our app, it will ask us for the notification permission.

Notification Permission

Isn't this amazing? 🤩🤩🤩

Amazing

And if we allow, then in console we will get the subscription object.

Subscription object

Now you can call your own api to save this details in DB.

But here we will copy the subscription object generated in our client, and replace the subscription value in our server.

const subscription = {
    endpoint:
        '<CLIENT_ENDPOINT>',
    expirationTime: null,
    keys: {
        p256dh: '<CLIENT_P256DH>',
        auth: '<CLIENT_AUTH>',
    },
};
Enter fullscreen mode Exit fullscreen mode

Now in separate terminal go to /server directory and run

node .
Enter fullscreen mode Exit fullscreen mode

You will immediately get your notification.

Notification

Wow

Now you can play with the click events by clicking the action buttons and the notification itself.

Conclusion 📋

Checkout web-push implementation for different backend technologies -
https://github.com/web-push-libs

Here is my GitHub link for this project - https://github.com/devsmranjan/web-push-notification-demo

Thank you for reading my article 🙂 . I hope you have learned something here.

Happy coding 👨‍💻👩‍💻

Thanks! Don't forget to give a ♥️ and follow :)

Latest comments (9)

Collapse
 
chan_austria777 profile image
chan 🤖

Is the payload object for web-push documented somewhere?
As far as i know, it should be handled on the client side, care to clarify?

Collapse
 
collimarco profile image
Marco Colli

github.com/web-push-libs is great, but it's missing the Ruby library.

You can find a Ruby library for web push here:
github.com/pushpad/web-push

We use it for pushpad.xyz , so it's always up-to-date and widely tested.

Collapse
 
chethancm2001 profile image
chethancm2001

how to send notification to specific user

Collapse
 
tylerjusfly profile image
Tyler • Edited

the Payload is sent to the mail in the options right? and without the angular front end, does it still work?

Collapse
 
thegoke profile image
Kevin Camilo GĂłmez GonzĂĄlez

No, its sent througth endpoint using the encryption keys and auth.

And yes it works, Web push works in differents workarounds, the only thing that you should be caring of is about registering the service worker. (see developer.mozilla.org/en-US/docs/W... for more information)

Collapse
 
ratfou profile image
RatFou

Hi, You don't use "swPush.notificationClicks" to handle click?

Collapse
 
thegoke profile image
Kevin Camilo GĂłmez GonzĂĄlez

You can but when the webpage is open.

If the webpage isn't open you need to modify your service worker and add this (this only works for angular ngsw-worker.js file)

// line 1914
this.scope.addEventListener('notificationclick', (event) => {
event.notification.close();
// Get all the Window clients
event.waitUntil(clients.matchAll({ includeUncontrolled: true, type: 'window' }).then(clientsArr => {
// If a Window tab matching the targeted URL already exists, focus that;
const hadWindowToFocus = clientsArr.some(windowClient => { windowClient.url === event.notification.data[event.action].url ? (windowClient.focus(), true) : false });
// Otherwise, open a new tab to the applicable URL and focus it.
if (!hadWindowToFocus) {
clients.openWindow(event.notification.data[event.action].url).then(windowClient => {
if (windowClient) {
windowClient.focus();
console.log('[Service Worker] - Notification click Received.', event);
}
});
}
}));
this.onClick(event);
});

Collapse
 
thegoke profile image
Kevin Camilo GĂłmez GonzĂĄlez

sorry if i post code in comments, this is my first comment :)

Collapse
 
devsmranjan profile image
Smruti Ranjan Rana

Yes you can use. Here I'm sending data with some default operations which are there in service worker file after you build your angular app.