Skip to content

Commit e730eb5

Browse files
committed
[MNY-343] Add checkout widget iframe
1 parent 56a1c0a commit e730eb5

File tree

10 files changed

+360
-78
lines changed

10 files changed

+360
-78
lines changed

.changeset/short-wasps-show.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"thirdweb": patch
3+
---
4+
5+
Remove fiat price shown in the button in `CheckoutWidget` to avoid showing it twice in the UI.

apps/dashboard/next.config.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,24 @@ const baseNextConfig: NextConfig = {
161161
],
162162
source: "/bridge/widget/:path*",
163163
},
164+
{
165+
headers: [
166+
{
167+
key: "Content-Security-Policy",
168+
value: EmbedContentSecurityPolicy.replace(/\s{2,}/g, " ").trim(),
169+
},
170+
],
171+
source: "/bridge/checkout-widget",
172+
},
173+
{
174+
headers: [
175+
{
176+
key: "Content-Security-Policy",
177+
value: EmbedContentSecurityPolicy.replace(/\s{2,}/g, " ").trim(),
178+
},
179+
],
180+
source: "/bridge/checkout-widget/:path*",
181+
},
164182
];
165183
},
166184
images: {

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,6 @@ export const NEXT_PUBLIC_ASSET_PAGE_CLIENT_ID =
4444

4545
export const NEXT_PUBLIC_BRIDGE_IFRAME_CLIENT_ID =
4646
process.env.NEXT_PUBLIC_BRIDGE_IFRAME_CLIENT_ID;
47+
48+
export const NEXT_PUBLIC_CHECKOUT_IFRAME_CLIENT_ID =
49+
process.env.NEXT_PUBLIC_CHECKOUT_IFRAME_CLIENT_ID;
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import type { SupportedFiatCurrency } from "thirdweb/react";
2+
3+
export function isValidCurrency(
4+
currency: string,
5+
): currency is SupportedFiatCurrency {
6+
if (currency in VALID_CURRENCIES) {
7+
return true;
8+
}
9+
return false;
10+
}
11+
12+
const VALID_CURRENCIES: Record<SupportedFiatCurrency, true> = {
13+
USD: true,
14+
EUR: true,
15+
GBP: true,
16+
JPY: true,
17+
KRW: true,
18+
CNY: true,
19+
INR: true,
20+
NOK: true,
21+
SEK: true,
22+
CHF: true,
23+
AUD: true,
24+
CAD: true,
25+
NZD: true,
26+
MXN: true,
27+
BRL: true,
28+
CLP: true,
29+
CZK: true,
30+
DKK: true,
31+
HKD: true,
32+
HUF: true,
33+
IDR: true,
34+
ILS: true,
35+
ISK: true,
36+
};
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { isAddress } from "thirdweb";
2+
3+
export function parseQueryParams<T>(
4+
value: string | string[] | undefined,
5+
fn: (value: string) => T | undefined,
6+
): T | undefined {
7+
if (typeof value === "string") {
8+
return fn(value);
9+
}
10+
return undefined;
11+
}
12+
13+
export const onlyAddress = (v: string) => (isAddress(v) ? v : undefined);
14+
export const onlyNumber = (v: string) =>
15+
Number.isNaN(Number(v)) ? undefined : Number(v);
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
"use client";
2+
3+
import { useMemo } from "react";
4+
import type { Address } from "thirdweb";
5+
import { defineChain } from "thirdweb";
6+
import { CheckoutWidget, type SupportedFiatCurrency } from "thirdweb/react";
7+
import { NEXT_PUBLIC_CHECKOUT_IFRAME_CLIENT_ID } from "@/constants/public-envs";
8+
import { getConfiguredThirdwebClient } from "@/constants/thirdweb.server";
9+
10+
export function CheckoutWidgetEmbed({
11+
chainId,
12+
amount,
13+
seller,
14+
tokenAddress,
15+
name,
16+
description,
17+
image,
18+
buttonLabel,
19+
feePayer,
20+
country,
21+
showThirdwebBranding,
22+
theme,
23+
currency,
24+
}: {
25+
chainId: number;
26+
amount: string;
27+
seller: Address;
28+
tokenAddress?: Address;
29+
name?: string;
30+
description?: string;
31+
image?: string;
32+
buttonLabel?: string;
33+
feePayer?: "user" | "seller";
34+
country?: string;
35+
showThirdwebBranding?: boolean;
36+
theme: "light" | "dark";
37+
currency?: SupportedFiatCurrency;
38+
}) {
39+
const client = useMemo(
40+
() =>
41+
getConfiguredThirdwebClient({
42+
clientId: NEXT_PUBLIC_CHECKOUT_IFRAME_CLIENT_ID,
43+
secretKey: undefined,
44+
teamId: undefined,
45+
}),
46+
[],
47+
);
48+
49+
// eslint-disable-next-line no-restricted-syntax
50+
const chain = useMemo(() => defineChain(chainId), [chainId]);
51+
52+
return (
53+
<CheckoutWidget
54+
className="shadow-xl"
55+
client={client}
56+
chain={chain}
57+
amount={amount}
58+
seller={seller}
59+
tokenAddress={tokenAddress}
60+
name={name}
61+
description={description}
62+
image={image}
63+
buttonLabel={buttonLabel}
64+
feePayer={feePayer}
65+
country={country}
66+
showThirdwebBranding={showThirdwebBranding}
67+
theme={theme}
68+
currency={currency}
69+
/>
70+
);
71+
}
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 BridgeEmbedLayout({
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: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import type { Metadata } from "next";
2+
import "@workspace/ui/global.css";
3+
import { InlineCode } from "@workspace/ui/components/code/inline-code";
4+
import { AlertTriangleIcon } from "lucide-react";
5+
import type { SupportedFiatCurrency } from "thirdweb/react";
6+
import { NEXT_PUBLIC_CHECKOUT_IFRAME_CLIENT_ID } from "@/constants/public-envs";
7+
import { isValidCurrency } from "../_common/isValidCurrency";
8+
import {
9+
onlyAddress,
10+
onlyNumber,
11+
parseQueryParams,
12+
} from "../_common/parseQueryParams";
13+
import { BridgeProviders } from "../(general)/components/client/Providers.client";
14+
import { CheckoutWidgetEmbed } from "./CheckoutWidgetEmbed.client";
15+
16+
const title = "thirdweb Checkout: Accept Crypto & Fiat Payments";
17+
const description =
18+
"Accept fiat or crypto payments on any chain—direct to your wallet. Instant checkout, webhook support, and full control over post-sale actions.";
19+
20+
export const metadata: Metadata = {
21+
description,
22+
openGraph: {
23+
description,
24+
title,
25+
},
26+
title,
27+
};
28+
29+
type SearchParams = {
30+
[key: string]: string | string[] | undefined;
31+
};
32+
33+
export default async function Page(props: {
34+
searchParams: Promise<SearchParams>;
35+
}) {
36+
const searchParams = await props.searchParams;
37+
38+
// Required params
39+
const chainId = parseQueryParams(searchParams.chain, onlyNumber);
40+
const amount = parseQueryParams(searchParams.amount, (v) => v);
41+
const seller = parseQueryParams(searchParams.seller, onlyAddress);
42+
43+
// Optional params
44+
const tokenAddress = parseQueryParams(searchParams.tokenAddress, onlyAddress);
45+
const title = parseQueryParams(searchParams.title, (v) => v);
46+
const productDescription = parseQueryParams(
47+
searchParams.description,
48+
(v) => v,
49+
);
50+
const image = parseQueryParams(searchParams.image, (v) => v);
51+
const buttonLabel = parseQueryParams(searchParams.buttonLabel, (v) => v);
52+
const feePayer = parseQueryParams(searchParams.feePayer, (v) =>
53+
v === "seller" || v === "user" ? v : undefined,
54+
);
55+
const country = parseQueryParams(searchParams.country, (v) => v);
56+
57+
const showThirdwebBranding = parseQueryParams(
58+
searchParams.showThirdwebBranding,
59+
(v) => v !== "false",
60+
);
61+
62+
const theme =
63+
parseQueryParams(searchParams.theme, (v) =>
64+
v === "light" ? "light" : "dark",
65+
) || "dark";
66+
67+
const currency = parseQueryParams(searchParams.currency, (v) =>
68+
isValidCurrency(v) ? (v as SupportedFiatCurrency) : undefined,
69+
);
70+
71+
// Validate required params
72+
if (!chainId || !amount || !seller) {
73+
return (
74+
<Providers theme={theme}>
75+
<div className="flex min-h-screen items-center justify-center bg-background px-4 py-8">
76+
<div className="w-full max-w-lg rounded-xl border bg-card p-6 shadow-xl">
77+
<div className="p-2.5 inline-flex rounded-full bg-background mb-4 border">
78+
<AlertTriangleIcon className="size-5 text-destructive-text" />
79+
</div>
80+
<h2 className="mb-2 font-semibold text-destructive-text text-lg">
81+
Invalid Configuration
82+
</h2>
83+
<p className="text-muted-foreground text-sm mb-4">
84+
The following query parameters are required but are missing:
85+
</p>
86+
<ul className="mt-2 text-left text-muted-foreground text-sm space-y-2">
87+
{!chainId && (
88+
<li>
89+
<InlineCode code="chain" /> - Chain ID (e.g., 1, 8453,
90+
42161)
91+
</li>
92+
)}
93+
{!amount && (
94+
<li>
95+
<InlineCode code="amount" /> - Amount to charge (e.g.,
96+
"0.01")
97+
</li>
98+
)}
99+
{!seller && (
100+
<li>
101+
<InlineCode code="seller" /> - Seller wallet address
102+
</li>
103+
)}
104+
</ul>
105+
</div>
106+
</div>
107+
</Providers>
108+
);
109+
}
110+
111+
return (
112+
<Providers theme={theme}>
113+
<div className="flex min-h-screen items-center justify-center bg-background px-4 py-8">
114+
<CheckoutWidgetEmbed
115+
chainId={chainId}
116+
amount={amount}
117+
seller={seller}
118+
tokenAddress={tokenAddress}
119+
name={title}
120+
description={productDescription}
121+
image={image}
122+
buttonLabel={buttonLabel}
123+
feePayer={feePayer}
124+
country={country}
125+
showThirdwebBranding={showThirdwebBranding}
126+
theme={theme}
127+
currency={currency}
128+
/>
129+
</div>
130+
</Providers>
131+
);
132+
}
133+
134+
function Providers({
135+
children,
136+
theme,
137+
}: {
138+
children: React.ReactNode;
139+
theme: string;
140+
}) {
141+
if (!NEXT_PUBLIC_CHECKOUT_IFRAME_CLIENT_ID) {
142+
throw new Error("NEXT_PUBLIC_CHECKOUT_IFRAME_CLIENT_ID is not set");
143+
}
144+
return (
145+
<BridgeProviders
146+
clientId={NEXT_PUBLIC_CHECKOUT_IFRAME_CLIENT_ID}
147+
forcedTheme={theme}
148+
>
149+
{children}
150+
</BridgeProviders>
151+
);
152+
}

0 commit comments

Comments
 (0)