Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/famous-tomatoes-change.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"thirdweb": minor
---

TransactionButton react native implementation
3 changes: 3 additions & 0 deletions packages/thirdweb/src/exports/react-native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ export {
} from "../react/core/hooks/connection/AutoConnect.js";
export { useAutoConnect } from "../react/core/hooks/connection/useAutoConnect.js";

export { TransactionButton } from "../react/native/ui/TransactionButton/TrabsactionButton.js";
export type { TransactionButtonProps } from "../react/core/hooks/transaction/button-core.js";

// wallet info
export {
useWalletInfo,
Expand Down
10 changes: 4 additions & 6 deletions packages/thirdweb/src/exports/react.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
export { darkTheme, lightTheme } from "../react/web/ui/design-system/index.js";
export { darkTheme, lightTheme } from "../react/core/design-system/index.js";
export type {
Theme,
ThemeOverrides,
} from "../react/web/ui/design-system/index.js";
} from "../react/core/design-system/index.js";

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

export {
TransactionButton,
type TransactionButtonProps,
} from "../react/web/ui/TransactionButton/index.js";
export { TransactionButton } from "../react/web/ui/TransactionButton/index.js";
export type { TransactionButtonProps } from "../react/core/hooks/transaction/button-core.js";

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,20 @@ export function CustomThemeProvider(props: {
theme: "light" | "dark" | Theme;
}) {
const { theme, children } = props;
const themeObj = parseTheme(theme);

return (
<CustomThemeCtx.Provider value={themeObj}>
{children}
</CustomThemeCtx.Provider>
);
}

