Skip to content

feat(adapters): persist WalletConnect sessions across page reloads - #835

Open
woahwhattheheck wants to merge 2 commits into
Stellar-split:mainfrom
woahwhattheheck:feat/774-walletconnect-session-persistence
Open

feat(adapters): persist WalletConnect sessions across page reloads#835
woahwhattheheck wants to merge 2 commits into
Stellar-split:mainfrom
woahwhattheheck:feat/774-walletconnect-session-persistence

Conversation

@woahwhattheheck

Copy link
Copy Markdown

Closes #774.

The adapter held the WalletConnect session only in memory, so every page
reload dropped it and the user had to scan a fresh QR code.

Changes

src/adapters/walletconnect.ts:

  • Persist on construction. Constructing the adapter is the point at which
    the SDK is handed a live session, so that is where the session is written.
  • WalletConnectAdapter.restore(client, storage?, nowSeconds?) — rebuilds
    an adapter from the stored session, or returns null when there is nothing
    usable.
  • disconnect() — ends the client session and clears the stored copy.
  • Standalone helpers persistWalletConnectSession,
    loadWalletConnectSession, clearWalletConnectSession, and the exported
    WALLETCONNECT_SESSION_KEY, for callers managing the session themselves.

expiry is a Unix timestamp in seconds, matching what WalletConnect puts
on a session object, and is checked on restore.

Details that shape the implementation

  • A session with no expiry is not persisted at all. Without one there is
    no way to tell a live session from a dead one on the next load, and silently
    restoring a dead session is worse for the user than asking for a reconnect —
    it fails later, at signing time, instead of immediately.
  • disconnect() clears storage in a finally. If the relay is
    unreachable the client's own disconnect rejects; clearing regardless means a
    relay error cannot leave a dead session behind to be restored next load. The
    rejection still propagates.
  • A corrupt or expired entry is removed as it is read, so a bad value
    cannot be re-parsed on every subsequent load.
  • Every storage access is guarded. localStorage may be absent (Node),
    disabled, full, or — in some privacy modes — a throwing getter rather than
    simply missing. Persisting degrades to a no-op in all of those cases rather
    than breaking an otherwise good session.

Storage is typed structurally (getItem/setItem/removeItem), so a React
Native shim or a test stub can be injected without this module depending on
DOM lib types.

Validation

  • npx vitest run test/walletconnect.session.test.ts — 21/21 passing.
    Covers round-trip, namespaced key, expiry (including exactly-at-now),
    unparseable JSON, structurally wrong records, absent store, a store that
    throws on every operation, construction with and without persist,
    construction without expiry, restore hit/expired/empty, restore leaving
    storage untouched, disconnect clearing on success and on relay failure, and
    a client exposing no disconnect.
  • Offline: an in-memory store stands in for localStorage and the sign client
    is a stub — no network, no DOM.
  • npx tsc --noEmit reports 210 errors on this branch and 210 on unmodified
    main
    — identical, with none in src/adapters/walletconnect.ts.

The adapter held the session only in memory, so every reload forced a fresh
QR scan. The session is now written to localStorage on construction, restored
by WalletConnectAdapter.restore when still valid, and cleared on disconnect.

Storage access is guarded throughout: a missing, disabled or throwing store
degrades to not persisting rather than breaking a working session.
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.

Persist WalletConnect session across page reloads

1 participant