Files
familysync/.planning/phases/03-event-write-back-pwa-install/03-07-PLAN.md
T

161 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
phase: 03-event-write-back-pwa-install
plan: 07
type: execute
wave: 3
depends_on: ["03-01"]
files_modified:
- apps/pwa/vite.config.ts
- apps/pwa/index.html
- apps/pwa/public/icon-192.png
- apps/pwa/public/icon-512.png
- apps/pwa/public/apple-touch-icon.png
- apps/pwa/src/components/InstallPrompt.tsx
- apps/pwa/src/components/CalendarShell.tsx
autonomous: true
requirements: [PWA-01, PWA-02]
user_setup: []
must_haves:
truths:
- "The production build emits a valid manifest.webmanifest with name/icons/display:standalone/scope:/ and a service worker"
- "The service worker's navigateFallbackDenylist excludes /callback, /api/, /health so the OIDC redirect is never intercepted (Gate 2 risk)"
- "On iOS Safari non-standalone, a first-visit install banner appears with a 5-step annotated Add-to-Home-Screen walkthrough"
- "On Android, an Install banner appears only when beforeinstallprompt fires and triggers the native prompt"
- "Neither install surface renders when the app is already installed (display-mode: standalone)"
artifacts:
- path: "apps/pwa/vite.config.ts"
provides: "VitePWA manifest + SW config with auth-safe denylist"
contains: "navigateFallbackDenylist"
- path: "apps/pwa/src/components/InstallPrompt.tsx"
provides: "iOS walkthrough banner/sheet + Android beforeinstallprompt banner"
min_lines: 80
key_links:
- from: "apps/pwa/vite.config.ts"
to: "OIDC /callback"
via: "navigateFallbackDenylist excludes /callback"
pattern: "callback"
- from: "apps/pwa/src/components/InstallPrompt.tsx"
to: "iOS standalone detection"
via: "isIOSSafariNonStandalone + display-mode media query"
pattern: "standalone"
---
<objective>
Make FamilySync installable (PWA-01) and guide first-time install (PWA-02): add the
`vite-plugin-pwa` manifest + service worker with an auth-safe `navigateFallbackDenylist`,
the required iOS `<head>` meta/icons, the PWA icon assets, and the `InstallPrompt`
component handling both the iOS annotated Add-to-Home-Screen walkthrough and the Android
`beforeinstallprompt` flow.
Purpose: PWA-01/02 are prerequisites for Phase 5 Web Push — the non-technical member must
be able to install unassisted. The single hard constraint is that the service worker MUST
NOT intercept the OIDC `/callback` (Gate 2 / Pitfall 1) or break iOS standalone login.
Output: configured VitePWA build, install icons + meta, InstallPrompt mounted in the shell.
</objective>
<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/phases/03-event-write-back-pwa-install/03-UI-SPEC.md
@.planning/phases/03-event-write-back-pwa-install/03-RESEARCH.md
@.planning/phases/03-event-write-back-pwa-install/03-PATTERNS.md
@apps/pwa/vite.config.ts
</context>
<tasks>
<task type="auto">
<name>Task 1: VitePWA manifest + service worker (auth-safe denylist) + iOS head/icons</name>
<files>apps/pwa/vite.config.ts, apps/pwa/index.html, apps/pwa/public/icon-192.png, apps/pwa/public/icon-512.png, apps/pwa/public/apple-touch-icon.png</files>
<read_first>
- apps/pwa/vite.config.ts (existing — keep the proxy block incl. /callback verbatim; add VitePWA to plugins)
- .planning/phases/03-event-write-back-pwa-install/03-RESEARCH.md (§Pattern 5 — full VitePWA config; required icons; head meta; §Pitfall 1 /callback denylist; §Pitfall 6 dev-mode SW caveat)
- .planning/phases/03-event-write-back-pwa-install/03-UI-SPEC.md (§PWA Manifest Contract — exact field values; §SW critical denylist)
- apps/pwa/index.html (existing head to extend)
</read_first>
<action>
Add the `VitePWA` plugin to the existing `plugins` array in vite.config.ts per RESEARCH.md Pattern 5: `registerType:'autoUpdate'`; `workbox.navigateFallback:'/index.html'`; `workbox.navigateFallbackDenylist: [/^\/callback/, /^\/api\//, /^\/health/]` (CRITICAL — OIDC + API must reach the server); `workbox.runtimeCaching: []` (no API caching). `manifest`: name "FamilySync", short_name "FamilySync", description "Family calendar and lists", theme_color "#4A90D9", background_color "#FFFFFF", display "standalone", scope "/", start_url "/", icons 192/512/512-maskable per the contract. Keep the existing `server.proxy` block (including `/callback`) exactly as-is.
Generate the three icon PNGs into apps/pwa/public/: `icon-192.png` (192×192), `icon-512.png` (512×512), `apple-touch-icon.png` (180×180). Create a simple solid `#4A90D9` background with a white "F" / calendar glyph using an available CLI tool (ImageMagick `convert`, `sharp` via a one-off node script, or similar). If no image tool is available, set autonomous:false is NOT needed — emit minimal valid PNGs programmatically (node Buffer / sharp). The icons must be valid PNGs at the exact pixel dimensions.
Add to apps/pwa/index.html `<head>` the five entries from RESEARCH.md Pattern 5: apple-touch-icon link (180×180), theme-color meta (#4A90D9), apple-mobile-web-app-capable yes, apple-mobile-web-app-status-bar-style default, apple-mobile-web-app-title FamilySync.
</action>
<verify>
<automated>cd /home/luc/Projects/familysync && pnpm --filter @familysync/pwa build && test -f apps/pwa/dist/manifest.webmanifest && node -e "const m=require('./apps/pwa/dist/manifest.webmanifest');if(m.display!=='standalone'||m.scope!=='/'||!m.icons.some(i=>i.sizes==='512x512'))process.exit(1)" && grep -q "navigateFallbackDenylist" apps/pwa/vite.config.ts && grep -q "apple-touch-icon" apps/pwa/index.html</automated>
</verify>
<acceptance_criteria>
- Production build emits `apps/pwa/dist/manifest.webmanifest` with display:standalone, scope:/, and a 512×512 icon.
- `grep -c "/^\\\\/callback/" apps/pwa/vite.config.ts` ≥1 (denylist present) — and `/callback` is in navigateFallbackDenylist.
- Three icon PNGs exist in apps/pwa/public at the correct dimensions (`file apps/pwa/public/icon-192.png` reports 192 x 192).
- index.html contains the five iOS head entries.
</acceptance_criteria>
<done>Build produces a valid installable manifest + auth-safe SW; iOS icons/meta present.</done>
</task>
<task type="auto" tdd="true">
<name>Task 2: InstallPrompt — iOS walkthrough banner/sheet + Android beforeinstallprompt</name>
<files>apps/pwa/src/components/InstallPrompt.tsx, apps/pwa/src/components/CalendarShell.tsx</files>
<read_first>
- apps/pwa/src/components/InstallPrompt.test.tsx (RED stubs from Plan 01 — isIOSSafariNonStandalone + useAndroidInstallPrompt contract)
- .planning/phases/03-event-write-back-pwa-install/03-RESEARCH.md (§Pattern 6 iOS detection + 5-step walkthrough; §Pattern 7 useAndroidInstallPrompt; §Code Examples isInstalled display-mode check)
- .planning/phases/03-event-write-back-pwa-install/03-UI-SPEC.md (§InstallPrompt iOS banner/sheet + Android banner; §Copywriting install strings; §Interaction Contract iOS/Android install; localStorage installPromptDismissed)
- apps/pwa/src/components/EmptyState.tsx (partial analog — informational surface, token usage)
- apps/pwa/src/components/EventDetailPopover.tsx (token + lucide icon import conventions)
</read_first>
<behavior>
Tests (InstallPrompt.test.tsx → GREEN): `isIOSSafariNonStandalone()` true for a mock iOS Safari non-standalone UA, false in standalone; `useAndroidInstallPrompt` sets `canInstall=true` when a mock `beforeinstallprompt` dispatches and calls preventDefault; the component renders nothing when `display-mode: standalone` matches; the iOS banner renders the heading "Install FamilySync" with a "How to install" link; the Android banner renders an "Install" button only when canInstall.
</behavior>
<action>
Implement `InstallPrompt.tsx` with `isIOSSafariNonStandalone()` and `useAndroidInstallPrompt()` per RESEARCH.md Patterns 6/7. Render nothing if already installed (`window.matchMedia('(display-mode: standalone)').matches` or `navigator.standalone`). iOS branch: a dismissible first-visit banner (Smartphone icon, heading/body/CTA per UI-SPEC copy) gated by `localStorage.installPromptDismissed`; "How to install" opens a full-screen bottom-sheet with the 5 annotated steps (exact step copy from UI-SPEC; annotation overlay color `--color-member-2` #F5A623; "Done" closes). Android branch: banner shown only when `canInstall`, with an "Install" button calling `triggerInstall()` then dismiss. Use tokens for all spacing/color, 44px touch targets, plain-text JSX children, `role="banner"`, dismiss `aria-label="Dismiss install prompt"`. Mount `<InstallPrompt />` in CalendarShell (top-level, below nav). Annotated screenshot images may be placeholder assets referenced by path under public/ (real screenshots can be dropped in later); the component structure and copy must be complete and correct.
</action>
<verify>
<automated>cd /home/luc/Projects/familysync && pnpm --filter @familysync/pwa test -- InstallPrompt && grep -q "InstallPrompt" apps/pwa/src/components/CalendarShell.tsx && grep -q "display-mode: standalone" apps/pwa/src/components/InstallPrompt.tsx && pnpm --filter @familysync/pwa exec tsc --noEmit</automated>
</verify>
<acceptance_criteria>
- InstallPrompt.test.tsx GREEN (iOS detection, Android prompt capture, standalone-hides, banner copy).
- `grep -q "isIOSSafariNonStandalone" apps/pwa/src/components/InstallPrompt.tsx`.
- InstallPrompt mounted in CalendarShell.
- Full PWA suite green; tsc --noEmit passes.
</acceptance_criteria>
<done>iOS guided walkthrough + Android prompt work; nothing shows when already installed; mounted in the shell.</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| service worker → navigation | The SW can intercept navigations including the OIDC callback |
## STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-03-20 | Spoofing | SW serving a cached shell for /callback, breaking OIDC code exchange / iOS standalone login | mitigate | `navigateFallbackDenylist: [/^\/callback/, /^\/api\//, /^\/health/]`; verified against a production build (Pitfall 1/6); Gate 2 confirms end-to-end (Plan 08) |
| T-03-21 | Tampering | SW caching authenticated API responses | mitigate | `runtimeCaching: []` — no /api caching; /api in denylist |
| T-03-22 | Information Disclosure | manifest/icons leaking nothing sensitive | accept | Static public assets only; no secrets in manifest |
</threat_model>
<verification>
- `pnpm --filter @familysync/pwa build` emits valid manifest.webmanifest + SW.
- `/callback`, `/api/`, `/health` all in navigateFallbackDenylist.
- `pnpm --filter @familysync/pwa test` green (InstallPrompt + existing); tsc --noEmit passes.
</verification>
<success_criteria>
- PWA-01: app installs to Home Screen (manifest + SW, standalone) on iOS and Android.
- PWA-02: first-time guided install (iOS walkthrough + Android prompt); never shown when installed.
- OIDC `/callback` is never SW-intercepted (Gate 2 prerequisite).
</success_criteria>
<output>
Create `.planning/phases/03-event-write-back-pwa-install/03-07-SUMMARY.md` when done.
</output>