Add onPurchaseSuccess callback for Pay (#4101)
## Problem solved
Short description of the bug fixed or feature added
<!-- start pr-codex -->
---
## PR-Codex overview
This PR adds `onPurchaseSuccess` callback to various components and hooks in the codebase, triggering when a user completes a purchase using thirdweb pay.
### Detailed summary
- Added `onPurchaseSuccess` callback to `PayEmbed`, `ConnectButton`, `TransactionButton`, and `useSendTransaction`
- Updated props in multiple components to include `onSuccess` callback parameter
- Added type definitions for `BuyWithCryptoStatus` and `BuyWithFiatStatus`
- Implemented logic to call `onSuccess` callback upon successful purchase completion
> The following files were skipped due to too many changes: `packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/FiatFlow.tsx`
> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`
<!-- end pr-codex -->
This commit is contained in:
@@ -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)
|
||||
>;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -132,6 +132,7 @@ export function useSendTransaction(config: SendTransactionConfig = {}) {
|
||||
mode: "transaction",
|
||||
transaction: data.tx,
|
||||
metadata: payModal?.metadata,
|
||||
onPurchaseSuccess: payModal?.onPurchaseSuccess,
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -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 (
|
||||
<WalletSwitcherConnectionScreen
|
||||
@@ -259,6 +281,7 @@ function BuyScreenContent(props: BuyScreenContentProps) {
|
||||
id: "buy-with-crypto",
|
||||
});
|
||||
}}
|
||||
onSuccess={onSwapSuccess}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -284,6 +307,7 @@ function BuyScreenContent(props: BuyScreenContentProps) {
|
||||
onDone={onDone}
|
||||
isEmbed={props.isEmbed}
|
||||
payer={payer}
|
||||
onSuccess={onFiatSuccess}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<Screen>(
|
||||
@@ -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 (
|
||||
<PostOnRampSwapFlow
|
||||
@@ -120,6 +135,7 @@ export function FiatFlow(props: {
|
||||
transactionMode={props.transactionMode}
|
||||
isEmbed={props.isEmbed}
|
||||
payer={props.payer}
|
||||
onSuccess={onPostOnrampSuccess}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+14
-1
@@ -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) {
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+3
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+2
@@ -73,6 +73,8 @@ export function FiatDetailsScreen(props: {
|
||||
setStopPolling(true);
|
||||
}}
|
||||
payer={props.payer}
|
||||
// viewing history - ignore onSuccess
|
||||
onSuccess={undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+16
@@ -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(() => {
|
||||
|
||||
Reference in New Issue
Block a user