diff --git a/apps/pwa/src/components/InstallPrompt.tsx b/apps/pwa/src/components/InstallPrompt.tsx index a6d6a4d..f19443e 100644 --- a/apps/pwa/src/components/InstallPrompt.tsx +++ b/apps/pwa/src/components/InstallPrompt.tsx @@ -278,10 +278,27 @@ function WalkthroughSheet({ onClose }: WalkthroughSheetProps) { * * Mount this at the top level of CalendarShell, below the nav bar. */ +// IN-03: localStorage access is guarded — in private-mode / SSR contexts the API can +// throw on read or write. Mirrors calendarStore.ts's pattern so a storage failure +// degrades gracefully (treated as "not dismissed") instead of crashing the component. +function readDismissed(): boolean { + try { + return localStorage.getItem('installPromptDismissed') === '1' + } catch { + return false + } +} + +function persistDismissed(): void { + try { + localStorage.setItem('installPromptDismissed', '1') + } catch { + // Ignore write failures (private mode / storage disabled) + } +} + export function InstallPrompt() { - const [dismissed, setDismissed] = useState( - () => localStorage.getItem('installPromptDismissed') === '1', - ) + const [dismissed, setDismissed] = useState(readDismissed) const [walkthroughOpen, setWalkthroughOpen] = useState(false) const { canInstall, triggerInstall } = useAndroidInstallPrompt() @@ -295,7 +312,7 @@ export function InstallPrompt() { if (installed) return null function dismiss() { - localStorage.setItem('installPromptDismissed', '1') + persistDismissed() setDismissed(true) }