first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,80 @@
# `install-prompt/install-prompt.ts`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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<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.