Skip to content

feat: new token selector#6790

Merged
limitofzero merged 329 commits intodevelopfrom
feat/token-selector-15
Jan 20, 2026
Merged

feat: new token selector#6790
limitofzero merged 329 commits intodevelopfrom
feat/token-selector-15

Conversation

@limitofzero
Copy link
Copy Markdown
Contributor

@limitofzero limitofzero commented Jan 5, 2026

SelectTokenWidget Architecture

Summary

Refactored the SelectTokenWidget with a modular, slot-based architecture for improved maintainability and extensibility.

Each UI section (Header, Search, TokenList, etc.) is a "slot" with a pure component and connected wrapper - source

image

Component Hierarchy

SelectTokenWidget (index.tsx)
├── SelectTokenModal
│   ├── ConnectedHeader ← useHeaderState()
│   │   └── Header (pure)
│   │
│   ├── ConnectedSearch ← useSearchState()
│   │   └── Search (pure)
│   │
│   ├── NetworkPanel
│   │   ├── ConnectedChainSelector ← useChainPanelState()
│   │   │   └── ChainSelector (pure)
│   │   │       └── MobileChainSelector → ChainChip
│   │   │
│   │   └── ConnectedDesktopChainPanel ← useChainPanelState()
│   │       └── DesktopChainPanel (pure)
│   │           └── ChainPanel
│   │               ├── ChainPanelHeader
│   │               └── ChainsSelector → ChainsList → ChainButton
│   │
│   └── ConnectedTokenList ← useTokenListData()
│       └── TokenList (pure)
│           └── TokensContent → TokensVirtualList
│               ├── TokensVirtualRowRenderer
│               ├── FavoriteTokensList → FavoriteTokenItem
│               └── TokenListItemContainer
│
├── Blocking Views (conditional via useViewWithFlows)
│   ├── ImportTokenView ← useImportTokenViewState()
│   ├── ImportListView ← useImportListViewState()
│   ├── ManageView ← useManageViewState()
│   └── LpTokenView ← useLpTokenViewState()
│
├── Custom Flows (pre/post flow slots)
│   └── ViewFlowConfig → CustomFlowSlot
│
└── MobileChainPanelPortal

Directory Structure

SelectTokenWidget/
├── index.tsx                    # Main entry point
├── styled.ts                    # Styled components
├── MobileChainPanelPortal.tsx   # Mobile chain panel portal
│
├── atoms/
│   └── index.ts                 # selectTokenModalUIAtom
│
├── types/
│   ├── index.ts
│   ├── TokenSelectorView.ts     # View enum (Main, ImportToken, etc.)
│   └── ViewFlowConfig.ts        # Custom flow types
│
├── hooks/                       # Domain hooks
│   ├── useWidgetOpenState.ts
│   ├── useWidgetEffects.ts
│   ├── useViewWithFlows.ts      # Pre/post flow orchestration
│   ├── useActiveBlockingView.ts
│   │
│   ├── useHeaderState.ts        # Header slot state
│   ├── useSearchState.ts        # Search slot state
│   ├── useChainPanelState.ts    # Chain selector state
│   │
│   ├── useImportTokenViewState.ts
│   ├── useImportListViewState.ts
│   ├── useManageViewState.ts
│   ├── useLpTokenViewState.ts
│   │
│   ├── useTokenAdminActions.ts
│   ├── useTokenDataSources.ts
│   ├── useTokenSelectionHandler.ts
│   ├── useWidgetMetadata.ts
│   │
│   ├── useImportTokenAndClose.ts
│   ├── useImportListAndBack.ts
│   ├── useResetTokenImport.ts
│   ├── useResetListImport.ts
│   │
│   ├── useRecentTokenSection.ts
│   ├── useManageWidgetVisibility.ts
│   ├── useDismissHandler.ts
│   └── usePoolPageHandlers.ts
│
└── internal/
    ├── index.ts
    ├── SelectTokenModal.tsx
    └── slots/
        ├── index.ts
        │
        ├── Header.tsx / ConnectedHeader.tsx
        ├── Search.tsx / ConnectedSearch.tsx
        ├── ChainSelector.tsx / ConnectedChainSelector.tsx
        ├── DesktopChainPanel.tsx / ConnectedDesktopChainPanel.tsx
        ├── TokenList.tsx / ConnectedTokenList.tsx
        ├── NetworkPanel.tsx
        │
        ├── ImportTokenView.tsx
        ├── ImportListView.tsx
        ├── ManageView.tsx
        └── LpTokenView.tsx

