161 lines
11 KiB
Markdown
161 lines
11 KiB
Markdown
---
|
||
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>
|