SCIX-847 Handle blocked browser storage gracefully#841
SCIX-847 Handle blocked browser storage gracefully#841thostetler wants to merge 7 commits intoadsabs:masterfrom
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #841 +/- ##
========================================
+ Coverage 62.5% 62.6% +0.2%
========================================
Files 317 320 +3
Lines 36576 36765 +189
Branches 1673 1691 +18
========================================
+ Hits 22827 22984 +157
- Misses 13709 13741 +32
Partials 40 40
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This PR adds defensive browser storage handling so the app doesn’t crash when localStorage/sessionStorage access throws SecurityError, and introduces UX fallbacks for blocked cookies/storage (moderate regression risk due to app-wide bootstrapping changes and new SSR/client branching).
Changes:
- Introduces
browserStorageutilities (safe local/session storage wrappers + availability probes) and wires Zustand persist to use a safe storage adapter. - Replaces direct
localStorage/sessionStorageusage in a few hooks/components with safe wrappers. - Adds UI: a full-page “cookies required” notice and a dismissible degraded-storage banner, plus tests for the new utilities/components.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/store/store.ts | Switches Zustand persist middleware to use createSafeStorage to avoid crashes when storage access throws. |
| src/pages/_app.tsx | Adds cookie/localStorage availability checks to render a blocking notice or a degraded banner on the client. |
| src/lib/useScrollRestoration.ts | Replaces sessionStorage direct calls with safe wrapper functions. |
| src/lib/useLandingFormPreference.ts | Replaces localStorage direct calls with safe wrapper functions. |
| src/lib/browserStorage.ts | New module providing safe storage wrappers and availability probes, plus a Zustand StateStorage adapter. |
| src/lib/tests/browserStorage.test.ts | Adds Vitest coverage for the new storage utilities. |
| src/components/StorageUnavailableNotice/StorageUnavailableNotice.tsx | New full-page notice UI shown when cookies are unavailable. |
| src/components/StorageUnavailableNotice/StorageUnavailableNotice.test.tsx | Adds tests for the notice component. |
| src/components/StorageDegradedBanner/StorageDegradedBanner.tsx | New dismissible warning banner for blocked localStorage (cookies still work). |
| src/components/StorageDegradedBanner/StorageDegradedBanner.test.tsx | Adds tests for the banner component. |
| src/components/SiteAlert/SiteAlert.tsx | Replaces localStorage direct calls with safe wrapper functions for dismiss state. |
| Browser storage is required | ||
| </Heading> | ||
| <Text> | ||
| SciX uses cookies and browser storage to maintain your session and preferences. Both are currently blocked by | ||
| your browser settings. |
There was a problem hiding this comment.
The notice copy says "cookies and browser storage ... Both are currently blocked", but this component is rendered based on cookies being unavailable (localStorage may still work). This message is misleading; adjust wording to focus on cookies/site data being blocked (and only mention local storage if you actually detect it).
| Browser storage is required | |
| </Heading> | |
| <Text> | |
| SciX uses cookies and browser storage to maintain your session and preferences. Both are currently blocked by | |
| your browser settings. | |
| Cookies are required | |
| </Heading> | |
| <Text> | |
| SciX uses cookies and site data to maintain your session and preferences. Cookies appear to be blocked by your | |
| browser settings. |
| export function createSafeStorage(): StateStorage { | ||
| return { | ||
| getItem(name: string): string | null { | ||
| try { | ||
| return localStorage.getItem(name); | ||
| } catch { | ||
| logger.debug({ name }, 'browserStorage: getItem failed'); | ||
| return null; |
There was a problem hiding this comment.
createSafeStorage() will also be used during SSR (the store is created server-side). In SSR, referencing localStorage will throw a ReferenceError that gets caught, but it will log debug messages and incur try/catch overhead on every request. Consider short-circuiting when window is undefined to return a no-op storage adapter without logging.
| import { StorageUnavailableNotice } from './StorageUnavailableNotice'; | ||
|
|
||
| const renderWithChakra = (ui: React.ReactElement) => render(<ChakraProvider>{ui}</ChakraProvider>); |
There was a problem hiding this comment.
This test uses the React namespace type (React.ReactElement) without importing React, which will fail under tsconfig jsx=react-jsx ("Cannot find namespace 'React'"). Import type ReactElement from 'react' (or remove the annotation) and use that instead.
| import { StorageUnavailableNotice } from './StorageUnavailableNotice'; | |
| const renderWithChakra = (ui: React.ReactElement) => render(<ChakraProvider>{ui}</ChakraProvider>); | |
| import type { ReactElement } from 'react'; | |
| import { StorageUnavailableNotice } from './StorageUnavailableNotice'; | |
| const renderWithChakra = (ui: ReactElement) => render(<ChakraProvider>{ui}</ChakraProvider>); |
| import { ChakraProvider } from '@chakra-ui/react'; | ||
| import { StorageDegradedBanner } from './StorageDegradedBanner'; | ||
|
|
||
| const renderWithChakra = (ui: React.ReactElement) => render(<ChakraProvider>{ui}</ChakraProvider>); |
There was a problem hiding this comment.
This test uses the React namespace type (React.ReactElement) without importing React, which will fail under tsconfig jsx=react-jsx ("Cannot find namespace 'React'"). Import type ReactElement from 'react' (or remove the annotation) and use that instead.
| import { ChakraProvider } from '@chakra-ui/react'; | |
| import { StorageDegradedBanner } from './StorageDegradedBanner'; | |
| const renderWithChakra = (ui: React.ReactElement) => render(<ChakraProvider>{ui}</ChakraProvider>); | |
| import type { ReactElement } from 'react'; | |
| import { ChakraProvider } from '@chakra-ui/react'; | |
| import { StorageDegradedBanner } from './StorageDegradedBanner'; | |
| const renderWithChakra = (ui: ReactElement) => render(<ChakraProvider>{ui}</ChakraProvider>); |
b4f9f3f to
bd5b08e
Compare
…sist Introduces browserStorage.ts with: - isCookiesAvailable/isLocalStorageAvailable probes (SecurityError-safe) - createSafeStorage: Zustand StateStorage adapter with SSR no-op short-circuit - safeLocalStorage*/safeSessionStorage* wrappers for direct call sites Wires createSafeStorage into Zustand persist middleware so store hydration no longer throws when localStorage access is denied.
Replaces localStorage/sessionStorage direct calls throughout with safe wrappers from browserStorage.ts. Prevents SecurityError from surfacing as uncaught or render-phase errors when browser blocks site data access. Files updated: useLandingFormPreference, useScrollRestoration, SiteAlert, useTour, pages/index, pages/search/index, pages/abs/abstract. Also moves useLandingFormPreference's initial read from a useState lazy initializer to useEffect, preventing render-phase SecurityError under Next.js/Turbopack.
- StorageUnavailableNotice: full-page blocking notice when cookies are unavailable, with links to browser-specific enable instructions - StorageDegradedBanner: dismissible warning banner when localStorage is blocked but cookies still work - _app.tsx: probes storage once on mount via useState+useEffect, renders the appropriate notice or banner without SSR hydration mismatch
bd5b08e to
f1b550f
Compare
Adds a banner prop to Layout so StorageDegradedBanner renders at full width between the navbar and main content, rather than inside the Container which constrains it with horizontal padding.
When localStorage throws, safeLocalStorageGet/Set now read/write a module-level Map instead of silently no-oping. This keeps dismissal state (e.g. tour 'seen' flags) alive for the page session even with storage blocked, preventing the tour from re-triggering on every render.
When a user has cookies or site data blocked in their browser, the app crashes with a SecurityError thrown by localStorage access during Zustand store initialization. Iron-session also cannot persist session cookies, causing continuous re-bootstrapping on every page load.