From 9f265bf734f8ebfad5d0667670a9b2077aae6d7d Mon Sep 17 00:00:00 2001 From: MananTank Date: Mon, 5 Jan 2026 20:26:31 +0000 Subject: [PATCH] [MNY-344] Dashboard: Add swap-widget iframe (#8603) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ## PR-Codex overview This PR introduces a new `NEXT_PUBLIC_SWAP_IFRAME_CLIENT_ID` constant and enhances the swap functionality with a `SwapWidgetEmbed` component, while refactoring the existing `Providers` component to `BridgeProvidersLite`. It also adds necessary headers for security and updates layout components. ### Detailed summary - Added `NEXT_PUBLIC_SWAP_IFRAME_CLIENT_ID` constant in `public-envs.ts`. - Updated `next.config.ts` to include security headers for swap widget routes. - Introduced `BridgeProvidersLite` component in `Providers.client.tsx`. - Created `SwapWidgetEmbed` component for swap functionality. - Refactored `page.tsx` to utilize `BridgeProvidersLite` and `SwapWidgetEmbed`. - Removed the old `Providers` component logic. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` ## Summary by CodeRabbit * **New Features** * Added a cross‑chain swap widget with page, embed component and layout, URL-prefill support, theming, and fiat currency options. * Added a lightweight provider wrapper for embed usage. * **Changes** * Checkout widget no longer auto-connects preconfigured wallets; provider usage unified to the lightweight wrapper. * **Chores** * Added CSP headers for swap widget routes. * Introduced public env var for swap iframe client ID. ✏️ Tip: You can customize this high-level summary in your review settings. --- apps/dashboard/next.config.ts | 18 +++ apps/dashboard/src/@/constants/public-envs.ts | 3 + .../components/client/Providers.client.tsx | 22 ++++ .../CheckoutWidgetEmbed.client.tsx | 17 --- .../src/app/bridge/checkout-widget/page.tsx | 31 +----- .../swap-widget/SwapWidgetEmbed.client.tsx | 104 ++++++++++++++++++ .../src/app/bridge/swap-widget/layout.tsx | 27 +++++ .../src/app/bridge/swap-widget/page.tsx | 83 ++++++++++++++ 8 files changed, 262 insertions(+), 43 deletions(-) create mode 100644 apps/dashboard/src/app/bridge/swap-widget/SwapWidgetEmbed.client.tsx create mode 100644 apps/dashboard/src/app/bridge/swap-widget/layout.tsx create mode 100644 apps/dashboard/src/app/bridge/swap-widget/page.tsx diff --git a/apps/dashboard/next.config.ts b/apps/dashboard/next.config.ts index 78c0106cccf..ec05e6af612 100644 --- a/apps/dashboard/next.config.ts +++ b/apps/dashboard/next.config.ts @@ -179,6 +179,24 @@ const baseNextConfig: NextConfig = { ], source: "/bridge/checkout-widget/:path*", }, + { + headers: [ + { + key: "Content-Security-Policy", + value: EmbedContentSecurityPolicy.replace(/\s{2,}/g, " ").trim(), + }, + ], + source: "/bridge/swap-widget", + }, + { + headers: [ + { + key: "Content-Security-Policy", + value: EmbedContentSecurityPolicy.replace(/\s{2,}/g, " ").trim(), + }, + ], + source: "/bridge/swap-widget/:path*", + }, ]; }, images: { diff --git a/apps/dashboard/src/@/constants/public-envs.ts b/apps/dashboard/src/@/constants/public-envs.ts index ca06822515b..41cbb58c13e 100644 --- a/apps/dashboard/src/@/constants/public-envs.ts +++ b/apps/dashboard/src/@/constants/public-envs.ts @@ -47,3 +47,6 @@ export const NEXT_PUBLIC_BRIDGE_IFRAME_CLIENT_ID = export const NEXT_PUBLIC_CHECKOUT_IFRAME_CLIENT_ID = process.env.NEXT_PUBLIC_CHECKOUT_IFRAME_CLIENT_ID; + +export const NEXT_PUBLIC_SWAP_IFRAME_CLIENT_ID = + process.env.NEXT_PUBLIC_SWAP_IFRAME_CLIENT_ID; diff --git a/apps/dashboard/src/app/bridge/(general)/components/client/Providers.client.tsx b/apps/dashboard/src/app/bridge/(general)/components/client/Providers.client.tsx index e92f44fb707..3c6a34ee4c8 100644 --- a/apps/dashboard/src/app/bridge/(general)/components/client/Providers.client.tsx +++ b/apps/dashboard/src/app/bridge/(general)/components/client/Providers.client.tsx @@ -40,3 +40,25 @@ export function BridgeProviders({ ); } + +export function BridgeProvidersLite({ + children, + forcedTheme, +}: { + children: React.ReactNode; + forcedTheme?: string; +}) { + return ( + + + {children} + + + ); +} 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 e78f6e01b43..bcc1b47781d 100644 --- a/apps/dashboard/src/app/bridge/checkout-widget/CheckoutWidgetEmbed.client.tsx +++ b/apps/dashboard/src/app/bridge/checkout-widget/CheckoutWidgetEmbed.client.tsx @@ -4,22 +4,9 @@ import { useMemo } from "react"; import type { Address } from "thirdweb"; import { defineChain } from "thirdweb"; import { CheckoutWidget, type SupportedFiatCurrency } from "thirdweb/react"; -import { createWallet } from "thirdweb/wallets"; -import { appMetadata } from "@/constants/connect"; import { NEXT_PUBLIC_CHECKOUT_IFRAME_CLIENT_ID } from "@/constants/public-envs"; import { getConfiguredThirdwebClient } from "@/constants/thirdweb.server"; -const bridgeWallets = [ - createWallet("io.metamask"), - createWallet("com.coinbase.wallet", { - appMetadata, - }), - createWallet("me.rainbow"), - createWallet("io.rabby"), - createWallet("io.zerion.wallet"), - createWallet("com.okex.wallet"), -]; - export function CheckoutWidgetEmbed({ chainId, amount, @@ -82,10 +69,6 @@ export function CheckoutWidgetEmbed({ theme={theme} currency={currency} paymentMethods={paymentMethods} - connectOptions={{ - wallets: bridgeWallets, - appMetadata, - }} onSuccess={() => { sendMessageToParent({ source: "checkout-widget", diff --git a/apps/dashboard/src/app/bridge/checkout-widget/page.tsx b/apps/dashboard/src/app/bridge/checkout-widget/page.tsx index 6a4b26da661..db084e59eb3 100644 --- a/apps/dashboard/src/app/bridge/checkout-widget/page.tsx +++ b/apps/dashboard/src/app/bridge/checkout-widget/page.tsx @@ -3,14 +3,13 @@ import "@workspace/ui/global.css"; import { InlineCode } from "@workspace/ui/components/code/inline-code"; import { AlertTriangleIcon } from "lucide-react"; import type { SupportedFiatCurrency } from "thirdweb/react"; -import { NEXT_PUBLIC_CHECKOUT_IFRAME_CLIENT_ID } from "@/constants/public-envs"; import { isValidCurrency } from "../_common/isValidCurrency"; import { onlyAddress, onlyNumber, parseQueryParams, } from "../_common/parseQueryParams"; -import { BridgeProviders } from "../(general)/components/client/Providers.client"; +import { BridgeProvidersLite } from "../(general)/components/client/Providers.client"; import { CheckoutWidgetEmbed } from "./CheckoutWidgetEmbed.client"; const title = "thirdweb Checkout: Accept Crypto & Fiat Payments"; @@ -79,7 +78,7 @@ export default async function Page(props: { // Validate required params if (!chainId || !amount || !seller) { return ( - +
@@ -112,12 +111,12 @@ export default async function Page(props: {
- + ); } return ( - +
-
- ); -} - -function Providers({ - children, - theme, -}: { - children: React.ReactNode; - theme: string; -}) { - if (!NEXT_PUBLIC_CHECKOUT_IFRAME_CLIENT_ID) { - throw new Error("NEXT_PUBLIC_CHECKOUT_IFRAME_CLIENT_ID is not set"); - } - return ( - - {children} - + ); } diff --git a/apps/dashboard/src/app/bridge/swap-widget/SwapWidgetEmbed.client.tsx b/apps/dashboard/src/app/bridge/swap-widget/SwapWidgetEmbed.client.tsx new file mode 100644 index 00000000000..a8836d29550 --- /dev/null +++ b/apps/dashboard/src/app/bridge/swap-widget/SwapWidgetEmbed.client.tsx @@ -0,0 +1,104 @@ +"use client"; + +import { useMemo } from "react"; +import type { Address } from "thirdweb"; +import { type SupportedFiatCurrency, SwapWidget } from "thirdweb/react"; +import { NEXT_PUBLIC_SWAP_IFRAME_CLIENT_ID } from "@/constants/public-envs"; +import { getConfiguredThirdwebClient } from "@/constants/thirdweb.server"; + +export function SwapWidgetEmbed({ + buyChainId, + buyTokenAddress, + buyAmount, + sellChainId, + sellTokenAddress, + sellAmount, + showThirdwebBranding, + theme, + currency, +}: { + buyChainId?: number; + buyTokenAddress?: Address; + buyAmount?: string; + sellChainId?: number; + sellTokenAddress?: Address; + sellAmount?: string; + showThirdwebBranding?: boolean; + theme: "light" | "dark"; + currency?: SupportedFiatCurrency; +}) { + const client = useMemo( + () => + getConfiguredThirdwebClient({ + clientId: NEXT_PUBLIC_SWAP_IFRAME_CLIENT_ID, + secretKey: undefined, + teamId: undefined, + }), + [], + ); + + const prefill = useMemo(() => { + const result: { + buyToken?: { chainId: number; tokenAddress?: string; amount?: string }; + sellToken?: { chainId: number; tokenAddress?: string; amount?: string }; + } = {}; + + if (buyChainId) { + result.buyToken = { + chainId: buyChainId, + tokenAddress: buyTokenAddress, + amount: buyAmount, + }; + } + + if (sellChainId) { + result.sellToken = { + chainId: sellChainId, + tokenAddress: sellTokenAddress, + amount: sellAmount, + }; + } + + return Object.keys(result).length > 0 ? result : undefined; + }, [ + buyChainId, + buyTokenAddress, + buyAmount, + sellChainId, + sellTokenAddress, + sellAmount, + ]); + + return ( + { + sendMessageToParent({ + source: "swap-widget", + type: "success", + }); + }} + onError={(error) => { + sendMessageToParent({ + source: "swap-widget", + type: "error", + message: error.message, + }); + }} + /> + ); +} + +function sendMessageToParent(content: object) { + try { + window.parent.postMessage(content, "*"); + } catch (error) { + console.error("Failed to send post message to parent window"); + console.error(error); + } +} diff --git a/apps/dashboard/src/app/bridge/swap-widget/layout.tsx b/apps/dashboard/src/app/bridge/swap-widget/layout.tsx new file mode 100644 index 00000000000..2993635a556 --- /dev/null +++ b/apps/dashboard/src/app/bridge/swap-widget/layout.tsx @@ -0,0 +1,27 @@ +import { Inter } from "next/font/google"; +import { cn } from "@/lib/utils"; + +const fontSans = Inter({ + display: "swap", + subsets: ["latin"], + variable: "--font-sans", +}); + +export default function SwapWidgetLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + {children} + + + ); +} diff --git a/apps/dashboard/src/app/bridge/swap-widget/page.tsx b/apps/dashboard/src/app/bridge/swap-widget/page.tsx new file mode 100644 index 00000000000..622f57f8d02 --- /dev/null +++ b/apps/dashboard/src/app/bridge/swap-widget/page.tsx @@ -0,0 +1,83 @@ +import type { Metadata } from "next"; +import "@workspace/ui/global.css"; +import type { SupportedFiatCurrency } from "thirdweb/react"; +import { isValidCurrency } from "../_common/isValidCurrency"; +import { + onlyAddress, + onlyNumber, + parseQueryParams, +} from "../_common/parseQueryParams"; +import { BridgeProvidersLite } from "../(general)/components/client/Providers.client"; +import { SwapWidgetEmbed } from "./SwapWidgetEmbed.client"; + +const title = "thirdweb Swap: Cross-Chain Token Swaps"; +const description = + "Swap tokens across any chain with the best rates. Cross-chain swaps made simple with thirdweb."; + +export const metadata: Metadata = { + description, + openGraph: { + description, + title, + }, + title, +}; + +type SearchParams = { + [key: string]: string | string[] | undefined; +}; + +export default async function Page(props: { + searchParams: Promise; +}) { + const searchParams = await props.searchParams; + + // Buy token params + const buyChainId = parseQueryParams(searchParams.buyChain, onlyNumber); + const buyTokenAddress = parseQueryParams( + searchParams.buyTokenAddress, + onlyAddress, + ); + const buyAmount = parseQueryParams(searchParams.buyAmount, (v) => v); + + // Sell token params + const sellChainId = parseQueryParams(searchParams.sellChain, onlyNumber); + const sellTokenAddress = parseQueryParams( + searchParams.sellTokenAddress, + onlyAddress, + ); + const sellAmount = parseQueryParams(searchParams.sellAmount, (v) => v); + + // Optional params + const showThirdwebBranding = parseQueryParams( + searchParams.showThirdwebBranding, + (v) => v !== "false", + ); + + const theme = + parseQueryParams(searchParams.theme, (v) => + v === "light" ? "light" : "dark", + ) || "dark"; + + const currency = parseQueryParams(searchParams.currency, (v) => + isValidCurrency(v) ? (v as SupportedFiatCurrency) : undefined, + ); + + return ( + +
+ +
+
+ ); +}