Resubscribe if service worker lost push subscription (#597)

* Also delete push subscription in IndexedDB

* Fix pushsubscriptionchange function signature

* Send SYNC_SUBSCRIPTION after successful registration

* Resubscribe if service worker lost subscription

---------

Co-authored-by: ekzyis <ek@stacker.news>
This commit is contained in:
ekzyis 2023-11-05 22:08:44 +01:00 committed by GitHub
parent 7040dbfce6
commit 5dfeb700bc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 44 additions and 12 deletions

View File

@ -88,6 +88,9 @@ export const ServiceWorkerProvider = ({ children }) => {
const { endpoint } = subscription
logger.info('unsubscribed from push notifications', { endpoint })
await deletePushSubscription({ variables: { endpoint } })
// also delete push subscription in IndexedDB so we can tell if the user disabled push subscriptions
// or we lost the push subscription due to a bug
navigator.serviceWorker.controller.postMessage({ action: 'DELETE_SUBSCRIPTION' })
logger.info('deleted push subscription from server', { endpoint })
}
@ -106,24 +109,36 @@ 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' })
logger.info('sent SYNC_SUBSCRIPTION to service worker')
}, [])
useEffect(() => {
if (!support.serviceWorker) {
if (!('serviceWorker' in navigator)) {
logger.info('device does not support service worker')
return
}
const wb = new Workbox('/sw.js', { scope: '/' })
wb.register().then(registration => {
logger.info('service worker registration successful')
setRegistration(registration)
})
}, [support.serviceWorker])
}, [])
useEffect(() => {
// wait until successful registration
if (!registration) return
// setup channel between app and service worker
const channel = new MessageChannel()
navigator?.serviceWorker?.controller?.postMessage({ action: 'ACTION_PORT' }, [channel.port2])
channel.port1.onmessage = (event) => {
if (event.data.action === 'RESUBSCRIBE') {
return subscribeToPushNotifications()
}
}
// 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' })
logger.info('sent SYNC_SUBSCRIPTION to service worker')
}, [registration])
return (
<ServiceWorkerContext.Provider value={{ registration, support, permission, requestNotificationPermission, togglePushSubscription }}>

View File

@ -7,6 +7,7 @@ const storage = new ServiceWorkerStorage('sw:storage', 1)
// for communication between app and service worker
// see https://developer.mozilla.org/en-US/docs/Web/API/MessageChannel
let messageChannelPort
let actionChannelPort
// keep track of item ids where we received a MENTION notification already to not show one again
const itemMentions = []
@ -103,14 +104,23 @@ export function onNotificationClick (sw) {
export function onPushSubscriptionChange (sw) {
// https://medium.com/@madridserginho/how-to-handle-webpush-api-pushsubscriptionchange-event-in-modern-browsers-6e47840d756f
return async (oldSubscription, newSubscription) => {
// `isSync` is passed if function was called because of 'SYNC_SUBSCRIPTION' event
// this makes sure we can differentiate between 'pushsubscriptionchange' events and our custom 'SYNC_SUBSCRIPTION' event
return async (event, isSync) => {
let { oldSubscription, newSubscription } = event
// https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope/pushsubscriptionchange_event
// fallbacks since browser may not set oldSubscription and newSubscription
messageChannelPort?.postMessage({ message: '[sw:handlePushSubscriptionChange] invoked' })
oldSubscription ??= await storage.getItem('subscription')
newSubscription ??= await sw.registration.pushManager.getSubscription()
if (!newSubscription) {
// no subscription exists at the moment
if (isSync && oldSubscription) {
// service worker lost the push subscription somehow
messageChannelPort?.postMessage({ message: '[sw:handlePushSubscriptionChange] service worker lost subscription' })
actionChannelPort?.postMessage({ action: 'RESUBSCRIBE' })
return
}
// no subscription exists at the moment
messageChannelPort?.postMessage({ message: '[sw:handlePushSubscriptionChange] no existing subscription found' })
return
}
@ -148,6 +158,10 @@ export function onPushSubscriptionChange (sw) {
export function onMessage (sw) {
return (event) => {
if (event.data.action === 'ACTION_PORT') {
actionChannelPort = event.ports[0]
return
}
if (event.data.action === 'MESSAGE_PORT') {
messageChannelPort = event.ports[0]
}
@ -157,7 +171,10 @@ export function onMessage (sw) {
return event.waitUntil(storage.setItem('subscription', event.data.subscription))
}
if (event.data.action === 'SYNC_SUBSCRIPTION') {
return event.waitUntil(onPushSubscriptionChange(sw)(event.oldSubscription, event.newSubscription))
return event.waitUntil(onPushSubscriptionChange(sw)(event, true))
}
if (event.data.action === 'DELETE_SUBSCRIPTION') {
return event.waitUntil(storage.removeItem('subscription'))
}
}
}