# Journey: Install as PWA

The cross-view flow from first browser visit through installed standalone
launch, offline continuity, service-worker update, push routing, and sign-out
cleanup. This journey exists to catch bugs that per-view checklists miss because
the individual screens can look correct while the PWA runtime is stale.

## Personas

- **Mobile-first signed-in user** — primary path; Android Chrome direct install
  and iOS Safari manual Add to Home Screen instructions.
- **Desktop returning user** — Chromium or Edge install from the browser prompt,
  address bar, or `/welcome/download`.
- **Anonymous browser visitor** — installs before signing in, then cold-starts
  into the welcome redirect flow.

## Pre-conditions

- App is served over HTTPS or `localhost`.
- Service workers are enabled unless the failure-mode test explicitly blocks
  them.
- Chromium/Edge/Android uses the browser `beforeinstallprompt` event; Safari on
  iPhone/iPad uses Share -> Add to Home Screen and never exposes
  `beforeinstallprompt`.
- There is no app-side "N qualifying interactions" counter in V1. The custom
  prompt appears only after the browser fires `beforeinstallprompt`, and the
  manual `/welcome/download` trigger dispatches the same saved event.

## Steps

### 1. Anonymous first browser visit

- [x] Browser navigation to `/` redirects an unauthenticated visitor to
      `/welcome?redirect=%2F`.
- [x] `/welcome` is public, hydrated, titled for OSHUN, and exposes the auth
      entry plus primary public CTA.
- [x] `/manifest.json` is linked once from the document head.
- [x] `/sw.js` resolves with a JavaScript content type when service workers are
      allowed.
- [x] Manifest install assets resolve: 192 and 512 icons, maskable variants, and
      four screenshots across narrow and wide form factors.
- [x] Mobile web metadata is present: viewport, theme color, and
      `apple-mobile-web-app-capable=yes`.

### 2. Consent and prompt ordering

- [x] Cookie consent is handled by the first-time visitor flow and compliance
      specs.
- [x] `PwaBootstrap` suppresses service-worker update prompts until consent is
      resolved so the update overlay does not compete with the cookie banner.
- [x] `PwaInstallPrompt` is not consent-gated in V1; it is driven by the
      browser's install event and its own 14-day dismissal cooldown.
- [x] Consent-unresolved update suppression is browser-driven in
      `pwa-install-update-offline.spec.ts`.

### 3. Install education and trigger surface

- [x] `/welcome/download` is public and explains desktop Chrome/Edge, Android
      Chrome, and iPhone/iPad Safari install paths.
- [x] Safari copy states that Safari uses the native Share flow instead of
      `beforeinstallprompt`.
- [x] Safari steps tell the user to choose Add to Home Screen and keep Open as
      Web App enabled when offered.
- [x] The Web PWA card explains that Chrome/Edge can show a direct prompt and
      Safari on iPhone/iPad uses Share -> Add to Home Screen.
- [x] The page previews four installed-workspace screenshots and exposes a live
      "Install from this browser" trigger.

### 4. Native install prompt

- [x] `beforeinstallprompt` is captured, `preventDefault()` is called, and
      `[data-pwa-prompt][data-pwa-state="available"]` renders.
- [x] Clicking "Install from this browser" dispatches the shared install trigger
      event.
- [x] Clicking the custom install control calls the deferred browser prompt.
- [x] Accepted prompt outcome transitions to
      `[data-pwa-prompt][data-pwa-state="installed"]`.
- [x] Dismissed prompt outcome or explicit "Not now" stores
      `oshun_pwa_install_dismissed` and suppresses the prompt across reloads for
      the cooldown window.
- [x] Standalone runtime detection records the installed state and suppresses
      the available prompt.

### 5. Standalone cold launch

- [x] Manifest `start_url` is `/?surface=pwa`, `display` is `standalone`, and
      `display_override` prefers `window-controls-overlay` before standalone.
- [x] `detectStandalonePwa()` accepts both `(display-mode: standalone)` and the
      iOS `navigator.standalone` flag.
- [x] `mockStandaloneLaunch()` verifies signed-in cold-start restoration from
      `/?surface=pwa`.
- [x] A signed-in standalone relaunch restores the saved target, for example
      `/domains/nisaba?origin=home`.
- [x] An unauthenticated standalone relaunch redirects to `/welcome` with the
      saved redirect preserved.
- [x] A cold start with an already-active service worker restores the route,
      stays prompt-free, and emits `pwa_update_applied` with
      `applyMode=silent-cold-start`.

### 6. Offline read continuity

- [x] Authenticated warmup populates the offline shell cache for Home, Explore,
      Activity, and Library.
