import { useSyncExternalStore } from "react"; type InstallPrompt = Event & { prompt: () => Promise; userChoice: Promise<{ outcome: "accepted" | "dismissed" }>; }; type PwaState = { enabled: boolean; installed: boolean; installPrompt: InstallPrompt | null; updateReady: boolean; error: string }; let state: PwaState = { enabled: false, installed: false, installPrompt: null, updateReady: false, error: "" }; const listeners = new Set<() => void>(); let registration: ServiceWorkerRegistration | undefined; let reloading = false; let started = false; const publish = (change: Partial) => { state = { ...state, ...change }; listeners.forEach(listener => listener()); }; const subscribe = (listener: () => void) => { listeners.add(listener); return () => { listeners.delete(listener); }; }; export const usePwa = () => useSyncExternalStore(subscribe, () => state, () => state); const subscribeOnline = (listener: () => void) => { window.addEventListener("online", listener); window.addEventListener("offline", listener); return () => { window.removeEventListener("online", listener); window.removeEventListener("offline", listener); }; }; export const useOnline = () => useSyncExternalStore(subscribeOnline, () => navigator.onLine !== false, () => true); /** Called once before React renders so early install events cannot be missed. */ export function startPwa() { if (started || !window.isSecureContext || !("serviceWorker" in navigator)) return; started = true; const standalone = window.matchMedia("(display-mode: standalone)"); const installed = () => standalone.matches || (navigator as Navigator & { standalone?: boolean }).standalone === true; publish({ enabled: true, installed: installed() }); standalone.addEventListener("change", () => publish({ installed: installed() })); window.addEventListener("beforeinstallprompt", event => { event.preventDefault(); publish({ installPrompt: event as InstallPrompt }); }); window.addEventListener("appinstalled", () => publish({ installed: true, installPrompt: null })); let controller = navigator.serviceWorker.controller; navigator.serviceWorker.addEventListener("controllerchange", () => { if (reloading) window.location.reload(); else if (controller) publish({ updateReady: true }); controller = navigator.serviceWorker.controller; }); const register = async () => { try { registration = await navigator.serviceWorker.register("/sw.js", { scope: "/", updateViaCache: "none" }); publish({ updateReady: Boolean(registration.waiting) }); const observeInstall = () => { const worker = registration?.installing; worker?.addEventListener("statechange", () => { if (worker.state === "installed") publish({ updateReady: Boolean(navigator.serviceWorker.controller), error: "" }); if (worker.state === "redundant" && !navigator.serviceWorker.controller) publish({ error: "Offline setup didn’t finish. Reconnect and reload to try again." }); }); }; registration.addEventListener("updatefound", observeInstall); observeInstall(); const check = () => { if (navigator.onLine && document.visibilityState === "visible") void registration?.update().catch(() => {}); }; document.addEventListener("visibilitychange", check); window.addEventListener("online", check); window.setInterval(check, 60 * 60 * 1000); } catch { publish({ error: "Offline setup is unavailable. Reconnect and reload to try again." }); } }; if (document.readyState === "complete") void register(); else window.addEventListener("load", () => { void register(); }, { once: true }); } export async function installApp(): Promise { const prompt = state.installPrompt; if (!prompt) return false; publish({ installPrompt: null }); try { await prompt.prompt(); await prompt.userChoice; } catch { publish({ error: "Could not open installation. Try your browser’s install menu." }); } return true; } export function applyUpdate() { if (!state.updateReady) return; if (registration?.waiting) { reloading = true; registration.waiting.postMessage({ type: "ACTIVATE_UPDATE" }); } else window.location.reload(); }