e3c60d1ef8
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>
123 lines
4.7 KiB
JavaScript
123 lines
4.7 KiB
JavaScript
import { createContext, useContext, useEffect, useState, useCallback } from 'react'
|
|
import { Workbox } from 'workbox-window'
|
|
import { gql, useMutation } from '@apollo/client'
|
|
|
|
const applicationServerKey = process.env.NEXT_PUBLIC_VAPID_PUBKEY
|
|
|
|
const ServiceWorkerContext = createContext()
|
|
|
|
export const ServiceWorkerProvider = ({ children }) => {
|
|
const [registration, setRegistration] = useState(null)
|
|
const [support, setSupport] = useState({ serviceWorker: undefined, pushManager: undefined })
|
|
const [permission, setPermission] = useState({ notification: undefined })
|
|
const [savePushSubscription] = useMutation(
|
|
gql`
|
|
mutation savePushSubscription(
|
|
$endpoint: String!
|
|
$p256dh: String!
|
|
$auth: String!
|
|
) {
|
|
savePushSubscription(
|
|
endpoint: $endpoint
|
|
p256dh: $p256dh
|
|
auth: $auth
|
|
) {
|
|
id
|
|
}
|
|
}
|
|
`)
|
|
const [deletePushSubscription] = useMutation(
|
|
gql`
|
|
mutation deletePushSubscription($endpoint: String!) {
|
|
deletePushSubscription(endpoint: $endpoint) {
|
|
id
|
|
}
|
|
}
|
|
`)
|
|
|
|
// I am not entirely sure if this is needed since at least in Brave,
|
|
// using `registration.pushManager.subscribe` also prompts the user.
|
|
// However, I am keeping this here since that's how it's done in most guides.
|
|
// Could be that this is required for the `registration.showNotification` call
|
|
// to work or that some browsers will break without this.
|
|
const requestNotificationPermission = useCallback(() => {
|
|
// https://web.dev/push-notifications-subscribing-a-user/#requesting-permission
|
|
return new Promise(function (resolve, reject) {
|
|
const permission = window.Notification.requestPermission(function (result) {
|
|
resolve(result)
|
|
})
|
|
if (permission) {
|
|
permission.then(resolve, reject)
|
|
}
|
|
}).then(function (permission) {
|
|
setPermission({ notification: permission })
|
|
if (permission === 'granted') return subscribeToPushNotifications()
|
|
})
|
|
})
|
|
|
|
const subscribeToPushNotifications = async () => {
|
|
const subscribeOptions = { userVisibleOnly: true, applicationServerKey }
|
|
// Brave users must enable a flag in brave://settings/privacy first
|
|
// see https://stackoverflow.com/a/69624651
|
|
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,
|
|
auth: pushSubscription.keys.auth
|
|
}
|
|
await savePushSubscription({ variables })
|
|
}
|
|
|
|
const unsubscribeFromPushNotifications = async (subscription) => {
|
|
await subscription.unsubscribe()
|
|
const { endpoint } = subscription
|
|
await deletePushSubscription({ variables: { endpoint } })
|
|
}
|
|
|
|
const togglePushSubscription = useCallback(async () => {
|
|
const pushSubscription = await registration.pushManager.getSubscription()
|
|
if (pushSubscription) return unsubscribeFromPushNotifications(pushSubscription)
|
|
return subscribeToPushNotifications()
|
|
})
|
|
|
|
useEffect(() => {
|
|
setSupport({
|
|
serviceWorker: 'serviceWorker' in navigator,
|
|
notification: 'Notification' in window,
|
|
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(() => {
|
|
if (!support.serviceWorker) return
|
|
const wb = new Workbox('/sw.js', { scope: '/' })
|
|
wb.register().then(registration => {
|
|
setRegistration(registration)
|
|
})
|
|
}, [support.serviceWorker])
|
|
|
|
return (
|
|
<ServiceWorkerContext.Provider value={{ registration, support, permission, requestNotificationPermission, togglePushSubscription }}>
|
|
{children}
|
|
</ServiceWorkerContext.Provider>
|
|
)
|
|
}
|
|
|
|
export function useServiceWorker () {
|
|
return useContext(ServiceWorkerContext)
|
|
}
|