Skip to content

Commit 5ad6b5a

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

File tree

9 files changed

+563
-242
lines changed

9 files changed

+563
-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 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: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
## Bridge Widget
2+
3+
Add the script in doucment header 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 -->
7+
<script src="https://unpkg.com/thirdweb/scripts/bridge-widget.js"></script>
8+
9+
<!-- add the bridge-widget node to the document -->
10+
<div id="bridge-widget"></div>
11+
```
12+
13+
### Basic Usage
14+
15+
```html
16+
<script>
17+
const node = document.getElementById("bridge-widget");
18+
BridgeWidget.render(node, {
19+
clientId: "your-client-id",
20+
theme: "dark",
21+
buy: {
22+
chainId: 8453,
23+
amount: "0.1",
24+
},
25+
});
26+
</script>
27+
```
28+
29+
### Custom Theme
30+
31+
```html
32+
<script>
33+
const node = document.getElementById("bridge-widget");
34+
BridgeWidget.render(node, {
35+
clientId: "your-client-id",
36+
theme: {
37+
type: "dark",
38+
colors: {
39+
modalBg: "red",
40+
},
41+
},
42+
buy: {
43+
chainId: 8453,
44+
amount: "0.1",
45+
},
46+
});
47+
</script>
48+
```
49+
50+
### Customing Swap UI
51+
52+
```html
53+
<script>
54+
const node = document.getElementById("bridge-widget");
55+
BridgeWidget.render(node, {
56+
clientId: "your-client-id",
57+
swap: {
58+
prefill: {
59+
buyToken: {
60+
chainId: 8453,
61+
tokenAddress: "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
62+
},
63+
},
64+
},
65+
});
66+
</script>
67+
```
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import type { Meta } from "@storybook/react";
2+
import { BridgeWidget } from "../../../react/web/ui/Bridge/bridge-widget/bridge-widget.js";
3+
import type { SwapWidget } from "../../../react/web/ui/Bridge/swap-widget/SwapWidget.js";
4+
import { storyClient } from "../../utils.js";
5+
6+
const meta: Meta<typeof SwapWidget> = {
7+
title: "Bridge/BridgeWidget",
8+
parameters: {
9+
layout: "centered",
10+
},
11+
decorators: [
12+
(Story) => {
13+
return (
14+
<div>
15+
<Story />
16+
</div>
17+
);
18+
},
19+
],
20+
};
21+
export default meta;
22+
23+
export function BasicUsage() {
24+
return (
25+
<BridgeWidget
26+
clientId={storyClient.clientId}
27+
buy={{ chainId: 8453, amount: "0.1" }}
28+
/>
29+
);
30+
}
31+
32+
export function LightTheme() {
33+
return (
34+
<BridgeWidget
35+
clientId={storyClient.clientId}
36+
theme="light"
37+
buy={{ chainId: 8453, amount: "0.1" }}
38+
/>
39+
);
40+
}
41+
42+
export function CurrencySet() {
43+
return (
44+
<BridgeWidget
45+
clientId={storyClient.clientId}
46+
currency="JPY"
47+
buy={{ chainId: 8453, amount: "0.1" }}
48+
/>
49+
);
50+
}
51+
52+
export function NoThirdwebBranding() {
53+
return (
54+
<BridgeWidget
55+
clientId={storyClient.clientId}
56+
theme="light"
57+
buy={{ chainId: 8453, amount: "0.1" }}
58+
showThirdwebBranding={false}
59+
/>
60+
);
61+
}
62+
63+
export function CustomTheme() {
64+
return (
65+
<BridgeWidget
66+
clientId={storyClient.clientId}
67+
buy={{ chainId: 8453, amount: "0.1" }}
68+
theme={{
69+
type: "light",
70+
colors: {
71+
modalBg: "#FFFFF0",
72+
tertiaryBg: "#DBE4C9",
73+
borderColor: "#8AA624",
74+
secondaryText: "#3E3F29",
75+
accentText: "#E43636",
76+
},
77+
}}
78+
/>
79+
);
80+
}

0 commit comments

Comments
 (0)