Skip to content

Commit 3671a80

Browse files
committed
[MNY-210] SDK: export a script to render BridgeEmbed
1 parent db38380 commit 3671a80

File tree

9 files changed

+567
-242
lines changed

9 files changed

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

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)