/** @typedef {Event & { prompt: () => Promise, userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }> }} BeforeInstallPromptEvent */ const PROMPT_KEY = '__recipesPwaDeferredPrompt'; const LISTENER_KEY = '__recipesPwaInstallListening'; let ready = $state(false); let installed = $state(false); let alreadyOnDevice = $state(false); function getDeferredPrompt() { if (typeof window === 'undefined') return null; return /** @type {BeforeInstallPromptEvent | null} */ (window[PROMPT_KEY] ?? null); } function setDeferredPrompt(/** @type {BeforeInstallPromptEvent | null} */ event) { if (typeof window === 'undefined') return; window[PROMPT_KEY] = event; ready = event !== null; } function isStandaloneDisplay() { if (typeof window === 'undefined') return false; return ( window.matchMedia('(display-mode: standalone)').matches || window.matchMedia('(display-mode: window-controls-overlay)').matches || /** @type {Navigator & { standalone?: boolean }} */ (window.navigator).standalone === true ); } function isIosDevice() { if (typeof navigator === 'undefined') return false; const ua = navigator.userAgent; return /iPad|iPhone|iPod/.test(ua) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1); } async function isBrave() { if (typeof navigator === 'undefined') return false; const brave = /** @type {Navigator & { brave?: { isBrave?: () => Promise } }} */ (navigator).brave; if (brave?.isBrave) { try { return await brave.isBrave(); } catch { /* fall through */ } } return /Brave/i.test(navigator.userAgent); } /** * Detect an existing install of this origin's PWA (desktop/Android Chromium). */ async function detectRelatedInstall() { if (typeof navigator === 'undefined' || !('getInstalledRelatedApps' in navigator)) { return false; } try { const apps = await /** @type {{ getInstalledRelatedApps: () => Promise }} */ ( navigator ).getInstalledRelatedApps(); return Array.isArray(apps) && apps.length > 0; } catch { return false; } } async function refreshInstallFlags() { installed = isStandaloneDisplay(); alreadyOnDevice = installed || (await detectRelatedInstall()); if (alreadyOnDevice) { setDeferredPrompt(null); } else if (getDeferredPrompt()) { ready = true; } } /** * Start listening for install eligibility. Safe to call multiple times. * Prompt event is stored on `window` so Vite HMR does not drop it. */ export function initPwaInstall() { if (typeof window === 'undefined') return; // Rehydrate after HMR / remount. if (getDeferredPrompt()) ready = true; void refreshInstallFlags(); if (window[LISTENER_KEY]) return; window[LISTENER_KEY] = true; window.addEventListener('beforeinstallprompt', (event) => { event.preventDefault(); setDeferredPrompt(/** @type {BeforeInstallPromptEvent} */ (event)); installed = false; alreadyOnDevice = false; }); window.addEventListener('appinstalled', () => { setDeferredPrompt(null); installed = true; alreadyOnDevice = true; }); const media = window.matchMedia('(display-mode: standalone)'); media.addEventListener?.('change', () => { void refreshInstallFlags(); }); } export function isInstallVisible() { // Hide only while running inside the installed app window. return !installed; } export function isInstallReady() { return getDeferredPrompt() !== null && !installed; } export function isAlreadyInstalledOnDevice() { return alreadyOnDevice && !installed; } /** * Triggers the native browser install prompt. Must be called from a user gesture. */ export async function promptInstall() { await refreshInstallFlags(); if (alreadyOnDevice && !getDeferredPrompt()) { return { ok: false, reason: /** @type {const} */ ('already-installed') }; } const event = getDeferredPrompt(); if (!event) { const brave = await isBrave(); const ios = isIosDevice(); return { ok: false, reason: /** @type {const} */ ('unavailable'), brave, ios }; } try { await event.prompt(); const choice = await event.userChoice; setDeferredPrompt(null); if (choice.outcome === 'accepted') { installed = true; alreadyOnDevice = true; return { ok: true, reason: /** @type {const} */ ('accepted') }; } return { ok: false, reason: /** @type {const} */ ('dismissed') }; } catch { const brave = await isBrave(); const ios = isIosDevice(); return { ok: false, reason: /** @type {const} */ ('unavailable'), brave, ios }; } } if (typeof window !== 'undefined') { initPwaInstall(); }