From 9423d2f43ae5353497cb0a19f6af22ff77745348 Mon Sep 17 00:00:00 2001 From: moscowchill Date: Fri, 25 Sep 2026 10:18:53 +0200 Subject: [PATCH 1/2] feat(wallet-picker): show MyQRLWallet as a single row MyQRLWallet announces itself twice over EIP-6963: com.qrlwallet.extension from the browser extension and com.qrlwallet.connect from the connect SDK, which covers the phone, web and desktop wallets. The picker listed both, so the user had to know which of two MyQRLWallet rows matched the wallet they actually have. groupMyQrlWallet() from @qrlwallet/connect-ui folds the pair into one row named MyQRLWallet, carrying the 3A mark. Clicking the row connects through the extension when it is installed, and a separate "Use phone or desktop app" button under it starts relay pairing. Without the extension the row itself starts relay pairing and there is no second button. The rdns line is replaced on that row by plain text naming the path a click takes. Every other announced wallet keeps its own row, its own icon and its own rdns line. The store's connect path is unchanged: both buttons hand a uuid to connectWallet(), which still resolves the transport from the rdns. The store imports the dependency-free @qrlwallet/connect-ui/wallets subpath because the store tests load this module in plain Node, where the package index would pull in the DOM pairing modal. Gates: npm run lint (zero warnings), npm run typecheck, npm test (42 pass), npm run build. Validated locally against a packed connect-ui 0.3.0. Claude-Session: https://claude.ai/code/session_01LWKZ1b4FBetebjo1kcNHJ8 --- frontend/package.json | 2 +- frontend/src/components/WalletPickerModal.tsx | 75 +++++++++++++------ frontend/src/stores/poolStore.ts | 68 +++++++++++++---- 3 files changed, 105 insertions(+), 40 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 0848d72..ca0c306 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -21,7 +21,7 @@ "@fontsource-variable/sora": "^5.2.8", "@noble/hashes": "^2.4.0", "@qrlwallet/connect": "4.0.0", - "@qrlwallet/connect-ui": "0.2.3", + "@qrlwallet/connect-ui": "0.3.0", "@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-tabs": "^1.1.1", "@theqrl/web3": "^1.0.3", diff --git a/frontend/src/components/WalletPickerModal.tsx b/frontend/src/components/WalletPickerModal.tsx index 359390f..16a0663 100644 --- a/frontend/src/components/WalletPickerModal.tsx +++ b/frontend/src/components/WalletPickerModal.tsx @@ -5,8 +5,12 @@ import { useStore } from "@/stores/store"; /** * EIP-6963 wallet picker. Lists the QRL-capable wallets the store discovered - * (the QRL browser extension and MyQRLWallet via the connect relay) and hands - * a click back to the store to run the right connect path. + * and hands a click back to the store to run the right connect path. + * + * MyQRLWallet announces once per transport (browser extension and connect + * relay), so the store folds the pair into a single row. Clicking that row + * uses the extension when it is installed, and the "Use phone or desktop app" + * button under it always starts relay pairing. */ export const WalletPickerModal = observer(() => { const { poolStore } = useStore(); @@ -42,29 +46,52 @@ export const WalletPickerModal = observer(() => { mobile or desktop.

) : ( - wallets.map((w) => ( - - )) + + {relayUuid && relayLabel ? ( + + ) : null} + + ); + }) )} diff --git a/frontend/src/stores/poolStore.ts b/frontend/src/stores/poolStore.ts index 47f1dd4..f45e657 100644 --- a/frontend/src/stores/poolStore.ts +++ b/frontend/src/stores/poolStore.ts @@ -5,6 +5,9 @@ import { attemptWalletRedirect, getAppStoreUrl, } from "@qrlwallet/connect"; +// The dependency-free subpath: this module is loaded in plain Node by the +// store tests, and the package index pulls in the DOM pairing modal. +import { groupMyQrlWallet } from "@qrlwallet/connect-ui/wallets"; import type { ContractAbi } from "@theqrl/web3"; import { NativeQrlPoolABI } from "@/abi/NativeQrlPool"; import { ACTIVE_NETWORK, type NetworkConfig } from "@/config/networks"; @@ -37,9 +40,9 @@ const QRL_EXTENSION_RDNS = new Set(["theqrl.org", "com.qrlwallet.extension"]); const QRL_CONNECT_RDNS = QRL_CONNECT_PROVIDER_INFO.rdns; /** - * MyQRLWallet 3A mark, shown for the relay entry in the wallet picker - * instead of the SDK's own baked EIP-6963 icon, so the picker carries the - * current wallet branding without a coordinated SDK release. + * MyQRLWallet 3A mark, shown on the merged MyQRLWallet row in the wallet + * picker in place of the icon either transport announces, so the picker + * carries the current wallet branding without a coordinated SDK release. */ const MYQRLWALLET_ICON = "/myqrlwallet-icon.svg"; @@ -49,15 +52,36 @@ interface EIP6963Detail { provider: ExtensionProvider; } -/** A wallet shown in the picker. `kind` drives the connect path. */ -export interface DiscoveredWallet { +/** + * The single MyQRLWallet row. MyQRLWallet announces twice over EIP-6963 (the + * browser extension and the connect SDK's relay entry); groupMyQrlWallet + * folds the pair into one row. `uuid` is the announcement a click on the row + * body uses, `secondaryUuid` the one the extra action uses. + */ +export interface MyQrlWalletRow { + kind: "myqrlwallet"; + uuid: string; + name: string; + icon: string; + /** Plain secondary line: "Browser extension" or "Phone, web or desktop". */ + primaryLabel: string; + /** Relay announcement for the extra action; null when the row has one path. */ + secondaryUuid: string | null; + secondaryLabel: string | null; +} + +/** Any other QRL-capable wallet, listed on its own. */ +export interface OtherWalletRow { + kind: "wallet"; uuid: string; name: string; icon: string; rdns: string; - kind: "extension" | "relay"; } +/** A row shown in the picker. */ +export type DiscoveredWallet = MyQrlWalletRow | OtherWalletRow; + /** Which transport the active provider uses (drives the tx param shape). */ type ProviderKind = "extension" | "relay"; @@ -416,15 +440,29 @@ export class PoolStore { if (this.discoveredMap.has(info.uuid)) return; this.discoveredMap.set(info.uuid, detail); runInAction(() => { - this.discoveredWallets = Array.from(this.discoveredMap.values()).map( - (d) => ({ - uuid: d.info.uuid, - name: d.info.name, - icon: - d.info.rdns === QRL_CONNECT_RDNS ? MYQRLWALLET_ICON : d.info.icon, - rdns: d.info.rdns, - kind: d.info.rdns === QRL_CONNECT_RDNS ? "relay" : "extension", - }), + this.discoveredWallets = groupMyQrlWallet(this.discoveredMap.values(), { + icon: MYQRLWALLET_ICON, + }).map((entry): DiscoveredWallet => + entry.kind === "myqrlwallet" + ? { + kind: "myqrlwallet", + uuid: entry.uuid, + name: entry.name, + icon: entry.icon, + primaryLabel: entry.primaryLabel, + secondaryUuid: + entry.secondary === "relay" + ? (entry.relay?.info.uuid ?? null) + : null, + secondaryLabel: entry.secondaryLabel, + } + : { + kind: "wallet", + uuid: entry.uuid, + name: entry.name, + icon: entry.icon, + rdns: entry.rdns, + }, ); }); }; From b71d17aba33a566a72a4bbed38906417e39768f7 Mon Sep 17 00:00:00 2001 From: moscowchill Date: Fri, 25 Sep 2026 10:29:32 +0200 Subject: [PATCH 2/2] chore(deps): lock @qrlwallet/connect-ui 0.3.0 from the registry Claude-Session: https://claude.ai/code/session_01LWKZ1b4FBetebjo1kcNHJ8 --- frontend/package-lock.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 887e174..c22451e 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -14,7 +14,7 @@ "@fontsource-variable/sora": "^5.2.8", "@noble/hashes": "^2.4.0", "@qrlwallet/connect": "4.0.0", - "@qrlwallet/connect-ui": "0.2.3", + "@qrlwallet/connect-ui": "0.3.0", "@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-tabs": "^1.1.1", "@theqrl/web3": "^1.0.3", @@ -1017,9 +1017,9 @@ } }, "node_modules/@qrlwallet/connect-ui": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@qrlwallet/connect-ui/-/connect-ui-0.2.3.tgz", - "integrity": "sha512-/qL9mZNVkUzX2ka9VJ3EKlQpNFsqrOyEnmEPMDnZEzyE3bullHQFWagYcm7ABM39TiSm3dCJiUM03pYRAriCyA==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@qrlwallet/connect-ui/-/connect-ui-0.3.0.tgz", + "integrity": "sha512-kJdhjcRUyzfrMWAvMm4n6eVchyJ+VY8pqyqgIXlGEMfcAaTJpHIseIf/x/8axB0W0i+EFHA9HFySDFvjyFo3+A==", "license": "MIT", "dependencies": { "qrcode": "^1.5.4"