import { useEffect, useState, useRef, useCallback } from 'react'; import { useLocalStorage } from '@/hooks/useLocalStorage'; import { useTranslation } from '@/contexts/language'; import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, } from '@/components/ui/sheet'; import { Button } from '@/components/ui/button'; import { Download, X, Share, Plus, Smartphone } from 'lucide-react'; interface BeforeInstallPromptEvent extends Event { prompt: () => Promise; userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>; } // Detect iOS const isIOS = () => { if (typeof navigator === 'undefined') return false; return /iPad|iPhone|iPod/.test(navigator.userAgent) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1); }; // Detect if running in standalone mode (PWA) const isStandalone = () => { if (typeof window === 'undefined') return false; return window.matchMedia('(display-mode: standalone)').matches || // iOS Safari specific check ('standalone' in window.navigator && (window.navigator as Navigator & { standalone: boolean }).standalone); }; // Detect mobile/tablet const isMobileOrTablet = () => { if (typeof navigator === 'undefined') return false; return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1); // iPad }; export function PWAInstallPrompt() { const [deferredPrompt, setDeferredPrompt] = useState(null); const [showPrompt, setShowPrompt] = useState(false); const [showManualPrompt, setShowManualPrompt] = useState(false); const [dismissed, setDismissed] = useLocalStorage('pwa-install-dismissed', false); const [installed, setInstalled] = useLocalStorage('pwa-installed', false); const [isIOSDevice] = useState(isIOS()); const hasPromptFiredRef = useRef(false); const { t } = useTranslation(); // Handler for beforeinstallprompt const handleBeforeInstallPrompt = useCallback((e: Event) => { e.preventDefault(); hasPromptFiredRef.current = true; setDeferredPrompt(e as BeforeInstallPromptEvent); }, []); // Handler for app installed const handleAppInstalled = useCallback(() => { setInstalled(true); setShowPrompt(false); setShowManualPrompt(false); setDeferredPrompt(null); }, [setInstalled]); useEffect(() => { // Don't show if already dismissed or installed if (dismissed || installed) return; // Check if running in standalone mode (already installed) if (isStandalone()) { setInstalled(true); return; } // Check if mobile/tablet - only show on mobile devices if (!isMobileOrTablet()) return; // Add event listeners window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt); window.addEventListener('appinstalled', handleAppInstalled); // For iOS, we can't use beforeinstallprompt - show manual instructions after delay if (isIOSDevice) { const timer = setTimeout(() => { setShowManualPrompt(true); }, 5000); return () => { clearTimeout(timer); window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt); window.removeEventListener('appinstalled', handleAppInstalled); }; } return () => { window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt); window.removeEventListener('appinstalled', handleAppInstalled); }; }, [dismissed, installed, setInstalled, isIOSDevice, handleBeforeInstallPrompt, handleAppInstalled]); // Separate effect to handle showing prompts based on deferredPrompt state useEffect(() => { if (dismissed || installed) return; if (!isMobileOrTablet() || isIOSDevice) return; // If we have a deferred prompt, show native install after delay if (deferredPrompt) { const timer = setTimeout(() => { setShowPrompt(true); }, 5000); return () => clearTimeout(timer); } // Fallback: If beforeinstallprompt doesn't fire within 7 seconds on Android, show manual prompt const fallbackTimer = setTimeout(() => { if (!hasPromptFiredRef.current && !deferredPrompt) { setShowManualPrompt(true); } }, 7000); return () => clearTimeout(fallbackTimer); }, [deferredPrompt, dismissed, installed, isIOSDevice]); const handleInstall = async () => { if (!deferredPrompt) return; // Show the install prompt await deferredPrompt.prompt(); // Wait for the user's response const { outcome } = await deferredPrompt.userChoice; if (outcome === 'accepted') { setInstalled(true); } // Clear the deferred prompt setDeferredPrompt(null); setShowPrompt(false); }; const handleDismiss = () => { setShowPrompt(false); setShowManualPrompt(false); setDismissed(true); }; // Native install prompt (Android Chrome) if (showPrompt && deferredPrompt) { return ( e.preventDefault()} >
{t('pwa.installTitle')} {t('pwa.installSubtitle')}
{/* Benefits list */}

⚡

{t('pwa.benefits.fast')}

📴

{t('pwa.benefits.offline')}

🛹

{t('pwa.benefits.fullAccess')}

); } // Manual install instructions (iOS and fallback for other browsers) if (showManualPrompt) { return ( e.preventDefault()} >
{t('pwa.addToHomeScreen')} {isIOSDevice ? t('pwa.iosStep1Hint') : t('pwa.androidStep1Hint')}
{isIOSDevice ? ( // iOS Instructions
1

{t('pwa.iosStep1')}

{t('pwa.iosStep1Hint')}

2

{t('pwa.iosStep2')}

{t('pwa.iosStep2Hint')}

3

{t('pwa.iosStep3')}

{t('pwa.iosStep3Hint')}

) : ( // Android/Other Instructions
1

{t('pwa.androidStep1')}

{t('pwa.androidStep1Hint')}

2

{t('pwa.androidStep2')}

{t('pwa.androidStep2Hint')}

3

{t('pwa.androidStep3')}

{t('pwa.androidStep3Hint')}

)}

{t('pwa.tagline')}

); } return null; }