62 lines
1.8 KiB
TypeScript
62 lines
1.8 KiB
TypeScript
import { useState, useEffect } from 'react'
|
|
|
|
interface BeforeInstallPromptEvent extends Event {
|
|
prompt: () => Promise<void>
|
|
userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>
|
|
}
|
|
|
|
interface PWAInstall {
|
|
canInstall: boolean // Android/Chrome: native prompt available
|
|
isIOS: boolean // iOS Safari: must show manual instructions
|
|
isInstalled: boolean // Already running as installed PWA
|
|
triggerInstall: () => Promise<void>
|
|
}
|
|
|
|
export function usePWAInstall(): PWAInstall {
|
|
const [deferredPrompt, setDeferredPrompt] = useState<BeforeInstallPromptEvent | null>(null)
|
|
const [isInstalled, setIsInstalled] = useState(false)
|
|
|
|
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !(window as unknown as { MSStream?: unknown }).MSStream
|
|
|
|
useEffect(() => {
|
|
// Detect if already installed (standalone mode)
|
|
const mq = window.matchMedia('(display-mode: standalone)')
|
|
const iosStandalone = (navigator as unknown as { standalone?: boolean }).standalone === true
|
|
if (mq.matches || iosStandalone) {
|
|
setIsInstalled(true)
|
|
return
|
|
}
|
|
|
|
const handler = (e: Event) => {
|
|
e.preventDefault()
|
|
setDeferredPrompt(e as BeforeInstallPromptEvent)
|
|
}
|
|
|
|
window.addEventListener('beforeinstallprompt', handler)
|
|
|
|
window.addEventListener('appinstalled', () => {
|
|
setIsInstalled(true)
|
|
setDeferredPrompt(null)
|
|
})
|
|
|
|
return () => window.removeEventListener('beforeinstallprompt', handler)
|
|
}, [])
|
|
|
|
const triggerInstall = async () => {
|
|
if (!deferredPrompt) return
|
|
await deferredPrompt.prompt()
|
|
const { outcome } = await deferredPrompt.userChoice
|
|
if (outcome === 'accepted') {
|
|
setIsInstalled(true)
|
|
setDeferredPrompt(null)
|
|
}
|
|
}
|
|
|
|
return {
|
|
canInstall: !!deferredPrompt,
|
|
isIOS,
|
|
isInstalled,
|
|
triggerInstall,
|
|
}
|
|
}
|