Skip to content

fix: harden popup empty state UX and make it self-healing#40

Merged
BitHighlander merged 3 commits intodevelopfrom
fix/popup-empty-state-ux
Apr 21, 2026
Merged

fix: harden popup empty state UX and make it self-healing#40
BitHighlander merged 3 commits intodevelopfrom
fix/popup-empty-state-ux

Conversation

@BitHighlander
Copy link
Copy Markdown
Collaborator

Summary

Companion to #39 (popup lifecycle hardening). That PR fixes why the popup can end up stuck; this PR fixes what the user sees when it does.

Targets three failure modes:

  1. Popup open with a bare "No events" string and no way forward.
  2. White-screen crash when `requestStorage.getEvents()` returns null (e.g. storage uninitialized after a service worker restart — `for (const event of null)` throws).
  3. A second request arriving while the popup is already open is invisible until the current request is resolved, because Events.tsx only fetches once on mount.

Changes

`pages/popup/src/components/Events.tsx` (rewritten)

  • Null-safe fetch: `(await requestStorage.getEvents()) || []`, wrapped in try/catch with a dedicated error state.
  • Live subscription: subscribes to `requestStorage.subscribe(...)` so new events (or cleanup of the current event) trigger a refetch without needing to unmount/remount the popup.
  • Auto-close on empty: after 3 seconds in the empty state the window closes itself. Covers: dapp cancelled upstream, storage cleanup ran before `window.close`, popup opened with no pending request.
  • Clear copy for loading / empty / error states: explains what's happening and tells the user the window will close itself.
  • currentIndex bounds check: if the event list shrinks below the current index (event removed while you were looking at it), snap back into bounds instead of rendering `undefined`.

`pages/popup/src/Popup.tsx`

  • The error boundary fallback was a literal `
    Error Occur
    `. Replaced with a Chakra-styled panel that explains what happened and has an explicit Close button, so a render-time crash still leaves the user with an obvious way out.
  • Loading fallback gets a spinner + label for consistency.

Test plan

  • Open popup with a pending request — renders Transaction as before.
  • Open popup with no pending request — shows "No pending requests", auto-closes after 3s.
  • Fire two dapp requests back to back — both become visible in the popup without needing to resolve the first.
  • Approve a sign request — popup shows completion, then closes (unchanged behavior).
  • Force `requestStorage.getEvents()` to throw (e.g. corrupted storage) — popup shows error state with Close button instead of white-screening.
  • Force a render-time error in EventsViewer — error boundary fallback renders with working Close button.

🤖 Generated with Claude Code

BitHighlander and others added 3 commits April 18, 2026 22:34
Addresses the "popup open but says 'no events' with no recovery"
failure mode, plus a latent crash if requestStorage returns null.

Events.tsx
- Null-guard requestStorage.getEvents() — previously a null return
  would throw on `for...of null` and white-screen the popup.
- Wrap the fetch in try/catch and render a dedicated error state
  instead of crashing.
- Subscribe to requestStorage changes so a second request arriving
  while the popup is open becomes visible immediately (no need to
  resolve the current request first).
- Auto-close the window 3s after landing in an empty state. Covers
  the case where a request was cancelled upstream, where cleanup ran
  before the window closed itself, or where the popup was opened
  with no pending request.
- Better copy in the empty / loading / error states so the user
  knows what is happening and that the window will close itself.
- Keep currentIndex in bounds if the event list shrinks beneath it.

Popup.tsx
- Replace the placeholder "Error Occur" / "Loading ..." fallbacks
  with a proper Chakra-styled error panel that includes a Close
  button, so a rendering crash doesn't leave the user with no way
  out other than clicking the OS window close button.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The useEffect-based bounds check snapped currentIndex back only AFTER
the render that caused it to go out of bounds. When requestStorage's
subscribe fired and the event list shrank beneath currentIndex, the
first render after the shrink still had the stale index — passing
events[currentIndex] = undefined into <Transaction />, which reads
event.id immediately and error-boundary crashes.

Compute a clamped safeIndex during render instead. currentIndex as
state is preserved for user-driven navigation; only the array access
is guarded.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The error-state UI copy already said the window would close itself,
but the auto-close effect bailed on fetchError, leaving the user stuck
on a dead-end error screen — the exact failure mode this PR is trying
to remove.

Collapse the two conditions into a single shouldAutoClose predicate so
any non-actionable state (empty OR fetch failure) triggers the timer.
Cleanup still runs correctly on recovery (error clears → new events
arrive → previous cleanup cancels the timer, new effect short-circuits).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@BitHighlander BitHighlander merged commit c2019ea into develop Apr 21, 2026
3 of 4 checks passed
@BitHighlander BitHighlander deleted the fix/popup-empty-state-ux branch April 21, 2026 00:25
@BitHighlander BitHighlander mentioned this pull request Apr 21, 2026
4 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant