/** * FieldInstallPrompt — install prompt for the worker sub-PWA. * * Cycle 6 sibling of `InstallPrompt` (mounted from the root layout). * This one fires only on /water routes and uses the field-PWA * branding (forest green icon, "Water Log — Tuxedo") so the prompt * that surfaces matches the manifest that will be installed. * * Same `BeforeInstallPromptEvent` capture pattern as InstallPrompt; * split into a separate component file because the worker surface * has its own theming + copy. */ "use client"; import { useEffect, useSyncExternalStore, useCallback, useReducer } from "react"; interface BeforeInstallPromptEvent extends Event { prompt(): Promise; userChoice: Promise<{ outcome: "accepted" | "dismissed" }>; } type PromptState = { deferred: BeforeInstallPromptEvent | null; visible: boolean; }; type PromptAction = | { type: "captured"; event: BeforeInstallPromptEvent } | { type: "show" } | { type: "hide" } | { type: "dismissed" } | { type: "reset" }; function promptReducer(state: PromptState, action: PromptAction): PromptState { switch (action.type) { case "captured": return { deferred: action.event, visible: false }; case "show": return state.deferred ? { ...state, visible: true } : state; case "hide": return { deferred: null, visible: false }; case "dismissed": return { ...state, visible: false }; case "reset": return { deferred: null, visible: false }; } } const INITIAL_PROMPT_STATE: PromptState = { deferred: null, visible: false }; export function FieldInstallPrompt() { const [prompt, dispatch] = useReducer(promptReducer, INITIAL_PROMPT_STATE); const isInstalled = useSyncExternalStore( () => () => {}, () => typeof window !== "undefined" && (window.matchMedia("(display-mode: standalone)").matches || // iOS Safari uses navigator.standalone for the home-screen PWA // launch. Cover both. (typeof (navigator as Navigator & { standalone?: boolean }).standalone === "boolean" && (navigator as Navigator & { standalone?: boolean }).standalone === true)), () => false, ); const mounted = useSyncExternalStore( () => () => {}, () => true, () => false, ); useEffect(() => { if (isInstalled) return; if (typeof window === "undefined") return; const handleBeforeInstall = (e: Event) => { e.preventDefault(); dispatch({ type: "captured", event: e as BeforeInstallPromptEvent }); // Show the field prompt sooner than the main one — workers // know they want this installed on day one. window.setTimeout(() => { dispatch({ type: "show" }); }, 4000); }; const handleAppInstalled = () => { dispatch({ type: "reset" }); }; window.addEventListener("beforeinstallprompt", handleBeforeInstall); window.addEventListener("appinstalled", handleAppInstalled); return () => { window.removeEventListener("beforeinstallprompt", handleBeforeInstall); window.removeEventListener("appinstalled", handleAppInstalled); }; }, [isInstalled]); const handleInstall = useCallback(async () => { const deferred = prompt.deferred; if (!deferred) return; await deferred.prompt(); const { outcome } = await deferred.userChoice; void outcome; dispatch({ type: "reset" }); }, [prompt.deferred]); const handleDismiss = useCallback(() => { dispatch({ type: "dismissed" }); try { sessionStorage.setItem("field-pwa-install-dismissed", "true"); } catch { // ignore } }, []); if (!mounted || !prompt.visible || isInstalled || !prompt.deferred) { return null; } return (

Install Water Log

One tap to a full-screen PIN entry from your home screen.

); } export default FieldInstallPrompt;