Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export function CheckoutWidgetEmbed({
showThirdwebBranding,
theme,
currency,
paymentMethods,
}: {
chainId: number;
amount: string;
Expand All @@ -48,6 +49,7 @@ export function CheckoutWidgetEmbed({
showThirdwebBranding?: boolean;
theme: "light" | "dark";
currency?: SupportedFiatCurrency;
paymentMethods?: ("crypto" | "card")[];
}) {
const client = useMemo(
() =>
Expand Down Expand Up @@ -79,6 +81,7 @@ export function CheckoutWidgetEmbed({
showThirdwebBranding={showThirdwebBranding}
theme={theme}
currency={currency}
paymentMethods={paymentMethods}
connectOptions={{
wallets: bridgeWallets,
appMetadata,
Expand Down
9 changes: 9 additions & 0 deletions apps/dashboard/src/app/bridge/checkout-widget/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,14 @@ export default async function Page(props: {
isValidCurrency(v) ? (v as SupportedFiatCurrency) : undefined,
);

const paymentMethods = parseQueryParams(searchParams.paymentMethods, (v) => {
if (v === "crypto" || v === "card") {
return [v] as ("crypto" | "card")[];
}

return undefined;
});

// Validate required params
if (!chainId || !amount || !seller) {
return (
Expand Down Expand Up @@ -125,6 +133,7 @@ export default async function Page(props: {
showThirdwebBranding={showThirdwebBranding}
theme={theme}
currency={currency}
paymentMethods={paymentMethods}
/>
</div>
</Providers>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
"use client";

import { useState } from "react";
import { useTheme } from "next-themes";
import { useEffect, useState } from "react";
import { arbitrum } from "thirdweb/chains";
import { TabButtons } from "@/components/ui/tab-buttons";
import { LeftSection } from "../components/LeftSection";
import { RightSection } from "../components/RightSection";
import type { BridgeComponentsPlaygroundOptions } from "../components/types";

const defaultOptions: BridgeComponentsPlaygroundOptions = {
integrationType: "react",
payOptions: {
buyTokenAddress: undefined,
buyTokenAmount: "0.01",
Expand All @@ -29,20 +32,74 @@ const defaultOptions: BridgeComponentsPlaygroundOptions = {
},
};

export function CheckoutPlayground() {
const [options, setOptions] =
useState<BridgeComponentsPlaygroundOptions>(defaultOptions);
function updatePageUrl(
tab: BridgeComponentsPlaygroundOptions["integrationType"],
) {
const url = new URL(window.location.href);
if (tab === defaultOptions.integrationType) {
url.searchParams.delete("tab");
} else {
url.searchParams.set("tab", tab || "");
}

window.history.replaceState({}, "", url.toString());
}

export function CheckoutPlayground(props: { defaultTab?: "iframe" | "react" }) {
const { theme } = useTheme();

const [options, setOptions] = useState<BridgeComponentsPlaygroundOptions>(
() => ({
...defaultOptions,
integrationType: props.defaultTab || defaultOptions.integrationType,
}),
);

// Change theme on global theme change
useEffect(() => {
setOptions((prev) => ({
...prev,
theme: {
...prev.theme,
type: theme === "dark" ? "dark" : "light",
},
}));
}, [theme]);

useEffect(() => {
updatePageUrl(options.integrationType);
}, [options.integrationType]);

return (
<div className="relative flex flex-col-reverse gap-6 xl:min-h-[900px] xl:flex-row xl:gap-6">
<div className="grow border-b pb-10 xl:mb-0 xl:border-r xl:border-b-0 xl:pr-6">
<LeftSection
widget="checkout"
options={options}
setOptions={setOptions}
/>
<div>
<TabButtons
tabs={[
{
name: "React",
onClick: () => setOptions({ ...options, integrationType: "react" }),
isActive: options.integrationType === "react",
},
{
name: "Iframe",
onClick: () =>
setOptions({ ...options, integrationType: "iframe" }),
isActive: options.integrationType === "iframe",
},
]}
/>

<div className="h-6" />

<div className="relative flex flex-col-reverse gap-6 xl:min-h-[900px] xl:flex-row xl:gap-6">
<div className="grow border-b pb-10 xl:mb-0 xl:border-r xl:border-b-0 xl:pr-6">
<LeftSection
widget="checkout"
options={options}
setOptions={setOptions}
/>
</div>
<RightSection widget="checkout" options={options} />
</div>
<RightSection widget="checkout" options={options} />
</div>
);
}
21 changes: 18 additions & 3 deletions apps/playground-web/src/app/bridge/checkout-widget/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ const description =
const ogDescription =
"Accept fiat or crypto payments on any chain—direct to your wallet. Instant checkout, webhook support, and full control over post-sale actions.";

const validTabs = ["iframe", "react"] as const;
type ValidTabs = (typeof validTabs)[number];

export const metadata = createMetadata({
description: ogDescription,
title,
Expand All @@ -19,16 +22,28 @@ export const metadata = createMetadata({
},
});

export default function Page() {
export default async function Page(props: {
searchParams: Promise<{
tab?: string | undefined | string[];
}>;
}) {
const searchParams = await props.searchParams;
const tab =
typeof searchParams.tab === "string" ? searchParams.tab : undefined;

const validTab = validTabs.includes(tab as ValidTabs)
? (tab as ValidTabs)
: undefined;

return (
<ThirdwebProvider>
<PageLayout
icon={CreditCardIcon}
title={title}
description={description}
docsLink="https://portal.thirdweb.com/references/typescript/v5/CheckoutWidget?utm_source=playground"
docsLink="https://portal.thirdweb.com/bridge/checkout-widget?utm_source=playground"
>
<CheckoutPlayground />
<CheckoutPlayground defaultTab={validTab} />
</PageLayout>
</ThirdwebProvider>
);
Expand Down
24 changes: 22 additions & 2 deletions apps/playground-web/src/app/bridge/components/CodeGen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
stringifyImports,
stringifyProps,
} from "../../../lib/code-gen";
import { buildCheckoutIframeUrl } from "./buildCheckoutIframeUrl";
import type { BridgeComponentsPlaygroundOptions } from "./types";

const CodeClient = lazy(() =>
Expand All @@ -25,19 +26,38 @@ export function CodeGen(props: {
widget: "buy" | "checkout" | "transaction";
options: BridgeComponentsPlaygroundOptions;
}) {
const isIframe =
props.widget === "checkout" && props.options.integrationType === "iframe";

return (
<div className="flex w-full grow flex-col">
<Suspense fallback={<CodeLoading />}>
<CodeClient
className="grow"
code={getCode(props.widget, props.options)}
lang="tsx"
code={
isIframe
? getIframeCode(props.options)
: getCode(props.widget, props.options)
}
lang={isIframe ? "html" : "tsx"}
/>
</Suspense>
</div>
);
}

function getIframeCode(options: BridgeComponentsPlaygroundOptions) {
const src = buildCheckoutIframeUrl(options);

return `\
<iframe
src="${src}"
height="700px"
width="100%"
style="border: 0;"
/>`;
}

function getCode(
widget: "buy" | "checkout" | "transaction",
options: BridgeComponentsPlaygroundOptions,
Expand Down
24 changes: 14 additions & 10 deletions apps/playground-web/src/app/bridge/components/LeftSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -484,16 +484,20 @@ export function LeftSection(props: {

<div className="h-6" />

{/* Colors */}
<ColorFormGroup
onChange={(newTheme) => {
setOptions((v) => ({
...v,
theme: newTheme,
}));
}}
theme={options.theme}
/>
{/* Colors - disabled for iframe */}
{!(
props.widget === "checkout" && options.integrationType === "iframe"
) && (
<ColorFormGroup
onChange={(newTheme) => {
setOptions((v) => ({
...v,
theme: newTheme,
}));
}}
theme={options.theme}
/>
)}

<div className="my-4 flex items-center gap-2">
<Checkbox
Expand Down
18 changes: 17 additions & 1 deletion apps/playground-web/src/app/bridge/components/RightSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
import { Button } from "../../../components/ui/button";
import { THIRDWEB_CLIENT } from "../../../lib/client";
import { cn } from "../../../lib/utils";
import { buildCheckoutIframeUrl } from "./buildCheckoutIframeUrl";
import { CodeGen } from "./CodeGen";
import type { BridgeComponentsPlaygroundOptions } from "./types";

Expand Down Expand Up @@ -156,7 +157,22 @@ export function RightSection(props: {
>
<BackgroundPattern />

{previewTab === "ui" && embed}
{previewTab === "ui" &&
(props.widget === "checkout" &&
props.options.integrationType === "iframe" ? (
<iframe
src={buildCheckoutIframeUrl(props.options)}
height="700px"
width="100%"
title="Checkout Widget"
className="fade-in-0 animate-in rounded-xl duration-500"
style={{
border: "0",
}}
/>
) : (
embed
))}

{previewTab === "code" && (
<CodeGen widget={props.widget} options={props.options} />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import type { BridgeComponentsPlaygroundOptions } from "./types";

const CHECKOUT_WIDGET_IFRAME_BASE_URL =
"https://thirdweb.com/bridge/checkout-widget";

export function buildCheckoutIframeUrl(
options: BridgeComponentsPlaygroundOptions,
) {
const url = new URL(CHECKOUT_WIDGET_IFRAME_BASE_URL);

// Required params
url.searchParams.set("chain", String(options.payOptions.buyTokenChain.id));
url.searchParams.set("amount", options.payOptions.buyTokenAmount);
url.searchParams.set("seller", options.payOptions.sellerAddress);

// Token address (optional - if not set, native token is used)
if (options.payOptions.buyTokenAddress) {
url.searchParams.set("tokenAddress", options.payOptions.buyTokenAddress);
}

// Theme (only add if light, dark is default)
if (options.theme.type === "light") {
url.searchParams.set("theme", "light");
}

// Currency (only add if not USD, USD is default)
if (options.payOptions.currency && options.payOptions.currency !== "USD") {
url.searchParams.set("currency", options.payOptions.currency);
}

// Branding
if (options.payOptions.showThirdwebBranding === false) {
url.searchParams.set("showThirdwebBranding", "false");
}

// Product info
if (options.payOptions.title) {
url.searchParams.set("title", options.payOptions.title);
}

if (options.payOptions.description) {
url.searchParams.set("description", options.payOptions.description);
}

if (options.payOptions.image) {
url.searchParams.set("image", options.payOptions.image);
}

if (options.payOptions.buttonLabel) {
url.searchParams.set("buttonLabel", options.payOptions.buttonLabel);
}

// Payment methods
if (
options.payOptions.paymentMethods &&
options.payOptions.paymentMethods.length === 1
) {
url.searchParams.set(
"paymentMethods",
options.payOptions.paymentMethods[0],
);
}

return url.toString();
}
1 change: 1 addition & 0 deletions apps/playground-web/src/app/bridge/components/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const CURRENCIES = [
type SupportedFiatCurrency = (typeof CURRENCIES)[number];

export type BridgeComponentsPlaygroundOptions = {
integrationType?: "iframe" | "react";
theme: {
type: "dark" | "light";
darkColorOverrides: ThemeOverrides["colors"];
Expand Down
Loading
Loading