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
5 changes: 5 additions & 0 deletions .changeset/honest-hands-clap.md
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
105 changes: 105 additions & 0 deletions apps/playground-web/src/app/bridge/swap-widget/components/code.tsx
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" />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

🧩 Analysis chain

Verify consistency with the existing CodeGen pattern.

The language is set to "ts" while the similar component in apps/playground-web/src/app/payments/components/CodeGen.tsx uses "tsx". Consider whether this should be consistent.


🏁 Script executed:

#!/bin/bash
# Check other CodeGen components to see what language they use
rg -n "lang=" apps/playground-web/src/app/ --type=tsx

Length 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.tsx

Length 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 || true

Length of output: 22935


Change lang from "ts" to "tsx" in swap-widget CodeGen.
getCode(...) returns JSX (<SwapWidget ... />); use "tsx" to match other CodeGen components and ensure correct highlighting.
File: apps/playground-web/src/app/bridge/swap-widget/components/code.tsx:23

🤖 Prompt for AI Agents
In apps/playground-web/src/app/bridge/swap-widget/components/code.tsx around
line 23, the CodeClient component is using lang="ts" but getCode(...) returns
JSX (<SwapWidget ... />); change the lang prop to "tsx" so the generated code is
highlighted/treated correctly (update the prop from "ts" to "tsx" on the
CodeClient invocation).

</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>
);
}
Loading
Loading