feat(05-04): usePushSubscription hook + PushPermissionPrompt + App mount
- Create apps/pwa/src/hooks/usePushSubscription.ts: subscribe (in tap handler, VAPID key cached), unsubscribe, permission; health-check on mount (D-10); urlBase64ToUint8Array helper; prefetchVapidKey export - Create apps/pwa/src/components/PushPermissionPrompt.tsx: WalkthroughSheet-style bottom sheet, Bell icon, 'Stay in the loop' heading, 48px Enable CTA (var(--color-member-0)), 44px Not-now ghost, no backdrop-dismiss, pushPermissionDismissed key, Loader2 spinner while awaiting - Mount PushPermissionPrompt in App.tsx (for installed-PWA path) and InstallPrompt.tsx (for post-Android-install justInstalled path) - Build green; tsc clean
This commit is contained in:
@@ -23,6 +23,7 @@
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Smartphone, X } from 'lucide-react'
|
||||
import { PushPermissionPrompt } from './PushPermissionPrompt.js'
|
||||
|
||||
// ── iOS detection ──────────────────────────────────────────────────────────
|
||||
|
||||
@@ -75,13 +76,19 @@ interface BeforeInstallPromptEvent extends Event {
|
||||
*/
|
||||
export function useAndroidInstallPrompt() {
|
||||
const [deferredPrompt, setDeferredPrompt] = useState<BeforeInstallPromptEvent | null>(null)
|
||||
// justInstalled: set to true when the appinstalled event fires, enabling the
|
||||
// post-install push permission prompt trigger (D-08).
|
||||
const [justInstalled, setJustInstalled] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e: Event) => {
|
||||
e.preventDefault()
|
||||
setDeferredPrompt(e as BeforeInstallPromptEvent)
|
||||
}
|
||||
const installedHandler = () => setDeferredPrompt(null)
|
||||
const installedHandler = () => {
|
||||
setDeferredPrompt(null)
|
||||
setJustInstalled(true)
|
||||
}
|
||||
|
||||
window.addEventListener('beforeinstallprompt', handler)
|
||||
window.addEventListener('appinstalled', installedHandler)
|
||||
@@ -101,7 +108,7 @@ export function useAndroidInstallPrompt() {
|
||||
}
|
||||
}
|
||||
|
||||
return { canInstall: deferredPrompt !== null, triggerInstall }
|
||||
return { canInstall: deferredPrompt !== null, triggerInstall, justInstalled }
|
||||
}
|
||||
|
||||
// ── iOS Walkthrough Sheet ─────────────────────────────────────────────────
|
||||
@@ -300,7 +307,7 @@ function persistDismissed(): void {
|
||||
export function InstallPrompt() {
|
||||
const [dismissed, setDismissed] = useState<boolean>(readDismissed)
|
||||
const [walkthroughOpen, setWalkthroughOpen] = useState(false)
|
||||
const { canInstall, triggerInstall } = useAndroidInstallPrompt()
|
||||
const { canInstall, triggerInstall, justInstalled } = useAndroidInstallPrompt()
|
||||
|
||||
// Re-check isInstalled on mount — matchMedia is only available in the browser
|
||||
const [installed, setInstalled] = useState(false)
|
||||
@@ -308,7 +315,14 @@ export function InstallPrompt() {
|
||||
setInstalled(isInstalled())
|
||||
}, [])
|
||||
|
||||
// Nothing to show when already installed
|
||||
// Show post-install permission prompt (D-08) immediately after the appinstalled event
|
||||
// fires — before the page knows it is in standalone mode. This covers Android Chrome
|
||||
// where the page stays loaded after install without a reload.
|
||||
if (justInstalled) {
|
||||
return <PushPermissionPrompt />
|
||||
}
|
||||
|
||||
// Nothing to show when already installed (App.tsx mounts PushPermissionPrompt there)
|
||||
if (installed) return null
|
||||
|
||||
function dismiss() {
|
||||
|
||||
Reference in New Issue
Block a user