Skip to content

Commit 83f11ec

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

File tree

12 files changed

+591
-243
lines changed

12 files changed

+591
-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/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: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import { useState } from "react";
2+
import { defineChain } from "../../../../../chains/utils.js";
3+
import { createThirdwebClient } 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+
darkTheme,
12+
fontSize,
13+
lightTheme,
14+
radius,
15+
spacing,
16+
type ThemeOverrides,
17+
} from "../../../../core/design-system/index.js";
18+
import { EmbedContainer } from "../../ConnectWallet/Modal/ConnectEmbed.js";
19+
import { Container } from "../../components/basic.js";
20+
import { Button } from "../../components/buttons.js";
21+
import { type BuyOrOnrampPrepareResult, BuyWidget } from "../BuyWidget.js";
22+
import { SwapWidget } from "../swap-widget/SwapWidget.js";
23+
import type { SwapPreparedQuote } from "../swap-widget/types.js";
24+
25+
// Note: do not use SwapWidgetProps or BuyWidgetProps references here to keep the output for bridge-widget.d.ts as simple as possible
26+
// Note: these props will be configured buy user in a <script> tag, so they need to be as simple as possible and can not rely on utils like `darkTheme`, `createThirdwebClient` etc..
27+
// For example, instead of having for a `Chain` prop, use `chainId` instead
28+
29+
export type BridgeWidgetProps = {
30+
clientId: string;
31+
theme?: "light" | "dark" | ({ type: "light" | "dark" } & ThemeOverrides);
32+
showThirdwebBranding?: boolean;
33+
currency?: SupportedFiatCurrency;
34+
swap?: {
35+
className?: string;
36+
style?: React.CSSProperties;
37+
onSuccess?: (quote: SwapPreparedQuote) => void;
38+
onError?: (error: Error, quote: SwapPreparedQuote) => void;
39+
onCancel?: (quote: SwapPreparedQuote) => void;
40+
onDisconnect?: () => void;
41+
persistTokenSelections?: boolean;
42+
prefill?: {
43+
buyToken?: {
44+
tokenAddress?: string;
45+
chainId: number;
46+
amount?: string;
47+
};
48+
sellToken?: {
49+
tokenAddress?: string;
50+
chainId: number;
51+
amount?: string;
52+
};
53+
};
54+
};
55+
buy: {
56+
amount: string; // TODO - make it optional
57+
chainId: number; // TODO - make it optional
58+
tokenAddress?: string;
59+
buttonLabel?: string;
60+
onCancel?: (quote: BuyOrOnrampPrepareResult | undefined) => void;
61+
onError?: (
62+
error: Error,
63+
quote: BuyOrOnrampPrepareResult | undefined,
64+
) => void;
65+
onSuccess?: (quote: BuyOrOnrampPrepareResult) => void;
66+
className?: string;
67+
country?: string;
68+
presetOptions?: [number, number, number];
69+
purchaseData?: PurchaseData;
70+
};
71+
};
72+
73+
export function BridgeWidget(props: BridgeWidgetProps) {
74+
const [tab, setTab] = useState<"swap" | "buy">("swap");
75+
const client = createThirdwebClient({ clientId: props.clientId });
76+
const themeObj =
77+
props.theme === undefined
78+
? "dark"
79+
: typeof props.theme === "object"
80+
? props.theme.type === "dark"
81+
? darkTheme(props.theme)
82+
: lightTheme(props.theme)
83+
: props.theme;
84+
85+
return (
86+
<CustomThemeProvider theme={themeObj}>
87+
<EmbedContainer
88+
modalSize="compact"
89+
style={{
90+
borderRadius: radius.xl,
91+
}}
92+
>
93+
<Container
94+
px="md"
95+
py="md"
96+
flex="row"
97+
gap="xs"
98+
borderColor="borderColor"
99+
style={{
100+
borderBottomWidth: 1,
101+
borderBottomStyle: "dashed",
102+
}}
103+
>
104+
<TabButton isActive={tab === "swap"} onClick={() => setTab("swap")}>
105+
Swap
106+
</TabButton>
107+
<TabButton isActive={tab === "buy"} onClick={() => setTab("buy")}>
108+
Buy
109+
</TabButton>
110+
</Container>
111+
112+
{tab === "swap" && (
113+
<SwapWidget
114+
client={client}
115+
prefill={props.swap?.prefill}
116+
className={props.swap?.className}
117+
showThirdwebBranding={props.showThirdwebBranding}
118+
currency={props.currency}
119+
theme={themeObj}
120+
onSuccess={props.swap?.onSuccess}
121+
onError={props.swap?.onError}
122+
onCancel={props.swap?.onCancel}
123+
onDisconnect={props.swap?.onDisconnect}
124+
persistTokenSelections={props.swap?.persistTokenSelections}
125+
style={{
126+
border: "none",
127+
...props.swap?.style,
128+
}}
129+
/>
130+
)}
131+
{tab === "buy" && (
132+
<BuyWidget
133+
client={client}
134+
amount={props.buy.amount}
135+
showThirdwebBranding={props.showThirdwebBranding}
136+
chain={defineChain(props.buy.chainId)}
137+
currency={props.currency}
138+
theme={themeObj}
139+
title="" // Keep it empty string to hide the title
140+
tokenAddress={props.buy.tokenAddress as `0x${string}`}
141+
buttonLabel={props.buy.buttonLabel}
142+
className={props.buy.className}
143+
country={props.buy.country}
144+
onCancel={props.buy.onCancel}
145+
onError={props.buy.onError}
146+
onSuccess={props.buy.onSuccess}
147+
presetOptions={props.buy.presetOptions}
148+
purchaseData={props.buy.purchaseData}
149+
paymentMethods={["card"]}
150+
style={{
151+
border: "none",
152+
}}
153+
/>
154+
)}
155+
</EmbedContainer>
156+
</CustomThemeProvider>
157+
);
158+
}
159+
160+
function TabButton(props: {
161+
isActive: boolean;
162+
onClick: () => void;
163+
children: React.ReactNode;
164+
}) {
165+
const theme = useCustomTheme();
166+
return (
167+
<Button
168+
variant="secondary"
169+
onClick={props.onClick}
170+
style={{
171+
borderRadius: radius.full,
172+
fontSize: fontSize.sm,
173+
fontWeight: 500,
174+
paddingInline: spacing["md+"],
175+
paddingBlock: spacing.sm,
176+
border: `1px solid ${
177+
props.isActive ? theme.colors.secondaryText : theme.colors.borderColor
178+
}`,
179+
}}
180+
>
181+
{props.children}
182+
</Button>
183+
);
184+
}

