With the foundation for SSL and push notifications in place, it’s time to enable client-side notifications. Here’s the standard workflow:
Let’s break it down:
To prevent spam, push notification requests must be triggered by a user action. Additionally, we’ll use the previously created VAPID keys. This logic will live in a Stimulus controller:
// app/javascript/push_notifictions_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
prompt(e) {
alert("permissions")
}
get vapidKey() {
return document.querySelector('meta[name="vapid_key"]')?.content
}
}
Add the key to the <head>
of your application layout and create a button to trigger the controller:
<head>
<%= tag.meta name: :vapid_key, content: ENV["VAPID_KEY"] %>
</head>
<body data-controller="push-notifications">
<button data-action="push-notifications#prompt">Enable Notifications</button>
</body>
When the button is clicked, we’ll request permission from the user and handle their response:
// app/javascript/push_notifictions_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
prompt(e) {
e.preventDefault()
// bail out early if not available or already granted
if (window.Notification?.permission === "granted" ||
!navigator.serviceWorker) return;
Notification.requestPermission().then(permission => {
if (permission !== "granted") {
console.error("Push notifications declined", permission)
// other error handling/prompts
return
}
this.setupPushSubscription()
})
}
setupPushSubscription() {}
}
Once permission is granted, create a subscription and submit it to the backend. Using @rails/request.js
simplifies this process:
// app/javascript/push_notifictions_controller.js
import { Controller } from "@hotwired/stimulus"
import { post } from '@rails/request.js'
export default class extends Controller {
async setupPushSubscription() {
// register our previously created service worker
navigator.serviceWorker.register("/service-worker")
const serviceWorkerRegistration = await navigator.serviceWorker.ready
await serviceWorkerRegistration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: this.vapidKey
})
this.saveSubscription()
}
async saveSubscription() {
const registration = await navigator.serviceWorker.ready
const subscription = await registration.pushManager.getSubscription()
if (!subscription) return
const response = await post("/web_push_subscriptions", {
body: JSON.stringify(subscription)
})
}
}
To store the subscription tokens, we’ll create a model:
rails generate model WebPushSubscription user:references endpoint auth p256dh user_agent
class WebPushSubscription < ApplicationController
def create
subscription = Current.user.web_push_subscriptions.find_or_initialize_by(
endpoint: params[:endpoint],
auth: params[:keys][:auth],
p256dh: params[:keys][:p256dh]
)
subscription.user_agent = request.user_agent
subscription.save!
head :ok
end
end
We are using find_or_initialize_by
to allow multiple submissions with the same subscription values.
We can add a simple method to our subscription to send a hash to our device. I’ll usually wrap this with an additional method to help standardize around specific keyword arguments:
class WebPushSubscription < ApplicationRecord
belongs_to :user
def send_notification(title: nil, url: nil) # add option standardized keys
data = {
title: title,
url: url
}.compact_blank
deliver_payload(data)
end
def deliver_payload(data)
WebPush.payload_send(
message: data.to_json,
endpoint: endpoint,
p256dh: p256dh,
auth: auth,
vapid: {
public_key: ENV.fetch("VAPID_PUBLIC"),
private_key: ENV.fetch("VAPID_PRIVATE")
}
)
end
end
Once we visit and enable notifications, we should be able to test this from the rails console
:
WebPushSubscription.last.send_notification(title: "Hello World")