Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
a960e98
feat: implement consent for all forms
limitofzero Dec 25, 2025
9bad188
feat: add cache for restricted token list
limitofzero Dec 25, 2025
8ac48eb
chore: remove unused
limitofzero Dec 25, 2025
94afb02
chore: remove unused
limitofzero Dec 25, 2025
b292fb2
feat: remove i18n for consents
limitofzero Dec 25, 2025
4e98cf3
feat: show conesnts before import modal
limitofzero Dec 25, 2025
22720a1
feat: add i18n for block reason
limitofzero Dec 25, 2025
1785f97
refactor: reuse a hook
limitofzero Dec 25, 2025
e5e2c2d
feat: don't allow to user switch on restricted token list for blocked…
limitofzero Dec 25, 2025
f8d0e91
feat: add block to import restricted list
limitofzero Dec 26, 2025
c3df4e4
Merge branch 'develop' into feat/implement-consents-2
limitofzero Dec 26, 2025
63a2178
Merge branch 'feat/implement-consents-2' into feat/implement-consent-…
limitofzero Dec 26, 2025
b3227da
Merge branch 'feat/implement-consent-for-token-importing' of github.c…
limitofzero Dec 26, 2025
7d2a497
feat: consent flow for token lists
limitofzero Dec 29, 2025
5a60f7d
test: add tests for hooks
limitofzero Dec 29, 2025
0a86fc8
fix: build
limitofzero Dec 30, 2025
7d40283
refactor: decompose component
limitofzero Dec 30, 2025
ac427ea
fix: don't hide list if the consent required
limitofzero Dec 30, 2025
0127f82
refactor: use helper for keys
limitofzero Dec 30, 2025
911160a
test: fix cases
limitofzero Dec 30, 2025
fe662c9
fix: don't require consent when wallet is disconnected
limitofzero Dec 30, 2025
2625c84
fix: merge conflicts
limitofzero Dec 30, 2025
a752b85
fix: image size in importing
limitofzero Dec 30, 2025
7b7a567
refactor: small improvements
limitofzero Dec 30, 2025
2c31147
fix: types
limitofzero Dec 30, 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
16 changes: 16 additions & 0 deletions apps/cowswap-frontend/src/locales/en-US.po
Original file line number Diff line number Diff line change
Expand Up @@ -2819,6 +2819,10 @@ msgstr "Native currency (e.g ETH)"
msgid "<0><1/> Experimental:</0> Add DeFI interactions before and after your trade."
msgstr "<0><1/> Experimental:</0> Add DeFI interactions before and after your trade."

#: apps/cowswap-frontend/src/modules/tokensList/pure/ImportListModal/index.tsx
msgid "This token list is not available in your region."
msgstr "This token list is not available in your region."

#: apps/cowswap-frontend/src/common/pure/ReceiveAmountInfo/index.tsx
msgid "Protocol fee"
msgstr "Protocol fee"
Expand Down Expand Up @@ -3596,6 +3600,10 @@ msgstr "CoW Protocol covers the fees and costs by executing your order at a slig
msgid "With hooks you can add specific actions <0>before</0> and <1>after</1> your swap."
msgstr "With hooks you can add specific actions <0>before</0> and <1>after</1> your swap."

#: apps/cowswap-frontend/src/modules/tokensList/hooks/useRestrictedTokenImportStatus.ts
msgid "This token is not available in your region."
msgstr "This token is not available in your region."

#: apps/cowswap-frontend/src/modules/account/containers/Transaction/ActivityDetails.tsx
msgid "No signatures yet"
msgstr "No signatures yet"
Expand Down Expand Up @@ -3624,6 +3632,10 @@ msgstr "Approving <0>{currencySymbolOrContext}</0> for trading"
msgid "Couldn't load balances"
msgstr "Couldn't load balances"

#: apps/cowswap-frontend/src/modules/tokensList/pure/ImportTokenListItem/index.tsx
msgid "Not available in your region"
msgstr "Not available in your region"

#: apps/cowswap-frontend/src/pages/Account/Delegate.tsx
msgid "Delegate"
msgstr "Delegate"
Expand Down Expand Up @@ -4305,6 +4317,10 @@ msgstr "before that time."
msgid "View transaction"
msgstr "View transaction"

