Skip to content

Commit 0b084d1

Browse files
committed
feat: show all tokens in source
1 parent d0f1afe commit 0b084d1

6 files changed

Lines changed: 77 additions & 61 deletions

File tree

src/components/TokenBalance.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,10 @@ import { useTheme } from '@mui/material/styles';
55

66
const TokenBalance = ({
77
balance,
8-
isSource,
98
isFetching,
109
price,
1110
}: {
1211
balance: sdkAmount.Amount | null;
13-
isSource?: boolean;
1412
isFetching?: boolean;
1513
price?: string | null;
1614
}) => {
@@ -26,8 +24,8 @@ const TokenBalance = ({
2624
);
2725
}
2826

29-
const shouldHideBalance =
30-
!isSource && balance && sdkAmount.display(balance) === '0';
27+
// Hide 0 / $0 for both source and destination lists
28+
const shouldHideBalance = !balance || sdkAmount.display(balance) === '0';
3129

3230
return (
3331
<Stack alignItems="flex-end">

src/hooks/useTokenList.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import {
99
sortTokensByPreference,
1010
applyTokenWhitelist,
1111
applyCustomTokenSupport,
12-
filterTokensByBalance,
1312
applyShittokenFilter,
1413
} from 'utils/tokenListUtils';
1514
import config from 'config';
@@ -62,13 +61,6 @@ export const useTokenList = ({
6261
tokens = applyShittokenFilter(tokens);
6362
}
6463

65-
// Filter by balance for source tokens when not searching
66-
// This allows users to search for zero-balance tokens by contract address if needed
67-
// Never filter destination tokens by balance
68-
if (isSourceList && !searchQuery) {
69-
tokens = filterTokensByBalance(tokens, balances, wallet.address);
70-
}
71-
7264
return tokens;
7365
// eslint-disable-next-line react-hooks/exhaustive-deps
7466
}, [

src/hooks/useTokenListWithSearch.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import type { Token } from 'config/tokens';
1212
import config from 'config';
1313
import { useTokens } from 'contexts/TokensContext';
1414
import { getTokenSymbol } from 'utils';
15-
import { filterTokensByBalance } from 'utils/tokenListUtils';
1615
import type { Balances } from 'utils/wallet/types';
1716
import { unionBy } from 'es-toolkit';
1817

@@ -140,11 +139,6 @@ export const useTokenListWithSearch = ({
140139
});
141140
}
142141

143-
// Filter by balance for source tokens when not searching
144-
if (isSource && !deferredSearch) {
145-
tokens = filterTokensByBalance(tokens, balances, walletAddress);
146-
}
147-
148142
// For destination token list in same-chain swaps, filter out the source token
149143
if (!isSource && isSameChainSwap && sourceToken) {
150144
tokens = tokens.filter(
@@ -161,8 +155,6 @@ export const useTokenListWithSearch = ({
161155
isSource,
162156
isSameChainSwap,
163157
sourceToken,
164-
balances,
165-
walletAddress,
166158
]);
167159

168160
const tokenPrices = useMemo(

src/views/v2/Bridge/AssetPicker/TokenItem.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,6 @@ function TokenItem(props: TokenItemProps) {
153153
<TokenBalance
154154
balance={props.balance}
155155
price={props.price}
156-
isSource={props.isSource}
157156
isFetching={props.isFetchingBalance}
158157
/>
159158
</ListItemButton>

src/views/v3/Bridge/AssetPicker/TokenItem.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ type TokenItemProps = {
2727
isSelected?: boolean;
2828
isFetchingBalance?: boolean;
2929
isSource?: boolean;
30+
dimmed?: boolean;
3031
};
3132

3233
function TokenItem(props: TokenItemProps) {
@@ -79,6 +80,7 @@ function TokenItem(props: TokenItemProps) {
7980
sx={{
8081
...styles.tokenListItem,
8182
...(props.isSelected && styles.tokenListItemSelected),
83+
...(props.dimmed ? { opacity: 0.6 } : {}),
8284
}}
8385
dense
8486
data-testid={`token-button-${chain.toLowerCase()}-${token.address.toString()}`}
@@ -154,7 +156,6 @@ function TokenItem(props: TokenItemProps) {
154156
<TokenBalance
155157
balance={props.balance}
156158
price={props.price}
157-
isSource={props.isSource}
158159
isFetching={props.isFetchingBalance}
159160
/>
160161
</ListItemButton>

src/views/v3/Bridge/AssetPicker/TokenList.tsx

Lines changed: 73 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useMemo } from 'react';
1+
import React, { Fragment, useMemo } from 'react';
22
import { Box, Card, CardContent, Skeleton, useTheme } from '@mui/material';
33
import CircularProgress from '@mui/material/CircularProgress';
44
import ListItemButton from '@mui/material/ListItemButton';
@@ -14,6 +14,7 @@ import config from 'config';
1414
import { useTokens } from 'contexts/TokensContext';
1515
import type { Balances } from 'utils/wallet/types';
1616
import { useTokenListWithSearch } from 'hooks/useTokenListWithSearch';
17+
import { amount as sdkAmount } from '@wormhole-foundation/sdk';
1718

1819
type Props = {
1920
tokenList: Array<Token>;
@@ -102,11 +103,6 @@ const TokenList = (props: Props) => {
102103

103104
// Determine the current state of the token list
104105
const listState = useMemo(() => {
105-
// For source list, require wallet to show balances/tokens from wallet
106-
if (props.isSource && !props.wallet?.address && !props.isConnectingWallet) {
107-
return 'empty';
108-
}
109-
110106
// Currently fetching initial data
111107
if (props.isFetching) {
112108
return 'loading';
@@ -130,27 +126,49 @@ const TokenList = (props: Props) => {
130126
const shouldShowLoadingState = listState === 'loading';
131127
const shouldShowEmptyMessage = listState === 'empty';
132128

129+
// Build sectioned list for source picker when not searching
130+
const isGroupingEnabled = props.isSource && !props.searchQuery;
131+
132+
const { itemsForRender, ownedCount } = useMemo(() => {
133+
if (!isGroupingEnabled) {
134+
return { itemsForRender: sortedTokens, ownedCount: 0 };
135+
}
136+
137+
const hasPositiveBalance = (token: Token) => {
138+
const bal = props.balances?.[token.key]?.balance;
139+
return !!(bal && sdkAmount.units(bal) > 0n);
140+
};
141+
142+
const nativeToken = sortedTokens.find((token) => token.isNativeGasToken);
143+
const ownedTokens = sortedTokens.filter((tok) => hasPositiveBalance(tok));
144+
const ownedSet = new Set(ownedTokens.map((token) => token.key));
145+
146+
const items: Token[] = [];
147+
// Owned tokens first, preserving order from sortedTokens
148+
items.push(...ownedTokens);
149+
150+
// Include native gas token next if not owned and exists
151+
if (nativeToken && !ownedSet.has(nativeToken.key)) {
152+
items.push(nativeToken);
153+
}
154+
155+
// Then the rest
156+
for (const token of sortedTokens) {
157+
if (ownedSet.has(token.key)) continue;
158+
if (nativeToken && token.key === nativeToken.key) continue;
159+
items.push(token);
160+
}
161+
162+
return { itemsForRender: items, ownedCount: ownedTokens.length };
163+
}, [isGroupingEnabled, props.balances, sortedTokens]);
164+
133165
const searchList = (
134166
<SearchableList<Token>
135167
searchPlaceholder={placeholder}
136168
sx={styles.tokenList}
137169
dataTestId="token-search-list"
138170
searchQuery={props.searchQuery}
139-
listTitle={
140-
shouldShowEmptyMessage ? (
141-
emptyMessage
142-
) : (
143-
<Box display="flex" width="100%">
144-
<Typography
145-
style={{ flexGrow: '2' }}
146-
fontSize={14}
147-
color={theme.palette.text.secondary}
148-
>
149-
Tokens on {props.selectedChainConfig.displayName}
150-
</Typography>
151-
</Box>
152-
)
153-
}
171+
listTitle={shouldShowEmptyMessage ? emptyMessage : ''}
154172
loading={
155173
shouldShowLoadingState &&
156174
[1, 2, 3].map((x) => (
@@ -161,32 +179,51 @@ const TokenList = (props: Props) => {
161179
</ListItemButton>
162180
))
163181
}
164-
items={sortedTokens}
182+
items={itemsForRender}
165183
onQueryChange={(query) => {
166184
props.onSearchQueryChange(query);
167185
}}
168-
renderFn={(token: Token) => {
186+
renderFn={(token: Token, index: number) => {
169187
const balance = props.balances?.[token.key]?.balance;
170188
const tokenPrice = tokenPrices.get(token.key);
171189
const price =
172190
balance && tokenPrice !== undefined
173191
? getUSDFormat(calculateUSDPriceRaw(tokenPrice, balance, token))
174192
: null;
175193

194+
const isRestSection = isGroupingEnabled && index >= ownedCount;
195+
176196
return (
177-
<TokenItem
178-
key={token.key}
179-
token={token}
180-
chain={props.selectedChainConfig.sdkName}
181-
onClick={() => {
182-
props.onSelectToken(token);
183-
}}
184-
isSource={props.isSource}
185-
balance={balance}
186-
price={price}
187-
isSelected={token.key === props.selectedToken?.key}
188-
isFetchingBalance={props.isFetchingBalances}
189-
/>
197+
<Fragment key={token.key}>
198+
{isGroupingEnabled && index === 0 && ownedCount > 0 && (
199+
<Box sx={{ padding: '4px 16px' }}>
200+
<Typography fontSize={14} color={theme.palette.text.secondary}>
201+
Your tokens
202+
</Typography>
203+
</Box>
204+
)}
205+
{isGroupingEnabled && index === ownedCount && (
206+
<Box sx={{ padding: '4px 16px' }}>
207+
<Typography fontSize={14} color={theme.palette.text.secondary}>
208+
All tokens
209+
</Typography>
210+
</Box>
211+
)}
212+
<TokenItem
213+
key={token.key}
214+
token={token}
215+
chain={props.selectedChainConfig.sdkName}
216+
onClick={() => {
217+
props.onSelectToken(token);
218+
}}
219+
isSource={props.isSource}
220+
balance={balance}
221+
price={price}
222+
isSelected={token.key === props.selectedToken?.key}
223+
isFetchingBalance={props.isFetchingBalances}
224+
dimmed={isRestSection}
225+
/>
226+
</Fragment>
190227
);
191228
}}
192229
/>
@@ -196,9 +233,6 @@ const TokenList = (props: Props) => {
196233
<Card sx={styles.card} variant="elevation">
197234
<CardContent sx={styles.tokenListContainer}>
198235
<Box sx={{ display: 'flex', padding: '0 16px' }}>
199-
<Typography width="100%" sx={styles.title}>
200-
Select a token
201-
</Typography>
202236
{isFetchingToken || props.isFetchingBalances ? (
203237
<CircularProgress
204238
sx={{ alignSelf: 'flex-end', marginBottom: '12px' }}

0 commit comments

Comments
 (0)