Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an example to implement Push Notifications? #132

Open
leo-petrucci opened this issue Feb 2, 2024 · 5 comments
Open

Add an example to implement Push Notifications? #132

leo-petrucci opened this issue Feb 2, 2024 · 5 comments

Comments

@leo-petrucci
Copy link

leo-petrucci commented Feb 2, 2024

Hey everyone, great job on the plugin, it's been super useful!

While working on my PWA and this plugin I had a lot of confusion around how to get Push Notifications working, I had a hard time finding the information I needed in the docs so I just wanted to share my thoughts here.

1. There's no examples

When Googling "vite pwa push notifications", these are the results I get:

You get the idea.

2. The docs aren't really very informative

This page is the only mention of push notifications in the docs.

This links to a page that I don't think exists anymore in the Workbox docs (It redirects to this homepage https://web.dev/explore/notifications).

And it also links to the Elk app repository, which is a really good implementation, but it's also a huge and extremely complicated app which is sure to overwhelm anyone that doesn't know what they're looking at.

3. Requiring an understanding of Workbox just to implement notifications

I understand that the docs are trying to push people to read the entirety of the Workbox docs to implement notifications, but to me that seems really overkill.

Push Notifications really only require two listeners, it shouldn't require a developer to learn how to use Workbox in its entirety before they can implement it. After all, we don't expect every developer that use vite-pwa to understand what's going on in their service worker when they first install the plugin, so why should it be different for adding notifications?

Push notifications are arguably the # 1 reason people want to make a PWA, so why make it so difficult for people to find the answer they need?

Actionable points

From this there's a couple of things that I think are missing:

  1. A definite answer in the docs that clearly states that to get push notifications working you need a custom service worker
  2. A minimal custom Service Worker example which does two things:
    a. Implements the basic functionality implemented by the service worker generated by the plugin (Offline support, and whatever else)
    b. Has a minimal implementation of receiving and clicking on notifications

I think we can all agree on the fact that we want PWA adoption to increase, I think doing this will improve the developer experience massively and lower the barrier of entry to making PWAs.

I'm in no way an expert with service workers, however this is my current service worker implementation in Typescript (pieced together from Googling and the Elk repository). If nobody else has a better example I'd love if this was added to the docs.

// ./service-worker/sw.ts

/// <reference lib="WebWorker" />
/// <reference types="vite/client" />
import {
  cleanupOutdatedCaches,
  createHandlerBoundToURL,
  precacheAndRoute,
} from "workbox-precaching";
import { NavigationRoute, registerRoute } from "workbox-routing";

declare const self: ServiceWorkerGlobalScope;

self.addEventListener("message", (event) => {
  if (event.data && event.data.type === "SKIP_WAITING") self.skipWaiting();
});

const entries = self.__WB_MANIFEST;

precacheAndRoute(entries);

// clean old assets
cleanupOutdatedCaches();

// only cache pages and external assets on local build + start or in production
if (import.meta.env.PROD) {
  // to allow work offline
  registerRoute(new NavigationRoute(createHandlerBoundToURL("index.html")));
}

self.addEventListener("push", onPush);
self.addEventListener("notificationclick", onNotificationClick);

export function onPush(event: PushEvent) {
  console.log("[Service Worker] Push Received.");

  if (event.data) {
    const { title, ...rest } = event.data.json();

    event.waitUntil(
      self.registration.showNotification(title, {
        ...rest,
      })
    );
  }
}

export function onNotificationClick(event: NotificationEvent) {
  const reactToNotificationClick = new Promise((resolve) => {
    event.notification.close();
    resolve(openUrl(event.notification.data.url));
  });

  event.waitUntil(reactToNotificationClick);
}

function findBestClient(clients: WindowClient[]) {
  const focusedClient = clients.find((client) => client.focused);
  const visibleClient = clients.find(
    (client) => client.visibilityState === "visible"
  );

  return focusedClient || visibleClient || clients[0];
}

async function openUrl(url: string) {
  const clients = await self.clients.matchAll({ type: "window" });
  // Chrome 42-48 does not support navigate
  if (clients.length !== 0 && "navigate" in clients[0]) {
    const client = findBestClient(clients as WindowClient[]);
    await client.navigate(url).then((client) => client?.focus());
  }

  await self.clients.openWindow(url);
}

And this is my Vite config:

      VitePWA({
        strategies: "injectManifest",
        srcDir: "./service-worker",
        filename: "sw.ts",
        scope: "/",
        devOptions: {
          enabled: mode === "development",
        },
        // ... other configs here
      })