pure/                            # Pure presentational components
├── ChainPanel/
│   ├── index.tsx
│   ├── ChainPanelHeader.tsx
│   └── chainPanelUtils.ts
│
├── ChainsSelector/
│   ├── ChainsSelector.tsx
│   ├── ChainsList.tsx
│   ├── ChainButton.tsx
│   ├── ChainsLoadingList.tsx
│   └── getChainAccent.ts
│
├── FavoriteTokensList/
│   ├── index.tsx
│   ├── FavoriteTokenItem.tsx
│   └── FavoriteTokensTooltip.tsx
│
├── TokensVirtualList/
│   ├── index.tsx
│   ├── types.ts
│   ├── TokensVirtualRowRenderer.tsx
│   └── tokensVirtualListUtils.ts
│
├── ImportTokenModal/
├── ImportListModal/
├── LpTokenLists/
├── TokenListItem/
├── TokenSearchContent/
└── ...

hooks/                           # Shared hooks
├── useRecentTokens.ts
├── useRecentTokensStorage.ts
├── useHydratedRecentTokens.ts
├── useChainsToSelect.ts
├── useSourceChainId.ts
├── useTokenListData.ts
├── useSelectTokenContext.ts
└── ...

utils/                           # Pure utility functions
├── chainsState.ts
├── recentTokensStorage.ts
├── sortChainsByDisplayOrder.ts
└── tokensListSorter.ts

Data Flow

selectTokenWidgetAtom (Jotai) ─────────────────────────────────────┐
        │                                                          │
        ▼                                                          ▼
   Domain Hooks                                          customFlowsRegistryAtom
   (useHeaderState, useSearchState, etc.)                          │
        │                                                          │
        ▼                                                          ▼
Connected Components                                    useViewWithFlows()
(ConnectedHeader, ConnectedSearch, etc.)                           │
        │                                                          │
        ▼                                                          ▼
  Pure Components                                      Pre/Post Flow Slots
(Header, Search, TokenList, etc.)                    (ConsentModals, etc.)

Custom Flows System

External modules can inject custom UI flows via customFlows prop:

interface ViewFlowConfig<TView extends TokenSelectorView> {
  preFlow?: CustomFlowSlot<TView>   // Shown before base view
  postFlow?: CustomFlowSlot<TView>  // Shown after base view
}

type CustomFlowResult<TView> = {
  content: ReactNode | null
  data?: Partial<ViewPropsMap[TView]>
}

Example usage (RWA consent flow):

<SelectTokenWidget
  customFlows={{
    [TokenSelectorView.ImportToken]: {
      preFlow: () => ({ content: <ConsentModal />, data: { restriction } })
    }
  }}
/>

To test

PREVIEW LINK: https://swap-dev-git-feat-token-selector-15-cowswap-dev.vercel.app/

Everything should work as for current token selector but with new UI/UX -> token list/search/bridging/network switching

Summary by CodeRabbit

Release Notes

  • New Features

    • Added bridge route availability checking for cross-chain swaps with loading indicators
    • Added recent tokens tracking and quick access in token selector
    • Enhanced chain/network selection UI with improved mobile responsiveness
    • Added token import consent flows for compliance requirements
  • Improvements

    • Refactored token selector modal with responsive slot-based design
    • Better token list organization with favorites and recent sections
    • Improved network unavailability messaging during cross-chain operations

✏️ Tip: You can customize this high-level summary in your review settings.

fairlighteth and others added 30 commits December 3, 2025 15:23
})
}

