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" 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, + }, ); }); };