---
phase: 03-event-write-back-pwa-install
plan: 07
subsystem: pwa, frontend
tags: [vite-plugin-pwa, service-worker, install-prompt, ios, android, workbox]

# Dependency graph
requires:
  - phase: 03-event-write-back-pwa-install
    plan: 01
    provides: vite-plugin-pwa installed in apps/pwa

provides:
  - VitePWA manifest + service worker with auth-safe navigateFallbackDenylist (T-03-20)
  - PWA icon assets (192x192, 512x512, 180x180 apple-touch-icon)
  - iOS head meta entries for A2HS install
  - InstallPrompt component: iOS walkthrough banner/sheet + Android beforeinstallprompt banner

affects: [03-08]

# Tech tracking
tech-stack:
  added: []
  patterns:
    - VitePWA navigateFallbackDenylist to exclude /callback, /api/, /health from SW interception
    - runtimeCaching: [] — no API response caching (T-03-21)
    - isIOSSafariNonStandalone() — iOS UA + navigator.standalone detection
    - useAndroidInstallPrompt() — captures beforeinstallprompt, deferred prompt pattern
    - localStorage.installPromptDismissed — persist banner dismissal cross-session

key-files:
  created:
    - apps/pwa/src/components/InstallPrompt.tsx
    - apps/pwa/public/icon-192.png
    - apps/pwa/public/icon-512.png
    - apps/pwa/public/apple-touch-icon.png
  modified:
    - apps/pwa/vite.config.ts
    - apps/pwa/index.html
    - apps/pwa/src/components/CalendarShell.tsx

key-decisions:
  - "Icons generated programmatically via pure Node.js (zlib/Buffer) — ImageMagick and sharp not available in the environment; minimal valid PNGs at exact pixel dimensions are functionally equivalent for PWA install purposes. Real branded icons can be dropped into public/ without any code change."
  - "Desktop InstallPrompt placement: wrapped CalendarContent in a flex-column div on desktop to allow InstallPrompt to appear as a top bar above the calendar grid without disrupting the row sidebar layout."

# Metrics
duration: ~5min
completed: 2026-06-05
---

# Phase 03 Plan 07: PWA Install — VitePWA Manifest + InstallPrompt Summary

**VitePWA manifest + auth-safe service worker + iOS icons/meta + InstallPrompt (iOS guided walkthrough + Android beforeinstallprompt) wired into CalendarShell**

## Performance

- **Duration:** ~5 min
- **Started:** 2026-06-05T22:01Z
- **Completed:** 2026-06-05T22:06Z
- **Tasks:** 2
- **Files modified:** 7

## Accomplishments

### Task 1: VitePWA manifest + service worker + iOS head/icons

- Added `VitePWA` plugin to `apps/pwa/vite.config.ts` with `registerType: 'autoUpdate'`
- Configured `workbox.navigateFallbackDenylist: [/^\/callback/, /^\/api\//, /^\/health/]` — OIDC /callback is never SW-intercepted (T-03-20 Gate 2 prerequisite)
- `runtimeCaching: []` — no authenticated API responses cached (T-03-21)
- Manifest: name/short_name FamilySync, description, theme_color #4A90D9, background_color #ffffff, display:standalone, scope:/, start_url:/, 3 icons (192, 512, 512-maskable)
- Existing `server.proxy` block preserved verbatim (including `/callback` proxy to localhost:3000)
- Generated three PNG icon files via pure Node.js (zlib/Buffer): `icon-192.png` (192×192), `icon-512.png` (512×512), `apple-touch-icon.png` (180×180) — solid #4A90D9 background with white "F" glyph
- Added five iOS `<head>` entries to `index.html`: apple-touch-icon link, theme-color meta (was already present, supplemented), apple-mobile-web-app-capable, apple-mobile-web-app-status-bar-style, apple-mobile-web-app-title
- Production build verified: `dist/manifest.webmanifest` emitted with display:standalone, scope:/, 3 icons; `dist/sw.js` and `dist/workbox-*.js` emitted

### Task 2: InstallPrompt (TDD GREEN — RED scaffold from Plan 01)

