15fb7f446b
* nwc wallet logs * persist logs in IndexedDB * Potential fix for empty error message * load logs limited to 5m ago from IDB * load logs from past via query param * Add 5m, 1h, 6h links for earlier logs * Show end of log * Clamp to logStart * Add log.module.css * Remove TODO about persistence * Use table for logs * <table> fixes bad format with fixed width and message overflow into start of next row * also using ---start of log--- instead of ---end of log--- now * removed time string in header nav * Rename .header to .logNav * Simply load all logs and remove navigation I realized the code for navigation was most likely premature optimization which even resulted in worse UX: Using the buttons to load logs from 5m, 1h, 6h ago sometimes meant that nothing happened at all since there were no logs from 5m, 1h, 6h ago. That's why I added a time string as "start of logs" so it's at least visible that it changed but that looked bad so I removed it. But all of this was not necessary: I can simply load all logs at once and then the user can scroll around however they like. I was worried that it would be bad for performance to load all logs at once since we might store a lot of logs but as mentioned, that's probably premature optimization. WHEN a lot of logs are stored AND this becomes a problem (What problem even? Slow page load?), THEN we can think about this. If page load ever becomes slow because of loading logs, we could probably simply not load the logs at page load but only when /wallet/logs is visited. But for now, this works fine. * Add follow checkbox * Create WalletLogs component * Embed wallet logs * Remove test error * Fix level padding * Add LNbits logs * Add logs for attaching LND and lnAddr * Use err.message || err.toString?.() consistently * Autowithdrawal logs * Use details from LND error * Don't log test invoice individually * Also refetch logs on error * Remove obsolete and annoying toasts * Replace scrollIntoView with scroll * Use constant embedded max-height * Fix missing width: 100% for embedded logs * Show full payment hash and preimage in logs * Also parse details from LND errors on autowithdrawal failures * Remove TODO * Fix accidental removal of wss:// check * Fix alignment of start marker and show empty if empty * Fix sendPayment loop * Split context in two
184 lines
7.7 KiB
JavaScript
184 lines
7.7 KiB
JavaScript
import { createContext, useContext, useEffect, useState, useCallback, useMemo } from 'react'
|
|
import { Workbox } from 'workbox-window'
|
|
import { gql, useMutation } from '@apollo/client'
|
|
import { detectOS, useServiceWorkerLogger } from './logger'
|
|
|
|
const applicationServerKey = process.env.NEXT_PUBLIC_VAPID_PUBKEY
|
|
|
|
const ServiceWorkerContext = createContext()
|
|
|
|
// message types for communication between app and service worker
|
|
export const MESSAGE_PORT = 'MESSAGE_PORT' // message to exchange message channel on which service worker will send messages back to app
|
|
export const ACTION_PORT = 'ACTION_PORT' // message to exchange action channel on which service worker will send actions back to app
|
|
export const SYNC_SUBSCRIPTION = 'SYNC_SUBSCRIPTION' // trigger onPushSubscriptionChange event in service worker manually
|
|
export const RESUBSCRIBE = 'RESUBSCRIBE' // trigger resubscribing to push notifications (sw -> app)
|
|
export const DELETE_SUBSCRIPTION = 'DELETE_SUBSCRIPTION' // delete subscription in IndexedDB (app -> sw)
|
|
export const STORE_SUBSCRIPTION = 'STORE_SUBSCRIPTION' // store subscription in IndexedDB (app -> sw)
|
|
export const STORE_OS = 'STORE_OS' // store OS in service worker
|
|
|
|
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 = useServiceWorkerLogger()
|
|
|
|
// 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 () => {
|
|
// serviceWorker.controller is null on forced refreshes
|
|
// see https://w3c.github.io/ServiceWorker/#navigator-service-worker-controller
|
|
if (!navigator.serviceWorker.controller) {
|
|
throw new Error('no active service worker found. try refreshing page.')
|
|
}
|
|
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 } })
|
|
// 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 })
|
|
}
|
|
|
|
const togglePushSubscription = useCallback(async () => {
|
|
const pushSubscription = await registration.pushManager.getSubscription()
|
|
if (pushSubscription) {
|
|
return unsubscribeFromPushNotifications(pushSubscription)
|
|
}
|
|
return subscribeToPushNotifications().then(async () => {
|
|
// request persistent storage: https://web.dev/learn/pwa/offline-data#data_persistence
|
|
const persisted = await navigator?.storage?.persisted?.()
|
|
if (!persisted && navigator?.storage?.persist) {
|
|
return navigator.storage.persist().then(persistent => {
|
|
logger.info('persistent storage:', persistent)
|
|
}).catch(logger.error)
|
|
}
|
|
})
|
|
})
|
|
|
|
useEffect(() => {
|
|
setSupport({
|
|
serviceWorker: 'serviceWorker' in navigator,
|
|
notification: 'Notification' in window,
|
|
pushManager: 'PushManager' in window
|
|
})
|
|
setPermission({ notification: 'Notification' in window ? window.Notification.permission : 'denied' })
|
|
|
|
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)
|
|
})
|
|
}, [])
|
|
|
|
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')
|
|
navigator?.serviceWorker?.controller?.postMessage?.({ action: STORE_OS, os: detectOS() })
|
|
}, [registration])
|
|
|
|
const contextValue = useMemo(() => ({
|
|
registration,
|
|
support,
|
|
permission,
|
|
requestNotificationPermission,
|
|
togglePushSubscription
|
|
}), [registration, support, permission, requestNotificationPermission, togglePushSubscription])
|
|
|
|
return (
|
|
<ServiceWorkerContext.Provider value={contextValue}>
|
|
{children}
|
|
</ServiceWorkerContext.Provider>
|
|
)
|
|
}
|
|
|
|
export function useServiceWorker () {
|
|
return useContext(ServiceWorkerContext)
|
|
}
|