Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
75 changes: 51 additions & 24 deletions frontend/src/components/WalletPickerModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -42,29 +46,52 @@ export const WalletPickerModal = observer(() => {
mobile or desktop.
</p>
) : (
wallets.map((w) => (
<button
key={w.uuid}
onClick={() => void poolStore.connectWallet(w.uuid)}
className="cursor-pointer flex w-full items-center gap-3 rounded-md border border-border bg-muted/30 px-3 py-3 text-left transition-colors hover:border-primary/40 hover:bg-primary/10"
>
{w.icon ? (
<img
src={w.icon}
alt=""
className="h-8 w-8 shrink-0 rounded-md"
/>
) : (
<span className="h-8 w-8 shrink-0 rounded-md bg-muted" />
)}
<span className="flex min-w-0 flex-1 flex-col">
<span className="truncate font-medium">{w.name}</span>
<span className="font-data truncate text-xs text-identity-accent">
{w.rdns}
wallets.map((w) => {
// The merged MyQRLWallet row offers relay pairing as a second,
// separately focusable button when the extension holds the
// primary click.
const relayUuid = w.kind === "myqrlwallet" ? w.secondaryUuid : null;
const relayLabel = w.kind === "myqrlwallet" ? w.secondaryLabel : null;
return (
<div key={w.uuid} className="space-y-1">
<button
onClick={() => void poolStore.connectWallet(w.uuid)}
className="cursor-pointer flex w-full items-center gap-3 rounded-md border border-border bg-muted/30 px-3 py-3 text-left transition-colors hover:border-primary/40 hover:bg-primary/10"
>
{w.icon ? (
<img
src={w.icon}
alt=""
className="h-8 w-8 shrink-0 rounded-md"
/>
) : (
<span className="h-8 w-8 shrink-0 rounded-md bg-muted" />
)}
<span className="flex min-w-0 flex-1 flex-col">
<span className="truncate font-medium">{w.name}</span>
{w.kind === "myqrlwallet" ? (
<span className="truncate text-xs text-muted-foreground">
{w.primaryLabel}
</span>
) : (
<span className="font-data truncate text-xs text-identity-accent">
{w.rdns}
</span>
)}
</span>
</span>
</button>
))
</button>
{relayUuid && relayLabel ? (
<button
onClick={() => void poolStore.connectWallet(relayUuid)}
aria-label={`${relayLabel} to connect ${w.name}`}
className="cursor-pointer rounded-sm pl-11 text-left text-xs text-primary underline underline-offset-4 hover:text-primary/80"
>
{relayLabel}
</button>
) : null}
</div>
);
})
)}
</CardContent>
</Card>
Expand Down
68 changes: 53 additions & 15 deletions frontend/src/stores/poolStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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";

Expand All @@ -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";

Expand Down Expand Up @@ -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,
},
);
});
};
Expand Down
Loading