interface EmptyStateFlagsParams {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nitpick, one wise man taught me to sort entities in file. Types declarations should be in the beginning of the file.

import { useRecentTokenSection } from '../containers/SelectTokenWidget/hooks/useRecentTokenSection'
import { SelectTokenContext } from '../types'

export interface TokenListData {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nitpick, it's better to be consistent in namings. TokenListData -> TokenListContext, since we have SelectTokenContext and others

maxItems?: number
}

export function useRecentTokensStorage({
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Would be great to extract side-effects from this hook.
In this hook we should only assemble and return RecentTokensStorageState (SRP).
Side-effects can go to a new updater.

const tradeTypeInfo = tradeTypeInfoFromState ?? tradeTypeInfoFromUrl
const tradeType = tradeTypeInfo?.tradeType
// Advanced trades lock the target chain so price guarantees stay valid while the widget is open.
const shouldLockTargetChain = tradeType === TradeType.LIMIT_ORDER || tradeType === TradeType.ADVANCED_ORDERS
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nitpick, I don't really like using tradeType in modules/tokenLists.
Would be cleaner, if we add another flag to SelectTokenWidgetState and only set it as true in limit and advanced widgets.

export function useOnSelectChain(): OnSelectChainHandler {
const updateSelectTokenWidget = useUpdateSelectTokenWidgetState()
const widgetState = useSelectTokenWidgetState()
const shouldForceOpen =
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

const isBridgingEnabled = useIsBridgingEnabled()
const isBridgingEnabled = useIsBridgingEnabled() // Reads from Jotai atom
const availableChains = useAvailableChains()
const isAdvancedTradeType = tradeType === TradeType.LIMIT_ORDER || tradeType === TradeType.ADVANCED_ORDERS
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

/**
* Registry mapping view names to their custom flow configurations.
*/
export type CustomFlowsRegistry = {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Looks fancy

const chainsPanelTitle =
field === Field.INPUT ? t`From network` : field === Field.OUTPUT ? t`To network` : t`Select network`

return { disableErc20, tokenListCategoryState, modalTitle, chainsPanelTitle }
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

useMemo


const shouldSwitch =
field === Field.INPUT &&
(tradeType === TradeType.LIMIT_ORDER || tradeType === TradeType.ADVANCED_ORDERS) &&
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Commented similar thing before

})
const importTokenCallback = useAddUserToken()

return { addCustomTokenLists, importTokenCallback }
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

useMemo

updateSelectTokenWidget({ selectedPoolAddress: undefined })
}, [updateSelectTokenWidget])

return { openPoolPage, closePoolPage }
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

useMemo

const openManageWidget = useCallback(() => updateModalUI({ isManageWidgetOpen: true }), [updateModalUI])
const closeManageWidget = useCallback(() => updateModalUI({ isManageWidgetOpen: false }), [updateModalUI])

return { isManageWidgetOpen, openManageWidget, closeManageWidget }
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

useMemo


if (!isManageWidgetOpen) return null

return {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

useMemo


if (!widgetState.selectedPoolAddress) return null

return {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

useMemo

I'm commenting it everywhere just to not forget about it

const onSelectChain = useOnSelectChain()
const isBridgeFeatureEnabled = useIsBridgingEnabled()

const shouldDisableForYield = tradeType === TradeType.YIELD && !ENABLE_YIELD_CHAIN_PANEL
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It's better to add a flag to SelectTokenWidgetState instead of using tradeType here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

there are multiple cases of usage tradeType in widget, not only for TradeType.YIELD type

I can replace it by few flags, at least 3

isManageWidgetOpen: false,
}

export const { atom: selectTokenModalUIAtom, updateAtom: updateSelectTokenModalUIAtom } = atomWithPartialUpdate(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nitpick, we used to call such directories state instead of atom

Comment thread apps/cowswap-frontend/src/modules/rwa/hooks/useImportTokenWithConsent.ts Outdated
Copy link
Copy Markdown
Contributor

@elena-zh elena-zh left a comment

Choose a reason for hiding this comment

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

Looks good, thank you

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@apps/cowswap-frontend/src/locales/en-US.po`:
- Around line 1755-1757: Replace the inconsistent "Receiver" copy with the
existing "Recipient" wording across the locale and component usage: update the
translation entry in apps/cowswap-frontend/src/locales/en-US.po where msgid
"Receiver" / msgstr "Receiver" and any duplicate entries (also noted around the
second occurrence) to use "Recipient"; then update the component that references
this string (apps/cowswap-frontend/src/common/pure/ReceiverInfo/index.tsx) to
use the "Recipient" key/text so the UI consistently shows "Recipient" instead of
"Receiver" (and similarly change any "to receiver" phrasing to "to recipient").

In
`@apps/cowswap-frontend/src/modules/tokensList/state/recentTokensStorageAtom.ts`:
- Around line 5-8: The doc is wrong: the localStorage read happens at module
load, not on first atom read; either update the comment above
recentTokensStorageAtom to state it is initialized at module import, or change
the implementation so the read is lazy — e.g., move the localStorage read into
the atom's default initializer (or a selector/getter used by
recentTokensStorageAtom) so that getRecentTokensFromStorage() (or the
localStorage access currently at module scope) runs only when the atom is first
read; update the comment to match whichever approach you choose and reference
recentTokensStorageAtom and the current storage-read helper name in the change.
♻️ Duplicate comments (4)
apps/cowswap-frontend/src/locales/en-US.po (4)

666-669: Plural edge case for “View all … networks”.

This was already raised earlier; still applies if {totalChains} can be 1.

Also applies to: 1954-1957


3256-3259: Network vs chain terminology consistency in availability/route messages.

Previously flagged copy consistency issue; still applies.

Also applies to: 4619-4622, 5021-5023, 5519-5521


6185-6187: “Clear” label for recents could be more explicit.

Previously flagged suggestion (e.g., “Clear recents”).


2383-2386: Hyphenation for “Cross‑chain swap”.

Previously flagged; still applies if this entry is kept.

🧹 Nitpick comments (3)
apps/cowswap-frontend/src/modules/tokensList/hooks/useRecentTokens.test.tsx (3)

96-114: Test logic is valid but could be more explicit about what's being tested.

This test verifies chain isolation, but since allTokens is empty, the result would be empty even if the chain matched (because hydration requires a matching token in allTokens). Consider adding a token to allTokens for the GNOSIS_CHAIN to make the test more explicit about testing chain filtering rather than the hydration/matching logic.

That said, the current test still passes and achieves its goal of verifying no cross-chain leakage at the storage level.

💡 Optional: Make the test more explicit
 it('does not return tokens from other chains', () => {
+  const gnosisToken = createTestToken(SupportedChainId.GNOSIS_CHAIN, ADDRESS_1, 'TKN')
+
   setStoredTokens({
     [SupportedChainId.GNOSIS_CHAIN]: [createStoredToken(SupportedChainId.GNOSIS_CHAIN, ADDRESS_1, 'TKN')],
   })
   const store = createStoreWithLocalStorage()
   const wrapper = createTestWrapper(store)

   const { result } = renderHook(
     () =>
       useRecentTokens({
-        allTokens: [],
+        allTokens: [gnosisToken],
         favoriteTokens: [],
         activeChainId: SupportedChainId.MAINNET,
       }),
     { wrapper },
   )

   expect(result.current.recentTokens).toEqual([])
 })

220-249: Consider verifying token order when testing maxItems limit.

The test correctly verifies that only 2 tokens are returned when maxItems: 2, but it doesn't verify which tokens are kept. For "recent tokens" functionality, the order typically matters (most recently added should be retained). Consider adding an assertion to verify the expected tokens are present.

💡 Optional: Verify which tokens are retained
   const { result } = renderHook(
     () =>
       useRecentTokens({
         allTokens: tokens,
         favoriteTokens: [],
         activeChainId: SupportedChainId.MAINNET,
         maxItems: 2,
       }),
     { wrapper },
   )

   expect(result.current.recentTokens).toHaveLength(2)
+  // Verify that the most recent tokens are kept (assuming T1, T2 are stored first)
+  const returnedSymbols = result.current.recentTokens.map(t => t.symbol)
+  expect(returnedSymbols).toEqual(['T1', 'T2']) // or ['T2', 'T3'] depending on ordering semantics
 })

51-250: Good foundational test coverage.

The test suite covers the core functionality well. For even more robust coverage, consider adding tests for:

  1. Chain switching behavior: Re-rendering with a different activeChainId and verifying tokens update accordingly
  2. localStorage persistence: Verifying that addRecentToken actually persists to localStorage (not just Jotai state)
  3. Duplicate handling: Adding the same token twice and verifying it doesn't create duplicates (or moves to front if that's the expected behavior)

These are optional enhancements that can be deferred if time-constrained.

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a99d6f5 and effda5b.

📒 Files selected for processing (13)
  • apps/cowswap-frontend/src/locales/en-US.po
  • apps/cowswap-frontend/src/modules/application/containers/App/Updaters.tsx
  • apps/cowswap-frontend/src/modules/tokensList/containers/TokenSearchResults/index.tsx
  • apps/cowswap-frontend/src/modules/tokensList/hooks/useRecentTokens.test.tsx
  • apps/cowswap-frontend/src/modules/tokensList/hooks/useRecentTokensStorage.test.tsx
  • apps/cowswap-frontend/src/modules/tokensList/hooks/useRecentTokensStorage.ts
  • apps/cowswap-frontend/src/modules/tokensList/hooks/useTokenListContext.ts
  • apps/cowswap-frontend/src/modules/tokensList/index.ts
  • apps/cowswap-frontend/src/modules/tokensList/pure/TokensContent/index.tsx
  • apps/cowswap-frontend/src/modules/tokensList/pure/TokensVirtualList/index.tsx
  • apps/cowswap-frontend/src/modules/tokensList/state/recentTokensStorageAtom.ts
  • apps/cowswap-frontend/src/modules/tokensList/state/tokenListViewAtom.ts
  • apps/cowswap-frontend/src/modules/tokensList/updaters/RecentTokensStorageUpdater.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/cowswap-frontend/src/modules/tokensList/hooks/useRecentTokensStorage.ts
🧰 Additional context used
🧠 Learnings (13)
📚 Learning: 2025-08-08T13:55:17.528Z
Learnt from: shoom3301
Repo: cowprotocol/cowswap PR: 6125
File: libs/tokens/src/state/tokens/allTokensAtom.ts:78-78
Timestamp: 2025-08-08T13:55:17.528Z
Learning: In libs/tokens/src/state/tokens/allTokensAtom.ts (TypeScript/Jotai), the team prefers to wait for token lists to initialize (listsStatesListAtom non-empty) before returning tokens. No fallback to favorites/user-added/native tokens should be used when listsStatesList is empty.

Applied to files:

  • apps/cowswap-frontend/src/modules/tokensList/state/recentTokensStorageAtom.ts
  • apps/cowswap-frontend/src/modules/tokensList/hooks/useTokenListContext.ts
  • apps/cowswap-frontend/src/modules/tokensList/containers/TokenSearchResults/index.tsx
  • apps/cowswap-frontend/src/modules/tokensList/index.ts
  • apps/cowswap-frontend/src/modules/tokensList/state/tokenListViewAtom.ts
  • apps/cowswap-frontend/src/modules/tokensList/hooks/useRecentTokensStorage.test.tsx
  • apps/cowswap-frontend/src/modules/tokensList/updaters/RecentTokensStorageUpdater.tsx
  • apps/cowswap-frontend/src/modules/tokensList/pure/TokensContent/index.tsx
  • apps/cowswap-frontend/src/modules/tokensList/pure/TokensVirtualList/index.tsx
  • apps/cowswap-frontend/src/modules/tokensList/hooks/useRecentTokens.test.tsx
📚 Learning: 2025-08-08T13:56:18.009Z
Learnt from: shoom3301
Repo: cowprotocol/cowswap PR: 6125
File: libs/tokens/src/updaters/TokensListsUpdater/index.tsx:29-31
Timestamp: 2025-08-08T13:56:18.009Z
Learning: In libs/tokens/src/updaters/TokensListsUpdater/index.tsx, the project’s current Jotai version requires using `unstable_getOnInit` (not `getOnInit`) in atomWithStorage options; keep `{ unstable_getOnInit: true }` until Jotai is upgraded.

Applied to files:

  • apps/cowswap-frontend/src/modules/tokensList/state/recentTokensStorageAtom.ts
  • apps/cowswap-frontend/src/modules/tokensList/index.ts
  • apps/cowswap-frontend/src/modules/tokensList/state/tokenListViewAtom.ts
  • apps/cowswap-frontend/src/modules/tokensList/hooks/useRecentTokensStorage.test.tsx
  • apps/cowswap-frontend/src/modules/tokensList/updaters/RecentTokensStorageUpdater.tsx
  • apps/cowswap-frontend/src/modules/tokensList/pure/TokensContent/index.tsx
  • apps/cowswap-frontend/src/modules/tokensList/pure/TokensVirtualList/index.tsx
  • apps/cowswap-frontend/src/modules/tokensList/hooks/useRecentTokens.test.tsx
  • apps/cowswap-frontend/src/modules/application/containers/App/Updaters.tsx
📚 Learning: 2025-08-12T06:33:19.348Z
Learnt from: shoom3301
Repo: cowprotocol/cowswap PR: 6137
File: libs/tokens/src/state/tokens/allTokensAtom.ts:34-65
Timestamp: 2025-08-12T06:33:19.348Z
Learning: In libs/tokens/src/state/tokens/allTokensAtom.ts, the parseTokenInfo() function returns a new instance of TokenInfo each time, making it safe to mutate properties like lpTokenProvider on the returned object without side effects.

Applied to files:

  • apps/cowswap-frontend/src/modules/tokensList/state/recentTokensStorageAtom.ts
  • apps/cowswap-frontend/src/modules/tokensList/state/tokenListViewAtom.ts
  • apps/cowswap-frontend/src/modules/tokensList/hooks/useRecentTokensStorage.test.tsx
  • apps/cowswap-frontend/src/modules/tokensList/updaters/RecentTokensStorageUpdater.tsx
  • apps/cowswap-frontend/src/modules/tokensList/pure/TokensContent/index.tsx
📚 Learning: 2025-12-30T18:49:03.377Z
Learnt from: limitofzero
Repo: cowprotocol/cowswap PR: 6770
File: apps/cowswap-frontend/src/modules/tokensList/hooks/useAddListImport.ts:27-31
Timestamp: 2025-12-30T18:49:03.377Z
Learning: In apps/cowswap-frontend/src/modules/tokensList/hooks/useAddListImport.ts, when restrictedLists.isLoaded is false or geoStatus.isLoading is true, the code intentionally proceeds with the import immediately without blocking. This allows imports to proceed during loading states, deferring consent checks to trade time when necessary data isn't yet available.

Applied to files:

  • apps/cowswap-frontend/src/modules/tokensList/hooks/useTokenListContext.ts
  • apps/cowswap-frontend/src/modules/tokensList/index.ts
  • apps/cowswap-frontend/src/modules/tokensList/state/tokenListViewAtom.ts
  • apps/cowswap-frontend/src/modules/tokensList/pure/TokensContent/index.tsx
  • apps/cowswap-frontend/src/modules/application/containers/App/Updaters.tsx
  • apps/cowswap-frontend/src/locales/en-US.po
📚 Learning: 2025-09-19T11:38:59.206Z
Learnt from: fairlighteth
Repo: cowprotocol/cowswap PR: 6232
File: apps/cowswap-frontend/src/modules/tokensList/pure/ChainsSelector/index.tsx:199-200
Timestamp: 2025-09-19T11:38:59.206Z
Learning: The makeBuildClickEvent function in apps/cowswap-frontend/src/modules/tokensList/pure/ChainsSelector/index.tsx takes five parameters: defaultChainId, contextLabel, mode, isSwapMode, and chainsCount. The chainsCount parameter is used to determine the CrossChain flag in analytics events.

Applied to files:

  • apps/cowswap-frontend/src/modules/tokensList/hooks/useTokenListContext.ts
  • apps/cowswap-frontend/src/modules/tokensList/index.ts
  • apps/cowswap-frontend/src/modules/tokensList/pure/TokensContent/index.tsx
  • apps/cowswap-frontend/src/modules/tokensList/pure/TokensVirtualList/index.tsx
  • apps/cowswap-frontend/src/locales/en-US.po
📚 Learning: 2025-08-12T06:33:19.348Z
Learnt from: shoom3301
Repo: cowprotocol/cowswap PR: 6137
File: libs/tokens/src/state/tokens/allTokensAtom.ts:34-65
Timestamp: 2025-08-12T06:33:19.348Z
Learning: In libs/tokens/src/utils/parseTokenInfo.ts, the parseTokenInfo() function returns a new instance of TokenInfo using object spread syntax ({ ...token, ... }), making it safe to mutate properties like lpTokenProvider on the returned object without side effects.

Applied to files:

  • apps/cowswap-frontend/src/modules/tokensList/hooks/useTokenListContext.ts
  • apps/cowswap-frontend/src/modules/tokensList/pure/TokensContent/index.tsx
📚 Learning: 2025-03-25T10:03:35.157Z
Learnt from: alfetopito
Repo: cowprotocol/cowswap PR: 5532
File: libs/common-hooks/src/useOnScroll.tsx:22-38
Timestamp: 2025-03-25T10:03:35.157Z
Learning: In React useEffect hooks, refs (RefObjects) should generally be included in dependency arrays even though changes to ref.current don't trigger effects. This is because the ref object itself could change between renders (especially in custom hooks where refs are passed as parameters), and it follows React's best practice of including all external values used in the effect.

Applied to files:

  • apps/cowswap-frontend/src/modules/tokensList/hooks/useTokenListContext.ts
📚 Learning: 2025-10-10T20:28:16.565Z
Learnt from: fairlighteth
Repo: cowprotocol/cowswap PR: 6347
File: apps/cowswap-frontend/src/modules/trade/pure/TradeConfirmation/index.tsx:49-49
Timestamp: 2025-10-10T20:28:16.565Z
Learning: In apps/cowswap-frontend/src/modules/trade, TradeConfirmation follows a two-layer architecture: TradeConfirmationView (pure/stateless) in pure/TradeConfirmation/index.tsx renders the UI, while TradeConfirmation (container) in containers/TradeConfirmation/index.tsx wraps it to freeze props during pending trades (via useStableTradeConfirmationProps), wire in signing state (useSigningStep), and inject trade confirmation state (useTradeConfirmState). Consuming modules should import the container TradeConfirmation from 'modules/trade' to preserve this stateful behavior.
<!-- [add_learning]
When reviewing component refactoring in apps/cowswap-frontend/src/modules/trade, recognize the pattern where a pure view component (e.g., TradeConfirmationView) is separated from a stateful container (e.g., TradeConfirmation) that wraps it. The container adds runtime state management (prop stabilization, signing state, etc.) while the view remains testable and composable. Do not flag usages that import th...

Applied to files:

  • apps/cowswap-frontend/src/modules/tokensList/containers/TokenSearchResults/index.tsx
  • apps/cowswap-frontend/src/modules/tokensList/index.ts
  • apps/cowswap-frontend/src/modules/tokensList/state/tokenListViewAtom.ts
  • apps/cowswap-frontend/src/modules/tokensList/pure/TokensContent/index.tsx
  • apps/cowswap-frontend/src/modules/application/containers/App/Updaters.tsx
  • apps/cowswap-frontend/src/locales/en-US.po
📚 Learning: 2025-09-25T08:48:53.495Z
Learnt from: shoom3301
Repo: cowprotocol/cowswap PR: 6299
File: apps/cowswap-frontend/src/entities/bridgeProvider/useBridgeSupportedNetworks.test.tsx:58-60
Timestamp: 2025-09-25T08:48:53.495Z
Learning: In the cowswap-frontend codebase, when writing SWR tests, the team prefers maximum test isolation by using `provider: () => new Map()` in SWRConfig wrappers, even if it recreates cache on every render, to ensure tests don't share any state.

Applied to files:

  • apps/cowswap-frontend/src/modules/tokensList/hooks/useRecentTokensStorage.test.tsx
  • apps/cowswap-frontend/src/modules/tokensList/hooks/useRecentTokens.test.tsx
📚 Learning: 2025-09-25T08:49:32.256Z
Learnt from: shoom3301
Repo: cowprotocol/cowswap PR: 6299
File: apps/cowswap-frontend/src/entities/bridgeProvider/useBridgeSupportedNetworks.test.tsx:62-67
Timestamp: 2025-09-25T08:49:32.256Z
Learning: In the cowswap-frontend codebase, when testing hooks that use multiple bridge providers, both providers are always properly mocked as complete jest.Mocked<BridgeProvider<BridgeQuoteResult>> objects with all required methods stubbed, ensuring no undefined returns that could break the hook logic.

Applied to files:

  • apps/cowswap-frontend/src/modules/tokensList/hooks/useRecentTokensStorage.test.tsx
  • apps/cowswap-frontend/src/modules/tokensList/hooks/useRecentTokens.test.tsx
📚 Learning: 2025-07-24T16:42:53.154Z
Learnt from: cowdan
Repo: cowprotocol/cowswap PR: 6009
File: apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/HighFeeWarning/HighFeeWarningTooltipContent.tsx:23-33
Timestamp: 2025-07-24T16:42:53.154Z
Learning: In apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/HighFeeWarning/HighFeeWarningTooltipContent.tsx, the use of toFixed(2) for percentage formatting in tooltip content is intentional and differs from the banner message formatting that uses toSignificant(2, undefined, Rounding.ROUND_DOWN). This formatting difference serves different UX purposes and should not be flagged as inconsistent.

Applied to files:

  • apps/cowswap-frontend/src/modules/tokensList/pure/TokensVirtualList/index.tsx
  • apps/cowswap-frontend/src/locales/en-US.po
📚 Learning: 2025-06-25T07:28:32.570Z
Learnt from: shoom3301
Repo: cowprotocol/cowswap PR: 5881
File: apps/cowswap-frontend/src/modules/cowShed/containers/ProxyRecipient/index.tsx:68-72
Timestamp: 2025-06-25T07:28:32.570Z
Learning: In the ProxyRecipient component (apps/cowswap-frontend/src/modules/cowShed/containers/ProxyRecipient/index.tsx), throwing an error when recipient address doesn't match proxy address is intentional design choice to prevent proceeding with incorrect data and ensure data integrity.

Applied to files:

  • apps/cowswap-frontend/src/locales/en-US.po
📚 Learning: 2025-02-20T15:59:33.749Z
Learnt from: shoom3301
Repo: cowprotocol/cowswap PR: 5443
File: apps/cowswap-frontend/src/modules/swap/containers/ConfirmSwapModalSetup/index.tsx:71-71
Timestamp: 2025-02-20T15:59:33.749Z
Learning: The swap module in apps/cowswap-frontend/src/modules/swap/ is marked for deletion in PR `#5444` as part of the swap widget unification effort.

Applied to files:

  • apps/cowswap-frontend/src/locales/en-US.po
🧬 Code graph analysis (6)
apps/cowswap-frontend/src/modules/tokensList/state/recentTokensStorageAtom.ts (1)
apps/cowswap-frontend/src/modules/tokensList/utils/recentTokensStorage.ts (3)
  • StoredRecentTokensByChain (19-19)
  • readStoredTokens (66-92)
  • RECENT_TOKENS_LIMIT (4-4)
apps/cowswap-frontend/src/modules/tokensList/hooks/useTokenListContext.ts (6)
libs/common-const/src/types.ts (1)
  • TokenWithLogo (6-36)
apps/cowswap-frontend/src/modules/tokensList/types.ts (1)
  • SelectTokenContext (13-24)
libs/wallet/src/api/hooks.ts (1)
  • useWalletInfo (24-26)
apps/cowswap-frontend/src/modules/tokensList/hooks/useTokensToSelect.ts (1)
  • useTokensToSelect (24-64)
apps/cowswap-frontend/src/modules/tokensList/hooks/useSelectTokenContext.ts (1)
  • useSelectTokenContext (16-45)
libs/common-utils/src/isInjectedWidget.ts (1)
  • isInjectedWidget (5-14)
apps/cowswap-frontend/src/modules/tokensList/state/tokenListViewAtom.ts (1)
apps/cowswap-frontend/src/modules/tokensList/containers/SelectTokenWidget/atoms/index.ts (1)
  • atom (19-21)
apps/cowswap-frontend/src/modules/tokensList/updaters/RecentTokensStorageUpdater.tsx (3)
apps/cowswap-frontend/src/modules/tokensList/index.ts (1)
  • RecentTokensStorageUpdater (16-16)
apps/cowswap-frontend/src/modules/tokensList/state/recentTokensStorageAtom.ts (1)
  • recentTokensStorageAtom (9-9)
apps/cowswap-frontend/src/modules/tokensList/utils/recentTokensStorage.ts (2)
  • persistStoredTokens (94-104)
  • StoredRecentTokensByChain (19-19)
apps/cowswap-frontend/src/modules/tokensList/pure/TokensContent/index.tsx (5)
apps/cowswap-frontend/src/modules/tokensList/hooks/useTokenListViewState.ts (1)
  • useTokenListViewState (5-7)
apps/cowswap-frontend/src/modules/tokensList/hooks/useTokenListContext.ts (1)
  • useTokenListContext (40-74)
libs/ui/src/pure/Loader/index.tsx (1)
  • Loader (32-43)
apps/cowswap-frontend/src/modules/tokensList/containers/TokenSearchResults/index.tsx (1)
  • TokenSearchResults (13-76)
apps/cowswap-frontend/src/modules/tokensList/pure/TokensVirtualList/index.tsx (1)
  • TokensVirtualList (24-68)
apps/cowswap-frontend/src/modules/tokensList/pure/TokensVirtualList/index.tsx (5)
libs/common-const/src/types.ts (1)
  • TokenWithLogo (6-36)
apps/cowswap-frontend/src/modules/tokensList/hooks/useTokenListContext.ts (1)
  • useTokenListContext (40-74)
apps/cowswap-frontend/src/modules/tokensList/pure/TokensVirtualList/tokensVirtualListUtils.ts (2)
  • sortTokensByBalance (13-33)
  • buildVirtualRows (43-72)
apps/cowswap-frontend/src/modules/tokensList/pure/TokensVirtualList/types.ts (1)
  • TokensVirtualRow (3-6)
apps/cowswap-frontend/src/modules/tokensList/pure/TokensVirtualList/TokensVirtualRowRenderer.tsx (1)
  • TokensVirtualRowRenderer (15-35)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Setup
  • GitHub Check: Cypress
🔇 Additional comments (14)
apps/cowswap-frontend/src/modules/tokensList/state/tokenListViewAtom.ts (1)

1-21: Looks good: minimal, focused UI state atom.

Cleanly scoped to local UI state and consistent with the existing atomWithPartialUpdate pattern.

apps/cowswap-frontend/src/modules/tokensList/hooks/useRecentTokens.test.tsx (3)

1-17: LGTM!

Imports are well-organized with clear separation between external packages and internal modules. All imported items appear to be used in the test file.


19-49: Well-structured test utilities.

The helper functions provide good abstraction for test setup. The pattern of creating a fresh Jotai store per test with createStoreWithLocalStorage() ensures proper test isolation. The beforeEach cleanup of localStorage (line 53) prevents state leakage between tests.


116-218: LGTM!

Good test coverage for:

  • Filtering favorite tokens from recent tokens list (with proper case-insensitive address comparison)
  • Adding tokens via addRecentToken
  • Guard against adding favorite tokens to recent list
  • Clearing tokens for the active chain

Proper use of act() for all state mutations ensures React state updates are flushed before assertions.

apps/cowswap-frontend/src/modules/tokensList/updaters/RecentTokensStorageUpdater.tsx (1)

15-53: LGTM — persistence + favorite cleanup look well-contained.

apps/cowswap-frontend/src/modules/application/containers/App/Updaters.tsx (1)

36-121: LGTM — updater wiring is consistent with existing pattern.

apps/cowswap-frontend/src/modules/tokensList/hooks/useRecentTokensStorage.test.tsx (1)

57-303: LGTM — solid coverage for storage, ordering, and chain isolation.

apps/cowswap-frontend/src/modules/tokensList/index.ts (1)

2-30: LGTM — export surface additions are clear and additive.

apps/cowswap-frontend/src/locales/en-US.po (2)

282-286: LGTM: new token‑selector labels and search/selection strings.

Copy reads cleanly and matches expected UI context.

Also applies to: 304-306, 646-648, 872-874, 884-887, 1006-1008, 1441-1443, 2136-2138, 2607-2610, 3925-3927, 4409-4412, 4571-4574, 5110-5112, 5268-5271, 5401-5403, 5592-5597


1292-1293: No additional feedback on these obsolete (#~) entries.

Also applies to: 2180-2183, 3658-3661, 4384-4387, 6263-6265

apps/cowswap-frontend/src/modules/tokensList/containers/TokenSearchResults/index.tsx (1)

13-58: Nice hook-based refactor and Enter handling.

The single-match Enter path now updates recent tokens before selection, and the bridge filtering logic stays clean.

apps/cowswap-frontend/src/modules/tokensList/pure/TokensContent/index.tsx (1)

12-68: Pinned-recents filtering looks solid.

The Set-based pinning removes recent tokens from the main list while preserving favorites for balance sorting. Clean and readable.

apps/cowswap-frontend/src/modules/tokensList/pure/TokensVirtualList/index.tsx (1)

17-66: Virtual row pipeline looks good.

The sorted-tokens + buildVirtualRows flow and chain-keyed VirtualList reset behavior fit the refactor well.

apps/cowswap-frontend/src/modules/tokensList/hooks/useTokenListContext.ts (1)

40-72: No action needed—the code already handles undefined chain IDs safely.

The activeChainId parameter is optional throughout the chain, and the implementation already accounts for this:

  • Recent tokens are stored using token.chainId (from the selected token), not activeChainId
  • useHydratedRecentTokens safely returns an empty array if activeChainId is undefined
  • clearRecentTokens checks if (activeChainId) before clearing

No fallback or additional guards are required.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment thread apps/cowswap-frontend/src/locales/en-US.po
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants