Skip to content

Commit f497b61

Browse files
committed
[MNY-352] Playground: add swap widget iframe (#8605)
<!-- ## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes" If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000): ## Notes for the reviewer Anything important to call out? Be sure to also clarify these in your comments. ## How to test Unit tests, playground, etc. --> <!-- start pr-codex --> --- ## PR-Codex overview This PR introduces enhancements to the `SwapWidgetPlayground`, allowing for integration with both `iframe` and `react` types. It modifies how themes and options are handled, improves URL management, and updates iframe handling for better user experience. ### Detailed summary - Added `integrationType` to `SwapWidgetPlaygroundOptions`. - Conditional rendering of `ColorFormGroup` based on `integrationType`. - Created `getPreviewSrc` function to manage iframe URLs. - Updated `persistTokenSelections` handling in various components. - Introduced `SwapWidgetPlaygroundAsync` for async loading. - Refactored `RightSection` to conditionally render `iframe` or `SwapWidget`. - Implemented URL update logic based on selected integration type. - Enhanced code generation for iframe and react integration types. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex -->
1 parent f344fb5 commit f497b61

File tree

11 files changed

+220
-54
lines changed

11 files changed

+220
-54
lines changed

apps/dashboard/src/app/bridge/swap-widget/SwapWidgetEmbed.client.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export function SwapWidgetEmbed({
1616
showThirdwebBranding,
1717
theme,
1818
currency,
19+
persistTokenSelections,
1920
}: {
2021
buyChainId?: number;
2122
buyTokenAddress?: Address;
@@ -26,6 +27,7 @@ export function SwapWidgetEmbed({
2627
showThirdwebBranding?: boolean;
2728
theme: "light" | "dark";
2829
currency?: SupportedFiatCurrency;
30+
persistTokenSelections?: boolean;
2931
}) {
3032
const client = useMemo(
3133
() =>
@@ -77,6 +79,7 @@ export function SwapWidgetEmbed({
7779
showThirdwebBranding={showThirdwebBranding}
7880
theme={theme}
7981
currency={currency}
82+
persistTokenSelections={persistTokenSelections}
8083
onSuccess={() => {
8184
sendMessageToParent({
8285
source: "swap-widget",

apps/dashboard/src/app/bridge/swap-widget/page.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,14 @@ export default async function Page(props: {
5151
// Optional params
5252
const showThirdwebBranding = parseQueryParams(
5353
searchParams.showThirdwebBranding,
54-
(v) => v !== "false",
54+
// biome-ignore lint/complexity/noUselessTernary: this is easier to understand
55+
(v) => (v === "false" ? false : true),
56+
);
57+
58+
const persistTokenSelections = parseQueryParams(
59+
searchParams.persistTokenSelections,
60+
// biome-ignore lint/complexity/noUselessTernary: this is easier to understand
61+
(v) => (v === "false" ? false : true),
5562
);
5663

5764
const theme =
@@ -76,6 +83,7 @@ export default async function Page(props: {
7683
showThirdwebBranding={showThirdwebBranding}
7784
theme={theme}
7885
currency={currency}
86+
persistTokenSelections={persistTokenSelections}
7987
/>
8088
</div>
8189
</BridgeProvidersLite>
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import type { SwapWidgetPlaygroundOptions } from "./types";
2+
3+
const SWAP_WIDGET_IFRAME_BASE_URL = "https://thirdweb.com/bridge/swap-widget";
4+
5+
export function buildSwapIframeUrl(
6+
options: SwapWidgetPlaygroundOptions,
7+
type: "code" | "preview" = "preview",
8+
) {
9+
const url = new URL(SWAP_WIDGET_IFRAME_BASE_URL);
10+
11+
if (type === "preview") {
12+
// always set it to false so playground doesn't show last used tokens
13+
url.searchParams.set("persistTokenSelections", "false");
14+
}
15+
16+
// Buy token params
17+
if (options.prefill?.buyToken?.chainId) {
18+
url.searchParams.set("buyChain", String(options.prefill.buyToken.chainId));
19+
20+
if (options.prefill.buyToken.tokenAddress) {
21+
url.searchParams.set(
22+
"buyTokenAddress",
23+
options.prefill.buyToken.tokenAddress,
24+
);
25+
}
26+
27+
if (options.prefill.buyToken.amount) {
28+
url.searchParams.set("buyAmount", options.prefill.buyToken.amount);
29+
}
30+
}
31+
32+
// Sell token params
33+
if (options.prefill?.sellToken?.chainId) {
34+
url.searchParams.set(
35+
"sellChain",
36+
String(options.prefill.sellToken.chainId),
37+
);
38+
39+
if (options.prefill.sellToken.tokenAddress) {
40+
url.searchParams.set(
41+
"sellTokenAddress",
42+
options.prefill.sellToken.tokenAddress,
43+
);
44+
}
45+
46+
if (options.prefill.sellToken.amount) {
47+
url.searchParams.set("sellAmount", options.prefill.sellToken.amount);
48+
}
49+
}
50+
51+
// Theme (only add if light, dark is default)
52+
if (options.theme.type === "light") {
53+
url.searchParams.set("theme", "light");
54+
}
55+
56+
// Currency (only add if not USD, USD is default)
57+
if (options.currency && options.currency !== "USD") {
58+
url.searchParams.set("currency", options.currency);
59+
}
60+
61+
// Branding
62+
if (options.showThirdwebBranding === false) {
63+
url.searchParams.set("showThirdwebBranding", "false");
64+
}
65+
66+
return url.toString();
67+
}

apps/playground-web/src/app/bridge/swap-widget/components/code.tsx

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { lazy, Suspense } from "react";
22
import { LoadingDots } from "@/components/ui/LoadingDots";
33
import { quotes, stringifyImports, stringifyProps } from "@/lib/code-gen";
4+
import { buildSwapIframeUrl } from "./buildSwapIframeUrl";
45
import type { SwapWidgetPlaygroundOptions } from "./types";
56

67
const CodeClient = lazy(() =>
@@ -18,16 +19,35 @@ function CodeLoading() {
1819
}
1920

2021
export function CodeGen(props: { options: SwapWidgetPlaygroundOptions }) {
22+
const code =
23+
props.options.integrationType === "iframe"
24+
? getIframeCode(props.options)
25+
: getReactCode(props.options);
26+
27+
const lang = props.options.integrationType === "iframe" ? "html" : "ts";
28+
2129
return (
2230
<div className="flex w-full grow flex-col">
2331
<Suspense fallback={<CodeLoading />}>
24-
<CodeClient className="grow" code={getCode(props.options)} lang="ts" />
32+
<CodeClient className="grow" code={code} lang={lang} />
2533
</Suspense>
2634
</div>
2735
);
2836
}
2937

30-
function getCode(options: SwapWidgetPlaygroundOptions) {
38+
function getIframeCode(options: SwapWidgetPlaygroundOptions) {
39+
// Use "code" type to exclude persistTokenSelections from the generated code
40+
const iframeUrl = buildSwapIframeUrl(options, "code");
41+
42+
return `<iframe
43+
src="${iframeUrl}"
44+
height="700px"
45+
width="100%"
46+
style="border: 0;"
47+
/>`;
48+
}
49+
50+
function getReactCode(options: SwapWidgetPlaygroundOptions) {
3151
const imports = {
3252
thirdweb: ["createThirdwebClient"] as string[],
3353
"thirdweb/react": ["SwapWidget"] as string[],

apps/playground-web/src/app/bridge/swap-widget/components/left-section.tsx

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -85,16 +85,18 @@ export function LeftSection(props: {
8585

8686
<div className="h-6" />
8787

88-
{/* Colors */}
89-
<ColorFormGroup
90-
onChange={(newTheme) => {
91-
setOptions((v) => ({
92-
...v,
93-
theme: newTheme,
94-
}));
95-
}}
96-
theme={options.theme}
97-
/>
88+
{/* Colors - disabled for iframe */}
89+
{options.integrationType !== "iframe" && (
90+
<ColorFormGroup
91+
onChange={(newTheme) => {
92+
setOptions((v) => ({
93+
...v,
94+
theme: newTheme,
95+
}));
96+
}}
97+
theme={options.theme}
98+
/>
99+
)}
98100

99101
<div className="my-4 flex items-center gap-2">
100102
<Checkbox

apps/playground-web/src/app/bridge/swap-widget/components/right-section.tsx

Lines changed: 26 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { darkTheme, lightTheme, SwapWidget } from "thirdweb/react";
44
import { Button } from "@/components/ui/button";
55
import { THIRDWEB_CLIENT } from "@/lib/client";
66
import { cn } from "@/lib/utils";
7+
import { buildSwapIframeUrl } from "./buildSwapIframeUrl";
78
import { CodeGen } from "./code";
89
import type { SwapWidgetPlaygroundOptions } from "./types";
910

@@ -50,43 +51,38 @@ export function RightSection(props: { options: SwapWidgetPlaygroundOptions }) {
5051
previewTab !== "code" && "items-center",
5152
)}
5253
>
53-
<BackgroundPattern />
54-
55-
{previewTab === "ui" && (
56-
<SwapWidget
57-
client={THIRDWEB_CLIENT}
58-
theme={themeObj}
59-
prefill={props.options.prefill}
60-
currency={props.options.currency}
61-
showThirdwebBranding={props.options.showThirdwebBranding}
62-
key={JSON.stringify({
63-
prefill: props.options.prefill,
64-
})}
65-
persistTokenSelections={false}
66-
/>
67-
)}
54+
{previewTab === "ui" &&
55+
(props.options.integrationType === "iframe" ? (
56+
<iframe
57+
src={buildSwapIframeUrl(props.options, "preview")}
58+
height="700px"
59+
width="100%"
60+
title="Swap Widget"
61+
className="fade-in-0 animate-in rounded-xl duration-500"
62+
style={{
63+
border: "0",
64+
}}
65+
/>
66+
) : (
67+
<SwapWidget
68+
client={THIRDWEB_CLIENT}
69+
theme={themeObj}
70+
prefill={props.options.prefill}
71+
currency={props.options.currency}
72+
showThirdwebBranding={props.options.showThirdwebBranding}
73+
key={JSON.stringify({
74+
prefill: props.options.prefill,
75+
})}
76+
persistTokenSelections={false}
77+
/>
78+
))}
6879

6980
{previewTab === "code" && <CodeGen options={props.options} />}
7081
</div>
7182
</div>
7283
);
7384
}
7485

75-
function BackgroundPattern() {
76-
const color = "hsl(var(--foreground)/15%)";
77-
return (
78-
<div
79-
className="absolute inset-0 z-[-1]"
80-
style={{
81-
backgroundImage: `radial-gradient(${color} 1px, transparent 1px)`,
82-
backgroundSize: "24px 24px",
83-
maskImage:
84-
"radial-gradient(ellipse 100% 100% at 50% 50%, black 30%, transparent 60%)",
85-
}}
86-
/>
87-
);
88-
}
89-
9086
function TabButtons(props: {
9187
tabs: Array<{
9288
name: string;

apps/playground-web/src/app/bridge/swap-widget/components/swap-widget-playground.tsx

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22

33
import { useTheme } from "next-themes";
44
import { useEffect, useState } from "react";
5+
import { TabButtons } from "@/components/ui/tab-buttons";
56
import { LeftSection } from "./left-section";
67
import { RightSection } from "./right-section";
78
import type { SwapWidgetPlaygroundOptions } from "./types";
89

910
const defaultOptions: SwapWidgetPlaygroundOptions = {
11+
integrationType: "react",
1012
prefill: undefined,
1113
currency: "USD",
1214
showThirdwebBranding: true,
@@ -17,11 +19,26 @@ const defaultOptions: SwapWidgetPlaygroundOptions = {
1719
},
1820
};
1921

20-
export function SwapWidgetPlayground() {
22+
function updatePageUrl(tab: SwapWidgetPlaygroundOptions["integrationType"]) {
23+
const url = new URL(window.location.href);
24+
if (tab === defaultOptions.integrationType) {
25+
url.searchParams.delete("tab");
26+
} else {
27+
url.searchParams.set("tab", tab || "");
28+
}
29+
30+
window.history.replaceState({}, "", url.toString());
31+
}
32+
33+
export function SwapWidgetPlayground(props: {
34+
defaultTab?: "iframe" | "react";
35+
}) {
2136
const { theme } = useTheme();
2237

23-
const [options, setOptions] =
24-
useState<SwapWidgetPlaygroundOptions>(defaultOptions);
38+
const [options, setOptions] = useState<SwapWidgetPlaygroundOptions>(() => ({
39+
...defaultOptions,
40+
integrationType: props.defaultTab || defaultOptions.integrationType,
41+
}));
2542

2643
// change theme on global theme change
2744
useEffect(() => {
@@ -34,12 +51,36 @@ export function SwapWidgetPlayground() {
3451
}));
3552
}, [theme]);
3653

54+
useEffect(() => {
55+
updatePageUrl(options.integrationType);
56+
}, [options.integrationType]);
57+
3758
return (
38-
<div className="relative flex flex-col-reverse gap-6 xl:min-h-[900px] xl:flex-row xl:gap-6">
39-
<div className="grow border-b pb-10 xl:mb-0 xl:border-r xl:border-b-0 xl:pr-6">
40-
<LeftSection options={options} setOptions={setOptions} />
59+
<div>
60+
<TabButtons
61+
tabs={[
62+
{
63+
name: "React",
64+
onClick: () => setOptions({ ...options, integrationType: "react" }),
65+
isActive: options.integrationType === "react",
66+
},
67+
{
68+
name: "Iframe",
69+
onClick: () =>
70+
setOptions({ ...options, integrationType: "iframe" }),
71+
isActive: options.integrationType === "iframe",
72+
},
73+
]}
74+
/>
75+
76+
<div className="h-6" />
77+
78+
<div className="relative flex flex-col-reverse gap-6 xl:min-h-[900px] xl:flex-row xl:gap-6">
79+
<div className="grow border-b pb-10 xl:mb-0 xl:border-r xl:border-b-0 xl:pr-6">
80+
<LeftSection options={options} setOptions={setOptions} />
81+
</div>
82+
<RightSection options={options} />
4183
</div>
42-
<RightSection options={options} />
4384
</div>
4485
);
4586
}

apps/playground-web/src/app/bridge/swap-widget/components/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { SwapWidgetProps, ThemeOverrides } from "thirdweb/react";
22

33
export type SwapWidgetPlaygroundOptions = {
4+
integrationType: "iframe" | "react";
45
theme: {
56
type: "dark" | "light";
67
darkColorOverrides: ThemeOverrides["colors"];

apps/playground-web/src/app/bridge/swap-widget/page.tsx

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ export const metadata = createMetadata({
1919
},
2020
});
2121

22-
export default function Page() {
22+
export default function Page(props: {
23+
searchParams: Promise<{ tab?: string }>;
24+
}) {
2325
return (
2426
<ThirdwebProvider>
2527
<PageLayout
@@ -28,8 +30,20 @@ export default function Page() {
2830
description={description}
2931
docsLink="https://portal.thirdweb.com/references/typescript/v5/SwapWidget?utm_source=playground"
3032
>
31-
<SwapWidgetPlayground />
33+
<SwapWidgetPlaygroundAsync searchParams={props.searchParams} />
3234
</PageLayout>
3335
</ThirdwebProvider>
3436
);
3537
}
38+
39+
async function SwapWidgetPlaygroundAsync(props: {
40+
searchParams: Promise<{ tab?: string }>;
41+
}) {
42+
const searchParams = await props.searchParams;
43+
const defaultTab =
44+
searchParams.tab === "iframe" || searchParams.tab === "react"
45+
? searchParams.tab
46+
: undefined;
47+
48+
return <SwapWidgetPlayground defaultTab={defaultTab} />;
49+
}

0 commit comments

Comments
 (0)