diff --git a/.changeset/shaggy-flowers-argue.md b/.changeset/shaggy-flowers-argue.md new file mode 100644 index 00000000000..c5a8e4fb1a7 --- /dev/null +++ b/.changeset/shaggy-flowers-argue.md @@ -0,0 +1,5 @@ +--- +"thirdweb": patch +--- + +Fiat onramp UI revamp in PayEmbed and support multi hop onramp flows diff --git a/apps/playground-web/src/app/connect/pay/page.tsx b/apps/playground-web/src/app/connect/pay/page.tsx index 9f741b02619..cc2db0883dc 100644 --- a/apps/playground-web/src/app/connect/pay/page.tsx +++ b/apps/playground-web/src/app/connect/pay/page.tsx @@ -41,7 +41,7 @@ function StyledPayEmbed() { <>

- Top Up + Fund Wallet

Inline component that allows users to buy any currency. diff --git a/apps/playground-web/src/app/navLinks.ts b/apps/playground-web/src/app/navLinks.ts index 182a6945a87..268e57304f2 100644 --- a/apps/playground-web/src/app/navLinks.ts +++ b/apps/playground-web/src/app/navLinks.ts @@ -65,7 +65,7 @@ export const staticSidebarLinks: SidebarLink[] = [ expanded: false, links: [ { - name: "Top up", + name: "Fund Wallet", href: "/connect/pay", }, { diff --git a/apps/playground-web/src/components/pay/embed.tsx b/apps/playground-web/src/components/pay/embed.tsx index 2b073042326..410445e4ee6 100644 --- a/apps/playground-web/src/components/pay/embed.tsx +++ b/apps/playground-web/src/components/pay/embed.tsx @@ -2,18 +2,78 @@ import { THIRDWEB_CLIENT } from "@/lib/client"; import { useTheme } from "next-themes"; -import { base } from "thirdweb/chains"; -import { PayEmbed } from "thirdweb/react"; +import { + arbitrum, + arbitrumNova, + base, + defineChain, + sepolia, + treasure, +} from "thirdweb/chains"; +import { PayEmbed, getDefaultToken } from "thirdweb/react"; import { StyledConnectButton } from "../styled-connect-button"; - export function StyledPayEmbedPreview() { const { theme } = useTheme(); return (

- + + 8453: [getDefaultToken(base, "USDC")!], + 42161: [ + { + address: "0x539bde0d7dbd336b79148aa742883198bbf60342", + name: "MAGIC", + symbol: "MAGIC", + }, + ], + [arbitrumNova.id]: [ + { + name: "Godcoin", + symbol: "GOD", + address: "0xb5130f4767ab0acc579f25a76e8f9e977cb3f948", + icon: "https://assets.coingecko.com/coins/images/53848/standard/GodcoinTickerIcon_02.png", + }, + ], + }} + detailsButton={{ + displayBalanceToken: { + 466: "0x675C3ce7F43b00045a4Dab954AF36160fb57cB45", + 8453: getDefaultToken(base, "USDC")?.address ?? "", + 42161: "0x539bde0d7dbd336b79148aa742883198bbf60342", + [arbitrumNova.id]: "0xb5130f4767ab0acc579f25a76e8f9e977cb3f948", + }, + }} + />
, ): UseQueryResult { return useQuery({ queryKey: ["useBuyWithFiatStatus", params], @@ -64,5 +65,6 @@ export function useBuyWithFiatStatus( }, refetchIntervalInBackground: true, retry: true, + ...params?.queryOptions, }); } diff --git a/packages/thirdweb/src/react/core/hooks/wallets/useSendToken.ts b/packages/thirdweb/src/react/core/hooks/wallets/useSendToken.ts index c26eb76986d..84722dd6fe2 100644 --- a/packages/thirdweb/src/react/core/hooks/wallets/useSendToken.ts +++ b/packages/thirdweb/src/react/core/hooks/wallets/useSendToken.ts @@ -1,13 +1,15 @@ -import { useMutation } from "@tanstack/react-query"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; import type { ThirdwebClient } from "../../../../client/client.js"; import { getContract } from "../../../../contract/contract.js"; import { resolveAddress } from "../../../../extensions/ens/resolve-address.js"; import { transfer } from "../../../../extensions/erc20/write/transfer.js"; import { sendTransaction } from "../../../../transaction/actions/send-transaction.js"; +import { waitForReceipt } from "../../../../transaction/actions/wait-for-tx-receipt.js"; import { prepareTransaction } from "../../../../transaction/prepare-transaction.js"; import { isAddress } from "../../../../utils/address.js"; import { isValidENSName } from "../../../../utils/ens/isValidENSName.js"; import { toWei } from "../../../../utils/units.js"; +import { invalidateWalletBalance } from "../../providers/invalidateWalletBalance.js"; import { useActiveWallet } from "./useActiveWallet.js"; /** @@ -33,6 +35,7 @@ import { useActiveWallet } from "./useActiveWallet.js"; */ export function useSendToken(client: ThirdwebClient) { const wallet = useActiveWallet(); + const queryClient = useQueryClient(); return useMutation({ async mutationFn(option: { tokenAddress?: string; @@ -83,7 +86,7 @@ export function useSendToken(client: ThirdwebClient) { value: toWei(amount), }); - await sendTransaction({ + return sendTransaction({ transaction: sendNativeTokenTx, account, }); @@ -103,11 +106,24 @@ export function useSendToken(client: ThirdwebClient) { to, }); - await sendTransaction({ + return sendTransaction({ transaction: tx, account, }); } }, + onSettled: async (data, error) => { + if (error) { + return; + } + if (data?.transactionHash) { + await waitForReceipt({ + transactionHash: data.transactionHash, + client, + chain: data.chain, + }); + } + invalidateWalletBalance(queryClient); + }, }); } diff --git a/packages/thirdweb/src/react/core/providers/invalidateWalletBalance.ts b/packages/thirdweb/src/react/core/providers/invalidateWalletBalance.ts index 85e42563c84..7fd4ccd2a61 100644 --- a/packages/thirdweb/src/react/core/providers/invalidateWalletBalance.ts +++ b/packages/thirdweb/src/react/core/providers/invalidateWalletBalance.ts @@ -4,9 +4,14 @@ export function invalidateWalletBalance( queryClient: QueryClient, chainId?: number, ) { - return queryClient.invalidateQueries({ + queryClient.invalidateQueries({ // invalidate any walletBalance queries for this chainId // TODO: add wallet address in here if we can get it somehow queryKey: chainId ? ["walletBalance", chainId] : ["walletBalance"], }); + queryClient.invalidateQueries({ + queryKey: chainId + ? ["internal_account_balance", chainId] + : ["internal_account_balance"], + }); } diff --git a/packages/thirdweb/src/react/core/utils/account.ts b/packages/thirdweb/src/react/core/utils/account.ts index f0ed135ca9c..c0241634950 100644 --- a/packages/thirdweb/src/react/core/utils/account.ts +++ b/packages/thirdweb/src/react/core/utils/account.ts @@ -2,7 +2,10 @@ import type { Chain } from "../../../chains/types.js"; import type { ThirdwebClient } from "../../../client/client.js"; import { NATIVE_TOKEN_ADDRESS } from "../../../constants/addresses.js"; import { convertCryptoToFiat } from "../../../pay/convert/cryptoToFiat.js"; -import type { SupportedFiatCurrency } from "../../../pay/convert/type.js"; +import { + type SupportedFiatCurrency, + getFiatSymbol, +} from "../../../pay/convert/type.js"; import { type Address, isAddress } from "../../../utils/address.js"; import { formatNumber } from "../../../utils/formatNumber.js"; import { shortenLargeNumber } from "../../../utils/shortenLargeNumber.js"; @@ -112,13 +115,6 @@ export async function loadAccountBalance(props: { }; } -function getFiatSymbol(showBalanceInFiat: SupportedFiatCurrency) { - switch (showBalanceInFiat) { - case "USD": - return "$"; - } -} - /** * Format the display balance for both crypto and fiat, in the Details button and Modal * If both crypto balance and fiat balance exist, we have to keep the string very short to avoid UI issues. diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/NetworkSelector.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/NetworkSelector.tsx index 214dd1e5a40..c4dbda45028 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/NetworkSelector.tsx +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/NetworkSelector.tsx @@ -704,7 +704,7 @@ export const ChainButton = /* @__PURE__ */ memo(function ChainButton(props: { {confirming && ( <> - {locale.confirmInWallet} + {locale.switchingNetwork} diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/icons/currencies/JPYIcon.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/icons/currencies/JPYIcon.tsx index 9d0048acd00..d101b0668b2 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/icons/currencies/JPYIcon.tsx +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/icons/currencies/JPYIcon.tsx @@ -9,7 +9,7 @@ export const JPYIcon: IconFC = (props) => { xmlns="http://www.w3.org/2000/svg" role="presentation" > - + + )?.paymentInfo?.sellerAddress; + const receiverAddress = defaultRecipientAddress || payer.account.address; return ( - ); diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/PayWIthCreditCard.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/PayWIthCreditCard.tsx index 78a92741aa4..415d1dfcd94 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/PayWIthCreditCard.tsx +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/PayWIthCreditCard.tsx @@ -12,7 +12,7 @@ import { Skeleton } from "../../../components/Skeleton.js"; import { Container } from "../../../components/basic.js"; import { Button } from "../../../components/buttons.js"; import { Text } from "../../../components/text.js"; -import type { CurrencyMeta } from "./fiat/currencies.js"; +import { type CurrencyMeta, getFiatIcon } from "./fiat/currencies.js"; /** * Shows an amount "value" and renders the selected token and chain @@ -55,7 +55,7 @@ export function PayWithCreditCard(props: { }} gap="sm" > - + {getFiatIcon(props.currency, "md")} {props.currency.shorthand} diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/CurrencySelection.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/CurrencySelection.tsx index 7f1d8269b13..b8d0b442430 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/CurrencySelection.tsx +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/CurrencySelection.tsx @@ -1,14 +1,11 @@ import styled from "@emotion/styled"; import { useCustomTheme } from "../../../../../../core/design-system/CustomThemeProvider.js"; -import { - iconSize, - spacing, -} from "../../../../../../core/design-system/index.js"; +import { spacing } from "../../../../../../core/design-system/index.js"; import { Spacer } from "../../../../components/Spacer.js"; import { Container, Line, ModalHeader } from "../../../../components/basic.js"; import { Button } from "../../../../components/buttons.js"; import { Text } from "../../../../components/text.js"; -import { type CurrencyMeta, currencies } from "./currencies.js"; +import { type CurrencyMeta, currencies, getFiatIcon } from "./currencies.js"; export function CurrencySelection(props: { onSelect: (currency: CurrencyMeta) => void; @@ -33,7 +30,7 @@ export function CurrencySelection(props: { onClick={() => props.onSelect(c)} gap="sm" > - + {getFiatIcon(c, "lg")} {c.shorthand} {c.name} 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 deleted file mode 100644 index 26d7fdc8a37..00000000000 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/FiatFlow.tsx +++ /dev/null @@ -1,152 +0,0 @@ -import { useCallback, useState } from "react"; -import { trackPayEvent } from "../../../../../../../analytics/track/pay.js"; -import type { ThirdwebClient } from "../../../../../../../client/client.js"; -import type { BuyWithFiatQuote } from "../../../../../../../pay/buyWithFiat/getQuote.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"; -import type { PayerInfo } from "../types.js"; -import { OnrampStatusScreen } from "./FiatStatusScreen.js"; -import { FiatSteps, fiatQuoteToPartialQuote } from "./FiatSteps.js"; -import { PostOnRampSwapFlow } from "./PostOnRampSwapFlow.js"; - -// 2 possible flows - -// If a Swap is required after doing onramp -// 1. show the 2 steps ui with step 1 highlighted, on continue button click: -// 2. open provider window, show onramp status screen, on onramp success: -// 3. show the 2 steps ui with step 2 highlighted, on continue button click: -// 4. show swap flow - -// If a Swap is not required after doing onramp -// - window will already be opened before this component is mounted and `openedWindow` prop will be set, show onramp status screen - -type Screen = - | { - id: "step-1"; - } - | { - id: "onramp-status"; - } - | { - id: "postonramp-swap"; - data: BuyWithFiatStatus; - } - | { - id: "step-2"; - }; - -export function FiatFlow(props: { - title: string; - quote: BuyWithFiatQuote; - onBack: () => void; - client: ThirdwebClient; - testMode: boolean; - theme: "light" | "dark"; - openedWindow: Window | null; - onDone: () => void; - transactionMode: boolean; - isEmbed: boolean; - payer: PayerInfo; - onSuccess: (status: BuyWithFiatStatus) => void; -}) { - const hasTwoSteps = isSwapRequiredPostOnramp(props.quote); - const [screen, setScreen] = useState( - hasTwoSteps - ? { - id: "step-1", - } - : { - id: "onramp-status", - }, - ); - - const [popupWindow, setPopupWindow] = useState( - props.openedWindow, - ); - - 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 === "step-1") { - return ( - { - const popup = openOnrampPopup(props.quote.onRampLink, props.theme); - trackPayEvent({ - event: "open_onramp_popup", - client: props.client, - walletAddress: props.payer.account.address, - walletType: props.payer.wallet.id, - }); - addPendingTx({ - type: "fiat", - intentId: props.quote.intentId, - }); - setPopupWindow(popup); - setScreen({ id: "onramp-status" }); - }} - /> - ); - } - - if (screen.id === "onramp-status") { - return ( - { - setScreen({ id: "postonramp-swap", data: _status }); - }} - transactionMode={props.transactionMode} - isEmbed={props.isEmbed} - onSuccess={props.onSuccess} - /> - ); - } - - if (screen.id === "postonramp-swap") { - return ( - { - // no op - }} - transactionMode={props.transactionMode} - isEmbed={props.isEmbed} - payer={props.payer} - onSuccess={onPostOnrampSuccess} - /> - ); - } - - // never - return null; -} diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/FiatScreenContent.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/FiatScreenContent.tsx index affe26a0156..910c7c85b5e 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/FiatScreenContent.tsx +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/FiatScreenContent.tsx @@ -3,7 +3,6 @@ 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 { isSwapRequiredPostOnramp } from "../../../../../../../pay/buyWithFiat/isSwapRequiredPostOnramp.js"; import type { FiatProvider } from "../../../../../../../pay/utils/commonTypes.js"; import { formatNumber } from "../../../../../../../utils/formatNumber.js"; import { @@ -33,9 +32,7 @@ import { type ERC20OrNativeToken, isNativeToken } from "../../nativeToken.js"; import { EstimatedTimeAndFees } from "../EstimatedTimeAndFees.js"; import { PayWithCreditCard } from "../PayWIthCreditCard.js"; import type { SelectedScreen } from "../main/types.js"; -import { openOnrampPopup } from "../openOnRamppopup.js"; import { FiatFees } from "../swap/Fees.js"; -import { addPendingTx } from "../swap/pendingSwapTx.js"; import type { PayerInfo } from "../types.js"; import { Providers } from "./Providers.js"; import type { CurrencyMeta } from "./currencies.js"; @@ -113,25 +110,9 @@ export function FiatScreenContent(props: { return; } - const hasTwoSteps = isSwapRequiredPostOnramp(fiatQuoteQuery.data); - let openedWindow: Window | null = null; - - if (!hasTwoSteps) { - openedWindow = openOnrampPopup( - fiatQuoteQuery.data.onRampLink, - typeof props.theme === "string" ? props.theme : props.theme.type, - ); - - addPendingTx({ - type: "fiat", - intentId: fiatQuoteQuery.data.intentId, - }); - } - setScreen({ id: "fiat-flow", quote: fiatQuoteQuery.data, - openedWindow, }); } 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 deleted file mode 100644 index 5a15b1e9bbc..00000000000 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/FiatStatusScreen.tsx +++ /dev/null @@ -1,258 +0,0 @@ -import { CheckCircledIcon } from "@radix-ui/react-icons"; -import { useQueryClient } from "@tanstack/react-query"; -import { useEffect, useRef } from "react"; -import type { ThirdwebClient } from "../../../../../../../client/client.js"; -import type { BuyWithFiatQuote } from "../../../../../../../pay/buyWithFiat/getQuote.js"; -import type { - BuyWithFiatStatus, - ValidBuyWithFiatStatus, -} from "../../../../../../../pay/buyWithFiat/getStatus.js"; -import { isMobile } from "../../../../../../../utils/web/isMobile.js"; -import { iconSize } from "../../../../../../core/design-system/index.js"; -import { useBuyWithFiatStatus } from "../../../../../../core/hooks/pay/useBuyWithFiatStatus.js"; -import { invalidateWalletBalance } from "../../../../../../core/providers/invalidateWalletBalance.js"; -import { Spacer } from "../../../../components/Spacer.js"; -import { Spinner } from "../../../../components/Spinner.js"; -import { StepBar } from "../../../../components/StepBar.js"; -import { Container, ModalHeader } from "../../../../components/basic.js"; -import { Button } from "../../../../components/buttons.js"; -import { Text } from "../../../../components/text.js"; -import { AccentFailIcon } from "../../../icons/AccentFailIcon.js"; -import { getBuyWithFiatStatusMeta } from "../pay-transactions/statusMeta.js"; -import { OnRampTxDetailsTable } from "./FiatTxDetailsTable.js"; - -type UIStatus = "loading" | "failed" | "completed" | "partialSuccess"; - -/** - * Poll for "Buy with Fiat" status - when the on-ramp is in progress - * - Show success screen if swap is not required and on-ramp is completed - * - Show Failed screen if on-ramp failed - * - call `onShowSwapFlow` if on-ramp is completed and swap is required - */ -export function OnrampStatusScreen(props: { - title: string; - client: ThirdwebClient; - onBack: () => void; - intentId: string; - hasTwoSteps: boolean; - openedWindow: Window | null; - quote: BuyWithFiatQuote; - onDone: () => void; - onShowSwapFlow: (status: BuyWithFiatStatus) => void; - transactionMode: boolean; - isEmbed: boolean; - onSuccess: ((status: BuyWithFiatStatus) => void) | undefined; -}) { - const queryClient = useQueryClient(); - const { openedWindow, onSuccess } = props; - const statusQuery = useBuyWithFiatStatus({ - intentId: props.intentId, - client: props.client, - }); - - // determine UI status - let uiStatus: UIStatus = "loading"; - if ( - statusQuery.data?.status === "ON_RAMP_TRANSFER_FAILED" || - statusQuery.data?.status === "PAYMENT_FAILED" - ) { - uiStatus = "failed"; - } else if (statusQuery.data?.status === "CRYPTO_SWAP_FALLBACK") { - uiStatus = "partialSuccess"; - } else if (statusQuery.data?.status === "ON_RAMP_TRANSFER_COMPLETED") { - 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) { - return; - } - - if ( - statusQuery.data?.status === "CRYPTO_SWAP_REQUIRED" || - statusQuery.data?.status === "ON_RAMP_TRANSFER_COMPLETED" - ) { - openedWindow.close(); - } - }, [statusQuery.data, openedWindow]); - - // invalidate wallet balance when onramp is completed - const invalidatedBalance = useRef(false); - useEffect(() => { - if ( - !invalidatedBalance.current && - statusQuery.data?.status === "ON_RAMP_TRANSFER_COMPLETED" - ) { - invalidatedBalance.current = true; - invalidateWalletBalance(queryClient); - } - }, [statusQuery.data, queryClient]); - - // show swap flow - useEffect(() => { - if (statusQuery.data?.status === "CRYPTO_SWAP_REQUIRED") { - props.onShowSwapFlow(statusQuery.data); - } - }, [statusQuery.data, props.onShowSwapFlow]); - - return ( - - - {props.hasTwoSteps && ( - <> - - - - - Step 1 of 2 - Buying {props.quote.onRampToken.token.symbol} with{" "} - {props.quote.fromCurrencyWithFees.currencySymbol} - - - )} - - - ); -} - -function OnrampStatusScreenUI(props: { - uiStatus: UIStatus; - fiatStatus?: BuyWithFiatStatus; - onDone: () => void; - client: ThirdwebClient; - transactionMode: boolean; - isEmbed: boolean; - quote: BuyWithFiatQuote; -}) { - const { uiStatus } = props; - - const statusMeta = props.fiatStatus - ? getBuyWithFiatStatusMeta(props.fiatStatus) - : undefined; - - const fiatStatus: ValidBuyWithFiatStatus | undefined = - props.fiatStatus && props.fiatStatus.status !== "NOT_FOUND" - ? props.fiatStatus - : undefined; - - const onRampTokenQuote = props.quote.onRampToken; - - const txDetails = ( - - ); - - return ( - - - - {uiStatus === "loading" && ( - <> - - - - - - - Buy Pending - - - {!isMobile() && Complete the purchase in popup} - - {txDetails} - - )} - - {uiStatus === "failed" && ( - <> - - - - - - - Transaction Failed - - - {txDetails} - - )} - - {uiStatus === "completed" && ( - <> - - - - - - - Buy Complete - - {props.fiatStatus && props.fiatStatus.status !== "NOT_FOUND" && ( - <> - - {txDetails} - - - )} - - - - )} - - ); -} diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/FiatSteps.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/FiatSteps.tsx index 5c6ae54f93e..af6605aec7c 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/FiatSteps.tsx +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/FiatSteps.tsx @@ -7,7 +7,6 @@ import { useMemo } from "react"; import { getCachedChain } from "../../../../../../../chains/utils.js"; import type { ThirdwebClient } from "../../../../../../../client/client.js"; import { NATIVE_TOKEN_ADDRESS } from "../../../../../../../constants/addresses.js"; -import type { BuyWithFiatQuote } from "../../../../../../../pay/buyWithFiat/getQuote.js"; import type { BuyWithFiatStatus } from "../../../../../../../pay/buyWithFiat/getStatus.js"; import { formatNumber } from "../../../../../../../utils/formatNumber.js"; import { formatExplorerTxUrl } from "../../../../../../../utils/url.js"; @@ -36,7 +35,7 @@ import { type FiatStatusMeta, getBuyWithFiatStatusMeta, } from "../pay-transactions/statusMeta.js"; -import { getCurrencyMeta } from "./currencies.js"; +import { getCurrencyMeta, getFiatIcon } from "./currencies.js"; export type BuyWithFiatPartialQuote = { fromCurrencySymbol: string; @@ -58,31 +57,6 @@ export type BuyWithFiatPartialQuote = { }; }; -export function fiatQuoteToPartialQuote( - quote: BuyWithFiatQuote, -): BuyWithFiatPartialQuote { - const data: BuyWithFiatPartialQuote = { - fromCurrencyAmount: quote.fromCurrencyWithFees.amount, - fromCurrencySymbol: quote.fromCurrencyWithFees.currencySymbol, - onRampTokenAmount: quote.onRampToken.amount, - toTokenAmount: quote.estimatedToAmountMin, - onRampToken: { - chainId: quote.onRampToken.token.chainId, - tokenAddress: quote.onRampToken.token.tokenAddress, - name: quote.onRampToken.token.name, - symbol: quote.onRampToken.token.symbol, - }, - toToken: { - chainId: quote.toToken.chainId, - tokenAddress: quote.toToken.tokenAddress, - name: quote.toToken.name, - symbol: quote.toToken.symbol, - }, - }; - - return data; -} - export function FiatSteps(props: { title: string; partialQuote: BuyWithFiatPartialQuote; @@ -171,7 +145,7 @@ export function FiatSteps(props: {
); - const fiatIcon = ; + const fiatIcon = getFiatIcon(currency, "sm"); const onRampTokenIcon = ( {props.children}
{props.state && text && ( - + {text} )} diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/FiatTxDetailsTable.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/FiatTxDetailsTable.tsx index e504b444cd0..218c9d26210 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/FiatTxDetailsTable.tsx +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/FiatTxDetailsTable.tsx @@ -14,7 +14,7 @@ import { ButtonLink } from "../../../../components/buttons.js"; import { Text } from "../../../../components/text.js"; import { TokenInfoRow } from "../pay-transactions/TokenInfoRow.js"; import type { FiatStatusMeta } from "../pay-transactions/statusMeta.js"; -import { getCurrencyMeta } from "./currencies.js"; +import { getCurrencyMeta, getFiatIcon } from "./currencies.js"; /** * Show a table with the details of a "OnRamp" transaction step in the "Buy with Fiat" flow. @@ -71,7 +71,7 @@ export function OnRampTxDetailsTable(props: { }} > - + {getFiatIcon(currencyMeta, "sm")} {formatNumber(Number(props.fiat.amount), 2)}{" "} {props.fiat.currencySymbol} diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/OnRampScreen.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/OnRampScreen.tsx new file mode 100644 index 00000000000..3bc6e459cab --- /dev/null +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/OnRampScreen.tsx @@ -0,0 +1,790 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { trackPayEvent } from "../../../../../../../analytics/track/pay.js"; +import { getCachedChain } from "../../../../../../../chains/utils.js"; +import type { ThirdwebClient } from "../../../../../../../client/client.js"; +import { getContract } from "../../../../../../../contract/contract.js"; +import { allowance } from "../../../../../../../extensions/erc20/__generated__/IERC20/read/allowance.js"; +import { approve } from "../../../../../../../extensions/erc20/write/approve.js"; +import type { BuyWithCryptoQuote } from "../../../../../../../pay/buyWithCrypto/getQuote.js"; +import type { BuyWithCryptoStatus } from "../../../../../../../pay/buyWithCrypto/getStatus.js"; +import type { BuyWithFiatQuote } from "../../../../../../../pay/buyWithFiat/getQuote.js"; +import { + type BuyWithFiatStatus, + getBuyWithFiatStatus, +} from "../../../../../../../pay/buyWithFiat/getStatus.js"; +import { + type OnRampStep, + getOnRampSteps, +} from "../../../../../../../pay/buyWithFiat/isSwapRequiredPostOnramp.js"; +import { sendBatchTransaction } from "../../../../../../../transaction/actions/send-batch-transaction.js"; +import { sendTransaction } from "../../../../../../../transaction/actions/send-transaction.js"; +import type { WaitForReceiptOptions } from "../../../../../../../transaction/actions/wait-for-tx-receipt.js"; +import { waitForReceipt } from "../../../../../../../transaction/actions/wait-for-tx-receipt.js"; +import { formatNumber } from "../../../../../../../utils/formatNumber.js"; +import { isEcosystemWallet } from "../../../../../../../wallets/ecosystem/is-ecosystem-wallet.js"; +import { isInAppWallet } from "../../../../../../../wallets/in-app/core/wallet/index.js"; +import type { Wallet } from "../../../../../../../wallets/interfaces/wallet.js"; +import { isSmartWallet } from "../../../../../../../wallets/smart/is-smart-wallet.js"; +import { spacing } from "../../../../../../core/design-system/index.js"; +import { useChainName } from "../../../../../../core/hooks/others/useChainQuery.js"; +import { useBuyWithCryptoQuote } from "../../../../../../core/hooks/pay/useBuyWithCryptoQuote.js"; +import { useBuyWithCryptoStatus } from "../../../../../../core/hooks/pay/useBuyWithCryptoStatus.js"; +import { useBuyWithFiatStatus } from "../../../../../../core/hooks/pay/useBuyWithFiatStatus.js"; +import { useConnectedWallets } from "../../../../../../core/hooks/wallets/useConnectedWallets.js"; +import { invalidateWalletBalance } from "../../../../../../core/providers/invalidateWalletBalance.js"; +import { Spacer } from "../../../../components/Spacer.js"; +import { Spinner } from "../../../../components/Spinner.js"; +import { SwitchNetworkButton } from "../../../../components/SwitchNetwork.js"; +import { Container, ModalHeader } from "../../../../components/basic.js"; +import { Button } from "../../../../components/buttons.js"; +import { Text } from "../../../../components/text.js"; +import { TokenSymbol } from "../../../../components/token/TokenSymbol.js"; +import { PayTokenIcon } from "../PayTokenIcon.js"; +import { openOnrampPopup } from "../openOnRamppopup.js"; +import type { FiatStatusMeta } from "../pay-transactions/statusMeta.js"; +import { StepConnectorArrow } from "../swap/StepConnector.js"; +import { WalletRow } from "../swap/WalletRow.js"; +import { addPendingTx } from "../swap/pendingSwapTx.js"; +import type { PayerInfo } from "../types.js"; +import { StepContainer } from "./FiatSteps.js"; + +type OnRampScreenState = { + steps: Array<{ + index: number; + step: OnRampStep; + status: FiatStatusMeta["progressStatus"]; + }>; + handleContinue: () => void; + isLoading: boolean; + isDone: boolean; + isFailed: boolean; +}; + +export function OnRampScreen(props: { + title: string; + quote: BuyWithFiatQuote; + onBack: () => void; + client: ThirdwebClient; + testMode: boolean; + theme: "light" | "dark"; + onDone: () => void; + transactionMode: boolean; + isEmbed: boolean; + payer: PayerInfo; + onSuccess: (status: BuyWithFiatStatus) => void; + receiverAddress: string; +}) { + const connectedWallets = useConnectedWallets(); + const isAutoMode = isInAppSigner({ + wallet: props.payer.wallet, + connectedWallets, + }); + const state = useOnRampScreenState({ + quote: props.quote, + client: props.client, + onSuccess: props.onSuccess, + onDone: props.onDone, + payer: props.payer, + theme: props.theme, + isAutoMode, + }); + const firstStepChainId = state.steps[0]?.step.token.chainId; + const currentStepIndex = state.steps.findIndex( + (step) => step.status === "pending" || step.status === "actionRequired", + ); + return ( + + + + + + + + + {state.steps.map(({ step, status }, index) => ( + + + + + {index < state.steps.length - 1 && ( + + )} + + ))} + + + + + + Keep this window open until all transactions are complete. + + + + + + {!state.isDone && + firstStepChainId && + firstStepChainId !== props.payer.chain.id ? ( + { + await props.payer.wallet.switchChain( + getCachedChain(firstStepChainId), + ); + }} + /> + ) : ( + + )} + + + ); +} + +function StepUI(props: { + step: OnRampStep; + index: number; + client: ThirdwebClient; + payer: PayerInfo; +}) { + const { step, client } = props; + const chain = useChainName(getCachedChain(step.token.chainId)); + return ( + + + + + + {step.action.charAt(0).toUpperCase() + step.action.slice(1)} + + + + + + {formatNumber(Number(step.amount), 5)} + + + + + + {chain.name} + + + + + + + ); +} + +function useOnRampScreenState(props: { + quote: BuyWithFiatQuote; + client: ThirdwebClient; + onSuccess: (status: BuyWithFiatStatus) => void; + onDone: () => void; + payer: PayerInfo; + theme: "light" | "dark"; + isAutoMode?: boolean; +}): OnRampScreenState { + const onRampSteps = getOnRampSteps(props.quote); + const [currentStepIndex, setCurrentStepIndex] = useState(0); + const [swapTxHash, setSwapTxHash] = useState<{ + hash: string; + chainId: number; + }>(); + const [popupWindow, setPopupWindow] = useState(null); + + // Track onramp status + const { uiStatus: fiatOnrampStatus } = useOnRampStatus({ + intentId: props.quote.intentId, + client: props.client, + onSuccess: (status) => { + if (onRampSteps.length === 1) { + // If only one step, this is the final success + props.onSuccess(status); + } else { + // Move to next step (swap) + setCurrentStepIndex((prev) => prev + 1); + } + }, + openedWindow: popupWindow, + }); + + // Get quote for current swap/bridge step if needed + const previousStep = onRampSteps[currentStepIndex - 1]; + const currentStep = onRampSteps[currentStepIndex]; + const swapQuoteQuery = useBuyWithCryptoQuote( + previousStep && currentStep + ? { + fromChainId: previousStep.token.chainId, + fromTokenAddress: previousStep.token.tokenAddress, + toAmount: currentStep.amount, + toChainId: currentStep.token.chainId, + toTokenAddress: currentStep.token.tokenAddress, + fromAddress: props.payer.account.address, + toAddress: props.payer.account.address, + client: props.client, + } + : undefined, + ); + + // Handle swap execution + const swapMutation = useSwapMutation({ + client: props.client, + payer: props.payer, + isFiatFlow: true, + }); + + // Track swap status + const { uiStatus: swapStatus } = useSwapStatus({ + client: props.client, + transactionHash: swapTxHash?.hash, + chainId: swapTxHash?.chainId, + onSuccess: () => { + if (currentStepIndex === onRampSteps.length - 1) { + // Last step completed - call final success + getBuyWithFiatStatus({ + intentId: props.quote.intentId, + client: props.client, + }).then(props.onSuccess); + } else { + // Reset swap state before moving to next step + setSwapTxHash(undefined); + swapMutation.reset(); + // Move to next step + setCurrentStepIndex((prev) => prev + 1); + } + }, + }); + + // Map steps to their current status + const steps = onRampSteps.map((step, index) => { + let status: FiatStatusMeta["progressStatus"] = "unknown"; + + if (index === 0) { + // First step (onramp) status + status = fiatOnrampStatus; + } else if (index < currentStepIndex) { + // Previous steps are completed + status = "completed"; + } else if (index === currentStepIndex) { + // Current step - could be swap or bridge + if (swapQuoteQuery.isLoading || swapMutation.isPending) { + status = "pending"; + } else if (swapQuoteQuery.error || swapMutation.error) { + status = "failed"; + } else if (swapTxHash) { + status = swapStatus; + } else { + status = "actionRequired"; + } + } + + return { + index, + step, + status, + }; + }); + + const isLoading = steps.some((step) => step.status === "pending"); + const isDone = steps.every((step) => step.status === "completed"); + const isFailed = steps.some((step) => step.status === "failed"); + + // Update handleContinue to handle done state + const handleContinue = useCallback(async () => { + if (isDone) { + props.onDone(); + return; + } + + if (currentStepIndex === 0) { + // First step - open onramp popup + const popup = openOnrampPopup(props.quote.onRampLink, props.theme); + trackPayEvent({ + event: "open_onramp_popup", + client: props.client, + walletAddress: props.payer.account.address, + walletType: props.payer.wallet.id, + }); + setPopupWindow(popup); + addPendingTx({ + type: "fiat", + intentId: props.quote.intentId, + }); + } else if (swapQuoteQuery.data && !swapTxHash) { + // Execute swap/bridge + try { + const result = await swapMutation.mutateAsync({ + quote: swapQuoteQuery.data, + }); + setSwapTxHash({ + hash: result.transactionHash, + chainId: result.chainId, + }); + } catch (e) { + console.error("Failed to execute swap:", e); + } + } else if (isFailed) { + // retry the quote step + setSwapTxHash(undefined); + swapMutation.reset(); + swapQuoteQuery.refetch(); + } + }, [ + isDone, + currentStepIndex, + swapQuoteQuery.data, + swapTxHash, + props.quote, + props.onDone, + swapMutation, + props.theme, + isFailed, + swapQuoteQuery.refetch, + swapMutation.reset, + props.client, + props.payer.account.address, + props.payer.wallet.id, + ]); + + // Auto-progress effect + useEffect(() => { + if (!props.isAutoMode) { + return; + } + + // Auto-start next swap step when previous step completes + if ( + !isLoading && + !isDone && + !isFailed && + currentStepIndex > 0 && + currentStepIndex < onRampSteps.length && + swapQuoteQuery.data && + !swapTxHash + ) { + handleContinue(); + } + }, [ + props.isAutoMode, + currentStepIndex, + swapQuoteQuery.data, + swapTxHash, + onRampSteps.length, + handleContinue, + isDone, + isFailed, + isLoading, + ]); + + return { + steps, + handleContinue, + isLoading, + isDone, + isFailed, + }; +} + +function useOnRampStatus(props: { + intentId: string; + client: ThirdwebClient; + onSuccess: (status: BuyWithFiatStatus) => void; + openedWindow: Window | null; +}) { + const queryClient = useQueryClient(); + const statusQuery = useBuyWithFiatStatus({ + intentId: props.intentId, + client: props.client, + queryOptions: { + enabled: !!props.openedWindow, + }, + }); + let uiStatus: FiatStatusMeta["progressStatus"] = "actionRequired"; + + switch (statusQuery.data?.status) { + case "ON_RAMP_TRANSFER_COMPLETED": + case "CRYPTO_SWAP_COMPLETED": + case "CRYPTO_SWAP_REQUIRED": + uiStatus = "completed"; + break; + case "CRYPTO_SWAP_FALLBACK": + uiStatus = "partialSuccess"; + break; + case "ON_RAMP_TRANSFER_FAILED": + case "PAYMENT_FAILED": + uiStatus = "failed"; + break; + case "PENDING_PAYMENT": + case "ON_RAMP_TRANSFER_IN_PROGRESS": + uiStatus = "pending"; + break; + default: + uiStatus = "actionRequired"; + break; + } + + const purchaseCbCalled = useRef(false); + useEffect(() => { + if (purchaseCbCalled.current || !props.onSuccess) { + return; + } + + if ( + statusQuery.data && + (uiStatus === "completed" || uiStatus === "partialSuccess") + ) { + purchaseCbCalled.current = true; + props.onSuccess(statusQuery.data); + } + }, [props.onSuccess, statusQuery.data, uiStatus]); + + // close the onramp popup if onramp is completed + useEffect(() => { + if (!props.openedWindow) { + return; + } + + if (uiStatus === "completed" || uiStatus === "partialSuccess") { + try { + if (props.openedWindow && !props.openedWindow.closed) { + props.openedWindow.close(); + } + } catch (e) { + console.warn("Failed to close payment window:", e); + } + } + }, [props.openedWindow, uiStatus]); + + // invalidate wallet balance when onramp is completed + const invalidatedBalance = useRef(false); + useEffect(() => { + if (!invalidatedBalance.current && uiStatus === "completed") { + invalidatedBalance.current = true; + invalidateWalletBalance(queryClient); + } + }, [uiStatus, queryClient]); + + return { uiStatus }; +} + +function useSwapStatus(props: { + client: ThirdwebClient; + transactionHash?: string; + chainId?: number; + onSuccess: (status: BuyWithCryptoStatus) => void; +}) { + const swapStatus = useBuyWithCryptoStatus( + props.transactionHash && props.chainId + ? { + client: props.client, + transactionHash: props.transactionHash, + chainId: props.chainId, + } + : undefined, + ); + + let uiStatus: FiatStatusMeta["progressStatus"] = "unknown"; + + switch (swapStatus.data?.status) { + case "COMPLETED": + uiStatus = "completed"; + break; + case "FAILED": + case "NOT_FOUND": + uiStatus = "failed"; + break; + case "PENDING": + uiStatus = "pending"; + break; + case "NONE": + uiStatus = "unknown"; + break; + default: + uiStatus = "unknown"; + break; + } + + const purchaseCbCalled = useRef(false); + useEffect(() => { + if (purchaseCbCalled.current || !props.onSuccess) { + return; + } + + if (swapStatus.data?.status === "COMPLETED") { + purchaseCbCalled.current = true; + props.onSuccess(swapStatus.data); + } + }, [props.onSuccess, swapStatus]); + + const queryClient = useQueryClient(); + const balanceInvalidated = useRef(false); + useEffect(() => { + if (uiStatus === "completed" && !balanceInvalidated.current) { + balanceInvalidated.current = true; + invalidateWalletBalance(queryClient); + } + }, [queryClient, uiStatus]); + + return { uiStatus }; +} + +function useSwapMutation(props: { + client: ThirdwebClient; + payer: PayerInfo; + isFiatFlow: boolean; +}) { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (input: { quote: BuyWithCryptoQuote }) => { + const { quote } = input; + const canBatch = props.payer.account.sendBatchTransaction; + const tokenContract = getContract({ + client: props.client, + address: quote.swapDetails.fromToken.tokenAddress, + chain: getCachedChain(quote.swapDetails.fromToken.chainId), + }); + const approveTxRequired = + quote.approvalData && + (await allowance({ + contract: tokenContract, + owner: props.payer.account.address, + spender: quote.approvalData.spenderAddress, + })) < BigInt(quote.approvalData.amountWei); + if (approveTxRequired && quote.approvalData && !canBatch) { + trackPayEvent({ + event: "prompt_swap_approval", + client: props.client, + walletAddress: props.payer.account.address, + walletType: props.payer.wallet.id, + fromToken: quote.swapDetails.fromToken.tokenAddress, + fromAmount: quote.swapDetails.fromAmountWei, + toToken: quote.swapDetails.toToken.tokenAddress, + toAmount: quote.swapDetails.toAmountWei, + chainId: quote.swapDetails.fromToken.chainId, + dstChainId: quote.swapDetails.toToken.chainId, + }); + + const transaction = approve({ + contract: tokenContract, + spender: quote.approvalData.spenderAddress, + amountWei: BigInt(quote.approvalData.amountWei), + }); + + const tx = await sendTransaction({ + account: props.payer.account, + transaction, + }); + + await waitForReceipt({ ...tx, maxBlocksWaitTime: 50 }); + + trackPayEvent({ + event: "swap_approval_success", + client: props.client, + walletAddress: props.payer.account.address, + walletType: props.payer.wallet.id, + fromToken: quote.swapDetails.fromToken.tokenAddress, + fromAmount: quote.swapDetails.fromAmountWei, + toToken: quote.swapDetails.toToken.tokenAddress, + toAmount: quote.swapDetails.toAmountWei, + chainId: quote.swapDetails.fromToken.chainId, + dstChainId: quote.swapDetails.toToken.chainId, + }); + } + + trackPayEvent({ + event: "prompt_swap_execution", + client: props.client, + walletAddress: props.payer.account.address, + walletType: props.payer.wallet.id, + fromToken: quote.swapDetails.fromToken.tokenAddress, + fromAmount: quote.swapDetails.fromAmountWei, + toToken: quote.swapDetails.toToken.tokenAddress, + toAmount: quote.swapDetails.toAmountWei, + chainId: quote.swapDetails.fromToken.chainId, + dstChainId: quote.swapDetails.toToken.chainId, + }); + const tx = quote.transactionRequest; + let _swapTx: WaitForReceiptOptions; + // check if we can batch approval and swap + if (canBatch && quote.approvalData && approveTxRequired) { + const approveTx = approve({ + contract: tokenContract, + spender: quote.approvalData.spenderAddress, + amountWei: BigInt(quote.approvalData.amountWei), + }); + + _swapTx = await sendBatchTransaction({ + account: props.payer.account, + transactions: [approveTx, tx], + }); + } else { + _swapTx = await sendTransaction({ + account: props.payer.account, + transaction: tx, + }); + } + + await waitForReceipt({ ..._swapTx, maxBlocksWaitTime: 50 }); + + trackPayEvent({ + event: "swap_execution_success", + client: props.client, + walletAddress: props.payer.account.address, + walletType: props.payer.wallet.id, + fromToken: quote.swapDetails.fromToken.tokenAddress, + fromAmount: quote.swapDetails.fromAmountWei, + toToken: quote.swapDetails.toToken.tokenAddress, + toAmount: quote.swapDetails.toAmountWei, + chainId: quote.swapDetails.fromToken.chainId, + dstChainId: quote.swapDetails.toToken.chainId, + }); + + // do not add pending tx if the swap is part of fiat flow + if (!props.isFiatFlow) { + addPendingTx({ + type: "swap", + txHash: _swapTx.transactionHash, + chainId: _swapTx.chain.id, + }); + } + + return { + transactionHash: _swapTx.transactionHash, + chainId: _swapTx.chain.id, + }; + }, + onSuccess: () => { + invalidateWalletBalance(queryClient); + }, + }); +} + +function isInAppSigner(options: { + wallet: Wallet; + connectedWallets: Wallet[]; +}) { + const isInAppOrEcosystem = (w: Wallet) => + isInAppWallet(w) || isEcosystemWallet(w); + const isSmartWalletWithAdmin = + isSmartWallet(options.wallet) && + options.connectedWallets.some( + (w) => + isInAppOrEcosystem(w) && + w.getAccount()?.address?.toLowerCase() === + options.wallet.getAdminAccount?.()?.address?.toLowerCase(), + ); + return isInAppOrEcosystem(options.wallet) || isSmartWalletWithAdmin; +} diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/currencies.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/currencies.tsx index 780415d6847..694da52dbda 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/currencies.tsx +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/currencies.tsx @@ -1,4 +1,6 @@ import { RadiobuttonIcon } from "@radix-ui/react-icons"; +import type { SupportedFiatCurrency } from "../../../../../../../pay/convert/type.js"; +import { iconSize } from "../../../../../../core/design-system/index.js"; import { CADIcon } from "../../../icons/currencies/CADIcon.js"; import { EURIcon } from "../../../icons/currencies/EURIcon.js"; import { GBPIcon } from "../../../icons/currencies/GBPIcon.js"; @@ -7,14 +9,16 @@ import { USDIcon } from "../../../icons/currencies/USDIcon.js"; import type { IconFC } from "../../../icons/types.js"; export type CurrencyMeta = { - shorthand: "USD" | "CAD" | "GBP" | "EUR" | "JPY"; + shorthand: SupportedFiatCurrency; + countryCode: string; name: string; symbol: string; - icon: IconFC; + icon?: IconFC; }; export const usdCurrency: CurrencyMeta = { shorthand: "USD", + countryCode: "US", name: "US Dollar", symbol: "$", icon: USDIcon, @@ -24,28 +28,44 @@ export const currencies: CurrencyMeta[] = [ usdCurrency, { shorthand: "CAD", + countryCode: "CA", name: "Canadian Dollar", symbol: "$", icon: CADIcon, }, { shorthand: "GBP", + countryCode: "GB", name: "British Pound", symbol: "£", icon: GBPIcon, }, { shorthand: "EUR", + countryCode: "EU", name: "Euro", symbol: "€", icon: EURIcon, }, { shorthand: "JPY", + countryCode: "JP", name: "Japanese Yen", symbol: "¥", icon: JPYIcon, }, + { + shorthand: "AUD", + countryCode: "AU", + name: "Australian Dollar", + symbol: "$", + }, + { + shorthand: "NZD", + countryCode: "NZ", + name: "New Zealand Dollar", + symbol: "$", + }, ]; export function getCurrencyMeta(shorthand: string): CurrencyMeta { @@ -56,6 +76,7 @@ export function getCurrencyMeta(shorthand: string): CurrencyMeta { ) ?? { // This should never happen icon: UnknownCurrencyIcon, + countryCode: "US", name: shorthand, symbol: "$", shorthand: shorthand as CurrencyMeta["shorthand"], @@ -63,6 +84,21 @@ export function getCurrencyMeta(shorthand: string): CurrencyMeta { ); } +export function getFiatIcon( + currency: CurrencyMeta, + size: keyof typeof iconSize, +): React.ReactNode { + return currency.icon ? ( + + ) : ( + {currency.shorthand} + ); +} const UnknownCurrencyIcon: IconFC = (props) => { return ; }; diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/main/types.ts b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/main/types.ts index 0c2373ab5b9..3baf7a0e792 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/main/types.ts +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/main/types.ts @@ -39,7 +39,6 @@ export type SelectedScreen = | { id: "fiat-flow"; quote: BuyWithFiatQuote; - openedWindow: Window | null; } | { id: "transfer-flow"; diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/main/useUISelectionStates.ts b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/main/useUISelectionStates.ts index f4f197d1e36..206283899d2 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/main/useUISelectionStates.ts +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/main/useUISelectionStates.ts @@ -163,6 +163,16 @@ function getDefaultCurrencyBasedOnLocation(): CurrencyMeta["shorthand"] { return "CAD"; } + // australia + if (timeZone.includes("australia")) { + return "AUD"; + } + + // new zealand + if (timeZone.includes("new zealand")) { + return "NZD"; + } + return "USD"; } catch { return "USD"; diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/StepConnector.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/StepConnector.tsx new file mode 100644 index 00000000000..412592c700d --- /dev/null +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/StepConnector.tsx @@ -0,0 +1,55 @@ +import { useCustomTheme } from "../../../../../../core/design-system/CustomThemeProvider.js"; +import { Container } from "../../../../components/basic.js"; + +export function StepConnectorArrow(props: { + active: boolean; +}) { + const theme = useCustomTheme(); + return ( + + + + + + + ); +} diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/SwapSummary.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/SwapSummary.tsx index 41fd822f902..601db6bc8f7 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/SwapSummary.tsx +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/SwapSummary.tsx @@ -1,14 +1,11 @@ -import { ChevronDownIcon } from "@radix-ui/react-icons"; import type { Chain } from "../../../../../../../chains/types.js"; import type { ThirdwebClient } from "../../../../../../../client/client.js"; import { useCustomTheme } from "../../../../../../core/design-system/CustomThemeProvider.js"; -import { - iconSize, - radius, -} from "../../../../../../core/design-system/index.js"; +import { radius } from "../../../../../../core/design-system/index.js"; import { Container } from "../../../../components/basic.js"; import { TokenRow } from "../../../../components/token/TokenRow.js"; import type { ERC20OrNativeToken } from "../../nativeToken.js"; +import { StepConnectorArrow } from "./StepConnector.js"; import { WalletRow } from "./WalletRow.js"; export function SwapSummary(props: { @@ -36,23 +33,21 @@ export function SwapSummary(props: { border: `1px solid ${theme.colors.borderColor}`, }} > - {isDifferentRecipient && ( - - - - )} + + + {/* Connector Icon */} - - - - - - + {/* Buy */} - ) : null} + ) : ( + + )} + {props.label ? ( + + {props.label} + + ) : null} {addressOrENS || shortenAddress(props.address)}