Handling Notifications with Service Workers

1. Registering the Service Worker

A Service Worker acts as a background script that allows our Progressive Web App (PWA) to handle push notifications, caching, and background syncing.

The first step is to register the service worker.

// In app/javascript/packs/application.js or another entry file

if ("serviceWorker" in navigator) {
  navigator.serviceWorker.register("/service_worker.js").then((registration) => {
    console.log("Service Worker registered with scope:", registration.scope);
  }).catch((error) => {
    console.error("Service Worker registration failed:", error);
  });
}

We only need to register the service worker once, so there’s no need to register it again within the service_worker.js file itself.


2. Listening for Push Events in the Service Worker

Inside the service_worker.js file, we’ll handle the push event. The service worker listens for push events and shows notifications to the user.

In app/javascript/service_worker.js, now start listening for push events:

self.addEventListener("push", (event) => {
  const data = event.data.json();
  self.registration.showNotification(data.title, {
    body: data.body,
    icon: "/assets/icon.png",
    badge: "/assets/badge.png"
  });
});

In this code, the push event is triggered when a push message is received.

The event data is parsed, and a notification is shown using showNotification. We can customize the notification’s content and appearance (such as adding an icon and badge).


3. Subscribing Users to Push Notifications

At this point, we have a service worker set up to listen for push notifications, but we need a way to subscribe users to receive these notifications. This involves interacting with the Push API, which allows us to send push messages from the server to the user's browser.

3.1 Creating push_notifications.js to Handle User Subscription

To handle user subscriptions, we need to create a new file, app/javascript/push_notifications.js. This file will contain the JavaScript needed to request permission from the user, subscribe them to the push service, and send the subscription details to our backend server for storage.

Here's how we can structure push_notifications.js:

// In app/javascript/push_notifications.js

async function subscribeUser() {
  // Ensure the service worker is ready
  const registration = await navigator.serviceWorker.ready;

  // Subscribe the user to push notifications
  const subscription = await registration.pushManager.subscribe({
    userVisibleOnly: true,  // Ensures that the notifications are visible to users
    applicationServerKey: "YOUR_PUBLIC_KEY"  // Our VAPID public key
  });

  // Send the subscription details to our server
  await fetch("/push_subscriptions", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(subscription)
  });

  console.log("User subscribed!");
}

// Run the subscription logic once the DOM is loaded
document.addEventListener("DOMContentLoaded", () => {
  if ("serviceWorker" in navigator && "PushManager" in window) {
    subscribeUser();
  }
});

3.2 Explanation of push_notifications.js

In this file, we define the subscribeUser function, which handles the entire user subscription process. Here's a breakdown:

  1. Checking if the Service Worker is Ready:

    • The first thing we do is ensure that the service worker is ready. We achieve this by calling navigator.serviceWorker.ready, which returns a promise resolving to the service worker registration.
  2. Subscribing the User:

    • The pushManager.subscribe method subscribes the user to push notifications. This is done using the VAPID public key (which we generated earlier) to authenticate the subscription.
  3. Sending the Subscription to the Backend:

    • After the user is subscribed, the subscription object (which contains endpoint URLs and encryption keys) is sent to our backend server through a POST request. This ensures that the user’s subscription is saved for future push notifications.
  4. Triggering Subscription Logic:

    • Finally, we wait until the DOM is fully loaded using document.addEventListener("DOMContentLoaded", ...), and then we call subscribeUser() if the browser supports service workers and the PushManager API.

This logic ensures that when the user visits our app, they’ll be asked to opt in for push notifications and, upon approval, be subscribed to them.

3.3 How Push Subscriptions Work with the Backend?

Once a user subscribes to push notifications, the browser generates a unique subscription object containing:

To send a notification, we need a backend service that stores these subscriptions and pushes messages to them.

In a Rails application, we can use the web-push gem to handle this. While the Push API (including pushManager.subscribe) is provided by the browser, the web-push gem is what allows our server to send notifications to these subscriptions.

Here's how we can send push notifications from the Rails backend:

require 'webpush'

#Example: Sending a push notification
def send_push_notification(subscription, message)
  Webpush.payload_send(
    message: message.to_json,
    endpoint: subscription[:endpoint],
    p256dh: subscription[:keys][:p256dh],
    auth: subscription[:keys][:auth],
    vapid: {
      subject: 'mailto:your-email@example.com',
      public_key: ENV['VAPID_PUBLIC_KEY'],
      private_key: ENV['VAPID_PRIVATE_KEY']
    }
  )
end

This method ensures that once the user is subscribed, we can send them push notifications using the web-push gem on the backend.


4. Handling Click Events on Notifications

To enhance user engagement, we need to ensure that clicking a push notification takes the user to the appropriate page in our PWA.

Add the following event listener to service_worker.js:

self.addEventListener("notificationclick", (event) => {
  event.notification.close();
  event.waitUntil(clients.openWindow("https://your-pwa.com"));
});

How It Works

  1. Closing the Notification:

    • The event.notification.close(); line ensures that the notification disappears when clicked.
  2. Opening the PWA in a New Tab:

    • The clients.openWindow("https://your-pwa.com") function opens the specified URL in a new browser tab or focuses on an existing one.

This ensures that clicking on a notification will direct the user to our app, improving navigation and user experience.


With push notifications enabled, our PWA can now engage users even when they are offline!