Skip to content

Commit 9f265bf

Browse files
committed
[MNY-344] Dashboard: Add swap-widget iframe (#8603)
<!-- ## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes" If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000): ## Notes for the reviewer Anything important to call out? Be sure to also clarify these in your comments. ## How to test Unit tests, playground, etc. --> <!-- start pr-codex --> --- ## 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}` <!-- end pr-codex --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## 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. <sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent e5519ea commit 9f265bf

File tree

8 files changed

+262
-43
lines changed

8 files changed

+262
-43
lines changed

apps/dashboard/next.config.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,24 @@ const baseNextConfig: NextConfig = {
179179
],
180180
source: "/bridge/checkout-widget/:path*",
181181
},
182+
{
183+
headers: [
184+
{
185+
key: "Content-Security-Policy",
186+
value: EmbedContentSecurityPolicy.replace(/\s{2,}/g, " ").trim(),
187+
},
188+
],
189+
source: "/bridge/swap-widget",
190+
},
191+
{
192+
headers: [
193+
{
194+
key: "Content-Security-Policy",
195+
value: EmbedContentSecurityPolicy.replace(/\s{2,}/g, " ").trim(),
196+
},
197+
],
198+
source: "/bridge/swap-widget/:path*",
199+
},
182200
];
183201
},
184202
images: {

apps/dashboard/src/@/constants/public-envs.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,6 @@ export const NEXT_PUBLIC_BRIDGE_IFRAME_CLIENT_ID =
4747

4848
export const NEXT_PUBLIC_CHECKOUT_IFRAME_CLIENT_ID =
4949
process.env.NEXT_PUBLIC_CHECKOUT_IFRAME_CLIENT_ID;
50+
51+
export const NEXT_PUBLIC_SWAP_IFRAME_CLIENT_ID =
52+
process.env.NEXT_PUBLIC_SWAP_IFRAME_CLIENT_ID;

apps/dashboard/src/app/bridge/(general)/components/client/Providers.client.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,25 @@ export function BridgeProviders({
4040
</ThirdwebProvider>
4141
);
4242
}
43+
44+
export function BridgeProvidersLite({
45+
children,
46+
forcedTheme,
47+
}: {
48+
children: React.ReactNode;
49+
forcedTheme?: string;
50+
}) {
51+
return (
52+
<ThirdwebProvider>
53+
<ThemeProvider
54+
attribute="class"
55+
defaultTheme="dark"
56+
disableTransitionOnChange
57+
enableSystem={false}
58+
forcedTheme={forcedTheme}
59+
>
60+
{children}
61+
</ThemeProvider>
62+
</ThirdwebProvider>
63+
);
64+
}

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

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,9 @@ import { useMemo } from "react";
44
import type { Address } from "thirdweb";
55
import { defineChain } from "thirdweb";
66
import { CheckoutWidget, type SupportedFiatCurrency } from "thirdweb/react";
7-
import { createWallet } from "thirdweb/wallets";
8-
import { appMetadata } from "@/constants/connect";
97
import { NEXT_PUBLIC_CHECKOUT_IFRAME_CLIENT_ID } from "@/constants/public-envs";
108
import { getConfiguredThirdwebClient } from "@/constants/thirdweb.server";
119

12-
const bridgeWallets = [
13-
createWallet("io.metamask"),
14-
createWallet("com.coinbase.wallet", {
15-
appMetadata,
16-
}),
17-
createWallet("me.rainbow"),
18-
createWallet("io.rabby"),
19-
createWallet("io.zerion.wallet"),
20-
createWallet("com.okex.wallet"),
21-
];
22-
2310
export function CheckoutWidgetEmbed({
2411
chainId,
2512
amount,
@@ -82,10 +69,6 @@ export function CheckoutWidgetEmbed({
8269
theme={theme}
8370
currency={currency}
8471
paymentMethods={paymentMethods}
85-
connectOptions={{
86-
wallets: bridgeWallets,
87-
appMetadata,
88-
}}
8972
onSuccess={() => {
9073
sendMessageToParent({
9174
source: "checkout-widget",

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

Lines changed: 5 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,13 @@ import "@workspace/ui/global.css";
33
import { InlineCode } from "@workspace/ui/components/code/inline-code";
44
import { AlertTriangleIcon } from "lucide-react";
55
import type { SupportedFiatCurrency } from "thirdweb/react";
6-
import { NEXT_PUBLIC_CHECKOUT_IFRAME_CLIENT_ID } from "@/constants/public-envs";
76
import { isValidCurrency } from "../_common/isValidCurrency";
87
import {
98
onlyAddress,
109
onlyNumber,
1110
parseQueryParams,
1211
} from "../_common/parseQueryParams";
13-
import { BridgeProviders } from "../(general)/components/client/Providers.client";
12+
import { BridgeProvidersLite } from "../(general)/components/client/Providers.client";
1413
import { CheckoutWidgetEmbed } from "./CheckoutWidgetEmbed.client";
1514

1615
const title = "thirdweb Checkout: Accept Crypto & Fiat Payments";
@@ -79,7 +78,7 @@ export default async function Page(props: {
7978
// Validate required params
8079
if (!chainId || !amount || !seller) {
8180
return (
82-
<Providers theme={theme}>
81+
<BridgeProvidersLite forcedTheme={theme}>
8382
<div className="flex min-h-screen items-center justify-center bg-background px-4 py-8">
8483
<div className="w-full max-w-lg rounded-xl border bg-card p-6 shadow-xl">
8584
<div className="p-2.5 inline-flex rounded-full bg-background mb-4 border">
@@ -112,12 +111,12 @@ export default async function Page(props: {
112111
</ul>
113112
</div>
114113
</div>
115-
</Providers>
114+
</BridgeProvidersLite>
116115
);
117116
}
118117

119118
return (
120-
<Providers theme={theme}>
119+
<BridgeProvidersLite forcedTheme={theme}>
121120
<div className="flex min-h-screen items-center justify-center bg-background px-4 py-8">
122121
<CheckoutWidgetEmbed
123122
chainId={chainId}
@@ -136,26 +135,6 @@ export default async function Page(props: {
136135
paymentMethods={paymentMethods}
137136
/>
138137
</div>
139-
</Providers>
140-
);
141-
}
142-
143-
function Providers({
144-
children,
145-
theme,
146-
}: {
147-
children: React.ReactNode;
148-
theme: string;
149-
}) {
150-
if (!NEXT_PUBLIC_CHECKOUT_IFRAME_CLIENT_ID) {
151-
throw new Error("NEXT_PUBLIC_CHECKOUT_IFRAME_CLIENT_ID is not set");
152-
}
153-
return (
154-
<BridgeProviders
155-
clientId={NEXT_PUBLIC_CHECKOUT_IFRAME_CLIENT_ID}
156-
forcedTheme={theme}
157-
>
158-
{children}
159-
</BridgeProviders>
138+
</BridgeProvidersLite>
160139
);
161140
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
"use client";
2+
3+
import { useMemo } from "react";
4+
import type { Address } from "thirdweb";
5+
import { type SupportedFiatCurrency, SwapWidget } from "thirdweb/react";
6+
import { NEXT_PUBLIC_SWAP_IFRAME_CLIENT_ID } from "@/constants/public-envs";
7+
import { getConfiguredThirdwebClient } from "@/constants/thirdweb.server";
8+
9+
export function SwapWidgetEmbed({
10+
buyChainId,
11+
buyTokenAddress,
12+
buyAmount,
13+
sellChainId,
14+
sellTokenAddress,
15+
sellAmount,
16+
showThirdwebBranding,
17+
theme,
18+
currency,
19+
}: {
20+
buyChainId?: number;
21+
buyTokenAddress?: Address;
22+
buyAmount?: string;
23+
sellChainId?: number;
24+
sellTokenAddress?: Address;
25+
sellAmount?: string;
26+
showThirdwebBranding?: boolean;
27+
theme: "light" | "dark";
28+
currency?: SupportedFiatCurrency;
29+
}) {
30+
const client = useMemo(
31+
() =>
32+
getConfiguredThirdwebClient({
33+
clientId: NEXT_PUBLIC_SWAP_IFRAME_CLIENT_ID,
34+
secretKey: undefined,
35+
teamId: undefined,
36+
}),
37+
[],
38+
);
39+
40+
const prefill = useMemo(() => {
41+
const result: {
42+
buyToken?: { chainId: number; tokenAddress?: string; amount?: string };
43+
sellToken?: { chainId: number; tokenAddress?: string; amount?: string };
44+
} = {};
45+
46+
if (buyChainId) {
47+
result.buyToken = {
48+
chainId: buyChainId,
49+
tokenAddress: buyTokenAddress,
50+
amount: buyAmount,
51+
};
52+
}
53+
54+
if (sellChainId) {
55+
result.sellToken = {
56+
chainId: sellChainId,
57+
tokenAddress: sellTokenAddress,
58+
amount: sellAmount,
59+
};
60+
}
61+
62+
return Object.keys(result).length > 0 ? result : undefined;
63+
}, [
64+
buyChainId,
65+
buyTokenAddress,
66+
buyAmount,
67+
sellChainId,
68+
sellTokenAddress,
69+
sellAmount,
70+
]);
71+
72+
return (
73+
<SwapWidget
74+
className="shadow-xl"
75+
client={client}
76+
prefill={prefill}
77+
showThirdwebBranding={showThirdwebBranding}
78+
theme={theme}
79+
currency={currency}
80+
onSuccess={() => {
81+
sendMessageToParent({
82+
source: "swap-widget",
83+
type: "success",
84+
});
85+
}}
86+
onError={(error) => {
87+
sendMessageToParent({
88+
source: "swap-widget",
89+
type: "error",
90+
message: error.message,
91+
});
92+
}}
93+
/>
94+
);
95+
}
96+
97+
function sendMessageToParent(content: object) {
98+
try {
99+
window.parent.postMessage(content, "*");
100+
} catch (error) {
101+
console.error("Failed to send post message to parent window");
102+
console.error(error);
103+
}
104+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { Inter } from "next/font/google";
2+
import { cn } from "@/lib/utils";
3+
4+
const fontSans = Inter({
5+
display: "swap",
6+
subsets: ["latin"],
7+
variable: "--font-sans",
8+
});
9+
10+
export default function SwapWidgetLayout({
11+
children,
12+
}: {
13+
children: React.ReactNode;
14+
}) {
15+
return (
16+
<html lang="en" suppressHydrationWarning>
17+
<body
18+
className={cn(
19+
"min-h-dvh bg-background font-sans antialiased flex flex-col",
20+
fontSans.variable,
21+
)}
22+
>
23+
{children}
24+
</body>
25+
</html>
26+
);
27+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import type { Metadata } from "next";
2+
import "@workspace/ui/global.css";
3+
import type { SupportedFiatCurrency } from "thirdweb/react";
4+
import { isValidCurrency } from "../_common/isValidCurrency";
5+
import {
6+
onlyAddress,
7+
onlyNumber,
8+
parseQueryParams,
9+
} from "../_common/parseQueryParams";
10+
import { BridgeProvidersLite } from "../(general)/components/client/Providers.client";
11+
import { SwapWidgetEmbed } from "./SwapWidgetEmbed.client";
12+
13+
const title = "thirdweb Swap: Cross-Chain Token Swaps";
14+
const description =
15+
"Swap tokens across any chain with the best rates. Cross-chain swaps made simple with thirdweb.";
16+
17+
export const metadata: Metadata = {
18+
description,
19+
openGraph: {
20+
description,
21+
title,
22+
},
23+
title,
24+
};
25+
26+
type SearchParams = {
27+
[key: string]: string | string[] | undefined;
28+
};
29+
30+
export default async function Page(props: {
31+
searchParams: Promise<SearchParams>;
32+
}) {
33+
const searchParams = await props.searchParams;
34+
35+
// Buy token params
36+
const buyChainId = parseQueryParams(searchParams.buyChain, onlyNumber);
37+
const buyTokenAddress = parseQueryParams(
38+
searchParams.buyTokenAddress,
39+
onlyAddress,
40+
);
41+
const buyAmount = parseQueryParams(searchParams.buyAmount, (v) => v);
42+
43+
// Sell token params
44+
const sellChainId = parseQueryParams(searchParams.sellChain, onlyNumber);
45+
const sellTokenAddress = parseQueryParams(
46+
searchParams.sellTokenAddress,
47+
onlyAddress,
48+
);
49+
const sellAmount = parseQueryParams(searchParams.sellAmount, (v) => v);
50+
51+
// Optional params
52+
const showThirdwebBranding = parseQueryParams(
53+
searchParams.showThirdwebBranding,
54+
(v) => v !== "false",
55+
);
56+
57+
const theme =
58+
parseQueryParams(searchParams.theme, (v) =>
59+
v === "light" ? "light" : "dark",
60+
) || "dark";
61+
62+
const currency = parseQueryParams(searchParams.currency, (v) =>
63+
isValidCurrency(v) ? (v as SupportedFiatCurrency) : undefined,
64+
);
65+
66+
return (
67+
<BridgeProvidersLite forcedTheme={theme}>
68+
<div className="flex min-h-screen items-center justify-center bg-background px-4 py-8">
69+
<SwapWidgetEmbed
70+
buyChainId={buyChainId}
71+
buyTokenAddress={buyTokenAddress}
72+
buyAmount={buyAmount}
73+
sellChainId={sellChainId}
74+
sellTokenAddress={sellTokenAddress}
75+
sellAmount={sellAmount}
76+
showThirdwebBranding={showThirdwebBranding}
77+
theme={theme}
78+
currency={currency}
79+
/>
80+
</div>
81+
</BridgeProvidersLite>
82+
);
83+
}

0 commit comments

Comments
 (0)