Sync push subscriptions on every page load (#370)

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 <ek@stacker.news>
This commit is contained in:
ekzyis 2023-08-08 03:03:34 +02:00 committed by GitHub
parent 49867f5dd5
commit e3c60d1ef8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 70 additions and 23 deletions

View File

@ -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(() => {

11
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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)