Skip to content

Commit e20f68f

Browse files
Improve token info discovery for x402 payments
1 parent e2931df commit e20f68f

File tree

14 files changed

+711
-270
lines changed

14 files changed

+711
-270
lines changed

.changeset/dirty-experts-kiss.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"thirdweb": patch
3+
---
4+
5+
Improve token info discovery for x402 payments
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
"use client";
2+
3+
import type React from "react";
4+
import { useId, useState } from "react";
5+
import { defineChain } from "thirdweb/chains";
6+
import { BridgeNetworkSelector } from "@/components/blocks/NetworkSelectors";
7+
import { Input } from "@/components/ui/input";
8+
import { Label } from "@/components/ui/label";
9+
import { TokenSelector } from "@/components/ui/TokenSelector";
10+
import { THIRDWEB_CLIENT } from "@/lib/client";
11+
import type { TokenMetadata } from "@/lib/types";
12+
import type { X402PlaygroundOptions } from "./types";
13+
14+
export function X402LeftSection(props: {
15+
options: X402PlaygroundOptions;
16+
setOptions: React.Dispatch<React.SetStateAction<X402PlaygroundOptions>>;
17+
}) {
18+
const { options, setOptions } = props;
19+
20+
// Local state for chain and token selection
21+
const [selectedChain, setSelectedChain] = useState<number | undefined>(() => {
22+
return options.chain?.id;
23+
});
24+
25+
const [selectedToken, setSelectedToken] = useState<
26+
{ chainId: number; address: string } | undefined
27+
>(() => {
28+
if (options.tokenAddress && options.chain?.id) {
29+
return {
30+
address: options.tokenAddress,
31+
chainId: options.chain.id,
32+
};
33+
}
34+
return undefined;
35+
});
36+
37+
const chainId = useId();
38+
const tokenId = useId();
39+
const amountId = useId();
40+
const payToId = useId();
41+
42+
const handleChainChange = (chainId: number) => {
43+
setSelectedChain(chainId);
44+
// Clear token selection when chain changes
45+
setSelectedToken(undefined);
46+
47+
setOptions((v) => ({
48+
...v,
49+
chain: defineChain(chainId),
50+
tokenAddress: "0x0000000000000000000000000000000000000000" as const,
51+
tokenSymbol: "",
52+
tokenDecimals: 18,
53+
}));
54+
};
55+
56+
const handleTokenChange = (token: TokenMetadata) => {
57+
setSelectedToken({
58+
address: token.address,
59+
chainId: selectedChain!,
60+
});
61+
62+
setOptions((v) => ({
63+
...v,
64+
tokenAddress: token.address as `0x${string}`,
65+
tokenSymbol: token.symbol ?? "",
66+
tokenDecimals: token.decimals ?? 18,
67+
}));
68+
};
69+
70+
const handleAmountChange = (e: React.ChangeEvent<HTMLInputElement>) => {
71+
setOptions((v) => ({
72+
...v,
73+
amount: e.target.value,
74+
}));
75+
};
76+
77+
const handlePayToChange = (e: React.ChangeEvent<HTMLInputElement>) => {
78+
setOptions((v) => ({
79+
...v,
80+
payTo: e.target.value as `0x${string}`,
81+
}));
82+
};
83+
84+
return (
85+
<div className="space-y-6">
86+
<div>
87+
<h2 className="mb-4 text-xl font-semibold">Configuration</h2>
88+
<div className="space-y-4">
89+
{/* Chain selection */}
90+
<div className="flex flex-col gap-2">
91+
<Label htmlFor={chainId}>Chain</Label>
92+
<BridgeNetworkSelector
93+
chainId={selectedChain}
94+
onChange={handleChainChange}
95+
placeholder="Select a chain"
96+
className="bg-card"
97+
/>
98+
</div>
99+
100+
{/* Token selection - only show if chain is selected */}
101+
{selectedChain && (
102+
<div className="flex flex-col gap-2">
103+
<Label htmlFor={tokenId}>Token</Label>
104+
<TokenSelector
105+
chainId={selectedChain}
106+
client={THIRDWEB_CLIENT}
107+
enabled={true}
108+
onChange={handleTokenChange}
109+
placeholder="Select a token"
110+
selectedToken={selectedToken}
111+
className="bg-card"
112+
/>
113+
</div>
114+
)}
115+
116+
{/* Amount input */}
117+
<div className="flex flex-col gap-2">
118+
<Label htmlFor={amountId}>Amount</Label>
119+
<Input
120+
id={amountId}
121+
type="text"
122+
placeholder="0.01"
123+
value={options.amount}
124+
onChange={handleAmountChange}
125+
className="bg-card"
126+
/>
127+
{options.tokenSymbol && (
128+
<p className="text-sm text-muted-foreground">
129+
Amount in {options.tokenSymbol}
130+
</p>
131+
)}
132+
</div>
133+
134+
{/* Pay To input */}
135+
<div className="flex flex-col gap-2">
136+
<Label htmlFor={payToId}>Pay To Address</Label>
137+
<Input
138+
id={payToId}
139+
type="text"
140+
placeholder="0x..."
141+
value={options.payTo}
142+
onChange={handlePayToChange}
143+
className="bg-card"
144+
/>
145+
<p className="text-sm text-muted-foreground">
146+
The wallet address that will receive the payment
147+
</p>
148+
</div>
149+
</div>
150+
</div>
151+
</div>
152+
);
153+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
"use client";
2+
3+
import React, { useState } from "react";
4+
import { useActiveAccount } from "thirdweb/react";
5+
import { chain, token } from "./constants";
6+
import type { X402PlaygroundOptions } from "./types";
7+
import { X402LeftSection } from "./X402LeftSection";
8+
import { X402RightSection } from "./X402RightSection";
9+
10+
const defaultOptions: X402PlaygroundOptions = {
11+
chain: chain,
12+
tokenAddress: token.address as `0x${string}`,
13+
tokenSymbol: token.symbol,
14+
tokenDecimals: token.decimals,
15+
amount: "0.01",
16+
payTo: "0x0000000000000000000000000000000000000000",
17+
};
18+
19+
export function X402Playground() {
20+
const [options, setOptions] = useState<X402PlaygroundOptions>(defaultOptions);
21+
const activeAccount = useActiveAccount();
22+
23+
// Update payTo address when wallet connects, but only if it's still the default
24+
React.useEffect(() => {
25+
if (
26+
activeAccount?.address &&
27+
options.payTo === "0x0000000000000000000000000000000000000000"
28+
) {
29+
setOptions((prev) => ({
30+
...prev,
31+
payTo: activeAccount.address as `0x${string}`,
32+
}));
33+
}
34+
}, [activeAccount?.address, options.payTo]);
35+
36+
return (
37+
<div className="relative flex flex-col-reverse gap-6 xl:min-h-[900px] xl:flex-row xl:gap-6">
38+
<div className="grow border-b pb-10 xl:mb-0 xl:border-r xl:border-b-0 xl:pr-6">
39+
<X402LeftSection options={options} setOptions={setOptions} />
40+
</div>
41+
<X402RightSection options={options} />
42+
</div>
43+
);
44+
}

0 commit comments

Comments
 (0)