How to add real web push notifications to your webapp - A geek with a hat

November 16, 2017 0 Comments

How to add real web push notifications to your webapp - A geek with a hat

 

 

You’ve probably seen web notifications before. YouTube shows them when it goes to a new song, Facebook pings them when a new message comes in, scammy websites ask for permissions and you say no. The usual.

You can fire those notifications from anywhere inside your JavaScript.

const notification = new Notification(title, { body: body, icon: iconURL  
});

const notification = new Notification(title, { body: body, icon: iconURL });

That creates a new browser notification and shows it to the user. Since Chrome 60-something on a Mac, they’re integrated with native notifications. It’s great.

You can close the browser notification with notification.close, and you can listen for click events with notification.onclick = () => .... Most often you’d want to use that click event to focus on your browser tab.

To make this work however, the user must keep your webapp open and you must keep your fingers crossed the browser hasn’t throttled your JavaScript too much for inactivity. Browsers do that, you know.

But did you know you can also add real push notifications to your webapp? The kind that come from a server and work even with no open tabs? Oh yes, just like a mobile app.

I had no idea this has been part of Chrome since version 40-something. Firefox supports it, too. Safari and Internet Explorer, no.

It is not a part of the JavaScript standard yet. MDN lists web push notification support as experimental.

Here’s what you’ll need to make it work 👇

  • a service worker
  • some code to register the service worker
  • some code to ask for notification permissions
  • bit of code to subscribe to web push notifications
  • a server to trigger the push notification

Service worker that listens for push events

You can think of service workers as a piece of JavaScript that lives in your user’s browser and does stuff. They can do stuff even when your site isn’t loaded, or even open.

Most commonly they’re used for caching in what’s called progressive web apps – PWA. They intercept requests and serve code from a local cache. Even if the browser is offline.

But none of that right now. We’re using them for push notifications.

A service worker will live in our user’s browser and listen for push events. The browser gets these events from a so-called push service, triggers the service worker, and our service worker does whatever.

For security reasons, we can’t control which push service the browser uses. The browser tells us instead. You’ll see how in the section about subscribing.

For privacy reasons, we also have to accept a sort of gentleman’s agreement with the browser where we promise to always show a notification when a push event comes in.

So… a service worker is just a JavaScript file. Nothing fancy going on. You use self to refer to the global scope because there’s no window and no document.

// service-worker.js  
 