export function parseTheme(theme: "light" | "dark" | Theme | undefined): Theme {
if (!theme) {
return darkThemeObj;
}

let themeObj: Theme;

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

return (
<CustomThemeCtx.Provider value={themeObj}>
{children}
</CustomThemeCtx.Provider>
);
return themeObj;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ type ThemeColors = {
};

const darkColors = {
base1: "hsl(230deg 11.63% 8.43%)",
base2: "hsl(230deg 11.63% 12%)",
base3: "hsl(230deg 11.63% 15%)",
base4: "hsl(230deg 11.63% 17%)",
base1: "hsl(230 11.63% 8.43%)",
base2: "hsl(230 11.63% 12%)",
base3: "hsl(230 11.63% 15%)",
base4: "hsl(230 11.63% 17%)",
primaryText: "#eeeef0",
secondaryText: "#7c7a85",
danger: "#e5484D",
Expand Down
158 changes: 158 additions & 0 deletions packages/thirdweb/src/react/core/hooks/transaction/button-core.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { useState } from "react";
import type { TransactionReceipt } from "viem";
import type { GaslessOptions } from "../../../../transaction/actions/gasless/types.js";
import {
type WaitForReceiptOptions,
waitForReceipt,
} from "../../../../transaction/actions/wait-for-tx-receipt.js";
import type { PreparedTransaction } from "../../../../transaction/prepare-transaction.js";
import { stringify } from "../../../../utils/json.js";
import {
type SendTransactionPayModalConfig,
useSendTransaction,
} from "../../../web/hooks/useSendTransaction.js";
import type { Theme } from "../../design-system/index.js";
import { useActiveAccount } from "../wallets/wallet-hooks.js";

/**
* Props for the [`TransactionButton`](https://portal.thirdweb.com/references/typescript/v5/TransactionButton) component.
*/
export type TransactionButtonProps = {
/**
* 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
*/
transaction: () => // biome-ignore lint/suspicious/noExplicitAny: TODO: fix any
| PreparedTransaction<any>
// biome-ignore lint/suspicious/noExplicitAny: TODO: fix any
| Promise<PreparedTransaction<any>>;

/**
* Callback that will be called when the transaction is submitted onchain
* @param transactionResult - The object of type [`WaitForReceiptOptions`](https://portal.thirdweb.com/references/typescript/v5/WaitForReceiptOptions)
*/
onTransactionSent?: (transactionResult: WaitForReceiptOptions) => void;
/**
*
* Callback that will be called when the transaction is confirmed onchain.
* If this callback is set, the component will wait for the transaction to be confirmed.
* @param receipt - The transaction receipt object of type [`TransactionReceipt`](https://portal.thirdweb.com/references/typescript/v5/TransactionReceipt)
*/
onTransactionConfirmed?: (receipt: TransactionReceipt) => void;
/**
* The Error thrown when trying to send the transaction
* @param error - The `Error` object thrown
*/
onError?: (error: Error) => void;
/**
* Callback to be called when the button is clicked
*/
onClick?: () => void;
/**
* The className to apply to the button element for custom styling
*/
className?: string;
/**
* The style to apply to the button element for custom styling
*/
style?: React.CSSProperties;
/**
* Remove all default styling from the button
*/
unstyled?: boolean;
/**
* The `React.ReactNode` to be rendered inside the button
*/
children: React.ReactNode;

/**
* Configuration for gasless transactions.
* Refer to [`GaslessOptions`](https://portal.thirdweb.com/references/typescript/v5/GaslessOptions) for more details.
*/
gasless?: GaslessOptions;

/**
* The button's disabled state
*/
disabled?: boolean;

/**
* Configuration for the "Pay Modal" that opens when the user doesn't have enough funds to send a transaction.
* Set `payModal: false` to disable the "Pay Modal" popup
*
* This configuration object includes the following properties to configure the "Pay Modal" UI:
*
* ### `locale`
* The language to use for the "Pay Modal" UI. Defaults to `"en_US"`.
*
* ### `supportedTokens`
* An object of type [`SupportedTokens`](https://portal.thirdweb.com/references/typescript/v5/SupportedTokens) to configure the tokens to show for a chain.
*
* ### `theme`
* The theme to use for the "Pay Modal" UI. Defaults to `"dark"`.
*
* 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.
*
* Refer to [`lightTheme`](https://portal.thirdweb.com/references/typescript/v5/lightTheme)
* or [`darkTheme`](https://portal.thirdweb.com/references/typescript/v5/darkTheme) helper functions to use the default light or dark theme and customize it.
*/
payModal?: SendTransactionPayModalConfig;

/**
* The theme to use for the button
*/
theme?: "dark" | "light" | Theme;
};

export function useTransactionButtonCore(props: TransactionButtonProps) {
const {
transaction,
onTransactionSent,
onTransactionConfirmed,
onError,
onClick,
gasless,
payModal,
} = props;
const account = useActiveAccount();
const [isPending, setIsPending] = useState(false);

const sendTransaction = useSendTransaction({
gasless,
payModal,
});

const handleClick = async () => {
if (onClick) {
onClick();
}
try {
setIsPending(true);
const resolvedTx = await transaction();
const result = await sendTransaction.mutateAsync(resolvedTx);

if (onTransactionSent) {
onTransactionSent(result);
}

if (onTransactionConfirmed) {
const receipt = await waitForReceipt(result);
if (receipt.status === "reverted") {
throw new Error(`Execution reverted: ${stringify(receipt, null, 2)}`);
}
onTransactionConfirmed(receipt);
}
} catch (error) {
if (onError) {
onError(error as Error);
}
} finally {
setIsPending(false);
}
};

return {
account,
handleClick,
isPending,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { type StyleProp, View, type ViewStyle } from "react-native";
import { parseTheme } from "../../../core/design-system/CustomThemeProvider.js";
import {
type TransactionButtonProps,
useTransactionButtonCore,
} from "../../../core/hooks/transaction/button-core.js";
import { ThemedButton } from "../components/button.js";
import { ThemedSpinner } from "../components/spinner.js";

/**
* TransactionButton component is used to render a button that triggers a transaction.
* - It shows a "Switch Network" button if the connected wallet is on a different chain than the transaction.
* @param props - The props for this component.
* Refer to [TransactionButtonProps](https://portal.thirdweb.com/references/typescript/v5/TransactionButtonProps) for details.
* @example
* ```tsx
* <TransactionButton
* transaction={() => {}}
* onTransactionConfirmed={handleSuccess}
* onError={handleError}
* >
* Confirm Transaction
* </TransactionButton>
* ```
* @component
*/
export function TransactionButton(props: TransactionButtonProps) {
const {
children,
transaction,
onTransactionSent,
onTransactionConfirmed,
onError,
onClick,
gasless,
payModal,
disabled,
unstyled,
...buttonProps
} = props;
const { account, handleClick, isPending } = useTransactionButtonCore(props);
const theme = parseTheme(buttonProps.theme);

return (
<ThemedButton
disabled={!account || disabled || isPending}
variant={"primary"}
onPress={handleClick}
style={buttonProps.style as StyleProp<ViewStyle>}
theme={theme}
>
<View style={{ opacity: isPending ? 0 : 1 }}>{children}</View>
{isPending && (
<View
style={{
position: "absolute",
flex: 1,
justifyContent: "center",
alignItems: "center",
top: 0,
bottom: 0,
margin: "auto",
}}
>
<ThemedSpinner color={theme.colors.primaryButtonText} />
</View>
)}
</ThemedButton>
);
}
46 changes: 46 additions & 0 deletions packages/thirdweb/src/react/native/ui/components/button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import {
StyleSheet,
TouchableOpacity,
type TouchableOpacityProps,
} from "react-native";
import type { Theme } from "../../../core/design-system/index.js";

export type ThemedButtonProps = TouchableOpacityProps & {
theme: Theme;
variant?: "primary" | "secondary";
};

export function ThemedButton(props: ThemedButtonProps) {
const variant = props.variant ?? "primary";
const bg = props.theme.colors.primaryButtonBg;
const { style: styleOverride, ...restProps } = props;
return (
<TouchableOpacity
activeOpacity={0.5}
style={[
styles.button,
{
borderColor: variant === "secondary" ? bg : "transparent",
borderWidth: variant === "secondary" ? 1 : 0,
backgroundColor: variant === "secondary" ? "transparent" : bg,
},
styleOverride,
]}
{...restProps}
>
{props.children}
</TouchableOpacity>
);
}

const styles = StyleSheet.create({
button: {
flex: 1,
flexDirection: "row",
gap: 8,
padding: 12,
borderRadius: 6,
justifyContent: "center",
alignItems: "center",
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { ActivityIndicator, type ActivityIndicatorProps } from "react-native";

export type ThemedSpinnerProps = ActivityIndicatorProps;

export function ThemedSpinner(props: ThemedSpinnerProps) {
return <ActivityIndicator {...props} />;
}
Loading