diff --git a/.changeset/fair-squids-provide.md b/.changeset/fair-squids-provide.md
new file mode 100644
index 000000000..8d2d5701c
--- /dev/null
+++ b/.changeset/fair-squids-provide.md
@@ -0,0 +1,52 @@
+---
+"thirdweb": patch
+---
+
+Add `onPurchaseSuccess` callback to `PayEmbed`, `ConnectButton`, `TransactionButton` and `useSendTransaction` and gets called when user completes the purchase using thirdweb pay.
+
+```tsx
+
+```
+
+```tsx
+
+```
+
+```tsx
+
+ Some Transaction
+
+```
+
+```ts
+const sendTransaction = useSendTransaction({
+ payModal: {
+ onPurchaseSuccess(info) {
+ console.log("purchase success", info);
+ },
+ },
+});
+```
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 20262183f..0e00ef8ee 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -18,5 +18,8 @@
},
"[json]": {
"editor.defaultFormatter": "biomejs.biome"
+ },
+ "[markdown]": {
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
}
}
diff --git a/apps/hardhat-boilerplate b/apps/hardhat-boilerplate
new file mode 160000
index 000000000..638fd58ec
--- /dev/null
+++ b/apps/hardhat-boilerplate
@@ -0,0 +1 @@
+Subproject commit 638fd58ec882990d3b654d45e37907eb15695bd2
diff --git a/packages/thirdweb/src/react/core/hooks/connection/ConnectButtonProps.ts b/packages/thirdweb/src/react/core/hooks/connection/ConnectButtonProps.ts
index e08f396ce..291c14ff9 100644
--- a/packages/thirdweb/src/react/core/hooks/connection/ConnectButtonProps.ts
+++ b/packages/thirdweb/src/react/core/hooks/connection/ConnectButtonProps.ts
@@ -1,5 +1,7 @@
import type { Chain } from "../../../../chains/types.js";
import type { ThirdwebClient } from "../../../../client/client.js";
+import type { BuyWithCryptoStatus } from "../../../../pay/buyWithCrypto/getStatus.js";
+import type { BuyWithFiatStatus } from "../../../../pay/buyWithFiat/getStatus.js";
import type { PreparedTransaction } from "../../../../transaction/prepare-transaction.js";
import type { Prettify } from "../../../../utils/type-utils.js";
import type { Account, Wallet } from "../../../../wallets/interfaces/wallet.js";
@@ -89,6 +91,21 @@ export type PayUIOptions = Prettify<
* This details will be stored with the purchase and can be retrieved later via the status API or Webhook
*/
purchaseData?: object;
+
+ /**
+ * Callback to be called when the user successfully completes the purchase.
+ */
+ onPurchaseSuccess?: (
+ info:
+ | {
+ type: "crypto";
+ status: BuyWithCryptoStatus;
+ }
+ | {
+ type: "fiat";
+ status: BuyWithFiatStatus;
+ },
+ ) => void;
} & (FundWalletOptions | DirectPaymentOptions | TranasctionOptions)
>;
diff --git a/packages/thirdweb/src/react/core/hooks/transaction/useSendTransaction.ts b/packages/thirdweb/src/react/core/hooks/transaction/useSendTransaction.ts
index adbea58d2..252cc78b6 100644
--- a/packages/thirdweb/src/react/core/hooks/transaction/useSendTransaction.ts
+++ b/packages/thirdweb/src/react/core/hooks/transaction/useSendTransaction.ts
@@ -1,5 +1,7 @@
import { type UseMutationResult, useMutation } from "@tanstack/react-query";
import type { Chain } from "../../../../chains/types.js";
+import type { BuyWithCryptoStatus } from "../../../../pay/buyWithCrypto/getStatus.js";
+import type { BuyWithFiatStatus } from "../../../../pay/buyWithFiat/getStatus.js";
import type { GaslessOptions } from "../../../../transaction/actions/gasless/types.js";
import { sendTransaction } from "../../../../transaction/actions/send-transaction.js";
import type { WaitForReceiptOptions } from "../../../../transaction/actions/wait-for-tx-receipt.js";
@@ -46,6 +48,20 @@ export type SendTransactionPayModalConfig =
testMode?: boolean;
};
purchaseData?: object;
+ /**
+ * Callback to be called when the user successfully completes the purchase.
+ */
+ onPurchaseSuccess?: (
+ info:
+ | {
+ type: "crypto";
+ status: BuyWithCryptoStatus;
+ }
+ | {
+ type: "fiat";
+ status: BuyWithFiatStatus;
+ },
+ ) => void;
}
| false;
diff --git a/packages/thirdweb/src/react/web/hooks/transaction/useSendTransaction.tsx b/packages/thirdweb/src/react/web/hooks/transaction/useSendTransaction.tsx
index b0f3dcac4..2192635ea 100644
--- a/packages/thirdweb/src/react/web/hooks/transaction/useSendTransaction.tsx
+++ b/packages/thirdweb/src/react/web/hooks/transaction/useSendTransaction.tsx
@@ -132,6 +132,7 @@ export function useSendTransaction(config: SendTransactionConfig = {}) {
mode: "transaction",
transaction: data.tx,
metadata: payModal?.metadata,
+ onPurchaseSuccess: payModal?.onPurchaseSuccess,
}}
/>,
);
diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/BuyScreen.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/BuyScreen.tsx
index a09e53f09..c4185a71b 100644
--- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/BuyScreen.tsx
+++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/BuyScreen.tsx
@@ -4,6 +4,8 @@ import type { Chain } from "../../../../../../chains/types.js";
import type { ThirdwebClient } from "../../../../../../client/client.js";
import { NATIVE_TOKEN_ADDRESS } from "../../../../../../constants/addresses.js";
import type { GetBuyWithCryptoQuoteParams } from "../../../../../../pay/buyWithCrypto/getQuote.js";
+import type { BuyWithCryptoStatus } from "../../../../../../pay/buyWithCrypto/getStatus.js";
+import type { BuyWithFiatStatus } from "../../../../../../pay/buyWithFiat/getStatus.js";
import { isSwapRequiredPostOnramp } from "../../../../../../pay/buyWithFiat/isSwapRequiredPostOnramp.js";
import { formatNumber } from "../../../../../../utils/formatNumber.js";
import type { Account } from "../../../../../../wallets/interfaces/wallet.js";
@@ -204,6 +206,26 @@ function BuyScreenContent(props: BuyScreenContentProps) {
// screens ----------------------------
+ const onSwapSuccess = useCallback(
+ (_status: BuyWithCryptoStatus) => {
+ props.payOptions.onPurchaseSuccess?.({
+ type: "crypto",
+ status: _status,
+ });
+ },
+ [props.payOptions.onPurchaseSuccess],
+ );
+
+ const onFiatSuccess = useCallback(
+ (_status: BuyWithFiatStatus) => {
+ props.payOptions.onPurchaseSuccess?.({
+ type: "fiat",
+ status: _status,
+ });
+ },
+ [props.payOptions.onPurchaseSuccess],
+ );
+
if (screen.id === "connect-payer-wallet") {
return (
);
}
@@ -284,6 +307,7 @@ function BuyScreenContent(props: BuyScreenContentProps) {
onDone={onDone}
isEmbed={props.isEmbed}
payer={payer}
+ onSuccess={onFiatSuccess}
/>
);
}
diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/FiatFlow.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/FiatFlow.tsx
index 96efcf517..905bd947e 100644
--- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/FiatFlow.tsx
+++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/FiatFlow.tsx
@@ -1,7 +1,10 @@
-import { useState } from "react";
+import { useCallback, useState } from "react";
import type { ThirdwebClient } from "../../../../../../../client/client.js";
import type { BuyWithFiatQuote } from "../../../../../../../pay/buyWithFiat/getQuote.js";
-import type { BuyWithFiatStatus } from "../../../../../../../pay/buyWithFiat/getStatus.js";
+import {
+ type BuyWithFiatStatus,
+ getBuyWithFiatStatus,
+} from "../../../../../../../pay/buyWithFiat/getStatus.js";
import { isSwapRequiredPostOnramp } from "../../../../../../../pay/buyWithFiat/isSwapRequiredPostOnramp.js";
import { openOnrampPopup } from "../openOnRamppopup.js";
import { addPendingTx } from "../swap/pendingSwapTx.js";
@@ -48,6 +51,7 @@ export function FiatFlow(props: {
transactionMode: boolean;
isEmbed: boolean;
payer: PayerInfo;
+ onSuccess: (status: BuyWithFiatStatus) => void;
}) {
const hasTwoSteps = isSwapRequiredPostOnramp(props.quote);
const [screen, setScreen] = useState(
@@ -101,10 +105,21 @@ export function FiatFlow(props: {
}}
transactionMode={props.transactionMode}
isEmbed={props.isEmbed}
+ onSuccess={props.onSuccess}
/>
);
}
+ const onPostOnrampSuccess = useCallback(() => {
+ // report the status of fiat status instead of post onramp swap status when post onramp swap is successful
+ getBuyWithFiatStatus({
+ intentId: props.quote.intentId,
+ client: props.client,
+ }).then((status) => {
+ props.onSuccess(status);
+ });
+ }, [props.onSuccess, props.quote.intentId, props.client]);
+
if (screen.id === "postonramp-swap") {
return (
);
}
diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/FiatStatusScreen.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/FiatStatusScreen.tsx
index bc1b5d082..7f30f31f4 100644
--- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/FiatStatusScreen.tsx
+++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/FiatStatusScreen.tsx
@@ -41,9 +41,10 @@ export function OnrampStatusScreen(props: {
onShowSwapFlow: (status: BuyWithFiatStatus) => void;
transactionMode: boolean;
isEmbed: boolean;
+ onSuccess: ((status: BuyWithFiatStatus) => void) | undefined;
}) {
const queryClient = useQueryClient();
- const { openedWindow } = props;
+ const { openedWindow, onSuccess } = props;
const statusQuery = useBuyWithFiatStatus({
intentId: props.intentId,
client: props.client,
@@ -62,6 +63,18 @@ export function OnrampStatusScreen(props: {
uiStatus = "completed";
}
+ const purchaseCbCalled = useRef(false);
+ useEffect(() => {
+ if (purchaseCbCalled.current || !onSuccess) {
+ return;
+ }
+
+ if (statusQuery.data?.status === "ON_RAMP_TRANSFER_COMPLETED") {
+ purchaseCbCalled.current = true;
+ onSuccess(statusQuery.data);
+ }
+ }, [onSuccess, statusQuery.data]);
+
// close the onramp popup if onramp is completed
useEffect(() => {
if (!openedWindow || !statusQuery.data) {
diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/PostOnRampSwap.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/PostOnRampSwap.tsx
index ca414bee8..0f100892c 100644
--- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/PostOnRampSwap.tsx
+++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/PostOnRampSwap.tsx
@@ -2,6 +2,7 @@ import { useQuery } from "@tanstack/react-query";
import { useEffect, useState } from "react";
import type { ThirdwebClient } from "../../../../../../../client/client.js";
import type { BuyWithCryptoQuote } from "../../../../../../../pay/buyWithCrypto/getQuote.js";
+import type { BuyWithCryptoStatus } from "../../../../../../../pay/buyWithCrypto/getStatus.js";
import { getPostOnRampQuote } from "../../../../../../../pay/buyWithFiat/getPostOnRampQuote.js";
import type { BuyWithFiatStatus } from "../../../../../../../pay/buyWithFiat/getStatus.js";
import { iconSize } from "../../../../../../core/design-system/index.js";
@@ -23,6 +24,7 @@ export function PostOnRampSwap(props: {
transactionMode: boolean;
isEmbed: boolean;
payer: PayerInfo;
+ onSuccess: ((status: BuyWithCryptoStatus) => void) | undefined;
}) {
const [lockedOnRampQuote, setLockedOnRampQuote] = useState<
BuyWithCryptoQuote | undefined
@@ -130,6 +132,7 @@ export function PostOnRampSwap(props: {
}}
transactionMode={props.transactionMode}
isEmbed={props.isEmbed}
+ onSuccess={props.onSuccess}
/>
);
}
diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/PostOnRampSwapFlow.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/PostOnRampSwapFlow.tsx
index 5507fb723..1bde4e8c7 100644
--- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/PostOnRampSwapFlow.tsx
+++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/PostOnRampSwapFlow.tsx
@@ -1,5 +1,6 @@
import { useState } from "react";
import type { ThirdwebClient } from "../../../../../../../client/client.js";
+import type { BuyWithCryptoStatus } from "../../../../../../../pay/buyWithCrypto/getStatus.js";
import type { BuyWithFiatStatus } from "../../../../../../../pay/buyWithFiat/getStatus.js";
import type { PayerInfo } from "../types.js";
import { type BuyWithFiatPartialQuote, FiatSteps } from "./FiatSteps.js";
@@ -22,6 +23,7 @@ export function PostOnRampSwapFlow(props: {
transactionMode: boolean;
isEmbed: boolean;
payer: PayerInfo;
+ onSuccess: ((status: BuyWithCryptoStatus) => void) | undefined;
}) {
const [statusForSwap, setStatusForSwap] = useState<
BuyWithFiatStatus | undefined
@@ -38,6 +40,7 @@ export function PostOnRampSwapFlow(props: {
transactionMode={props.transactionMode}
isEmbed={props.isEmbed}
payer={props.payer}
+ onSuccess={props.onSuccess}
/>
);
}
diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/pay-transactions/FiatDetailsScreen.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/pay-transactions/FiatDetailsScreen.tsx
index 3ebc79c2d..d3fdadb18 100644
--- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/pay-transactions/FiatDetailsScreen.tsx
+++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/pay-transactions/FiatDetailsScreen.tsx
@@ -73,6 +73,8 @@ export function FiatDetailsScreen(props: {
setStopPolling(true);
}}
payer={props.payer}
+ // viewing history - ignore onSuccess
+ onSuccess={undefined}
/>
);
}
diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/SwapFlow.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/SwapFlow.tsx
index c2cd55dfc..672ccc8d3 100644
--- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/SwapFlow.tsx
+++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/SwapFlow.tsx
@@ -3,6 +3,7 @@ import { getCachedChain } from "../../../../../../../chains/utils.js";
import type { ThirdwebClient } from "../../../../../../../client/client.js";
import { NATIVE_TOKEN_ADDRESS } from "../../../../../../../constants/addresses.js";
import type { BuyWithCryptoQuote } from "../../../../../../../pay/buyWithCrypto/getQuote.js";
+import type { BuyWithCryptoStatus } from "../../../../../../../pay/buyWithCrypto/getStatus.js";
import type { TokenInfo } from "../../../../../../core/utils/defaultTokens.js";
import { type ERC20OrNativeToken, NATIVE_TOKEN } from "../../nativeToken.js";
import type { PayerInfo } from "../types.js";
@@ -20,6 +21,7 @@ type SwapFlowProps = {
onTryAgain: () => void;
transactionMode: boolean;
isEmbed: boolean;
+ onSuccess: ((status: BuyWithCryptoStatus) => void) | undefined;
};
export function SwapFlow(props: SwapFlowProps) {
@@ -84,6 +86,7 @@ export function SwapFlow(props: SwapFlowProps) {
transactionMode={props.transactionMode}
isEmbed={props.isEmbed}
quote={quote}
+ onSuccess={props.onSuccess}
/>
);
}
diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/SwapStatusScreen.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/SwapStatusScreen.tsx
index ad392fdca..7b788e0b9 100644
--- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/SwapStatusScreen.tsx
+++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/SwapStatusScreen.tsx
@@ -3,6 +3,7 @@ import { useQueryClient } from "@tanstack/react-query";
import { useEffect, useRef } from "react";
import type { ThirdwebClient } from "../../../../../../../client/client.js";
import type { BuyWithCryptoQuote } from "../../../../../../../pay/buyWithCrypto/getQuote.js";
+import type { BuyWithCryptoStatus } from "../../../../../../../pay/buyWithCrypto/getStatus.js";
import { iconSize } from "../../../../../../core/design-system/index.js";
import { useBuyWithCryptoStatus } from "../../../../../../core/hooks/pay/useBuyWithCryptoStatus.js";
import { invalidateWalletBalance } from "../../../../../../core/providers/invalidateWalletBalance.js";
@@ -26,7 +27,10 @@ export function SwapStatusScreen(props: {
transactionMode: boolean;
isEmbed: boolean;
quote: BuyWithCryptoQuote;
+ onSuccess: ((status: BuyWithCryptoStatus) => void) | undefined;
}) {
+ const { onSuccess } = props;
+
const swapStatus = useBuyWithCryptoStatus({
client: props.client,
transactionHash: props.swapTxHash,
@@ -46,6 +50,18 @@ export function SwapStatusScreen(props: {
uiStatus = "partialSuccess";
}
+ const purchaseCbCalled = useRef(false);
+ useEffect(() => {
+ if (purchaseCbCalled.current || !onSuccess) {
+ return;
+ }
+
+ if (swapStatus.data?.status === "COMPLETED") {
+ purchaseCbCalled.current = true;
+ onSuccess(swapStatus.data);
+ }
+ }, [onSuccess, swapStatus]);
+
const queryClient = useQueryClient();
const balanceInvalidated = useRef(false);
useEffect(() => {