--- 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 `` 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 `` 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 `` in a new `flex-column` div so that `` 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 `` 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*