- [x] Those shell routes remain navigable after `context.setOffline(true)`.
- [x] `/healthz` failing while `navigator.onLine === true` surfaces the offline
      banner through the real connectivity probe.
- [x] A captive-portal-shaped `200 text/html` `/healthz` response is rejected
      and keeps the offline banner visible.
- [x] A genuine BFF health sentinel clears the banner on the online/retry check.
- [x] The real service worker serves a cached shell for an uncached deep
      navigation while offline, and the hydrated client renders
      `<PwaOfflineFallback>`.
- [x] `<PwaOfflineFallback>` includes retry, Home/Explore/Activity/Library
      shortcuts, recent cached content when available, and an empty-state copy
      when no recent records exist.
- [x] Nisaba offline-pinned BFF responses are readable inside the 24-hour window
      and expire outside it.

### 7. Offline writes and reconnect

- [x] Library collection toggles can persist locally while the browser is
      offline.
- [x] Saving a live Nisaba search result while offline writes
      `oshun.web.library.saved-items.v1` and shows the "Saved locally / sync
      will retry" hint.
- [x] Tara reflection submit while offline queues a local reflection item and
      renders the queued sync state.
- [x] A real service-worker `OSHUN_QUEUE_ACTION` Arete check-in persists to
      IndexedDB, survives an offline reload, replays through Chromium Background
      Sync, writes the live BFF, and drains the queue.
- [x] Service-worker queue status emits `pwa_sync_queued`; update prompts with
      queued writes render a queued-write note before refresh.

### 8. Service-worker update

- [x] A staged waiting worker renders `PwaUpdatePrompt` after consent resolves.
- [x] Standard shell routes show the "Workspace update ready" copy and a "Later"
      deferral that keeps the route stable.
- [x] A "Later" dismissal is session-scoped and re-surfaces on the next cold
      launch while the worker is still waiting.
- [x] Reading routes use reading-safe copy and "Keep reading"; dismissal keeps
      the live Nisaba reading route stable.
- [x] "Refresh now" posts `OSHUN_SKIP_WAITING`, receives `controllerchange`,
      reloads, clears the prompt, and keeps the route stable.
- [x] Mid-passage scroll position is preserved after an explicit reading-route
      update refresh.
- [x] Multiple open tabs apply the staged update once, preserve their own
      routes, and do not loop stale chunk errors.
- [x] Real `sw.js` activation purges stale OSHUN caches while preserving current
      and foreign caches.
- [x] An in-flight Arete check-in that already reached the BFF remains durable
      across the update reload.
- [x] A lost skip-waiting response surfaces the manual "Reload now" escape.

### 9. Push notification leg

- [x] Profile notification settings can register and unregister this browser as
      a `web-push` device token through the `/v1/device-tokens` wire shape.
- [x] Device registration failures report an error and never claim registered.
- [x] No-VAPID deployments fail closed and hide the register button.
- [x] The actual `public/sw.js` push runtime handles a Nyx payload, preserves
      the `actionPath`, and calls `showNotification` with title, body, icon,
      badge, tag, vibration, and notification data.
- [x] `notificationclick` cold-opens the target route when no window exists.
- [x] `notificationclick` focuses an existing app window and posts
      `OSHUN_NOTIFICATION_TAP` when a window is already open.

### 10. Sign-out and member-data cleanup

- [x] `clearPwaRelaunchTarget()` removes the relaunch localStorage key and
      expires the relaunch cookie.
- [x] `requestServiceWorkerMemberDataClear()` posts `OSHUN_CLEAR_MEMBER_DATA` to
      the controlling service worker.
- [x] `sw.js` clears member-owned BFF/media caches and queued offline mutations
      when it receives `OSHUN_CLEAR_MEMBER_DATA`.
- [x] The next unauthenticated standalone launch routes through `/welcome`
      instead of leaking a protected target.

## Post-conditions

- The installed-shell contract is valid: manifest, icons, screenshots,
  standalone start URL, service worker, and launch metadata all resolve.
- A browser-accepted install suppresses the custom prompt; a dismissal remains
  suppressed for the defined 14-day cooldown.
- Signed-in relaunch restores only a sanitized saved target; signed-out relaunch
  preserves that target through the welcome/auth gate.
- Warmed shell routes and eligible pinned data have an explicit offline path;
  unsupported deep data renders the offline fallback rather than a false live
  state.
- Waiting-worker updates are deferable and route-safe, with queued writes
  disclosed before refresh.
- Sign-out clears the relaunch pointer, member caches, and queued member writes
  from the controlling service worker.

## Failure modes

- [x] **Service-worker registration blocked** — the runtime reports the failure
      and leaves the browser experience usable without claiming offline support.
- [x] **Install prompt dismissed** — cooldown storage suppresses repeated
      prompting across reloads.
