feat(03-07): InstallPrompt — iOS walkthrough banner + Android beforeinstallprompt

- Implement isIOSSafariNonStandalone(): iOS UA + navigator.standalone detection
- Implement useAndroidInstallPrompt(): captures beforeinstallprompt, exposes canInstall/triggerInstall
- InstallPrompt renders nothing when display-mode:standalone or navigator.standalone (already installed)
- iOS branch: dismissible banner with 'Install FamilySync' heading, 'How to install' link
  opens 5-step walkthrough sheet (exact UI-SPEC copy, orange #F5A623 step number annotation)
- Android branch: banner with 'Install' button shown only when canInstall=true
- localStorage.installPromptDismissed persists banner dismissal
- role="banner", dismiss aria-label="Dismiss install prompt", 44px touch targets
- Mount <InstallPrompt /> in CalendarShell (phone: below AppNav; desktop: top of content area)
- InstallPrompt.test.tsx GREEN (5 behavior tests); full PWA suite 44 tests green; tsc clean
This commit is contained in:
Lucas Berger
2026-06-05 18:05:30 -04:00
parent bd8283774d
commit e0fb34b252
2 changed files with 482 additions and 1 deletions
+6 -1
View File
@@ -48,6 +48,7 @@ import { EventDetailPopover } from './EventDetailPopover.js'
import { AppNav } from './AppNav.js'
import { ColorLegend } from './ColorLegend.js'
import { SkeletonCalendar } from './SkeletonCalendar.js'
import { InstallPrompt } from './InstallPrompt.js'
// ── Helpers ────────────────────────────────────────────────────────────────
@@ -314,6 +315,7 @@ export function CalendarShell() {
currentUserColor={meQuery.data?.user.color}
currentUserName={meQuery.data?.user.displayName ?? undefined}
/>
<InstallPrompt />
<CalendarContent />
<EventDetailPopover />
</div>
@@ -341,7 +343,10 @@ export function CalendarShell() {
/>
{/* Main content area */}
<CalendarContent />
<div style={{ flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
<InstallPrompt />
<CalendarContent />
</div>
{/* EventDetailPopover — standalone mode driven by Zustand openEventId */}
<EventDetailPopover />