Skip to content

Commit c00581e

Browse files
committed
[MNY-190] Playground: Add SwapWidget
1 parent 4482692 commit c00581e

File tree

17 files changed

+788
-42
lines changed

17 files changed

+788
-42
lines changed

.changeset/honest-hands-clap.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+
Add `persistTokenSelections` prop on `SwapWidget` to allow disable token selection persistance to local storage
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { lazy, Suspense } from "react";
2+
import { LoadingDots } from "@/components/ui/LoadingDots";
3+
import type { SwapWidgetPlaygroundOptions } from "./types";
4+
5+
const CodeClient = lazy(() =>
6+
import("../../../../components/code/code.client").then((m) => ({
7+
default: m.CodeClient,
8+
})),
9+
);
10+
11+
function CodeLoading() {
12+
return (
13+
<div className="flex min-h-[300px] grow items-center justify-center bg-card border rounded-lg">
14+
<LoadingDots />
15+
</div>
16+
);
17+
}
18+
19+
export function CodeGen(props: { options: SwapWidgetPlaygroundOptions }) {
20+
return (
21+
<div className="flex w-full grow flex-col">
22+
<Suspense fallback={<CodeLoading />}>
23+
<CodeClient className="grow" code={getCode(props.options)} lang="ts" />
24+
</Suspense>
25+
</div>
26+
);
27+
}
28+
29+
function getCode(options: SwapWidgetPlaygroundOptions) {
30+
const imports = {
31+
react: [] as string[],
32+
};
33+
34+
let themeProp: string | undefined;
35+
if (
36+
options.theme.type === "dark" &&
37+
Object.keys(options.theme.darkColorOverrides || {}).length > 0
38+
) {
39+
themeProp = `darkTheme({
40+
colors: ${JSON.stringify(options.theme.darkColorOverrides)},
41+
})`;
42+
imports.react.push("darkTheme");
43+
}
44+
45+
if (options.theme.type === "light") {
46+
if (Object.keys(options.theme.lightColorOverrides || {}).length > 0) {
47+
themeProp = `lightTheme({
48+
colors: ${JSON.stringify(options.theme.lightColorOverrides)},
49+
})`;
50+
imports.react.push("lightTheme");
51+
} else {
52+
themeProp = quotes("light");
53+
}
54+
}
55+
56+
const props: Record<string, string | undefined | boolean> = {
57+
theme: themeProp,
58+
prefill:
59+
options.prefill?.buyToken || options.prefill?.sellToken
60+
? JSON.stringify(options.prefill, null, 2)
61+
: undefined,
62+
currency:
63+
options.currency !== "USD" && options.currency
64+
? quotes(options.currency)
65+
: undefined,
66+
showThirdwebBranding:
67+
options.showThirdwebBranding === false ? false : undefined,
68+
client: "client",
69+
};
70+
71+
return `\
72+
import { createThirdwebClient } from "thirdweb";
73+
import { SwapWidget } from "thirdweb/react";
74+
75+
const client = createThirdwebClient({
76+
clientId: "....",
77+
});
78+
79+
80+
function Example() {
81+
return (
82+
<SwapWidget
83+
${stringifyProps(props)}
84+
/>
85+
);
86+
}`;
87+
}
88+
89+
function quotes(value: string) {
90+
return `"${value}"`;
91+
}
92+
93+
function stringifyProps(props: Record<string, string | undefined | boolean>) {
94+
const _props: Record<string, string | undefined | boolean> = {};
95+
96+
for (const key in props) {
97+
if (props[key] !== undefined && props[key] !== "") {
98+
_props[key] = props[key];
99+
}
100+
}
101+
102+
return Object.entries(_props)
103+
.map(([key, value]) => `${key}={${value}}`)
104+
.join("\n\t ");
105+
}
Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
"use client";
2+
3+
import { CoinsIcon, PaletteIcon } from "lucide-react";
4+
import type React from "react";
5+
import { useId } from "react";
6+
import { getAddress, NATIVE_TOKEN_ADDRESS } from "thirdweb";
7+
import { CollapsibleSection } from "@/app/wallets/sign-in/components/CollapsibleSection";
8+
import { ColorFormGroup } from "@/app/wallets/sign-in/components/ColorFormGroup";
9+
import { BridgeNetworkSelector } from "@/components/blocks/NetworkSelectors";
10+
import { CustomRadioGroup } from "@/components/ui/CustomRadioGroup";
11+
import { Checkbox } from "@/components/ui/checkbox";
12+
import { Input } from "@/components/ui/input";
13+
import { Label } from "@/components/ui/label";
14+
import {
15+
Select,
16+
SelectContent,
17+
SelectItem,
18+
SelectTrigger,
19+
SelectValue,
20+
} from "@/components/ui/select";
21+
import { TokenSelector } from "@/components/ui/TokenSelector";
22+
import { THIRDWEB_CLIENT } from "@/lib/client";
23+
import type { SwapWidgetPlaygroundOptions } from "./types";
24+
25+
export function LeftSection(props: {
26+
options: SwapWidgetPlaygroundOptions;
27+
setOptions: React.Dispatch<React.SetStateAction<SwapWidgetPlaygroundOptions>>;
28+
lockedWidget?: "buy" | "checkout" | "transaction";
29+
}) {
30+
const { options, setOptions } = props;
31+
const setThemeType = (themeType: "dark" | "light") => {
32+
setOptions((v) => ({
33+
...v,
34+
theme: {
35+
...v.theme,
36+
type: themeType,
37+
},
38+
}));
39+
};
40+
41+
const themeId = useId();
42+
43+
return (
44+
<div className="flex flex-col gap-4">
45+
<CollapsibleSection defaultOpen icon={CoinsIcon} title="Token Selection">
46+
<div className="flex flex-col gap-6 pt-5">
47+
<section className="flex flex-col gap-3">
48+
<Label htmlFor="currency">Display Currency</Label>
49+
<Select
50+
value={options.currency || "USD"}
51+
onValueChange={(value) => {
52+
setOptions((v) => ({
53+
...v,
54+
currency: value as SwapWidgetPlaygroundOptions["currency"],
55+
}));
56+
}}
57+
>
58+
<SelectTrigger className="bg-card">
59+
<SelectValue placeholder="Select currency" />
60+
</SelectTrigger>
61+
<SelectContent>
62+
<SelectItem value="USD">USD - US Dollar</SelectItem>
63+
<SelectItem value="EUR">EUR - Euro</SelectItem>
64+
<SelectItem value="GBP">GBP - British Pound</SelectItem>
65+
<SelectItem value="JPY">JPY - Japanese Yen</SelectItem>
66+
<SelectItem value="KRW">KRW - Korean Won</SelectItem>
67+
<SelectItem value="CNY">CNY - Chinese Yuan</SelectItem>
68+
<SelectItem value="INR">INR - Indian Rupee</SelectItem>
69+
<SelectItem value="NOK">NOK - Norwegian Krone</SelectItem>
70+
<SelectItem value="SEK">SEK - Swedish Krona</SelectItem>
71+
<SelectItem value="CHF">CHF - Swiss Franc</SelectItem>
72+
<SelectItem value="AUD">AUD - Australian Dollar</SelectItem>
73+
<SelectItem value="CAD">CAD - Canadian Dollar</SelectItem>
74+
<SelectItem value="NZD">NZD - New Zealand Dollar</SelectItem>
75+
<SelectItem value="MXN">MXN - Mexican Peso</SelectItem>
76+
<SelectItem value="BRL">BRL - Brazilian Real</SelectItem>
77+
<SelectItem value="CLP">CLP - Chilean Peso</SelectItem>
78+
<SelectItem value="CZK">CZK - Czech Koruna</SelectItem>
79+
<SelectItem value="DKK">DKK - Danish Krone</SelectItem>
80+
<SelectItem value="HKD">HKD - Hong Kong Dollar</SelectItem>
81+
<SelectItem value="HUF">HUF - Hungarian Forint</SelectItem>
82+
<SelectItem value="IDR">IDR - Indonesian Rupiah</SelectItem>
83+
<SelectItem value="ILS">ILS - Israeli Shekel</SelectItem>
84+
<SelectItem value="ISK">ISK - Icelandic Krona</SelectItem>
85+
</SelectContent>
86+
</Select>
87+
</section>
88+
89+
<div className="border-t border-dashed" />
90+
91+
<TokenFieldset
92+
title="Sell Token"
93+
type="sellToken"
94+
options={options}
95+
setOptions={setOptions}
96+
/>
97+
98+
<div className="border-t border-dashed" />
99+
100+
<TokenFieldset
101+
title="Buy Token"
102+
type="buyToken"
103+
options={options}
104+
setOptions={setOptions}
105+
/>
106+
</div>
107+
</CollapsibleSection>
108+
109+
<CollapsibleSection icon={PaletteIcon} title="Appearance">
110+
{/* Theme */}
111+
<section className="flex flex-col gap-3 pt-6">
112+
<Label htmlFor="theme"> Theme </Label>
113+
<CustomRadioGroup
114+
id={themeId}
115+
onValueChange={setThemeType}
116+
options={[
117+
{ label: "Dark", value: "dark" },
118+
{ label: "Light", value: "light" },
119+
]}
120+
value={options.theme.type}
121+
/>
122+
</section>
123+
124+
<div className="h-6" />
125+
126+
{/* Colors */}
127+
<ColorFormGroup
128+
onChange={(newTheme) => {
129+
setOptions((v) => ({
130+
...v,
131+
theme: newTheme,
132+
}));
133+
}}
134+
theme={options.theme}
135+
/>
136+
137+
<div className="my-4 flex items-center gap-2">
138+
<Checkbox
139+
checked={options.showThirdwebBranding}
140+
id={"branding"}
141+
onCheckedChange={(checked) => {
142+
setOptions((v) => ({
143+
...v,
144+
showThirdwebBranding: checked === true,
145+
}));
146+
}}
147+
/>
148+
<Label htmlFor={"branding"}>Show Branding</Label>
149+
</div>
150+
</CollapsibleSection>
151+
</div>
152+
);
153+
}
154+
155+
function TokenFieldset(props: {
156+
type: "buyToken" | "sellToken";
157+
title: string;
158+
options: SwapWidgetPlaygroundOptions;
159+
setOptions: React.Dispatch<React.SetStateAction<SwapWidgetPlaygroundOptions>>;
160+
}) {
161+
const { options, setOptions } = props;
162+
163+
const chainId = options.prefill?.[props.type]?.chainId;
164+
const tokenAddress = options.prefill?.[props.type]?.tokenAddress;
165+
166+
return (
167+
<div>
168+
<h3 className="mb-1 font-medium">{props.title}</h3>
169+
<p className="text-sm text-muted-foreground mb-3">
170+
Sets the default token and amount to{" "}
171+
{props.type === "buyToken" ? "buy" : "sell"} in the widget, <br />
172+
User can change this default selection in the widget
173+
</p>
174+
<div className="space-y-4">
175+
{/* Chain selection */}
176+
<div className="space-y-2">
177+
<Label>Chain</Label>
178+
<BridgeNetworkSelector
179+
chainId={chainId}
180+
onChange={(chainId) => {
181+
setOptions((v) => ({
182+
...v,
183+
prefill: {
184+
...v.prefill,
185+
[props.type]: {
186+
...v.prefill?.[props.type],
187+
chainId,
188+
tokenAddress: undefined, // clear token selection
189+
},
190+
},
191+
}));
192+
}}
193+
placeholder="Select a chain"
194+
className="bg-card"
195+
/>
196+
</div>
197+
198+
{/* Token selection - only show if chain is selected */}
199+
<div className="space-y-2">
200+
<Label>Token</Label>
201+
<TokenSelector
202+
addNativeTokenIfMissing={true}
203+
chainId={chainId}
204+
client={THIRDWEB_CLIENT}
205+
disableAddress
206+
enabled={true}
207+
onChange={(token) => {
208+
setOptions((v) => ({
209+
...v,
210+
prefill: {
211+
...v.prefill,
212+
[props.type]: {
213+
chainId: token.chainId,
214+
tokenAddress: token.address,
215+
},
216+
},
217+
}));
218+
}}
219+
placeholder="Select a token"
220+
selectedToken={
221+
tokenAddress && chainId
222+
? {
223+
address: tokenAddress,
224+
chainId: chainId,
225+
}
226+
: chainId
227+
? {
228+
address: getAddress(NATIVE_TOKEN_ADDRESS),
229+
chainId: chainId,
230+
}
231+
: undefined
232+
}
233+
className="bg-card"
234+
/>
235+
</div>
236+
237+
{chainId && (
238+
<div className="space-y-2">
239+
<Label> Token Amount</Label>
240+
<Input
241+
className="bg-card"
242+
value={options.prefill?.[props.type]?.amount || ""}
243+
onChange={(e) => {
244+
setOptions((v) => {
245+
return {
246+
...v,
247+
prefill: {
248+
...v.prefill,
249+
[props.type]: {
250+
...v.prefill?.[props.type],
251+
amount: e.target.value,
252+
chainId: chainId,
253+
},
254+
},
255+
};
256+
});
257+
}}
258+
placeholder="0.01"
259+
/>
260+
</div>
261+
)}
262+
</div>
263+
</div>
264+
);
265+
}

0 commit comments

Comments
 (0)