Skip to content

Commit e11ace4

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

File tree

21 files changed

+722
-684
lines changed

21 files changed

+722
-684
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 disabling token selection persistence 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: ["SwapWidget"] 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 { ${imports.react.join(", ")} } 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: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
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+
15+
import { TokenSelector } from "@/components/ui/TokenSelector";
16+
import { THIRDWEB_CLIENT } from "@/lib/client";
17+
import { CurrencySelector } from "../../../../components/blocks/CurrencySelector";
18+
import type { SwapWidgetPlaygroundOptions } from "./types";
19+
20+
export function LeftSection(props: {
21+
options: SwapWidgetPlaygroundOptions;
22+
setOptions: React.Dispatch<React.SetStateAction<SwapWidgetPlaygroundOptions>>;
23+
}) {
24+
const { options, setOptions } = props;
25+
const setThemeType = (themeType: "dark" | "light") => {
26+
setOptions((v) => ({
27+
...v,
28+
theme: {
29+
...v.theme,
30+
type: themeType,
31+
},
32+
}));
33+
};
34+
35+
const themeId = useId();
36+
37+
return (
38+
<div className="flex flex-col gap-4">
39+
<CollapsibleSection defaultOpen icon={CoinsIcon} title="Token Selection">
40+
<div className="flex flex-col gap-6 pt-5">
41+
<section className="flex flex-col gap-3">
42+
<Label htmlFor="currency">Display Currency</Label>
43+
<CurrencySelector
44+
value={options.currency}
45+
onChange={(currency) => {
46+
setOptions((v) => ({ ...v, currency }));
47+
}}
48+
/>
49+
</section>
50+
51+
<div className="border-t border-dashed" />
52+
53+
<TokenFieldset
54+
title="Sell Token"
55+
type="sellToken"
56+
options={options}
57+
setOptions={setOptions}
58+
/>
59+
60+
<div className="border-t border-dashed" />
61+
62+
<TokenFieldset
63+
title="Buy Token"
64+
type="buyToken"
65+
options={options}
66+
setOptions={setOptions}
67+
/>
68+
</div>
69+
</CollapsibleSection>
70+
71+
<CollapsibleSection icon={PaletteIcon} title="Appearance">
72+
{/* Theme */}
73+
<section className="flex flex-col gap-3 pt-6">
74+
<Label htmlFor="theme"> Theme </Label>
75+
<CustomRadioGroup
76+
id={themeId}
77+
onValueChange={setThemeType}
78+
options={[
79+
{ label: "Dark", value: "dark" },
80+
{ label: "Light", value: "light" },
81+
]}
82+
value={options.theme.type}
83+
/>
84+
</section>
85+
86+
<div className="h-6" />
87+
88+
{/* Colors */}
89+
<ColorFormGroup
90+
onChange={(newTheme) => {
91+
setOptions((v) => ({
92+
...v,
93+
theme: newTheme,
94+
}));
95+
}}
96+
theme={options.theme}
97+
/>
98+
99+
<div className="my-4 flex items-center gap-2">
100+
<Checkbox
101+
checked={options.showThirdwebBranding}
102+
id={"branding"}
103+
onCheckedChange={(checked) => {
104+
setOptions((v) => ({
105+
...v,
106+
showThirdwebBranding: checked === true,
107+
}));
108+
}}
109+
/>
110+
<Label htmlFor={"branding"}>Show Branding</Label>
111+
</div>
112+
</CollapsibleSection>
113+
</div>
114+
);
115+
}
116+
117+
function TokenFieldset(props: {
118+
type: "buyToken" | "sellToken";
119+
title: string;
120+
options: SwapWidgetPlaygroundOptions;
121+
setOptions: React.Dispatch<React.SetStateAction<SwapWidgetPlaygroundOptions>>;
122+
}) {
123+
const { options, setOptions } = props;
124+
125+
const chainId = options.prefill?.[props.type]?.chainId;
126+
const tokenAddress = options.prefill?.[props.type]?.tokenAddress;
127+
128+
return (
129+
<div>
130+
<h3 className="mb-1 font-medium">{props.title}</h3>
131+
<p className="text-sm text-muted-foreground mb-3">
132+
Sets the default token and amount to{" "}
133+
{props.type === "buyToken" ? "buy" : "sell"} in the widget, <br />
134+
User can change this default selection in the widget
135+
</p>
136+
<div className="space-y-4">
137+
{/* Chain selection */}
138+
<div className="space-y-2">
139+
<Label>Chain</Label>
140+
<BridgeNetworkSelector
141+
chainId={chainId}
142+
onChange={(chainId) => {
143+
setOptions((v) => ({
144+
...v,
145+
prefill: {
146+
...v.prefill,
147+
[props.type]: {
148+
...v.prefill?.[props.type],
149+
chainId,
150+
tokenAddress: undefined, // clear token selection
151+
},
152+
},
153+
}));
154+
}}
155+
placeholder="Select a chain"
156+
className="bg-card"
157+
/>
158+
</div>
159+
160+
{/* Token selection - only show if chain is selected */}
161+
<div className="space-y-2">
162+
<Label>Token</Label>
163+
<TokenSelector
164+
chainId={chainId}
165+
client={THIRDWEB_CLIENT}
166+
disableAddress
167+
enabled={true}
168+
onChange={(token) => {
169+
setOptions((v) => ({
170+
...v,
171+
prefill: {
172+
...v.prefill,
173+
[props.type]: {
174+
chainId: token.chainId,
175+
tokenAddress: token.address,
176+
},
177+
},
178+
}));
179+
}}
180+
placeholder="Select a token"
181+
selectedToken={
182+
tokenAddress && chainId
183+
? {
184+
address: tokenAddress,
185+
chainId: chainId,
186+
}
187+
: chainId
188+
? {
189+
address: getAddress(NATIVE_TOKEN_ADDRESS),
190+
chainId: chainId,
191+
}
192+
: undefined
193+
}
194+
className="bg-card"
195+
/>
196+
</div>
197+
198+
{chainId && (
199+
<div className="space-y-2">
200+
<Label> Token Amount</Label>
201+
<Input
202+
className="bg-card"
203+
value={options.prefill?.[props.type]?.amount || ""}
204+
onChange={(e) => {
205+
setOptions((v) => {
206+
return {
207+
...v,
208+
prefill: {
209+
...v.prefill,
210+
[props.type]: {
211+
...v.prefill?.[props.type],
212+
amount: e.target.value,
213+
chainId: chainId,
214+
},
215+
},
216+
};
217+
});
218+
}}
219+
placeholder="0.01"
220+
/>
221+
</div>
222+
)}
223+
</div>
224+
</div>
225+
);
226+
}

0 commit comments

Comments
 (0)