whisper-money/resources/js/lib
Víctor Falcón 239317f7f9
fix(ui): stop unusable web storage from white-screening the app (#743)
## The bug

Two Sentry issues share one minified boot frame, `vq` →
`initializeTheme()`:

-
[PHP-LARAVEL-57](https://whisper-money.sentry.io/issues/PHP-LARAVEL-57)
— `TypeError: Cannot read properties of null (reading 'getItem')`.
Chrome Mobile WebView 131 / Android 15: the host app disabled DOM
storage, so `window.localStorage` is `null`.
-
[PHP-LARAVEL-4Y](https://whisper-money.sentry.io/issues/PHP-LARAVEL-4Y)
— `SecurityError: The operation is insecure.`, 17 events. Blocking
cookies/site data makes the very first access throw.

`app.tsx` runs `initializeTheme()` and `initializeChartColorScheme()` at
module scope, before React mounts, so either one white-screens the app.
Same class as PHP-LARAVEL-41 (Safari <14 `addEventListener`), which is
why `lib/media-query.ts` exists — `lib/safe-storage.ts` is its sibling.

## What both reviews caught

My first pass only routed the two initializers, and **that would not
have closed either issue**. Both events carry url
`https://whisper.money/`, and the landing page reads `localStorage`
unguarded in a mount effect (`welcome.tsx:2105`) — those users would
have white-screened a few milliseconds later instead. There is also **no
error boundary anywhere in this app**, so any throw in render or an
effect unmounts the whole root; there is no "broken widget" failure
mode.

So the fix also covers:

- **`welcome.tsx`** — the landing page's locale effect, the url both
issues carry.
- **`chunk-load-recovery.ts:61,71`** — `window.sessionStorage` was
evaluated in an argument list, *outside* the try/catch that guards its
use. For the SecurityError cohort the global `error` listener itself
threw, and that throw was reported as another error event, re-entering
the same listener. It also meant chunk-load auto-recovery was dead
precisely on the browsers most likely to hold stale assets.
- **`edit-transaction-dialog.tsx:104`** — a read inside a lazy
`useState`, i.e. during render, on `/transactions` and account pages.
Its `typeof window !== 'undefined'` guard checks the wrong axis:
`window` exists, `window.localStorage` is null.

## Not just "doesn't crash" — the preference still works

Making the crash survivable exposed what happens next. The `appearance`
cookie is unencrypted, `HandleAppearance` renders the page from it, and
the blade applies it before any JS runs. Then `initializeTheme` read an
empty localStorage, fell back to `'system'`, and stripped the `dark`
class again — so a no-storage user who chose Dark got light on every
load. The mount effect was worse: it called `updateAppearance`, which
writes the cookie, so simply opening the app reset the one channel that
still persisted for these users.

Now the cookie is read as a fallback, the mount effect hydrates without
writing back, and `initializeChartColorScheme` only overrides the
blade-rendered `data-chart-color` when something is actually stored
(previously it reset everyone to `colorful`, leaving the CSS palette
fighting the colors the charts draw with).

For users whose storage works, behaviour is unchanged: `readStoredValue`
returns exactly what `getItem` returned, `null` included.

## Tests

7 vitest cases. The helper's four (working storage, null global,
throwing getter, throwing `setItem`) plus three that pin the invariant
that actually regressed — the boot initializers survive a null and a
throwing storage, and leave the server's theme alone when nothing is
stored. Verified they fail if `initializeTheme` goes back to a bare
`localStorage` call; the helper-only tests do not.

## Follow-ups (not here)

- **An error boundary in `app.tsx`** — both reviews called this the
highest-leverage item in the whole change. ~10 unguarded storage sites
remain, and without a boundary each is a fresh white screen rather than
a contained failure plus a Sentry report. It needs fallback UI and a
visual check, so it deserves its own PR.
- **The remaining unguarded sites**, in blast-radius order:
`lib/debug.ts:4` (called ~30× per rule evaluation from the rule engine,
so auto-categorization dies for these users),
`transaction-analysis-drawer.tsx` / `category-analysis-drawer.tsx`
(drawers fail to open), `lib/key-storage.ts` (on a 1s interval, but
gated behind the deprecated encryption setup — and note storage that
silently fails to persist is the *wrong* policy for a key),
`hooks/use-admin.tsx` (no call sites — delete it).
- Six call sites already hand-roll this exact try/catch idiom, which is
the argument for the helper existing; folding them in would also remove
three duplicated localStorage stubs from the test suite.

Fixes PHP-LARAVEL-57
Fixes PHP-LARAVEL-4Y
2026-08-09 15:33:22 +00:00
..
banking-connections.test.ts fix(open-banking): only block re-adding a bank when a live connection exists (#569) 2026-06-20 13:09:04 +02:00
banking-connections.ts fix(open-banking): only block re-adding a bank when a live connection exists (#569) 2026-06-20 13:09:04 +02:00
category-tree.test.ts feat: parent/child category tree (#474) 2026-06-03 19:30:12 +02:00
category-tree.ts feat: parent/child category tree (#474) 2026-06-03 19:30:12 +02:00
chart-calculations.test.ts fix(dashboard): render negative net worth as a downward bar (#722) 2026-07-22 09:32:18 +00:00
chart-calculations.ts fix(dashboard): render negative net worth as a downward bar (#722) 2026-07-22 09:32:18 +00:00
chart-tooltip-position.test.ts fix(chart): stop tooltip render loop crashing the dashboard (PHP-LARAVEL-3B) (#657) 2026-07-07 18:39:09 +00:00
chart-tooltip-position.ts fix(chart): stop tooltip render loop crashing the dashboard (PHP-LARAVEL-3B) (#657) 2026-07-07 18:39:09 +00:00
chunk-load-recovery.test.ts Fix Vite asset preload recovery (#399) 2026-05-14 15:59:08 +02:00
chunk-load-recovery.ts fix(ui): stop unusable web storage from white-screening the app (#743) 2026-08-09 15:33:22 +00:00
connect-providers.tsx feat(banking): enable Interactive Brokers for all users (#593) 2026-06-26 11:03:21 +02:00
crypto.ts refactor(encryption): strip client-side transaction encryption (#514) 2026-06-20 16:13:26 +00:00
csrf.test.ts refactor(js): extract shared getCsrfToken util (#475) 2026-06-03 17:26:09 +02:00
csrf.ts refactor(js): extract shared getCsrfToken util (#475) 2026-06-03 17:26:09 +02:00
cursor-pagination.test.ts Fix Sentry transaction and dashboard crashes (#372) 2026-05-10 11:10:31 +01:00
cursor-pagination.ts Fix Sentry transaction and dashboard crashes (#372) 2026-05-10 11:10:31 +01:00
debug.ts fix: make useIsMobile hook and utility functions SSR-safe 2025-12-08 17:33:54 +01:00
dexie-db.ts fix(sync): don't crash when IndexedDB is unavailable (PHP-LARAVEL-43) (#654) 2026-07-07 04:59:10 +00:00
file-parser.test.ts fix(import): honor selected date format for CSV imports (#494) 2026-06-05 15:10:48 +02:00
file-parser.ts fix(import): honor selected date format for CSV imports (#494) 2026-06-05 15:10:48 +02:00
import-config-storage.ts feat(import): persist per-account import configuration on the backend (#698) 2026-07-18 16:10:40 +02:00
key-storage.ts refactor(encryption): strip client-side transaction encryption (#514) 2026-06-20 16:13:26 +00:00
media-query.test.ts fix(appearance): support MediaQueryList change events on legacy Safari (PHP-LARAVEL-41) (#646) 2026-07-05 17:03:35 +00:00
media-query.ts fix(appearance): support MediaQueryList change events on legacy Safari (PHP-LARAVEL-41) (#646) 2026-07-05 17:03:35 +00:00
new-transactions.test.ts feat(transactions): make new-transaction marker cross-device (#611) 2026-06-29 19:11:37 +02:00
new-transactions.ts feat(transactions): make new-transaction marker cross-device (#611) 2026-06-29 19:11:37 +02:00
orphan-components.test.ts chore(frontend): add orphan component detection and remove dead components (#181) 2026-03-02 12:43:27 +01:00
posthog.test.ts Harden browser storage and PostHog recording (#402) 2026-05-14 15:57:48 +02:00
posthog.ts feat(posthog): route analytics through reverse proxy (#463) 2026-06-01 09:29:30 +02:00
rule-builder-utils.test.ts feat(transactions): add counterparty fields (#440) 2026-05-27 16:20:55 +02:00
rule-builder-utils.ts feat: parent/child category tree (#474) 2026-06-03 19:30:12 +02:00
rule-engine-parity.test.ts refactor: consolidate duplicated financial calculations (#643) 2026-07-04 22:26:44 +02:00
rule-engine.ts feat(transactions): add counterparty fields (#440) 2026-05-27 16:20:55 +02:00
safe-storage.test.ts fix(ui): stop unusable web storage from white-screening the app (#743) 2026-08-09 15:33:22 +00:00
safe-storage.ts fix(ui): stop unusable web storage from white-screening the app (#743) 2026-08-09 15:33:22 +00:00
sankey-utils.ts Render the Cashflow Sankey with recharts (responsive + expense drill-down) (#670) 2026-07-12 16:26:17 +00:00
sentry.test.ts fix(sentry): drop browser-extension noise before sending events (#568) 2026-06-20 12:47:05 +02:00
sentry.ts fix(sentry): drop browser-extension noise before sending events (#568) 2026-06-20 12:47:05 +02:00
subscription-payment-issue-toast.test.ts feat: keep past due subscriptions active (#416) 2026-05-22 10:19:11 +01:00
subscription-payment-issue-toast.ts feat: keep past due subscriptions active (#416) 2026-05-22 10:19:11 +01:00
sync-manager.test.ts fix(sync): don't crash when IndexedDB is unavailable (PHP-LARAVEL-43) (#654) 2026-07-07 04:59:10 +00:00
sync-manager.ts fix(sync): don't crash when IndexedDB is unavailable (PHP-LARAVEL-43) (#654) 2026-07-07 04:59:10 +00:00
transaction-delete-confirmation.test.ts Fix Spanish translations in category and delete flows (#397) 2026-05-14 11:36:53 +01:00
transaction-delete-confirmation.ts Fix Spanish translations in category and delete flows (#397) 2026-05-14 11:36:53 +01:00
transaction-filter-serialization.ts feat(transactions): save and reuse transaction filters (#496) 2026-06-05 18:00:14 +02:00
transaction-re-evaluation.test.ts Add labels to automation rules (#379) 2026-05-11 14:56:25 +02:00
transaction-re-evaluation.ts Add labels to automation rules (#379) 2026-05-11 14:56:25 +02:00
utils.ts Prevent duplicate notes when re-evaluating automation rules 2025-12-04 16:42:54 +01:00