Skip to content

feat(frontend): allow multiple simultaneous WalletConnect connections#13152

Open
sbpublic wants to merge 10 commits into
mainfrom
feat/frontend/multiple-walletconnect-connections
Open

feat(frontend): allow multiple simultaneous WalletConnect connections#13152
sbpublic wants to merge 10 commits into
mainfrom
feat/frontend/multiple-walletconnect-connections

Conversation

@sbpublic

@sbpublic sbpublic commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator

Motivation

Today a user can only have one WalletConnect connection open at a time — connecting a second dApp silently drops the first, and the "Connected Apps" list can only ever show one entry. This is an app-layer restriction; Reown WalletKit natively supports many concurrent sessions. This change lets users connect several dApps (e.g. Uniswap and Liquidium) at once and manage each independently.

Implements docs/ai/spec-driven-development/specs/2026-06-20-impr-multiple-walletconnect-connections.md.

Changes

  • Add disconnectSession(topic) to the WalletConnect listener (single-session teardown) alongside the existing all-sessions disconnect().
  • Add a reactive walletConnectSessionsStore as the source of truth for the connected-apps UI (the persistent listener reference no longer changes on add/remove).
  • Reuse the existing listener when adding a connection (cold-start init uses cleanSlate: false); add syncSessions(), a per-session disconnect service that falls through to full teardown when the last app is removed, and a guarded resetListenerIfNoSessions().
  • Keep sibling connections alive on session_delete and on proposal reject/cancel/pair-failure.
  • Sessions modal: per-row close disconnects only that topic; new "Disconnect all" toolbar button (wallet_connect.text.disconnect_all).
  • Add the disconnect_all label and translate it for all 14 supported non-English languages.
  • Update PRODUCT.md; add the spec.

Divergence from the spec

  • The single-session_delete guard lives in the call-site callbacks via a shared resetListenerIfNoSessions() rather than inside onSessionDelete (same effect, less duplication).
  • Added beyond the spec: guarded the proposal reject/cancel and pair-failure paths too — they previously called resetListener() unconditionally, which would drop a live first connection when a second proposal is rejected or fails to pair. resetListenerIfNoSessions() also detaches handlers before reset to avoid leaking duplicate handlers on the singleton WalletKit emitter.
  • Per-row button uses a testId instead of a new aria-label string, keeping to the single new i18n key the spec specified.

Tests

  • New/extended unit tests: provider disconnectSession, sessions store, services (syncSessions, per-session disconnect, guarded reset, last-app teardown), and modal (per-row disconnect by topic, "Disconnect all", reactive updates).
  • npm run format, npm run lint --max-warnings 0, npm run check all clean; the 108 wallet-connect/scanner tests pass. (Full local npm run test has unrelated failures from a local Node/jsdom localStorage clash in untouched files — not reproducible in CI.)

Review feedback

  • Copilot (toast on failure): disconnectSession swallows its own errors, so the per-row "disconnected" toast fired even on a failed disconnect. The service now returns ResultSuccess and the modal toasts only on success.
  • Copilot (accessibility): the icon-only per-row disconnect button had no accessible name. Added a localized ariaLabel via a new i18n key wallet_connect.text.disconnect_app ("Disconnect $name"), translated across the supported languages.
  • Cowork (Step 6 review): added a direct connectListener test for acceptance criterion 1 (connecting a 2nd dApp must not drop the 1st) — asserts the existing listener is reused (disconnect not called, WalletConnectClient.init not re-invoked, pair + attachHandlers run on it).

🤖 Generated with Claude Code — Opus 4.8 (claude-opus-4-8)

sbpublic and others added 6 commits June 20, 2026 10:33
Add disconnectSession(topic) to the WalletConnectListener interface and
WalletConnectClient, scoped to a single topic, alongside the existing
all-sessions disconnect().

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The persistent listener wraps a singleton WalletKit, so its reference
no longer changes when a session is added or removed. Add
walletConnectSessionsStore as the reactive source of truth for the
connected-apps UI.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…WalletConnect session

Reuse the existing listener when adding a connection instead of tearing
it down (cold-start init now uses cleanSlate: false). Add syncSessions(),
a per-session disconnectSession(topic) service that falls through to a
full teardown when the last app is removed, and resetListenerIfNoSessions()
which detaches handlers and resets only when no sessions remain. Apply the
guard to session_delete and to the proposal reject/cancel and pair-failure
paths so they no longer drop sibling connections.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…sessions modal