- Implemented `isIOSSafariNonStandalone()`: UA regex for iPad/iPhone/iPod + `navigator.standalone !== true`
- Implemented `useAndroidInstallPrompt()`: captures `beforeinstallprompt`, resets on `appinstalled`; returns `{ canInstall, triggerInstall }`
- `InstallPrompt` renders nothing when `display-mode: standalone` or `navigator.standalone` (already installed)
- iOS branch: dismissible banner (Smartphone icon, "Install FamilySync" heading, body + "How to install" link) gated by `localStorage.installPromptDismissed`; "How to install" opens `WalkthroughSheet` — full-screen bottom sheet with 5 annotated steps per UI-SPEC copy; orange (#F5A623) step number circles
- Android branch: banner with "Install" CTA shown only when `canInstall === true`; triggers native prompt, then dismisses
- `role="banner"`, `aria-label="Dismiss install prompt"`, 44px touch targets throughout
- Mounted `<InstallPrompt />` in `CalendarShell` for both phone and tablet/desktop layouts
- `InstallPrompt.test.tsx` GREEN: 5 tests (iOS UA detection, standalone false, Android UA false, canInstall=true on beforeinstallprompt, canInstall=false on appinstalled)
- Full PWA suite: 44 tests across 6 files — all green; `tsc --noEmit` clean

## Task Commits

1. **Task 1: VitePWA manifest + auth-safe SW denylist + iOS head/icons** — `bd82837`
2. **Task 2: InstallPrompt — iOS walkthrough banner + Android beforeinstallprompt** — `e0fb34b`

## Files Created/Modified

- `apps/pwa/vite.config.ts` — added VitePWA plugin with manifest, workbox config, auth-safe denylist
- `apps/pwa/index.html` — added 4 iOS head entries (theme-color was already present)
- `apps/pwa/public/icon-192.png` — 192×192 PNG icon (solid #4A90D9 + white "F")
- `apps/pwa/public/icon-512.png` — 512×512 PNG icon (solid #4A90D9 + white "F")
- `apps/pwa/public/apple-touch-icon.png` — 180×180 PNG icon (solid #4A90D9 + white "F")
- `apps/pwa/src/components/InstallPrompt.tsx` — iOS walkthrough + Android install prompt component (476 lines)
- `apps/pwa/src/components/CalendarShell.tsx` — import + mount InstallPrompt

## Decisions Made

- **Icon generation via pure Node.js:** ImageMagick was not available in the environment and `sharp` is not an installed project dependency. Generated minimal valid PNGs programmatically using Node.js `zlib.deflateSync` + PNG chunk encoding. Icons are structurally correct at exact pixel dimensions and pass `file` dimension checks. Placeholder visuals (solid #4A90D9 background with white "F") are sufficient for PWA installability; the operator can drop in final branded icons at any time without code changes.
- **Desktop layout wrapper:** On tablet/desktop, wrapped `<CalendarContent />` in a new `flex-column` div so that `<InstallPrompt />` can appear as a top bar above the calendar grid without disrupting the outer `flex-row` sidebar/content layout.

## Deviations from Plan

None — plan executed exactly as written. Icon generation method (pure Node.js vs ImageMagick/sharp) was anticipated by the plan's fallback note and is not a deviation.

## TDD Gate Compliance

- Task 2 had `tdd="true"` with a pre-existing RED scaffold (Plan 03-01 Task 4).
- RED gate: `InstallPrompt.test.tsx` confirmed failing before implementation (module not found error).
- GREEN gate: commit `e0fb34b` implements the component; all 5 test behaviors pass.
- No separate RED commit needed (RED scaffold existed from Plan 01, committed as `bbfccda`).

## Known Stubs

- **Icon visuals:** `icon-192.png`, `icon-512.png`, `apple-touch-icon.png` use a solid #4A90D9 fill with a simple white "F" glyph. These are functional for PWA installability (manifest validation, iOS A2HS icon display) but are placeholder art. Final branded icons can replace these files in `apps/pwa/public/` without any code change.
- **iOS walkthrough screenshots:** The walkthrough sheet renders 5 annotated step-text items. Actual iOS screenshots with annotation overlays (referenced by the plan as "annotated screenshot images may be placeholder assets") are not included — the component structure, copy, and annotation color (#F5A623) are complete; real screenshots can be added as `<img>` elements within the steps in a future pass.

## Threat Surface Scan

No new security-relevant surface introduced beyond what was in the threat model:
- T-03-20 (SW intercepts /callback): **mitigated** — `navigateFallbackDenylist` confirmed in vite.config.ts
- T-03-21 (SW caches API responses): **mitigated** — `runtimeCaching: []`
- T-03-22 (icons/manifest leak secrets): **accepted** — static public assets only

## Self-Check: PASSED

- `apps/pwa/vite.config.ts` — exists, contains `navigateFallbackDenylist`
- `apps/pwa/index.html` — exists, contains `apple-touch-icon`
- `apps/pwa/public/icon-192.png` — 192×192 PNG verified
- `apps/pwa/public/icon-512.png` — 512×512 PNG verified
- `apps/pwa/public/apple-touch-icon.png` — 180×180 PNG verified
- `apps/pwa/src/components/InstallPrompt.tsx` — exists, 476 lines
- `apps/pwa/src/components/CalendarShell.tsx` — contains InstallPrompt import + mount
- `dist/manifest.webmanifest` — display:standalone, scope:/, 3 icons verified
- Commit `bd82837` — verified in git log
- Commit `e0fb34b` — verified in git log

---
*Phase: 03-event-write-back-pwa-install*
*Completed: 2026-06-05*
