diff --git a/.changeset/two-pandas-knock.md b/.changeset/two-pandas-knock.md new file mode 100644 index 00000000000..3ee39edd5e4 --- /dev/null +++ b/.changeset/two-pandas-knock.md @@ -0,0 +1,5 @@ +--- +"thirdweb": patch +--- + +Add token details screen in token selection UI in SwapWidget, BridgeWidget diff --git a/packages/thirdweb/src/react/web/ui/Bridge/FundWallet.tsx b/packages/thirdweb/src/react/web/ui/Bridge/FundWallet.tsx index 91fe38d892c..a0490e05a6e 100644 --- a/packages/thirdweb/src/react/web/ui/Bridge/FundWallet.tsx +++ b/packages/thirdweb/src/react/web/ui/Bridge/FundWallet.tsx @@ -213,6 +213,7 @@ export function FundWallet(props: FundWalletProps) { > ) : ( - + {props.selectedToken?.data?.symbol} )} @@ -138,8 +146,6 @@ export function SelectedTokenButton(props: { size="xs" color="secondaryText" style={{ - overflow: "hidden", - textOverflow: "ellipsis", whiteSpace: "nowrap", }} > diff --git a/packages/thirdweb/src/react/web/ui/Bridge/swap-widget/hooks.ts b/packages/thirdweb/src/react/web/ui/Bridge/swap-widget/hooks.ts index 8278302e851..13a55b4a87e 100644 --- a/packages/thirdweb/src/react/web/ui/Bridge/swap-widget/hooks.ts +++ b/packages/thirdweb/src/react/web/ui/Bridge/swap-widget/hooks.ts @@ -1,9 +1,12 @@ +import { useQuery } from "@tanstack/react-query"; import { useMemo } from "react"; +import type { ThirdwebClient } from "../../../../../client/client.js"; +import { getToken } from "../../../../../pay/convert/get-token.js"; import type { Wallet } from "../../../../../wallets/interfaces/wallet.js"; import { useActiveAccount } from "../../../../core/hooks/wallets/useActiveAccount.js"; import { useActiveWallet } from "../../../../core/hooks/wallets/useActiveWallet.js"; import { useActiveWalletChain } from "../../../../core/hooks/wallets/useActiveWalletChain.js"; -import type { ActiveWalletInfo } from "./types.js"; +import type { ActiveWalletInfo, TokenSelection } from "./types.js"; export function useActiveWalletInfo( activeWalletOverride?: Wallet, @@ -25,3 +28,26 @@ export function useActiveWalletInfo( : undefined; }, [activeAccount, activeWallet, activeChain, activeWalletOverride]); } + +export function useTokenPrice(options: { + token: TokenSelection | undefined; + client: ThirdwebClient; +}) { + return useQuery({ + queryKey: ["token-price", options.token], + enabled: !!options.token, + queryFn: () => { + if (!options.token) { + throw new Error("Token is required"); + } + return getToken( + options.client, + options.token.tokenAddress, + options.token.chainId, + ); + }, + refetchOnMount: false, + retry: false, + refetchOnWindowFocus: false, + }); +} diff --git a/packages/thirdweb/src/react/web/ui/Bridge/swap-widget/select-token-ui.tsx b/packages/thirdweb/src/react/web/ui/Bridge/swap-widget/select-token-ui.tsx index ef4673d256f..f1bc0d67e00 100644 --- a/packages/thirdweb/src/react/web/ui/Bridge/swap-widget/select-token-ui.tsx +++ b/packages/thirdweb/src/react/web/ui/Bridge/swap-widget/select-token-ui.tsx @@ -1,7 +1,11 @@ +import { PlusIcon } from "@radix-ui/react-icons"; import { useCallback, useMemo, useState } from "react"; import type { Token } from "../../../../../bridge/index.js"; import type { BridgeChain } from "../../../../../bridge/types/Chain.js"; import type { ThirdwebClient } from "../../../../../client/client.js"; +import { isNativeTokenAddress } from "../../../../../constants/addresses.js"; +import type { SupportedFiatCurrency } from "../../../../../pay/convert/type.js"; +import { shortenAddress } from "../../../../../utils/address.js"; import { toTokens } from "../../../../../utils/units.js"; import { useCustomTheme } from "../../../../core/design-system/CustomThemeProvider.js"; import { @@ -11,17 +15,26 @@ import { spacing, } from "../../../../core/design-system/index.js"; import { CoinsIcon } from "../../ConnectWallet/icons/CoinsIcon.js"; +import { InfoIcon } from "../../ConnectWallet/icons/InfoIcon.js"; import { WalletDotIcon } from "../../ConnectWallet/icons/WalletDotIcon.js"; -import { Container, noScrollBar } from "../../components/basic.js"; -import { Button } from "../../components/buttons.js"; +import { formatCurrencyAmount } from "../../ConnectWallet/screens/formatTokenBalance.js"; +import { + Container, + Line, + ModalHeader, + noScrollBar, +} from "../../components/basic.js"; +import { Button, IconButton } from "../../components/buttons.js"; +import { CopyIcon } from "../../components/CopyIcon.js"; import { Img } from "../../components/Img.js"; import { Skeleton } from "../../components/Skeleton.js"; import { Spacer } from "../../components/Spacer.js"; import { Spinner } from "../../components/Spinner.js"; -import { Text } from "../../components/text.js"; +import { Link, Text } from "../../components/text.js"; import { StyledDiv } from "../../design-system/elements.js"; import { useDebouncedValue } from "../../hooks/useDebouncedValue.js"; import { useIsMobile } from "../../hooks/useisMobile.js"; +import { useTokenPrice } from "./hooks.js"; import { SearchInput } from "./SearchInput.js"; import { SelectChainButton } from "./SelectChainButton.js"; import { SelectBridgeChain } from "./select-chain.js"; @@ -48,6 +61,7 @@ type SelectTokenUIProps = { buyChainId: number | undefined; sellChainId: number | undefined; }; + currency: SupportedFiatCurrency; }; function findChain(chains: BridgeChain[], activeChainId: number | undefined) { @@ -238,6 +252,7 @@ function SelectTokenUI( props.onClose(); }} isMobile={false} + key={props.selectedChain?.chainId} selectedToken={props.selectedToken} isFetching={props.isFetching} ownedTokens={props.ownedTokens} @@ -248,6 +263,7 @@ function SelectTokenUI( client={props.client} search={props.search} setSearch={props.setSearch} + currency={props.currency} /> @@ -257,6 +273,7 @@ function SelectTokenUI( if (screen === "select-token") { return ( { props.setSelectedToken(token); props.onClose(); @@ -272,6 +289,7 @@ function SelectTokenUI( client={props.client} search={props.search} setSearch={props.setSearch} + currency={props.currency} /> ); } @@ -320,6 +338,7 @@ function TokenButton(props: { token: TokenBalance | Token; client: ThirdwebClient; onSelect: (tokenWithPrices: TokenSelection) => void; + onInfoClick: (tokenAddress: string, chainId: number) => void; isSelected: boolean; }) { const theme = useCustomTheme(); @@ -332,6 +351,11 @@ function TokenButton(props: { ? props.token.price_data.price_usd * Number(tokenBalanceInUnits) : undefined; + const tokenAddress = + "balance" in props.token ? props.token.token_address : props.token.address; + const chainId = + "balance" in props.token ? props.token.chain_id : props.token.chainId; + return ( ); } +function TokenInfoScreen(props: { + tokenAddress: string; + chainId: number; + client: ThirdwebClient; + onBack: () => void; + currency: SupportedFiatCurrency; +}) { + const theme = useCustomTheme(); + const tokenQuery = useTokenPrice({ + token: { + tokenAddress: props.tokenAddress, + chainId: props.chainId, + }, + client: props.client, + }); + const token = tokenQuery.data; + + if (tokenQuery.isPending) { + return ( + + + + ); + } + + if (!token) { + return ( + + + Token not found + + + ); + } + + const isNativeToken = isNativeTokenAddress(props.tokenAddress); + const explorerLink = isNativeToken + ? `https://thirdweb.com/${props.chainId}` + : `https://thirdweb.com/${props.chainId}/${props.tokenAddress}`; + + return ( + + {/* Header */} + + + + + + {/* Content */} + + {/* name + icon */} + + + Name + + + + + } + /> + + {token.name} + + + + + {/* symbol */} + + + {/* price */} + {"prices" in token && ( + + )} + + {/* market cap */} + {!!token.marketCapUsd && ( + + )} + + {/* volume 24h */} + {!!token.volume24hUsd && ( + + )} + + {/* address + link */} + + + Contract Address + + + + {!isNativeToken && ( + + )} + + + {isNativeToken + ? "Native Currency" + : shortenAddress(props.tokenAddress)} + + + + + + ); +} + +function TokenInfoRow(props: { label: string; value: string }) { + return ( + + + {props.label} + + + {props.value} + + + ); +} + function TokenSelectionScreen(props: { selectedChain: BridgeChain | undefined; isMobile: boolean; @@ -461,12 +715,30 @@ function TokenSelectionScreen(props: { showMore: (() => void) | undefined; selectedToken: TokenSelection | undefined; onSelectToken: (token: TokenSelection) => void; + currency: SupportedFiatCurrency; }) { + const [tokenInfoScreen, setTokenInfoScreen] = useState<{ + tokenAddress: string; + chainId: number; + } | null>(null); + const noTokensFound = !props.isFetching && props.otherTokens.length === 0 && props.ownedTokens.length === 0; + if (tokenInfoScreen) { + return ( + setTokenInfoScreen(null)} + currency={props.currency} + /> + ); + } + return ( @@ -495,7 +767,7 @@ function TokenSelectionScreen(props: { minHeight: "300px", }} > - + )} @@ -574,6 +846,9 @@ function TokenSelectionScreen(props: { token={token} client={props.client} onSelect={props.onSelectToken} + onInfoClick={(tokenAddress, chainId) => + setTokenInfoScreen({ tokenAddress, chainId }) + } isSelected={ !!props.selectedToken && props.selectedToken.tokenAddress.toLowerCase() === @@ -615,6 +890,9 @@ function TokenSelectionScreen(props: { token={token} client={props.client} onSelect={props.onSelectToken} + onInfoClick={(tokenAddress, chainId) => + setTokenInfoScreen({ tokenAddress, chainId }) + } isSelected={ !!props.selectedToken && props.selectedToken.tokenAddress.toLowerCase() === @@ -626,12 +904,17 @@ function TokenSelectionScreen(props: { {props.showMore && ( )} diff --git a/packages/thirdweb/src/react/web/ui/Bridge/swap-widget/swap-ui.tsx b/packages/thirdweb/src/react/web/ui/Bridge/swap-widget/swap-ui.tsx index 42abe822b8b..7fe62fd7c79 100644 --- a/packages/thirdweb/src/react/web/ui/Bridge/swap-widget/swap-ui.tsx +++ b/packages/thirdweb/src/react/web/ui/Bridge/swap-widget/swap-ui.tsx @@ -7,7 +7,6 @@ import type { prepare as SellPrepare } from "../../../../../bridge/Sell.js"; import type { TokenWithPrices } from "../../../../../bridge/types/Token.js"; import type { ThirdwebClient } from "../../../../../client/client.js"; import { NATIVE_TOKEN_ADDRESS } from "../../../../../constants/addresses.js"; -import { getToken } from "../../../../../pay/convert/get-token.js"; import type { SupportedFiatCurrency } from "../../../../../pay/convert/type.js"; import { getAddress } from "../../../../../utils/address.js"; import { toTokens, toUnits } from "../../../../../utils/units.js"; @@ -43,6 +42,7 @@ import { ActiveWalletDetails } from "../common/active-wallet-details.js"; import { DecimalInput } from "../common/decimal-input.js"; import { SelectedTokenButton } from "../common/selected-token-button.js"; import { useTokenBalance } from "../common/token-balance.js"; +import { useTokenPrice } from "./hooks.js"; import { SelectToken } from "./select-token-ui.js"; import type { ActiveWalletInfo, @@ -82,29 +82,6 @@ type SwapUIProps = { onDisconnect: (() => void) | undefined; }; -function useTokenPrice(options: { - token: TokenSelection | undefined; - client: ThirdwebClient; -}) { - return useQuery({ - queryKey: ["token-price", options.token], - enabled: !!options.token, - queryFn: () => { - if (!options.token) { - throw new Error("Token is required"); - } - return getToken( - options.client, - options.token.tokenAddress, - options.token.chainId, - ); - }, - refetchOnMount: false, - retry: false, - refetchOnWindowFocus: false, - }); -} - /** * @internal */ @@ -244,6 +221,7 @@ export function SwapUI(props: SwapUIProps) { }} client={props.client} selectedToken={props.buyToken} + currency={props.currency} setSelectedToken={(token) => { props.setBuyToken(token); // if buy token is same as sell token, unset sell token @@ -282,6 +260,7 @@ export function SwapUI(props: SwapUIProps) { }} client={props.client} selectedToken={props.sellToken} + currency={props.currency} setSelectedToken={(token) => { props.setSellToken(token); // if sell token is same as buy token, unset buy token diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/icons/InfoIcon.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/icons/InfoIcon.tsx new file mode 100644 index 00000000000..7df5e56edce --- /dev/null +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/icons/InfoIcon.tsx @@ -0,0 +1,23 @@ +import type { IconFC } from "./types.js"; + +export const InfoIcon: IconFC = (props) => { + return ( + + + + + + ); +}; diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/formatTokenBalance.ts b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/formatTokenBalance.ts index 2934f0f5611..7b2e67deb5b 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/formatTokenBalance.ts +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/formatTokenBalance.ts @@ -46,10 +46,20 @@ function formatMoney( locale: string, currencyCode: string, ): string { + if (value < 0) { + return new Intl.NumberFormat(locale, { + style: "currency", + currency: currencyCode, + maximumFractionDigits: 6, + minimumFractionDigits: 0, + }).format(value); + } + return new Intl.NumberFormat(locale, { style: "currency", currency: currencyCode, maximumFractionDigits: 2, minimumFractionDigits: 0, + notation: "compact", }).format(value); }