diff --git a/src/lib/pwa/pwa_install.svelte.ts b/src/lib/pwa/pwa_install.svelte.ts index f999b327..a80c6620 100644 --- a/src/lib/pwa/pwa_install.svelte.ts +++ b/src/lib/pwa/pwa_install.svelte.ts @@ -5,11 +5,14 @@ * * Usage: * - Call `pwa_install.init()` once in the root +layout.svelte (browser $effect). + * init() handles dismiss state and standalone detection (needs localStorage/DOM). * - Import `pwa_install` in any component to read state and trigger the prompt. * * Platform behaviour: - * - Chrome / Android / Desktop Chrome: intercepts `beforeinstallprompt`, - * lets us show a custom button that calls the native install flow. + * - Chrome / Chromium / Android: intercepts `beforeinstallprompt` at module load time + * (critical — the event fires very early, before $effects run). Shows a custom button. + * - Firefox desktop: does NOT support `beforeinstallprompt`. Firefox provides its own + * install button in the address bar when a valid manifest + service worker are present. * - iOS Safari: `beforeinstallprompt` never fires; we detect iOS + not-standalone * and show manual "Share → Add to Home Screen" instructions instead. * - If already running in standalone mode (i.e. already installed) → hide everything. @@ -33,6 +36,21 @@ let _is_installed = $state(false); let _is_dismissed = $state(false); let _initialized = false; // plain boolean — not reactive, guards against double-init +// --- Early event capture (module load time) --- +// `beforeinstallprompt` fires within ~1 second of page load — before any $effect runs. +// Register here so we never miss it regardless of when init() is called. +if (browser) { + window.addEventListener('beforeinstallprompt', (e) => { + e.preventDefault(); + _deferred_prompt = e as BeforeInstallPromptEvent; + }); + + window.addEventListener('appinstalled', () => { + _is_installed = true; + _deferred_prompt = null; + }); +} + // --- Private helpers (SSR-safe) --- function _in_standalone(): boolean { @@ -83,7 +101,10 @@ export const pwa_install = { }, /** - * Register browser event listeners. Call ONCE from root +layout.svelte. + * Finish initialisation after the layout mounts. Call ONCE from root +layout.svelte. + * Handles dismiss state (needs localStorage) and standalone detection (needs DOM). + * Event listeners for beforeinstallprompt/appinstalled are already registered at + * module load time — this does NOT register them again. * Safe to call multiple times (idempotent). */ init(): void { @@ -107,18 +128,6 @@ export const pwa_install = { localStorage.removeItem(DISMISS_KEY); } } - - // Capture the deferred install prompt (Chrome/Android/Desktop Chrome). - window.addEventListener('beforeinstallprompt', (e) => { - e.preventDefault(); - _deferred_prompt = e as BeforeInstallPromptEvent; - }); - - // Fired by the browser after the user accepts the install prompt. - window.addEventListener('appinstalled', () => { - _is_installed = true; - _deferred_prompt = null; - }); }, /** Trigger the native install prompt. No-op if no deferred prompt is available. */