Skip to content

Commit e814318

Browse files
committed
[MNY-210] SDK: export a script to render BridgeEmbed
1 parent c26fc67 commit e814318

File tree

14 files changed

+829
-243
lines changed

14 files changed

+829
-243
lines changed

packages/thirdweb/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
"@types/prompts": "2.4.9",
6666
"@types/qrcode": "1.5.5",
6767
"@types/react": "19.1.8",
68+
"@types/react-dom": "19.1.6",
6869
"@viem/anvil": "0.0.10",
6970
"@vitejs/plugin-react": "^4.6.0",
7071
"@vitest/coverage-v8": "3.2.4",
@@ -89,6 +90,7 @@
8990
"sharp": "^0.34.2",
9091
"size-limit": "11.2.0",
9192
"storybook": "9.0.15",
93+
"tsup": "^8.5.0",
9294
"typedoc": "0.27.9",
9395
"typedoc-better-json": "0.9.4",
9496
"typescript": "5.8.3",
@@ -323,7 +325,8 @@
323325
"scripts": {
324326
"bench": "vitest -c ./test/vitest.config.ts bench",
325327
"bench:compare": "bun run ./benchmarks/run.ts",
326-
"build": "pnpm clean && pnpm build:types && pnpm build:cjs && pnpm build:esm",
328+
"build": "pnpm clean && pnpm build:types && pnpm build:cjs && pnpm build:esm && pnpm build:tsup",
329+
"build:tsup": "tsup",
327330
"build-storybook": "storybook build",
328331
"build:cjs": "tsc --noCheck --project ./tsconfig.build.json --module commonjs --outDir ./dist/cjs --verbatimModuleSyntax false && printf '{\"type\":\"commonjs\"}' > ./dist/cjs/package.json",
329332
"build:esm": "tsc --noCheck --project ./tsconfig.build.json --module es2020 --outDir ./dist/esm && printf '{\"type\": \"module\",\"sideEffects\":false}' > ./dist/esm/package.json",

packages/thirdweb/src/exports/react.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,10 @@ export {
137137
BuyWidget,
138138
type BuyWidgetProps,
139139
} from "../react/web/ui/Bridge/BuyWidget.js";
140+
export {
141+
BridgeWidget,
142+
type BridgeWidgetProps,
143+
} from "../react/web/ui/Bridge/bridge-widget/bridge-widget.js";
140144
export {
141145
CheckoutWidget,
142146
type CheckoutWidgetProps,

packages/thirdweb/src/react/web/ui/Bridge/BuyWidget.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import type { LocaleId } from "../types.js";
3333
import { BridgeOrchestrator, type UIOptions } from "./BridgeOrchestrator.js";
3434
import { UnsupportedTokenScreen } from "./UnsupportedTokenScreen.js";
3535

36-
type BuyOrOnrampPrepareResult = Extract<
36+
export type BuyOrOnrampPrepareResult = Extract<
3737
BridgePrepareResult,
3838
{ type: "buy" | "onramp" }
3939
>;
Lines changed: 345 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,345 @@
1+
import { useState } from "react";
2+
import { defineChain } from "../../../../../chains/utils.js";
3+
import type { ThirdwebClient } from "../../../../../client/client.js";
4+
import type { SupportedFiatCurrency } from "../../../../../pay/convert/type.js";
5+
import type { PurchaseData } from "../../../../../pay/types.js";
6+
import {
7+
CustomThemeProvider,
8+
useCustomTheme,
9+
} from "../../../../core/design-system/CustomThemeProvider.js";
10+
import {
11+
fontSize,
12+
radius,
13+
spacing,
14+
type Theme,
15+
} from "../../../../core/design-system/index.js";
16+
import { EmbedContainer } from "../../ConnectWallet/Modal/ConnectEmbed.js";
17+
import { Container } from "../../components/basic.js";
18+
import { Button } from "../../components/buttons.js";
19+
import { type BuyOrOnrampPrepareResult, BuyWidget } from "../BuyWidget.js";
20+
import { SwapWidget } from "../swap-widget/SwapWidget.js";
21+
import type { SwapPreparedQuote } from "../swap-widget/types.js";
22+
23+
/**
24+
* Props for the `BridgeWidget` component.
25+
*/
26+
export type BridgeWidgetProps = {
27+
/**
28+
* A client is the entry point to the thirdweb SDK. It is required for all other actions.
29+
* You can create a client using the `createThirdwebClient` function. Refer to the
30+
* [Creating a Client](https://portal.thirdweb.com/typescript/v5/client) documentation for more information.
31+
*
32+
* You must provide a `clientId` or `secretKey` in order to initialize a client. Pass `clientId` for client-side usage and `secretKey` for server-side usage.
33+
*
34+
* @example
35+
* ```ts
36+
* import { createThirdwebClient } from "thirdweb";
37+
*
38+
* const client = createThirdwebClient({
39+
* clientId: "<your_client_id>",
40+
* });
41+
* ```
42+
*/
43+
client: ThirdwebClient;
44+
45+
/**
46+
* Set the theme for the widget. By default it is set to `"dark"`.
47+
*
48+
* Theme can be set to either `"dark"`, `"light"` or a custom theme object.
49+
* You can also import [`lightTheme`](https://portal.thirdweb.com/references/typescript/v5/lightTheme)
50+
* or [`darkTheme`](https://portal.thirdweb.com/references/typescript/v5/darkTheme) from `thirdweb/react`
51+
* to use the default themes as base and override parts of it.
52+
*
53+
* @example
54+
* ```ts
55+
* import { lightTheme } from "thirdweb/react";
56+
*
57+
* const customTheme = lightTheme({
58+
* colors: { modalBg: "red" },
59+
* });
60+
* ```
61+
*/
62+
theme?: "light" | "dark" | Theme;
63+
64+
/**
65+
* Whether to show thirdweb branding in the widget.
66+
* @default true
67+
*/
68+
showThirdwebBranding?: boolean;
69+
70+
/**
71+
* The currency to use for fiat pricing in the widget.
72+
* @default "USD"
73+
*/
74+
currency?: SupportedFiatCurrency;
75+
76+
/**
77+
* Configuration for the Swap tab. This mirrors {@link SwapWidget} options where applicable.
78+
*/
79+
swap?: {
80+
/** Optional class name applied to the Swap tab content container. */
81+
className?: string;
82+
/** Optional style overrides applied to the Swap tab content container. */
83+
style?: React.CSSProperties;
84+
/** Callback invoked when a swap is successful. */
85+
onSuccess?: (quote: SwapPreparedQuote) => void;
86+
/** Callback invoked when an error occurs during swapping. */
87+
onError?: (error: Error, quote: SwapPreparedQuote) => void;
88+
/** Callback invoked when the user cancels the swap. */
89+
onCancel?: (quote: SwapPreparedQuote) => void;
90+
/** Callback invoked when the user disconnects the active wallet. */
91+
onDisconnect?: () => void;
92+
/**
93+
* Whether to persist token selections to localStorage so that revisits pre-select last used tokens.
94+
* Prefill values take precedence over persisted selections.
95+
* @default true
96+
*/
97+
persistTokenSelections?: boolean;
98+
/**
99+
* Prefill initial buy/sell token selections. If `tokenAddress` is not provided, the native token will be used.
100+
*
101+
* @example
102+
* ### Set an ERC20 token as the buy token
103+
* ```tsx
104+
* <BridgeWidget
105+
* client={client}
106+
* buy={{ amount: "0.1", chainId: 8453 }}
107+
* swap={{
108+
* prefill: {
109+
* buyToken: {
110+
* chainId: 8453,
111+
* tokenAddress: "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
112+
* },
113+
* },
114+
* }}
115+
* />
116+
* ```
117+
*
118+
* @example
119+
* ### Set a native token as the sell token
120+
* ```tsx
121+
* <BridgeWidget
122+
* client={client}
123+
* buy={{ amount: "0.1", chainId: 8453 }}
124+
* swap={{
125+
* prefill: {
126+
* sellToken: { chainId: 8453 },
127+
* },
128+
* }}
129+
* />
130+
* ```
131+
*/
132+
prefill?: {
133+
/** Buy token selection. If `tokenAddress` is omitted, the native token will be used. */
134+
buyToken?: {
135+
tokenAddress?: string;
136+
chainId: number;
137+
/** Optional human-readable amount to prefill for buy. */
138+
amount?: string;
139+
};
140+
/** Sell token selection. If `tokenAddress` is omitted, the native token will be used. */
141+
sellToken?: {
142+
tokenAddress?: string;
143+
chainId: number;
144+
/** Optional human-readable amount to prefill for sell. */
145+
amount?: string;
146+
};
147+
};
148+
};
149+
150+
/**
151+
* Configuration for the Buy tab. This mirrors {@link BuyWidget} options where applicable.
152+
*/
153+
buy: {
154+
/**
155+
* The amount to buy (as a decimal string), e.g. "1.5" for 1.5 tokens.
156+
*/
157+
amount: string; // TODO - make it optional
158+
/**
159+
* The chain the accepted token is on.
160+
*/
161+
chainId: number; // TODO - make it optional
162+
/**
163+
* Address of the token to buy. Leave undefined for the native token, or use 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE.
164+
*/
165+
tokenAddress?: string;
166+
/** Custom label for the main action button. */
167+
buttonLabel?: string;
168+
/** Callback triggered when the user cancels the purchase. */
169+
onCancel?: (quote: BuyOrOnrampPrepareResult | undefined) => void;
170+
/** Callback triggered when the purchase encounters an error. */
171+
onError?: (
172+
error: Error,
173+
quote: BuyOrOnrampPrepareResult | undefined,
174+
) => void;
175+
/** Callback triggered when the purchase is successful. */
176+
onSuccess?: (quote: BuyOrOnrampPrepareResult) => void;
177+
/** Optional class name applied to the Buy tab content container. */
178+
className?: string;
179+
/** The user's ISO 3166 alpha-2 country code. Used to determine onramp provider support. */
180+
country?: string;
181+
/** Preset fiat amounts to display in the UI. Defaults to [5, 10, 20]. */
182+
presetOptions?: [number, number, number];
183+
/** Arbitrary data to be included in returned status and webhook events. */
184+
purchaseData?: PurchaseData;
185+
};
186+
};
187+
188+
/**
189+
* A combined widget for swapping or buying tokens with cross-chain support.
190+
*
191+
* This component renders two tabs – "Swap" and "Buy" – and orchestrates the appropriate flow
192+
* by composing {@link SwapWidget} and {@link BuyWidget} under the hood.
193+
*
194+
* - The Swap tab enables token-to-token swaps (including cross-chain).
195+
* - The Buy tab enables purchasing a specific token; by default, it uses card onramp in this widget.
196+
*
197+
* @param props - Props of type {@link BridgeWidgetProps} to configure the BridgeWidget component.
198+
*
199+
* @example
200+
* ### Basic usage
201+
* ```tsx
202+
* <BridgeWidget
203+
* client={client}
204+
* currency="USD"
205+
* theme="dark"
206+
* showThirdwebBranding
207+
* buy={{
208+
* // Buy 0.1 native tokens on Base
209+
* amount: "0.1",
210+
* chainId: 8453,
211+
* }}
212+
* />
213+
* ```
214+
*
215+
* ### Prefill swap tokens and configure buy
216+
* ```tsx
217+
* <BridgeWidget
218+
* client={client}
219+
* swap={{
220+
* prefill: {
221+
* buyToken: {
222+
* // Base USDC
223+
* chainId: 8453,
224+
* tokenAddress: "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
225+
* },
226+
* sellToken: {
227+
* // Polygon native token (MATIC)
228+
* chainId: 137,
229+
* },
230+
* },
231+
* }}
232+
* buy={{
233+
* amount: "100",
234+
* chainId: 8453,
235+
* tokenAddress: "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
236+
* buttonLabel: "Buy USDC",
237+
* }}
238+
* />
239+
* ```
240+
*
241+
* @bridge
242+
*/
243+
export function BridgeWidget(props: BridgeWidgetProps) {
244+
const [tab, setTab] = useState<"swap" | "buy">("swap");
245+
246+
return (
247+
<CustomThemeProvider theme={props.theme}>
248+
<EmbedContainer
249+
modalSize="compact"
250+
style={{
251+
borderRadius: radius.xl,
252+
}}
253+
>
254+
<Container
255+
px="md"
256+
py="md"
257+
flex="row"
258+
gap="xs"
259+
borderColor="borderColor"
260+
style={{
261+
borderBottomWidth: 1,
262+
borderBottomStyle: "dashed",
263+
}}
264+
>
265+
<TabButton isActive={tab === "swap"} onClick={() => setTab("swap")}>
266+
Swap
267+
</TabButton>
268+
<TabButton isActive={tab === "buy"} onClick={() => setTab("buy")}>
269+
Buy
270+
</TabButton>
271+
</Container>
272+
273+
{tab === "swap" && (
274+
<SwapWidget
275+
client={props.client}
276+
prefill={props.swap?.prefill}
277+
className={props.swap?.className}
278+
showThirdwebBranding={props.showThirdwebBranding}
279+
currency={props.currency}
280+
theme={props.theme}
281+
onSuccess={props.swap?.onSuccess}
282+
onError={props.swap?.onError}
283+
onCancel={props.swap?.onCancel}
284+
onDisconnect={props.swap?.onDisconnect}
285+
persistTokenSelections={props.swap?.persistTokenSelections}
286+
style={{
287+
border: "none",
288+
...props.swap?.style,
289+
}}
290+
/>
291+
)}
292+
{tab === "buy" && (
293+
<BuyWidget
294+
client={props.client}
295+
amount={props.buy.amount}
296+
showThirdwebBranding={props.showThirdwebBranding}
297+
chain={defineChain(props.buy.chainId)}
298+
currency={props.currency}
299+
theme={props.theme}
300+
title="" // Keep it empty string to hide the title
301+
tokenAddress={props.buy.tokenAddress as `0x${string}`}
302+
buttonLabel={props.buy.buttonLabel}
303+
className={props.buy.className}
304+
country={props.buy.country}
305+
onCancel={props.buy.onCancel}
306+
onError={props.buy.onError}
307+
onSuccess={props.buy.onSuccess}
308+
presetOptions={props.buy.presetOptions}
309+
purchaseData={props.buy.purchaseData}
310+
paymentMethods={["card"]}
311+
style={{
312+
border: "none",
313+
}}
314+
/>
315+
)}
316+
</EmbedContainer>
317+
</CustomThemeProvider>
318+
);
319+
}
320+
321+
function TabButton(props: {
322+
isActive: boolean;
323+
onClick: () => void;
324+
children: React.ReactNode;
325+
}) {
326+
const theme = useCustomTheme();
327+
return (
328+
<Button
329+
variant="secondary"
330+
onClick={props.onClick}
331+
style={{
332+
borderRadius: radius.full,
333+
fontSize: fontSize.sm,
334+
fontWeight: 500,
335+
paddingInline: spacing["md+"],
336+
paddingBlock: spacing.sm,
337+
border: `1px solid ${
338+
props.isActive ? theme.colors.secondaryText : theme.colors.borderColor
339+
}`,
340+
}}
341+
>
342+
{props.children}
343+
</Button>
344+
);
345+
}

0 commit comments

Comments
 (0)