diff --git a/.changeset/nine-otters-pay.md b/.changeset/nine-otters-pay.md new file mode 100644 index 00000000000..51129b1f58e --- /dev/null +++ b/.changeset/nine-otters-pay.md @@ -0,0 +1,5 @@ +--- +"thirdweb": patch +--- + +SwapWidget UI improvements diff --git a/apps/dashboard/src/@/components/blocks/BuyAndSwapEmbed.tsx b/apps/dashboard/src/@/components/blocks/BuyAndSwapEmbed.tsx index 73ccf6ae774..f2dc10e9c8a 100644 --- a/apps/dashboard/src/@/components/blocks/BuyAndSwapEmbed.tsx +++ b/apps/dashboard/src/@/components/blocks/BuyAndSwapEmbed.tsx @@ -107,12 +107,13 @@ export function BuyAndSwapEmbed(props: { theme={themeObj} className="!rounded-2xl !border-none !w-full" prefill={{ - sellToken: { + // buy this token by default + buyToken: { chainId: props.chain.id, tokenAddress: props.tokenAddress, }, - // only set `buyToken` as "Native token" if `sellToken` is not a "native token" already - buyToken: props.tokenAddress + // sell the native token by default (but if buytoken is a native token, don't set) + sellToken: props.tokenAddress ? { chainId: props.chain.id, } diff --git a/apps/dashboard/src/@/utils/sdk-component-theme.ts b/apps/dashboard/src/@/utils/sdk-component-theme.ts index 005818d2ac6..5541d2e530b 100644 --- a/apps/dashboard/src/@/utils/sdk-component-theme.ts +++ b/apps/dashboard/src/@/utils/sdk-component-theme.ts @@ -31,9 +31,9 @@ export function getSDKTheme(theme: "light" | "dark"): Theme { selectedTextBg: "hsl(var(--inverted))", selectedTextColor: "hsl(var(--inverted-foreground))", separatorLine: "hsl(var(--border))", - skeletonBg: "hsl(var(--muted))", + skeletonBg: "hsl(var(--secondary-foreground)/15%)", success: "hsl(var(--success-text))", - tertiaryBg: "hsl(var(--muted)/50%)", + tertiaryBg: "hsl(var(--muted)/30%)", tooltipBg: "hsl(var(--popover))", tooltipText: "hsl(var(--popover-foreground))", }, diff --git a/apps/playground-web/src/app/bridge/swap-widget/components/right-section.tsx b/apps/playground-web/src/app/bridge/swap-widget/components/right-section.tsx index 97431df203b..6441d31e26d 100644 --- a/apps/playground-web/src/app/bridge/swap-widget/components/right-section.tsx +++ b/apps/playground-web/src/app/bridge/swap-widget/components/right-section.tsx @@ -59,7 +59,9 @@ export function RightSection(props: { options: SwapWidgetPlaygroundOptions }) { prefill={props.options.prefill} currency={props.options.currency} showThirdwebBranding={props.options.showThirdwebBranding} - key={JSON.stringify(props.options)} + key={JSON.stringify({ + prefill: props.options.prefill, + })} persistTokenSelections={false} /> )} diff --git a/apps/portal/src/app/bridge/swap/page.mdx b/apps/portal/src/app/bridge/swap/page.mdx index 41ee17c68ce..8d5afd6bbc1 100644 --- a/apps/portal/src/app/bridge/swap/page.mdx +++ b/apps/portal/src/app/bridge/swap/page.mdx @@ -13,6 +13,7 @@ import { TypeScriptIcon, } from "@/icons"; import SwapWidgetImage from "./swap-dark.png"; +import SwapWidgetImageLight from "./swap-light.png"; export const metadata = createMetadata({ image: { @@ -55,7 +56,12 @@ function Example() { } ``` +
+
+
+ +
## Live Playground diff --git a/apps/portal/src/app/bridge/swap/swap-dark.png b/apps/portal/src/app/bridge/swap/swap-dark.png index 48ca746d6f8..267571d55b9 100644 Binary files a/apps/portal/src/app/bridge/swap/swap-dark.png and b/apps/portal/src/app/bridge/swap/swap-dark.png differ diff --git a/apps/portal/src/app/bridge/swap/swap-light.png b/apps/portal/src/app/bridge/swap/swap-light.png new file mode 100644 index 00000000000..26ceef282d0 Binary files /dev/null and b/apps/portal/src/app/bridge/swap/swap-light.png differ diff --git a/packages/thirdweb/src/react/core/design-system/index.ts b/packages/thirdweb/src/react/core/design-system/index.ts index 023270dde16..64ca0076207 100644 --- a/packages/thirdweb/src/react/core/design-system/index.ts +++ b/packages/thirdweb/src/react/core/design-system/index.ts @@ -200,6 +200,7 @@ export const iconSize = { "4xl": "128", lg: "32", md: "24", + "sm+": "20", sm: "16", xl: "48", xs: "12", diff --git a/packages/thirdweb/src/react/web/ui/Bridge/swap-widget/SearchInput.tsx b/packages/thirdweb/src/react/web/ui/Bridge/swap-widget/SearchInput.tsx index 969fdade1b6..482f45ef6bf 100644 --- a/packages/thirdweb/src/react/web/ui/Bridge/swap-widget/SearchInput.tsx +++ b/packages/thirdweb/src/react/web/ui/Bridge/swap-widget/SearchInput.tsx @@ -36,6 +36,7 @@ export function SearchInput(props: { variant="outline" placeholder={props.placeholder} value={props.value} + sm style={{ paddingLeft: "44px", }} diff --git a/packages/thirdweb/src/react/web/ui/Bridge/swap-widget/SwapWidget.tsx b/packages/thirdweb/src/react/web/ui/Bridge/swap-widget/SwapWidget.tsx index caaa4c036c9..226b83a339a 100644 --- a/packages/thirdweb/src/react/web/ui/Bridge/swap-widget/SwapWidget.tsx +++ b/packages/thirdweb/src/react/web/ui/Bridge/swap-widget/SwapWidget.tsx @@ -162,6 +162,10 @@ export type SwapWidgetProps = { * @default true */ persistTokenSelections?: boolean; + /** + * Called when the user disconnects the active wallet + */ + onDisconnect?: () => void; }; /** @@ -325,46 +329,11 @@ function SwapWidgetContent(props: SwapWidgetProps) { }); const [buyToken, setBuyToken] = useState(() => { - if (props.prefill?.buyToken) { - return { - tokenAddress: - props.prefill.buyToken.tokenAddress || - getAddress(NATIVE_TOKEN_ADDRESS), - chainId: props.prefill.buyToken.chainId, - }; - } - - if (!isPersistEnabled) { - return undefined; - } - - const lastUsedBuyToken = getLastUsedTokens()?.buyToken; - - // the token that will be set as initial value of sell token - const sellToken = getInitialSellToken( - props.prefill, - getLastUsedTokens()?.sellToken, - ); - - // if both tokens are same, ignore "buyToken", keep "sellToken" - if ( - lastUsedBuyToken && - sellToken && - lastUsedBuyToken.tokenAddress.toLowerCase() === - sellToken.tokenAddress.toLowerCase() && - lastUsedBuyToken.chainId === sellToken.chainId - ) { - return undefined; - } - - return lastUsedBuyToken; + return getInitialTokens(props.prefill, isPersistEnabled).buyToken; }); const [sellToken, setSellToken] = useState(() => { - return getInitialSellToken( - props.prefill, - isPersistEnabled ? getLastUsedTokens()?.sellToken : undefined, - ); + return getInitialTokens(props.prefill, isPersistEnabled).sellToken; }); // persist selections to localStorage whenever they change @@ -394,6 +363,7 @@ function SwapWidgetContent(props: SwapWidgetProps) { if (screen.id === "1:swap-ui" || !activeWalletInfo) { return ( 1000) { - return ( - - {compactFormatter.format(Number(props.value))} - - ); - } - const [integerPart, fractionPart] = props.value.split("."); - - return ( -
- - {integerPart} - - - .{fractionPart || "00"} - -
- ); -} - -const compactFormatter = new Intl.NumberFormat("en-US", { - notation: "compact", - maximumFractionDigits: 2, -}); diff --git a/packages/thirdweb/src/react/web/ui/Bridge/swap-widget/select-chain.tsx b/packages/thirdweb/src/react/web/ui/Bridge/swap-widget/select-chain.tsx index 270b5e8e927..04f0f8909a5 100644 --- a/packages/thirdweb/src/react/web/ui/Bridge/swap-widget/select-chain.tsx +++ b/packages/thirdweb/src/react/web/ui/Bridge/swap-widget/select-chain.tsx @@ -4,6 +4,7 @@ import type { ThirdwebClient } from "../../../../../client/client.js"; import { fontSize, iconSize, + radius, spacing, } from "../../../../core/design-system/index.js"; import { Container, Line, ModalHeader } from "../../components/basic.js"; @@ -21,6 +22,7 @@ type SelectBuyTokenProps = { client: ThirdwebClient; onSelectChain: (chain: BridgeChain) => void; selectedChain: BridgeChain | undefined; + isMobile: boolean; }; /** @@ -56,11 +58,15 @@ export function SelectBridgeChainUI( }); return ( -
- - - - + + {props.isMobile && ( + <> + + + + + + )} @@ -79,10 +85,12 @@ export function SelectBridgeChainUI( props.onSelectChain(chain)} isSelected={chain.chainId === props.selectedChain?.chainId} + isMobile={props.isMobile} /> ))} @@ -119,7 +128,7 @@ export function SelectBridgeChainUI(
)} - + ); } @@ -144,6 +153,7 @@ function ChainButton(props: { client: ThirdwebClient; onClick: () => void; isSelected: boolean; + isMobile: boolean; }) { return ( 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 a96362495c8..e943d9af534 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,9 +1,9 @@ -import { DiscIcon } from "@radix-ui/react-icons"; import { 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 { toTokens } from "../../../../../utils/units.js"; +import { useCustomTheme } from "../../../../core/design-system/CustomThemeProvider.js"; import { fontSize, iconSize, @@ -12,15 +12,15 @@ import { } from "../../../../core/design-system/index.js"; import { CoinsIcon } from "../../ConnectWallet/icons/CoinsIcon.js"; import { WalletDotIcon } from "../../ConnectWallet/icons/WalletDotIcon.js"; -import { formatTokenAmount } from "../../ConnectWallet/screens/formatTokenBalance.js"; -import { Container, Line, ModalHeader } from "../../components/basic.js"; +import { Container, noScrollBar } from "../../components/basic.js"; import { Button } from "../../components/buttons.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 { DecimalRenderer } from "./common.js"; +import { StyledDiv } from "../../design-system/elements.js"; +import { useIsMobile } from "../../hooks/useisMobile.js"; import { SearchInput } from "./SearchInput.js"; import { SelectChainButton } from "./SelectChainButton.js"; import { SelectBridgeChain } from "./select-chain.js"; @@ -31,6 +31,7 @@ import { useTokenBalances, useTokens, } from "./use-tokens.js"; +import { tokenAmountFormatter } from "./utils.js"; /** * @internal @@ -138,6 +139,7 @@ function SelectTokenUI( showMore: (() => void) | undefined; }, ) { + const isMobile = useIsMobile(); const [screen, setScreen] = useState<"select-chain" | "select-token">( "select-token", ); @@ -180,184 +182,70 @@ function SelectTokenUI( }); }, [otherTokens]); - const noTokensFound = - !props.isFetching && - sortedOtherTokens.length === 0 && - props.ownedTokens.length === 0; - - if (screen === "select-token") { + if (!isMobile) { return ( - - - - - - - {!props.selectedChain && ( -
+ + setScreen("select-token")} + client={props.client} + isMobile={false} + onSelectChain={(chain) => { + props.setSelectedChain(chain); + setScreen("select-token"); }} - > - -
- )} - - {props.selectedChain && ( - <> - - setScreen("select-chain")} - selectedChain={props.selectedChain} - client={props.client} - /> - - - {/* search */} - - - - - - - - {props.isFetching && - new Array(20).fill(0).map((_, i) => ( - // biome-ignore lint/suspicious/noArrayIndexKey: ok - - ))} - - {!props.isFetching && sortedOwnedTokens.length > 0 && ( - - - - Your Tokens - - - )} - - {!props.isFetching && - sortedOwnedTokens.map((token) => ( - - ))} - - {!props.isFetching && sortedOwnedTokens.length > 0 && ( - - - - Other Tokens - - - )} - - {!props.isFetching && - sortedOtherTokens.map((token) => ( - - ))} - - {props.showMore && ( - - )} - - {noTokensFound && ( -
- - No Tokens Found - -
- )} -
-
- - )} + selectedChain={props.selectedChain} + /> + + + setScreen("select-chain")} + client={props.client} + search={props.search} + setSearch={props.setSearch} + /> +
); } + if (screen === "select-token") { + return ( + setScreen("select-chain")} + client={props.client} + search={props.search} + setSearch={props.setSearch} + /> + ); + } + if (screen === "select-chain") { return ( setScreen("select-token")} client={props.client} onSelectChain={(chain) => { @@ -379,7 +267,7 @@ function TokenButtonSkeleton() { display: "flex", alignItems: "center", gap: spacing.sm, - padding: `${spacing.sm} ${spacing.sm}`, + padding: `${spacing.xs} ${spacing.xs}`, height: "70px", }} > @@ -398,6 +286,7 @@ function TokenButton(props: { onSelect: (tokenWithPrices: TokenSelection) => void; isSelected: boolean; }) { + const theme = useCustomTheme(); const tokenBalanceInUnits = "balance" in props.token ? toTokens(BigInt(props.token.balance), props.token.decimals) @@ -416,7 +305,7 @@ function TokenButton(props: { fontWeight: 500, fontSize: fontSize.md, border: "1px solid transparent", - padding: `${spacing.sm} ${spacing.xs}`, + padding: `${spacing.xs} ${spacing.xs}`, textAlign: "left", lineHeight: "1.5", borderRadius: radius.lg, @@ -451,7 +340,14 @@ function TokenButton(props: { }} fallback={ - + } /> @@ -473,18 +369,15 @@ function TokenButton(props: { {props.token.symbol} + {"balance" in props.token && ( - + {tokenAmountFormatter.format( + Number( + toTokens(BigInt(props.token.balance), props.token.decimals), + ), )} - color="primaryText" - weight={500} - /> + )} {usdValue && ( - - $ + + ${usdValue.toFixed(2)} - )} @@ -526,3 +411,223 @@ function TokenButton(props: { ); } + +function TokenSelectionScreen(props: { + selectedChain: BridgeChain | undefined; + isMobile: boolean; + onSelectChain: () => void; + client: ThirdwebClient; + search: string; + setSearch: (search: string) => void; + isFetching: boolean; + ownedTokens: TokenBalance[]; + otherTokens: Token[]; + showMore: (() => void) | undefined; + selectedToken: TokenSelection | undefined; + onSelectToken: (token: TokenSelection) => void; +}) { + const noTokensFound = + !props.isFetching && + props.otherTokens.length === 0 && + props.ownedTokens.length === 0; + + return ( + + + + Select Token + + + + Select a token from the list or search for a token by symbol or + address + + + + {!props.selectedChain && ( +
+ +
+ )} + + {props.selectedChain && ( + <> + {props.isMobile ? ( + + + + ) : ( + + )} + + {/* search */} + + + + + + + + {props.isFetching && + new Array(20).fill(0).map((_, i) => ( + // biome-ignore lint/suspicious/noArrayIndexKey: ok + + ))} + + {!props.isFetching && props.ownedTokens.length > 0 && ( + + + + Your Tokens + + + )} + + {!props.isFetching && + props.ownedTokens.map((token) => ( + + ))} + + {!props.isFetching && props.ownedTokens.length > 0 && ( + + + + Other Tokens + + + )} + + {!props.isFetching && + props.otherTokens.map((token) => ( + + ))} + + {props.showMore && ( + + )} + + {noTokensFound && ( +
+ + No Tokens Found + +
+ )} +
+ + )} +
+ ); +} + +const LeftContainer = /* @__PURE__ */ StyledDiv((_) => { + const theme = useCustomTheme(); + return { + display: "flex", + flexDirection: "column", + overflowY: "auto", + ...noScrollBar, + borderRight: `1px solid ${theme.colors.separatorLine}`, + position: "relative", + }; +}); 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 9cf75e982b5..abf487621a1 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 @@ -1,9 +1,5 @@ import styled from "@emotion/styled"; -import { - ChevronDownIcon, - ChevronRightIcon, - DiscIcon, -} from "@radix-ui/react-icons"; +import { ChevronDownIcon } from "@radix-ui/react-icons"; import { useQuery } from "@tanstack/react-query"; import { useState } from "react"; import type { prepare as BuyPrepare } from "../../../../../bridge/Buy.js"; @@ -15,12 +11,10 @@ import { defineChain } from "../../../../../chains/utils.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 { - getFiatSymbol, - type SupportedFiatCurrency, -} from "../../../../../pay/convert/type.js"; -import { getAddress } from "../../../../../utils/address.js"; +import type { SupportedFiatCurrency } from "../../../../../pay/convert/type.js"; +import { getAddress, shortenAddress } from "../../../../../utils/address.js"; import { toTokens, toUnits } from "../../../../../utils/units.js"; +import { AccountProvider } from "../../../../core/account/provider.js"; import { useCustomTheme } from "../../../../core/design-system/CustomThemeProvider.js"; import { fontSize, @@ -31,11 +25,16 @@ import { } from "../../../../core/design-system/index.js"; import { useWalletBalance } from "../../../../core/hooks/others/useWalletBalance.js"; import type { BridgePrepareRequest } from "../../../../core/hooks/useBridgePrepare.js"; +import { WalletProvider } from "../../../../core/wallet/provider.js"; import { ConnectButton } from "../../ConnectWallet/ConnectButton.js"; +import { DetailsModal } from "../../ConnectWallet/Details.js"; import { ArrowUpDownIcon } from "../../ConnectWallet/icons/ArrowUpDownIcon.js"; -import { WalletDotIcon } from "../../ConnectWallet/icons/WalletDotIcon.js"; +import connectLocaleEn from "../../ConnectWallet/locale/en.js"; import { PoweredByThirdweb } from "../../ConnectWallet/PoweredByTW.js"; -import { formatTokenAmount } from "../../ConnectWallet/screens/formatTokenBalance.js"; +import { + formatCurrencyAmount, + formatTokenAmount, +} from "../../ConnectWallet/screens/formatTokenBalance.js"; import { Container } from "../../components/basic.js"; import { Button } from "../../components/buttons.js"; import { Input } from "../../components/formElements.js"; @@ -43,8 +42,13 @@ import { Img } from "../../components/Img.js"; import { Modal } from "../../components/Modal.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 { DecimalRenderer } from "./common.js"; +import { useIsMobile } from "../../hooks/useisMobile.js"; +import { AccountAvatar } from "../../prebuilt/Account/avatar.js"; +import { AccountBlobbie } from "../../prebuilt/Account/blobbie.js"; +import { AccountName } from "../../prebuilt/Account/name.js"; +import { WalletIcon } from "../../prebuilt/Wallet/icon.js"; import { SelectToken } from "./select-token-ui.js"; import type { ActiveWalletInfo, @@ -82,6 +86,7 @@ type SwapUIProps = { type: "buy" | "sell"; amount: string; }) => void; + onDisconnect: (() => void) | undefined; }; function useTokenPrice(options: { @@ -110,9 +115,16 @@ function useTokenPrice(options: { * @internal */ export function SwapUI(props: SwapUIProps) { - const [modalState, setModalState] = useState< - "select-buy-token" | "select-sell-token" | undefined - >(undefined); + const [modalState, setModalState] = useState<{ + screen: "select-buy-token" | "select-sell-token"; + isOpen: boolean; + }>({ + screen: "select-buy-token", + isOpen: false, + }); + + const [detailsModalOpen, setDetailsModalOpen] = useState(false); + const isMobile = useIsMobile(); // Token Prices ---------------------------------------------------------------------------- const buyTokenQuery = useTokenPrice({ @@ -193,14 +205,19 @@ export function SwapUI(props: SwapUIProps) { ); // ---------------------------------------------------------------------------- + const disableContinue = + !preparedResultQuery.data || + preparedResultQuery.isFetching || + notEnoughBalance; return ( { if (!v) { - setModalState(undefined); + setModalState((v) => ({ + ...v, + isOpen: false, + })); } }} > - {modalState === "select-buy-token" && ( + {modalState.screen === "select-buy-token" && ( setModalState(undefined)} + onBack={() => + setModalState((v) => ({ + ...v, + isOpen: false, + })) + } client={props.client} selectedToken={props.buyToken} setSelectedToken={(token) => { props.setBuyToken(token); - setModalState(undefined); + setModalState((v) => ({ + ...v, + isOpen: false, + })); // if buy token is same as sell token, unset sell token if ( props.sellToken && @@ -234,14 +262,22 @@ export function SwapUI(props: SwapUIProps) { /> )} - {modalState === "select-sell-token" && ( + {modalState.screen === "select-sell-token" && ( setModalState(undefined)} + onBack={() => + setModalState((v) => ({ + ...v, + isOpen: false, + })) + } client={props.client} selectedToken={props.sellToken} setSelectedToken={(token) => { props.setSellToken(token); - setModalState(undefined); + setModalState((v) => ({ + ...v, + isOpen: false, + })); // if sell token is same as buy token, unset buy token if ( props.buyToken && @@ -257,10 +293,35 @@ export function SwapUI(props: SwapUIProps) { )} + {detailsModalOpen && ( + { + setDetailsModalOpen(false); + }} + onDisconnect={() => { + props.onDisconnect?.(); + }} + chains={[]} + connectOptions={props.connectOptions} + /> + )} + {/* Sell */} { + if (sellTokenBalanceQuery.data) { + props.setAmountSelection({ + type: "sell", + amount: sellTokenBalanceQuery.data.displayValue, + }); + } + }} + activeWalletInfo={props.activeWalletInfo} isConnected={!!props.activeWalletInfo} - notEnoughBalance={notEnoughBalance} balance={{ data: sellTokenBalanceQuery.data?.value, isFetching: sellTokenBalanceQuery.isFetching, @@ -269,7 +330,7 @@ export function SwapUI(props: SwapUIProps) { data: sellTokenAmount, isFetching: isSellAmountFetching, }} - label="Sell" + type="sell" setAmount={(value) => { props.setAmountSelection({ type: "sell", amount: value }); }} @@ -283,7 +344,15 @@ export function SwapUI(props: SwapUIProps) { } client={props.client} currency={props.currency} - onSelectToken={() => setModalState("select-sell-token")} + onSelectToken={() => + setModalState({ + screen: "select-sell-token", + isOpen: true, + }) + } + onWalletClick={() => { + setDetailsModalOpen(true); + }} /> {/* Switch */} @@ -302,8 +371,12 @@ export function SwapUI(props: SwapUIProps) { {/* Buy */} { + setDetailsModalOpen(true); + }} + activeWalletInfo={props.activeWalletInfo} isConnected={!!props.activeWalletInfo} - notEnoughBalance={false} balance={{ data: buyTokenBalanceQuery.data?.value, isFetching: buyTokenBalanceQuery.isFetching, @@ -312,7 +385,7 @@ export function SwapUI(props: SwapUIProps) { data: buyTokenAmount, isFetching: isBuyAmountFetching, }} - label="Buy" + type="buy" selectedToken={ props.buyToken ? { @@ -326,7 +399,12 @@ export function SwapUI(props: SwapUIProps) { }} client={props.client} currency={props.currency} - onSelectToken={() => setModalState("select-buy-token")} + onSelectToken={() => + setModalState({ + screen: "select-buy-token", + isOpen: true, + }) + } /> {/* error message */} @@ -342,7 +420,7 @@ export function SwapUI(props: SwapUIProps) { Failed to get a quote ) : ( - + )} {/* Button */} @@ -361,11 +439,7 @@ export function SwapUI(props: SwapUIProps) { /> ) : ( )} @@ -599,10 +679,11 @@ function DecimalInput(props: { style={{ border: "none", boxShadow: "none", - fontSize: fontSize.xxl, + fontSize: fontSize.xl, fontWeight: 500, paddingInline: 0, paddingBlock: 0, + letterSpacing: "-0.025em", }} type="text" value={props.value} @@ -612,13 +693,13 @@ function DecimalInput(props: { } function TokenSection(props: { - label: string; - notEnoughBalance: boolean; + type: "buy" | "sell"; amount: { data: string; isFetching: boolean; }; setAmount: (amount: string) => void; + activeWalletInfo: ActiveWalletInfo | undefined; selectedToken: | { data: TokenWithPrices | undefined; @@ -633,7 +714,10 @@ function TokenSection(props: { data: bigint | undefined; isFetching: boolean; }; + onWalletClick: () => void; + onMaxClick: (() => void) | undefined; }) { + const theme = useCustomTheme(); const chainQuery = useBridgeChains(props.client); const chain = chainQuery.data?.find( (chain) => chain.chainId === props.selectedToken?.data?.chainId, @@ -648,140 +732,191 @@ function TokenSection(props: { return ( - {/* row1 : label */} - - {props.label} - + {/* make the background semi-transparent */} + - {/* row2 : amount and select token */} -
- {props.amount.isFetching ? ( - - ) : ( - - )} + {/* row1 : label */} + + + + {props.type === "buy" ? "BUY" : "SELL"} + + + {props.activeWalletInfo && ( + + + + )} + - {!props.selectedToken ? ( - - ) : ( + - )} -
- {/* row3 : fiat value/error and balance */} -
- {/* Exceeds Balance / Fiat Value */} - {props.notEnoughBalance ? ( - - {" "} - Exceeds Balance{" "} - - ) : ( -
- - {getFiatSymbol(props.currency)} - - {props.amount.isFetching ? ( - - ) : ( -
- + + {props.amount.isFetching ? ( +
+ +
+ ) : ( + -
- )} -
- )} + )} - {/* Balance */} - {props.isConnected && props.selectedToken && ( -
- {props.balance.data === undefined || - props.selectedToken.data === undefined ? ( - - ) : ( - + Max + + )} + + + + + {/* row3 : fiat value and balance */} +
+
- - + ) : ( + + {formatCurrencyAmount(props.currency, totalFiatValue || 0)} + + )} +
+ + {/* Balance */} + {props.isConnected && props.selectedToken && ( +
+ {props.balance.data === undefined || + props.selectedToken.data === undefined ? ( + + ) : ( +
+ + Balance: + + + {formatTokenAmount( + props.balance.data, + props.selectedToken.data.decimals, + 5, + )} + +
)} - /> - - )} -
- )} -
+
+ )} +
+
+
+
); } @@ -797,92 +932,141 @@ function SelectedTokenButton(props: { onSelectToken: () => void; chain: BridgeChain | undefined; }) { + const theme = useCustomTheme(); return ( ); @@ -894,11 +1078,13 @@ function SwitchButton(props: { onClick: () => void }) { style={{ display: "flex", justifyContent: "center", - marginBlock: `-14px`, + marginBlock: `-13px`, + zIndex: 2, + position: "relative", }} > { props.onClick(); const node = e.currentTarget.querySelector("svg"); @@ -912,7 +1098,7 @@ function SwitchButton(props: { onClick: () => void }) { } }} > - + ); @@ -922,10 +1108,11 @@ const SwitchButtonInner = /* @__PURE__ */ styled(Button)(() => { const theme = useCustomTheme(); return { "&:hover": { - background: theme.colors.modalBg, + background: theme.colors.secondaryButtonBg, }, - borderRadius: radius.lg, + borderRadius: radius.full, padding: spacing.xs, + color: theme.colors.primaryText, background: theme.colors.modalBg, border: `1px solid ${theme.colors.borderColor}`, }; @@ -948,3 +1135,78 @@ function useTokenBalance(props: { : undefined, }); } + +function ActiveWalletDetails(props: { + activeWalletInfo: ActiveWalletInfo; + client: ThirdwebClient; +}) { + const wallet = props.activeWalletInfo.activeWallet; + const account = props.activeWalletInfo.activeAccount; + + const accountBlobbie = ( + + ); + const accountAvatarFallback = ( + + ); + + return ( + + + + + + + + {shortenAddress(account.address)} + } + loadingComponent={ + {shortenAddress(account.address)} + } + /> + + + + + + ); +} + +const WalletButton = /* @__PURE__ */ styled(Button)(() => { + const theme = useCustomTheme(); + return { + color: theme.colors.secondaryText, + transition: "color 200ms ease", + "&:hover": { + color: theme.colors.primaryText, + }, + }; +}); diff --git a/packages/thirdweb/src/react/web/ui/Bridge/swap-widget/utils.ts b/packages/thirdweb/src/react/web/ui/Bridge/swap-widget/utils.ts index a92d79e481d..86b56aa63c5 100644 --- a/packages/thirdweb/src/react/web/ui/Bridge/swap-widget/utils.ts +++ b/packages/thirdweb/src/react/web/ui/Bridge/swap-widget/utils.ts @@ -1,3 +1,9 @@ export function cleanedChainName(name: string) { return name.replace("Mainnet", ""); } + +export const tokenAmountFormatter = new Intl.NumberFormat("en-US", { + notation: "compact", + maximumFractionDigits: 5, + minimumFractionDigits: 2, +}); diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/icons/ArrowUpDownIcon.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/icons/ArrowUpDownIcon.tsx index f97ba66fb65..817bdc508bf 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/icons/ArrowUpDownIcon.tsx +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/icons/ArrowUpDownIcon.tsx @@ -3,21 +3,21 @@ import type { IconFC } from "./types.js"; export const ArrowUpDownIcon: IconFC = (props) => { return ( - - - - + + + ); }; diff --git a/packages/thirdweb/src/react/web/ui/components/basic.tsx b/packages/thirdweb/src/react/web/ui/components/basic.tsx index 270bd21b1ab..1e282ca668c 100644 --- a/packages/thirdweb/src/react/web/ui/components/basic.tsx +++ b/packages/thirdweb/src/react/web/ui/components/basic.tsx @@ -84,7 +84,7 @@ export function Container(props: { expand?: boolean; center?: "x" | "y" | "both"; gap?: keyof typeof spacing; - children: React.ReactNode; + children?: React.ReactNode; style?: React.CSSProperties; p?: keyof typeof spacing; px?: keyof typeof spacing; diff --git a/packages/thirdweb/src/react/web/ui/components/buttons.tsx b/packages/thirdweb/src/react/web/ui/components/buttons.tsx index 655d9963204..a327fcf40b9 100644 --- a/packages/thirdweb/src/react/web/ui/components/buttons.tsx +++ b/packages/thirdweb/src/react/web/ui/components/buttons.tsx @@ -21,6 +21,7 @@ type ButtonProps = { fullWidth?: boolean; gap?: keyof typeof spacing; bg?: keyof Theme["colors"]; + hoverBg?: keyof Theme["colors"]; }; export const Button = /* @__PURE__ */ StyledButton((props: ButtonProps) => { @@ -95,6 +96,9 @@ export const Button = /* @__PURE__ */ StyledButton((props: ButtonProps) => { transition: "border 200ms ease", WebkitTapHighlightColor: "transparent", width: props.fullWidth ? "100%" : undefined, + "&:hover": { + background: props.hoverBg ? theme.colors[props.hoverBg] : undefined, + }, ...(() => { if (props.variant === "outline") { return { @@ -120,7 +124,7 @@ export const Button = /* @__PURE__ */ StyledButton((props: ButtonProps) => { if (props.variant === "ghost-solid") { return { "&:hover": { - background: theme.colors.tertiaryBg, + background: theme.colors[props.hoverBg || "tertiaryBg"], }, border: "1px solid transparent", }; @@ -137,7 +141,7 @@ export const Button = /* @__PURE__ */ StyledButton((props: ButtonProps) => { if (props.variant === "secondary") { return { "&:hover": { - background: theme.colors.secondaryButtonHoverBg, + background: theme.colors[props.hoverBg || "secondaryButtonHoverBg"], }, }; } diff --git a/packages/thirdweb/src/react/web/ui/components/formElements.tsx b/packages/thirdweb/src/react/web/ui/components/formElements.tsx index 7b75e0b60b8..c2489878471 100644 --- a/packages/thirdweb/src/react/web/ui/components/formElements.tsx +++ b/packages/thirdweb/src/react/web/ui/components/formElements.tsx @@ -2,6 +2,7 @@ import { useCustomTheme } from "../../../core/design-system/CustomThemeProvider.js"; import { fontSize, + media, radius, spacing, type Theme, @@ -97,11 +98,14 @@ export const Input = /* @__PURE__ */ StyledInput((props) => { color: theme.colors.primaryText, display: "block", fontFamily: "inherit", - fontSize: fontSize.md, + fontSize: fontSize.sm, outline: "none", padding: props.sm ? spacing.sm : fontSize.sm, WebkitAppearance: "none", width: "100%", + [media.mobile]: { + fontSize: fontSize.md, + }, }; }); diff --git a/packages/thirdweb/src/react/web/ui/hooks/useisMobile.ts b/packages/thirdweb/src/react/web/ui/hooks/useisMobile.ts new file mode 100644 index 00000000000..0ab1c0be695 --- /dev/null +++ b/packages/thirdweb/src/react/web/ui/hooks/useisMobile.ts @@ -0,0 +1,21 @@ +import * as React from "react"; + +const MOBILE_BREAKPOINT = 640; + +export function useIsMobile() { + const [isMobile, setIsMobile] = React.useState( + undefined, + ); + + React.useEffect(() => { + const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`); + const onChange = () => { + setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); + }; + mql.addEventListener("change", onChange); + setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); + return () => mql.removeEventListener("change", onChange); + }, []); + + return !!isMobile; +} diff --git a/packages/thirdweb/src/stories/Bridge/Swap/SelectChain.stories.tsx b/packages/thirdweb/src/stories/Bridge/Swap/SelectChain.stories.tsx index 0d5e4508fab..a3877a2e19d 100644 --- a/packages/thirdweb/src/stories/Bridge/Swap/SelectChain.stories.tsx +++ b/packages/thirdweb/src/stories/Bridge/Swap/SelectChain.stories.tsx @@ -16,13 +16,14 @@ const meta = { } satisfies Meta; export default meta; -export function WithData() { +export function WithDataDesktop() { const [selectedChain, setSelectedChain] = useState( undefined, ); return ( {}} @@ -32,13 +33,50 @@ export function WithData() { ); } -export function Loading() { +export function LoadingDesktop() { const [selectedChain, setSelectedChain] = useState( undefined, ); return ( {}} + isPending={true} + chains={[]} + selectedChain={selectedChain} + /> + + ); +} + +export function WithDataMobile() { + const [selectedChain, setSelectedChain] = useState( + undefined, + ); + return ( + + {}} + selectedChain={selectedChain} + /> + + ); +} + +export function LoadingMobile() { + const [selectedChain, setSelectedChain] = useState( + undefined, + ); + return ( + + {}} diff --git a/packages/thirdweb/src/stories/Bridge/Swap/SwapWidget.stories.tsx b/packages/thirdweb/src/stories/Bridge/Swap/SwapWidget.stories.tsx index 03bafc8bb8a..8f7381a2698 100644 --- a/packages/thirdweb/src/stories/Bridge/Swap/SwapWidget.stories.tsx +++ b/packages/thirdweb/src/stories/Bridge/Swap/SwapWidget.stories.tsx @@ -1,7 +1,6 @@ import type { Meta } from "@storybook/react"; import { lightTheme } from "../../../react/core/design-system/index.js"; import { SwapWidget } from "../../../react/web/ui/Bridge/swap-widget/SwapWidget.js"; -import { ConnectButton } from "../../../react/web/ui/ConnectWallet/ConnectButton.js"; import { storyClient } from "../../utils.js"; const meta: Meta = { @@ -14,15 +13,6 @@ const meta: Meta = { return (
-
- -
); }, @@ -31,15 +21,28 @@ const meta: Meta = { export default meta; export function BasicUsage() { - return ; + return ; } export function CurrencySet() { - return ; + return ( + + ); } export function LightMode() { - return ; + return ( + + ); } export function NoThirdwebBranding() { @@ -48,6 +51,7 @@ export function NoThirdwebBranding() { client={storyClient} currency="JPY" showThirdwebBranding={false} + persistTokenSelections={false} /> ); } @@ -57,6 +61,7 @@ export function CustomTheme() {