Skip to content

feat: default wallet picker to in-page modal - #2379

Closed
singhyash05 wants to merge 2 commits into
canton-network:mainfrom
singhyash05:feat/wallet-picker-modal
Closed

singhyash05 wants to merge 2 commits into
canton-network:mainfrom
singhyash05:feat/wallet-picker-modal

Conversation

@singhyash05

Copy link
Copy Markdown
Contributor

Why

dApps should open the wallet picker as an in-page modal by default. Popup stays available with setWalletPicker(pickWallet). Continues #2237.

What we did

Took the modal from draft shape into something we could ship as the SDK default, then rewired everything that assumed popup.

  • Built the production modal UI (~940 lines) plus styles, and Chromium unit tests (~735 lines) covering open, select, dismiss, error, and retry
  • Refactored dapp-sdk picker plumbing: default is pickWalletModal; popup is opt-in; connected/error/retry/back split by modal vs popup; WalletConnect QR mounts in the modal
  • Reworked Playwright helpers (openWalletPicker, modal host/row locators, WG connect) so e2e drives the new path instead of waiting on a picker popup
  • Wrote walletpicker-modal.spec.ts for open / backdrop+back / live remote connect
  • Rewrote walletpicker.spec.ts for modal error UX (in-modal alert, not popup "Try Again"). Hit a real lifecycle bug: after modal connect the WG login window closes, so WG popup logout no longer clears dApp Status. Switched that step to dApp Disconnect and fixed ping App / useStatus so Status clears on disconnect
  • Aligned extension ping-page.ts with the modal default

Ledger port (2975)

Localnet exposes JSON Ledger API on 2975, not 5003. Updated wallet-gateway/test/config.json and Config.test.ts so bootstrap/connect e2e hits the real localnet port (same as example-config).

Testing (what we actually ran)

  • Clean / build / unit on touched packages (core-wallet-ui-components, dapp-sdk, core-wallet-test-utils, wallet-gateway-remote)
  • Full monorepo: pnpm clean:all → build:all --skip-nx-cache → test:all
  • E2e against live localnet + start:all. First connect runs failed after remote clean wiped sqlite (empty networks). Reseeded DB, confirmed 6 networks, re-ran until green:
CI=1 pnpm nx playwright:e2e @canton-network/example-ping -- \
  tests/walletpicker-modal.spec.ts tests/walletpicker.spec.ts --project=chromium

Result: 4/4 passed.

Test plan

  • Touched-package units + test:all
  • Modal + picker e2e (chromium), 4/4
  • Optional: full ping chromium e2e
  • Manual: default modal; setWalletPicker(pickWallet) restores popup

@singhyash05
singhyash05 requested a review from a team as a code owner August 30, 2026 02:52
@mjuchli-da mjuchli-da self-assigned this Aug 31, 2026

@mjuchli-da mjuchli-da 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.

Hi @singhyash05

Thanks for the PR, the new modal looks very nice!

I have a few high-level things:

  • Could you add both "connect" buttons to the example-ping dapp?
  • The order of the items listed is different from the popup picker. I believe in the popup we have the order: 1. default remotes (defaultAdapters) 2. additional by dapp (additionalAdapters) 3. announced (installed) 4. suggested (not-installed, if enableSuggestedWallets)
  • WalletConnect entry opens a blank tab
  • WalletConnect URL/QR is not showing
  • CI does not pass

We (@alexmatson-da and I) will provide a more detailed review shortly.

