diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d583e5242b..14b85358bf7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ - fixed: Improve the unstake error experience by replacing the popup alert and generic "unknown error occurred" with the real error in the scene's error field, and showing a clear message when the wallet lacks the balance to cover the unstaking network fee. - fixed: Tapping Max on the Sell scene no longer briefly shows the entered fiat amount in the crypto field while the max is being calculated. - fixed: An info card no longer disappears into an empty gap when the carousel's card list shrinks. A card's position comes entirely from an animated transform keyed on its index, and that transform is not re-applied when a surviving card shifts slots, so dropping a card left the ones after it parked a full card-width off-screen. The carousel now remounts a card whose slot changes. Reproduces wherever the list shrinks after mount - most visibly when a `noBalance` card is filtered out as balances finish loading. +- fixed: Manage Tokens search now finds a token by its contract address, matching the Assets search. ## 4.50.2 (2026-08-06) diff --git a/eslint.config.mjs b/eslint.config.mjs index e817da3e660..a818fa26926 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -296,8 +296,6 @@ export default [ 'src/components/scenes/Loans/LoanManageScene.tsx', 'src/components/scenes/Loans/LoanStatusScene.tsx', - 'src/components/scenes/ManageTokensScene.tsx', - 'src/components/scenes/NotificationCenterScene.tsx', 'src/components/scenes/NotificationScene.tsx', @@ -366,7 +364,6 @@ export default [ 'src/components/themed/SceneHeader.tsx', - 'src/components/themed/SearchFooter.tsx', 'src/components/themed/SelectableRow.tsx', 'src/components/themed/ShareButtons.tsx', diff --git a/src/__tests__/walletSearch.test.ts b/src/__tests__/walletSearch.test.ts index 9df5582cd4c..a93a3c123c1 100644 --- a/src/__tests__/walletSearch.test.ts +++ b/src/__tests__/walletSearch.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from '@jest/globals' -import type { EdgeToken } from 'edge-core-js' +import type { EdgeToken, EdgeTokenMap } from 'edge-core-js' import { searchWalletList } from '../components/services/SortedWalletList' import { filterWalletCreateItemListBySearchText } from '../selectors/getCreateWalletList' @@ -14,6 +14,7 @@ import { testTetherToken, testWstethToken } from '../util/fake/fakeSearchTestData' +import { searchTokenIds } from '../util/tokenSearch' // ----------------------------------------------------------------------------- // searchWalletList Tests @@ -503,3 +504,83 @@ describe('Regression: Original search issues', () => { }) }) }) + +// ----------------------------------------------------------------------------- +// searchTokenIds Tests +// ----------------------------------------------------------------------------- + +describe('searchTokenIds', () => { + // MOG uses a checksummed contract address, as the token lists do: + const mogToken: EdgeToken = { + currencyCode: 'MOG', + displayName: 'Mog Coin', + denominations: [{ name: 'MOG', multiplier: '100000000000000000' }], + networkLocation: { + contractAddress: '0xaaeE1A9723aaDB7afA2810263653A34bA2C21C7a' + } + } + const mogTokenId = 'aaee1a9723aadb7afa2810263653a34ba2c21c7a' + const tetherTokenId = 'dac17f958d2ee523a2206206994597c13d831ec7' + const wstethTokenId = '7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0' + + const allTokens: EdgeTokenMap = { + [mogTokenId]: mogToken, + [tetherTokenId]: testTetherToken, + [wstethTokenId]: testWstethToken + } + const tokenIds = [mogTokenId, tetherTokenId, wstethTokenId] + + test('returns all tokenIds when search is empty', () => { + expect(searchTokenIds(allTokens, tokenIds, '')).toEqual(tokenIds) + }) + + test('matches currency code', () => { + expect(searchTokenIds(allTokens, tokenIds, 'MOG')).toEqual([mogTokenId]) + }) + + test('matches display name', () => { + expect(searchTokenIds(allTokens, tokenIds, 'Tether')).toEqual([ + tetherTokenId + ]) + }) + + test('matches a full contract address', () => { + expect( + searchTokenIds( + allTokens, + tokenIds, + '0xaaeE1A9723aaDB7afA2810263653A34bA2C21C7a' + ) + ).toEqual([mogTokenId]) + }) + + test('matches a full contract address in any case', () => { + expect( + searchTokenIds( + allTokens, + tokenIds, + '0xaaee1a9723aadb7afa2810263653a34ba2c21c7a' + ) + ).toEqual([mogTokenId]) + }) + + test('matches a partial contract address', () => { + expect(searchTokenIds(allTokens, tokenIds, '0xdac17f')).toEqual([ + tetherTokenId + ]) + }) + + test('returns nothing for an unknown contract address', () => { + expect( + searchTokenIds( + allTokens, + tokenIds, + '0x0000000000000000000000000000000000000000' + ) + ).toEqual([]) + }) + + test('skips tokenIds missing from the token map', () => { + expect(searchTokenIds(allTokens, ['missing-token-id'], 'mog')).toEqual([]) + }) +}) diff --git a/src/components/scenes/ManageTokensScene.tsx b/src/components/scenes/ManageTokensScene.tsx index 02512622e7e..9573b8fb382 100644 --- a/src/components/scenes/ManageTokensScene.tsx +++ b/src/components/scenes/ManageTokensScene.tsx @@ -12,7 +12,7 @@ import type { EdgeAppSceneProps } from '../../types/routerTypes' import type { FlatListItem } from '../../types/types' import { getWalletName } from '../../util/CurrencyWalletHelpers' import { logActivity } from '../../util/logger' -import { normalizeForSearch } from '../../util/utils' +import { searchTokenIds } from '../../util/tokenSearch' import { ButtonsView } from '../buttons/ButtonsView' import { SceneWrapper } from '../common/SceneWrapper' import { withWallet } from '../hoc/withWallet' @@ -148,15 +148,10 @@ const ManageTokensSceneComponent: React.FC = props => { }, [allTokens, sortingBaselineSet]) // Filter the list of tokens based on the search term: - const filteredTokenIds = React.useMemo(() => { - const target = normalizeForSearch(searchValue) - return sortedTokenIds.filter(tokenId => { - const token = allTokens[tokenId] - const currencyCode = normalizeForSearch(token.currencyCode) - const displayName = normalizeForSearch(token.displayName) - return currencyCode.includes(target) || displayName.includes(target) - }) - }, [allTokens, searchValue, sortedTokenIds]) + const filteredTokenIds = React.useMemo( + () => searchTokenIds(allTokens, sortedTokenIds, searchValue), + [allTokens, searchValue, sortedTokenIds] + ) // Split the list of tokens based on if there were auto-detected tokens given const autoDetectedTokenIds = React.useMemo( @@ -351,6 +346,7 @@ const ManageTokensSceneComponent: React.FC = props => { iconComponent={SearchIconAnimated} value={searchValue} onChangeText={setSearchValue} + testID="manageTokensSearch" /> {sectionList == null ? ( diff --git a/src/components/themed/SearchFooter.tsx b/src/components/themed/SearchFooter.tsx index bf3a8c9603e..f2d7424c429 100644 --- a/src/components/themed/SearchFooter.tsx +++ b/src/components/themed/SearchFooter.tsx @@ -104,6 +104,7 @@ export const SearchFooter: React.FC = props => { horizontalRem={1} verticalRem={0.5} autoCorrect={false} + testID={name} /> ) diff --git a/src/components/themed/WalletListCurrencyRow.tsx b/src/components/themed/WalletListCurrencyRow.tsx index 1c365e221ff..d13c494fdec 100644 --- a/src/components/themed/WalletListCurrencyRow.tsx +++ b/src/components/themed/WalletListCurrencyRow.tsx @@ -175,50 +175,55 @@ const WalletListCurrencyRowComponent: React.FC = props => { ) return ( - - {isPaused - ? lstrings.fragment_wallets_wallet_paused - : lstrings.fragment_wallets_wallet_disabled} - - ) : null - } - onLongPress={handleLongPress} - onPress={handlePress} - paddingRem={0.5} - gradientBackground={{ - colors: [primaryColor, '#00000000'], - start: { x: 0, y: 0 }, - end: { x: 1, y: 0 } - }} + - - {firstRow} - - - - - - {tickerText} - {nameNode} + + {isPaused + ? lstrings.fragment_wallets_wallet_paused + : lstrings.fragment_wallets_wallet_disabled} + + ) : null + } + onLongPress={handleLongPress} + onPress={handlePress} + gradientBackground={{ + colors: [primaryColor, '#00000000'], + start: { x: 0, y: 0 }, + end: { x: 1, y: 0 } + }} + testID={`walletListRow_${walletName}_${displayCurrencyCode}`} + > + + {firstRow} + + + + + + {tickerText} + {nameNode} + - - + + ) } diff --git a/src/util/tokenSearch.ts b/src/util/tokenSearch.ts new file mode 100644 index 00000000000..cf70b80931d --- /dev/null +++ b/src/util/tokenSearch.ts @@ -0,0 +1,35 @@ +import type { EdgeTokenMap } from 'edge-core-js' + +import { normalizeForSearch } from './utils' + +/** + * Filters a list of tokenIds using a search string. + * + * Asset identification fields (currency code and display name) match anywhere + * in the field, while network location values (ie. contract address) match the + * same way the wallet list and create-wallet list searches do. + */ +export function searchTokenIds( + allTokens: EdgeTokenMap, + tokenIds: string[], + searchText: string +): string[] { + const target = normalizeForSearch(searchText) + if (target === '') return tokenIds + + return tokenIds.filter(tokenId => { + const token = allTokens[tokenId] + if (token == null) return false + + const { currencyCode, displayName, networkLocation } = token + if (normalizeForSearch(currencyCode).includes(target)) return true + if (normalizeForSearch(displayName).includes(target)) return true + + // Search networkLocation values ie. contractAddress: + if (networkLocation == null) return false + return Object.values(networkLocation).some( + value => + typeof value === 'string' && normalizeForSearch(value).includes(target) + ) + }) +}