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 ? (
+
+ ) : (
+
+ );
+}
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)}