Files
hapi/web/src/hooks/usePWAInstall.ts
T
weishu ea9f0f3b1a feat: implement progressive web app support with offline capabilities
Add complete PWA implementation including service worker registration,
offline support, and installation prompts:

- Add vite-plugin-pwa and workbox-window dependencies for PWA tooling
- Configure VitePWA plugin with web app manifest and app metadata
- Set up Workbox caching strategies for API endpoints and CDN assets
- Implement service worker auto-update with user-triggered refresh
- Create usePWAInstall hook to handle beforeinstallprompt events
- Create useOnlineStatus hook for monitoring network connectivity
- Add InstallPrompt component with haptic feedback integration
- Add OfflineBanner component to notify users of offline status
- Configure PWA icons and assets (64x64, 192x192, 512x512 variants)
- Add TypeScript type declarations for virtual PWA register module
- Integrate PWA components and service worker into App.tsx and main.tsx
- Add PWA meta tags and viewport configuration to index.html
2025-12-18 13:40:36 +08:00

85 lines
2.6 KiB
TypeScript

import { useCallback, useEffect, useState } from 'react'
interface BeforeInstallPromptEvent extends Event {
prompt: () => Promise<void>
userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>
}
type InstallState = 'idle' | 'available' | 'installing' | 'installed'
export function usePWAInstall(): {
installState: InstallState
canInstall: boolean
isStandalone: boolean
promptInstall: () => Promise<boolean>
} {
const [installState, setInstallState] = useState<InstallState>('idle')
const [deferredPrompt, setDeferredPrompt] = useState<BeforeInstallPromptEvent | null>(null)
const isStandalone =
typeof window !== 'undefined' &&
(window.matchMedia('(display-mode: standalone)').matches ||
(window.navigator as Navigator & { standalone?: boolean }).standalone === true)
useEffect(() => {
if (isStandalone) {
setInstallState('installed')
return
}
const handleBeforeInstallPrompt = (e: Event) => {
e.preventDefault()
setDeferredPrompt(e as BeforeInstallPromptEvent)
setInstallState('available')
}
const handleAppInstalled = () => {
setInstallState('installed')
setDeferredPrompt(null)
}
window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt)
window.addEventListener('appinstalled', handleAppInstalled)
return () => {
window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt)
window.removeEventListener('appinstalled', handleAppInstalled)
}
}, [isStandalone])
const promptInstall = useCallback(async (): Promise<boolean> => {
if (!deferredPrompt) {
return false
}
// Clear immediately to prevent re-entrancy while userChoice is pending
const prompt = deferredPrompt
setDeferredPrompt(null)
setInstallState('installing')
try {
await prompt.prompt()
const { outcome } = await prompt.userChoice
if (outcome === 'accepted') {
setInstallState('installed')
return true
} else {
// User dismissed, wait for a new beforeinstallprompt event
setInstallState('idle')
return false
}
} catch {
setInstallState('idle')
return false
}
}, [deferredPrompt])
return {
installState,
canInstall: installState === 'available',
isStandalone,
promptInstall
}
}