diff --git a/.changeset/better-lines-rule.md b/.changeset/better-lines-rule.md new file mode 100644 index 00000000000..ccf5ccf51e9 --- /dev/null +++ b/.changeset/better-lines-rule.md @@ -0,0 +1,13 @@ +--- +"thirdweb": patch +--- + +Update the `onSuccess`, `onError`, and `onCancel` callback props of the `BuyWidget` to be called with the `quote` object + +```tsx + console.log("Swap completed:", quote)} + onError={(error, quote) => console.error("Swap failed:", error, quote)} + onCancel={(quote) => console.log("Swap cancelled:", quote)} +/> +``` diff --git a/apps/dashboard/src/@/analytics/report.ts b/apps/dashboard/src/@/analytics/report.ts index bbea78f2ab8..0712427ff9b 100644 --- a/apps/dashboard/src/@/analytics/report.ts +++ b/apps/dashboard/src/@/analytics/report.ts @@ -225,7 +225,7 @@ type AssetContractType = */ export function reportAssetBuySuccessful(properties: { chainId: number; - contractType: AssetContractType; + contractType: AssetContractType | undefined; assetType: "nft" | "coin"; }) { posthog.capture("asset buy successful", { @@ -243,9 +243,51 @@ type TokenSwapParams = { pageType: "asset" | "bridge" | "chain"; }; +type TokenBuyParams = { + buyTokenChainId: number | undefined; + buyTokenAddress: string | undefined; + pageType: "asset" | "bridge" | "chain"; +}; + +/** + * ### Why do we need to report this event? + * - To track number of successful token buys + * - To track which tokens are being bought the most + * + * ### Who is responsible for this event? + * @MananTank + */ +export function reportTokenBuySuccessful(properties: TokenBuyParams) { + posthog.capture("token buy successful", properties); +} + +/** + * ### Why do we need to report this event? + * - To track number of failed token buys + * - To track which token buys are failing + * + * ### Who is responsible for this event? + * @MananTank + */ +export function reportTokenBuyFailed(properties: TokenBuyParams) { + posthog.capture("token buy failed", properties); +} + /** * ### Why do we need to report this event? - * - To track number of successful token swaps from the token page + * - To track number of cancelled token buys + * - To track which token buys are being cancelled + * + * ### Who is responsible for this event? + * @MananTank + */ +export function reportTokenBuyCancelled(properties: TokenBuyParams) { + posthog.capture("token buy cancelled", properties); +} + +/** + * ### Why do we need to report this event? + * - To track number of successful token swaps * - To track which tokens are being swapped the most * * ### Who is responsible for this event? @@ -271,7 +313,7 @@ export function reportSwapWidgetShown(properties: { /** * ### Why do we need to report this event? - * - To track number of failed token swaps from the token page + * - To track number of failed token swaps * - To track which tokens are being swapped the most * * ### Who is responsible for this event? @@ -287,7 +329,7 @@ export function reportTokenSwapFailed( /** * ### Why do we need to report this event? - * - To track number of cancelled token swaps from the token page + * - To track number of cancelled token swaps * - To track which tokens are being swapped the most * * ### Who is responsible for this event? @@ -299,7 +341,7 @@ export function reportTokenSwapCancelled(properties: TokenSwapParams) { /** * ### Why do we need to report this event? - * - To track number of failed asset purchases from the token page + * - To track number of failed asset purchases * - To track the errors that users encounter when trying to purchase an asset * * ### Who is responsible for this event? @@ -307,7 +349,7 @@ export function reportTokenSwapCancelled(properties: TokenSwapParams) { */ export function reportAssetBuyFailed(properties: { chainId: number; - contractType: AssetContractType; + contractType: AssetContractType | undefined; assetType: "nft" | "coin"; error: string; }) { @@ -329,7 +371,7 @@ export function reportAssetBuyFailed(properties: { */ export function reportAssetBuyCancelled(properties: { chainId: number; - contractType: AssetContractType; + contractType: AssetContractType | undefined; assetType: "nft" | "coin"; }) { posthog.capture("asset buy cancelled", { diff --git a/apps/dashboard/src/@/components/blocks/BuyAndSwapEmbed.tsx b/apps/dashboard/src/@/components/blocks/BuyAndSwapEmbed.tsx index ae4f30a9955..ab67e638b6c 100644 --- a/apps/dashboard/src/@/components/blocks/BuyAndSwapEmbed.tsx +++ b/apps/dashboard/src/@/components/blocks/BuyAndSwapEmbed.tsx @@ -9,6 +9,9 @@ import { reportAssetBuyFailed, reportAssetBuySuccessful, reportSwapWidgetShown, + reportTokenBuyCancelled, + reportTokenBuyFailed, + reportTokenBuySuccessful, reportTokenSwapCancelled, reportTokenSwapFailed, reportTokenSwapSuccessful, @@ -66,32 +69,81 @@ export function BuyAndSwapEmbed(props: { connectOptions={{ autoConnect: false, }} - onError={(e) => { + onError={(e, quote) => { const errorMessage = parseError(e); + + reportTokenBuyFailed({ + buyTokenChainId: + quote?.type === "buy" + ? quote.intent.destinationChainId + : quote?.type === "onramp" + ? quote.intent.chainId + : undefined, + buyTokenAddress: + quote?.type === "buy" + ? quote.intent.destinationTokenAddress + : quote?.type === "onramp" + ? quote.intent.tokenAddress + : undefined, + pageType: props.pageType, + }); + if (props.pageType === "asset") { reportAssetBuyFailed({ assetType: "coin", chainId: props.chain.id, - contractType: "DropERC20", error: errorMessage, + contractType: undefined, }); } }} - onCancel={() => { + onCancel={(quote) => { + reportTokenBuyCancelled({ + buyTokenChainId: + quote?.type === "buy" + ? quote.intent.destinationChainId + : quote?.type === "onramp" + ? quote.intent.chainId + : undefined, + buyTokenAddress: + quote?.type === "buy" + ? quote.intent.destinationTokenAddress + : quote?.type === "onramp" + ? quote.intent.tokenAddress + : undefined, + pageType: props.pageType, + }); + if (props.pageType === "asset") { reportAssetBuyCancelled({ assetType: "coin", chainId: props.chain.id, - contractType: "DropERC20", + contractType: undefined, }); } }} - onSuccess={() => { + onSuccess={(quote) => { + reportTokenBuySuccessful({ + buyTokenChainId: + quote.type === "buy" + ? quote.intent.destinationChainId + : quote.type === "onramp" + ? quote.intent.chainId + : undefined, + buyTokenAddress: + quote.type === "buy" + ? quote.intent.destinationTokenAddress + : quote.type === "onramp" + ? quote.intent.tokenAddress + : undefined, + pageType: props.pageType, + }); + if (props.pageType === "asset") { reportAssetBuySuccessful({ assetType: "coin", chainId: props.chain.id, - contractType: "DropERC20", + contractType: undefined, }); } }} diff --git a/packages/thirdweb/src/react/web/ui/Bridge/BridgeOrchestrator.tsx b/packages/thirdweb/src/react/web/ui/Bridge/BridgeOrchestrator.tsx index 8371bd1ba6d..7b29dafca23 100644 --- a/packages/thirdweb/src/react/web/ui/Bridge/BridgeOrchestrator.tsx +++ b/packages/thirdweb/src/react/web/ui/Bridge/BridgeOrchestrator.tsx @@ -82,17 +82,17 @@ export interface BridgeOrchestratorProps { /** * Called when the flow is completed successfully */ - onComplete: () => void; + onComplete: (quote: BridgePrepareResult) => void; /** * Called when the flow encounters an error */ - onError: (error: Error) => void; + onError: (error: Error, quote: BridgePrepareResult | undefined) => void; /** * Called when the user cancels the flow */ - onCancel: () => void; + onCancel: (quote: BridgePrepareResult | undefined) => void; /** * Connect options for wallet connection @@ -189,19 +189,22 @@ export function BridgeOrchestrator({ }, [send, uiOptions.mode]); // Handle post-buy transaction completion - const handlePostBuyTransactionComplete = useCallback(() => { - onComplete?.(); - send({ type: "RESET" }); - }, [onComplete, send]); + const handlePostBuyTransactionComplete = useCallback( + (quote: BridgePrepareResult) => { + onComplete?.(quote); + send({ type: "RESET" }); + }, + [onComplete, send], + ); // Handle errors const handleError = useCallback( (error: Error) => { console.error(error); - onError?.(error); + onError?.(error, state.context.quote); send({ error, type: "ERROR_OCCURRED" }); }, - [onError, send], + [onError, send, state.context.quote], ); // Handle payment method selection @@ -227,10 +230,13 @@ export function BridgeOrchestrator({ // Handle execution complete const handleExecutionComplete = useCallback( - (completedStatuses: CompletedStatusResult[]) => { + ( + completedStatuses: CompletedStatusResult[], + quote: BridgePrepareResult, + ) => { send({ completedStatuses, type: "EXECUTION_COMPLETE" }); if (uiOptions.mode !== "transaction") { - onComplete?.(); + onComplete?.(quote); } }, [send, onComplete, uiOptions.mode], @@ -241,6 +247,8 @@ export function BridgeOrchestrator({ send({ type: "RETRY" }); }, [send]); + const quote = state.context.quote; + // Handle requirements resolved from FundWallet and DirectPayment const handleRequirementsResolved = useCallback( (amount: string, token: TokenWithPrices, receiverAddress: Address) => { @@ -263,7 +271,7 @@ export function BridgeOrchestrator({ error={state.context.currentError} onCancel={() => { send({ type: "RESET" }); - onCancel?.(); + onCancel?.(quote); }} onRetry={handleRetry} /> @@ -369,31 +377,33 @@ export function BridgeOrchestrator({ /> )} - {state.value === "execute" && - state.context.quote && - state.context.request && ( - { - send({ type: "BACK" }); - }} - onCancel={onCancel} - onComplete={handleExecutionComplete} - request={state.context.request} - wallet={state.context.selectedPaymentMethod?.payerWallet} - windowAdapter={webWindowAdapter} - /> - )} + {state.value === "execute" && quote && state.context.request && ( + { + send({ type: "BACK" }); + }} + onCancel={() => { + onCancel(quote); + }} + onComplete={(completedStatuses) => { + handleExecutionComplete(completedStatuses, quote); + }} + request={state.context.request} + wallet={state.context.selectedPaymentMethod?.payerWallet} + windowAdapter={webWindowAdapter} + /> + )} {state.value === "success" && - state.context.quote && + quote && state.context.completedStatuses && ( handlePostBuyTransactionComplete(quote)} onTxSent={() => { // Do nothing }} diff --git a/packages/thirdweb/src/react/web/ui/Bridge/BuyWidget.tsx b/packages/thirdweb/src/react/web/ui/Bridge/BuyWidget.tsx index e7beb51f858..e0aa02f54f8 100644 --- a/packages/thirdweb/src/react/web/ui/Bridge/BuyWidget.tsx +++ b/packages/thirdweb/src/react/web/ui/Bridge/BuyWidget.tsx @@ -23,6 +23,7 @@ import { CustomThemeProvider } from "../../../core/design-system/CustomThemeProv import type { Theme } from "../../../core/design-system/index.js"; import type { SiweAuthOptions } from "../../../core/hooks/auth/useSiweAuth.js"; import type { ConnectButton_connectModalOptions } from "../../../core/hooks/connection/ConnectButtonProps.js"; +import type { BridgePrepareResult } from "../../../core/hooks/useBridgePrepare.js"; import type { SupportedTokens } from "../../../core/utils/defaultTokens.js"; import { useConnectLocale } from "../ConnectWallet/locale/getConnectLocale.js"; import { EmbedContainer } from "../ConnectWallet/Modal/ConnectEmbed.js"; @@ -32,6 +33,11 @@ import type { LocaleId } from "../types.js"; import { BridgeOrchestrator, type UIOptions } from "./BridgeOrchestrator.js"; import { UnsupportedTokenScreen } from "./UnsupportedTokenScreen.js"; +type BuyOrOnrampPrepareResult = Extract< + BridgePrepareResult, + { type: "buy" | "onramp" } +>; + export type BuyWidgetProps = { /** * Customize the supported tokens that users can pay with. @@ -155,17 +161,17 @@ export type BuyWidgetProps = { /** * Callback triggered when the purchase is successful. */ - onSuccess?: () => void; + onSuccess?: (quote: BuyOrOnrampPrepareResult) => void; /** * Callback triggered when the purchase encounters an error. */ - onError?: (error: Error) => void; + onError?: (error: Error, quote: BuyOrOnrampPrepareResult | undefined) => void; /** * Callback triggered when the user cancels the purchase. */ - onCancel?: () => void; + onCancel?: (quote: BuyOrOnrampPrepareResult | undefined) => void; /** * @hidden @@ -447,14 +453,23 @@ export function BuyWidget(props: BuyWidgetProps) { client={props.client} connectLocale={localeQuery.data} connectOptions={props.connectOptions} - onCancel={() => { - props.onCancel?.(); + onCancel={(quote) => { + // type guard + if (quote?.type === "buy" || quote?.type === "onramp") { + props.onCancel?.(quote); + } }} - onComplete={() => { - props.onSuccess?.(); + onComplete={(quote) => { + // type guard + if (quote?.type === "buy" || quote?.type === "onramp") { + props.onSuccess?.(quote); + } }} - onError={(err: Error) => { - props.onError?.(err); + onError={(err: Error, quote) => { + // type guard + if (quote?.type === "buy" || quote?.type === "onramp") { + props.onError?.(err, quote); + } }} paymentLinkId={props.paymentLinkId} paymentMethods={props.paymentMethods}