'use client'; import { usePathname } from 'next/navigation'; import { useCallback, useEffect, useState } from 'react'; interface BeforeInstallPromptEvent extends Event { prompt: () => Promise; userChoice: Promise<{ outcome: 'accepted' | 'dismissed'; platform: string }>; } const DISMISS_STORAGE_KEY = 'pz-install-prompt-dismissed-at'; const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000; const CONVERSION_PATH_PATTERNS = ['/basket', '/orders/']; const IN_APP_BROWSER_PATTERN = /FBAN|FBAV|FB_IAB|Instagram|Twitter|Line\/|LinkedInApp|TikTok|Snapchat|; wv\)/i; const NON_SAFARI_IOS_PATTERN = /CriOS|FxiOS|EdgiOS|OPiOS|YaBrowser|UCBrowser/i; const safeRead = (key: string): string | null => { try { return window.localStorage.getItem(key); } catch { return null; } }; const safeWrite = (key: string, value: string): void => { try { window.localStorage.setItem(key, value); } catch { // Safari Private Mode, quota exceeded — fail soft } }; const isWithinDismissWindow = (timestamp: string | null): boolean => { if (!timestamp) return false; const dismissedAt = Number(timestamp); if (!Number.isFinite(dismissedAt)) return false; return Date.now() - dismissedAt < SEVEN_DAYS_MS; }; const detectIOS = (): boolean => { const ua = window.navigator.userAgent; const isAppleMobile = /iPad|iPhone|iPod/.test(ua); // iPadOS 13+ reports as Macintosh; disambiguate via touch points const isIPadOSAsMac = /Mac/.test(ua) && navigator.maxTouchPoints > 1; const isMSStream = !!(window as Window & { MSStream?: unknown }).MSStream; return (isAppleMobile || isIPadOSAsMac) && !isMSStream; }; const detectStandalone = (): boolean => window.matchMedia('(display-mode: standalone)').matches || (window.navigator as Navigator & { standalone?: boolean }).standalone === true; const detectInAppBrowser = (): boolean => IN_APP_BROWSER_PATTERN.test(window.navigator.userAgent); const detectNonSafariIOSBrowser = (): boolean => NON_SAFARI_IOS_PATTERN.test(window.navigator.userAgent); const isConversionPath = (pathname: string): boolean => CONVERSION_PATH_PATTERNS.some((pattern) => pathname.includes(pattern)); export const useInstallPrompt = () => { const pathname = usePathname() ?? ''; const [isHydrated, setIsHydrated] = useState(false); const [deferredPrompt, setDeferredPrompt] = useState(null); const [isIOS, setIsIOS] = useState(false); const [isStandalone, setIsStandalone] = useState(false); const [isInAppBrowser, setIsInAppBrowser] = useState(false); const [isNonSafariIOS, setIsNonSafariIOS] = useState(false); const [isDismissed, setIsDismissed] = useState(false); useEffect(() => { setIsStandalone(detectStandalone()); setIsIOS(detectIOS()); setIsInAppBrowser(detectInAppBrowser()); setIsNonSafariIOS(detectNonSafariIOSBrowser()); setIsDismissed(isWithinDismissWindow(safeRead(DISMISS_STORAGE_KEY))); setIsHydrated(true); const handleBeforeInstallPrompt = (event: Event) => { event.preventDefault(); setDeferredPrompt(event as BeforeInstallPromptEvent); }; const handleAppInstalled = () => { setDeferredPrompt(null); setIsStandalone(true); }; const handleStorageChange = (event: StorageEvent) => { if (event.key !== DISMISS_STORAGE_KEY) return; setIsDismissed(isWithinDismissWindow(event.newValue)); }; window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt); window.addEventListener('appinstalled', handleAppInstalled); window.addEventListener('storage', handleStorageChange); return () => { window.removeEventListener( 'beforeinstallprompt', handleBeforeInstallPrompt ); window.removeEventListener('appinstalled', handleAppInstalled); window.removeEventListener('storage', handleStorageChange); }; }, []); const dismiss = useCallback(() => { safeWrite(DISMISS_STORAGE_KEY, Date.now().toString()); setIsDismissed(true); }, []); const install = useCallback(async () => { if (!deferredPrompt) return null; await deferredPrompt.prompt(); const { outcome } = await deferredPrompt.userChoice; if (outcome === 'accepted') { setDeferredPrompt(null); } else { dismiss(); } return outcome; }, [deferredPrompt, dismiss]); const canInstall = isHydrated && !isStandalone && !isDismissed && !isInAppBrowser && !isConversionPath(pathname) && (deferredPrompt !== null || (isIOS && !isNonSafariIOS)); return { canInstall, isIOS, isStandalone, install, dismiss }; };