From 03a809a33f5a026162bda15586ad8f6d00a45b3a Mon Sep 17 00:00:00 2001 From: MananTank Date: Wed, 14 Aug 2024 21:05:41 +0000 Subject: [PATCH] Add onPurchaseSuccess callback for Pay (#4101) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem solved Short description of the bug fixed or feature added --- ## 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}` --- .changeset/fair-squids-provide.md | 52 +++++++++++++++++++ .vscode/settings.json | 3 ++ apps/hardhat-boilerplate | 1 + .../hooks/connection/ConnectButtonProps.ts | 17 ++++++ .../hooks/transaction/useSendTransaction.ts | 16 ++++++ .../hooks/transaction/useSendTransaction.tsx | 1 + .../ConnectWallet/screens/Buy/BuyScreen.tsx | 24 +++++++++ .../screens/Buy/fiat/FiatFlow.tsx | 20 ++++++- .../screens/Buy/fiat/FiatStatusScreen.tsx | 15 +++++- .../screens/Buy/fiat/PostOnRampSwap.tsx | 3 ++ .../screens/Buy/fiat/PostOnRampSwapFlow.tsx | 3 ++ .../pay-transactions/FiatDetailsScreen.tsx | 2 + .../screens/Buy/swap/SwapFlow.tsx | 3 ++ .../screens/Buy/swap/SwapStatusScreen.tsx | 16 ++++++ 14 files changed, 173 insertions(+), 3 deletions(-) create mode 100644 .changeset/fair-squids-provide.md create mode 160000 apps/hardhat-boilerplate 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(() => {