For the record, I'm fully willing to write the documentation page myself if given the go-ahead.

@userquin
Copy link
Member

userquin commented Feb 2, 2024

Push notifications is out of scope from vite-plugin-pwa, check this workbox entry about using strategies: https://developer.chrome.com/docs/workbox/the-ways-of-workbox#when_to_use_injectmanifest

The elk.zone repo has push notifications and web shared target api PWA capabilities, check also the PWA cookbook in the docs for some impl. details:

You also need to deal with permissions and push notification backend registration: https://github.com/elk-zone/elk/tree/main/composables/push-notifications.

Since Elk should deal with multiple backend, the logic has some complex logic (you cannot register multiple push notification from the same app, the browser will prevent multiple registrations, will be treated as spam: https://docs.elk.zone/pwa#push-notifications-subscription-logic).

You can also check mastodon sw impl. (and the logic there using react instead vue): https://github.com/mastodon/mastodon/tree/787279ad67cb9bd06b4628943f19ae8054a60b33/app/javascript/mastodon/service_worker

@leo-petrucci
Copy link
Author

Hey @userquin, thank you for the quick reply.

Push notifications is out of scope from vite-plugin-pwa

That's absolutely understandable, that's why it's not supported by default. However, they are important enough to be mentioned in the documentation.

Is there a specific reason as to why the docs on Push implementation shouldn't be expanded?

As I said I've looked at all the resources and browsed the Discord for a bit, there were quite a few people that were confused by how to do Push. You were there to help them, thankfully, but wouldn't it be better if that info was somewhere official and accessible to all without joining the Discord?

As good as Elk is I don't think linking to the repo and telling people to figure it out is great DX.

@prtk418
Copy link

prtk418 commented Feb 6, 2024

Agree with @leo-petrucci , more details on how to enable push notifications on the doc would really be helpful.

@SavageCore
Copy link

SavageCore commented Apr 9, 2024

Here are my findings for a basic implementation without using injectManifest.

To generate the required VAPID Keys you can use web-push: npx web-push generate-vapid-keys --json

First, you need to create a push service worker listener. This is a separate file that will be imported into the main service worker file generated by workbox.

// ./public/service-worker/push.js

self.addEventListener('push', onPush);

async function onPush(event) {
  if (event.data) {
    const data = event.data.json();
    const { title, ...rest } = data;

    // Send the push data to the application
    const clients = await self.clients.matchAll();
    clients.forEach((client) => client.postMessage(data));

    await event.waitUntil(
      self.registration.showNotification(title, {
        ...rest,
      }),
    );
  }
}

Next, you need to import it in vite.config.ts:

VitePWA({
    ...
    workbox: {
        importScripts: ['/service-worker/push.js'],
        ...
    }
})

Finally, you need to register for notifications in your application code.

//  Request permission for notifications
const permission = await Notification.requestPermission();
if (permission !== 'granted') {
  console.log('Permission not granted for Notification');
  return;
}

const registration = await navigator.serviceWorker.ready;
try {
  const subscription = await registration.pushManager.subscribe({
    userVisibleOnly: true,
    // Replace with your own VAPID public key
    applicationServerKey:
      '',
  });

  // Send the subscription to your server
  // const response = await fetch('/api/subscribe', {
  //   method: 'POST',
  //   headers: {
  //     'Content-Type': 'application/json',
  //   },
  //   body: JSON.stringify(subscription),
  // });

  console.log('User is subscribed:', subscription);
} catch (err) {
  console.log('Failed to subscribe the user: ', err);
}

// Listen for messages from the service worker
navigator.serviceWorker.addEventListener('message', (event) => {
  console.log('Received a message from service worker:', event.data);
});

Example for sending from a server:

const webpush = require('web-push')

webpush.setVapidDetails(
  'mailto:[email protected]',
  process.env.VAPID_PUBLIC_KEY,
  process.env.VAPID_PRIVATE_KEY
)

// pushSubscription is the subscription object you got from the client earlier
webpush.sendNotification(pushSubscription,
  JSON.stringify({
    title: 'Test notification',
    body: 'This is a test notification',
    icon: 'https://example.com/icon.png'
  })
).catch(error => {
  console.error('Error sending push notification:', error);
});

@desarrollosI
Copy link

@SavageCore Dude thank you so much! it worked for me, i was about to cry one week stuck in this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants