mirror of
https://github.com/wu736139669/hapi.git
synced 2026-11-09 01:51:39 +00:00
* feat(web): in-app PWA update prompt when new service worker is available (closes #938) User-controlled reload with a persistent banner, visibility-triggered SW checks, and an expandable rationale. Switches registerType to prompt. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): align vite.config with soup layers for clean driver merge Keeps registerType prompt while matching garden IWER stubs and PWA share_target shape expected by feat/pwa-share-target in the manifest. Co-authored-by: Cursor <cursoragent@cursor.com> * Revert "fix(web): align vite.config with soup layers for clean driver merge" This reverts commit 6f0915b0884d029a2413d8819a4dfe81d7c4e595. * fix(web): make PWA reload apply waiting service worker updates Handle SKIP_WAITING in injectManifest sw.ts and reload via controllerchange with a timed fallback when vite-plugin-pwa prompt mode does not navigate. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): satisfy setTimeout mock typing in PWA reload tests Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): register PWA service worker before auth gates Mount PwaUpdateProvider at app root and show the update banner on login and error screens so registerSW runs for logged-out users too. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): offset PWA update banner below top status banners Reserve top-12 when syncing or reconnecting so the reload prompt stays visible above SyncingBanner and ReconnectingBanner. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): offset PWA update banner below voice error banner Use PwaUpdateBannerWithStatusOffset inside VoiceProvider so voice errors share the same top-12 reservation as sync and reconnect banners. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
124 lines
3.6 KiB
TypeScript
124 lines
3.6 KiB
TypeScript
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
import { registerSW } from 'virtual:pwa-register'
|
|
|
|
export const PWA_UPDATE_CHECK_INTERVAL_MS = 60 * 60 * 1000
|
|
export const PWA_UPDATE_RELOAD_FALLBACK_MS = 2000
|
|
|
|
export async function requestPwaUpdateReload(
|
|
updateSW: ((reloadPage?: boolean) => Promise<void>) | null | undefined,
|
|
options: {
|
|
reloadPage?: () => void
|
|
setTimeoutFn?: typeof setTimeout
|
|
clearTimeoutFn?: typeof clearTimeout
|
|
} = {},
|
|
): Promise<void> {
|
|
const reloadPage = options.reloadPage ?? (() => window.location.reload())
|
|
const setTimeoutFn = options.setTimeoutFn ?? setTimeout
|
|
const clearTimeoutFn = options.clearTimeoutFn ?? clearTimeout
|
|
|
|
if (!updateSW) {
|
|
reloadPage()
|
|
return
|
|
}
|
|
|
|
let reloaded = false
|
|
const doReload = () => {
|
|
if (reloaded) {
|
|
return
|
|
}
|
|
reloaded = true
|
|
reloadPage()
|
|
}
|
|
|
|
const onControllerChange = () => {
|
|
navigator.serviceWorker.removeEventListener('controllerchange', onControllerChange)
|
|
doReload()
|
|
}
|
|
|
|
navigator.serviceWorker.addEventListener('controllerchange', onControllerChange)
|
|
|
|
let fallbackTimer: ReturnType<typeof setTimeout> | undefined
|
|
|
|
try {
|
|
await updateSW(true)
|
|
} catch (error) {
|
|
console.error('PWA update failed', error)
|
|
navigator.serviceWorker.removeEventListener('controllerchange', onControllerChange)
|
|
if (fallbackTimer !== undefined) {
|
|
clearTimeoutFn(fallbackTimer)
|
|
}
|
|
doReload()
|
|
return
|
|
}
|
|
|
|
fallbackTimer = setTimeoutFn(() => {
|
|
navigator.serviceWorker.removeEventListener('controllerchange', onControllerChange)
|
|
doReload()
|
|
}, PWA_UPDATE_RELOAD_FALLBACK_MS)
|
|
}
|
|
|
|
export function setupRegistrationUpdateChecks(
|
|
registration: ServiceWorkerRegistration,
|
|
): () => void {
|
|
const intervalId = window.setInterval(() => {
|
|
void registration.update()
|
|
}, PWA_UPDATE_CHECK_INTERVAL_MS)
|
|
|
|
const handleVisibilityChange = () => {
|
|
if (document.visibilityState === 'visible') {
|
|
void registration.update()
|
|
}
|
|
}
|
|
|
|
document.addEventListener('visibilitychange', handleVisibilityChange)
|
|
|
|
return () => {
|
|
window.clearInterval(intervalId)
|
|
document.removeEventListener('visibilitychange', handleVisibilityChange)
|
|
}
|
|
}
|
|
|
|
export function usePwaUpdate() {
|
|
const [needRefresh, setNeedRefresh] = useState(false)
|
|
const updateSWRef = useRef<((reloadPage?: boolean) => Promise<void>) | null>(null)
|
|
const cleanupRef = useRef<(() => void) | null>(null)
|
|
|
|
useEffect(() => {
|
|
const updateSW = registerSW({
|
|
onNeedRefresh() {
|
|
setNeedRefresh(true)
|
|
},
|
|
onOfflineReady() {
|
|
console.log('App ready for offline use')
|
|
},
|
|
onRegistered(registration) {
|
|
cleanupRef.current?.()
|
|
cleanupRef.current = null
|
|
|
|
if (!registration) {
|
|
return
|
|
}
|
|
|
|
cleanupRef.current = setupRegistrationUpdateChecks(registration)
|
|
},
|
|
onRegisterError(error) {
|
|
console.error('SW registration error:', error)
|
|
},
|
|
})
|
|
|
|
updateSWRef.current = updateSW
|
|
|
|
return () => {
|
|
cleanupRef.current?.()
|
|
cleanupRef.current = null
|
|
updateSWRef.current = null
|
|
}
|
|
}, [])
|
|
|
|
const reload = useCallback(() => {
|
|
void requestPwaUpdateReload(updateSWRef.current)
|
|
}, [])
|
|
|
|
return { needRefresh, reload }
|
|
}
|