2.6 KiB
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 explicitclearDismissed().- Injectable
WindowLike+StorageLikeseams so tests run withoutglobalThispatching. - OTel
pwa_install_prompt_totalcounter withoutcomeattribute.
Usage: Basic
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:
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<InstallPromptState>.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.