adding api for pay direct transfers (#4224)
Co-authored-by: Joaquim Verges <[email protected]> Co-authored-by: Edward Sun <[email protected]> Co-authored-by: Edward Sun <[email protected]>
This commit is contained in:
co-authored by
Joaquim Verges
Edward Sun
Edward Sun
parent
120d61f410
commit
1405598e7e
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"thirdweb": patch
|
||||
---
|
||||
|
||||
Added support for direct transfers in Pay
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { base } from "thirdweb/chains";
|
||||
import { sepolia } from "thirdweb/chains";
|
||||
import { PayEmbed, getDefaultToken } from "thirdweb/react";
|
||||
import { THIRDWEB_CLIENT } from "../../lib/client";
|
||||
import { StyledConnectButton } from "../styled-connect-button";
|
||||
@@ -16,9 +16,9 @@ export function BuyMerchPreview() {
|
||||
payOptions={{
|
||||
mode: "direct_payment",
|
||||
paymentInfo: {
|
||||
amount: "15",
|
||||
chain: base,
|
||||
token: getDefaultToken(base, "USDC"),
|
||||
amount: "0.1",
|
||||
chain: sepolia,
|
||||
token: getDefaultToken(sepolia, "USDC"),
|
||||
sellerAddress: "0xEb0effdFB4dC5b3d5d3aC6ce29F3ED213E95d675",
|
||||
},
|
||||
metadata: {
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
|
||||
import { useTheme } from "next-themes";
|
||||
import { getContract } from "thirdweb";
|
||||
import { base, polygon } from "thirdweb/chains";
|
||||
import { polygon, sepolia } from "thirdweb/chains";
|
||||
import { transfer } from "thirdweb/extensions/erc20";
|
||||
import { claimTo, getNFT } from "thirdweb/extensions/erc1155";
|
||||
import {
|
||||
PayEmbed,
|
||||
TransactionButton,
|
||||
getDefaultToken,
|
||||
useActiveAccount,
|
||||
useReadContract,
|
||||
} from "thirdweb/react";
|
||||
@@ -21,8 +22,9 @@ const nftContract = getContract({
|
||||
});
|
||||
|
||||
const usdcContract = getContract({
|
||||
address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
||||
chain: base,
|
||||
// biome-ignore lint/style/noNonNullAssertion: its there
|
||||
address: getDefaultToken(sepolia, "USDC")!.address,
|
||||
chain: sepolia,
|
||||
client: THIRDWEB_CLIENT,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
export {
|
||||
getBuyWithCryptoQuote,
|
||||
type BuyWithCryptoQuote,
|
||||
type QuoteApprovalParams,
|
||||
type QuoteTokenInfo,
|
||||
type GetBuyWithCryptoQuoteParams,
|
||||
} from "../pay/buyWithCrypto/getQuote.js";
|
||||
|
||||
export {
|
||||
getBuyWithCryptoTransfer,
|
||||
type BuyWithCryptoTransfer,
|
||||
type GetBuyWithCryptoTransferParams,
|
||||
} from "../pay/buyWithCrypto/getTransfer.js";
|
||||
|
||||
export {
|
||||
type QuoteApprovalParams,
|
||||
type QuoteTokenInfo,
|
||||
} from "../pay/buyWithCrypto/commonTypes.js";
|
||||
|
||||
export {
|
||||
getBuyWithCryptoStatus,
|
||||
type BuyWithCryptoStatus,
|
||||
|
||||
@@ -166,11 +166,14 @@ export { toEther, toTokens, toUnits, toWei, fromGwei } from "../utils/units.js";
|
||||
export {
|
||||
getBuyWithCryptoQuote,
|
||||
type BuyWithCryptoQuote,
|
||||
type QuoteApprovalParams,
|
||||
type QuoteTokenInfo,
|
||||
type GetBuyWithCryptoQuoteParams,
|
||||
} from "../pay/buyWithCrypto/getQuote.js";
|
||||
|
||||
export {
|
||||
type QuoteApprovalParams,
|
||||
type QuoteTokenInfo,
|
||||
} from "../pay/buyWithCrypto/commonTypes.js";
|
||||
|
||||
export {
|
||||
getBuyWithCryptoStatus,
|
||||
type BuyWithCryptoStatus,
|
||||
@@ -183,6 +186,12 @@ export {
|
||||
type BuyWithCryptoHistoryParams,
|
||||
} from "../pay/buyWithCrypto/getHistory.js";
|
||||
|
||||
export {
|
||||
getBuyWithCryptoTransfer,
|
||||
type GetBuyWithCryptoTransferParams,
|
||||
type BuyWithCryptoTransfer,
|
||||
} from "../pay/buyWithCrypto/getTransfer.js";
|
||||
|
||||
export type {
|
||||
PayOnChainTransactionDetails,
|
||||
PayTokenInfo,
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { ApproveParams } from "../../extensions/erc20/write/approve.js";
|
||||
import type { BaseTransactionOptions } from "../../transaction/types.js";
|
||||
|
||||
export type QuoteTokenInfo = {
|
||||
chainId: number;
|
||||
tokenAddress: string;
|
||||
decimals: number;
|
||||
priceUSDCents: number;
|
||||
name?: string;
|
||||
symbol?: string;
|
||||
};
|
||||
|
||||
export type QuoteTransactionRequest = {
|
||||
data: string;
|
||||
to: string;
|
||||
value: string;
|
||||
from: string;
|
||||
chainId: number;
|
||||
gasPrice: string;
|
||||
gasLimit: string;
|
||||
};
|
||||
|
||||
export type QuoteApprovalInfo = {
|
||||
chainId: number;
|
||||
tokenAddress: string;
|
||||
spenderAddress: string;
|
||||
amountWei: string;
|
||||
};
|
||||
|
||||
export type QuotePaymentToken = {
|
||||
token: QuoteTokenInfo;
|
||||
amountWei: string;
|
||||
amount: string;
|
||||
amountUSDCents: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* @buyCrypto
|
||||
*/
|
||||
export type QuoteApprovalParams = BaseTransactionOptions<ApproveParams>;
|
||||
@@ -2,14 +2,16 @@ import type { Hash } from "viem";
|
||||
import { getCachedChain } from "../../chains/utils.js";
|
||||
import type { ThirdwebClient } from "../../client/client.js";
|
||||
import { getContract } from "../../contract/contract.js";
|
||||
import {
|
||||
type ApproveParams,
|
||||
approve,
|
||||
} from "../../extensions/erc20/write/approve.js";
|
||||
import { approve } from "../../extensions/erc20/write/approve.js";
|
||||
import type { PrepareTransactionOptions } from "../../transaction/prepare-transaction.js";
|
||||
import type { BaseTransactionOptions } from "../../transaction/types.js";
|
||||
import { getClientFetch } from "../../utils/fetch.js";
|
||||
import { getPayBuyWithCryptoQuoteEndpoint } from "../utils/definitions.js";
|
||||
import type {
|
||||
QuoteApprovalInfo,
|
||||
QuotePaymentToken,
|
||||
QuoteTokenInfo,
|
||||
QuoteTransactionRequest,
|
||||
} from "./commonTypes.js";
|
||||
|
||||
/**
|
||||
* The parameters for [`getBuyWithCryptoQuote`](https://portal.thirdweb.com/references/typescript/v5/getBuyWithCryptoQuote) function
|
||||
@@ -107,40 +109,9 @@ export type GetBuyWithCryptoQuoteParams = {
|
||||
/**
|
||||
* @buyCrypto
|
||||
*/
|
||||
export type QuoteTokenInfo = {
|
||||
chainId: number;
|
||||
tokenAddress: string;
|
||||
decimals: number;
|
||||
priceUSDCents: number;
|
||||
name?: string;
|
||||
symbol?: string;
|
||||
};
|
||||
|
||||
type QuotePaymentToken = {
|
||||
token: QuoteTokenInfo;
|
||||
amountWei: string;
|
||||
amount: string;
|
||||
amountUSDCents: number;
|
||||
};
|
||||
|
||||
type QuoteTransactionRequest = {
|
||||
data: string;
|
||||
to: string;
|
||||
value: string;
|
||||
from: string;
|
||||
chainId: number;
|
||||
gasPrice: string;
|
||||
gasLimit: string;
|
||||
};
|
||||
|
||||
type BuyWithCryptoQuoteRouteResponse = {
|
||||
transactionRequest: QuoteTransactionRequest;
|
||||
approval?: {
|
||||
chainId: number;
|
||||
tokenAddress: string;
|
||||
spenderAddress: string;
|
||||
amountWei: string;
|
||||
};
|
||||
approval?: QuoteApprovalInfo;
|
||||
|
||||
fromAddress: string;
|
||||
toAddress: string;
|
||||
@@ -173,11 +144,6 @@ type BuyWithCryptoQuoteRouteResponse = {
|
||||
bridge?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* @buyCrypto
|
||||
*/
|
||||
export type QuoteApprovalParams = BaseTransactionOptions<ApproveParams>;
|
||||
|
||||
/**
|
||||
* @buyCrypto
|
||||
*/
|
||||
|
||||
@@ -56,7 +56,7 @@ export type BuyWithCryptoSubStatuses =
|
||||
| "UNKNOWN_ERROR"
|
||||
| "REFUNDED";
|
||||
|
||||
export type SwapType = "SAME_CHAIN" | "CROSS_CHAIN";
|
||||
export type SwapType = "SAME_CHAIN" | "CROSS_CHAIN" | "TRANSFER";
|
||||
|
||||
/**
|
||||
* The object returned by the [`getBuyWithCryptoStatus`](https://portal.thirdweb.com/references/typescript/v5/getBuyWithCryptoStatus) function to represent the status of a quoted transaction
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
import type { Hash } from "viem";
|
||||
import { getCachedChain } from "../../chains/utils.js";
|
||||
import type { ThirdwebClient } from "../../client/client.js";
|
||||
import { getContract } from "../../contract/contract.js";
|
||||
import { approve } from "../../extensions/erc20/write/approve.js";
|
||||
import type { PrepareTransactionOptions } from "../../transaction/prepare-transaction.js";
|
||||
import type { Address } from "../../utils/address.js";
|
||||
import { getClientFetch } from "../../utils/fetch.js";
|
||||
import { getPayBuyWithCryptoTransferEndpoint } from "../utils/definitions.js";
|
||||
import type {
|
||||
QuoteApprovalInfo,
|
||||
QuotePaymentToken,
|
||||
QuoteTokenInfo,
|
||||
QuoteTransactionRequest,
|
||||
} from "./commonTypes.js";
|
||||
|
||||
/**
|
||||
* The parameters for [`getBuyWithCryptoTransfer`](https://portal.thirdweb.com/references/typescript/v5/getBuyWithCryptoTransfer) function
|
||||
* It facilitates a token transfer.
|
||||
* @buyCrypto
|
||||
*/
|
||||
export type GetBuyWithCryptoTransferParams = {
|
||||
/**
|
||||
* A client is the entry point to the thirdweb SDK. It is required for all other actions.
|
||||
*
|
||||
* You can create a client using the `createThirdwebClient` function.
|
||||
* Refer to the [Creating a Client](https://portal.thirdweb.com/typescript/v5/client) documentation for more information.
|
||||
*
|
||||
*/
|
||||
client: ThirdwebClient;
|
||||
|
||||
/**
|
||||
* The address of wallet that pays for the tokens.
|
||||
*/
|
||||
fromAddress: string;
|
||||
|
||||
/**
|
||||
* The address of the wallet where the tokens are sent
|
||||
*/
|
||||
toAddress: string;
|
||||
|
||||
/**
|
||||
* The chain id of the transfer token.
|
||||
*/
|
||||
chainId: number;
|
||||
|
||||
/**
|
||||
* The token address of the transfer token.
|
||||
*/
|
||||
tokenAddress: string;
|
||||
|
||||
/**
|
||||
* The amount of token to be transferred.
|
||||
*/
|
||||
amount: string;
|
||||
|
||||
/**
|
||||
* Extra details to store with the purchase.
|
||||
*
|
||||
* This details will be stored with the purchase and can be retrieved later via the status API or Webhook
|
||||
*/
|
||||
purchaseData?: object;
|
||||
};
|
||||
|
||||
/**
|
||||
* @buyCrypto
|
||||
*/
|
||||
type BuyWithCryptoTransferResponse = {
|
||||
quoteId: string;
|
||||
transactionRequest: QuoteTransactionRequest;
|
||||
approval?: QuoteApprovalInfo;
|
||||
fromAddress: string;
|
||||
toAddress: string;
|
||||
token: QuoteTokenInfo;
|
||||
paymentToken: QuotePaymentToken;
|
||||
processingFee: QuotePaymentToken;
|
||||
estimatedGasCostUSDCents: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* @buyCrypto
|
||||
*/
|
||||
export type BuyWithCryptoTransfer = {
|
||||
transactionRequest: PrepareTransactionOptions;
|
||||
approval?: PrepareTransactionOptions;
|
||||
fromAddress: string;
|
||||
toAddress: string;
|
||||
paymentToken: QuotePaymentToken;
|
||||
processingFee: QuotePaymentToken;
|
||||
estimatedGasCostUSDCents: number;
|
||||
client: ThirdwebClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a quote of type [`BuyWithCryptoTransfer`](https://portal.thirdweb.com/references/typescript/v5/BuyWithCryptoTransfer) to facilitate a token transfer transaction.
|
||||
* Using this instead of a native transfer allows you to receive status and webhooks about successful or failed payments.
|
||||
*
|
||||
* Once you have the quote, you can use `prepareTransaction` and prepare the transaction for submission.
|
||||
* @param params - object of type [`GetBuyWithCryptoTransferParams`](https://portal.thirdweb.com/references/typescript/v5/GetBuyWithCryptoTransferParams)
|
||||
* @returns Object of type [`BuyWithCryptoTransfer`](https://portal.thirdweb.com/references/typescript/v5/BuyWithCryptoTransfer) which contains the information about the transfer
|
||||
* @example
|
||||
*
|
||||
* ```ts
|
||||
* import { getBuyWithCryptoTransfer } from "thirdweb/pay";
|
||||
*
|
||||
* const transfer = await getBuyWithCryptoTransfer({
|
||||
* client,
|
||||
* fromAddress: "0x...", // wallet address
|
||||
* toAddress: "0x...", // recipient address - likely to be your wallet
|
||||
* chainId: 10, // chain id of the token
|
||||
* tokenAddress: "0x...", // address of the token
|
||||
* amount: "10", // amount of token to transfer
|
||||
* purchaseData: { // any metadata for you to attribute this purchase
|
||||
* "customerId": "yourId"
|
||||
* }
|
||||
* });
|
||||
* ```
|
||||
* @buyCrypto
|
||||
*/
|
||||
export async function getBuyWithCryptoTransfer(
|
||||
params: GetBuyWithCryptoTransferParams,
|
||||
): Promise<BuyWithCryptoTransfer> {
|
||||
try {
|
||||
const clientFetch = getClientFetch(params.client);
|
||||
|
||||
const response = await clientFetch(getPayBuyWithCryptoTransferEndpoint(), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
fromAddress: params.fromAddress,
|
||||
toAddress: params.toAddress,
|
||||
chainId: params.chainId,
|
||||
tokenAddress: params.tokenAddress,
|
||||
amount: params.amount,
|
||||
purchaseData: params.purchaseData,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorObj = await response.json();
|
||||
if (errorObj && "error" in errorObj) {
|
||||
throw errorObj;
|
||||
}
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data: BuyWithCryptoTransferResponse = (await response.json()).result;
|
||||
|
||||
const transfer: BuyWithCryptoTransfer = {
|
||||
transactionRequest: {
|
||||
chain: getCachedChain(data.transactionRequest.chainId),
|
||||
client: params.client,
|
||||
data: data.transactionRequest.data as Hash,
|
||||
to: data.transactionRequest.to as Address,
|
||||
value: BigInt(data.transactionRequest.value),
|
||||
gas: BigInt(data.transactionRequest.gasLimit),
|
||||
},
|
||||
approval: data.approval
|
||||
? approve({
|
||||
contract: getContract({
|
||||
client: params.client,
|
||||
address: data.approval.tokenAddress,
|
||||
chain: getCachedChain(data.approval.chainId),
|
||||
}),
|
||||
spender: data.approval.spenderAddress as Address,
|
||||
amountWei: BigInt(data.approval.amountWei),
|
||||
})
|
||||
: undefined,
|
||||
fromAddress: data.fromAddress,
|
||||
toAddress: data.toAddress,
|
||||
paymentToken: data.paymentToken,
|
||||
processingFee: data.processingFee,
|
||||
estimatedGasCostUSDCents: data.estimatedGasCostUSDCents,
|
||||
client: params.client,
|
||||
};
|
||||
|
||||
return transfer;
|
||||
} catch (error) {
|
||||
console.error("Error getting buy with crypto transfer", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,13 @@ export const getPayBuyWithCryptoStatusUrl = () =>
|
||||
export const getPayBuyWithCryptoQuoteEndpoint = () =>
|
||||
`${getPayBaseUrl()}/buy-with-crypto/quote/v1`;
|
||||
|
||||
/**
|
||||
* Endpoint to get "Buy with Crypto" transfer.
|
||||
* @internal
|
||||
*/
|
||||
export const getPayBuyWithCryptoTransferEndpoint = () =>
|
||||
`${getPayBaseUrl()}/buy-with-crypto/transfer/v1`;
|
||||
|
||||
/**
|
||||
* Endpoint to get a "Buy with Fiat" quote.
|
||||
* @internal
|
||||
|
||||
@@ -325,12 +325,19 @@ function BuyScreenContent(props: BuyScreenContentProps) {
|
||||
onBack={goBack}
|
||||
payer={payer}
|
||||
client={props.client}
|
||||
onDone={onDone}
|
||||
chain={toChain}
|
||||
token={toToken}
|
||||
tokenAmount={tokenAmount}
|
||||
receiverAddress={receiverAddress}
|
||||
transactionMode={props.payOptions.mode === "transaction"}
|
||||
isEmbed={props.isEmbed}
|
||||
onDone={onDone}
|
||||
onTryAgain={() => {
|
||||
setScreen({
|
||||
id: "buy-with-crypto",
|
||||
});
|
||||
}}
|
||||
onSuccess={onSwapSuccess}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+26
@@ -100,7 +100,9 @@ export function SwapTxDetailsTable(
|
||||
) {
|
||||
let uiData: SwapTxDetailsData;
|
||||
let showStatusRow = true;
|
||||
let isTransfer = false;
|
||||
if (props.type === "status") {
|
||||
isTransfer = props.status.swapType === "TRANSFER";
|
||||
const status = props.status;
|
||||
if (props.hideStatusRow) {
|
||||
showStatusRow = false;
|
||||
@@ -189,6 +191,30 @@ export function SwapTxDetailsTable(
|
||||
</>
|
||||
);
|
||||
|
||||
if (isTransfer) {
|
||||
return (
|
||||
<div>
|
||||
{/* source chain Tx hash link */}
|
||||
{fromChainExplorers.explorers?.[0]?.url && sourceTxHash && (
|
||||
<ButtonLink
|
||||
fullWidth
|
||||
variant="outline"
|
||||
href={`${fromChainExplorers.explorers[0].url}/tx/${sourceTxHash}`}
|
||||
target="_blank"
|
||||
gap="xs"
|
||||
style={{
|
||||
fontSize: fontSize.sm,
|
||||
padding: spacing.sm,
|
||||
}}
|
||||
>
|
||||
View on {fromChainName.name} Explorer
|
||||
<ExternalLinkIcon width={iconSize.sm} height={iconSize.sm} />
|
||||
</ButtonLink>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{isPartialSuccess && gotToken ? (
|
||||
|
||||
+3
-3
@@ -26,7 +26,7 @@ export function SwapStatusScreen(props: {
|
||||
onDone: () => void;
|
||||
transactionMode: boolean;
|
||||
isEmbed: boolean;
|
||||
quote: BuyWithCryptoQuote;
|
||||
quote: BuyWithCryptoQuote | undefined;
|
||||
onSuccess: ((status: BuyWithCryptoStatus) => void) | undefined;
|
||||
}) {
|
||||
const { onSuccess } = props;
|
||||
@@ -82,13 +82,13 @@ export function SwapStatusScreen(props: {
|
||||
hideStatusRow={true}
|
||||
client={props.client}
|
||||
/>
|
||||
) : (
|
||||
) : props.quote ? (
|
||||
<SwapTxDetailsTable
|
||||
type="quote"
|
||||
quote={props.quote}
|
||||
client={props.client}
|
||||
/>
|
||||
);
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<Container animate="fadein">
|
||||
|
||||
+284
@@ -0,0 +1,284 @@
|
||||
import { CheckCircledIcon, CrossCircledIcon } from "@radix-ui/react-icons";
|
||||
import { useState } from "react";
|
||||
import type { Chain } from "../../../../../../../chains/types.js";
|
||||
import type { ThirdwebClient } from "../../../../../../../client/client.js";
|
||||
import { NATIVE_TOKEN_ADDRESS } from "../../../../../../../constants/addresses.js";
|
||||
import { getContract } from "../../../../../../../contract/contract.js";
|
||||
import { transfer } from "../../../../../../../extensions/erc20/write/transfer.js";
|
||||
import { getBuyWithCryptoTransfer } from "../../../../../../../pay/buyWithCrypto/getTransfer.js";
|
||||
import { sendAndConfirmTransaction } from "../../../../../../../transaction/actions/send-and-confirm-transaction.js";
|
||||
import { sendTransaction } from "../../../../../../../transaction/actions/send-transaction.js";
|
||||
import { prepareTransaction } from "../../../../../../../transaction/prepare-transaction.js";
|
||||
import { toWei } from "../../../../../../../utils/units.js";
|
||||
import { iconSize } from "../../../../../../core/design-system/index.js";
|
||||
import { useChainSymbol } from "../../../../../../core/hooks/others/useChainQuery.js";
|
||||
import { Spacer } from "../../../../components/Spacer.js";
|
||||
import { Spinner } from "../../../../components/Spinner.js";
|
||||
import { StepBar } from "../../../../components/StepBar.js";
|
||||
import { SwitchNetworkButton } from "../../../../components/SwitchNetwork.js";
|
||||
import { Container, Line, ModalHeader } from "../../../../components/basic.js";
|
||||
import { Button } from "../../../../components/buttons.js";
|
||||
import { Text } from "../../../../components/text.js";
|
||||
import { type ERC20OrNativeToken, isNativeToken } from "../../nativeToken.js";
|
||||
import { Step } from "../Stepper.js";
|
||||
import { WalletRow } from "../WalletSelectorButton.js";
|
||||
import { TokenInfoRow } from "../pay-transactions/TokenInfoRow.js";
|
||||
import type { PayerInfo } from "../types.js";
|
||||
import { ConnectorLine } from "./ConfirmationScreen.js";
|
||||
|
||||
type TrasnferConfirmationScreenProps = {
|
||||
title: string;
|
||||
onBack?: () => void;
|
||||
setTransactionHash: (txHash: string) => void;
|
||||
payer: PayerInfo;
|
||||
receiverAddress: string;
|
||||
client: ThirdwebClient;
|
||||
onDone: () => void;
|
||||
chain: Chain;
|
||||
token: ERC20OrNativeToken;
|
||||
tokenAmount: string;
|
||||
transactionMode?: boolean;
|
||||
};
|
||||
|
||||
export function TransferConfirmationScreen(
|
||||
props: TrasnferConfirmationScreenProps,
|
||||
) {
|
||||
const {
|
||||
title,
|
||||
onBack,
|
||||
receiverAddress,
|
||||
client,
|
||||
payer,
|
||||
onDone,
|
||||
chain,
|
||||
token,
|
||||
tokenAmount,
|
||||
transactionMode,
|
||||
setTransactionHash,
|
||||
} = props;
|
||||
const [step, setStep] = useState<"approve" | "transfer" | "execute">(
|
||||
"transfer",
|
||||
);
|
||||
const [status, setStatus] = useState<"idle" | "pending" | "error" | "done">(
|
||||
"idle",
|
||||
);
|
||||
const { symbol } = useChainSymbol(chain);
|
||||
|
||||
return (
|
||||
<Container p="lg">
|
||||
<ModalHeader title={title} onBack={onBack} />
|
||||
<Spacer y="xl" />
|
||||
|
||||
{transactionMode && (
|
||||
<>
|
||||
<StepBar steps={2} currentStep={step === "transfer" ? 1 : 2} />
|
||||
<Spacer y="sm" />
|
||||
<Text size="sm">
|
||||
{step === "transfer"
|
||||
? "Step 1 of 2 - Transfer funds"
|
||||
: "Step 2 of 2 - Finalize transaction"}
|
||||
</Text>
|
||||
<Spacer y="xl" />
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Sender Address */}
|
||||
<Container
|
||||
flex="row"
|
||||
center="y"
|
||||
style={{
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<Text size="sm">From</Text>
|
||||
<WalletRow address={payer.account.address} client={client} />
|
||||
</Container>
|
||||
|
||||
<Spacer y="md" />
|
||||
<Line />
|
||||
<Spacer y="md" />
|
||||
|
||||
{/* Receiver Address */}
|
||||
<Container
|
||||
flex="row"
|
||||
center="y"
|
||||
style={{
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<Text size="sm">To</Text>
|
||||
<WalletRow address={receiverAddress} client={client} />
|
||||
</Container>
|
||||
|
||||
<Spacer y="md" />
|
||||
<Line />
|
||||
<Spacer y="md" />
|
||||
|
||||
{/* Token Info */}
|
||||
<TokenInfoRow
|
||||
chainId={chain.id}
|
||||
client={client}
|
||||
label={"Amount"}
|
||||
tokenAmount={tokenAmount}
|
||||
tokenSymbol={isNativeToken(token) ? symbol || "" : token.symbol}
|
||||
tokenAddress={
|
||||
isNativeToken(token) ? NATIVE_TOKEN_ADDRESS : token.address
|
||||
}
|
||||
/>
|
||||
|
||||
<Spacer y="lg" />
|
||||
|
||||
{transactionMode && (
|
||||
<>
|
||||
<Spacer y="sm" />
|
||||
<Container
|
||||
gap="sm"
|
||||
flex="row"
|
||||
style={{
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
center="y"
|
||||
color="accentText"
|
||||
>
|
||||
<Step
|
||||
isDone={step === "execute"}
|
||||
isActive={step === "transfer"}
|
||||
label={step === "transfer" ? "Transfer" : "Done"}
|
||||
/>
|
||||
<ConnectorLine />
|
||||
<Step
|
||||
isDone={false}
|
||||
label="Finalize"
|
||||
isActive={step === "execute"}
|
||||
/>
|
||||
</Container>
|
||||
<Spacer y="lg" />
|
||||
</>
|
||||
)}
|
||||
|
||||
{status === "error" && (
|
||||
<>
|
||||
<Container flex="row" gap="xs" center="both" color="danger">
|
||||
<CrossCircledIcon width={iconSize.sm} height={iconSize.sm} />
|
||||
<Text color="danger" size="sm">
|
||||
{step === "transfer" ? "Failed to Transfer" : "Failed to Execute"}
|
||||
</Text>
|
||||
</Container>
|
||||
<Spacer y="md" />
|
||||
</>
|
||||
)}
|
||||
|
||||
{!transactionMode && step === "execute" && status === "done" && (
|
||||
<>
|
||||
<Container flex="row" gap="xs" center="both" color="success">
|
||||
<CheckCircledIcon width={iconSize.sm} height={iconSize.sm} />
|
||||
<Text color="success" size="sm">
|
||||
{"Payment completed"}
|
||||
</Text>
|
||||
</Container>
|
||||
<Spacer y="md" />
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Execute */}
|
||||
{payer.chain.id !== chain.id ? (
|
||||
<SwitchNetworkButton
|
||||
fullWidth
|
||||
variant="accent"
|
||||
switchChain={async () => {
|
||||
await props.payer.wallet.switchChain(chain);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
variant="accent"
|
||||
fullWidth
|
||||
disabled={status === "pending"}
|
||||
onClick={async () => {
|
||||
if (step === "execute") {
|
||||
onDone();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setStatus("pending");
|
||||
|
||||
// TRANSACTION MODE = transfer funds to another one of your wallets before executing the tx
|
||||
if (transactionMode) {
|
||||
const transaction = isNativeToken(token)
|
||||
? prepareTransaction({
|
||||
client,
|
||||
chain,
|
||||
to: receiverAddress,
|
||||
value: toWei(tokenAmount),
|
||||
})
|
||||
: transfer({
|
||||
contract: getContract({
|
||||
address: token.address,
|
||||
chain: chain,
|
||||
client: client,
|
||||
}),
|
||||
to: receiverAddress,
|
||||
amount: tokenAmount,
|
||||
});
|
||||
await sendAndConfirmTransaction({
|
||||
account: props.payer.account,
|
||||
transaction,
|
||||
});
|
||||
// switch to execute step
|
||||
setStep("execute");
|
||||
setStatus("idle");
|
||||
} else {
|
||||
const transferResponse = await getBuyWithCryptoTransfer({
|
||||
client,
|
||||
fromAddress: payer.account.address,
|
||||
toAddress: receiverAddress,
|
||||
chainId: chain.id,
|
||||
tokenAddress: isNativeToken(token)
|
||||
? NATIVE_TOKEN_ADDRESS
|
||||
: token.address,
|
||||
amount: tokenAmount,
|
||||
purchaseData: undefined, // TODO (pay): add purchase data
|
||||
});
|
||||
|
||||
console.log("transferResponse", transferResponse);
|
||||
if (transferResponse.approval) {
|
||||
setStep("approve");
|
||||
// approve the transfer
|
||||
await sendAndConfirmTransaction({
|
||||
account: props.payer.account,
|
||||
transaction: transferResponse.approval,
|
||||
});
|
||||
}
|
||||
|
||||
setStep("transfer");
|
||||
// execute the transfer
|
||||
const transaction = transferResponse.transactionRequest;
|
||||
const tx = await sendTransaction({
|
||||
account: props.payer.account,
|
||||
transaction,
|
||||
});
|
||||
// switches to the status polling screen
|
||||
setTransactionHash(tx.transactionHash);
|
||||
setStatus("idle");
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setStatus("error");
|
||||
}
|
||||
}}
|
||||
gap="xs"
|
||||
>
|
||||
{step === "execute" && (status === "done" ? "Done" : "Continue")}
|
||||
{step === "transfer" &&
|
||||
(status === "pending" ? "Confirming" : "Confirm")}
|
||||
{step === "approve" &&
|
||||
(status === "pending" ? "Approving" : "Approve")}
|
||||
{status === "pending" && (
|
||||
<Spinner size="sm" color="accentButtonText" />
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
+29
-220
@@ -1,28 +1,11 @@
|
||||
import { CheckCircledIcon, CrossCircledIcon } from "@radix-ui/react-icons";
|
||||
import { useState } from "react";
|
||||
import type { Chain } from "../../../../../../../chains/types.js";
|
||||
import type { ThirdwebClient } from "../../../../../../../client/client.js";
|
||||
import { NATIVE_TOKEN_ADDRESS } from "../../../../../../../constants/addresses.js";
|
||||
import { getContract } from "../../../../../../../contract/contract.js";
|
||||
import { transfer } from "../../../../../../../extensions/erc20/write/transfer.js";
|
||||
import { sendAndConfirmTransaction } from "../../../../../../../transaction/actions/send-and-confirm-transaction.js";
|
||||
import { prepareTransaction } from "../../../../../../../transaction/prepare-transaction.js";
|
||||
import { toWei } from "../../../../../../../utils/units.js";
|
||||
import { iconSize } from "../../../../../../core/design-system/index.js";
|
||||
import { useChainSymbol } from "../../../../../../core/hooks/others/useChainQuery.js";
|
||||
import { Spacer } from "../../../../components/Spacer.js";
|
||||
import { Spinner } from "../../../../components/Spinner.js";
|
||||
import { StepBar } from "../../../../components/StepBar.js";
|
||||
import { SwitchNetworkButton } from "../../../../components/SwitchNetwork.js";
|
||||
import { Container, Line, ModalHeader } from "../../../../components/basic.js";
|
||||
import { Button } from "../../../../components/buttons.js";
|
||||
import { Text } from "../../../../components/text.js";
|
||||
import { type ERC20OrNativeToken, isNativeToken } from "../../nativeToken.js";
|
||||
import { Step } from "../Stepper.js";
|
||||
import { WalletRow } from "../WalletSelectorButton.js";
|
||||
import { TokenInfoRow } from "../pay-transactions/TokenInfoRow.js";
|
||||
import type { BuyWithCryptoStatus } from "../../../../../../../pay/buyWithCrypto/getStatus.js";
|
||||
import type { ERC20OrNativeToken } from "../../nativeToken.js";
|
||||
import type { PayerInfo } from "../types.js";
|
||||
import { ConnectorLine } from "./ConfirmationScreen.js";
|
||||
import { SwapStatusScreen } from "./SwapStatusScreen.js";
|
||||
import { TransferConfirmationScreen } from "./TransferConfirmationScreen.js";
|
||||
|
||||
type TrasnferFlowProps = {
|
||||
title: string;
|
||||
@@ -31,6 +14,9 @@ type TrasnferFlowProps = {
|
||||
receiverAddress: string;
|
||||
client: ThirdwebClient;
|
||||
onDone: () => void;
|
||||
onTryAgain: () => void;
|
||||
isEmbed: boolean;
|
||||
onSuccess: ((status: BuyWithCryptoStatus) => void) | undefined;
|
||||
chain: Chain;
|
||||
token: ERC20OrNativeToken;
|
||||
tokenAmount: string;
|
||||
@@ -38,206 +24,29 @@ type TrasnferFlowProps = {
|
||||
};
|
||||
|
||||
export function TransferFlow(props: TrasnferFlowProps) {
|
||||
const {
|
||||
title,
|
||||
onBack,
|
||||
receiverAddress,
|
||||
client,
|
||||
payer,
|
||||
onDone,
|
||||
chain,
|
||||
token,
|
||||
tokenAmount,
|
||||
transactionMode,
|
||||
} = props;
|
||||
const [step, setStep] = useState<"transfer" | "execute">("transfer");
|
||||
const [status, setStatus] = useState<"idle" | "pending" | "error" | "done">(
|
||||
"idle",
|
||||
);
|
||||
const { symbol } = useChainSymbol(chain);
|
||||
const [transferTxHash, setTransferTxHash] = useState<string | undefined>();
|
||||
|
||||
if (transferTxHash) {
|
||||
return (
|
||||
<SwapStatusScreen
|
||||
title={props.title}
|
||||
onBack={props.onBack}
|
||||
onTryAgain={props.onTryAgain}
|
||||
swapTxHash={transferTxHash}
|
||||
client={props.client}
|
||||
onDone={props.onDone}
|
||||
transactionMode={false}
|
||||
isEmbed={props.isEmbed}
|
||||
quote={undefined}
|
||||
onSuccess={props.onSuccess}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container p="lg">
|
||||
<ModalHeader title={title} onBack={onBack} />
|
||||
<Spacer y="xl" />
|
||||
|
||||
{transactionMode && (
|
||||
<>
|
||||
<StepBar steps={2} currentStep={step === "transfer" ? 1 : 2} />
|
||||
<Spacer y="sm" />
|
||||
<Text size="sm">
|
||||
{step === "transfer"
|
||||
? "Step 1 of 2 - Transfer funds"
|
||||
: "Step 2 of 2 - Finalize transaction"}
|
||||
</Text>
|
||||
<Spacer y="xl" />
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Sender Address */}
|
||||
<Container
|
||||
flex="row"
|
||||
center="y"
|
||||
style={{
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<Text size="sm">From</Text>
|
||||
<WalletRow address={payer.account.address} client={client} />
|
||||
</Container>
|
||||
|
||||
<Spacer y="md" />
|
||||
<Line />
|
||||
<Spacer y="md" />
|
||||
|
||||
{/* Receiver Address */}
|
||||
<Container
|
||||
flex="row"
|
||||
center="y"
|
||||
style={{
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<Text size="sm">To</Text>
|
||||
<WalletRow address={receiverAddress} client={client} />
|
||||
</Container>
|
||||
|
||||
<Spacer y="md" />
|
||||
<Line />
|
||||
<Spacer y="md" />
|
||||
|
||||
{/* Token Info */}
|
||||
<TokenInfoRow
|
||||
chainId={chain.id}
|
||||
client={client}
|
||||
label={"Amount"}
|
||||
tokenAmount={tokenAmount}
|
||||
tokenSymbol={isNativeToken(token) ? symbol || "" : token.symbol}
|
||||
tokenAddress={
|
||||
isNativeToken(token) ? NATIVE_TOKEN_ADDRESS : token.address
|
||||
}
|
||||
/>
|
||||
|
||||
<Spacer y="lg" />
|
||||
|
||||
{transactionMode && (
|
||||
<>
|
||||
<Spacer y="sm" />
|
||||
<Container
|
||||
gap="sm"
|
||||
flex="row"
|
||||
style={{
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
center="y"
|
||||
color="accentText"
|
||||
>
|
||||
<Step
|
||||
isDone={step === "execute"}
|
||||
isActive={step === "transfer"}
|
||||
label={step === "transfer" ? "Transfer" : "Done"}
|
||||
/>
|
||||
<ConnectorLine />
|
||||
<Step
|
||||
isDone={false}
|
||||
label="Finalize"
|
||||
isActive={step === "execute"}
|
||||
/>
|
||||
</Container>
|
||||
<Spacer y="lg" />
|
||||
</>
|
||||
)}
|
||||
|
||||
{status === "error" && (
|
||||
<>
|
||||
<Container flex="row" gap="xs" center="both" color="danger">
|
||||
<CrossCircledIcon width={iconSize.sm} height={iconSize.sm} />
|
||||
<Text color="danger" size="sm">
|
||||
{step === "transfer" ? "Failed to Transfer" : "Failed to Execute"}
|
||||
</Text>
|
||||
</Container>
|
||||
<Spacer y="md" />
|
||||
</>
|
||||
)}
|
||||
|
||||
{!transactionMode && step === "execute" && status === "done" && (
|
||||
<>
|
||||
<Container flex="row" gap="xs" center="both" color="success">
|
||||
<CheckCircledIcon width={iconSize.sm} height={iconSize.sm} />
|
||||
<Text color="success" size="sm">
|
||||
{"Payment completed"}
|
||||
</Text>
|
||||
</Container>
|
||||
<Spacer y="md" />
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Execute */}
|
||||
{payer.chain.id !== chain.id ? (
|
||||
<SwitchNetworkButton
|
||||
fullWidth
|
||||
variant="accent"
|
||||
switchChain={async () => {
|
||||
await props.payer.wallet.switchChain(chain);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
variant="accent"
|
||||
fullWidth
|
||||
disabled={status === "pending"}
|
||||
onClick={async () => {
|
||||
if (step === "execute") {
|
||||
onDone();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setStatus("pending");
|
||||
|
||||
const transaction = isNativeToken(token)
|
||||
? prepareTransaction({
|
||||
client,
|
||||
chain,
|
||||
to: receiverAddress,
|
||||
value: toWei(tokenAmount),
|
||||
})
|
||||
: transfer({
|
||||
contract: getContract({
|
||||
address: token.address,
|
||||
chain: chain,
|
||||
client: client,
|
||||
}),
|
||||
to: receiverAddress,
|
||||
amount: tokenAmount,
|
||||
});
|
||||
|
||||
await sendAndConfirmTransaction({
|
||||
account: props.payer.account,
|
||||
transaction,
|
||||
});
|
||||
|
||||
setStep("execute");
|
||||
if (transactionMode) {
|
||||
setStatus("idle");
|
||||
} else {
|
||||
setStatus("done");
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setStatus("error");
|
||||
}
|
||||
}}
|
||||
gap="xs"
|
||||
>
|
||||
{step === "execute" && (status === "done" ? "Done" : "Continue")}
|
||||
{step === "transfer" &&
|
||||
(status === "pending" ? "Confirming" : "Confirm")}
|
||||
{status === "pending" && (
|
||||
<Spinner size="sm" color="accentButtonText" />
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</Container>
|
||||
<TransferConfirmationScreen
|
||||
{...props}
|
||||
setTransactionHash={setTransferTxHash}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user