Demystifying Internet Push Notifications | PQVST
For my current One Day Build: Expense Tracking undertaking I needed to allow notifications in a progressive internet app. ChatGPT struggled to generate any good code for me, and I additionally struggled to seek out any minimal clear explanations on-line.
This weblog submit goals to stroll by way of all of the items wanted to implement internet push notifications. I’ve additionally created a whole minimal working instance with a node.js backend, for individuals who favor to only view the code as an alternative:
https://github.com/pqvst/minimal-web-push
Briefly, internet push works by your app interacting with a “push service” offered by the browser vendor. There are three important steps for this, outlined within the diagram beneath:
Your client-side code creates a Internet Push Subscription, and sends the subscription to your backend. A subscription is just a little bit of JSON that incorporates a singular (browser particular) endpoint and a few encryption keys. Right here’s an instance of what a subscription appears like for Firefox (therefore the mozilla.com endpoint).
{
"endpoint": "https://updates.push.companies.mozilla.com/wpush/v2/...",
"expirationTime": null,
"keys": {
"auth": "...",
"p256dh": "..."
}
}
With Safari you’ll obtain an Apple endpoint (https://internet.push.apple.com/...
) and in Chrome you’ll get a Google endpoint (https://fcm.googleapis.com/fcm/ship/...
).
Your backend code makes use of the subscription particulars to ship a push notification to the push service hosted by the browser vendor. The push service then makes certain to ship it again to your browser.
Your browser receives the push notification and triggers a callback in your service employee. Your service employee can then select to show a notification or do no matter else you wish to do.
VAPID keys are required to ensure Internet Push works on all the primary browsers. VAPID stands for Voluntary Application Server Identification (VAPID) and is basically only a spec for how you can generate a set of public-private keys. Regardless of the identify, they’re actually probably not voluntary, since each Chrome and Safari require you to offer VAPID keys. The one browser I’ve examined that doesn’t require them is Firefox.
In the event you attempt to subscribe to push notifications in Safari with out VAPID keys you’ll obtain the next error:
Subscribing for push requires an applicationServerKey
In Chrome you’ll obtain this:
DOMException: Registration failed - lacking applicationServerKey, and gcm_sender_id not present in manifest
When you can technically generate your VAPID keys by your self, it’s a lot simpler to make use of a generator, like vapidkeys.com, which can generate a set of keys for you.
As a way to ship internet push notifications out of your backend software server, you must assemble, encode, and encrypt the messages correctly. Relying on the programming language you’re utilizing, it’s seemingly you’ll be capable of discover a library that will help you with this.
In the event you’re utilizing a node.js backend then including web-push assist is very easy. There’s a pleasant library known as web-push
that takes care of crafting internet push notifications for you.
1. Import and configure internet push
import webPush from 'web-push';
To get began with web-push
merely name the operate to set your VAPID keys. You’ll want to embody an electronic mail handle as properly (prefixed with mailto:
).
// TODO: Generate VAPID keys (e.g. https://vapidkeys.com/)
const vapid = {
publicKey: '...',
privateKey: '...',
};
webPush.setVapidDetails(
'mailto:<email-address>',
vapid.publicKey,
vapid.privateKey
);
Internet push subscriptions are generated on the consumer facet, so you’ll most probably want some technique to move subscriptions out of your frontend to your backend. You’ll then additionally wish to save these ultimately, for instance by storing it in your database or simply saving them in a persevered JSON file. In the event you don’t save the subscription knowledge you then’ll lose all of your present subscriptions when your server restarts!
app.submit('/subscribe', authenticateRequest, (req, res) => {
const sub = req.physique;
// TODO: Persist subscription (e.g. to db)
res.standing(200).finish();
});
The one different factor you should implement is a technique to truly create and ship new notifications. If we’re broadcasting a notification to all subscriptions then we simply merely loop by way of our array of saved subscriptions and name sendNotification
utilizing the web-push library.
You’ll obtain an error if a consumer has revoked the notification permission in your web page (or if the subscription has expired). You possibly can catch these errors and take away invalid subscriptions.
async operate pushNotification(payload) {
await Promise.all(subscriptions.map(async (sub) => {
attempt {
await webPush.sendNotification(sub, payload); // throws if not profitable
} catch (err) {
console.log(sub.endpoint, '->', err.message);
// TODO: Delete subscription (e.g. from db)
}
}));
}
// Check ship notification
pushNotification('It is a take a look at notification!');
The client-side implementation is a little more sophisticated. You will have two recordsdata: one to your service employee and one to your important client-side software. The one factor we have to put in our server-worker is dealing with the callback for incoming notifications.
self.addEventListener('push', (occasion) => {
const choices = {
physique: occasion.knowledge.textual content(),
icon: '/apple-touch-icon.png',
badge: '/badge.png',
};
occasion.waitUntil(self.registration.showNotification('My App', choices));
});
Why can we wrap our calls in event.waitUntil? Since service-workers run as a background course of, there’s an opportunity that the server employee pauses/terminates it. By wrapping guarantees in
waitUntil
we inform the browser that work is on-going and that it shouldn’t terminate our service employee till the work is completed.
In our important software script we’ve to care for requesting the notifications permission, register our service employee, and really create a push notification subscription utilizing the browser’s pushManager
API.
1. Request notifications permission
First we’d like to ensure we’ve permission to push notifications (with out this our notifications are pointless). Someplace in your web page you’ll most likely wish to show a hyperlink or button that consumer’s can click on to allow notifications.
<a id="promptLink" onclick="onPromptClick()">Allow notifications</a>
If notifications are already granted (or denied) we are able to conceal the hyperlink, or replace the UI accordingly.
operate updatePrompt() {
if ('Notification' in window) {
if (Notification.permission == 'granted' || Notification.permission == 'denied') {
promptLink.fashion.show = 'none';
} else {
promptLink.fashion.show = 'block';
}
}
}
operate onPromptClick() {
if ('Notification' in window) {
Notification.requestPermission().then((permission) => {
updatePrompt();
if (permission === 'granted') {
console.log('Notification permission granted.');
init();
} else if (permission === 'denied') {
console.warn('Notification permission denied.');
}
});
}
}
2. Register Service Employee and Allow Push Notifications
Subsequent we ensure service employees are supported and register our service employee in order that we are able to obtain notifications. Lastly we’ll use the browser pushManager
API to request a push notification subscription, which we’ll then ship to our backend server.
For this step you’ll want your VAPID public key (ensure to solely embody your public key in client-side code, and maintain your personal key secret).
The method is fairly self explanatory. Be sure that service employees are supported, register the service employee, after which test if we have already got an energetic push notification subscription, in any other case, create a brand new subscription.
In each instances we ship the subscription knowledge to our backend to make it possible for it’s saved.
const vapidPublicKey = '...';
async operate initServiceWorker() {
if ('serviceWorker' in navigator) {
const swRegistration = await navigator.serviceWorker.register('sw.js');
const subscription = await swRegistration.pushManager.getSubscription();
if (subscription) {
console.log('Consumer is already subscribed:', subscription);
sendSubscriptionToServer(subscription);
} else {
const subscription = await swRegistration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: vapidPublicKey
});
console.log('Consumer subscribed:', subscription);
sendSubscriptionToServer(subscription);
}
} else {
console.warn('Service employee isn't supported');
}
}
operate sendSubscriptionToServer(subscription) {
fetch('/subscribe', {
technique: 'submit',
physique: JSON.stringify(subscription),
headers: { 'content-type': 'software/json' }
});
}
window.addEventListener('load', () => {
initServiceWorker();
updatePrompt();
});
Debugging Tip: Reloading the Service Employee
Observe that the service employee does not mechanically reload if you reload the web page. In the event you’re working domestically and making modifications to the service employee, you both must manually reload the service employee in your browser’s dev instruments or you may allow the choice to mechanically reload the service employee when the web page reloads!
Bonus Characteristic: Clickable notifications
One other factor you’ll most likely wish to do is make your notifications clickable. Initially I assumed that clicking a notification would mechanically open up the related web page. Nonetheless, this isn’t the case. You’ll need to implement this your self in your service employee.
The code to realize this is a little more sophisticated than I anticipated. Right here’s one of the best instance I managed to seek out on-line, which ensures that the notification is cleared after clicking it, after which both opens a brand new browser occasion/tab or focuses the prevailing tab if it’s already open.
const targetUrl = '...';
self.addEventListener('notificationclick', (occasion) => {
self.console.log('notificationclick');
occasion.notification.shut(); // Android wants specific shut.
occasion.waitUntil(
shoppers.matchAll({sort: 'window'}).then( windowClients => {
// Test if there may be already a window/tab open with the goal URL
for (var i = 0; i < windowClients.size; i++) {
var consumer = windowClients[i];
// If that's the case, simply focus it.
if (consumer.url === targetUrl && 'focus' in consumer) {
return consumer.focus();
}
}
// If not, then open the goal URL in a brand new window/tab.
if (shoppers.openWindow) {
return shoppers.openWindow(targetUrl);
}
})
);
});
From my testing up to now, this appears to work properly on all browsers (Firefox, Chrome, Safari, Android, iOS).