The connected-apps list now reads the reactive sessions store. Each row's
close button disconnects only that session by topic; a new "Disconnect all"
toolbar button (i18n key wallet_connect.text.disconnect_all) tears every
connection down at once.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Update PRODUCT.md to describe multiple independently-managed dApp
connections and add the spec to the spec-driven-development specs folder.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…orted languages

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the frontend WalletConnect integration to support multiple simultaneous dApp sessions, aligning the app-layer behavior with Reown WalletKit’s native multi-session capability. It introduces per-topic session teardown, makes the “Connected Apps” UI reactive via a dedicated sessions store, and adjusts lifecycle handling so that adding/removing one session doesn’t drop siblings.

Changes:

  • Add disconnectSession(topic) to the listener/provider and introduce service helpers (syncSessions, resetListenerIfNoSessions) to keep a stable listener while sessions come and go.
  • Add walletConnectSessionsStore as the reactive source of truth for the connected-apps UI; update modal/session/review flows to keep it in sync.
  • Add “Disconnect all” UI + i18n key (wallet_connect.text.disconnect_all) and extend unit tests for the new multi-session behavior.

Reviewed changes

Copilot reviewed 30 out of 31 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/frontend/src/lib/services/wallet-connect.services.ts Reuse existing listener on add; add session sync/reset helpers and per-topic disconnect service.
src/frontend/src/lib/providers/wallet-connect.providers.ts Implement per-topic disconnect via WalletKit disconnectSession.
src/frontend/src/lib/stores/wallet-connect.store.ts Add walletConnectSessionsStore to drive reactive connected-apps UI.
src/frontend/src/lib/types/wallet-connect.ts Extend listener contract with disconnectSession(topic).
src/frontend/src/lib/components/wallet-connect/WalletConnectSessionsModal.svelte Render sessions from sessions store; add per-row disconnect + “Disconnect all” button.
src/frontend/src/lib/components/wallet-connect/WalletConnectSession.svelte Seed sessions store after reconnect; keep listener alive unless last session ends.
src/frontend/src/lib/components/wallet-connect/WalletConnectReview.svelte Guard reset paths to avoid dropping sibling sessions; sync sessions after approve/remove.
src/frontend/src/lib/types/i18n.d.ts Add wallet_connect.text.disconnect_all typing.
src/frontend/src/lib/i18n/en.json Add wallet_connect.text.disconnect_all string.
src/frontend/src/lib/i18n/ar.json Add wallet_connect.text.disconnect_all string.
src/frontend/src/lib/i18n/cs.json Add wallet_connect.text.disconnect_all string.
src/frontend/src/lib/i18n/de.json Add wallet_connect.text.disconnect_all string.
src/frontend/src/lib/i18n/es.json Add wallet_connect.text.disconnect_all string.
src/frontend/src/lib/i18n/fr.json Add wallet_connect.text.disconnect_all string.
src/frontend/src/lib/i18n/hi.json Add wallet_connect.text.disconnect_all string.
src/frontend/src/lib/i18n/it.json Add wallet_connect.text.disconnect_all string.
src/frontend/src/lib/i18n/ja.json Add wallet_connect.text.disconnect_all string.
src/frontend/src/lib/i18n/ko-KR.json Add wallet_connect.text.disconnect_all string.
src/frontend/src/lib/i18n/pl.json Add wallet_connect.text.disconnect_all string.
src/frontend/src/lib/i18n/pt.json Add wallet_connect.text.disconnect_all string.
src/frontend/src/lib/i18n/ru.json Add wallet_connect.text.disconnect_all string.
src/frontend/src/lib/i18n/vi.json Add wallet_connect.text.disconnect_all string.
src/frontend/src/lib/i18n/zh-CN.json Add wallet_connect.text.disconnect_all string.
src/frontend/src/tests/lib/services/wallet-connect.services.spec.ts Add coverage for syncSessions, guarded reset, and per-topic disconnect teardown behavior.
src/frontend/src/tests/lib/stores/wallet-connect.store.spec.ts Add coverage for sessions store set/reset behavior.
src/frontend/src/tests/lib/providers/wallet-connect.providers.spec.ts Add coverage for provider per-topic disconnect.
src/frontend/src/tests/lib/components/wallet-connect/WalletConnectSessionsModal.spec.ts Add coverage for per-row disconnect by topic, “Disconnect all”, and reactive list updates.
src/frontend/src/tests/btc/services/wallet-connect.services.spec.ts Update listener mock to include disconnectSession.
src/frontend/src/tests/sol/services/wallet-connect.services.spec.ts Update listener mock to include disconnectSession.
docs/ai/spec-driven-development/specs/2026-06-20-impr-multiple-walletconnect-connections.md Add spec documenting the multi-session design + acceptance criteria.
docs/ai/PRODUCT.md Document multi-connection WalletConnect behavior in product notes.

