diff --git a/components/serviceworker.js b/components/serviceworker.js index 08ab1840..6bb42e43 100644 --- a/components/serviceworker.js +++ b/components/serviceworker.js @@ -62,6 +62,13 @@ export const ServiceWorkerProvider = ({ children }) => { let pushSubscription = await registration.pushManager.subscribe(subscribeOptions) // convert keys from ArrayBuffer to string pushSubscription = JSON.parse(JSON.stringify(pushSubscription)) + // Send subscription to service worker to save it so we can use it later during `pushsubscriptionchange` + // see https://medium.com/@madridserginho/how-to-handle-webpush-api-pushsubscriptionchange-event-in-modern-browsers-6e47840d756f + navigator.serviceWorker.controller.postMessage({ + action: 'STORE_SUBSCRIPTION', + subscription: pushSubscription + }) + // send subscription to server const variables = { endpoint: pushSubscription.endpoint, p256dh: pushSubscription.keys.p256dh, @@ -89,6 +96,10 @@ export const ServiceWorkerProvider = ({ children }) => { pushManager: 'PushManager' in window }) setPermission({ notification: 'Notification' in window ? window.Notification.permission : 'denied' }) + // since (a lot of) browsers don't support the pushsubscriptionchange event, + // we sync with server manually by checking on every page reload if the push subscription changed. + // see https://medium.com/@madridserginho/how-to-handle-webpush-api-pushsubscriptionchange-event-in-modern-browsers-6e47840d756f + navigator.serviceWorker.controller.postMessage({ action: 'SYNC_SUBSCRIPTION' }) }, []) useEffect(() => { diff --git a/package-lock.json b/package-lock.json index e8bdb320..426c0174 100644 --- a/package-lock.json +++ b/package-lock.json @@ -75,6 +75,7 @@ "remove-markdown": "^0.5.0", "sass": "^1.64.1", "tldts": "^6.0.13", + "serviceworker-storage": "^0.1.0", "typescript": "^5.1.6", "unist-util-visit": "^5.0.0", "url-unshort": "^6.1.0", @@ -16934,6 +16935,11 @@ "node": ">= 0.8.0" } }, + "node_modules/serviceworker-storage": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/serviceworker-storage/-/serviceworker-storage-0.1.0.tgz", + "integrity": "sha512-Vum11Npe8oiFYY05OIhD6obfVP3oCSfBj/NKQGzNLbn6Fr5424j1pv/SvPcbVrDIovdC3EmgGxLgfsLFXgZR1A==" + }, "node_modules/set-cookie-parser": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz", @@ -31637,6 +31643,11 @@ "send": "0.18.0" } }, + "serviceworker-storage": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/serviceworker-storage/-/serviceworker-storage-0.1.0.tgz", + "integrity": "sha512-Vum11Npe8oiFYY05OIhD6obfVP3oCSfBj/NKQGzNLbn6Fr5424j1pv/SvPcbVrDIovdC3EmgGxLgfsLFXgZR1A==" + }, "set-cookie-parser": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz", diff --git a/package.json b/package.json index f72a5174..df82cce8 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,7 @@ "remove-markdown": "^0.5.0", "sass": "^1.64.1", "tldts": "^6.0.13", + "serviceworker-storage": "^0.1.0", "typescript": "^5.1.6", "unist-util-visit": "^5.0.0", "url-unshort": "^6.1.0", diff --git a/sw/index.js b/sw/index.js index c8796442..654caeb6 100644 --- a/sw/index.js +++ b/sw/index.js @@ -5,6 +5,11 @@ import { setDefaultHandler } from 'workbox-routing' import { NetworkOnly } from 'workbox-strategies' import { enable } from 'workbox-navigation-preload' import manifest from './precache-manifest.json' +import ServiceWorkerStorage from 'serviceworker-storage' + +self.__WB_DISABLE_DEV_LOGS = true + +const storage = new ServiceWorkerStorage('sw:storage', 1) // preloading improves startup performance // https://developer.chrome.com/docs/workbox/modules/workbox-navigation-preload/ @@ -80,35 +85,54 @@ self.addEventListener('notificationclick', (event) => { event.notification.close() }) -self.addEventListener('pushsubscriptionchange', (event) => { +// https://medium.com/@madridserginho/how-to-handle-webpush-api-pushsubscriptionchange-event-in-modern-browsers-6e47840d756f +self.addEventListener('message', (event) => { + if (event.data.action === 'STORE_SUBSCRIPTION') { + return event.waitUntil(storage.setItem('subscription', event.data.subscription)) + } + if (event.data.action === 'SYNC_SUBSCRIPTION') { + return event.waitUntil(handlePushSubscriptionChange()) + } +}) + +async function handlePushSubscriptionChange (oldSubscription, newSubscription) { // https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope/pushsubscriptionchange_event + // fallbacks since browser may not set oldSubscription and newSubscription + oldSubscription ??= await storage.getItem('subscription') + newSubscription ??= await self.registration.pushManager.getSubscription() + if (!newSubscription) { + // no subscription exists at the moment + return + } + if (oldSubscription?.endpoint === newSubscription.endpoint) { + // subscription did not change. no need to sync with server + return + } + // convert keys from ArrayBuffer to string + newSubscription = JSON.parse(JSON.stringify(newSubscription)) + const variables = { + endpoint: newSubscription.endpoint, + p256dh: newSubscription.keys.p256dh, + auth: newSubscription.keys.auth, + oldEndpoint: oldSubscription?.endpoint + } const query = ` mutation savePushSubscription($endpoint: String!, $p256dh: String!, $auth: String!, $oldEndpoint: String!) { savePushSubscription(endpoint: $endpoint, p256dh: $p256dh, auth: $auth, oldEndpoint: $oldEndpoint) { id } }` + const body = JSON.stringify({ query, variables }) + await fetch('/api/graphql', { + method: 'POST', + headers: { + 'Content-type': 'application/json' + }, + body + }) + await storage.setItem('subscription', JSON.parse(JSON.stringify(newSubscription))) +} - const subscription = self.registration.pushManager - .subscribe(event.oldSubscription.options) - .then((subscription) => { - // convert keys from ArrayBuffer to string - subscription = JSON.parse(JSON.stringify(subscription)) - const variables = { - endpoint: subscription.endpoint, - p256dh: subscription.keys.p256dh, - auth: subscription.keys.auth, - oldEndpoint: event.oldSubscription.endpoint - } - const body = JSON.stringify({ query, variables }) - return fetch('/api/graphql', { - method: 'POST', - headers: { - 'Content-type': 'application/json' - }, - body - }) - }) - - event.waitUntil(subscription) +self.addEventListener('pushsubscriptionchange', (event) => { + event.waitUntil(handlePushSubscriptionChange(event.oldSubscription, event.newSubscription)) }, false)