stacker.news/components/serviceworker.js
ekzyis 3a7c3f7af2
Add setting to send diagnostics back to SN (#463)
* Add diagnostics settings & endpoint

Stackers can now help us to identify and fix bugs by enabling diagnostics.

This will send anonymized data to us.

For now, this is only used to send events around push notifications.

* Send diagnostics to slack

* Detect OS

* Diagnostics data is only pseudonymous, not anonymous

It's only pseudonymous since with additional knowledge (which stacker uses which fancy name), we could trace the events back to individual stackers.

Data is only anonymous if this is not possible - it must be irreversible.

* Check if window.navigator is defined

* Use Slack SDK

* Catch errors of slack requests

---------

Co-authored-by: ekzyis <ek@stacker.news>
Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2023-09-18 18:00:16 -05:00

138 lines
5.3 KiB
JavaScript

import { createContext, useContext, useEffect, useState, useCallback } from 'react'
import { Workbox } from 'workbox-window'
import { gql, useMutation } from '@apollo/client'
import { useLogger } from './logger'
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
}
}
`)
const logger = useLogger()
// 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)
const { endpoint } = pushSubscription
logger.info('subscribed to push notifications', { endpoint })
// 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
})
logger.info('sent STORE_SUBSCRIPTION to service worker', { endpoint })
// send subscription to server
const variables = {
endpoint,
p256dh: pushSubscription.keys.p256dh,
auth: pushSubscription.keys.auth
}
await savePushSubscription({ variables })
logger.info('sent push subscription to server', { endpoint })
}
const unsubscribeFromPushNotifications = async (subscription) => {
await subscription.unsubscribe()
const { endpoint } = subscription
logger.info('unsubscribed from push notifications', { endpoint })
await deletePushSubscription({ variables: { endpoint } })
logger.info('deleted push subscription from server', { 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' })
logger.info('sent SYNC_SUBSCRIPTION to service worker')
}, [])
useEffect(() => {
if (!support.serviceWorker) {
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])
return (
<ServiceWorkerContext.Provider value={{ registration, support, permission, requestNotificationPermission, togglePushSubscription }}>
{children}
</ServiceWorkerContext.Provider>
)
}
export function useServiceWorker () {
return useContext(ServiceWorkerContext)
}