sbpublic and others added 2 commits June 22, 2026 08:44
disconnectSession swallows its own errors, so disconnectOne showed the
"disconnected" success toast even when the disconnect failed. Return
ResultSuccess from the service and toast only when success is true.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…cessible name

The icon-only per-row disconnect button had no accessible name. Add a
localized ariaLabel via a new i18n key wallet_connect.text.disconnect_app
("Disconnect $name"), resolved per row with the dApp name, translated
across supported languages.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@sbpublic

Copy link
Copy Markdown
Collaborator Author

Independent spec review (Step 6 — Cowork)

I authored the spec but not the code, so this is the independent review pass. I read the full diff at commit 7fa3b05 (I did not run the app or CI — the format/lint/check/test gates remain CI's backstop). Overall this looks ready to merge: it faithfully implements all six in-scope acceptance criteria and the three resolved decisions, and gets the subtle correctness points right.

Verified against the spec. The three choke points are fixed as designed — initListener reuses the existing listener instead of tearing it down (criterion 1); per-topic disconnectSession on the provider + service with fall-through to full teardown when the last app goes (criteria 2, 4); resetListenerIfNoSessions replaces the blanket resetListener on session_delete (criterion 3). The reactive walletConnectSessionsStore + syncSessions() drives the list and re-syncs onMount. "Disconnect all" is shown only when at least one session exists, with accessible names and testIds. PRODUCT.md is updated (including the one-request-at-a-time caveat), and walletConnectPaired is kept as "listener present". Nice catch beyond the spec: the proposal reject/cancel and failed-pairing paths now also route through resetListenerIfNoSessions, so an aborted second connection no longer drops the first.

The two spec open questions resolved cleanly: the add path never re-runs init (so the unconditional clearLocalStorage is moot), and session_delete uses a getActiveSessions() re-query rather than the event topic — correct, since WalletKit removes the session before the callback fires.

Finding — test gap (main actionable item). Acceptance criterion 1, the headline behavior (connecting a 2nd dApp must not drop the 1st), has no direct automated test. connectListener/initListener are not unit-tested (pre-existing), though the helpers they now rely on (syncSessions, resetListenerIfNoSessions, disconnectSession) are well covered. Suggest a describe('connectListener', ...) block in src/frontend/src/tests/lib/services/wallet-connect.services.spec.ts — driving through the exported connectListener since initListener is private — that pre-seeds walletConnectListenerStore with a mock listener and asserts: the existing listener's disconnect is NOT called, WalletConnectClient.init is NOT re-invoked, and pair(uri) + attachHandlers run on the existing listener. Plus the manual two-dApp E2E already in spec section 8.

Minor / low (pre-existing). onSessionDeleteCallback (goToScanStep) stays registered on the singleton WalletKit after the scanner modal closes and now fires on every per-session delete; it is guarded by isNullish(modal) so it is harmless — just flagging it since multi-session makes it fire more often.

Confirm in UI QA. In a running build (and in dark theme): closing one row updates the list in place without reopening the modal, and a dApp-side disconnect updates the open list. The store wiring looks correct; this is the "correct in the diff still needs a human in the running app" check.

@sbpublic

Copy link
Copy Markdown
Collaborator Author

Good catch — adding direct connectListener coverage for criterion 1.

Test gap: agreed — the headline "adding a 2nd dApp keeps the 1st connected" path had no direct test, only the helpers it composes. Adding a describe('connectListener', …) to wallet-connect.services.spec.ts that pre-seeds walletConnectListenerStore with a mock listener and asserts the existing listener's disconnect is not called, WalletConnectClient.init is not re-invoked, and pair(uri) + attachHandlers run on the existing listener. The two-dApp manual E2E stays per spec §8.

onSessionDeleteCallback (minor): confirmed pre-existing and isNullish(modal)-guarded, so it's a no-op once the scanner modal closes — leaving as-is and noting it as a possible fast-follow rather than widening this PR's scope.

UI QA: over to you for the running-build / dark-theme check (in-place row removal on per-row close, and a dApp-side disconnect updating the open list).

🤖 Claude Opus 4.8

… listener

Add a connectListener test for acceptance criterion 1 (connecting a new
dApp must not drop already-connected dApps): pre-seed the listener store
and assert the existing listener's disconnect is not called,
WalletConnectClient.init is not re-invoked, and pair(uri) + attachHandlers
run on the existing listener.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@sbpublic sbpublic marked this pull request as ready for review June 22, 2026 07:22
@sbpublic sbpublic requested a review from a team as a code owner June 22, 2026 07:22
@zeropath-ai

zeropath-ai Bot commented Jun 22, 2026

Copy link
Copy Markdown

No security or compliance issues detected. Reviewed everything up to 31a1518.

Security Overview
Detected Code Changes
Change Type Relevant files
Enhancement ► docs/ai/PRODUCT.md
    Update WalletConnect description to mention multiple simultaneous connections.
► docs/ai/spec-driven-development/specs/2026-06-20-impr-multiple-walletconnect-connections.md
    Add new spec file for allowing multiple simultaneous WalletConnect connections.
► src/frontend/src/lib/components/wallet-connect/WalletConnectReview.svelte
    Modify resetListener call to resetListenerIfNoSessions.
    Add syncSessions call after callback execution.
► src/frontend/src/lib/components/wallet-connect/WalletConnectSession.svelte
    Modify resetListener call to resetListenerIfNoSessions.
    Add syncSessions call after session restoration.
► src/frontend/src/lib/components/wallet-connect/WalletConnectSessionsModal.svelte
    Update to use walletConnectSessionsStore for sessions.
    Add onMount call to syncSessions.
    Implement disconnectOne function for individual session disconnection.
    Implement disconnectAll function for disconnecting all sessions.
    Add accessible names for disconnect buttons.
    Show disconnect all button only when sessions exist.
► src/frontend/src/lib/i18n/ar.json
    Add translations for "Disconnect all" and "Disconnect app".
► src/frontend/src/lib/i18n/cs.json
    Add translations for "Disconnect all" and "Disconnect app".
► src/frontend/src/lib/i18n/de.json
    Add translations for "Disconnect all" and "Disconnect app".
► src/frontend/src/lib/i18n/en.json
    Add translations for "Disconnect all" and "Disconnect app".
► src/frontend/src/lib/i18n/es.json
    Add translations for "Disconnect all" and "Disconnect app".
► src/frontend/src/lib/i18n/fr.json
    Add translations for "Disconnect all" and "Disconnect app".
► src/frontend/src/lib/i18n/hi.json
    Add translations for "Disconnect all" and "Disconnect app".
► src/frontend/src/lib/i18n/it.json
    Add translations for "Disconnect all" and "Disconnect app".
► src/frontend/src/lib/i18n/ja.json
    Add translations for "Disconnect all" and "Disconnect app".
► src/frontend/src/lib/i18n/ko-KR.json
    Add translations for "Disconnect all" and "Disconnect app".
► src/frontend/src/lib/i18n/pl.json
    Add translations for "Disconnect all" and "Disconnect app".
► src/frontend/src/lib/i18n/pt.json
    Add translations for "Disconnect all" and "Disconnect app".
► src/frontend/src/lib/i18n/ru.json
    Add translations for "Disconnect all" and "Disconnect app".
► src/frontend/src/lib/i18n/vi.json
    Add translations for "Disconnect all" and "Disconnect app".
► src/frontend/src/lib/i18n/zh-CN.json
    Add translations for "Disconnect all" and "Disconnect app".
► src/frontend/src/lib/providers/wallet-connect.providers.ts
    Add disconnectSession method to WalletConnectClient.
► src/frontend/src/lib/services/wallet-connect.services.ts
    Implement syncSessions to update walletConnectSessionsStore.
    Implement resetListenerIfNoSessions to conditionally reset listener.
    Implement disconnectSession service function.
    Modify initListener to reuse existing listener.
    Modify connectListener to use resetListenerIfNoSessions and syncSessions.
► src/frontend/src/lib/stores/wallet-connect.store.ts
    Add walletConnectSessionsStore for managing active sessions.
► src/frontend/src/lib/types/i18n.d.ts
    Add type definitions for new i18n keys.
► src/frontend/src/lib/types/wallet-connect.ts
    Add abstract method disconnectSession to WalletConnectListener.
► src/frontend/src/tests/lib/components/wallet-connect/WalletConnectSessionsModal.spec.ts
    Add tests for per-session disconnect functionality.
    Add tests for accessible names of disconnect buttons.
    Add tests for disconnect all functionality.
Refactor ► src/frontend/src/lib/components/wallet-connect/WalletConnectReview.svelte
    Replace resetListener with resetListenerIfNoSessions.
► src/frontend/src/lib/services/wallet-connect.services.ts
    Refactor initListener to reuse existing listener.
    Refactor connectListener to handle potential errors more gracefully.
Other ► src/frontend/src/lib/components/wallet-connect/WalletConnectSessionsModal.svelte
    Add aria-label to disconnect button.
► src/frontend/src/tests/btc/services/wallet-connect.services.spec.ts
    Add mock for disconnectSession method.
► src/frontend/src/tests/lib/components/wallet-connect/WalletConnectSessionsModal.spec.ts
    Mock walletConnectServices.disconnectSession and toastsStore.toastsShow.

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.

2 participants