Skip to content
Open
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
7063b6a
feat(tokenselector): implement SelectTokenModal with enhanced token s…
fairlighteth Nov 14, 2025
06e4157
Merge branch 'feat/token-selector-6' into feat/token-selector-7
fairlighteth Nov 18, 2025
62d9df0
Merge branch 'feat/token-selector-6' into feat/token-selector-7
fairlighteth Nov 18, 2025
0a36b49
Merge branch 'feat/token-selector-6' into feat/token-selector-7
fairlighteth Nov 25, 2025
24260ca
Merge branch 'feat/token-selector-6' into feat/token-selector-7
fairlighteth Nov 27, 2025
b3f48f1
refactor: extract TokenColumnContent component and clean up SelectTok…
fairlighteth Nov 27, 2025
8288b94
refactor: rename SelectTokenModalProps to TokenListContentProps
fairlighteth Nov 27, 2025
8c81595
refactor: add TODO for prop count reduction in TokenListContentProps
fairlighteth Nov 27, 2025
bc32977
Merge branch 'feat/token-selector-6' into feat/token-selector-7
fairlighteth Dec 3, 2025
bcd4762
refactor: replace legacy chain selector function with a dedicated com…
fairlighteth Dec 3, 2025
49a3fe6
Merge branch 'feat/token-selector-6' into feat/token-selector-7
fairlighteth Dec 4, 2025
6587732
Merge branch 'feat/token-selector-6' into feat/token-selector-7
fairlighteth Dec 4, 2025
b863b55
Merge branch 'feat/token-selector-6' into feat/token-selector-7
fairlighteth Dec 5, 2025
601257c
Merge branch 'feat/token-selector-6' into feat/token-selector-7
fairlighteth Dec 9, 2025
b3783a7
Merge branch 'feat/token-selector-6' into feat/token-selector-7
fairlighteth Dec 10, 2025
32d9b94
refactor: replace TokensContentSection with TokensContent
fairlighteth Dec 10, 2025
b01ecc7
Merge branch 'feat/token-selector-6' into feat/token-selector-7
fairlighteth Dec 10, 2025
0436f9c
Merge branch 'feat/token-selector-6' into feat/token-selector-7
fairlighteth Dec 11, 2025
6ad1e2a
Merge branch 'feat/token-selector-6' into feat/token-selector-7
fairlighteth Dec 11, 2025
7b8d9ba
Merge branch 'feat/token-selector-6' into feat/token-selector-7
fairlighteth Dec 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { ReactNode } from 'react'

import { ChainInfo } from '@cowprotocol/cow-sdk'
import { TokenListCategory } from '@cowprotocol/tokens'

import { SelectTokenModalContent } from './SelectTokenModalContent'
import * as styledEl from './styled'

import { LpTokenListsWidget } from '../../containers/LpTokenListsWidget'
import { ChainsToSelectState, TokenSelectionHandler } from '../../types'
import { ChainsSelector } from '../ChainsSelector'

type TokenListCategoryState = [TokenListCategory[] | null, (category: TokenListCategory[] | null) => void]

export interface TokenColumnContentProps {
displayLpTokenLists?: boolean
account: string | undefined
inputValue: string
onSelectToken: TokenSelectionHandler
openPoolPage(poolAddress: string): void
disableErc20?: boolean
tokenListCategoryState: TokenListCategoryState
isRouteAvailable: boolean | undefined
chainsToSelect?: ChainsToSelectState
onSelectChain: (chain: ChainInfo) => void
children: ReactNode
}

export function TokenColumnContent({
displayLpTokenLists,
account,
inputValue,
onSelectToken,
openPoolPage,
disableErc20,
tokenListCategoryState,
isRouteAvailable,
chainsToSelect,
onSelectChain,
children,
}: TokenColumnContentProps): ReactNode {
if (displayLpTokenLists) {
return (
<LpTokenListsWidget
account={account}
search={inputValue}
onSelectToken={onSelectToken}
openPoolPage={openPoolPage}
disableErc20={disableErc20}
tokenListCategoryState={tokenListCategoryState}
>
{children}
</LpTokenListsWidget>
)
}

return (
<>
<LegacyChainSelector chainsToSelect={chainsToSelect} onSelectChain={onSelectChain} />
<SelectTokenModalContent isRouteAvailable={isRouteAvailable}>{children}</SelectTokenModalContent>
</>
)
}
Comment on lines +41 to +63
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

