-
Notifications
You must be signed in to change notification settings - Fork 619
[MNY-190] Playground: Add SwapWidget #8069
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "thirdweb": patch | ||
| --- | ||
|
|
||
| Add `persistTokenSelections` prop on `SwapWidget` to allow disabling token selection persistence to local storage | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| import { lazy, Suspense } from "react"; | ||
| import { LoadingDots } from "@/components/ui/LoadingDots"; | ||
| import type { SwapWidgetPlaygroundOptions } from "./types"; | ||
|
|
||
| const CodeClient = lazy(() => | ||
| import("../../../../components/code/code.client").then((m) => ({ | ||
| default: m.CodeClient, | ||
| })), | ||
| ); | ||
|
|
||
| function CodeLoading() { | ||
| return ( | ||
| <div className="flex min-h-[300px] grow items-center justify-center bg-card border rounded-lg"> | ||
| <LoadingDots /> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| export function CodeGen(props: { options: SwapWidgetPlaygroundOptions }) { | ||
| return ( | ||
| <div className="flex w-full grow flex-col"> | ||
| <Suspense fallback={<CodeLoading />}> | ||
| <CodeClient className="grow" code={getCode(props.options)} lang="ts" /> | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chainVerify consistency with the existing CodeGen pattern. The language is set to 🏁 Script executed: #!/bin/bash
# Check other CodeGen components to see what language they use
rg -n "lang=" apps/playground-web/src/app/ --type=tsxLength of output: 27 🏁 Script executed: #!/bin/bash
# Search for all lang= usages in TSX files under the playground-web app
rg -n "lang=" -g "*.tsx" apps/playground-web/src/app/
# Inspect the CodeClient invocation in the CodeGen component for its lang value
rg -n "<CodeClient" -g "*.tsx" apps/playground-web/src/app/payments/components/CodeGen.tsxLength of output: 3398 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "=== swap-widget file (first 300 lines) ==="
sed -n '1,300p' apps/playground-web/src/app/bridge/swap-widget/components/code.tsx || true
echo
echo "=== Search for getCode usages ==="
rg -n "getCode" apps/playground-web/src/app -S || true
echo
echo "=== payments CodeGen (first 200 lines) ==="
sed -n '1,200p' apps/playground-web/src/app/payments/components/CodeGen.tsx || true
echo
echo "=== All occurrences of <CodeClient ...> ==="
rg -n "<CodeClient" apps/playground-web/src/app -S || true
echo
echo "=== Locate CodeClient definition (likely component) ==="
rg -n "export (default )?.*CodeClient|function CodeClient|const CodeClient" -S || true
echo
echo "=== Fallback: search for files exporting Code components (CodeServer, CodeClient) ==="
rg -n "CodeServer|CodeClient" -S || trueLength of output: 22935 Change lang from "ts" to "tsx" in swap-widget CodeGen. 🤖 Prompt for AI Agents |
||
| </Suspense> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| function getCode(options: SwapWidgetPlaygroundOptions) { | ||
| const imports = { | ||
| react: ["SwapWidget"] as string[], | ||
| }; | ||
|
|
||
| let themeProp: string | undefined; | ||
| if ( | ||
| options.theme.type === "dark" && | ||
| Object.keys(options.theme.darkColorOverrides || {}).length > 0 | ||
| ) { | ||
| themeProp = `darkTheme({ | ||
| colors: ${JSON.stringify(options.theme.darkColorOverrides)}, | ||
| })`; | ||
| imports.react.push("darkTheme"); | ||
| } | ||
|
|
||
| if (options.theme.type === "light") { | ||
| if (Object.keys(options.theme.lightColorOverrides || {}).length > 0) { | ||
| themeProp = `lightTheme({ | ||
| colors: ${JSON.stringify(options.theme.lightColorOverrides)}, | ||
| })`; | ||
| imports.react.push("lightTheme"); | ||
| } else { | ||
| themeProp = quotes("light"); | ||
| } | ||
| } | ||
|
|
||
| const props: Record<string, string | undefined | boolean> = { | ||
| theme: themeProp, | ||
| prefill: | ||
| options.prefill?.buyToken || options.prefill?.sellToken | ||
| ? JSON.stringify(options.prefill, null, 2) | ||
| : undefined, | ||
| currency: | ||
| options.currency !== "USD" && options.currency | ||
| ? quotes(options.currency) | ||
| : undefined, | ||
| showThirdwebBranding: | ||
| options.showThirdwebBranding === false ? false : undefined, | ||
| client: "client", | ||
| }; | ||
|
|
||
| return `\ | ||
| import { createThirdwebClient } from "thirdweb"; | ||
| import { ${imports.react.join(", ")} } from "thirdweb/react"; | ||
|
|
||
| const client = createThirdwebClient({ | ||
| clientId: "....", | ||
| }); | ||
|
|
||
|
|
||
| function Example() { | ||
| return ( | ||
| <SwapWidget | ||
| ${stringifyProps(props)} | ||
| /> | ||
| ); | ||
| }`; | ||
| } | ||
|
|
||
| function quotes(value: string) { | ||
| return `"${value}"`; | ||
| } | ||
|
|
||
| function stringifyProps(props: Record<string, string | undefined | boolean>) { | ||
| const _props: Record<string, string | undefined | boolean> = {}; | ||
|
|
||
| for (const key in props) { | ||
| if (props[key] !== undefined && props[key] !== "") { | ||
| _props[key] = props[key]; | ||
| } | ||
| } | ||
|
|
||
| return Object.entries(_props) | ||
| .map(([key, value]) => `${key}={${value}}`) | ||
| .join("\n\t "); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,226 @@ | ||
| "use client"; | ||
|
|
||
| import { CoinsIcon, PaletteIcon } from "lucide-react"; | ||
| import type React from "react"; | ||
| import { useId } from "react"; | ||
| import { getAddress, NATIVE_TOKEN_ADDRESS } from "thirdweb"; | ||
| import { CollapsibleSection } from "@/app/wallets/sign-in/components/CollapsibleSection"; | ||
| import { ColorFormGroup } from "@/app/wallets/sign-in/components/ColorFormGroup"; | ||
| import { BridgeNetworkSelector } from "@/components/blocks/NetworkSelectors"; | ||
| import { CustomRadioGroup } from "@/components/ui/CustomRadioGroup"; | ||
| import { Checkbox } from "@/components/ui/checkbox"; | ||
| import { Input } from "@/components/ui/input"; | ||
| import { Label } from "@/components/ui/label"; | ||
|
|
||
| import { TokenSelector } from "@/components/ui/TokenSelector"; | ||
| import { THIRDWEB_CLIENT } from "@/lib/client"; | ||
| import { CurrencySelector } from "../../../../components/blocks/CurrencySelector"; | ||
| import type { SwapWidgetPlaygroundOptions } from "./types"; | ||
|
|
||
| export function LeftSection(props: { | ||
| options: SwapWidgetPlaygroundOptions; | ||
| setOptions: React.Dispatch<React.SetStateAction<SwapWidgetPlaygroundOptions>>; | ||
| }) { | ||
| const { options, setOptions } = props; | ||
| const setThemeType = (themeType: "dark" | "light") => { | ||
| setOptions((v) => ({ | ||
| ...v, | ||
| theme: { | ||
| ...v.theme, | ||
| type: themeType, | ||
| }, | ||
| })); | ||
| }; | ||
|
|
||
| const themeId = useId(); | ||
|
|
||
| return ( | ||
| <div className="flex flex-col gap-4"> | ||
| <CollapsibleSection defaultOpen icon={CoinsIcon} title="Token Selection"> | ||
| <div className="flex flex-col gap-6 pt-5"> | ||
| <section className="flex flex-col gap-3"> | ||
| <Label htmlFor="currency">Display Currency</Label> | ||
| <CurrencySelector | ||
| value={options.currency} | ||
| onChange={(currency) => { | ||
| setOptions((v) => ({ ...v, currency })); | ||
| }} | ||
| /> | ||
| </section> | ||
|
|
||
| <div className="border-t border-dashed" /> | ||
|
|
||
| <TokenFieldset | ||
| title="Sell Token" | ||
| type="sellToken" | ||
| options={options} | ||
| setOptions={setOptions} | ||
| /> | ||
|
|
||
| <div className="border-t border-dashed" /> | ||
|
|
||
| <TokenFieldset | ||
| title="Buy Token" | ||
| type="buyToken" | ||
| options={options} | ||
| setOptions={setOptions} | ||
| /> | ||
| </div> | ||
| </CollapsibleSection> | ||
|
|
||
| <CollapsibleSection icon={PaletteIcon} title="Appearance"> | ||
| {/* Theme */} | ||
| <section className="flex flex-col gap-3 pt-6"> | ||
| <Label htmlFor="theme"> Theme </Label> | ||
| <CustomRadioGroup | ||
| id={themeId} | ||
| onValueChange={setThemeType} | ||
| options={[ | ||
| { label: "Dark", value: "dark" }, | ||
| { label: "Light", value: "light" }, | ||
| ]} | ||
| value={options.theme.type} | ||
| /> | ||
| </section> | ||
|
|
||
| <div className="h-6" /> | ||
|
|
||
| {/* Colors */} | ||
| <ColorFormGroup | ||
| onChange={(newTheme) => { | ||
| setOptions((v) => ({ | ||
| ...v, | ||
| theme: newTheme, | ||
| })); | ||
| }} | ||
| theme={options.theme} | ||
| /> | ||
|
|
||
| <div className="my-4 flex items-center gap-2"> | ||
| <Checkbox | ||
| checked={options.showThirdwebBranding} | ||
| id={"branding"} | ||
| onCheckedChange={(checked) => { | ||
| setOptions((v) => ({ | ||
| ...v, | ||
| showThirdwebBranding: checked === true, | ||
| })); | ||
| }} | ||
| /> | ||
| <Label htmlFor={"branding"}>Show Branding</Label> | ||
| </div> | ||
| </CollapsibleSection> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| function TokenFieldset(props: { | ||
| type: "buyToken" | "sellToken"; | ||
| title: string; | ||
| options: SwapWidgetPlaygroundOptions; | ||
| setOptions: React.Dispatch<React.SetStateAction<SwapWidgetPlaygroundOptions>>; | ||
| }) { | ||
| const { options, setOptions } = props; | ||
|
|
||
| const chainId = options.prefill?.[props.type]?.chainId; | ||
| const tokenAddress = options.prefill?.[props.type]?.tokenAddress; | ||
|
|
||
| return ( | ||
| <div> | ||
| <h3 className="mb-1 font-medium">{props.title}</h3> | ||
| <p className="text-sm text-muted-foreground mb-3"> | ||
| Sets the default token and amount to{" "} | ||
| {props.type === "buyToken" ? "buy" : "sell"} in the widget, <br /> | ||
| User can change this default selection in the widget | ||
| </p> | ||
| <div className="space-y-4"> | ||
| {/* Chain selection */} | ||
| <div className="space-y-2"> | ||
| <Label>Chain</Label> | ||
| <BridgeNetworkSelector | ||
| chainId={chainId} | ||
| onChange={(chainId) => { | ||
| setOptions((v) => ({ | ||
| ...v, | ||
| prefill: { | ||
| ...v.prefill, | ||
| [props.type]: { | ||
| ...v.prefill?.[props.type], | ||
| chainId, | ||
| tokenAddress: undefined, // clear token selection | ||
| }, | ||
| }, | ||
| })); | ||
| }} | ||
| placeholder="Select a chain" | ||
| className="bg-card" | ||
| /> | ||
| </div> | ||
|
|
||
| {/* Token selection - only show if chain is selected */} | ||
| <div className="space-y-2"> | ||
| <Label>Token</Label> | ||
| <TokenSelector | ||
| chainId={chainId} | ||
| client={THIRDWEB_CLIENT} | ||
| disableAddress | ||
| enabled={true} | ||
| onChange={(token) => { | ||
| setOptions((v) => ({ | ||
| ...v, | ||
| prefill: { | ||
| ...v.prefill, | ||
| [props.type]: { | ||
| chainId: token.chainId, | ||
| tokenAddress: token.address, | ||
| }, | ||
| }, | ||
| })); | ||
| }} | ||
| placeholder="Select a token" | ||
| selectedToken={ | ||
| tokenAddress && chainId | ||
| ? { | ||
| address: tokenAddress, | ||
| chainId: chainId, | ||
| } | ||
| : chainId | ||
| ? { | ||
| address: getAddress(NATIVE_TOKEN_ADDRESS), | ||
| chainId: chainId, | ||
| } | ||
| : undefined | ||
| } | ||
| className="bg-card" | ||
| /> | ||
| </div> | ||
|
|
||
| {chainId && ( | ||
| <div className="space-y-2"> | ||
| <Label> Token Amount</Label> | ||
| <Input | ||
| className="bg-card" | ||
| value={options.prefill?.[props.type]?.amount || ""} | ||
| onChange={(e) => { | ||
| setOptions((v) => { | ||
| return { | ||
| ...v, | ||
| prefill: { | ||
| ...v.prefill, | ||
| [props.type]: { | ||
| ...v.prefill?.[props.type], | ||
| amount: e.target.value, | ||
| chainId: chainId, | ||
| }, | ||
| }, | ||
| }; | ||
| }); | ||
| }} | ||
| placeholder="0.01" | ||
| /> | ||
| </div> | ||
| )} | ||
| </div> | ||
| </div> | ||
| ); | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.