diff --git a/packages/widget/src/components/TokenList/PinTokenButton.tsx b/packages/widget/src/components/TokenList/PinTokenButton.tsx new file mode 100644 index 000000000..e9dab8681 --- /dev/null +++ b/packages/widget/src/components/TokenList/PinTokenButton.tsx @@ -0,0 +1,50 @@ +import PushPinIcon from '@mui/icons-material/PushPin' +import PushPinOutlinedIcon from '@mui/icons-material/PushPinOutlined' +import { IconButton } from '@mui/material' +import { usePinnedTokensStore } from '../../stores/pinnedTokens/PinnedTokensStore.js' + +interface PinTokenButtonProps { + chainId: number + tokenAddress: string +} + +export const PinTokenButton = ({ + chainId, + tokenAddress, +}: PinTokenButtonProps) => { + const [pinnedTokens, pinToken, unpinToken] = usePinnedTokensStore((state) => [ + state.pinnedTokens, + state.pinToken, + state.unpinToken, + ]) + + const isPinned = + pinnedTokens[chainId]?.includes(tokenAddress.toLowerCase()) ?? false + + const handleClick = (e: React.MouseEvent) => { + e.stopPropagation() + ;(e.currentTarget as HTMLElement).blur() + if (isPinned) { + unpinToken(chainId, tokenAddress) + } else { + pinToken(chainId, tokenAddress) + } + } + + const PinIcon = isPinned ? PushPinIcon : PushPinOutlinedIcon + + return ( + + + + ) +} diff --git a/packages/widget/src/components/TokenList/TokenList.tsx b/packages/widget/src/components/TokenList/TokenList.tsx index 955bb3705..17f75a012 100644 --- a/packages/widget/src/components/TokenList/TokenList.tsx +++ b/packages/widget/src/components/TokenList/TokenList.tsx @@ -41,6 +41,7 @@ export const TokenList: FC = memo(({ formType, headerRef }) => { const { tokens, withCategories, + withPinnedTokens, isTokensLoading, isBalanceLoading, isSearchLoading, @@ -77,6 +78,7 @@ export const TokenList: FC = memo(({ formType, headerRef }) => { isLoading={isTokensLoading || isSearchLoading} isBalanceLoading={isBalanceLoading} showCategories={showCategories} + showPinnedTokens={withPinnedTokens} onClick={handleTokenClick} selectedTokenAddress={selectedTokenAddress} isAllNetworks={isAllNetworks} diff --git a/packages/widget/src/components/TokenList/TokenListItem.tsx b/packages/widget/src/components/TokenList/TokenListItem.tsx index eb9c76b08..5663a53a7 100644 --- a/packages/widget/src/components/TokenList/TokenListItem.tsx +++ b/packages/widget/src/components/TokenList/TokenListItem.tsx @@ -19,6 +19,7 @@ import { formatTokenAmount, formatTokenPrice } from '../../utils/format.js' import { shortenAddress } from '../../utils/wallet.js' import { TokenAvatar } from '../Avatar/TokenAvatar.js' import { ListItemButton } from '../ListItem/ListItemButton.js' +import { PinTokenButton } from './PinTokenButton.js' import { IconButton, ListItem } from './TokenList.style.js' import type { TokenListItemAvatarProps, @@ -226,17 +227,17 @@ const TokenListItemButton: React.FC = memo( appear={false} mountOnEnter > - + + @@ -279,6 +280,8 @@ const TokenListItemButton: React.FC = memo( = memo( > {shortenAddress(token.address)} - + + + + diff --git a/packages/widget/src/components/TokenList/VirtualizedTokenList.tsx b/packages/widget/src/components/TokenList/VirtualizedTokenList.tsx index 4134d4767..44ba2d457 100644 --- a/packages/widget/src/components/TokenList/VirtualizedTokenList.tsx +++ b/packages/widget/src/components/TokenList/VirtualizedTokenList.tsx @@ -23,6 +23,7 @@ export const VirtualizedTokenList: FC = ({ isLoading, isBalanceLoading, showCategories, + showPinnedTokens, onClick, isAllNetworks, }) => { @@ -62,29 +63,33 @@ export const VirtualizedTokenList: FC = ({ const estimateSize = useCallback( (index: number) => { const currentToken = tokens[index] - - // Base size for TokenListItem + const previousToken = tokens[index - 1] let size = tokenItemHeight - // Early return if categories are not shown + // Pinned tokens (always shown, even in all networks mode) + if (currentToken.pinned && index === 0) { + size += 24 + } + if (previousToken?.pinned && !currentToken.pinned) { + size += 32 + } + if (!showCategories) { return size } - const previousToken = tokens[index - 1] - - // Adjust size for the first featured token - if (currentToken.featured && index === 0) { + if (currentToken.featured && !currentToken.pinned && index === 0) { size += 24 } - // Adjust size based on changes between the current and previous tokens - const isCategoryChanged = - (previousToken?.amount && !currentToken.amount) || - (previousToken?.featured && !currentToken.featured) || - (previousToken?.popular && !currentToken.popular) - - if (isCategoryChanged) { + // Category transition (excluding pinned tokens) + const isNotPinned = !currentToken.pinned && !previousToken?.pinned + if ( + isNotPinned && + ((previousToken?.amount && !currentToken.amount) || + (previousToken?.featured && !currentToken.featured) || + (previousToken?.popular && !currentToken.popular)) + ) { size += 32 } @@ -93,7 +98,6 @@ export const VirtualizedTokenList: FC = ({ [tokens, showCategories] ) - // Chunk the tokens for infinite loading simulation const virtualizerConfig = useMemo( () => ({ count: tokens.length, @@ -135,47 +139,64 @@ export const VirtualizedTokenList: FC = ({ {getVirtualItems().map((item) => { const currentToken = tokens[item.index] const previousToken: TokenAmount | undefined = tokens[item.index - 1] - const chain = chainsSet?.get(currentToken.chainId) - const isFirstFeaturedToken = currentToken.featured && item.index === 0 - - const isTransitionFromFeaturedTokens = - previousToken?.featured && !currentToken.featured + const isNotPinned = !currentToken.pinned + const isFirstPinnedToken = currentToken.pinned && item.index === 0 + const isTransitionFromPinned = previousToken?.pinned && isNotPinned + // Category transitions (excluding pinned) + const isTransitionFromFeatured = + previousToken?.featured && !currentToken.featured && isNotPinned const isTransitionFromMyTokens = - previousToken?.amount && !currentToken.amount - - const isTransitionToMyTokens = - isTransitionFromFeaturedTokens && currentToken.amount - - const isTransitionToPopularTokens = - (isTransitionFromFeaturedTokens || isTransitionFromMyTokens) && - currentToken.popular - - const shouldShowAllTokensCategory = - isTransitionFromMyTokens || - isTransitionFromFeaturedTokens || - (previousToken?.popular && !currentToken.popular) - - const startAdornmentLabel = - !isAllNetworks && showCategories - ? (() => { - if (isFirstFeaturedToken) { - return t('main.featuredTokens') - } - if (isTransitionToMyTokens) { - return t('main.myTokens') - } - if (isTransitionToPopularTokens) { - return t('main.popularTokens') - } - if (shouldShowAllTokensCategory) { - return t('main.allTokens') - } - return null - })() - : null + previousToken?.amount && !currentToken.amount && isNotPinned + const isTransitionFromPopular = + previousToken?.popular && !currentToken.popular && isNotPinned + + // Determine which category label to show + const startAdornmentLabel = (() => { + if (showPinnedTokens && isFirstPinnedToken) { + return t('main.pinnedTokens') + } + if (showPinnedTokens && !showCategories && isTransitionFromPinned) { + return t('main.allTokens') + } + if (!showCategories) { + return null + } + + if ( + (isTransitionFromPinned && currentToken.featured) || + (currentToken.featured && isNotPinned && item.index === 0) + ) { + return t('main.featuredTokens') + } + if ( + (isTransitionFromFeatured || isTransitionFromPinned) && + currentToken.amount && + isNotPinned + ) { + return t('main.myTokens') + } + if ( + (isTransitionFromFeatured || + isTransitionFromMyTokens || + isTransitionFromPinned) && + currentToken.popular && + isNotPinned + ) { + return t('main.popularTokens') + } + if ( + isTransitionFromMyTokens || + isTransitionFromFeatured || + isTransitionFromPinned || + isTransitionFromPopular + ) { + return t('main.allTokens') + } + return null + })() const isSelected = selectedTokenAddress === currentToken.address && @@ -200,7 +221,13 @@ export const VirtualizedTokenList: FC = ({ fontWeight: 600, lineHeight: '16px', px: 1.5, - pt: isFirstFeaturedToken ? 0 : 1, + pt: + isFirstPinnedToken || + (currentToken.featured && + isNotPinned && + item.index === 0) + ? 0 + : 1, pb: 1, }} > diff --git a/packages/widget/src/components/TokenList/types.ts b/packages/widget/src/components/TokenList/types.ts index af9e8500a..4a96f49de 100644 --- a/packages/widget/src/components/TokenList/types.ts +++ b/packages/widget/src/components/TokenList/types.ts @@ -15,6 +15,7 @@ export interface VirtualizedTokenListProps { isBalanceLoading: boolean chainId?: number showCategories?: boolean + showPinnedTokens?: boolean onClick(tokenAddress: string, chainId?: number): void selectedTokenAddress?: string isAllNetworks: boolean diff --git a/packages/widget/src/hooks/useTokenBalances.ts b/packages/widget/src/hooks/useTokenBalances.ts index c63a51af6..2cdaa0cd7 100644 --- a/packages/widget/src/hooks/useTokenBalances.ts +++ b/packages/widget/src/hooks/useTokenBalances.ts @@ -1,6 +1,7 @@ import { useMemo } from 'react' import { useWidgetConfig } from '../providers/WidgetProvider/WidgetProvider.js' import type { FormType } from '../stores/form/types.js' +import { usePinnedTokensStore } from '../stores/pinnedTokens/PinnedTokensStore.js' import { isSearchMatch, processTokenBalances } from '../utils/tokenList.js' import { useAccountsBalancesData } from './useAccountsBalancesData.js' import { useTokenBalancesQueries } from './useTokenBalancesQueries.js' @@ -29,10 +30,46 @@ export const useTokenBalances = ( const { tokens: configTokens } = useWidgetConfig() + const pinnedTokens = usePinnedTokensStore((state) => state.pinnedTokens) + const isBalanceLoading = (isBalanceQueriesLoading || isAccountsLoading) && !allTokensWithBalances?.length + // Create function to check if token is pinned + const isPinnedToken = useMemo(() => { + if (isAllNetworks) { + // For all networks, check all pinned tokens + const allPinned: Array<{ chainId: number; tokenAddress: string }> = [] + Object.entries(pinnedTokens).forEach(([chainIdStr, addresses]) => { + const chainId = Number.parseInt(chainIdStr, 10) + addresses.forEach((address) => { + allPinned.push({ chainId, tokenAddress: address }) + }) + }) + const pinnedSet = new Set( + allPinned.map((p) => `${p.chainId}-${p.tokenAddress.toLowerCase()}`) + ) + return (chainId: number, tokenAddress: string) => { + const key = `${chainId}-${tokenAddress.toLowerCase()}` + return pinnedSet.has(key) + } + } else if (selectedChainId) { + // For single chain, check only selected chain + const chainPinnedTokens = pinnedTokens[selectedChainId] || [] + const pinnedSet = new Set( + chainPinnedTokens.map((addr) => addr.toLowerCase()) + ) + return (chainId: number, tokenAddress: string) => { + return ( + chainId === selectedChainId && + pinnedSet.has(tokenAddress.toLowerCase()) + ) + } + } + return undefined + }, [isAllNetworks, selectedChainId, pinnedTokens]) + const displayedTokensList = useMemo(() => { const tokensByChain = isAllNetworks ? Object.values(allTokens ?? {}).flat() @@ -69,14 +106,15 @@ export const useTokenBalances = ( isAllNetworks, ]) - const { processedTokens, withCategories } = useMemo(() => { + const { processedTokens, withCategories, withPinnedTokens } = useMemo(() => { return processTokenBalances( isBalanceLoading, isAllNetworks || !!search, configTokens, selectedChainId, displayedTokensList, - displayedTokensWithBalances + displayedTokensWithBalances, + isPinnedToken ) }, [ isBalanceLoading, @@ -86,11 +124,13 @@ export const useTokenBalances = ( displayedTokensList, displayedTokensWithBalances, search, + isPinnedToken, ]) return { tokens: processedTokens ?? [], withCategories, + withPinnedTokens, isTokensLoading, isSearchLoading, isBalanceLoading, diff --git a/packages/widget/src/i18n/en.json b/packages/widget/src/i18n/en.json index 42bda9d9f..fe9ae63fb 100644 --- a/packages/widget/src/i18n/en.json +++ b/packages/widget/src/i18n/en.json @@ -237,6 +237,7 @@ "myTokens": "My tokens", "onChain": "on {{chainName}}", "ownedBy": "Owned by", + "pinnedTokens": "Pinned tokens", "popularTokens": "Popular tokens", "priceImpact": "Price impact", "process": { diff --git a/packages/widget/src/stores/StoreProvider.tsx b/packages/widget/src/stores/StoreProvider.tsx index 2cfd3ac2d..bfd60943f 100644 --- a/packages/widget/src/stores/StoreProvider.tsx +++ b/packages/widget/src/stores/StoreProvider.tsx @@ -4,6 +4,7 @@ import { BookmarkStoreProvider } from './bookmarks/BookmarkStore.js' import { ChainOrderStoreProvider } from './chains/ChainOrderStore.js' import { FormStoreProvider } from './form/FormStore.js' import { HeaderStoreProvider } from './header/useHeaderStore.js' +import { PinnedTokensStoreProvider } from './pinnedTokens/PinnedTokensStore.js' import { RouteExecutionStoreProvider } from './routes/RouteExecutionStore.js' import { SplitSubvariantStoreProvider } from './settings/useSplitSubvariantStore.js' @@ -22,13 +23,15 @@ export const StoreProvider: React.FC> = ({ > - - - - {children} - - - + + + + + {children} + + + + diff --git a/packages/widget/src/stores/pinnedTokens/PinnedTokensStore.tsx b/packages/widget/src/stores/pinnedTokens/PinnedTokensStore.tsx new file mode 100644 index 000000000..503137bf3 --- /dev/null +++ b/packages/widget/src/stores/pinnedTokens/PinnedTokensStore.tsx @@ -0,0 +1,38 @@ +import { createContext, useContext, useRef } from 'react' +import { useShallow } from 'zustand/shallow' +import type { PersistStoreProviderProps } from '../types.js' +import { createPinnedTokensStore } from './createPinnedTokensStore.js' +import type { PinnedTokensState, PinnedTokensStore } from './types.js' + +const PinnedTokensStoreContext = createContext(null) + +export const PinnedTokensStoreProvider: React.FC = ({ + children, + ...props +}) => { + const storeRef = useRef(null) + + if (!storeRef.current) { + storeRef.current = createPinnedTokensStore(props) + } + + return ( + + {children} + + ) +} + +export function usePinnedTokensStore( + selector: (store: PinnedTokensState) => T +) { + const useStore = useContext(PinnedTokensStoreContext) + + if (!useStore) { + throw new Error( + `You forgot to wrap your component in <${PinnedTokensStoreProvider.name}>.` + ) + } + + return useStore(useShallow(selector)) +} diff --git a/packages/widget/src/stores/pinnedTokens/createPinnedTokensStore.ts b/packages/widget/src/stores/pinnedTokens/createPinnedTokensStore.ts new file mode 100644 index 000000000..e1db26926 --- /dev/null +++ b/packages/widget/src/stores/pinnedTokens/createPinnedTokensStore.ts @@ -0,0 +1,71 @@ +import { create } from 'zustand' +import { persist } from 'zustand/middleware' +import type { PersistStoreProps } from '../types.js' +import type { PinnedTokensState } from './types.js' + +export const createPinnedTokensStore = ({ namePrefix }: PersistStoreProps) => + create()( + persist( + (set, get) => ({ + pinnedTokens: {}, + pinToken: (chainId: number, tokenAddress: string) => { + set((state) => { + const normalizedAddress = tokenAddress.toLowerCase() + const chainTokens = state.pinnedTokens[chainId] || [] + if (!chainTokens.includes(normalizedAddress)) { + return { + pinnedTokens: { + ...state.pinnedTokens, + [chainId]: [...chainTokens, normalizedAddress], + }, + } + } + return state + }) + }, + unpinToken: (chainId: number, tokenAddress: string) => { + set((state) => { + const normalizedAddress = tokenAddress.toLowerCase() + const chainTokens = state.pinnedTokens[chainId] || [] + if (chainTokens.includes(normalizedAddress)) { + return { + pinnedTokens: { + ...state.pinnedTokens, + [chainId]: chainTokens.filter( + (addr) => addr !== normalizedAddress + ), + }, + } + } + return state + }) + }, + isPinned: (chainId: number, tokenAddress: string) => { + const normalizedAddress = tokenAddress.toLowerCase() + const chainTokens = get().pinnedTokens[chainId] || [] + return chainTokens.includes(normalizedAddress) + }, + getPinnedTokens: (chainId: number) => { + return get().pinnedTokens[chainId] || [] + }, + getAllPinnedTokens: () => { + const allPinned: Array<{ chainId: number; tokenAddress: string }> = [] + const pinnedTokens = get().pinnedTokens + Object.entries(pinnedTokens).forEach(([chainIdStr, addresses]) => { + const chainId = Number.parseInt(chainIdStr, 10) + addresses.forEach((address) => { + allPinned.push({ chainId, tokenAddress: address }) + }) + }) + return allPinned + }, + }), + { + name: `${namePrefix || 'li.fi'}-pinned-tokens`, + version: 0, + partialize: (state) => ({ + pinnedTokens: state.pinnedTokens, + }), + } + ) + ) diff --git a/packages/widget/src/stores/pinnedTokens/types.ts b/packages/widget/src/stores/pinnedTokens/types.ts new file mode 100644 index 000000000..256d78f9f --- /dev/null +++ b/packages/widget/src/stores/pinnedTokens/types.ts @@ -0,0 +1,20 @@ +import type { StoreApi } from 'zustand' +import type { UseBoundStoreWithEqualityFn } from 'zustand/traditional' + +export interface PinnedTokensProps { + pinnedTokens: Record +} + +export interface PinnedTokensActions { + pinToken: (chainId: number, tokenAddress: string) => void + unpinToken: (chainId: number, tokenAddress: string) => void + isPinned: (chainId: number, tokenAddress: string) => boolean + getPinnedTokens: (chainId: number) => string[] + getAllPinnedTokens: () => Array<{ chainId: number; tokenAddress: string }> +} + +export type PinnedTokensState = PinnedTokensProps & PinnedTokensActions + +export type PinnedTokensStore = UseBoundStoreWithEqualityFn< + StoreApi +> diff --git a/packages/widget/src/types/token.ts b/packages/widget/src/types/token.ts index a90d08f2b..a0b15ee19 100644 --- a/packages/widget/src/types/token.ts +++ b/packages/widget/src/types/token.ts @@ -6,6 +6,7 @@ import type { interface TokenFlags { featured?: boolean popular?: boolean + pinned?: boolean } export interface TokenAmount extends SDKTokenAmount, TokenFlags {} diff --git a/packages/widget/src/utils/tokenList.ts b/packages/widget/src/utils/tokenList.ts index 116242ea0..eb3af2c9e 100644 --- a/packages/widget/src/utils/tokenList.ts +++ b/packages/widget/src/utils/tokenList.ts @@ -21,21 +21,42 @@ export const processTokenBalances = ( configTokens?: WidgetTokens, selectedChainId?: number, tokens?: TokenExtended[], - tokensWithBalances?: TokenAmount[] + tokensWithBalances?: TokenAmount[], + isPinnedToken?: (chainId: number, tokenAddress: string) => boolean ) => { if (isBalanceLoading) { if (noCategories) { const sortedTokens = [...(tokens ?? [])].sort(sortByVolume) + // Separate pinned tokens + if (isPinnedToken) { + const pinned: TokenAmount[] = [] + const notPinned: TokenAmount[] = [] + for (const token of sortedTokens) { + if (isPinnedToken(token.chainId, token.address)) { + const pinnedToken = { ...token, pinned: true } as TokenAmount + pinned.push(pinnedToken) + } else { + notPinned.push(token) + } + } + return { + processedTokens: [...pinned, ...notPinned], + withCategories: false, + withPinnedTokens: !!pinned.length, + } + } return { processedTokens: sortedTokens, withCategories: false, + withPinnedTokens: false, } } else { return processedTypedTokens( tokens ?? [], [], selectedChainId, - configTokens + configTokens, + isPinnedToken ) } } @@ -58,16 +79,55 @@ export const processTokenBalances = ( .sort(sortByVolume) ?? [] if (noCategories) { + // Separate pinned tokens + if (isPinnedToken) { + const pinnedWithBalances: TokenAmount[] = [] + const notPinnedWithBalances: TokenAmount[] = [] + const pinnedWithoutBalances: TokenAmount[] = [] + const notPinnedWithoutBalances: TokenAmount[] = [] + + for (const token of sortedTokensWithBalances) { + if (isPinnedToken(token.chainId, token.address)) { + const pinnedToken = { ...token, pinned: true } as TokenAmount + pinnedWithBalances.push(pinnedToken) + } else { + notPinnedWithBalances.push(token) + } + } + + for (const token of tokensWithoutBalances) { + if (isPinnedToken(token.chainId, token.address)) { + const pinnedToken = { ...token, pinned: true } as TokenAmount + pinnedWithoutBalances.push(pinnedToken) + } else { + notPinnedWithoutBalances.push(token) + } + } + + return { + processedTokens: [ + ...pinnedWithBalances, + ...pinnedWithoutBalances, + ...notPinnedWithBalances, + ...notPinnedWithoutBalances, + ], + withCategories: false, + withPinnedTokens: + !!pinnedWithBalances.length || !!pinnedWithoutBalances.length, + } + } return { processedTokens: [...sortedTokensWithBalances, ...tokensWithoutBalances], withCategories: false, + withPinnedTokens: false, } } else { return processedTypedTokens( tokensWithoutBalances, sortedTokensWithBalances, selectedChainId, - configTokens + configTokens, + isPinnedToken ) } } @@ -77,12 +137,14 @@ const processedTypedTokens = ( tokens: TokenAmount[], tokensWithBalances: TokenAmount[], selectedChainId?: number, - configTokens?: WidgetTokens + configTokens?: WidgetTokens, + isPinnedToken?: (chainId: number, tokenAddress: string) => boolean ) => { const filteredTokensMap = new Map( tokens.map((token) => [token.address, token]) ) + const pinnedTokens: TokenAmount[] = [] const featuredTokensFromConfig: TokenAmount[] = [] const popularTokensFromConfig: TokenAmount[] = [] @@ -108,6 +170,16 @@ const processedTypedTokens = ( } else { featuredTokensFromConfig.push(tokenAmount) } + + // Additionally add to pinned tokens if it is pinned + const isPinned = + isPinnedToken && selectedChainId + ? isPinnedToken(selectedChainId, token.address) + : false + if (isPinned) { + const pinnedToken = { ...tokenAmount, pinned: true } as TokenAmount + pinnedTokens.push(pinnedToken) + } }) }) @@ -124,8 +196,17 @@ const processedTypedTokens = ( const otherTokens: TokenAmount[] = [] + // Separate pinned tokens and categorize remaining tokens for (const token of remainingTokens) { - if (token.featured) { + const isPinned = + isPinnedToken && selectedChainId + ? isPinnedToken(selectedChainId, token.address) + : false + + if (isPinned) { + const pinnedToken = { ...token, pinned: true } as TokenAmount + pinnedTokens.push(pinnedToken) + } else if (token.featured) { featuredTokensFromConfig.push(token) } else if (token.popular) { popularTokensFromConfig.push(token) @@ -134,20 +215,42 @@ const processedTypedTokens = ( } } + // Also check tokens with balances for pinned status + const pinnedTokensWithBalances: TokenAmount[] = [] + const nonPinnedTokensWithBalances: TokenAmount[] = [] + + if (isPinnedToken && selectedChainId) { + for (const token of tokensWithBalances) { + if (isPinnedToken(selectedChainId, token.address)) { + const pinnedToken = { ...token, pinned: true } as TokenAmount + pinnedTokensWithBalances.push(pinnedToken) + } else { + nonPinnedTokensWithBalances.push(token) + } + } + } else { + nonPinnedTokensWithBalances.push(...tokensWithBalances) + } + + const sortedPinnedTokens = [ + ...pinnedTokens, + ...pinnedTokensWithBalances, + ].sort(sortByVolume) const sortedFeaturedTokens = [...featuredTokensFromConfig].sort(sortByVolume) const sortedPopularTokens = [...popularTokensFromConfig].sort(sortByVolume) const sortedOtherTokens = [...otherTokens].sort(sortByVolume) return { processedTokens: [ + ...sortedPinnedTokens, ...sortedFeaturedTokens, - ...tokensWithBalances, + ...nonPinnedTokensWithBalances, ...sortedPopularTokens, ...sortedOtherTokens, ], - withCategories: Boolean( - featuredTokensFromConfig?.length || popularTokensFromConfig?.length - ), + withCategories: + !!featuredTokensFromConfig.length || !!popularTokensFromConfig.length, + withPinnedTokens: !!sortedPinnedTokens.length, } }