Skip to content

Commit 1f572b5

Browse files
committed
[MNY-355] Playground: Add Buy Widget iframe playground
1 parent 6060ba7 commit 1f572b5

File tree

6 files changed

+214
-31
lines changed

6 files changed

+214
-31
lines changed

apps/playground-web/src/app/bridge/buy-widget/BuyPlayground.tsx

Lines changed: 65 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
"use client";
22

3-
import { useState } from "react";
3+
import { useTheme } from "next-themes";
4+
import { useEffect, useState } from "react";
45
import { arbitrum } from "thirdweb/chains";
6+
import { TabButtons } from "@/components/ui/tab-buttons";
57
import { LeftSection } from "../components/LeftSection";
68
import { RightSection } from "../components/RightSection";
79
import type { BridgeComponentsPlaygroundOptions } from "../components/types";
810

911
const defaultOptions: BridgeComponentsPlaygroundOptions = {
12+
integrationType: "react",
1013
payOptions: {
1114
buyTokenAddress: undefined,
1215
buyTokenAmount: "0.002",
@@ -29,16 +32,70 @@ const defaultOptions: BridgeComponentsPlaygroundOptions = {
2932
},
3033
};
3134

32-
export function BuyPlayground() {
33-
const [options, setOptions] =
34-
useState<BridgeComponentsPlaygroundOptions>(defaultOptions);
35+
function updatePageUrl(
36+
tab: BridgeComponentsPlaygroundOptions["integrationType"],
37+
) {
38+
const url = new URL(window.location.href);
39+
if (tab === defaultOptions.integrationType) {
40+
url.searchParams.delete("tab");
41+
} else {
42+
url.searchParams.set("tab", tab || "");
43+
}
44+
45+
window.history.replaceState({}, "", url.toString());
46+
}
47+
48+
export function BuyPlayground(props: { defaultTab?: "iframe" | "react" }) {
49+
const { theme } = useTheme();
50+
51+
const [options, setOptions] = useState<BridgeComponentsPlaygroundOptions>(
52+
() => ({
53+
...defaultOptions,
54+
integrationType: props.defaultTab || defaultOptions.integrationType,
55+
}),
56+
);
57+
58+
// Change theme on global theme change
59+
useEffect(() => {
60+
setOptions((prev) => ({
61+
...prev,
62+
theme: {
63+
...prev.theme,
64+
type: theme === "dark" ? "dark" : "light",
65+
},
66+
}));
67+
}, [theme]);
68+
69+
useEffect(() => {
70+
updatePageUrl(options.integrationType);
71+
}, [options.integrationType]);
3572

3673
return (
37-
<div className="relative flex flex-col-reverse gap-6 xl:min-h-[900px] xl:flex-row xl:gap-6">
38-
<div className="grow border-b pb-10 xl:mb-0 xl:border-r xl:border-b-0 xl:pr-6">
39-
<LeftSection widget="buy" options={options} setOptions={setOptions} />
74+
<div>
75+
<TabButtons
76+
tabs={[
77+
{
78+
name: "React",
79+
onClick: () => setOptions({ ...options, integrationType: "react" }),
80+
isActive: options.integrationType === "react",
81+
},
82+
{
83+
name: "Iframe",
84+
onClick: () =>
85+
setOptions({ ...options, integrationType: "iframe" }),
86+
isActive: options.integrationType === "iframe",
87+
},
88+
]}
89+
/>
90+
91+
<div className="h-6" />
92+
93+
<div className="relative flex flex-col-reverse gap-6 xl:min-h-[900px] xl:flex-row xl:gap-6">
94+
<div className="grow border-b pb-10 xl:mb-0 xl:border-r xl:border-b-0 xl:pr-6">
95+
<LeftSection widget="buy" options={options} setOptions={setOptions} />
96+
</div>
97+
<RightSection widget="buy" options={options} />
4098
</div>
41-
<RightSection widget="buy" options={options} />
4299
</div>
43100
);
44101
}

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

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

22-
export default function Page() {
22+
export default async function Page(props: {
23+
searchParams: Promise<{ tab?: string }>;
24+
}) {
25+
const searchParams = await props.searchParams;
26+
const defaultTab =
27+
searchParams.tab === "iframe" || searchParams.tab === "react"
28+
? searchParams.tab
29+
: undefined;
30+
2331
return (
2432
<ThirdwebProvider>
2533
<PageLayout
@@ -28,7 +36,7 @@ export default function Page() {
2836
description={description}
2937
docsLink="https://portal.thirdweb.com/references/typescript/v5/BuyWidget?utm_source=playground"
3038
>
31-
<BuyPlayground />
39+
<BuyPlayground defaultTab={defaultTab} />
3240
</PageLayout>
3341
</ThirdwebProvider>
3442
);

apps/playground-web/src/app/bridge/components/CodeGen.tsx

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
stringifyImports,
66
stringifyProps,
77
} from "../../../lib/code-gen";
8+
import { buildBuyIframeUrl } from "./buildBuyIframeUrl";
89
import { buildCheckoutIframeUrl } from "./buildCheckoutIframeUrl";
910
import type { BridgeComponentsPlaygroundOptions } from "./types";
1011

@@ -27,7 +28,8 @@ export function CodeGen(props: {
2728
options: BridgeComponentsPlaygroundOptions;
2829
}) {
2930
const isIframe =
30-
props.widget === "checkout" && props.options.integrationType === "iframe";
31+
(props.widget === "checkout" || props.widget === "buy") &&
32+
props.options.integrationType === "iframe";
3133

3234
return (
3335
<div className="flex w-full grow flex-col">
@@ -36,7 +38,7 @@ export function CodeGen(props: {
3638
className="grow"
3739
code={
3840
isIframe
39-
? getIframeCode(props.options)
41+
? getIframeCode(props.widget as "buy" | "checkout", props.options)
4042
: getCode(props.widget, props.options)
4143
}
4244
lang={isIframe ? "html" : "tsx"}
@@ -46,13 +48,22 @@ export function CodeGen(props: {
4648
);
4749
}
4850

49-
function getIframeCode(options: BridgeComponentsPlaygroundOptions) {
50-
const src = buildCheckoutIframeUrl(options);
51+
function getIframeCode(
52+
widget: "buy" | "checkout",
53+
options: BridgeComponentsPlaygroundOptions,
54+
) {
55+
const src =
56+
widget === "buy"
57+
? buildBuyIframeUrl(options)
58+
: buildCheckoutIframeUrl(options);
59+
60+
const hasImage = src.includes("image=");
61+
const height = hasImage ? "850px" : "700px";
5162

5263
return `\
5364
<iframe
5465
src="${src}"
55-
height="700px"
66+
height="${height}"
5667
width="100%"
5768
style="border: 0;"
5869
/>`;

apps/playground-web/src/app/bridge/components/LeftSection.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -486,7 +486,8 @@ export function LeftSection(props: {
486486

487487
{/* Colors - disabled for iframe */}
488488
{!(
489-
props.widget === "checkout" && options.integrationType === "iframe"
489+
(props.widget === "checkout" || props.widget === "buy") &&
490+
options.integrationType === "iframe"
490491
) && (
491492
<ColorFormGroup
492493
onChange={(newTheme) => {

apps/playground-web/src/app/bridge/components/RightSection.tsx

Lines changed: 49 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
import { Button } from "../../../components/ui/button";
1616
import { THIRDWEB_CLIENT } from "../../../lib/client";
1717
import { cn } from "../../../lib/utils";
18+
import { buildBuyIframeUrl } from "./buildBuyIframeUrl";
1819
import { buildCheckoutIframeUrl } from "./buildCheckoutIframeUrl";
1920
import { CodeGen } from "./CodeGen";
2021
import type { BridgeComponentsPlaygroundOptions } from "./types";
@@ -155,21 +156,15 @@ export function RightSection(props: {
155156
previewTab !== "code" && "items-center",
156157
)}
157158
>
158-
<BackgroundPattern />
159-
160159
{previewTab === "ui" &&
161-
(props.widget === "checkout" &&
162-
props.options.integrationType === "iframe" ? (
163-
<iframe
164-
src={buildCheckoutIframeUrl(props.options)}
165-
height="700px"
166-
width="100%"
167-
title="Checkout Widget"
168-
className="fade-in-0 animate-in rounded-xl duration-500"
169-
style={{
170-
border: "0",
171-
}}
172-
/>
160+
(props.options.integrationType === "iframe" ? (
161+
props.widget === "checkout" ? (
162+
<CheckoutIframePreview options={props.options} />
163+
) : props.widget === "buy" ? (
164+
<BuyWidgetIframePreview options={props.options} />
165+
) : (
166+
embed
167+
)
173168
) : (
174169
embed
175170
))}
@@ -182,7 +177,46 @@ export function RightSection(props: {
182177
);
183178
}
184179

185-
function BackgroundPattern() {
180+
function CheckoutIframePreview(props: {
181+
options: BridgeComponentsPlaygroundOptions;
182+
}) {
183+
const src = buildCheckoutIframeUrl(props.options);
184+
return (
185+
<iframe
186+
src={src}
187+
height="700px"
188+
width="100%"
189+
title="Checkout Widget"
190+
className="fade-in-0 animate-in rounded-xl duration-500"
191+
style={{
192+
border: "0",
193+
}}
194+
/>
195+
);
196+
}
197+
198+
function BuyWidgetIframePreview(props: {
199+
options: BridgeComponentsPlaygroundOptions;
200+
}) {
201+
const src = buildBuyIframeUrl(props.options);
202+
203+
const hasImage = src.includes("image=");
204+
const height = hasImage ? "850px" : "700px";
205+
return (
206+
<iframe
207+
src={src}
208+
height={height}
209+
width="100%"
210+
title="Buy Widget"
211+
className="fade-in-0 animate-in rounded-xl duration-500"
212+
style={{
213+
border: "0",
214+
}}
215+
/>
216+
);
217+
}
218+
219+
function _BackgroundPattern() {
186220
const color = "hsl(var(--foreground)/15%)";
187221
return (
188222
<div
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import type { BridgeComponentsPlaygroundOptions } from "./types";
2+
3+
const BUY_WIDGET_IFRAME_BASE_URL = "https://thirdweb.com/bridge/buy-widget";
4+
5+
export function buildBuyIframeUrl(options: BridgeComponentsPlaygroundOptions) {
6+
const url = new URL(BUY_WIDGET_IFRAME_BASE_URL);
7+
8+
// Chain (optional)
9+
if (options.payOptions.buyTokenChain?.id) {
10+
url.searchParams.set("chain", String(options.payOptions.buyTokenChain.id));
11+
}
12+
13+
// Token address (optional - if not set, native token is used)
14+
if (options.payOptions.buyTokenAddress) {
15+
url.searchParams.set("tokenAddress", options.payOptions.buyTokenAddress);
16+
}
17+
18+
// Amount (optional)
19+
if (options.payOptions.buyTokenAmount) {
20+
url.searchParams.set("amount", options.payOptions.buyTokenAmount);
21+
}
22+
23+
// Receiver address (optional)
24+
if (options.payOptions.receiverAddress) {
25+
url.searchParams.set("receiver", options.payOptions.receiverAddress);
26+
}
27+
28+
// Theme (only add if light, dark is default)
29+
if (options.theme.type === "light") {
30+
url.searchParams.set("theme", "light");
31+
}
32+
33+
// Currency (only add if not USD, USD is default)
34+
if (options.payOptions.currency && options.payOptions.currency !== "USD") {
35+
url.searchParams.set("currency", options.payOptions.currency);
36+
}
37+
38+
// Branding
39+
if (options.payOptions.showThirdwebBranding === false) {
40+
url.searchParams.set("showThirdwebBranding", "false");
41+
}
42+
43+
// Product info
44+
if (options.payOptions.title) {
45+
url.searchParams.set("title", options.payOptions.title);
46+
}
47+
48+
if (options.payOptions.description) {
49+
url.searchParams.set("description", options.payOptions.description);
50+
}
51+
52+
if (options.payOptions.image) {
53+
url.searchParams.set("image", options.payOptions.image);
54+
}
55+
56+
if (options.payOptions.buttonLabel) {
57+
url.searchParams.set("buttonLabel", options.payOptions.buttonLabel);
58+
}
59+
60+
// Payment methods
61+
if (
62+
options.payOptions.paymentMethods &&
63+
options.payOptions.paymentMethods.length === 1
64+
) {
65+
url.searchParams.set(
66+
"paymentMethods",
67+
options.payOptions.paymentMethods[0],
68+
);
69+
}
70+
71+
return url.toString();
72+
}

0 commit comments

Comments
 (0)