Skip to content

Commit

Permalink
Add onPurchaseSuccess callback for Pay (thirdweb-dev#4101)
Browse files Browse the repository at this point in the history
## 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 -->
  • Loading branch information
MananTank committed Aug 14, 2024
1 parent 874ef7a commit 03a809a
Show file tree
Hide file tree
Showing 14 changed files with 173 additions and 3 deletions.
52 changes: 52 additions & 0 deletions .changeset/fair-squids-provide.md
Original file line number Diff line number Diff line change
@@ -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
<PayEmbed
client={client}
payOptions={{
onPurchaseSuccess(info) {
console.log("purchase success", info);
},
}}
/>
```

```tsx
<ConnectButton
client={client}
detailsModal={{
payOptions: {
onPurchaseSuccess(info) {
console.log("purchase success", info);
},
},
}}
/>
```

```tsx
<TransactionButton
transaction={...}
payModal={{
onPurchaseSuccess(info) {
console.log("purchase success", info);
},
}}
>
Some Transaction
</TransactionButton>
```

```ts
const sendTransaction = useSendTransaction({
payModal: {
onPurchaseSuccess(info) {
console.log("purchase success", info);
},
},
});
```
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,8 @@
},
"[json]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[markdown]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}
1 change: 1 addition & 0 deletions apps/hardhat-boilerplate
Submodule hardhat-boilerplate added at 638fd5
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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)
>;

Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ export function useSendTransaction(config: SendTransactionConfig = {}) {
mode: "transaction",
transaction: data.tx,
metadata: payModal?.metadata,
onPurchaseSuccess: payModal?.onPurchaseSuccess,
}}
/>,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -259,6 +281,7 @@ function BuyScreenContent(props: BuyScreenContentProps) {
id: "buy-with-crypto",
});
}}
onSuccess={onSwapSuccess}
/>
);
}
Expand All @@ -284,6 +307,7 @@ function BuyScreenContent(props: BuyScreenContentProps) {
onDone={onDone}
isEmbed={props.isEmbed}
payer={payer}
onSuccess={onFiatSuccess}
/>
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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>(
Expand Down Expand Up @@ -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
Expand All @@ -120,6 +135,7 @@ export function FiatFlow(props: {
transactionMode={props.transactionMode}
isEmbed={props.isEmbed}
payer={props.payer}
onSuccess={onPostOnrampSuccess}
/>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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
Expand Down Expand Up @@ -130,6 +132,7 @@ export function PostOnRampSwap(props: {
}}
transactionMode={props.transactionMode}
isEmbed={props.isEmbed}
onSuccess={props.onSuccess}
/>
);
}
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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
Expand All @@ -38,6 +40,7 @@ export function PostOnRampSwapFlow(props: {
transactionMode={props.transactionMode}
isEmbed={props.isEmbed}
payer={props.payer}
onSuccess={props.onSuccess}
/>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ export function FiatDetailsScreen(props: {
setStopPolling(true);
}}
payer={props.payer}
// viewing history - ignore onSuccess
onSuccess={undefined}
/>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -20,6 +21,7 @@ type SwapFlowProps = {
onTryAgain: () => void;
transactionMode: boolean;
isEmbed: boolean;
onSuccess: ((status: BuyWithCryptoStatus) => void) | undefined;
};

export function SwapFlow(props: SwapFlowProps) {
Expand Down Expand Up @@ -84,6 +86,7 @@ export function SwapFlow(props: SwapFlowProps) {
transactionMode={props.transactionMode}
isEmbed={props.isEmbed}
quote={quote}
onSuccess={props.onSuccess}
/>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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,
Expand All @@ -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(() => {
Expand Down

0 comments on commit 03a809a

Please sign in to comment.