function showNotification(event) { return new Promise(resolve => { const { body, title, tag } = JSON.parse(event.data.text());  
  self.registration .getNotifications({ tag }) .then(existingNotifications => { // close? ignore? }) .then(() => { const icon = `/path/to/icon`; return self.registration .showNotification(title, { body, tag, icon }) }) .then(resolve) })
}  
 
self.addEventListener("push", event => { event.waitUntil( showNotification(event) ); }  
});  
 
self.addEventListener("notificationclick", event => { event.waitUntil(clients.openWindow("/"));  
});

// service-worker.js function showNotification(event) { return new Promise(resolve => { const { body, title, tag } = JSON.parse(event.data.text()); self.registration .getNotifications({ tag }) .then(existingNotifications => { // close? ignore? }) .then(() => { const icon = `/path/to/icon`; return self.registration .showNotification(title, { body, tag, icon }) }) .then(resolve) }) } self.addEventListener("push", event => { event.waitUntil( showNotification(event) ); } }); self.addEventListener("notificationclick", event => { event.waitUntil(clients.openWindow("/")); });

That’s all the code that goes into our service worker.

In showNotification, we return a Promise that resolves once we’re done showing our notification.

We use JSON.parse to unpack notification parameters using the convention of putting JSON into the message portion of our push notification.

Before showing our push notification with self.registration.showNotification, we can clear any existing notifications with the same tag using self.registration.getNotifications. This gives us a list of notifications that we can .close.

I’ve noticed (at least on a Mac) that subsequent notifications with the same tag sometimes don’t show up unless you close the previous ones.

We use self.addEventListener to listen for both push and onclick events. In the push handler, we call showNotification to show our web push notification, and in notificationclick we open our site.

That’s the service worker 😄

Register the service worker

Registering said service worker happens in our regular JavaScript code. Usually in the main index.js entry point because there can be only one service worker for an entire domain.

function registerServiceWorker() { navigator.serviceWorker .register('/service-worker.js') .then(registration => { console.log( "ServiceWorker registered with scope:", registration.scope ); }) .catch(e => console.error("ServiceWorker failed:", e));  
}  
 
if (navigator && navigator.serviceWorker) { registerServiceWorker();  
}

function registerServiceWorker() { navigator.serviceWorker .register('/service-worker.js') .then(registration => { console.log( "ServiceWorker registered with scope:", registration.scope ); }) .catch(e => console.error("ServiceWorker failed:", e)); } if (navigator && navigator.serviceWorker) { registerServiceWorker(); }

We call navigator.serviceWorker.registe with the URL of our service worker and wait for the promise to resolve. If all goes well, we print a success message, otherwise we print an error.

This is not a piece of code you’ll have to write often. Once per project at most. Many just find it online and copypasta when they need it, I think.

One caveat to keep in mind is that service workers are limited to the scope they’re served from. You can’t serve that file from a CDN, or from a static.domain.com, or even domain.com/static/.

This is a security precaution.

My solution was to use the Webpack sw-loader and configure it to compile this particular file into a different location and with a different publicPath than all other JavaScript, which goes into CDNs and has fingerprinting and stuff.

That part was tricky, but it’s very specific to every webapp, so it’s not a good candidate for this article 🙂

Ask for web notification permissions

If you’ve ever used web notifications before, you already know this part: You have to ask the user for permission.

Here’s how that goes

const permission = Notification.requestPermission();  
 
if (permission ! 'granted') { // no notifications  
}else{ // yay notifications  
}

const permission = Notification.requestPermission(); if (permission ! 'granted') { // no notifications }else{ // yay notifications }

Yep, that’s it.

Running Notification.requestPermission pops up a little dialog for our user asking them to grant us web notification permissions. If they do, the permission is set to granted. Other options include denied, and I think it stays null if they ignore us.

You can check permission status at any time with Notification.permission.

Subscribe to web push notifications

Here comes the interesting part: subscribing to web push notifications. This is where we ask the browser “Hey, which push service do you wanna use?”

The process has 2 to 3 steps depending on how you count 👇

  1. Create VAPID keys for our server, one-time
  2. Ask browser to subscribe
  3. Save subscription info for our user

VAPID keys are a way for our server to identify itself with the push service. That way our user’s browser can be sure that push notifications are coming from us and not from some random spammer who got in the way.

I used the Webpush gem to generate these keys, there’s a web-push package for node as well.

# One-time, on the server  
vapid_key = Webpush.generate_key  
 
# Save these in your application server settings  
vapid_key.public_key  
vapid_key.private_key

# One-time, on the server vapid_key = Webpush.generate_key # Save these in your application server settings vapid_key.public_key vapid_key.private_key

You will have to send the public_key to your frontend somehow because you’ll need it to subscribe to web push notifications. The subscription itself happens like this:

function subscribeToPushNotifications(registration) { return registration.pushManager .subscribe({ userVisibleOnly: true, applicationServerKey: window.vapidPublicKey }) .then(pushSubscription => { console.log( "Received PushSubscription:", JSON.stringify(pushSubscription) ); return pushSubscription; });  
}

function subscribeToPushNotifications(registration) { return registration.pushManager .subscribe({ userVisibleOnly: true, applicationServerKey: window.vapidPublicKey }) .then(pushSubscription => { console.log( "Received PushSubscription:", JSON.stringify(pushSubscription) ); return pushSubscription; }); }

We call registration.pushManager.subscribe to subscribe with our applicationServerKey. That’s the vapid.public_key. And as I mentioned earlier, we promise to always show a notification with userVisibleOnly: true.

Not a single browser currently supports userVisibleOnly: false because it could lead to privacy issues. Think sending a push and having your service worker send back detailed GPS location for your user. No bueno.

That pushSubscription will have a bunch of data inside. Everything from which API endpoint to use when sending notifications to info about how to authenticate with the service.

The browser is in full control.

You should save that info on the backend and associate it with your user. As far as I can tell, it’s unique for every subscription request and definitely for every browser.

The tricky part here is when the same user is using multiple browsers and devices. If you want to send push notifications to all of them, you’re going to have to keep multiple copies of this info.

Trigger a web push notification

To trigger a web push notification from your server, I suggest using a library. Something like Webpush for Ruby or web-push for node. I’m sure libraries exist for other languages as well.

Here’s how you’d send a notification in Ruby/Rails:

def send_web_push_notification(user_id) subscription = User.find(user_id).web_push_subscription  
  message = { title: "You have a message!", body: "This is the message body", tag: "new-message" }
  unless subscription.nil? Webpush.payload_send( message: JSON.generate(message), endpoint: subscription["endpoint"], p256dh: subscription["keys"]["p256dh"], auth: subscription["keys"]["auth"], ttl: 15, vapid: { subject: 'mailto:admin@example.com', public_key: Rails.application.config.webpush_keys[:public_key], private_key: Rails.application.config.webpush_keys[:private_key] } ) end
end

def send_web_push_notification(user_id) subscription = User.find(user_id).web_push_subscription message = { title: "You have a message!", body: "This is the message body", tag: "new-message" } unless subscription.nil? Webpush.payload_send( message: JSON.generate(message), endpoint: subscription["endpoint"], p256dh: subscription["keys"]["p256dh"], auth: subscription["keys"]["auth"], ttl: 15, vapid: { subject: 'mailto:admin@example.com', public_key: Rails.application.config.webpush_keys[:public_key], private_key: Rails.application.config.webpush_keys[:private_key] } ) end end

We use a database JSON field called web_push_subscription to save the pushSubscription info on our users.

If that field has info, we can use Webpush to send a notification to the API. The API then sends it out to our service worker, which then shows a notification.

Fin

You should now be able to add web push notifications to your webapp. I left out some details around setting up Webpack to serve service worker code correctly, but we can get into that some other day.

If you do decide to add push notifications to your webapp, please use them responsibly. Remember what happened to mobile app notifications and what a mess that has become.

Wouldn’t want to train users to automatically deny notification permissions now would we 🙂

You should follow me on twitter, here.


Tag cloud