Skip to content

Commit e2a5adc

Browse files
authored
Merge branch 'main' into yash/fix-ref-publish
2 parents 2aac3c7 + d35cad8 commit e2a5adc

File tree

20 files changed

+1014
-175
lines changed

20 files changed

+1014
-175
lines changed
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+
Add Etherlink Shadownet testnet chain (chain ID 127823)

apps/dashboard/next.config.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,24 @@ const baseNextConfig: NextConfig = {
197197
],
198198
source: "/bridge/swap-widget/:path*",
199199
},
200+
{
201+
headers: [
202+
{
203+
key: "Content-Security-Policy",
204+
value: EmbedContentSecurityPolicy.replace(/\s{2,}/g, " ").trim(),
205+
},
206+
],
207+
source: "/bridge/buy-widget",
208+
},
209+
{
210+
headers: [
211+
{
212+
key: "Content-Security-Policy",
213+
value: EmbedContentSecurityPolicy.replace(/\s{2,}/g, " ").trim(),
214+
},
215+
],
216+
source: "/bridge/buy-widget/:path*",
217+
},
200218
];
201219
},
202220
images: {

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,6 @@ export const NEXT_PUBLIC_CHECKOUT_IFRAME_CLIENT_ID =
5050

5151
export const NEXT_PUBLIC_SWAP_IFRAME_CLIENT_ID =
5252
process.env.NEXT_PUBLIC_SWAP_IFRAME_CLIENT_ID;
53+
54+
export const NEXT_PUBLIC_BUY_IFRAME_CLIENT_ID =
55+
process.env.NEXT_PUBLIC_BUY_IFRAME_CLIENT_ID;
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
"use client";
2+
3+
import { useMemo } from "react";
4+
import { type Address, defineChain } from "thirdweb";
5+
import { BuyWidget, type SupportedFiatCurrency } from "thirdweb/react";
6+
import { NEXT_PUBLIC_BUY_IFRAME_CLIENT_ID } from "@/constants/public-envs";
7+
import { getConfiguredThirdwebClient } from "@/constants/thirdweb.server";
8+
9+
export function BuyWidgetEmbed({
10+
chainId,
11+
tokenAddress,
12+
amount,
13+
showThirdwebBranding,
14+
theme,
15+
currency,
16+
title,
17+
description,
18+
image,
19+
paymentMethods,
20+
buttonLabel,
21+
receiverAddress,
22+
country,
23+
}: {
24+
chainId?: number;
25+
tokenAddress?: Address;
26+
amount?: string;
27+
showThirdwebBranding?: boolean;
28+
theme: "light" | "dark";
29+
currency?: SupportedFiatCurrency;
30+
title?: string;
31+
description?: string;
32+
image?: string;
33+
paymentMethods?: ("crypto" | "card")[];
34+
buttonLabel?: string;
35+
receiverAddress?: Address;
36+
country?: string;
37+
}) {
38+
const client = useMemo(
39+
() =>
40+
getConfiguredThirdwebClient({
41+
clientId: NEXT_PUBLIC_BUY_IFRAME_CLIENT_ID,
42+
secretKey: undefined,
43+
teamId: undefined,
44+
}),
45+
[],
46+
);
47+
48+
const chain = useMemo(() => {
49+
if (!chainId) return undefined;
50+
// eslint-disable-next-line no-restricted-syntax
51+
return defineChain(chainId);
52+
}, [chainId]);
53+
54+
return (
55+
<BuyWidget
56+
className="shadow-xl"
57+
client={client}
58+
chain={chain}
59+
tokenAddress={tokenAddress}
60+
amount={amount}
61+
showThirdwebBranding={showThirdwebBranding}
62+
theme={theme}
63+
currency={currency}
64+
title={title}
65+
description={description}
66+
image={image}
67+
paymentMethods={paymentMethods}
68+
buttonLabel={buttonLabel}
69+
receiverAddress={receiverAddress}
70+
country={country}
71+
onSuccess={() => {
72+
sendMessageToParent({
73+
source: "buy-widget",
74+
type: "success",
75+
});
76+
}}
77+
onError={(error) => {
78+
sendMessageToParent({
79+
source: "buy-widget",
80+
type: "error",
81+
message: error.message,
82+
});
83+
}}
84+
/>
85+
);
86+
}
87+
88+
function sendMessageToParent(content: object) {
89+
try {
90+
window.parent.postMessage(content, "*");
91+
} catch (error) {
92+
console.error("Failed to send post message to parent window");
93+
console.error(error);
94+
}
95+
}
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 BuyWidgetLayout({
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: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
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 { BuyWidgetEmbed } from "./BuyWidgetEmbed.client";
12+
13+
const title = "thirdweb Buy: Purchase Crypto with Ease";
14+
const description =
15+
"Buy crypto tokens with credit card or other payment methods. Simple and secure purchases 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+
// Token params
36+
const chainId = parseQueryParams(searchParams.chain, onlyNumber);
37+
const tokenAddress = parseQueryParams(searchParams.tokenAddress, onlyAddress);
38+
const amount = parseQueryParams(searchParams.amount, (v) => v);
39+
40+
// Optional params
41+
const showThirdwebBranding = parseQueryParams(
42+
searchParams.showThirdwebBranding,
43+
// biome-ignore lint/complexity/noUselessTernary: this is easier to understand
44+
(v) => (v === "false" ? false : true),
45+
);
46+
47+
const theme =
48+
parseQueryParams(searchParams.theme, (v) =>
49+
v === "light" ? "light" : "dark",
50+
) || "dark";
51+
52+
const currency = parseQueryParams(searchParams.currency, (v) =>
53+
isValidCurrency(v) ? (v as SupportedFiatCurrency) : undefined,
54+
);
55+
56+
// Metadata params
57+
const widgetTitle = parseQueryParams(searchParams.title, (v) => v);
58+
const widgetDescription = parseQueryParams(
59+
searchParams.description,
60+
(v) => v,
61+
);
62+
const image = parseQueryParams(searchParams.image, (v) => v);
63+
64+
// Payment params
65+
const paymentMethods = parseQueryParams(searchParams.paymentMethods, (v) => {
66+
if (v === "crypto" || v === "card") {
67+
return [v] as ("crypto" | "card")[];
68+
}
69+
return undefined;
70+
});
71+
72+
const buttonLabel = parseQueryParams(searchParams.buttonLabel, (v) => v);
73+
const receiverAddress = parseQueryParams(searchParams.receiver, onlyAddress);
74+
const country = parseQueryParams(searchParams.country, (v) => v);
75+
76+
return (
77+
<BridgeProvidersLite forcedTheme={theme}>
78+
<div className="flex min-h-screen items-center justify-center bg-background px-4 py-8">
79+
<BuyWidgetEmbed
80+
chainId={chainId}
81+
tokenAddress={tokenAddress}
82+
amount={amount}
83+
showThirdwebBranding={showThirdwebBranding}
84+
theme={theme}
85+
currency={currency}
86+
title={widgetTitle}
87+
description={widgetDescription}
88+
image={image}
89+
paymentMethods={paymentMethods}
90+
buttonLabel={buttonLabel}
91+
receiverAddress={receiverAddress}
92+
country={country}
93+
/>
94+
</div>
95+
</BridgeProvidersLite>
96+
);
97+
}

apps/playground-web/src/app/bridge/buy-widget/BuyPlayground.tsx

Lines changed: 65 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
"use client";
22

3-
import { useState } from "react";
3+
import { useTheme } from "next-themes";
4+
import { useEffect, useState } from "react";
45
import { arbitrum } from "thirdweb/chains";
6+
import { TabButtons } from "@/components/ui/tab-buttons";
57
import { LeftSection } from "../components/LeftSection";
68
import { RightSection } from "../components/RightSection";
79
import type { BridgeComponentsPlaygroundOptions } from "../components/types";
810

911
const defaultOptions: BridgeComponentsPlaygroundOptions = {
12+
integrationType: "react",
1013
payOptions: {
1114
buyTokenAddress: undefined,
1215
buyTokenAmount: "0.002",
@@ -29,16 +32,70 @@ const defaultOptions: BridgeComponentsPlaygroundOptions = {
2932
},
3033
};
3134

32-
export function BuyPlayground() {
33-
const [options, setOptions] =
34-
useState<BridgeComponentsPlaygroundOptions>(defaultOptions);
35+
function updatePageUrl(
36+
tab: BridgeComponentsPlaygroundOptions["integrationType"],
37+
) {
38+
const url = new URL(window.location.href);
39+
if (tab === defaultOptions.integrationType) {
40+
url.searchParams.delete("tab");
41+
} else {
42+
url.searchParams.set("tab", tab || "");
43+
}
44+
45+
window.history.replaceState({}, "", url.toString());
46+
}
47+
48+
export function BuyPlayground(props: { defaultTab?: "iframe" | "react" }) {
49+
const { theme } = useTheme();
50+
51+
const [options, setOptions] = useState<BridgeComponentsPlaygroundOptions>(
52+
() => ({
53+
...defaultOptions,
54+
integrationType: props.defaultTab || defaultOptions.integrationType,
55+
}),
56+
);
57+
58+
// Change theme on global theme change
59+
useEffect(() => {
60+
setOptions((prev) => ({
61+
...prev,
62+
theme: {
63+
...prev.theme,
64+
type: theme === "dark" ? "dark" : "light",
65+
},
66+
}));
67+
}, [theme]);
68+
69+
useEffect(() => {
70+
updatePageUrl(options.integrationType);
71+
}, [options.integrationType]);
3572

3673
return (
37-
<div className="relative flex flex-col-reverse gap-6 xl:min-h-[900px] xl:flex-row xl:gap-6">
38-
<div className="grow border-b pb-10 xl:mb-0 xl:border-r xl:border-b-0 xl:pr-6">
39-
<LeftSection widget="buy" options={options} setOptions={setOptions} />
74+
<div>
75+
<TabButtons
76+
tabs={[
77+
{
78+
name: "React",
79+
onClick: () => setOptions({ ...options, integrationType: "react" }),
80+
isActive: options.integrationType === "react",
81+
},
82+
{
83+
name: "Iframe",
84+
onClick: () =>
85+
setOptions({ ...options, integrationType: "iframe" }),
86+
isActive: options.integrationType === "iframe",
87+
},
88+
]}
89+
/>
90+
91+
<div className="h-6" />
92+
93+
<div className="relative flex flex-col-reverse gap-6 xl:min-h-[900px] xl:flex-row xl:gap-6">
94+
<div className="grow border-b pb-10 xl:mb-0 xl:border-r xl:border-b-0 xl:pr-6">
95+
<LeftSection widget="buy" options={options} setOptions={setOptions} />
96+
</div>
97+
<RightSection widget="buy" options={options} />
4098
</div>
41-
<RightSection widget="buy" options={options} />
4299
</div>
43100
);
44101
}

apps/playground-web/src/app/bridge/buy-widget/page.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,15 @@ export const metadata = createMetadata({
1919
},
2020
});
2121

22-
export default function Page() {
22+
export default async function Page(props: {
23+
searchParams: Promise<{ tab?: string }>;
24+
}) {
25+
const searchParams = await props.searchParams;
26+
const defaultTab =
27+
searchParams.tab === "iframe" || searchParams.tab === "react"
28+
? searchParams.tab
29+
: undefined;
30+
2331
return (
2432
<ThirdwebProvider>
2533
<PageLayout
@@ -28,7 +36,7 @@ export default function Page() {
2836
description={description}
2937
docsLink="https://portal.thirdweb.com/references/typescript/v5/BuyWidget?utm_source=playground"
3038
>
31-
<BuyPlayground />
39+
<BuyPlayground defaultTab={defaultTab} />
3240
</PageLayout>
3341
</ThirdwebProvider>
3442
);

0 commit comments

Comments
 (0)