Skip to content
Merged
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
18 changes: 18 additions & 0 deletions apps/dashboard/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
3 changes: 3 additions & 0 deletions apps/dashboard/src/@/constants/public-envs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,25 @@ export function BridgeProviders({
</ThirdwebProvider>
);
}

export function BridgeProvidersLite({
children,
forcedTheme,
}: {
children: React.ReactNode;
forcedTheme?: string;
}) {
return (
<ThirdwebProvider>
<ThemeProvider
attribute="class"
defaultTheme="dark"
disableTransitionOnChange
enableSystem={false}
forcedTheme={forcedTheme}
>
{children}
</ThemeProvider>
</ThirdwebProvider>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -82,10 +69,6 @@ export function CheckoutWidgetEmbed({
theme={theme}
currency={currency}
paymentMethods={paymentMethods}
connectOptions={{
wallets: bridgeWallets,
appMetadata,
}}
onSuccess={() => {
sendMessageToParent({
source: "checkout-widget",
Expand Down
31 changes: 5 additions & 26 deletions apps/dashboard/src/app/bridge/checkout-widget/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -79,7 +78,7 @@ export default async function Page(props: {
// Validate required params
if (!chainId || !amount || !seller) {
return (
<Providers theme={theme}>
<BridgeProvidersLite forcedTheme={theme}>
<div className="flex min-h-screen items-center justify-center bg-background px-4 py-8">
<div className="w-full max-w-lg rounded-xl border bg-card p-6 shadow-xl">
<div className="p-2.5 inline-flex rounded-full bg-background mb-4 border">
Expand Down Expand Up @@ -112,12 +111,12 @@ export default async function Page(props: {
</ul>
</div>
</div>
</Providers>
</BridgeProvidersLite>
);
}

return (
<Providers theme={theme}>
<BridgeProvidersLite forcedTheme={theme}>
<div className="flex min-h-screen items-center justify-center bg-background px-4 py-8">
<CheckoutWidgetEmbed
chainId={chainId}
Expand All @@ -136,26 +135,6 @@ export default async function Page(props: {
paymentMethods={paymentMethods}
/>
</div>
</Providers>
);
}

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 (
<BridgeProviders
clientId={NEXT_PUBLIC_CHECKOUT_IFRAME_CLIENT_ID}
forcedTheme={theme}
>
{children}
</BridgeProviders>
</BridgeProvidersLite>
);
}
104 changes: 104 additions & 0 deletions apps/dashboard/src/app/bridge/swap-widget/SwapWidgetEmbed.client.tsx
Original file line number Diff line number Diff line change
@@ -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,
}),
[],
);
Comment on lines +30 to +38
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Missing validation for required environment variable.

NEXT_PUBLIC_SWAP_IFRAME_CLIENT_ID is used without validation at line 33. If this environment variable is undefined, the swap widget will fail to initialize properly, likely resulting in authentication or connection errors that are difficult to debug.

🔎 Recommended validation pattern

Add validation at component initialization:

 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;
 }) {
+  if (!NEXT_PUBLIC_SWAP_IFRAME_CLIENT_ID) {
+    throw new Error(
+      "NEXT_PUBLIC_SWAP_IFRAME_CLIENT_ID environment variable is required"
+    );
+  }
+
   const client = useMemo(
     () =>
       getConfiguredThirdwebClient({
         clientId: NEXT_PUBLIC_SWAP_IFRAME_CLIENT_ID,
         secretKey: undefined,
         teamId: undefined,
       }),
     [],
   );

Alternatively, render an error UI similar to the invalid configuration handling in checkout-widget/page.tsx (lines 80-115).

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In @apps/dashboard/src/app/bridge/swap-widget/SwapWidgetEmbed.client.tsx around
lines 30-38, The code calls getConfiguredThirdwebClient with
NEXT_PUBLIC_SWAP_IFRAME_CLIENT_ID without validating it; add a check at
component initialization (before useMemo or inside it) to verify
NEXT_PUBLIC_SWAP_IFRAME_CLIENT_ID is defined and non-empty, and if not either
throw a clear error or render an error fallback UI (follow the invalid
configuration handling pattern used in checkout-widget/page.tsx lines ~80-115);
update the useMemo call that creates the client (getConfiguredThirdwebClient) to
only run when the validated clientId exists and include a descriptive log or
error message to aid debugging.


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 (
<SwapWidget
className="shadow-xl"
client={client}
prefill={prefill}
showThirdwebBranding={showThirdwebBranding}
theme={theme}
currency={currency}
onSuccess={() => {
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);
}
}
27 changes: 27 additions & 0 deletions apps/dashboard/src/app/bridge/swap-widget/layout.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<html lang="en" suppressHydrationWarning>
<body
className={cn(
"min-h-dvh bg-background font-sans antialiased flex flex-col",
fontSans.variable,
)}
>
{children}
</body>
</html>
);
}
83 changes: 83 additions & 0 deletions apps/dashboard/src/app/bridge/swap-widget/page.tsx
Original file line number Diff line number Diff line change
@@ -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<SearchParams>;
}) {
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 (
<BridgeProvidersLite forcedTheme={theme}>
<div className="flex min-h-screen items-center justify-center bg-background px-4 py-8">
<SwapWidgetEmbed
buyChainId={buyChainId}
buyTokenAddress={buyTokenAddress}
buyAmount={buyAmount}
sellChainId={sellChainId}
sellTokenAddress={sellTokenAddress}
sellAmount={sellAmount}
showThirdwebBranding={showThirdwebBranding}
theme={theme}
currency={currency}
/>
</div>
</BridgeProvidersLite>
);
}
Loading