Skip to content

Commit d5397aa

Browse files
committed
Playground: Add CheckoutWidget iframe
1 parent ec73f1f commit d5397aa

File tree

10 files changed

+242
-28
lines changed

10 files changed

+242
-28
lines changed

apps/dashboard/src/app/bridge/checkout-widget/CheckoutWidgetEmbed.client.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export function CheckoutWidgetEmbed({
3434
showThirdwebBranding,
3535
theme,
3636
currency,
37+
paymentMethods,
3738
}: {
3839
chainId: number;
3940
amount: string;
@@ -48,6 +49,7 @@ export function CheckoutWidgetEmbed({
4849
showThirdwebBranding?: boolean;
4950
theme: "light" | "dark";
5051
currency?: SupportedFiatCurrency;
52+
paymentMethods?: ("crypto" | "card")[];
5153
}) {
5254
const client = useMemo(
5355
() =>
@@ -79,6 +81,7 @@ export function CheckoutWidgetEmbed({
7981
showThirdwebBranding={showThirdwebBranding}
8082
theme={theme}
8183
currency={currency}
84+
paymentMethods={paymentMethods}
8285
connectOptions={{
8386
wallets: bridgeWallets,
8487
appMetadata,

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,14 @@ export default async function Page(props: {
6868
isValidCurrency(v) ? (v as SupportedFiatCurrency) : undefined,
6969
);
7070

71+
const paymentMethods = parseQueryParams(searchParams.paymentMethods, (v) => {
72+
if (v === "crypto" || v === "card") {
73+
return [v] as ("crypto" | "card")[];
74+
}
75+
76+
return undefined;
77+
});
78+
7179
// Validate required params
7280
if (!chainId || !amount || !seller) {
7381
return (
@@ -125,6 +133,7 @@ export default async function Page(props: {
125133
showThirdwebBranding={showThirdwebBranding}
126134
theme={theme}
127135
currency={currency}
136+
paymentMethods={paymentMethods}
128137
/>
129138
</div>
130139
</Providers>

apps/playground-web/src/app/bridge/checkout-widget/CheckoutPlayground.tsx

Lines changed: 69 additions & 12 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.01",
@@ -29,20 +32,74 @@ const defaultOptions: BridgeComponentsPlaygroundOptions = {
2932
},
3033
};
3134

32-
export function CheckoutPlayground() {
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 CheckoutPlayground(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
40-
widget="checkout"
41-
options={options}
42-
setOptions={setOptions}
43-
/>
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
96+
widget="checkout"
97+
options={options}
98+
setOptions={setOptions}
99+
/>
100+
</div>
101+
<RightSection widget="checkout" options={options} />
44102
</div>
45-
<RightSection widget="checkout" options={options} />
46103
</div>
47104
);
48105
}

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

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ const description =
1010
const ogDescription =
1111
"Accept fiat or crypto payments on any chain—direct to your wallet. Instant checkout, webhook support, and full control over post-sale actions.";
1212

13+
const validTabs = ["iframe", "react"] as const;
14+
type ValidTabs = (typeof validTabs)[number];
15+
1316
export const metadata = createMetadata({
1417
description: ogDescription,
1518
title,
@@ -19,16 +22,28 @@ export const metadata = createMetadata({
1922
},
2023
});
2124

22-
export default function Page() {
25+
export default async function Page(props: {
26+
searchParams: Promise<{
27+
tab?: string | undefined | string[];
28+
}>;
29+
}) {
30+
const searchParams = await props.searchParams;
31+
const tab =
32+
typeof searchParams.tab === "string" ? searchParams.tab : undefined;
33+
34+
const validTab = validTabs.includes(tab as ValidTabs)
35+
? (tab as ValidTabs)
36+
: undefined;
37+
2338
return (
2439
<ThirdwebProvider>
2540
<PageLayout
2641
icon={CreditCardIcon}
2742
title={title}
2843
description={description}
29-
docsLink="https://portal.thirdweb.com/references/typescript/v5/CheckoutWidget?utm_source=playground"
44+
docsLink="https://portal.thirdweb.com/bridge/checkout-widget?utm_source=playground"
3045
>
31-
<CheckoutPlayground />
46+
<CheckoutPlayground defaultTab={validTab} />
3247
</PageLayout>
3348
</ThirdwebProvider>
3449
);

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

Lines changed: 22 additions & 2 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 { buildCheckoutIframeUrl } from "./buildCheckoutIframeUrl";
89
import type { BridgeComponentsPlaygroundOptions } from "./types";
910

1011
const CodeClient = lazy(() =>
@@ -25,19 +26,38 @@ export function CodeGen(props: {
2526
widget: "buy" | "checkout" | "transaction";
2627
options: BridgeComponentsPlaygroundOptions;
2728
}) {
29+
const isIframe =
30+
props.widget === "checkout" && props.options.integrationType === "iframe";
31+
2832
return (
2933
<div className="flex w-full grow flex-col">
3034
<Suspense fallback={<CodeLoading />}>
3135
<CodeClient
3236
className="grow"
33-
code={getCode(props.widget, props.options)}
34-
lang="tsx"
37+
code={
38+
isIframe
39+
? getIframeCode(props.options)
40+
: getCode(props.widget, props.options)
41+
}
42+
lang={isIframe ? "html" : "tsx"}
3543
/>
3644
</Suspense>
3745
</div>
3846
);
3947
}
4048

49+
function getIframeCode(options: BridgeComponentsPlaygroundOptions) {
50+
const src = buildCheckoutIframeUrl(options, "code");
51+
52+
return `\
53+
<iframe
54+
src="${src}"
55+
height="700px"
56+
width="100%"
57+
style="border: 0;"
58+
/>`;
59+
}
60+
4161
function getCode(
4262
widget: "buy" | "checkout" | "transaction",
4363
options: BridgeComponentsPlaygroundOptions,

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

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -484,16 +484,20 @@ export function LeftSection(props: {
484484

485485
<div className="h-6" />
486486

487-
{/* Colors */}
488-
<ColorFormGroup
489-
onChange={(newTheme) => {
490-
setOptions((v) => ({
491-
...v,
492-
theme: newTheme,
493-
}));
494-
}}
495-
theme={options.theme}
496-
/>
487+
{/* Colors - disabled for iframe */}
488+
{!(
489+
props.widget === "checkout" && options.integrationType === "iframe"
490+
) && (
491+
<ColorFormGroup
492+
onChange={(newTheme) => {
493+
setOptions((v) => ({
494+
...v,
495+
theme: newTheme,
496+
}));
497+
}}
498+
theme={options.theme}
499+
/>
500+
)}
497501

498502
<div className="my-4 flex items-center gap-2">
499503
<Checkbox

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

Lines changed: 17 additions & 1 deletion
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 { buildCheckoutIframeUrl } from "./buildCheckoutIframeUrl";
1819
import { CodeGen } from "./CodeGen";
1920
import type { BridgeComponentsPlaygroundOptions } from "./types";
2021

@@ -156,7 +157,22 @@ export function RightSection(props: {
156157
>
157158
<BackgroundPattern />
158159

159-
{previewTab === "ui" && embed}
160+
{previewTab === "ui" &&
161+
(props.widget === "checkout" &&
162+
props.options.integrationType === "iframe" ? (
163+
<iframe
164+
src={buildCheckoutIframeUrl(props.options, "preview")}
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+
/>
173+
) : (
174+
embed
175+
))}
160176

161177
{previewTab === "code" && (
162178
<CodeGen widget={props.widget} options={props.options} />
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import type { BridgeComponentsPlaygroundOptions } from "./types";
2+
3+
const CHECKOUT_WIDGET_IFRAME_BASE_URL =
4+
"https://thirdweb.com/bridge/checkout-widget";
5+
6+
export function buildCheckoutIframeUrl(
7+
options: BridgeComponentsPlaygroundOptions,
8+
_type: "code" | "preview",
9+
) {
10+
const url = new URL(CHECKOUT_WIDGET_IFRAME_BASE_URL);
11+
12+
// Required params
13+
url.searchParams.set("chain", String(options.payOptions.buyTokenChain.id));
14+
url.searchParams.set("amount", options.payOptions.buyTokenAmount);
15+
url.searchParams.set("seller", options.payOptions.sellerAddress);
16+
17+
// Token address (optional - if not set, native token is used)
18+
if (options.payOptions.buyTokenAddress) {
19+
url.searchParams.set("tokenAddress", options.payOptions.buyTokenAddress);
20+
}
21+
22+
// Theme (only add if light, dark is default)
23+
if (options.theme.type === "light") {
24+
url.searchParams.set("theme", "light");
25+
}
26+
27+
// Currency (only add if not USD, USD is default)
28+
if (options.payOptions.currency && options.payOptions.currency !== "USD") {
29+
url.searchParams.set("currency", options.payOptions.currency);
30+
}
31+
32+
// Branding
33+
if (options.payOptions.showThirdwebBranding === false) {
34+
url.searchParams.set("showThirdwebBranding", "false");
35+
}
36+
37+
// Product info
38+
if (options.payOptions.title) {
39+
url.searchParams.set("title", options.payOptions.title);
40+
}
41+
42+
if (options.payOptions.description) {
43+
url.searchParams.set("description", options.payOptions.description);
44+
}
45+
46+
if (options.payOptions.image) {
47+
url.searchParams.set("image", options.payOptions.image);
48+
}
49+
50+
if (options.payOptions.buttonLabel) {
51+
url.searchParams.set("buttonLabel", options.payOptions.buttonLabel);
52+
}
53+
54+
// Payment methods
55+
if (
56+
options.payOptions.paymentMethods &&
57+
options.payOptions.paymentMethods.length === 1
58+
) {
59+
url.searchParams.set(
60+
"paymentMethods",
61+
options.payOptions.paymentMethods[0],
62+
);
63+
}
64+
65+
return url.toString();
66+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ const CURRENCIES = [
3131
type SupportedFiatCurrency = (typeof CURRENCIES)[number];
3232

3333
export type BridgeComponentsPlaygroundOptions = {
34+
integrationType?: "iframe" | "react";
3435
theme: {
3536
type: "dark" | "light";
3637
darkColorOverrides: ThemeOverrides["colors"];

0 commit comments

Comments
 (0)