packages/thirdweb/src/react/web/ui/ConnectWallet/Modal/ConnectEmbed.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,8 @@ export const EmbedContainer = /* @__PURE__ */ StyledDiv<{
479479
lineHeight: "normal",
480480
overflow: "hidden",
481481
position: "relative",
482-
width: modalSize === "compact" ? modalMaxWidthCompact : modalMaxWidthWide,
482+
width: "100vw",
483+
maxWidth:
484+
modalSize === "compact" ? modalMaxWidthCompact : modalMaxWidthWide,
483485
};
484486
});
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { type Container, createRoot } from "react-dom/client";
2+
import { ThirdwebProvider } from "../react/web/providers/thirdweb-provider.js";
3+
import {
4+
BridgeWidget,
5+
type BridgeWidgetProps,
6+
} from "../react/web/ui/Bridge/bridge-widget/bridge-widget.js";
7+
8+
// Note: This file is built as a UMD module with globalName "BridgeWidget"
9+
// This will be available as a global function called `BridgeWidget.render`
10+
export function render(element: Container, props: BridgeWidgetProps) {
11+
createRoot(element).render(
12+
<ThirdwebProvider>
13+
<BridgeWidget {...props} />
14+
</ThirdwebProvider>,
15+
);
16+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# Bridge Widget
2+
3+
Add the script in document head and the element where you want to render the bridge widget in your page
4+
5+
```html
6+
<!-- add the bridge-widget script in the head of the document, it adds the `BridgeWidget` global variable to the window object -->
7+
<script src="https://unpkg.com/thirdweb/scripts/bridge-widget.js"></script>
8+
9+
<!-- add a unique id to an element where you want to render the bridge widget -->
10+
<div id="bridge-widget"></div>
11+
```
12+
13+
### Basic Usage
14+
15+
```html
16+
<script>
17+
// get the element where you want to render the bridge widget
18+
const node = document.getElementById("bridge-widget");
19+
20+
// render the widget
21+
BridgeWidget.render(node, {
22+
clientId: "your-client-id",
23+
theme: "dark",
24+
buy: {
25+
chainId: 8453,
26+
amount: "0.1",
27+
},
28+
});
29+
</script>
30+
```
31+
32+
### Custom Theme
33+
34+
```html
35+
<script>
36+
BridgeWidget.render(node, {
37+
clientId: "your-client-id",
38+
theme: {
39+
type: "dark",
40+
colors: {
41+
modalBg: "red",
42+
},
43+
},
44+
buy: {
45+
chainId: 8453,
46+
amount: "0.1",
47+
},
48+
});
49+
</script>
50+
```
51+
52+
### Customizing Swap UI
53+
54+
```html
55+
<script>
56+
BridgeWidget.render(node, {
57+
clientId: "your-client-id",
58+
swap: {
59+
prefill: {
60+
buyToken: {
61+
chainId: 8453,
62+
tokenAddress: "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
63+
},
64+
},
65+
},
66+
buy: {
67+
chainId: 8453,
68+
amount: "0.1",
69+
},
70+
});
71+
</script>
72+
```
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import type { Meta } from "@storybook/react";
2+
import { BridgeWidget } from "../../../react/web/ui/Bridge/bridge-widget/bridge-widget.js";
3+
import { storyClient } from "../../utils.js";
4+
5+
const meta: Meta<typeof BridgeWidget> = {
6+
title: "Bridge/BridgeWidget",
7+
parameters: {
8+
layout: "centered",
9+
},
10+
decorators: [
11+
(Story) => {
12+
return (
13+
<div>
14+
<Story />
15+
</div>
16+
);
17+
},
18+
],
19+
};
20+
export default meta;
21+
22+
export function BasicUsage() {
23+
return (
24+
<BridgeWidget
25+
clientId={storyClient.clientId}
26+
buy={{ chainId: 8453, amount: "0.1" }}
27+
/>
28+
);
29+
}
30+
31+
export function LightTheme() {
32+
return (
33+
<BridgeWidget
34+
clientId={storyClient.clientId}
35+
theme="light"
36+
buy={{ chainId: 8453, amount: "0.1" }}
37+
/>
38+
);
39+
}
40+
41+
export function CurrencySet() {
42+
return (
43+
<BridgeWidget
44+
clientId={storyClient.clientId}
45+
currency="JPY"
46+
buy={{ chainId: 8453, amount: "0.1" }}
47+
/>
48+
);
49+
}
50+
51+
export function NoThirdwebBranding() {
52+
return (
53+
<BridgeWidget
54+
clientId={storyClient.clientId}
55+
theme="light"
56+
buy={{ chainId: 8453, amount: "0.1" }}
57+
showThirdwebBranding={false}
58+
/>
59+
);
60+
}
61+
62+
export function CustomTheme() {
63+
return (
64+
<BridgeWidget
65+
clientId={storyClient.clientId}
66+
buy={{ chainId: 8453, amount: "0.1" }}
67+
theme={{
68+
type: "light",
69+
colors: {
70+
modalBg: "#FFFFF0",
71+
tertiaryBg: "#DBE4C9",
72+
borderColor: "#8AA624",
73+
secondaryText: "#3E3F29",
74+
accentText: "#E43636",
75+
},
76+
}}
77+
/>
78+
);
79+
}

0 commit comments

Comments
 (0)