Skip to content

Commit dbf74aa

Browse files
feat: TransactionButton react native implementation (thirdweb-dev#3231)
1 parent 63b4e6c commit dbf74aa

File tree

89 files changed

+652
-327
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

89 files changed

+652
-327
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"thirdweb": minor
3+
---
4+
5+
TransactionButton react native implementation

packages/thirdweb/src/exports/react-native.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,9 @@ export {
8888
} from "../react/core/hooks/connection/AutoConnect.js";
8989
export { useAutoConnect } from "../react/core/hooks/connection/useAutoConnect.js";
9090

91+
export { TransactionButton } from "../react/native/ui/TransactionButton/TrabsactionButton.js";
92+
export type { TransactionButtonProps } from "../react/core/hooks/transaction/button-core.js";
93+
9194
// wallet info
9295
export {
9396
useWalletInfo,

packages/thirdweb/src/exports/react.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
export { darkTheme, lightTheme } from "../react/web/ui/design-system/index.js";
1+
export { darkTheme, lightTheme } from "../react/core/design-system/index.js";
22
export type {
33
Theme,
44
ThemeOverrides,
5-
} from "../react/web/ui/design-system/index.js";
5+
} from "../react/core/design-system/index.js";
66

77
export { ConnectButton } from "../react/web/ui/ConnectWallet/ConnectButton.js";
88
export {
@@ -21,10 +21,8 @@ export type { NetworkSelectorProps } from "../react/web/ui/ConnectWallet/Network
2121
export type { WelcomeScreen } from "../react/web/ui/ConnectWallet/screens/types.js";
2222
export type { LocaleId } from "../react/web/ui/types.js";
2323

24-
export {
25-
TransactionButton,
26-
type TransactionButtonProps,
27-
} from "../react/web/ui/TransactionButton/index.js";
24+
export { TransactionButton } from "../react/web/ui/TransactionButton/index.js";
25+
export type { TransactionButtonProps } from "../react/core/hooks/transaction/button-core.js";
2826

2927
export { ThirdwebProvider } from "../react/core/providers/thirdweb-provider.js";
3028

packages/thirdweb/src/react/web/ui/design-system/CustomThemeProvider.tsx renamed to packages/thirdweb/src/react/core/design-system/CustomThemeProvider.tsx

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,20 @@ export function CustomThemeProvider(props: {
1212
theme: "light" | "dark" | Theme;
1313
}) {
1414
const { theme, children } = props;
15+
const themeObj = parseTheme(theme);
16+
17+
return (
18+
<CustomThemeCtx.Provider value={themeObj}>
19+
{children}
20+
</CustomThemeCtx.Provider>
21+
);
22+
}
23+
24+
export function parseTheme(theme: "light" | "dark" | Theme | undefined): Theme {
25+
if (!theme) {
26+
return darkThemeObj;
27+
}
28+
1529
let themeObj: Theme;
1630

1731
if (typeof theme === "string") {
@@ -20,11 +34,7 @@ export function CustomThemeProvider(props: {
2034
themeObj = theme;
2135
}
2236

23-
return (
24-
<CustomThemeCtx.Provider value={themeObj}>
25-
{children}
26-
</CustomThemeCtx.Provider>
27-
);
37+
return themeObj;
2838
}
2939

3040
/**

packages/thirdweb/src/react/web/ui/design-system/index.ts renamed to packages/thirdweb/src/react/core/design-system/index.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ type ThemeColors = {
1414
};
1515

1616
const darkColors = {
17-
base1: "hsl(230deg 11.63% 8.43%)",
18-
base2: "hsl(230deg 11.63% 12%)",
19-
base3: "hsl(230deg 11.63% 15%)",
20-
base4: "hsl(230deg 11.63% 17%)",
17+
base1: "hsl(230 11.63% 8.43%)",
18+
base2: "hsl(230 11.63% 12%)",
19+
base3: "hsl(230 11.63% 15%)",
20+
base4: "hsl(230 11.63% 17%)",
2121
primaryText: "#eeeef0",
2222
secondaryText: "#7c7a85",
2323
danger: "#e5484D",
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import { useState } from "react";
2+
import type { TransactionReceipt } from "viem";
3+
import type { GaslessOptions } from "../../../../transaction/actions/gasless/types.js";
4+
import {
5+
type WaitForReceiptOptions,
6+
waitForReceipt,
7+
} from "../../../../transaction/actions/wait-for-tx-receipt.js";
8+
import type { PreparedTransaction } from "../../../../transaction/prepare-transaction.js";
9+
import { stringify } from "../../../../utils/json.js";
10+
import {
11+
type SendTransactionPayModalConfig,
12+
useSendTransaction,
13+
} from "../../../web/hooks/useSendTransaction.js";
14+
import type { Theme } from "../../design-system/index.js";
15+
import { useActiveAccount } from "../wallets/wallet-hooks.js";
16+
17+
/**
18+
* Props for the [`TransactionButton`](https://portal.thirdweb.com/references/typescript/v5/TransactionButton) component.
19+
*/
20+
export type TransactionButtonProps = {
21+
/**
22+
* The a function returning a prepared transaction of type [`PreparedTransaction`](https://portal.thirdweb.com/references/typescript/v5/PreparedTransaction) to be sent when the button is clicked
23+
*/
24+
transaction: () => // biome-ignore lint/suspicious/noExplicitAny: TODO: fix any
25+
| PreparedTransaction<any>
26+
// biome-ignore lint/suspicious/noExplicitAny: TODO: fix any
27+
| Promise<PreparedTransaction<any>>;
28+
29+
/**
30+
* Callback that will be called when the transaction is submitted onchain
31+
* @param transactionResult - The object of type [`WaitForReceiptOptions`](https://portal.thirdweb.com/references/typescript/v5/WaitForReceiptOptions)
32+
*/
33+
onTransactionSent?: (transactionResult: WaitForReceiptOptions) => void;
34+
/**
35+
*
36+
* Callback that will be called when the transaction is confirmed onchain.
37+
* If this callback is set, the component will wait for the transaction to be confirmed.
38+
* @param receipt - The transaction receipt object of type [`TransactionReceipt`](https://portal.thirdweb.com/references/typescript/v5/TransactionReceipt)
39+
*/
40+
onTransactionConfirmed?: (receipt: TransactionReceipt) => void;
41+
/**
42+
* The Error thrown when trying to send the transaction
43+
* @param error - The `Error` object thrown
44+
*/
45+
onError?: (error: Error) => void;
46+
/**
47+
* Callback to be called when the button is clicked
48+
*/
49+
onClick?: () => void;
50+
/**
51+
* The className to apply to the button element for custom styling
52+
*/
53+
className?: string;
54+
/**
55+
* The style to apply to the button element for custom styling
56+
*/
57+
style?: React.CSSProperties;
58+
/**
59+
* Remove all default styling from the button
60+
*/
61+
unstyled?: boolean;
62+
/**
63+
* The `React.ReactNode` to be rendered inside the button
64+
*/
65+
children: React.ReactNode;
66+
67+
/**
68+
* Configuration for gasless transactions.
69+
* Refer to [`GaslessOptions`](https://portal.thirdweb.com/references/typescript/v5/GaslessOptions) for more details.
70+
*/
71+
gasless?: GaslessOptions;
72+
73+
/**
74+
* The button's disabled state
75+
*/
76+
disabled?: boolean;
77+
78+
/**
79+
* Configuration for the "Pay Modal" that opens when the user doesn't have enough funds to send a transaction.
80+
* Set `payModal: false` to disable the "Pay Modal" popup
81+
*
82+
* This configuration object includes the following properties to configure the "Pay Modal" UI:
83+
*
84+
* ### `locale`
85+
* The language to use for the "Pay Modal" UI. Defaults to `"en_US"`.
86+
*
87+
* ### `supportedTokens`
88+
* An object of type [`SupportedTokens`](https://portal.thirdweb.com/references/typescript/v5/SupportedTokens) to configure the tokens to show for a chain.
89+
*
90+
* ### `theme`
91+
* The theme to use for the "Pay Modal" UI. Defaults to `"dark"`.
92+
*
93+
* It can be set to `"light"` or `"dark"` or an object of type [`Theme`](https://portal.thirdweb.com/references/typescript/v5/Theme) for a custom theme.
94+
*
95+
* Refer to [`lightTheme`](https://portal.thirdweb.com/references/typescript/v5/lightTheme)
96+
* or [`darkTheme`](https://portal.thirdweb.com/references/typescript/v5/darkTheme) helper functions to use the default light or dark theme and customize it.
97+
*/
98+
payModal?: SendTransactionPayModalConfig;
99+
100+
/**
101+
* The theme to use for the button
102+
*/
103+
theme?: "dark" | "light" | Theme;
104+
};
105+
106+
export function useTransactionButtonCore(props: TransactionButtonProps) {
107+
const {
108+
transaction,
109+
onTransactionSent,
110+
onTransactionConfirmed,
111+
onError,
112+
onClick,
113+
gasless,
114+
payModal,
115+
} = props;
116+
const account = useActiveAccount();
117+
const [isPending, setIsPending] = useState(false);
118+
119+
const sendTransaction = useSendTransaction({
120+
gasless,
121+
payModal,
122+
});
123+
124+
const handleClick = async () => {
125+
if (onClick) {
126+
onClick();
127+
}
128+
try {
129+
setIsPending(true);
130+
const resolvedTx = await transaction();
131+
const result = await sendTransaction.mutateAsync(resolvedTx);
132+
133+
if (onTransactionSent) {
134+
onTransactionSent(result);
135+
}
136+
137+
if (onTransactionConfirmed) {
138+
const receipt = await waitForReceipt(result);
139+
if (receipt.status === "reverted") {
140+
throw new Error(`Execution reverted: ${stringify(receipt, null, 2)}`);
141+
}
142+
onTransactionConfirmed(receipt);
143+
}
144+
} catch (error) {
145+
if (onError) {
146+
onError(error as Error);
147+
}
148+
} finally {
149+
setIsPending(false);
150+
}
151+
};
152+
153+
return {
154+
account,
155+
handleClick,
156+
isPending,
157+
};
158+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { type StyleProp, View, type ViewStyle } from "react-native";
2+
import { parseTheme } from "../../../core/design-system/CustomThemeProvider.js";
3+
import {
4+
type TransactionButtonProps,
5+
useTransactionButtonCore,
6+
} from "../../../core/hooks/transaction/button-core.js";
7+
import { ThemedButton } from "../components/button.js";
8+
import { ThemedSpinner } from "../components/spinner.js";
9+
10+
/**
11+
* TransactionButton component is used to render a button that triggers a transaction.
12+
* - It shows a "Switch Network" button if the connected wallet is on a different chain than the transaction.
13+
* @param props - The props for this component.
14+
* Refer to [TransactionButtonProps](https://portal.thirdweb.com/references/typescript/v5/TransactionButtonProps) for details.
15+
* @example
16+
* ```tsx
17+
* <TransactionButton
18+
* transaction={() => {}}
19+
* onTransactionConfirmed={handleSuccess}
20+
* onError={handleError}
21+
* >
22+
* Confirm Transaction
23+
* </TransactionButton>
24+
* ```
25+
* @component
26+
*/
27+
export function TransactionButton(props: TransactionButtonProps) {
28+
const {
29+
children,
30+
transaction,
31+
onTransactionSent,
32+
onTransactionConfirmed,
33+
onError,
34+
onClick,
35+
gasless,
36+
payModal,
37+
disabled,
38+
unstyled,
39+
...buttonProps
40+
} = props;
41+
const { account, handleClick, isPending } = useTransactionButtonCore(props);
42+
const theme = parseTheme(buttonProps.theme);
43+
44+
return (
45+
<ThemedButton
46+
disabled={!account || disabled || isPending}
47+
variant={"primary"}
48+
onPress={handleClick}
49+
style={buttonProps.style as StyleProp<ViewStyle>}
50+
theme={theme}
51+
>
52+
<View style={{ opacity: isPending ? 0 : 1 }}>{children}</View>
53+
{isPending && (
54+
<View
55+
style={{
56+
position: "absolute",
57+
flex: 1,
58+
justifyContent: "center",
59+
alignItems: "center",
60+
top: 0,
61+
bottom: 0,
62+
margin: "auto",
63+
}}
64+
>
65+
<ThemedSpinner color={theme.colors.primaryButtonText} />
66+
</View>
67+
)}
68+
</ThemedButton>
69+
);
70+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import {
2+
StyleSheet,
3+
TouchableOpacity,
4+
type TouchableOpacityProps,
5+
} from "react-native";
6+
import type { Theme } from "../../../core/design-system/index.js";
7+
8+
export type ThemedButtonProps = TouchableOpacityProps & {
9+
theme: Theme;
10+
variant?: "primary" | "secondary";
11+
};
12+
13+
export function ThemedButton(props: ThemedButtonProps) {
14+
const variant = props.variant ?? "primary";
15+
const bg = props.theme.colors.primaryButtonBg;
16+
const { style: styleOverride, ...restProps } = props;
17+
return (
18+
<TouchableOpacity
19+
activeOpacity={0.5}
20+
style={[
21+
styles.button,
22+
{
23+
borderColor: variant === "secondary" ? bg : "transparent",
24+
borderWidth: variant === "secondary" ? 1 : 0,
25+
backgroundColor: variant === "secondary" ? "transparent" : bg,
26+
},
27+
styleOverride,
28+
]}
29+
{...restProps}
30+
>
31+
{props.children}
32+
</TouchableOpacity>
33+
);
34+
}
35+
36+
const styles = StyleSheet.create({
37+
button: {
38+
flex: 1,
39+
flexDirection: "row",
40+
gap: 8,
41+
padding: 12,
42+
borderRadius: 6,
43+
justifyContent: "center",
44+
alignItems: "center",
45+
},
46+
});
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { ActivityIndicator, type ActivityIndicatorProps } from "react-native";
2+
3+
export type ThemedSpinnerProps = ActivityIndicatorProps;
4+
5+
export function ThemedSpinner(props: ThemedSpinnerProps) {
6+
return <ActivityIndicator {...props} />;
7+
}

0 commit comments

Comments
 (0)