Skip to content

Commit 300e4c8

Browse files
MananTankjnsdls
andauthored
feat: Integrate Buy Modal with useSendTransaction hook (#2767)
Signed-off-by: Manan Tank <manantankm@gmail.com> Co-authored-by: Jonas Daniels <jonas.daniels@outlook.com>
1 parent 8cb8a74 commit 300e4c8

Some content is hidden

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

43 files changed

+934
-245
lines changed

.changeset/perfect-ducks-walk.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
"thirdweb": minor
3+
---
4+
5+
Open "Buy Modal" UI when sending transaction using the `useSendTransaction` hook if the user does not have enough funds to execute the transaction to prompt the user to buy tokens
6+
7+
`useSendTransaction` now takes an optional `config` option to customize the "Buy Modal" UI
8+
9+
```tsx
10+
const sendTransaction = useSendTransaction({
11+
buyModal: {
12+
locale: 'en_US',
13+
theme: 'light'
14+
}
15+
});
16+
```

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

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export {
1717

1818
// contract related
1919
export { useReadContract } from "../react/core/hooks/contract/useReadContract.js";
20-
export { useSendTransaction } from "../react/core/hooks/contract/useSendTransaction.js";
20+
2121
export { useEstimateGas } from "../react/core/hooks/contract/useEstimateGas.js";
2222
export { useWaitForReceipt } from "../react/core/hooks/contract/useWaitForReceipt.js";
2323
export { useContractEvents } from "../react/core/hooks/contract/useContractEvents.js";
@@ -45,3 +45,20 @@ export {
4545
useBuyWithCryptoHistory,
4646
type BuyWithCryptoHistoryQueryParams,
4747
} from "../react/core/hooks/pay/useBuyWithCryptoHistory.js";
48+
49+
import { useSendTransactionCore } from "../react/core/hooks/contract/useSendTransaction.js";
50+
51+
/**
52+
* A hook to send a transaction.
53+
* @returns A mutation object to send a transaction.
54+
* @example
55+
* ```jsx
56+
* import { useSendTransaction } from "thirdweb/react-native";
57+
* const { mutate: sendTx, data: transactionResult } = useSendTransaction();
58+
*
59+
* // later
60+
* sendTx(tx);
61+
* ```
62+
* @transaction
63+
*/
64+
export const useSendTransaction = useSendTransactionCore;

packages/thirdweb/src/exports/react.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@ export {
2929
export { ThirdwebProvider } from "../react/core/providers/thirdweb-provider.js";
3030

3131
// tokens
32-
export type { SupportedTokens } from "../react/web/ui/ConnectWallet/defaultTokens.js";
32+
export type {
33+
SupportedTokens,
34+
TokenInfo,
35+
} from "../react/web/ui/ConnectWallet/defaultTokens.js";
3336
export { defaultTokens } from "../react/web/ui/ConnectWallet/defaultTokens.js";
3437

3538
// Media Renderer
@@ -53,7 +56,10 @@ export {
5356

5457
// contract related
5558
export { useReadContract } from "../react/core/hooks/contract/useReadContract.js";
56-
export { useSendTransaction } from "../react/core/hooks/contract/useSendTransaction.js";
59+
export {
60+
useSendTransaction,
61+
type SendTransactionConfig,
62+
} from "../react/web/hooks/useSendTransaction.js";
5763
export { useSendBatchTransaction } from "../react/core/hooks/contract/useSendBatchTransaction.js";
5864
export { useSendAndConfirmTransaction } from "../react/core/hooks/contract/useSendAndConfirmTransaction.js";
5965
export { useEstimateGas } from "../react/core/hooks/contract/useEstimateGas.js";
Lines changed: 106 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,22 @@
11
import { type UseMutationResult, useMutation } from "@tanstack/react-query";
2+
import { estimateGasCost } from "../../../../exports/transaction.js";
3+
import { resolvePromisedValue } from "../../../../exports/utils.js";
4+
import { getWalletBalance } from "../../../../exports/wallets.js";
25
import { sendTransaction } from "../../../../transaction/actions/send-transaction.js";
36
import type { WaitForReceiptOptions } from "../../../../transaction/actions/wait-for-tx-receipt.js";
47
import type { PreparedTransaction } from "../../../../transaction/prepare-transaction.js";
8+
import type { GetWalletBalanceResult } from "../../../../wallets/utils/getWalletBalance.js";
9+
import { fetchSwapSupportedChains } from "../../../web/ui/ConnectWallet/screens/Buy/swap/useSwapSupportedChains.js";
510
import { useActiveAccount } from "../wallets/wallet-hooks.js";
611

12+
type ShowModalData = {
13+
tx: PreparedTransaction;
14+
sendTx: () => void;
15+
rejectTx: () => void;
16+
totalCostWei: bigint;
17+
walletBalance: GetWalletBalanceResult;
18+
};
19+
720
/**
821
* A hook to send a transaction.
922
* @returns A mutation object to send a transaction.
@@ -15,24 +28,107 @@ import { useActiveAccount } from "../wallets/wallet-hooks.js";
1528
* // later
1629
* sendTx(tx);
1730
* ```
18-
* @transaction
31+
* @internal
1932
*/
20-
export function useSendTransaction(): UseMutationResult<
21-
WaitForReceiptOptions,
22-
Error,
23-
PreparedTransaction
24-
> {
33+
export function useSendTransactionCore(
34+
showBuyModal?: (data: ShowModalData) => void,
35+
): UseMutationResult<WaitForReceiptOptions, Error, PreparedTransaction> {
2536
const account = useActiveAccount();
2637

2738
return useMutation({
28-
mutationFn: async (transaction) => {
39+
mutationFn: async (tx) => {
2940
if (!account) {
3041
throw new Error("No active account");
3142
}
32-
return await sendTransaction({
33-
transaction,
34-
account,
43+
44+
if (!showBuyModal) {
45+
return sendTransaction({
46+
transaction: tx,
47+
account,
48+
});
49+
}
50+
51+
return new Promise<WaitForReceiptOptions>((resolve, reject) => {
52+
const sendTx = async () => {
53+
try {
54+
const res = await sendTransaction({
55+
transaction: tx,
56+
account,
57+
});
58+
59+
resolve(res);
60+
} catch (e) {
61+
reject(e);
62+
}
63+
};
64+
65+
(async () => {
66+
try {
67+
const swapSupportedChains = await fetchSwapSupportedChains(
68+
tx.client,
69+
);
70+
71+
const isBuySupported = swapSupportedChains.find(
72+
(c) => c.id === tx.chain.id,
73+
);
74+
75+
// buy not supported, can't show modal - send tx directly
76+
if (!isBuySupported) {
77+
sendTx();
78+
return;
79+
}
80+
81+
// buy supported, check if there is enouch balance - if not show modal to buy tokens
82+
83+
const [walletBalance, totalCostWei] = await Promise.all([
84+
getWalletBalance({
85+
address: account.address,
86+
chain: tx.chain,
87+
client: tx.client,
88+
}),
89+
getTotalTxCostForBuy(tx),
90+
]);
91+
92+
const walletBalanceWei = walletBalance.value;
93+
94+
// if enough balance, send tx
95+
if (totalCostWei < walletBalanceWei) {
96+
sendTx();
97+
return;
98+
}
99+
100+
// if not enough balance - show modal
101+
showBuyModal({
102+
tx,
103+
sendTx,
104+
rejectTx: () => {
105+
reject(new Error("Not enough balance"));
106+
},
107+
totalCostWei,
108+
walletBalance,
109+
});
110+
} catch (e) {
111+
console.error("Failed to estimate cost", e);
112+
// send it anyway?
113+
sendTx();
114+
}
115+
})();
35116
});
36117
},
37118
});
38119
}
120+
121+
export async function getTotalTxCostForBuy(tx: PreparedTransaction) {
122+
// Must pass 0 otherwise it will throw on some chains
123+
const gasCost = await estimateGasCost({
124+
transaction: { ...tx, value: 0n },
125+
});
126+
127+
const bufferCost = gasCost.wei / 10n;
128+
129+
// Note: get tx.value AFTER estimateGasCost
130+
const txValue = await resolvePromisedValue(tx.value);
131+
132+
// add 10% extra gas cost to the estimate to ensure user buys enough to cover the tx cost
133+
return gasCost.wei + bufferCost + (txValue || 0n);
134+
}

packages/thirdweb/src/react/core/hooks/others/useWalletBalance.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,17 @@ import {
33
type GetWalletBalanceOptions,
44
getWalletBalance,
55
} from "../../../../wallets/utils/getWalletBalance.js";
6-
import { useConnectUI } from "./useWalletConnectionCtx.js";
6+
7+
// NOTE: Do not use useConnectUI here - because this hook is also used outside of Connect UI context
78

89
/**
910
* Custom hook to fetch the balance of a wallet for a specific token.
1011
* @param options - The options for fetching the wallet balance.
1112
* @returns The result of the query.
1213
* @internal
1314
*/
14-
export function useWalletBalance(
15-
options: Omit<Partial<GetWalletBalanceOptions>, "client">,
16-
) {
17-
const { chain, address, tokenAddress } = options;
18-
const { client } = useConnectUI();
15+
export function useWalletBalance(options: Partial<GetWalletBalanceOptions>) {
16+
const { chain, address, tokenAddress, client } = options;
1917
const query = queryOptions({
2018
queryKey: [
2119
"walletBalance",
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { createContext } from "react";
2+
3+
export const SetRootElementContext = createContext<
4+
(el: React.ReactNode) => void
5+
>(() => {});

packages/thirdweb/src/react/core/providers/thirdweb-provider.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
} from "../../../transaction/actions/wait-for-tx-receipt.js";
77
import { isBaseTransactionOptions } from "../../../transaction/types.js";
88
import { isObjectWithKeys } from "../../../utils/type-guards.js";
9+
import { SetRootElementContext } from "./RootElementContext.js";
910

1011
/**
1112
* The ThirdwebProvider is component is a provider component that sets up the React Query client.
@@ -25,6 +26,7 @@ import { isObjectWithKeys } from "../../../utils/type-guards.js";
2526
* @component
2627
*/
2728
export function ThirdwebProvider(props: React.PropsWithChildren) {
29+
const [el, setEl] = useState<React.ReactNode>(null);
2830
const [queryClient] = useState(
2931
() =>
3032
new QueryClient({
@@ -77,7 +79,10 @@ export function ThirdwebProvider(props: React.PropsWithChildren) {
7779

7880
return (
7981
<QueryClientProvider client={queryClient}>
80-
{props.children}
82+
<SetRootElementContext.Provider value={setEl}>
83+
{props.children}
84+
</SetRootElementContext.Provider>
85+
{el}
8186
</QueryClientProvider>
8287
);
8388
}

0 commit comments

Comments
 (0)