#: apps/cowswap-frontend/src/modules/tokensList/containers/SelectTokenWidget/index.tsx
msgid "This list requires consent before importing."
msgstr "This list requires consent before importing."

#: apps/cowswap-frontend/src/modules/erc20Approve/containers/ActiveOrdersWithAffectedPermit/ActiveOrdersWithAffectedPermit.tsx
msgid "is"
msgstr "is"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import {
ProgressBarExecutingOrdersUpdater,
} from 'modules/orderProgressBar'
import { OrdersNotificationsUpdater } from 'modules/orders'
import { useSourceChainId } from 'modules/tokensList'
import { BlockedListSourcesUpdater, useSourceChainId } from 'modules/tokensList'
import { TradeType, useTradeTypeInfo } from 'modules/trade'
import { UsdPricesUpdater } from 'modules/usdAmount'
import { LpTokensWithBalancesUpdater, PoolsInfoUpdater, VampireAttackUpdater } from 'modules/yield/shared'
Expand Down Expand Up @@ -115,6 +115,7 @@ export function Updaters(): ReactNode {
bridgeNetworkInfo={bridgeNetworkInfo?.data}
/>
<RestrictedTokensListUpdater isRwaGeoblockEnabled={!!isRwaGeoblockEnabled} />
<BlockedListSourcesUpdater />
<TokensListsTagsUpdater />

<WidgetTokensUpdater />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,9 @@ import { ReactNode, useCallback, useMemo } from 'react'

import { useWalletInfo } from '@cowprotocol/wallet'

import { RwaConsentKey, RwaConsentModal, useRwaConsentModalState, useRwaConsentStatus } from 'modules/rwa'
import { useTradeConfirmActions } from 'modules/trade'

import { useRwaConsentModalState } from '../../hooks/useRwaConsentModalState'
import { useRwaConsentStatus } from '../../hooks/useRwaConsentStatus'
import { RwaConsentModal } from '../../pure/RwaConsentModal'
import { RwaConsentKey } from '../../types/rwaConsent'

export function RwaConsentModalContainer(): ReactNode {
const { account } = useWalletInfo()
const { isModalOpen, closeModal, context } = useRwaConsentModalState()
Expand Down Expand Up @@ -37,7 +33,14 @@ export function RwaConsentModalContainer(): ReactNode {

confirmConsent()
closeModal()
tradeConfirmActions.onOpen()

// if this is a token import flow, call the success callback to proceed to import modal
// if this is a trade flow, open the trade confirmation
if (context.onImportSuccess) {
context.onImportSuccess()
} else {
tradeConfirmActions.onOpen()
}
}, [account, context, consentKey, confirmConsent, closeModal, tradeConfirmActions])

if (!isModalOpen || !context) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
import { useAtomValue, useSetAtom } from 'jotai'
import { useCallback } from 'react'

import { TokenWithLogo } from '@cowprotocol/common-const'

import {
rwaConsentModalStateAtom,
updateRwaConsentModalStateAtom,
RwaConsentModalState,
RwaConsentModalContext,
} from '../state/rwaConsentModalStateAtom'

export interface RwaConsentModalContext {
consentHash: string
token?: TokenWithLogo
}
export type { RwaConsentModalContext }

export function useRwaConsentModalState(): {
isModalOpen: boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useMemo } from 'react'

import { useFeatureFlags } from '@cowprotocol/common-hooks'
import { areTokensEqual } from '@cowprotocol/common-utils'
import { useAnyRestrictedToken, RestrictedTokenInfo } from '@cowprotocol/tokens'
import { getCountryAsKey, RestrictedTokenInfo, useAnyRestrictedToken } from '@cowprotocol/tokens'
import { Nullish } from '@cowprotocol/types'
import { useWalletInfo } from '@cowprotocol/wallet'
import { Currency, Token } from '@uniswap/sdk-core'
Expand Down Expand Up @@ -93,8 +93,8 @@ export function useRwaTokenStatus({ inputCurrency, outputCurrency }: UseRwaToken
// If we can determine the country, use it regardless of consent status
// Note: while loading, country is null so we fall through to consent check
if (geoStatus.country !== null) {
const country = geoStatus.country.toUpperCase()
if (rwaTokenInfo.blockedCountries.has(country)) {
const countryKey = getCountryAsKey(geoStatus.country)
if (rwaTokenInfo.blockedCountries.has(countryKey)) {
return RwaTokenStatus.Restricted
}
return RwaTokenStatus.Allowed
Expand Down
1 change: 1 addition & 0 deletions apps/cowswap-frontend/src/modules/rwa/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export * from './state/geoDataAtom'
export * from './hooks/useRwaConsentStatus'
export * from './hooks/useRwaConsentModalState'
export * from './hooks/useGeoCountry'
export * from './hooks/useGeoStatus'
export * from './hooks/useRwaTokenStatus'
export * from './pure/RwaConsentModal'
export * from './containers/RwaConsentModalContainer'
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export interface RwaConsentModalProps {
onDismiss(): void
onConfirm(): void
token?: TokenWithLogo
consentHash?: string
}

export function RwaConsentModal(props: RwaConsentModalProps): ReactNode {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@ import { atom } from 'jotai'
import { TokenWithLogo } from '@cowprotocol/common-const'
import { atomWithPartialUpdate } from '@cowprotocol/common-utils'

export interface RwaConsentModalContext {
consentHash: string
token?: TokenWithLogo
pendingImportTokens?: TokenWithLogo[]
onImportSuccess?: () => void
}

export interface RwaConsentModalState {
isModalOpen: boolean
context?: {
consentHash: string
token?: TokenWithLogo
}
context?: RwaConsentModalContext
}

const initialRwaConsentModalState: RwaConsentModalState = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
import { useMemo } from 'react'
import { ReactNode, useMemo } from 'react'

import { useCowAnalytics } from '@cowprotocol/analytics'
import { ListSearchResponse, ListState, useListsEnabledState, useRemoveList, useToggleList } from '@cowprotocol/tokens'
import {
ListSearchResponse,
ListState,
useFilterBlockedLists,
useIsListBlocked,
useListsEnabledState,
useRemoveList,
} from '@cowprotocol/tokens'
import { Loader } from '@cowprotocol/ui'

import { Trans } from '@lingui/react/macro'

import { useGeoCountry } from 'modules/rwa'

import { CowSwapAnalyticsCategory, toCowSwapGtmEvent } from 'common/analytics/types'

import * as styledEl from './styled'

import { useAddListImport } from '../../hooks/useAddListImport'
import { useConsentAwareToggleList } from '../../hooks/useConsentAwareToggleList'
import { ImportTokenListItem } from '../../pure/ImportTokenListItem'
import { ListItem } from '../../pure/ListItem'

Expand All @@ -26,15 +36,19 @@ export interface ManageListsProps {
isListUrlValid?: boolean
}

// TODO: Break down this large function into smaller functions
// TODO: Add proper return type annotation
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export function ManageLists(props: ManageListsProps) {
export function ManageLists(props: ManageListsProps): ReactNode {
const { lists, listSearchResponse, isListUrlValid } = props

const country = useGeoCountry()

// only filter by country (blocked), NOT by consent requirement
// lists requiring consent should be visible so users can give consent
const filteredLists = useFilterBlockedLists(lists, country)

const activeTokenListsIds = useListsEnabledState()
const addListImport = useAddListImport()
const cowAnalytics = useCowAnalytics()
const toggleList = useConsentAwareToggleList()

const removeList = useRemoveList((source) => {
cowAnalytics.sendEvent({
Expand All @@ -44,15 +58,8 @@ export function ManageLists(props: ManageListsProps) {
})
})

const toggleList = useToggleList((enable, source) => {
cowAnalytics.sendEvent({
category: CowSwapAnalyticsCategory.LIST,
action: `${enable ? 'Enable' : 'Disable'} List`,
label: source,
})
})

const { source, listToImport, loading } = useListSearchResponse(listSearchResponse)
const { isBlocked } = useIsListBlocked(listToImport?.source, country)

return (
<styledEl.Wrapper>
Expand All @@ -71,6 +78,7 @@ export function ManageLists(props: ManageListsProps) {
<ImportTokenListItem
source={source}
list={listToImport}
isBlocked={isBlocked}
data-click-event={toCowSwapGtmEvent({
category: CowSwapAnalyticsCategory.LIST,
action: 'Import List',
Expand All @@ -85,7 +93,7 @@ export function ManageLists(props: ManageListsProps) {
</styledEl.ImportListsContainer>
)}
<styledEl.ListsContainer id="tokens-lists-table">
{lists
{filteredLists
.sort((a, b) => (a.priority || 0) - (b.priority || 0))
.map((list) => (
<ListItem
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,21 @@ import {
useAddList,
useAddUserToken,
useAllListsList,
useIsListBlocked,
useTokenListsTags,
useUnsupportedTokens,
useUserAddedTokens,
} from '@cowprotocol/tokens'
import { useWalletInfo } from '@cowprotocol/wallet'

import { t } from '@lingui/core/macro'
import styled from 'styled-components/macro'

import { Field } from 'legacy/state/types'

import { useTokensBalancesCombined } from 'modules/combinedBalances'
import { usePermitCompatibleTokens } from 'modules/permit'
import { useGeoCountry } from 'modules/rwa'
import { useLpTokensWithBalances } from 'modules/yield/shared'

import { CowSwapAnalyticsCategory } from 'common/analytics/types'
Expand All @@ -29,8 +32,10 @@ import { getDefaultTokenListCategories } from './getDefaultTokenListCategories'

import { useChainsToSelect } from '../../hooks/useChainsToSelect'
import { useCloseTokenSelectWidget } from '../../hooks/useCloseTokenSelectWidget'
import { useIsListRequiresConsent } from '../../hooks/useIsListRequiresConsent'
import { useOnSelectChain } from '../../hooks/useOnSelectChain'
import { useOnTokenListAddingError } from '../../hooks/useOnTokenListAddingError'
import { useRestrictedTokenImportStatus } from '../../hooks/useRestrictedTokenImportStatus'
import { useSelectTokenWidgetState } from '../../hooks/useSelectTokenWidgetState'
import { useTokensToSelect } from '../../hooks/useTokensToSelect'
import { useUpdateSelectTokenWidgetState } from '../../hooks/useUpdateSelectTokenWidgetState'
Expand Down Expand Up @@ -118,6 +123,15 @@ export function SelectTokenWidget({ displayLpTokenLists, standalone }: SelectTok

const closeTokenSelectWidget = useCloseTokenSelectWidget()

const { isImportDisabled, blockReason } = useRestrictedTokenImportStatus(tokenToImport)
const country = useGeoCountry()
const { isBlocked } = useIsListBlocked(listToImport?.source, country)
const { requiresConsent } = useIsListRequiresConsent(listToImport?.source)

// without wallet: only block if country is restricted, otherwise list is always visible
// with wallet: block if country is restricted or if consent is required (unknown country)
const isListBlocked = isBlocked || (!!account && requiresConsent)

const openPoolPage = useCallback(
(selectedPoolAddress: string) => {
updateSelectTokenWidget({ selectedPoolAddress })
Expand All @@ -140,11 +154,23 @@ export function SelectTokenWidget({ displayLpTokenLists, standalone }: SelectTok
closeTokenSelectWidget()
}, [closeTokenSelectWidget])

const importTokenAndClose = (tokens: TokenWithLogo[]): void => {
importTokenCallback(tokens)
onSelectToken?.(tokens[0])
onDismiss()
}
const selectAndClose = useCallback(
(token: TokenWithLogo): void => {
onSelectToken?.(token)
onDismiss()
},
[onSelectToken, onDismiss],
)

const importTokenAndClose = useCallback(
(tokens: TokenWithLogo[]): void => {
importTokenCallback(tokens)
if (tokens[0]) {
selectAndClose(tokens[0])
}
},
[importTokenCallback, selectAndClose],
)

const importListAndBack = (list: ListState): void => {
try {
Expand All @@ -168,14 +194,22 @@ export function SelectTokenWidget({ displayLpTokenLists, standalone }: SelectTok
onDismiss={onDismiss}
onBack={resetTokenImport}
onImport={importTokenAndClose}
isImportDisabled={isImportDisabled}
blockReason={blockReason}
/>
)
}

if (listToImport && !standalone) {
// only show consent message when wallet is connected and consent is required
const listBlockReason =
account && requiresConsent ? t`This list requires consent before importing.` : undefined

return (
<ImportListModal
list={listToImport}
isBlocked={isListBlocked}
blockReason={listBlockReason}
onDismiss={onDismiss}
onBack={resetTokenImport}
onImport={importListAndBack}
Expand Down
Loading