# `install-prompt/install-prompt.ts` _(no submodules)_ ## Purpose `InstallPromptManager` captures the browser-fired `beforeinstallprompt` event and exposes it as a typed state machine the application can drive programmatically. The manager also handles `appinstalled` for hot installations, persists dismissal across browser sessions (so dismissed users are not re-prompted on every visit), and emits OTel counters partitioned by outcome (`accepted` / `dismissed`). ## Features - State machine: `idle → available → (accepted | dismissed)`. - `prompt()` invokes the captured deferred event and awaits the user's choice. - `dismiss()` clears the captured event without showing it, persisting the decision so future installs require an explicit `clearDismissed()`. - Injectable `WindowLike` + `StorageLike` seams so tests run without `globalThis` patching. - OTel `pwa_install_prompt_total` counter with `outcome` attribute. ## Usage: Basic ```ts import { InstallPromptManager } from "@suaveplan/pwa"; const ipm = new InstallPromptManager(); ipm.listen(); ipm.onStateChange((s) => { if (s === "available") showInstallBanner(); }); ``` ## Usage: Advanced Inject window + storage seams and drive every transition: ```ts import { InstallPromptManager } from "@suaveplan/pwa"; const window = createFakeWindow(); const storage = createMemoryStorage(); const ipm = new InstallPromptManager({ window, storage }); ipm.listen(); window.dispatchBeforeInstallPrompt(); expect(ipm.isAvailable).toBe(true); await ipm.prompt(); expect(ipm.state).toBe("accepted"); ``` ## API Reference - `new InstallPromptManager(options?: InstallPromptManagerOptions)`. - `state: InstallPromptState` (readonly). - `isAvailable: boolean`. - `wasDismissed: boolean`. - `listen(): void`. - `unlisten(): void`. - `prompt(): Promise`. - `dismiss(): void`. - `clearDismissed(): void`. - `onStateChange(listener): () => void`. - `reset(): void`. ## Implementation Notes The `WindowLike` seam exposes only `addEventListener` / `removeEventListener` so tests can pass a tiny `EventTarget` subclass without faking the full `Window` interface. The dismissal-persisting writes use a try/catch because `StorageLike` implementations can throw (`QuotaExceededError` under heavy quotas) and the install-prompt should remain functional even when persistence fails — at worst the user will be re-prompted next session. The manager explicitly does NOT trigger any platform-native UI itself: `prompt()` defers entirely to the browser's deferred event, and `dismiss()` only updates the internal state machine + storage.