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:
Isaac Dubuque
2024-08-23 23:45:23 +00:00
committed by GitHub
co-authored by Joaquim Verges Edward Sun Edward Sun
parent 120d61f410
commit 1405598e7e
15 changed files with 627 additions and 278 deletions
+5
View File
@@ -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,
});
+11 -2
View File
@@ -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,
+11 -2
View File
@@ -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}
/>
);
}
@@ -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 ? (
@@ -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">
@@ -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>
);
}
@@ -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}
/>
);
}