diff --git a/apps/playground-web/src/app/insight/utils.ts b/apps/playground-web/src/app/insight/utils.ts index 360e8f9a267..a9503422a65 100644 --- a/apps/playground-web/src/app/insight/utils.ts +++ b/apps/playground-web/src/app/insight/utils.ts @@ -55,17 +55,22 @@ export async function fetchBlueprintSpec(params: { } export async function fetchAllBlueprints() { - // fetch list - const blueprintSpecs = await fetchBlueprintList(); - - // fetch all blueprints - const blueprints = await Promise.all( - blueprintSpecs.map((spec) => - fetchBlueprintSpec({ - blueprintId: spec.id, - }), - ), - ); - - return blueprints; + try { + // fetch list + const blueprintSpecs = await fetchBlueprintList(); + + // fetch all blueprints + const blueprints = await Promise.all( + blueprintSpecs.map((spec) => + fetchBlueprintSpec({ + blueprintId: spec.id, + }), + ), + ); + + return blueprints; + } catch (error) { + console.error(error); + return []; + } } diff --git a/apps/playground-web/src/components/pay/direct-payment.tsx b/apps/playground-web/src/components/pay/direct-payment.tsx index 09879b44a77..3641f94cbfd 100644 --- a/apps/playground-web/src/components/pay/direct-payment.tsx +++ b/apps/playground-web/src/components/pay/direct-payment.tsx @@ -1,6 +1,5 @@ "use client"; - -import { sepolia } from "thirdweb/chains"; +import { base } from "thirdweb/chains"; import { PayEmbed, getDefaultToken } from "thirdweb/react"; import { THIRDWEB_CLIENT } from "../../lib/client"; import { StyledConnectButton } from "../styled-connect-button"; @@ -16,9 +15,9 @@ export function BuyMerchPreview() { payOptions={{ mode: "direct_payment", paymentInfo: { - amount: "0.1", - chain: sepolia, - token: getDefaultToken(sepolia, "USDC"), + amount: "2", + chain: base, + token: getDefaultToken(base, "USDC"), sellerAddress: "0xEb0effdFB4dC5b3d5d3aC6ce29F3ED213E95d675", }, metadata: { diff --git a/apps/playground-web/src/components/pay/transaction-button.tsx b/apps/playground-web/src/components/pay/transaction-button.tsx index ee2404a6932..82cebc4effc 100644 --- a/apps/playground-web/src/components/pay/transaction-button.tsx +++ b/apps/playground-web/src/components/pay/transaction-button.tsx @@ -2,7 +2,7 @@ import { useTheme } from "next-themes"; import { getContract } from "thirdweb"; -import { base, sepolia } from "thirdweb/chains"; +import { base, polygon } from "thirdweb/chains"; import { transfer } from "thirdweb/extensions/erc20"; import { claimTo, getNFT } from "thirdweb/extensions/erc1155"; import { @@ -21,12 +21,12 @@ const nftContract = getContract({ client: THIRDWEB_CLIENT, }); -const USDC = getDefaultToken(sepolia, "USDC"); +const USDC = getDefaultToken(polygon, "USDC"); const usdcContract = getContract({ // biome-ignore lint/style/noNonNullAssertion: its there address: USDC!.address, - chain: sepolia, + chain: polygon, client: THIRDWEB_CLIENT, }); @@ -55,6 +55,7 @@ export function PayTransactionPreview() { to: account?.address || "", }), metadata: nft?.metadata, + buyWithFiat: false, }} /> )} @@ -69,6 +70,7 @@ export function PayTransactionButtonPreview() { return ( <> +
{account && (
diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/BuyScreen.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/BuyScreen.tsx index 507fb089f88..9984a284ae0 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/BuyScreen.tsx +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/BuyScreen.tsx @@ -8,11 +8,9 @@ import type { BuyWithFiatStatus } from "../../../../../../pay/buyWithFiat/getSta import { formatNumber } from "../../../../../../utils/formatNumber.js"; import type { Account } from "../../../../../../wallets/interfaces/wallet.js"; import type { WalletId } from "../../../../../../wallets/wallet-types.js"; -import { useCustomTheme } from "../../../../../core/design-system/CustomThemeProvider.js"; import { type Theme, fontSize, - radius, spacing, } from "../../../../../core/design-system/index.js"; import type { @@ -27,10 +25,9 @@ import { LoadingScreen } from "../../../../wallets/shared/LoadingScreen.js"; import type { PayEmbedConnectOptions } from "../../../PayEmbed.js"; import { ChainName } from "../../../components/ChainName.js"; import { Spacer } from "../../../components/Spacer.js"; -import { Container, ModalHeader } from "../../../components/basic.js"; +import { Container, Line, ModalHeader } from "../../../components/basic.js"; import { Button } from "../../../components/buttons.js"; import { Input } from "../../../components/formElements.js"; -import { Text } from "../../../components/text.js"; import { TokenSymbol } from "../../../components/token/TokenSymbol.js"; import { ConnectButton } from "../../ConnectButton.js"; import { ChainButton, NetworkSelectorContent } from "../../NetworkSelector.js"; @@ -432,67 +429,6 @@ function BuyScreenContent(props: BuyScreenContentProps) { ); } - if ( - screen.id === "select-from-token" && - supportedSourcesQuery.data && - sourceSupportedTokens - ) { - const chains = supportedSourcesQuery.data.map((x) => x.chain); - const goBack = () => setScreen(screen.backScreen); - // if token selection is disabled - only show network selector screen - if ( - payOptions.buyWithCrypto !== false && - payOptions.buyWithCrypto?.prefillSource?.allowEdits?.token === false - ) { - return ( - - ); - } - - return ( - { - setScreen({ - id: "connect-payer-wallet", - backScreen: screen, - }); - }} - modalTitle="Available tokens" - connectLocale={props.connectLocale} - client={props.client} - sourceTokens={sourceSupportedTokens} - sourceSupportedTokens={sourceSupportedTokens} - toChain={toChain} - toToken={toToken} - tokenAmount={tokenAmount} - fromChain={fromChain} - fromToken={fromToken} - mode={payOptions.mode} - hiddenWallets={props.hiddenWallets} - onSelect={(w, token, chain) => { - const account = w.getAccount(); - if (account) { - setPayer({ - account, - chain, - wallet: w, - }); - setFromToken(token); - setFromChain(chain); - } - goBack(); - }} - /> - ); - } - return (
@@ -526,19 +462,10 @@ function BuyScreenContent(props: BuyScreenContentProps) { {(screen.id === "select-payment-method" || screen.id === "buy-with-crypto" || - screen.id === "buy-with-fiat") && + screen.id === "buy-with-fiat" || + screen.id === "select-from-token") && payer && ( { - setScreen({ - id: mode === "swap" ? "buy-with-crypto" : "buy-with-fiat", - }); - }} disabled={ ("prefillBuy" in payOptions && payOptions.prefillBuy?.allowEdits?.amount === false) || @@ -551,7 +478,19 @@ function BuyScreenContent(props: BuyScreenContentProps) { setTokenAmount={setTokenAmount} client={client} onBack={() => { - setScreen({ id: "main" }); + if ( + screen.id === "buy-with-crypto" || + screen.id === "buy-with-fiat" + ) { + setScreen({ + id: "select-from-token", + backScreen: { id: "main" }, + }); + } else if (screen.id === "select-from-token") { + setScreen(screen.backScreen); + } else { + setScreen({ id: "main" }); + } }} > {screen.id === "buy-with-crypto" && activeAccount && ( @@ -614,6 +553,45 @@ function BuyScreenContent(props: BuyScreenContentProps) { setHasEditedAmount={setHasEditedAmount} /> )} + + {screen.id === "select-from-token" && + supportedSourcesQuery.data && + sourceSupportedTokens && ( + { + setScreen({ + id: "connect-payer-wallet", + backScreen: screen, + }); + }} + onPayWithFiat={() => { + setScreen({ + id: "buy-with-fiat", + }); + }} + onSelectToken={(w, token, chain) => { + const account = w.getAccount(); + if (account) { + setPayer({ + account, + chain, + wallet: w, + }); + setFromToken(token); + setFromChain(chain); + } + setScreen({ id: "buy-with-crypto" }); + }} + /> + )} )}
@@ -802,7 +780,10 @@ function MainScreen(props: { if (buyWithFiatEnabled && !buyWithCryptoEnabled) { props.setScreen({ id: "buy-with-fiat" }); } else { - props.setScreen({ id: "buy-with-crypto" }); + props.setScreen({ + id: "select-from-token", + backScreen: { id: "main" }, + }); } }} /> @@ -825,7 +806,10 @@ function MainScreen(props: { if (buyWithFiatEnabled && !buyWithCryptoEnabled) { props.setScreen({ id: "buy-with-fiat" }); } else { - props.setScreen({ id: "buy-with-crypto" }); + props.setScreen({ + id: "select-from-token", + backScreen: { id: "main" }, + }); } }} /> @@ -888,7 +872,10 @@ function MainScreen(props: { if (buyWithFiatEnabled && !buyWithCryptoEnabled) { props.setScreen({ id: "buy-with-fiat" }); } else { - props.setScreen({ id: "buy-with-crypto" }); + props.setScreen({ + id: "select-from-token", + backScreen: { id: "main" }, + }); } }} > @@ -912,12 +899,7 @@ function TokenSelectedLayout(props: { client: ThirdwebClient; onBack: () => void; disabled?: boolean; - mode: "buy" | "swap"; - onModeChange: (mode: "buy" | "swap") => void; - isBuyWithFiatEnabled: boolean; - isBuyWithCryptoEnabled: boolean; }) { - const theme = useCustomTheme(); return ( @@ -940,78 +922,9 @@ function TokenSelectedLayout(props: { disabled={props.disabled} /> - - - Pay with - {props.isBuyWithFiatEnabled && props.isBuyWithCryptoEnabled && ( - - -
- - - )} - - - - + + + {props.children} diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/TransactionModeScreen.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/TransactionModeScreen.tsx index 2e7a56b0caf..5970ad46fbd 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/TransactionModeScreen.tsx +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/TransactionModeScreen.tsx @@ -33,7 +33,7 @@ import { isNativeToken, } from "../nativeToken.js"; import { useTransactionCostAndData } from "./main/useBuyTxStates.js"; -import { WalletRow } from "./swap/TokenSelectorScreen.js"; +import { WalletRow } from "./swap/WalletRow.js"; import type { SupportedChainAndTokens } from "./swap/useSwapSupportedChains.js"; export function TransactionModeScreen(props: { 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 6447e65958c..affe26a0156 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 @@ -199,6 +199,7 @@ export function FiatScreenContent(props: { )} + Pay with credit card
- Pay + Pay x.chain.id !== 1) // dont use mainnet as a default source, unless its the only source - : undefined; + // TODO (pay) - auto select token based on connected wallet balances // Source token and chain selection --------------------------------------------------- const [fromChain_, setFromChain] = useState(); @@ -95,12 +90,7 @@ export function useFromTokenSelectionStates(options: { (payOptions.mode === "transaction" && payOptions.transaction?.chain) || (payOptions.mode === "direct_payment" && payOptions.paymentInfo?.chain); - const fromChainFromApi = firstSupportedSource?.chain - ? firstSupportedSource.chain - : undefined; - - const fromChain = - fromChain_ || fromChainDevSpecified || fromChainFromApi || polygon; + const fromChain = fromChain_ || fromChainDevSpecified || undefined; const [fromToken_, setFromToken] = useState(); @@ -110,12 +100,8 @@ export function useFromTokenSelectionStates(options: { payOptions.buyWithCrypto?.prefillSource?.token) || (payOptions.mode === "direct_payment" && payOptions.paymentInfo.token); - // May be updated in the future - const fromTokenFromApi = NATIVE_TOKEN; - // supported tokens query in here - const fromToken = - fromToken_ || fromTokenDevSpecified || fromTokenFromApi || NATIVE_TOKEN; + const fromToken = fromToken_ || fromTokenDevSpecified || undefined; return { fromChain, diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/ConfirmationScreen.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/ConfirmationScreen.tsx index 89d0777e772..cd930784600 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/ConfirmationScreen.tsx +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/ConfirmationScreen.tsx @@ -12,29 +12,20 @@ import { type WaitForReceiptOptions, waitForReceipt, } from "../../../../../../../transaction/actions/wait-for-tx-receipt.js"; -import { shortenAddress } from "../../../../../../../utils/address.js"; -import { formatNumber } from "../../../../../../../utils/formatNumber.js"; import { useCustomTheme } from "../../../../../../core/design-system/CustomThemeProvider.js"; -import { - fontSize, - iconSize, -} from "../../../../../../core/design-system/index.js"; -import { useChainName } from "../../../../../../core/hooks/others/useChainQuery.js"; -import { useEnsName } from "../../../../../../core/utils/wallet.js"; -import { Skeleton } from "../../../../components/Skeleton.js"; +import { iconSize } from "../../../../../../core/design-system/index.js"; import { Spacer } from "../../../../components/Spacer.js"; import { Spinner } from "../../../../components/Spinner.js"; import { StepBar } from "../../../../components/StepBar.js"; import { SwitchNetworkButton } from "../../../../components/SwitchNetwork.js"; -import { Container, Line, ModalHeader } from "../../../../components/basic.js"; +import { Container, ModalHeader } from "../../../../components/basic.js"; import { Button } from "../../../../components/buttons.js"; import { Text } from "../../../../components/text.js"; import { StyledDiv } from "../../../../design-system/elements.js"; import type { ERC20OrNativeToken } from "../../nativeToken.js"; -import { PayTokenIcon } from "../PayTokenIcon.js"; import { Step } from "../Stepper.js"; import type { PayerInfo } from "../types.js"; -import { formatSeconds } from "./formatSeconds.js"; +import { SwapSummary } from "./SwapSummary.js"; import { addPendingTx } from "./pendingSwapTx.js"; /** @@ -74,9 +65,6 @@ export function SwapConfirmationScreen(props: { const receiver = props.quote.swapDetails.toAddress; const sender = props.quote.swapDetails.fromAddress; - const isDifferentRecipient = receiver.toLowerCase() !== sender.toLowerCase(); - - const ensName = useEnsName({ client: props.client, address: receiver }); return ( @@ -94,58 +82,26 @@ export function SwapConfirmationScreen(props: { ) : ( - - )} - - {/* Pay */} - - - - - {/* Receive */} - {!isDifferentRecipient && ( - - - + <> + + Confirm payment + + )} - {/* Fees */} - - - - - {/* Time */} - - - ~ - {formatSeconds( - props.quote.swapDetails.estimated.durationSeconds || 0, - )} - - - - {/* Send to */} - {isDifferentRecipient && ( - - - {ensName.data || shortenAddress(receiver)} - - - )} + - + {/* Show 2 steps - Approve and confirm */} {needsApprovalStep && ( @@ -348,100 +304,3 @@ export const ConnectorLine = /* @__PURE__ */ StyledDiv(() => { flex: 1, }; }); - -function RenderTokenInfo(props: { - chain: Chain; - token: ERC20OrNativeToken; - amount: string; - symbol: string; - client: ThirdwebClient; -}) { - const { name } = useChainName(props.chain); - return ( - - - - {props.amount} {props.symbol} - - - - - {name ? ( - {name} - ) : ( - - )} - - ); -} - -function ConfirmItem(props: { - label: string; - children: React.ReactNode; -}) { - return ( - <> - - - {props.label} - - {props.children} - - - - ); -} - -/** - * @internal - */ -function SwapFeesRightAligned(props: { - quote: BuyWithCryptoQuote; -}) { - return ( - - {props.quote.processingFees.map((fee) => { - const feeAmount = formatNumber(Number(fee.amount), 6); - return ( - - - {feeAmount === 0 ? "~" : ""} - {feeAmount} {fee.token.symbol} - - - (${(fee.amountUSDCents / 100).toFixed(2)}) - - - ); - })} - - ); -} diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/PayWithCrypto.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/PayWithCrypto.tsx index ca32442f9d8..8a535e2e5ab 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/PayWithCrypto.tsx +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/PayWithCrypto.tsx @@ -16,7 +16,7 @@ import { TokenRow } from "../../../../components/token/TokenRow.js"; import { TokenSymbol } from "../../../../components/token/TokenSymbol.js"; import { formatTokenBalance } from "../../formatTokenBalance.js"; import { type NativeToken, isNativeToken } from "../../nativeToken.js"; -import { WalletRow } from "./TokenSelectorScreen.js"; +import { WalletRow } from "./WalletRow.js"; /** * Shows an amount "value" and renders the selected token and chain @@ -26,8 +26,8 @@ import { WalletRow } from "./TokenSelectorScreen.js"; */ export function PayWithCryptoQuoteInfo(props: { value: string; - chain: Chain; - token: TokenInfo | NativeToken; + chain: Chain | undefined; + token: TokenInfo | NativeToken | undefined; isLoading: boolean; client: ThirdwebClient; freezeChainAndTokenSelection?: boolean; @@ -36,12 +36,19 @@ export function PayWithCryptoQuoteInfo(props: { onSelectToken: () => void; }) { const theme = useCustomTheme(); - const balanceQuery = useWalletBalance({ - address: props.payerAccount.address, - chain: props.chain, - tokenAddress: isNativeToken(props.token) ? undefined : props.token.address, - client: props.client, - }); + const balanceQuery = useWalletBalance( + { + address: props.payerAccount.address, + chain: props.chain, + tokenAddress: isNativeToken(props.token) + ? undefined + : props.token?.address, + client: props.client, + }, + { + enabled: !!props.chain && !!props.token, + }, + ); return ( - {balanceQuery.data ? ( + {props.token && props.chain && balanceQuery.data ? ( - {formatTokenBalance(balanceQuery.data, false)} + {formatTokenBalance(balanceQuery.data, false, 4)} - ) : ( + ) : props.token && props.chain && balanceQuery.isLoading ? ( - )} + ) : null} - {/* Quoted price */} + {/* Quoted price & token selector */} diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/SwapScreenContent.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/SwapScreenContent.tsx index 0c954b4fe48..bdddf776f0a 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/SwapScreenContent.tsx +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/SwapScreenContent.tsx @@ -42,8 +42,8 @@ export function SwapScreenContent(props: { tokenAmount: string; toToken: ERC20OrNativeToken; toChain: Chain; - fromChain: Chain; - fromToken: ERC20OrNativeToken; + fromChain: Chain | undefined; + fromToken: ERC20OrNativeToken | undefined; showFromTokenSelector: () => void; payer: PayerInfo; client: ThirdwebClient; @@ -81,42 +81,50 @@ export function SwapScreenContent(props: { "fees" | "receiver" | "payer" >("fees"); - const fromTokenBalanceQuery = useWalletBalance({ - address: payer.account.address, - chain: fromChain, - tokenAddress: isNativeToken(fromToken) ? undefined : fromToken.address, - client, - }); + const fromTokenBalanceQuery = useWalletBalance( + { + address: payer.account.address, + chain: fromChain, + tokenAddress: isNativeToken(fromToken) ? undefined : fromToken?.address, + client, + }, + { + enabled: !!fromChain && !!fromToken, + }, + ); const fromTokenId = isNativeToken(fromToken) ? NATIVE_TOKEN_ADDRESS - : fromToken.address.toLowerCase(); + : fromToken?.address?.toLowerCase(); const toTokenId = isNativeToken(toToken) ? NATIVE_TOKEN_ADDRESS : toToken.address.toLowerCase(); const swapRequired = !!tokenAmount && - !(fromChain.id === toChain.id && fromTokenId === toTokenId); - const quoteParams: GetBuyWithCryptoQuoteParams | undefined = swapRequired - ? { - // wallets - fromAddress: payer.account.address, - toAddress: receiverAddress, - // from - fromChainId: fromChain.id, - fromTokenAddress: isNativeToken(fromToken) - ? NATIVE_TOKEN_ADDRESS - : fromToken.address, - // to - toChainId: toChain.id, - toTokenAddress: isNativeToken(toToken) - ? NATIVE_TOKEN_ADDRESS - : toToken.address, - toAmount: tokenAmount, - client, - purchaseData: payOptions.purchaseData, - } - : undefined; + !!fromChain && + !!fromTokenId && + !(fromChain?.id === toChain.id && fromTokenId === toTokenId); + const quoteParams: GetBuyWithCryptoQuoteParams | undefined = + fromChain && fromToken && swapRequired + ? { + // wallets + fromAddress: payer.account.address, + toAddress: receiverAddress, + // from + fromChainId: fromChain.id, + fromTokenAddress: isNativeToken(fromToken) + ? NATIVE_TOKEN_ADDRESS + : fromToken.address, + // to + toChainId: toChain.id, + toTokenAddress: isNativeToken(toToken) + ? NATIVE_TOKEN_ADDRESS + : toToken.address, + toAmount: tokenAmount, + client, + purchaseData: payOptions.purchaseData, + } + : undefined; const quoteQuery = useBuyWithCryptoQuote(quoteParams, { // refetch every 30 seconds @@ -159,11 +167,13 @@ export function SwapScreenContent(props: { Number(fromTokenBalanceQuery.data.displayValue) < Number(sourceTokenAmount); const disableContinue = + !fromChain || + !fromToken || (swapRequired && !quoteQuery.data) || isNotEnoughBalance || allowanceQuery.isLoading; const switchChainRequired = - props.payer.wallet.getChain()?.id !== fromChain.id; + props.payer.wallet.getChain()?.id !== fromChain?.id; const errorMsg = !quoteQuery.isLoading && quoteQuery.error @@ -240,6 +250,19 @@ export function SwapScreenContent(props: { {/* Quote info */} + + Pay with + {fromToken && fromChain ? ( + + ) : ( + "crypto" + )} +
- {swapRequired && ( + {swapRequired && fromChain && fromToken && ( - Not enough funds. + Insufficient funds
)} @@ -317,9 +340,10 @@ export function SwapScreenContent(props: { fullWidth onClick={() => props.showFromTokenSelector()} > - Select another token + Pay with another token ) : switchChainRequired && + fromChain && !quoteQuery.isLoading && !allowanceQuery.isLoading && !isNotEnoughBalance && 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 new file mode 100644 index 00000000000..a4af2b4c07c --- /dev/null +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/SwapSummary.tsx @@ -0,0 +1,128 @@ +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 { Container } from "../../../../components/basic.js"; +import { TokenRow } from "../../../../components/token/TokenRow.js"; +import type { ERC20OrNativeToken } from "../../nativeToken.js"; +import { WalletRow } from "./WalletRow.js"; + +export function SwapSummary(props: { + sender: string; + receiver: string; + client: ThirdwebClient; + fromToken: ERC20OrNativeToken; + fromChain: Chain; + toToken: ERC20OrNativeToken; + toChain: Chain; + fromAmount: string; + toAmount: string; +}) { + const theme = useCustomTheme(); + const isDifferentRecipient = + props.receiver.toLowerCase() !== props.sender.toLowerCase(); + return ( + + {/* Sell */} + + {isDifferentRecipient && ( + + + + )} + {}} + style={{ + background: "transparent", + borderRadius: 0, + border: "none", + }} + /> + + {/* Connector Icon */} + + + + + + + {/* Buy */} + + {isDifferentRecipient && ( + + + + )} + {}} + style={{ + background: "transparent", + borderRadius: 0, + border: "none", + borderTop: `1px solid ${theme.colors.borderColor}`, + }} + /> + + + ); +} diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/TokenSelectorScreen.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/TokenSelectorScreen.tsx index 853576cc1de..a7c0a0efdbc 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/TokenSelectorScreen.tsx +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/TokenSelectorScreen.tsx @@ -1,10 +1,14 @@ import styled from "@emotion/styled"; +import { + CardStackIcon, + ChevronRightIcon, + Cross2Icon, +} from "@radix-ui/react-icons"; import { useQuery } from "@tanstack/react-query"; import type { Chain } from "../../../../../../../chains/types.js"; import { getCachedChain } from "../../../../../../../chains/utils.js"; import type { ThirdwebClient } from "../../../../../../../client/client.js"; import { NATIVE_TOKEN_ADDRESS } from "../../../../../../../constants/addresses.js"; -import { shortenAddress } from "../../../../../../../utils/address.js"; import type { Wallet } from "../../../../../../../wallets/interfaces/wallet.js"; import { type GetWalletBalanceResult, @@ -13,7 +17,6 @@ import { import type { WalletId } from "../../../../../../../wallets/wallet-types.js"; import { useCustomTheme } from "../../../../../../core/design-system/CustomThemeProvider.js"; import { - type fontSize, iconSize, radius, spacing, @@ -25,28 +28,23 @@ import { } from "../../../../../../core/hooks/others/useChainQuery.js"; import { useActiveAccount } from "../../../../../../core/hooks/wallets/useActiveAccount.js"; import { useConnectedWallets } from "../../../../../../core/hooks/wallets/useConnectedWallets.js"; +import { useDisconnect } from "../../../../../../core/hooks/wallets/useDisconnect.js"; import type { SupportedTokens, TokenInfo, } from "../../../../../../core/utils/defaultTokens.js"; -import { - useEnsAvatar, - useEnsName, -} from "../../../../../../core/utils/wallet.js"; import { LoadingScreen } from "../../../../../wallets/shared/LoadingScreen.js"; -import { Img } from "../../../../components/Img.js"; +import { Spacer } from "../../../../components/Spacer.js"; import { TextDivider } from "../../../../components/TextDivider.js"; import { TokenIcon } from "../../../../components/TokenIcon.js"; -import { WalletImage } from "../../../../components/WalletImage.js"; -import { Container, Line, ModalHeader } from "../../../../components/basic.js"; +import { Container } from "../../../../components/basic.js"; import { Button } from "../../../../components/buttons.js"; import { Text } from "../../../../components/text.js"; -import { Blobbie } from "../../../Blobbie.js"; import { OutlineWalletIcon } from "../../../icons/OutlineWalletIcon.js"; -import type { ConnectLocale } from "../../../locale/types.js"; import { formatTokenBalance } from "../../formatTokenBalance.js"; import { type ERC20OrNativeToken, isNativeToken } from "../../nativeToken.js"; import { FiatValue } from "./FiatValue.js"; +import { WalletRow } from "./WalletRow.js"; type TokenBalance = { balance: GetWalletBalanceResult; @@ -54,28 +52,28 @@ type TokenBalance = { token: TokenInfo; }; +type WalletKey = { + id: WalletId; + address: string; +}; + export function TokenSelectorScreen(props: { client: ThirdwebClient; sourceTokens: SupportedTokens | undefined; sourceSupportedTokens: SupportedTokens | undefined; toChain: Chain; toToken: ERC20OrNativeToken; - fromToken: ERC20OrNativeToken; - fromChain: Chain; tokenAmount: string; mode: PayUIOptions["mode"]; hiddenWallets?: WalletId[]; - onSelect: (wallet: Wallet, token: TokenInfo, chain: Chain) => void; - onBack: () => void; + onSelectToken: (wallet: Wallet, token: TokenInfo, chain: Chain) => void; onConnect: () => void; - modalTitle?: string; - connectLocale: ConnectLocale; + onPayWithFiat: () => void; }) { const connectedWallets = useConnectedWallets(); const activeAccount = useActiveAccount(); const chainInfo = useChainMetadata(props.toChain); const theme = useCustomTheme(); - const locale = props.connectLocale.sendFundsScreen; const walletsAndBalances = useQuery({ queryKey: [ @@ -90,12 +88,16 @@ export function TokenSelectorScreen(props: { ], queryFn: async () => { // in parallel, get the balances of all the wallets on each of the sourceSupportedTokens - const walletBalanceMap = new Map(); + const walletBalanceMap = new Map(); const balancePromises = connectedWallets.flatMap((wallet) => { const account = wallet.getAccount(); if (!account) return []; - walletBalanceMap.set(wallet, []); + const walletKey: WalletKey = { + id: wallet.id, + address: account.address, + }; + walletBalanceMap.set(walletKey, []); // inject the destination token too since it can be used as well to pay/transfer const toToken = isNativeToken(props.toToken) @@ -139,7 +141,7 @@ export function TokenSelectorScreen(props: { : balance.value > 0n; if (shouldInclude) { - const existingBalances = walletBalanceMap.get(wallet) || []; + const existingBalances = walletBalanceMap.get(walletKey) || []; existingBalances.push({ balance, chain, token }); existingBalances.sort((a, b) => { if ( @@ -188,47 +190,44 @@ export function TokenSelectorScreen(props: { - - - - - - + {filteredWallets.length === 0 ? ( + + + No suitable payment token found +
+ in connected wallets +
+
+ ) : ( + + Select payment token + + + )} - - {filteredWallets.length === 0 && ( - - - - No suitable payment token found -
- in connected wallets -
-
-
- )} + {filteredWallets.map(([w, balances]) => { - const address = w.getAccount()?.address; - if (!address) return null; + const address = w.address; + const wallet = connectedWallets.find( + (w) => w.getAccount()?.address === address, + ); + if (!wallet) return null; return ( ); })} @@ -237,10 +236,8 @@ export function TokenSelectorScreen(props: { variant="secondary" fullWidth onClick={props.onConnect} - gap="xs" bg="tertiaryBg" style={{ - borderRadius: radius.md, border: `1px solid ${theme.colors.borderColor}`, padding: spacing.sm, }} @@ -254,7 +251,30 @@ export function TokenSelectorScreen(props: { > - Connect another wallet + Pay with another wallet + + + + @@ -272,28 +292,74 @@ function WalletRowWithBalances(props: { onClick: (wallet: Wallet, token: TokenInfo, chain: Chain) => void; hideConnectButton?: boolean; }) { + const theme = useCustomTheme(); const displayedBalances = props.balances; + const activeAccount = useActiveAccount(); + const { disconnect } = useDisconnect(); + const isActiveAccount = activeAccount?.address === props.address; return ( - - + + + {!isActiveAccount && ( + + )} - + {props.balances.length > 0 ? ( - displayedBalances.map((b) => ( + displayedBalances.map((b, idx) => ( props.onClick(props.wallet, b.token, b.chain)} key={`${b.token.address}-${b.chain.id}`} tokenBalance={b} wallet={props.wallet} + style={{ + borderTopLeftRadius: 0, + borderTopRightRadius: 0, + borderBottomRightRadius: + idx === displayedBalances.length - 1 ? radius.lg : 0, + borderBottomLeftRadius: + idx === displayedBalances.length - 1 ? radius.lg : 0, + borderBottom: + idx === displayedBalances.length - 1 + ? "none" + : `1px solid ${theme.colors.borderColor}`, + }} /> )) ) : ( - Not enough funds + Insufficient funds )} @@ -307,33 +373,35 @@ function TokenBalanceRow(props: { tokenBalance: TokenBalance; wallet: Wallet; onClick: (token: TokenInfo, wallet: Wallet) => void; + style?: React.CSSProperties; }) { - const { tokenBalance, wallet, onClick, client } = props; + const { tokenBalance, wallet, onClick, client, style } = props; const chainInfo = useChainName(tokenBalance.chain); return ( onClick(tokenBalance.token, wallet)} variant="secondary" + style={style} > - + - + {tokenBalance.token.symbol} {chainInfo && {chainInfo.name}} - + - {/* */} + ); } -export function WalletRow(props: { - client: ThirdwebClient; - address: string; - iconSize?: keyof typeof iconSize; - textSize?: keyof typeof fontSize; - walletId?: WalletId; - wallet?: Wallet; -}) { - const { client, address } = props; - const walletId = props.walletId; - const theme = useCustomTheme(); - const ensNameQuery = useEnsName({ - client, - address, - }); - const addressOrENS = ensNameQuery.data || shortenAddress(address); - const ensAvatarQuery = useEnsAvatar({ - client, - ensName: ensNameQuery.data, - }); - return ( - - - {ensAvatarQuery.data ? ( - - ) : walletId ? ( - - ) : ( - - - - )} - - - {addressOrENS || shortenAddress(props.address)} - - - - ); -} - -const StyledButton = /* @__PURE__ */ styled(Button)((_) => { +const StyledButton = /* @__PURE__ */ styled(Button)((props) => { const theme = useCustomTheme(); return { - background: theme.colors.tertiaryBg, + background: "transparent", justifyContent: "space-between", flexDirection: "row", padding: spacing.sm, - border: `1px solid ${theme.colors.borderColor}`, + paddingRight: spacing.xs, gap: spacing.sm, "&:hover": { background: theme.colors.secondaryButtonBg, transform: "scale(1.01)", }, transition: "background 200ms ease, transform 150ms ease", + ...props.style, }; }); diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/TransferConfirmationScreen.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/TransferConfirmationScreen.tsx index f54095afb59..1f40f2662eb 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/TransferConfirmationScreen.tsx +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/TransferConfirmationScreen.tsx @@ -16,20 +16,18 @@ import type { Address } from "../../../../../../../utils/address.js"; import { toWei } from "../../../../../../../utils/units.js"; import { iconSize } from "../../../../../../core/design-system/index.js"; import type { PayUIOptions } from "../../../../../../core/hooks/connection/ConnectButtonProps.js"; -import { useChainSymbol } from "../../../../../../core/hooks/others/useChainQuery.js"; import { Spacer } from "../../../../components/Spacer.js"; import { Spinner } from "../../../../components/Spinner.js"; import { StepBar } from "../../../../components/StepBar.js"; import { SwitchNetworkButton } from "../../../../components/SwitchNetwork.js"; -import { Container, Line, ModalHeader } from "../../../../components/basic.js"; +import { Container, ModalHeader } from "../../../../components/basic.js"; import { Button } from "../../../../components/buttons.js"; import { Text } from "../../../../components/text.js"; import { type ERC20OrNativeToken, isNativeToken } from "../../nativeToken.js"; import { Step } from "../Stepper.js"; -import { TokenInfoRow } from "../pay-transactions/TokenInfoRow.js"; import type { PayerInfo } from "../types.js"; import { ConnectorLine } from "./ConfirmationScreen.js"; -import { WalletRow } from "./TokenSelectorScreen.js"; +import { SwapSummary } from "./SwapSummary.js"; type TransferConfirmationScreenProps = { title: string; @@ -72,14 +70,13 @@ export function TransferConfirmationScreen( | { id: "error"; error: string } | { id: "done" } >({ id: "idle" }); - const { symbol } = useChainSymbol(chain); return ( - {transactionMode && ( + {transactionMode ? ( <> @@ -88,52 +85,25 @@ export function TransferConfirmationScreen( ? "Step 1 of 2 - Transfer funds" : "Step 2 of 2 - Finalize transaction"} - + + + ) : ( + <> + Confirm payment + )} - {/* Sender Address */} - - From - - - - - - - - {/* Receiver Address */} - - To - - - - - - - - {/* Token Info */} - diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/WalletRow.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/WalletRow.tsx new file mode 100644 index 00000000000..6aac9909188 --- /dev/null +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/WalletRow.tsx @@ -0,0 +1,70 @@ +import type { ThirdwebClient } from "../../../../../../../client/client.js"; +import { shortenAddress } from "../../../../../../../utils/address.js"; +import { isEcosystemWallet } from "../../../../../../../wallets/ecosystem/is-ecosystem-wallet.js"; +import { isSmartWallet } from "../../../../../../../wallets/smart/index.js"; +import { + fontSize, + iconSize, +} from "../../../../../../core/design-system/index.js"; +import { useConnectedWallets } from "../../../../../../core/hooks/wallets/useConnectedWallets.js"; +import { + useEnsName, + useWalletInfo, +} from "../../../../../../core/utils/wallet.js"; +import { useProfiles } from "../../../../../hooks/wallets/useProfiles.js"; +import { Skeleton } from "../../../../components/Skeleton.js"; +import { WalletImage } from "../../../../components/WalletImage.js"; +import { Container } from "../../../../components/basic.js"; +import { Text } from "../../../../components/text.js"; + +export function WalletRow(props: { + client: ThirdwebClient; + address: string; + iconSize?: keyof typeof iconSize; + textSize?: keyof typeof fontSize; +}) { + const { client, address } = props; + const connectedWallets = useConnectedWallets(); + const profile = useProfiles({ client }); + const wallet = connectedWallets.find( + (w) => w.getAccount()?.address?.toLowerCase() === address.toLowerCase(), + ); + const email = + wallet && + (wallet.id === "inApp" || + isEcosystemWallet(wallet) || + isSmartWallet(wallet)) + ? profile.data?.find((p) => !!p.details.email)?.details.email + : undefined; + const walletInfo = useWalletInfo(wallet?.id); + const ensNameQuery = useEnsName({ + client, + address, + }); + const addressOrENS = ensNameQuery.data || shortenAddress(address); + return ( + + + {wallet ? ( + + ) : null} + + + {addressOrENS || shortenAddress(props.address)} + + {profile.isLoading ? ( + + ) : email || walletInfo?.data?.name ? ( + + {email || walletInfo?.data?.name} + + ) : null} + + + + ); +} diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/TokenSelector.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/TokenSelector.tsx index d79f40babae..afc0ebf8293 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/TokenSelector.tsx +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/TokenSelector.tsx @@ -214,7 +214,6 @@ export function TokenSelector(props: { style={{ paddingTop: 0, paddingBottom: spacing.lg, - // maxHeight: props.chainSelection ? "300px" : "400px", }} > {!input && ( @@ -284,7 +283,6 @@ export function TokenSelector(props: { } function SelectTokenButton(props: { - // token?: TokenInfo; token: ERC20OrNativeToken; chain: Chain; onClick: () => void; @@ -310,7 +308,7 @@ function SelectTokenButton(props: { client={props.client} /> - + {tokenName ? ( {tokenName} diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/nativeToken.ts b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/nativeToken.ts index 087e5f86ea8..dc4b9295a12 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/nativeToken.ts +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/nativeToken.ts @@ -10,11 +10,13 @@ export const NATIVE_TOKEN: NativeToken = { nativeToken: true }; * @internal */ export function isNativeToken( - token: Partial | NativeToken, + token?: Partial | NativeToken, ): token is NativeToken { return ( - "nativeToken" in token || - token.address?.toLowerCase() === NATIVE_TOKEN_ADDRESS.toLowerCase() + (token && + ("nativeToken" in token || + token.address?.toLowerCase() === NATIVE_TOKEN_ADDRESS.toLowerCase())) || + false ); } diff --git a/packages/thirdweb/src/react/web/ui/components/token/TokenRow.tsx b/packages/thirdweb/src/react/web/ui/components/token/TokenRow.tsx index c96dba4dd44..131790627a7 100644 --- a/packages/thirdweb/src/react/web/ui/components/token/TokenRow.tsx +++ b/packages/thirdweb/src/react/web/ui/components/token/TokenRow.tsx @@ -10,17 +10,17 @@ import { spacing, } from "../../../../core/design-system/index.js"; import { useChainName } from "../../../../core/hooks/others/useChainQuery.js"; +import { PayTokenIcon } from "../../ConnectWallet/screens/Buy/PayTokenIcon.js"; import type { ERC20OrNativeToken } from "../../ConnectWallet/screens/nativeToken.js"; import { Skeleton } from "../Skeleton.js"; -import { TokenIcon } from "../TokenIcon.js"; import { Container } from "../basic.js"; import { Button } from "../buttons.js"; import { Text } from "../text.js"; import { TokenSymbol } from "./TokenSymbol.js"; export function TokenRow(props: { - token: ERC20OrNativeToken; - chain: Chain; + token: ERC20OrNativeToken | undefined; + chain: Chain | undefined; client: ThirdwebClient; onSelectToken: () => void; freezeChainAndToken?: boolean; @@ -29,6 +29,32 @@ export function TokenRow(props: { style?: React.CSSProperties; }) { const { name } = useChainName(props.chain); + + if (!props.token || !props.chain) { + return ( + + ); + } + return ( - - + {/* Token Symbol */} - + {props.isLoading ? ( @@ -92,9 +114,11 @@ export function TokenRow(props: { )} - - - + {!props.freezeChainAndToken && ( + + + + )} ); }