From 805b838fd3c2a243b5d530e4388a482ba1c58847 Mon Sep 17 00:00:00 2001 From: MananTank Date: Tue, 30 Dec 2025 11:17:26 +0000 Subject: [PATCH] [MNY-346] Playground: Add CheckoutWidget iframe (#8591) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ## PR-Codex overview This PR introduces support for customizable payment methods in the `checkout` widget, allowing users to select between "crypto" and "card" options. It also refines the integration type handling and enhances the iframe functionality for the checkout experience. ### Detailed summary - Added `integrationType` option to `BridgeComponentsPlaygroundOptions`. - Introduced `paymentMethods` parsing in `page.tsx` for checkout widget. - Updated `LeftSection` to conditionally render `ColorFormGroup`. - Enhanced `CheckoutWidgetEmbed` to accept `paymentMethods`. - Modified `RightSection` to support iframe rendering for checkout. - Created `buildCheckoutIframeUrl` function to generate URLs with payment methods. - Updated `CodeGen` to handle iframe code generation. - Added documentation for payment methods in `iframe` page. - Adjusted `CheckoutPlayground` to manage integration types and theme changes. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` ## Summary by CodeRabbit * **New Features** * Checkout widget: optional paymentMethods filter (crypto or card) and iframe integration. * Playground: tab-based switcher (React / Iframe), URL state persistence, and theme-aware preview. * Preview/code generation: iframe-specific preview and HTML snippet export for iframe integration. * **Documentation** * Added "Payment Methods" docs for iframe integration with examples. ✏️ Tip: You can customize this high-level summary in your review settings. --- .../CheckoutWidgetEmbed.client.tsx | 3 + .../src/app/bridge/checkout-widget/page.tsx | 9 +++ .../checkout-widget/CheckoutPlayground.tsx | 81 ++++++++++++++++--- .../src/app/bridge/checkout-widget/page.tsx | 21 ++++- .../src/app/bridge/components/CodeGen.tsx | 24 +++++- .../src/app/bridge/components/LeftSection.tsx | 24 +++--- .../app/bridge/components/RightSection.tsx | 18 ++++- .../components/buildCheckoutIframeUrl.ts | 65 +++++++++++++++ .../src/app/bridge/components/types.ts | 1 + .../bridge/checkout-widget/iframe/page.mdx | 23 ++++++ 10 files changed, 241 insertions(+), 28 deletions(-) create mode 100644 apps/playground-web/src/app/bridge/components/buildCheckoutIframeUrl.ts diff --git a/apps/dashboard/src/app/bridge/checkout-widget/CheckoutWidgetEmbed.client.tsx b/apps/dashboard/src/app/bridge/checkout-widget/CheckoutWidgetEmbed.client.tsx index 57bc6194fab..e78f6e01b43 100644 --- a/apps/dashboard/src/app/bridge/checkout-widget/CheckoutWidgetEmbed.client.tsx +++ b/apps/dashboard/src/app/bridge/checkout-widget/CheckoutWidgetEmbed.client.tsx @@ -34,6 +34,7 @@ export function CheckoutWidgetEmbed({ showThirdwebBranding, theme, currency, + paymentMethods, }: { chainId: number; amount: string; @@ -48,6 +49,7 @@ export function CheckoutWidgetEmbed({ showThirdwebBranding?: boolean; theme: "light" | "dark"; currency?: SupportedFiatCurrency; + paymentMethods?: ("crypto" | "card")[]; }) { const client = useMemo( () => @@ -79,6 +81,7 @@ export function CheckoutWidgetEmbed({ showThirdwebBranding={showThirdwebBranding} theme={theme} currency={currency} + paymentMethods={paymentMethods} connectOptions={{ wallets: bridgeWallets, appMetadata, diff --git a/apps/dashboard/src/app/bridge/checkout-widget/page.tsx b/apps/dashboard/src/app/bridge/checkout-widget/page.tsx index 57e6792ea31..6a4b26da661 100644 --- a/apps/dashboard/src/app/bridge/checkout-widget/page.tsx +++ b/apps/dashboard/src/app/bridge/checkout-widget/page.tsx @@ -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 ( @@ -125,6 +133,7 @@ export default async function Page(props: { showThirdwebBranding={showThirdwebBranding} theme={theme} currency={currency} + paymentMethods={paymentMethods} /> diff --git a/apps/playground-web/src/app/bridge/checkout-widget/CheckoutPlayground.tsx b/apps/playground-web/src/app/bridge/checkout-widget/CheckoutPlayground.tsx index b270b575265..f6b2b2ee7c6 100644 --- a/apps/playground-web/src/app/bridge/checkout-widget/CheckoutPlayground.tsx +++ b/apps/playground-web/src/app/bridge/checkout-widget/CheckoutPlayground.tsx @@ -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", @@ -29,20 +32,74 @@ const defaultOptions: BridgeComponentsPlaygroundOptions = { }, }; -export function CheckoutPlayground() { - const [options, setOptions] = - useState(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( + () => ({ + ...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 ( -
-
- +
+ setOptions({ ...options, integrationType: "react" }), + isActive: options.integrationType === "react", + }, + { + name: "Iframe", + onClick: () => + setOptions({ ...options, integrationType: "iframe" }), + isActive: options.integrationType === "iframe", + }, + ]} + /> + +
+ +
+
+ +
+
-
); } diff --git a/apps/playground-web/src/app/bridge/checkout-widget/page.tsx b/apps/playground-web/src/app/bridge/checkout-widget/page.tsx index b336e64c80e..decdedc53dc 100644 --- a/apps/playground-web/src/app/bridge/checkout-widget/page.tsx +++ b/apps/playground-web/src/app/bridge/checkout-widget/page.tsx @@ -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, @@ -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 ( - + ); diff --git a/apps/playground-web/src/app/bridge/components/CodeGen.tsx b/apps/playground-web/src/app/bridge/components/CodeGen.tsx index 14366a814a4..0578f2fa115 100644 --- a/apps/playground-web/src/app/bridge/components/CodeGen.tsx +++ b/apps/playground-web/src/app/bridge/components/CodeGen.tsx @@ -5,6 +5,7 @@ import { stringifyImports, stringifyProps, } from "../../../lib/code-gen"; +import { buildCheckoutIframeUrl } from "./buildCheckoutIframeUrl"; import type { BridgeComponentsPlaygroundOptions } from "./types"; const CodeClient = lazy(() => @@ -25,19 +26,38 @@ export function CodeGen(props: { widget: "buy" | "checkout" | "transaction"; options: BridgeComponentsPlaygroundOptions; }) { + const isIframe = + props.widget === "checkout" && props.options.integrationType === "iframe"; + return (
}>
); } +function getIframeCode(options: BridgeComponentsPlaygroundOptions) { + const src = buildCheckoutIframeUrl(options); + + return `\ +