@mjuchli-da mjuchli-da linked an issue Sep 1, 2026 that may be closed by this pull request
Comment thread wallet-gateway/test/config.json Outdated
},
"ledgerApi": {
"baseUrl": "http://127.0.0.1:5003"
"baseUrl": "http://127.0.0.1:2975"

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.

I don't see a reason for these ports to change: these networks are configured to use a Canton node started outside of localnet (pnpm start:canton), and has a default ledger API port of 5003.

There is already a WG network defined for localnet instances in the config, that should be the one selected on the login screen (when using pnpm start:localnet)

export const WALLET_PICKER_MODAL_HOST = '[data-swk-wallet-picker-modal]'

export type WalletPickerSurface = {
kind: 'modal' | 'popup'

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.

do we really want to support both modal and popup style pickers? IMO I don't really see a reason to continue to provide the popup as an option, I'd be in favor of going full modal-only

cc @mjuchli-da @joel-da for thoughts

Comment thread sdk/dapp-sdk/src/sdk.ts
Comment on lines +457 to +492
// Race the connection against a "back" click in the picker
// so the user can abort a slow attempt and pick again.
const connectPromise = discovery.connect(targetId)
connectPromise.catch(() => {
// Swallow late rejections if the user already went back.
})
const outcome = await Promise.race([
connectPromise.then(() => 'connected' as const),
waitForWalletPickerModalBack().then(
() => 'back' as const
),
])

if (outcome === 'back') {
// Abort the in-flight attempt and re-await a selection.
try {
await discovery.disconnect()
} catch {
// best-effort teardown of any partial session
}
this.client = null

try {
const retrySelection =
await this.waitForPickerRetry()
connectionAttempts.dispatchEvent(
new CustomEvent<WalletPickerEntry>('attempt', {
detail: retrySelection,
})
)
} catch (retryError) {
cleanup()
reject(retryError)
}
return
}

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.

I wonder if using an AbortController here can help clean up the code and make it more concise/idiomatic. I dont have much first-hand experience with this API, so I leave it to you to see if its worth the gain.

My basic idea/sketch of what it might look like:

const abortController = new AbortController()

// when we create the wallet picker, we give it an optional abortController
export async function pickWalletModal(
    entries: WalletPickerEntry[],
    abortController?: AbortController
): Promise<WalletPickerResult> {
    active?.destroy()
    active = new WalletPickerModalController(entries, abortController)
    return active.awaitSelection()
}

// inside the wallet picker implementation, we call abort when the user clicks the back button
awaitBack(): Promise<void> {
    if (this.backRequested) {
        this.backRequested = false
        
        // Invoke the abort signal here
        this.abortController.abort()
    }
    return new Promise<void>((resolve) => {
        this.backPending = { resolve }
    })
}

// ----------------------
// later, when we call discovery.connect, we pass in the associated *signal* from the same instance (also optional)
discovery.connect(targetId, { signal: abortController.signal })

// inside the implementation, tear down 
async connect(providerId?: ProviderId | undefined, signal?: AbortSignal | undefined): Promise<void> {
    let targetId = providerId
    // ...

    signal.onabort = () => {
      this.disconnect()
      // ... other cleanup
    }
}

may not be totally accurate, but thats the general idea

@alexmatson-da

Copy link
Copy Markdown
Contributor

Took a review over, in addition to Marc's comments I also have a few high-level items after checking out the branch:

  • we lost browser detection for extension links (I see the "Get for chrome" label on Send when using firefox)
  • the modal needs per-dapp persistence (localstorage) of custom WG URLs, that later appear as individual entries in the picker (and are deletable by the user)
  • status is not fully cleared if user clicks "Logout" from inside the WG Remote popup window

Ship the production wallet picker as an in-page modal (SDK default) while keeping the popup path via setWalletPicker(pickWallet). Wire dapp-sdk + WalletConnect QR for the modal, add unit/e2e coverage, adapt ping disconnect status + extension helpers, and point WG test ledger URLs at localnet 2975.

Signed-off-by: singhyash05 <yashsingh5609@gmail.com>
Restore ledger port 5003, match popup list order, fix WC QR
(no blank tab), Firefox install badge, custom WG localStorage,
ping dual connect + status clear, and modal e2e popup race.

Signed-off-by: singhyash05 <yashsingh5609@gmail.com>
@singhyash05
singhyash05 force-pushed the feat/wallet-picker-modal branch from 407272a to c1f9391 Compare September 20, 2026 01:49
@singhyash05

Copy link
Copy Markdown
Contributor Author

Thanks @mjuchli-da @alexmatson-da addressed the review feedback on c1f9391 (rebased on latest main).

Marc

  • Both connect buttons on example-ping: connect to Wallet (modal default) + connect (popup) via setWalletPicker(pickWallet)
  • List order matches popup / discovery: defaultAdapters → additionalAdapters → announced → saved custom remotes → Remote Wallet → suggested
  • WalletConnect blank tab: openPopupForUri defaults to false
  • WC URL/QR in modal: adapter calls setWalletPickerModalWalletConnectUri
  • CI / e2e: local picker e2e green with pnpm start:canton (ledger :5003) — 4/4 on walletpicker-modal + walletpicker chromium

Alex

  • Ports: reverted non-LocalNet networks to 5003; LocalNet stays 2975 (as discussed)
  • Firefox badge: only label matching platform; otherwise Get (no “Get for chrome” on Firefox)
  • Custom WG localStorage: persist + deletable recent entries (unit coverage)
  • Status clear: ping useStatus / useConnect clear Status on disconnect / statusChanged (WG logout path). Note: after modal connect the login popup closes, so e2e uses dApp Disconnect; please re-check WG Logout via Open wallet if you want that exact path
  • Modal-only: kept popup as opt-in for now (matches Marc’s dual-button request). Happy to drop popup in a follow-up if you + @joel-da agree
  • AbortController: left as follow-up — current Promise.race + back waiter works; can refactor if you want that cleanup in this PR

Happy to take another pass once CI reports.

@mateuszpiatkowski-da

Copy link
Copy Markdown
Contributor

Thanks for taking the time to contribute this PR.

After reviewing the changes, we believe the submission falls under the cases described in our AI Policy concerning insufficiently reviewed AI-generated output. In particular, the size and structure of the changes suggest that substantial portions were generated with AI without the level of self-review we require before submitting a PR.
For that reason, we’ll be closing this PR.

Please feel free to reopen once the a self-review was conducted and code quality has been improved.

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.

Review and improve wallet-picker

4 participants