Skip to content

Commit d35cad8

Browse files
committed
[MNY-355] Playground: Add Buy Widget iframe playground (#8615)
<!-- ## 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 focuses on enhancing the `buy` widget functionality by introducing iframe support and improving the handling of integration types. It also adds new components for rendering iframe previews and updates the URL management for better integration experience. ### Detailed summary - Updated `LeftSection.tsx` to allow both `checkout` and `buy` widgets for iframe integration. - Modified `Page` component in `page.tsx` to handle async `searchParams` and pass `defaultTab` to `BuyPlayground`. - Enhanced `CodeGen` to support widget type in iframe code generation. - Added `CheckoutIframePreview` and `BuyWidgetIframePreview` components in `RightSection.tsx` for rendering respective iframes. - Created `buildBuyIframeUrl` function in `buildBuyIframeUrl.ts` for generating URLs for the buy widget. - Updated `BuyPlayground` to manage state and URL for integration type selection using tabs. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex -->
1 parent c1275a5 commit d35cad8

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)