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() {