Skip to content

Commit 64f166b

Browse files
committed
[MNY-189] SDK: SwapWidget UI improvements (#8080)
<!-- ## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes" If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000): ## Notes for the reviewer Anything important to call out? Be sure to also clarify these in your comments. ## How to test Unit tests, playground, etc. --> <!-- start pr-codex --> --- ## PR-Codex overview This PR focuses on UI improvements for the `SwapWidget`, enhancing responsiveness, styling, and functionality, particularly for mobile users. It also refines token selection and formatting for better user experience. ### Detailed summary - Removed a changeset file related to `SwapWidget` UI improvements. - Adjusted breakpoints and font sizes in `design-system/index.ts`. - Enhanced `SearchInput` styling in `SearchInput.tsx`. - Added `tokenAmountFormatter` utility in `utils.ts`. - Made `children` prop optional in `basic.tsx`. - Updated color values in `sdk-component-theme.ts`. - Enhanced `Input` component styles in `formElements.tsx`. - Improved `BuyAndSwapEmbed.tsx` with better token handling. - Introduced `useIsMobile` hook for mobile detection. - Updated `ArrowUpDownIcon.tsx` SVG for better rendering. - Added hover background support in `buttons.tsx`. - Renamed `WithData` to `WithDataDesktop` in `SelectChain.stories.tsx`. - Added mobile versions of `WithData` and `Loading` functions in `SelectChain.stories.tsx`. - Updated `SwapWidget` stories to manage token selections and themes. - Refined `select-chain.tsx` for better mobile handling. - Improved `swap-ui.tsx` with mobile responsiveness and token selection logic. - Enhanced `select-token-ui.tsx` with mobile UI improvements and token display logic. - Added `ActiveWalletDetails` for displaying wallet information. - Updated various button and token display styles for consistency and better UX. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Swap widget: onDisconnect callback, details modal, max-fill, improved wallet/account display, clearer loading/insufficient-balance states; stories now disable token-selection persistence. * Mobile detection hook enabling separate mobile/desktop flows. * **UI/Style** * Enhanced token/chain visuals (gradients, smaller icon size), adjusted spacing and input sizing, customizable button hover backgrounds. * **Refactor** * Unified token amount formatting and simplified numeric displays. * **Chores** * Added changeset entry for UI improvements. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent e9225c3 commit 64f166b

File tree

18 files changed

+1001
-557
lines changed

18 files changed

+1001
-557
lines changed

.changeset/nine-otters-pay.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"thirdweb": patch
3+
---
4+
5+
SwapWidget UI improvements

apps/dashboard/src/@/components/blocks/BuyAndSwapEmbed.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,12 +107,13 @@ export function BuyAndSwapEmbed(props: {
107107
theme={themeObj}
108108
className="!rounded-2xl !border-none !w-full"
109109
prefill={{
110-
sellToken: {
110+
// buy this token by default
111+
buyToken: {
111112
chainId: props.chain.id,
112113
tokenAddress: props.tokenAddress,
113114
},
114-
// only set `buyToken` as "Native token" if `sellToken` is not a "native token" already
115-
buyToken: props.tokenAddress
115+
// sell the native token by default (but if buytoken is a native token, don't set)
116+
sellToken: props.tokenAddress
116117
? {
117118
chainId: props.chain.id,
118119
}

apps/dashboard/src/@/utils/sdk-component-theme.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@ export function getSDKTheme(theme: "light" | "dark"): Theme {
3131
selectedTextBg: "hsl(var(--inverted))",
3232
selectedTextColor: "hsl(var(--inverted-foreground))",
3333
separatorLine: "hsl(var(--border))",
34-
skeletonBg: "hsl(var(--muted))",
34+
skeletonBg: "hsl(var(--secondary-foreground)/15%)",
3535
success: "hsl(var(--success-text))",
36-
tertiaryBg: "hsl(var(--muted)/50%)",
36+
tertiaryBg: "hsl(var(--muted)/30%)",
3737
tooltipBg: "hsl(var(--popover))",
3838
tooltipText: "hsl(var(--popover-foreground))",
3939
},

packages/thirdweb/src/react/core/design-system/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ export const iconSize = {
200200
"4xl": "128",
201201
lg: "32",
202202
md: "24",
203+
"sm+": "20",
203204
sm: "16",
204205
xl: "48",
205206
xs: "12",

packages/thirdweb/src/react/web/ui/Bridge/swap-widget/SearchInput.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export function SearchInput(props: {
3636
variant="outline"
3737
placeholder={props.placeholder}
3838
value={props.value}
39+
sm
3940
style={{
4041
paddingLeft: "44px",
4142
}}

packages/thirdweb/src/react/web/ui/Bridge/swap-widget/SwapWidget.tsx

Lines changed: 59 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,10 @@ export type SwapWidgetProps = {
162162
* @default true
163163
*/
164164
persistTokenSelections?: boolean;
165+
/**
166+
* Called when the user disconnects the active wallet
167+
*/
168+
onDisconnect?: () => void;
165169
};
166170

167171
/**
@@ -325,46 +329,11 @@ function SwapWidgetContent(props: SwapWidgetProps) {
325329
});
326330

327331
const [buyToken, setBuyToken] = useState<TokenSelection | undefined>(() => {
328-
if (props.prefill?.buyToken) {
329-
return {
330-
tokenAddress:
331-
props.prefill.buyToken.tokenAddress ||
332-
getAddress(NATIVE_TOKEN_ADDRESS),
333-
chainId: props.prefill.buyToken.chainId,
334-
};
335-
}
336-
337-
if (!isPersistEnabled) {
338-
return undefined;
339-
}
340-
341-
const lastUsedBuyToken = getLastUsedTokens()?.buyToken;
342-
343-
// the token that will be set as initial value of sell token
344-
const sellToken = getInitialSellToken(
345-
props.prefill,
346-
getLastUsedTokens()?.sellToken,
347-
);
348-
349-
// if both tokens are same, ignore "buyToken", keep "sellToken"
350-
if (
351-
lastUsedBuyToken &&
352-
sellToken &&
353-
lastUsedBuyToken.tokenAddress.toLowerCase() ===
354-
sellToken.tokenAddress.toLowerCase() &&
355-
lastUsedBuyToken.chainId === sellToken.chainId
356-
) {
357-
return undefined;
358-
}
359-
360-
return lastUsedBuyToken;
332+
return getInitialTokens(props.prefill).buyToken;
361333
});
362334

363335
const [sellToken, setSellToken] = useState<TokenSelection | undefined>(() => {
364-
return getInitialSellToken(
365-
props.prefill,
366-
isPersistEnabled ? getLastUsedTokens()?.sellToken : undefined,
367-
);
336+
return getInitialTokens(props.prefill).sellToken;
368337
});
369338

370339
// persist selections to localStorage whenever they change
@@ -394,6 +363,7 @@ function SwapWidgetContent(props: SwapWidgetProps) {
394363
if (screen.id === "1:swap-ui" || !activeWalletInfo) {
395364
return (
396365
<SwapUI
366+
onDisconnect={props.onDisconnect}
397367
showThirdwebBranding={
398368
props.showThirdwebBranding === undefined
399369
? true
@@ -533,17 +503,60 @@ function SwapWidgetContent(props: SwapWidgetProps) {
533503
return null;
534504
}
535505

536-
function getInitialSellToken(
537-
prefill: SwapWidgetProps["prefill"],
538-
lastUsedSellToken: TokenSelection | undefined,
539-
) {
540-
if (prefill?.sellToken) {
506+
function getInitialTokens(prefill: SwapWidgetProps["prefill"]): {
507+
buyToken: TokenSelection | undefined;
508+
sellToken: TokenSelection | undefined;
509+
} {
510+
const lastUsedTokens = getLastUsedTokens();
511+
const buyToken = prefill?.buyToken
512+
? {
513+
tokenAddress:
514+
prefill.buyToken.tokenAddress || getAddress(NATIVE_TOKEN_ADDRESS),
515+
chainId: prefill.buyToken.chainId,
516+
}
517+
: lastUsedTokens?.buyToken;
518+
519+
const sellToken = prefill?.sellToken
520+
? {
521+
tokenAddress:
522+
prefill.sellToken.tokenAddress || getAddress(NATIVE_TOKEN_ADDRESS),
523+
chainId: prefill.sellToken.chainId,
524+
}
525+
: lastUsedTokens?.sellToken;
526+
527+
// if both tokens are same
528+
if (
529+
buyToken &&
530+
sellToken &&
531+
buyToken.tokenAddress?.toLowerCase() ===
532+
sellToken.tokenAddress?.toLowerCase() &&
533+
buyToken.chainId === sellToken.chainId
534+
) {
535+
// if sell token prefill is specified, ignore buy token
536+
if (prefill?.sellToken) {
537+
return {
538+
buyToken: undefined,
539+
sellToken: sellToken,
540+
};
541+
}
542+
543+
// if buy token prefill is specified, ignore sell token
544+
if (prefill?.buyToken) {
545+
return {
546+
buyToken: buyToken,
547+
sellToken: undefined,
548+
};
549+
}
550+
551+
// if none of the two are specified via prefill, keep buy token
541552
return {
542-
tokenAddress:
543-
prefill.sellToken.tokenAddress || getAddress(NATIVE_TOKEN_ADDRESS),
544-
chainId: prefill.sellToken.chainId,
553+
buyToken: buyToken,
554+
sellToken: undefined,
545555
};
546556
}
547557

548-
return lastUsedSellToken;
558+
return {
559+
buyToken: buyToken,
560+
sellToken: sellToken,
561+
};
549562
}

packages/thirdweb/src/react/web/ui/Bridge/swap-widget/common.tsx

Lines changed: 0 additions & 35 deletions
This file was deleted.

packages/thirdweb/src/react/web/ui/Bridge/swap-widget/select-chain.tsx

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { ThirdwebClient } from "../../../../../client/client.js";
44
import {
55
fontSize,
66
iconSize,
7+
radius,
78
spacing,
89
} from "../../../../core/design-system/index.js";
910
import { Container, Line, ModalHeader } from "../../components/basic.js";
@@ -21,6 +22,7 @@ type SelectBuyTokenProps = {
2122
client: ThirdwebClient;
2223
onSelectChain: (chain: BridgeChain) => void;
2324
selectedChain: BridgeChain | undefined;
25+
isMobile: boolean;
2426
};
2527

2628
/**
@@ -56,11 +58,15 @@ export function SelectBridgeChainUI(
5658
});
5759

5860
return (
59-
<div>
60-
<Container px="md" py="md+">
61-
<ModalHeader onBack={props.onBack} title="Select Chain" />
62-
</Container>
63-
<Line />
61+
<Container fullHeight flex="column">
62+
{props.isMobile && (
63+
<>
64+
<Container px="md" py="md+">
65+
<ModalHeader onBack={props.onBack} title="Select Chain" />
66+
</Container>
67+
<Line />
68+
</>
69+
)}
6470

6571
<Spacer y="md" />
6672

@@ -79,10 +85,12 @@ export function SelectBridgeChainUI(
7985
<Spacer y="sm" />
8086

8187
<Container
88+
expand
8289
px="md"
90+
gap={props.isMobile ? undefined : "xxs"}
8391
flex="column"
8492
style={{
85-
height: "400px",
93+
maxHeight: props.isMobile ? "400px" : "none",
8694
overflowY: "auto",
8795
scrollbarWidth: "none",
8896
paddingBottom: spacing.md,
@@ -95,6 +103,7 @@ export function SelectBridgeChainUI(
95103
client={props.client}
96104
onClick={() => props.onSelectChain(chain)}
97105
isSelected={chain.chainId === props.selectedChain?.chainId}
106+
isMobile={props.isMobile}
98107
/>
99108
))}
100109

@@ -119,7 +128,7 @@ export function SelectBridgeChainUI(
119128
</div>
120129
)}
121130
</Container>
122-
</div>
131+
</Container>
123132
);
124133
}
125134

@@ -144,6 +153,7 @@ function ChainButton(props: {
144153
client: ThirdwebClient;
145154
onClick: () => void;
146155
isSelected: boolean;
156+
isMobile: boolean;
147157
}) {
148158
return (
149159
<Button
@@ -152,18 +162,21 @@ function ChainButton(props: {
152162
style={{
153163
justifyContent: "flex-start",
154164
fontWeight: 500,
155-
fontSize: fontSize.md,
165+
fontSize: props.isMobile ? fontSize.md : fontSize.sm,
156166
border: "1px solid transparent",
157-
padding: `${spacing.sm} ${spacing.sm}`,
167+
padding: !props.isMobile ? `${spacing.xs} ${spacing.xs}` : undefined,
158168
}}
159-
gap="sm"
160169
onClick={props.onClick}
170+
gap="sm"
161171
>
162172
<Img
163173
src={props.chain.icon}
164174
client={props.client}
165-
width={iconSize.lg}
166-
height={iconSize.lg}
175+
width={props.isMobile ? iconSize.lg : iconSize.md}
176+
height={props.isMobile ? iconSize.lg : iconSize.md}
177+
style={{
178+
borderRadius: radius.full,
179+
}}
167180
/>
168181
{cleanedChainName(props.chain.name)}
169182
</Button>

0 commit comments

Comments
 (0)