isRouteAvailable is ignored when displayLpTokenLists is true.
Right now the “route not supported” message (SelectTokenModalContent) is only applied in the non-LP branch; if isRouteAvailable === false during LP view, the warning won’t render. Consider wrapping children with SelectTokenModalContent in both branches (or documenting why LP mode is exempt).

🤖 Prompt for AI Agents
In
apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/TokenColumnContent.tsx
around lines 41 to 63, the isRouteAvailable prop is ignored when
displayLpTokenLists is true so the "route not supported" UI never shows for LP
view; update the LP branch to wrap the children passed into LpTokenListsWidget
with <SelectTokenModalContent
isRouteAvailable={isRouteAvailable}>...</SelectTokenModalContent> (or move
SelectTokenModalContent to wrap the entire returned JSX in both branches) so the
route-availability warning renders consistently; ensure props and children are
preserved and that LegacyChainSelector remains in the non-LP branch as currently
intended.


interface LegacyChainSelectorProps {
chainsToSelect: ChainsToSelectState | undefined
onSelectChain: (chain: ChainInfo) => void
}

function LegacyChainSelector({ chainsToSelect, onSelectChain }: LegacyChainSelectorProps): ReactNode {
if (!chainsToSelect?.chains?.length) {
return null
}

return (
<styledEl.LegacyChainsWrapper>
<ChainsSelector
isLoading={chainsToSelect.isLoading || false}
chains={chainsToSelect.chains}
defaultChainId={chainsToSelect.defaultChainId}
onSelectChain={onSelectChain}
/>
</styledEl.LegacyChainsWrapper>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { ReactNode, useMemo, useState } from 'react'

import { BackButton } from '@cowprotocol/ui'

import { SettingsIcon } from 'modules/trade/pure/Settings'

import * as styledEl from './styled'

import { SelectTokenContext } from '../../types'

import type { SelectTokenModalProps } from './types'

export function useSelectTokenContext(props: SelectTokenModalProps): SelectTokenContext {
const {
selectedToken,
balancesState,
unsupportedTokens,
permitCompatibleTokens,
onSelectToken,
onTokenListItemClick,
account,
tokenListTags,
} = props

return useMemo(
() => ({
balancesState,
selectedToken,
onSelectToken,
onTokenListItemClick,
unsupportedTokens,
permitCompatibleTokens,
tokenListTags,
isWalletConnected: !!account,
}),
[
balancesState,
selectedToken,
onSelectToken,
onTokenListItemClick,
unsupportedTokens,
permitCompatibleTokens,
tokenListTags,
account,
],
)
}

export function useTokenSearchInput(defaultInputValue = ''): [string, (value: string) => void, string] {
const [inputValue, setInputValue] = useState<string>(defaultInputValue)

return [inputValue, setInputValue, inputValue.trim()]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Memoize, maybe?
While the values are strings, it's returning a new array which is not? I might be wrong, not sure I'm mixing things up.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got this assessment on this, let me know if we should add it on this branch or not:

trim() returns a primitive, so deps like [searchInput,…] only rerun when the text actually changes; the tuple is destructured, so array identity doesn’t leak. Memoizing would be optional polish with negligible impact—only cost now is a trivial .trim() per render.

}

interface TitleBarActionsProps {
showManageButton: boolean
onDismiss(): void
onOpenManageWidget(): void
title: string
}

export function TitleBarActions({
showManageButton,
onDismiss,
onOpenManageWidget,
title,
}: TitleBarActionsProps): ReactNode {
return (
<styledEl.TitleBar>
<styledEl.TitleGroup>
<BackButton onClick={onDismiss} />
<styledEl.ModalTitle>{title}</styledEl.ModalTitle>
</styledEl.TitleGroup>
{showManageButton && (
<styledEl.TitleActions>
<styledEl.TitleActionButton
id="token-selector-title-manage-button"
onClick={onOpenManageWidget}
aria-label="Manage token lists"
title="Manage token lists"
Comment on lines +79 to +80
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should they be marked for translation?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but addressed in branch 10 (05327ab9e)

>
<SettingsIcon />
</styledEl.TitleActionButton>
</styledEl.TitleActions>
)}
</styledEl.TitleBar>
)
Comment on lines +62 to +87
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Localize the “Manage token lists” labels (aria-label/title).
These are user-facing strings and currently bypass i18n, which can break the “switch locales” acceptance criteria.

🤖 Prompt for AI Agents
In
apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/helpers.tsx
around lines 62 to 87, the aria-label and title for the manage button are
hardcoded ("Manage token lists"); replace them with localized strings by using
the project's i18n hook (e.g., useTranslation/useI18n) at the top of the
TitleBarActions component, call the translation function to get localized values
for both aria-label and title, and swap the hardcoded literals for those
translated strings; also add corresponding translation keys (e.g.,
tokens.manageLists) to the locale JSON files so all locales have this string.

}
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import { BalancesState } from '@cowprotocol/balances-and-allowances'
import { CHAIN_INFO } from '@cowprotocol/common-const'
import { getRandomInt } from '@cowprotocol/common-utils'
import { SupportedChainId, ChainInfo } from '@cowprotocol/cow-sdk'
import { BigNumber } from '@ethersproject/bignumber'

import styled from 'styled-components/macro'

import { allTokensMock, favoriteTokensMock } from '../../mocks'
import { mapChainInfo } from '../../utils/mapChainInfo'

import { SelectTokenModal, SelectTokenModalProps } from './index'

const Wrapper = styled.div`
max-height: 90vh;
margin: 20px auto;
display: flex;
width: 450px;
width: 520px;
`

const unsupportedTokens = {}
Expand All @@ -26,6 +28,20 @@ const balances = allTokensMock.reduce<BalancesState['values']>((acc, token) => {
return acc
}, {})

const chainsMock: ChainInfo[] = [
SupportedChainId.MAINNET,
SupportedChainId.BASE,
SupportedChainId.ARBITRUM_ONE,
].reduce<ChainInfo[]>((acc, id) => {
const info = CHAIN_INFO[id]

if (info) {
acc.push(mapChainInfo(id, info))
}

return acc
}, [])

const defaultProps: SelectTokenModalProps = {
tokenListTags: {},
account: undefined,
Expand All @@ -35,7 +51,11 @@ const defaultProps: SelectTokenModalProps = {
favoriteTokens: favoriteTokensMock,
areTokensLoading: false,
areTokensFromBridge: false,
chainsToSelect: undefined,
chainsToSelect: {
chains: chainsMock,
isLoading: false,
defaultChainId: SupportedChainId.MAINNET,
},
Comment on lines +54 to +58
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/index.cosmos.tsx | head -80

Repository: cowprotocol/cowswap

Length of output: 2790


🏁 Script executed:

cd apps/cowswap-frontend && grep -n "SEPOLIA\|MAINNET" src/modules/tokensList/pure/SelectTokenModal/index.cosmos.tsx

Repository: cowprotocol/cowswap

Length of output: 238


🏁 Script executed:

cd apps/cowswap-frontend && head -50 src/modules/tokensList/pure/SelectTokenModal/index.tsx

Repository: cowprotocol/cowswap

Length of output: 1499


🏁 Script executed:

cd apps/cowswap-frontend && grep -B5 -A5 "balancesState:" src/modules/tokensList/pure/SelectTokenModal/index.cosmos.tsx

Repository: cowprotocol/cowswap

Length of output: 325


🏁 Script executed:

cd apps/cowswap-frontend && sed -n '31,43p' src/modules/tokensList/pure/SelectTokenModal/index.cosmos.tsx

Repository: cowprotocol/cowswap

Length of output: 326


Fix chain ID mismatch in fixture: defaultChainId (MAINNET) vs balancesState.chainId (SEPOLIA).
The balancesState is set to SEPOLIA, which is not even in the available chains list (chainsMock contains only MAINNET, BASE, ARBITRUM_ONE). This causes inconsistency: the default selectable chain is MAINNET but balances display for an unavailable chain. Either add SEPOLIA to chainsMock and update defaultChainId accordingly, or change balancesState.chainId to MAINNET to match the default.

Also applies to: 66-67

🤖 Prompt for AI Agents
In
apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/index.cosmos.tsx
around lines 54-58 (and also apply same fix to lines 66-67), the fixture has a
chain ID mismatch: defaultChainId is SupportedChainId.MAINNET while
balancesState.chainId is SEPOLIA which is not present in chainsMock. Fix by
making fixtures consistent — either add SEPOLIA to chainsMock and set
defaultChainId to SEPOLIA (and ensure chainsMock ordering includes it), or
change balancesState.chainId to SupportedChainId.MAINNET so it matches the
existing chainsMock and defaultChainId; update any related mock data
accordingly.

onSelectChain(chain: ChainInfo) {
console.log('onSelectChain', chain)
},
Expand All @@ -48,6 +68,7 @@ const defaultProps: SelectTokenModalProps = {
},
selectedToken,
isRouteAvailable: true,
modalTitle: 'Swap from',
recentTokens: favoriteTokensMock.slice(0, 2),
selectedTargetChainId: SupportedChainId.SEPOLIA,
onSelectToken() {
Expand Down
Loading
Loading