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:
parent
49867f5dd5
commit
e3c60d1ef8
|
@ -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(() => {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
56
sw/index.js
56
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 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', {
|
||||
await fetch('/api/graphql', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-type': 'application/json'
|
||||
},
|
||||
body
|
||||
})
|
||||
})
|
||||
await storage.setItem('subscription', JSON.parse(JSON.stringify(newSubscription)))
|
||||
}
|
||||
|
||||
event.waitUntil(subscription)
|
||||
self.addEventListener('pushsubscriptionchange', (event) => {
|
||||
event.waitUntil(handlePushSubscriptionChange(event.oldSubscription, event.newSubscription))
|
||||
}, false)
|
||||
|
|
Loading…
Reference in New Issue