- [x] **Consent unresolved while an update waits** — only the consent surface
      wins; the update prompt appears after accept or reject resolves consent.
- [x] **Captive portal reports online** — HTML from `/healthz` fails the health
      sentinel and the offline banner stays visible.
- [x] **Skip-waiting acknowledgement is lost** — a manual reload action appears
      instead of leaving the UI indefinitely applying.
- [x] **Multiple tabs receive one update** — coordination prevents stale-chunk
      loops while each tab preserves its route.
- [x] **Offline mutation pending during update** — the prompt discloses queued
      work, and an already-accepted BFF write remains durable across reload.
- [x] **Push registration unavailable** — missing VAPID or a failed device-token
      write never paints a registered state.
- [x] **Sign-out followed by standalone cold launch** — protected relaunch data
      is cleared before the public welcome path opens.

## E2E coverage

- [`apps/oshun/web/e2e/pwa-smoke.spec.ts`](../../apps/oshun/web/e2e/pwa-smoke.spec.ts)
  covers public first visit, manifest and asset validity, SW asset availability,
  mobile metadata, download/install education including Safari A2HS copy, native
  install-prompt trigger and accepted state, authenticated and unauthenticated
  standalone relaunches, and reading-route update apply/deferral.
- [`apps/oshun/web/e2e/pwa-install-update-offline.spec.ts`](../../apps/oshun/web/e2e/pwa-install-update-offline.spec.ts)
  covers install dismissal cooldown, standard update deferral/re-prompt/ready
  transition, consent-unresolved update suppression, offline recent-content
  fallback, and uncached deep-route fallback controls.
- [`apps/oshun/web/e2e/pwa-lifecycle-deepening.spec.ts`](../../apps/oshun/web/e2e/pwa-lifecycle-deepening.spec.ts)
  covers refresh-now apply, multi-tab reload coordination, real cache purge,
  silent cold-start telemetry, in-flight writes, queued-write update prompts,
  offline banner/retry, and reduced-motion update spinner behavior.
- [`apps/oshun/web/e2e/pwa-failure-modes.spec.ts`](../../apps/oshun/web/e2e/pwa-failure-modes.spec.ts)
  covers SW-registration-blocked rendering, update/offline overlay co-mount,
  captive-portal false-online rejection, stuck apply manual reload, and the
  controllerchange reload-loop guard.
- [`apps/oshun/web/e2e/offline-shell-routes.spec.ts`](../../apps/oshun/web/e2e/offline-shell-routes.spec.ts),
  [`offline-write-deepening.spec.ts`](../../apps/oshun/web/e2e/offline-write-deepening.spec.ts),
  and
  [`offline-background-sync.spec.ts`](../../apps/oshun/web/e2e/offline-background-sync.spec.ts)
  cover shell route availability, real SW uncached-document fallback, BFF health
  probing, local offline edits, local sync hints, and real Background Sync
  replay.
- [`apps/oshun/web/e2e/push-registration.spec.ts`](../../apps/oshun/web/e2e/push-registration.spec.ts)
  and
  [`apps/oshun/web/e2e/nyx-service-worker-notification.spec.ts`](../../apps/oshun/web/e2e/nyx-service-worker-notification.spec.ts)
  cover browser push device registration and service-worker push/click routing.

## Per-view files touched by this journey

- [`shell/03-pwa-behavior.md`](../shell/03-pwa-behavior.md)
- [`shell/04-auth-session.md`](../shell/04-auth-session.md)
- [`shell/05-notifications.md`](../shell/05-notifications.md)
- [`customer/00-public/welcome.md`](../customer/00-public/welcome.md)
- [`customer/02-home-discovery/home.md`](../customer/02-home-discovery/home.md)

## Cross-references

- Feature spec: [`V1/features.md`](../../V1/features.md)
- Architecture:
  [`V1/ARCHITECTURE.md`](../../V1/ARCHITECTURE.md#customer-web--appsoshunweb)
- Manifest: `apps/oshun/web/public/manifest.json`
- Service worker: `apps/oshun/web/public/sw.js`

## Open questions / known boundaries

- [x] OS-level installation and uninstall from the Android/iOS launcher cannot
      be completed by Playwright; the suite asserts the browser event boundary,
      manifest contract, installed prompt state, and standalone launch behavior.
- [x] Safari Add to Home Screen cannot expose a headless browser install prompt;
      the automated contract is the manual Safari copy and standalone detection
      branch.
- [x] Storage-quota eviction is covered at policy/code level where available,
      but there is no deterministic cross-browser quota-exhaustion E2E gate.
- [x] Lighthouse PWA scoring is not the release gate in this journey; manifest,
      assets, service worker, offline, relaunch, and update behavior are covered
      by targeted checks with clearer failure attribution.
