From e3c60d1ef8bbe9549f90153a7b573bc82f238a26 Mon Sep 17 00:00:00 2001 From: ekzyis <27162016+ekzyis@users.noreply.github.com> Date: Tue, 8 Aug 2023 03:03:34 +0200 Subject: [PATCH] Sync push subscriptions on every page load (#370) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Most browsers don't support the pushsubscriptionchange event. We workaround this by saving the current push subscription in IndexedDB so we can check during every page load if the push subscription changed. If that is the case, we manually sync the push subscription with the server. However, this solution is not perfect as mentioned in https://medium.com/@madridserginho/how-to-handle-webpush-api-pushsubscriptionchange-event-in-modern-browsers-6e47840d756f which was used for reference: > This solution is not perfect, the user could lose some push notifications if he doesn’t open the webapp for a long time. Co-authored-by: ekzyis --- components/serviceworker.js | 11 ++++++ package-lock.json | 11 ++++++ package.json | 1 + sw/index.js | 70 +++++++++++++++++++++++++------------ 4 files changed, 70 insertions(+), 23 deletions(-) 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)