From cc8b52e32f79290280d149fdf8b3d75320fe0a4b Mon Sep 17 00:00:00 2001
From: Juanmi <95381763+juanmigdr@users.noreply.github.com>
Date: Fri, 28 Nov 2025 00:36:27 +0100
Subject: [PATCH 1/2] chore: [Trending] bug fixes and improvements (#23359)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
Some bug fixes on Trending plus some improvements:
Improvements:
- Search results were not the same when searching on omnisearch and when
searching on each of the pages separately, I have moved the search to
each of the hooks so that search is handled there (also removes the need
of getSearchQuery from the sectionsConfig)
- Removed the need for a keyExtractor in the sectionsConfig
- Homogenized and reworked Skeletons so that the experience as a user is
better
Bug fixes:
- Clicking on a native asset within trending throws error and makes the
app restart
## **Changelog**
CHANGELOG entry: trending bug fixes and improvements
## **Related issues**
Fixes: https://consensyssoftware.atlassian.net/browse/ASSETS-1837
## **Manual testing steps**
```gherkin
Feature: my feature name
Scenario: user [verb for user action]
Given [describe expected initial app state]
When user [verb for user action]
Then [describe expected outcome]
```
## **Screenshots/Recordings**
### **Before**
### **After**
## **Pre-merge author checklist**
- [ ] I’ve followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile
Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [ ] I've completed the PR template to the best of my ability
- [ ] I’ve included tests if applicable
- [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [ ] I’ve applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.
## **Pre-merge reviewer checklist**
- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.
---
> [!NOTE]
> Moves search filtering into section hooks, standardizes skeleton UIs,
and fixes navigation for native tokens from Trending.
>
> - **Search architecture**:
> - Move filtering into hooks: `useTrendingSearch`,
`useSitesData(searchQuery, limit)`, `usePerpsSearch`, and sections via
`useSectionsData(searchQuery)`; remove per-section
`getSearchableText`/key extractors.
> - `useTrendingSearch`: filters trending by query, merges with search
API, dedupes; exposes loading; tests added.
> - `useSearchRequest`: `chainIds` optional; debounced explore search
uses section-provided data.
> - **Perps**:
> - Add `filterMarketsByQuery` in `marketUtils` (+ tests) and use in
`usePerpsSearch` and sections.
> - **Sites**:
> - `useSitesData` now returns all sites and filters locally by
`searchQuery`; API fetch unchanged; tests updated.
> - `SitesFullView` passes `searchQuery` to hook; removes local
filtering.
> - **Trending tokens**:
> - `TrendingTokenRowItem`: refactor badge/source helpers; add
native-token nav support (`isNative`/`isETH`) and memoization; color
helper for pct change; tests for ETH/MATIC/native cases.
> - `TrendingTokensFullView`: uses unified search results; updates
skeleton rendering.
> - **Explore search & sections**:
> - `useExploreSearch`: show top items on empty query; delegate
filtering to sections; improved loading/debounce handling.
> - Switch list `keyExtractor`s to stable index-based keys per section.
> - `SECTIONS_CONFIG`: pass `searchQuery` to hooks; unify Perps
skeleton.
> - **UI/Skeletons**:
> - Rework `TrendingTokensSkeleton` and `SiteSkeleton` to consistent
44px circular icon + text rows; tests adjusted.
>
> Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
43099a7133eb4675bde1706571483c6cd778c076. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).
---
.../UI/Perps/hooks/usePerpsSearch.ts | 9 +-
.../UI/Perps/utils/marketUtils.test.ts | 69 +++++
app/components/UI/Perps/utils/marketUtils.ts | 31 +++
.../SiteSkeleton/SiteSkeleton.test.tsx | 16 +-
.../components/SiteSkeleton/SiteSkeleton.tsx | 54 ++--
...eSiteData.test.ts => useSitesData.test.ts} | 11 +-
.../Sites/hooks/useSiteData/useSitesData.ts | 32 ++-
.../TrendingTokenRowItem.test.tsx | 116 ++++++++
.../TrendingTokenRowItem.tsx | 241 ++++++++---------
.../TrendingTokensSkeleton.test.tsx | 78 ++++--
.../TrendingTokensSkeleton.tsx | 83 ++----
.../useSearchRequest/useSearchRequest.ts | 4 +-
.../useTrendingSearch.test.ts | 89 ++++++-
.../useTrendingSearch/useTrendingSearch.ts | 13 +-
.../SitesFullView/SitesFullView.test.tsx | 42 +--
.../Views/SitesFullView/SitesFullView.tsx | 19 +-
.../TrendingTokensFullView.test.tsx | 12 +-
.../TrendingTokensFullView.tsx | 38 +--
.../ExploreSearchResults.tsx | 2 +-
.../config/useExploreSearch.test.ts | 248 +++++-------------
.../config/useExploreSearch.ts | 5 +-
.../components/SectionCard/SectionCard.tsx | 2 +-
.../SectionCarrousel/SectionCarrousel.tsx | 2 +-
.../TrendingView/config/sections.config.tsx | 34 +--
24 files changed, 715 insertions(+), 535 deletions(-)
rename app/components/UI/Sites/hooks/useSiteData/{useSiteData.test.ts => useSitesData.test.ts} (97%)
diff --git a/app/components/UI/Perps/hooks/usePerpsSearch.ts b/app/components/UI/Perps/hooks/usePerpsSearch.ts
index f28da065eb7..10955a7a260 100644
--- a/app/components/UI/Perps/hooks/usePerpsSearch.ts
+++ b/app/components/UI/Perps/hooks/usePerpsSearch.ts
@@ -1,5 +1,6 @@
import { useState, useCallback, useMemo } from 'react';
import type { PerpsMarketData } from '../controllers/types';
+import { filterMarketsByQuery } from '../utils/marketUtils';
interface UsePerpsSearchParams {
/**
@@ -87,13 +88,7 @@ export const usePerpsSearch = ({
return markets;
}
- const lowerQuery = searchQuery.toLowerCase().trim();
-
- return markets.filter(
- (market) =>
- market.symbol?.toLowerCase().includes(lowerQuery) ||
- market.name?.toLowerCase().includes(lowerQuery),
- );
+ return filterMarketsByQuery(markets, searchQuery);
}, [markets, searchQuery, isSearchVisible]);
return {
diff --git a/app/components/UI/Perps/utils/marketUtils.test.ts b/app/components/UI/Perps/utils/marketUtils.test.ts
index 067509f7392..6d46076c0da 100644
--- a/app/components/UI/Perps/utils/marketUtils.test.ts
+++ b/app/components/UI/Perps/utils/marketUtils.test.ts
@@ -8,8 +8,10 @@ import {
shouldIncludeMarket,
validateMarketPattern,
getPerpsDisplaySymbol,
+ filterMarketsByQuery,
} from './marketUtils';
import type { CandleData } from '../types/perps-types';
+import type { PerpsMarketData } from '../controllers/types';
import { CandlePeriod } from '../constants/chartConfig';
jest.mock('../constants/hyperLiquidConfig', () => ({
@@ -916,4 +918,71 @@ describe('marketUtils', () => {
});
});
});
+
+ describe('filterMarketsByQuery', () => {
+ const mockMarkets: Partial[] = [
+ { symbol: 'BTC', name: 'Bitcoin' },
+ { symbol: 'ETH', name: 'Ethereum' },
+ { symbol: 'xyz:TSLA', name: 'Tesla Stock' },
+ ];
+
+ it('returns all markets when query is empty or whitespace', () => {
+ const result1 = filterMarketsByQuery(
+ mockMarkets as PerpsMarketData[],
+ '',
+ );
+ const result2 = filterMarketsByQuery(
+ mockMarkets as PerpsMarketData[],
+ ' ',
+ );
+
+ expect(result1).toEqual(mockMarkets);
+ expect(result2).toEqual(mockMarkets);
+ });
+
+ it('filters markets by symbol case-insensitively', () => {
+ const result = filterMarketsByQuery(
+ mockMarkets as PerpsMarketData[],
+ 'btc',
+ );
+
+ expect(result).toEqual([mockMarkets[0]]);
+ });
+
+ it('filters markets by name case-insensitively', () => {
+ const result = filterMarketsByQuery(
+ mockMarkets as PerpsMarketData[],
+ 'ethereum',
+ );
+
+ expect(result).toEqual([mockMarkets[1]]);
+ });
+
+ it('filters markets by partial matches in symbol or name', () => {
+ const result = filterMarketsByQuery(
+ mockMarkets as PerpsMarketData[],
+ 'Stock',
+ );
+
+ expect(result).toEqual([mockMarkets[2]]);
+ });
+
+ it('returns empty array when no markets match query', () => {
+ const result = filterMarketsByQuery(
+ mockMarkets as PerpsMarketData[],
+ 'NonExistent',
+ );
+
+ expect(result).toEqual([]);
+ });
+
+ it('trims whitespace from query before matching', () => {
+ const result = filterMarketsByQuery(
+ mockMarkets as PerpsMarketData[],
+ ' BTC ',
+ );
+
+ expect(result).toEqual([mockMarkets[0]]);
+ });
+ });
});
diff --git a/app/components/UI/Perps/utils/marketUtils.ts b/app/components/UI/Perps/utils/marketUtils.ts
index 66c5a05f1b6..57256ae5643 100644
--- a/app/components/UI/Perps/utils/marketUtils.ts
+++ b/app/components/UI/Perps/utils/marketUtils.ts
@@ -392,6 +392,37 @@ export const getMarketBadgeType = (
// Main DEX markets without marketType show no badge
market.marketType || (market.marketSource ? 'experimental' : undefined);
+/**
+ * Filter markets by search query
+ * Searches through both symbol and name fields (case-insensitive)
+ *
+ * @param markets - Array of markets to filter
+ * @param searchQuery - Search query string
+ * @returns Filtered array of markets matching the query
+ *
+ * @example
+ * filterMarketsByQuery([{ symbol: 'BTC', name: 'Bitcoin' }], 'btc') // → [{ symbol: 'BTC', name: 'Bitcoin' }]
+ * filterMarketsByQuery([{ symbol: 'BTC', name: 'Bitcoin' }], 'coin') // → [{ symbol: 'BTC', name: 'Bitcoin' }]
+ * filterMarketsByQuery([{ symbol: 'BTC', name: 'Bitcoin' }], '') // → [{ symbol: 'BTC', name: 'Bitcoin' }]
+ */
+export const filterMarketsByQuery = (
+ markets: PerpsMarketData[],
+ searchQuery: string,
+): PerpsMarketData[] => {
+ // Return all markets if query is empty
+ if (!searchQuery?.trim()) {
+ return markets;
+ }
+
+ const lowerQuery = searchQuery.toLowerCase().trim();
+
+ return markets.filter(
+ (market) =>
+ market.symbol?.toLowerCase().includes(lowerQuery) ||
+ market.name?.toLowerCase().includes(lowerQuery),
+ );
+};
+
/**
* Generate the appropriate icon URL for an asset symbol
* Handles both regular assets and HIP-3 assets (dex:symbol format)
diff --git a/app/components/UI/Sites/components/SiteSkeleton/SiteSkeleton.test.tsx b/app/components/UI/Sites/components/SiteSkeleton/SiteSkeleton.test.tsx
index 7a418ceb535..3149960421d 100644
--- a/app/components/UI/Sites/components/SiteSkeleton/SiteSkeleton.test.tsx
+++ b/app/components/UI/Sites/components/SiteSkeleton/SiteSkeleton.test.tsx
@@ -44,10 +44,10 @@ describe('SiteSkeleton', () => {
const skeletons = getAllByTestId('skeleton');
const logoSkeleton = skeletons[0];
- expect(logoSkeleton.props.style).toContainEqual({
- height: 40,
- width: 40,
- });
+ expect(logoSkeleton.props.style).toEqual([
+ { height: 44, width: 44 },
+ { borderRadius: 100 },
+ ]);
});
it('renders name skeleton with correct dimensions', () => {
@@ -68,10 +68,10 @@ describe('SiteSkeleton', () => {
const skeletons = getAllByTestId('skeleton');
const urlSkeleton = skeletons[2];
- expect(urlSkeleton.props.style).toContainEqual({
- height: 16,
- width: '40%',
- });
+ expect(urlSkeleton.props.style).toEqual([
+ { height: 18, width: '80%' },
+ { marginBottom: 0, marginTop: 2 },
+ ]);
});
});
});
diff --git a/app/components/UI/Sites/components/SiteSkeleton/SiteSkeleton.tsx b/app/components/UI/Sites/components/SiteSkeleton/SiteSkeleton.tsx
index 130d3a81722..9abc415b43f 100644
--- a/app/components/UI/Sites/components/SiteSkeleton/SiteSkeleton.tsx
+++ b/app/components/UI/Sites/components/SiteSkeleton/SiteSkeleton.tsx
@@ -1,40 +1,56 @@
import React from 'react';
-import { View, StyleSheet } from 'react-native';
+import { View, StyleSheet, type ViewStyle } from 'react-native';
import Skeleton from '../../../../../component-library/components/Skeleton/Skeleton';
const styles = StyleSheet.create({
container: {
+ display: 'flex',
flexDirection: 'row',
- alignItems: 'center',
- paddingVertical: 16,
+ alignItems: 'flex-start',
+ alignSelf: 'stretch',
+ paddingTop: 8,
+ paddingBottom: 8,
},
iconSkeleton: {
- borderRadius: 20,
- marginBottom: 0,
+ borderRadius: 100, // Fully circular
},
- contentContainer: {
+ leftContainer: {
+ paddingLeft: 16,
flex: 1,
- marginLeft: 16,
+ justifyContent: 'space-between',
+ },
+ tokenHeaderRow: {
+ display: 'flex',
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 4,
},
- nameSkeleton: {
- marginBottom: 8,
+ tokenNameSkeleton: {
+ marginBottom: 0,
},
- urlSkeleton: {
+ marketStatsSkeleton: {
+ marginTop: 2,
marginBottom: 0,
},
});
-const SiteSkeleton = () => (
+const iconSize = 44;
+const SitesSkeleton: React.FC = () => (
- {/* Logo skeleton */}
-
-
- {/* Content skeleton */}
-
-
-
+
+
+
+
+
+
+
+
);
-export default SiteSkeleton;
+export default SitesSkeleton;
diff --git a/app/components/UI/Sites/hooks/useSiteData/useSiteData.test.ts b/app/components/UI/Sites/hooks/useSiteData/useSitesData.test.ts
similarity index 97%
rename from app/components/UI/Sites/hooks/useSiteData/useSiteData.test.ts
rename to app/components/UI/Sites/hooks/useSiteData/useSitesData.test.ts
index e6c1fc7e160..428dd6477db 100644
--- a/app/components/UI/Sites/hooks/useSiteData/useSiteData.test.ts
+++ b/app/components/UI/Sites/hooks/useSiteData/useSitesData.test.ts
@@ -96,7 +96,7 @@ describe('useSitesData', () => {
json: async () => ({ dapps: [] }),
});
- renderHook(() => useSitesData({ limit: 50 }));
+ renderHook(() => useSitesData(undefined, 50));
await waitFor(() => {
expect(fetch).toHaveBeenCalledWith(
@@ -280,9 +280,12 @@ describe('useSitesData', () => {
json: async () => ({ dapps: [] }),
});
- const { rerender } = renderHook(({ limit }) => useSitesData({ limit }), {
- initialProps: { limit: 10 },
- });
+ const { rerender } = renderHook(
+ ({ limit }) => useSitesData(undefined, limit),
+ {
+ initialProps: { limit: 10 },
+ },
+ );
await waitFor(() => {
expect(fetch).toHaveBeenCalledTimes(1);
diff --git a/app/components/UI/Sites/hooks/useSiteData/useSitesData.ts b/app/components/UI/Sites/hooks/useSiteData/useSitesData.ts
index 5b2372f4946..707250ce478 100644
--- a/app/components/UI/Sites/hooks/useSiteData/useSitesData.ts
+++ b/app/components/UI/Sites/hooks/useSiteData/useSitesData.ts
@@ -1,4 +1,4 @@
-import { useEffect, useState, useCallback } from 'react';
+import { useEffect, useState, useCallback, useMemo } from 'react';
import Logger from '../../../../../util/Logger';
import type { SiteData } from '../../components/SiteRowItem/SiteRowItem';
@@ -21,10 +21,6 @@ interface ApiSitesResponse {
dapps: ApiDappResponse[];
}
-interface UseSitesDataParams {
- limit?: number;
-}
-
interface UseSitesDataResult {
sites: SiteData[];
isLoading: boolean;
@@ -51,10 +47,11 @@ const extractDisplayUrl = (url: string): string => {
* @param params - Parameters for the API request
* @returns Sites data, loading state, and error
*/
-export const useSitesData = ({
+export const useSitesData = (
+ searchQuery?: string,
limit = 100,
-}: UseSitesDataParams = {}): UseSitesDataResult => {
- const [sites, setSites] = useState([]);
+): UseSitesDataResult => {
+ const [allSites, setAllSites] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
@@ -84,13 +81,13 @@ export const useSitesData = ({
featured: dapp.featured,
}));
- setSites(transformedSites);
+ setAllSites(transformedSites);
} catch (err) {
const fetchError = err instanceof Error ? err : new Error(String(err));
Logger.error(fetchError, '[useSitesData] Error fetching sites');
setError(fetchError);
// Don't use fallback data - return empty array to show the error
- setSites([]);
+ setAllSites([]);
} finally {
setIsLoading(false);
}
@@ -104,5 +101,20 @@ export const useSitesData = ({
fetchSites();
}, [fetchSites]);
+ // Filter sites locally based on search query
+ const sites = useMemo(() => {
+ if (!searchQuery?.trim()) {
+ return allSites;
+ }
+
+ const query = searchQuery.toLowerCase().trim();
+ return allSites.filter(
+ (site) =>
+ site.name.toLowerCase().includes(query) ||
+ site.displayUrl.toLowerCase().includes(query) ||
+ site.url.toLowerCase().includes(query),
+ );
+ }, [allSites, searchQuery]);
+
return { sites, isLoading, error, refetch };
};
diff --git a/app/components/UI/Trending/components/TrendingTokenRowItem/TrendingTokenRowItem.test.tsx b/app/components/UI/Trending/components/TrendingTokenRowItem/TrendingTokenRowItem.test.tsx
index 12500ebddc1..8fb3602b3ff 100644
--- a/app/components/UI/Trending/components/TrendingTokenRowItem/TrendingTokenRowItem.test.tsx
+++ b/app/components/UI/Trending/components/TrendingTokenRowItem/TrendingTokenRowItem.test.tsx
@@ -563,6 +563,122 @@ describe('TrendingTokenRowItem', () => {
image:
'https://static.cx.metamask.io/api/v2/tokenIcons/assets/eip155/1/erc20/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48.png',
pricePercentChange1d: 3.44,
+ isNative: false,
+ isETH: false,
+ });
+ });
+
+ it('navigates to Asset page with isETH true for native ETH on Ethereum mainnet', () => {
+ const token = createMockToken({
+ assetId: 'eip155:1/slip44:60',
+ symbol: 'ETH',
+ name: 'Ethereum',
+ decimals: 18,
+ });
+
+ const networkAddedState = {
+ ...mockState,
+ engine: {
+ ...mockState.engine,
+ backgroundState: {
+ ...mockState.engine.backgroundState,
+ NetworkController: {
+ networkConfigurations: {},
+ networkConfigurationsByChainId: {
+ '0x1': {
+ chainId: '0x1',
+ caipChainId: 'eip155:1',
+ name: 'Ethereum Mainnet',
+ },
+ },
+ },
+ MultichainNetworkController: {
+ ...mockState.engine.backgroundState.MultichainNetworkController,
+ multichainNetworkConfigurationsByChainId: {},
+ },
+ },
+ },
+ };
+
+ const { getByTestId } = renderWithProvider(
+ ,
+ { state: networkAddedState },
+ false,
+ );
+
+ const tokenRow = getByTestId(
+ 'trending-token-row-item-eip155:1/slip44:60',
+ );
+ fireEvent.press(tokenRow);
+
+ expect(mockNavigate).toHaveBeenCalledWith('Asset', {
+ chainId: '0x1',
+ address: '0x0000000000000000000000000000000000000000',
+ symbol: 'ETH',
+ name: 'Ethereum',
+ decimals: 18,
+ image:
+ 'https://static.cx.metamask.io/api/v2/tokenIcons/assets/eip155/1/slip44/60.png',
+ pricePercentChange1d: 3.44,
+ isNative: true,
+ isETH: true,
+ });
+ });
+
+ it('navigates to Asset page with isNative true and isETH false for native token on non-Ethereum chain', () => {
+ const token = createMockToken({
+ assetId: 'eip155:137/slip44:966',
+ symbol: 'MATIC',
+ name: 'Polygon',
+ decimals: 18,
+ });
+
+ const networkAddedState = {
+ ...mockState,
+ engine: {
+ ...mockState.engine,
+ backgroundState: {
+ ...mockState.engine.backgroundState,
+ NetworkController: {
+ networkConfigurations: {},
+ networkConfigurationsByChainId: {
+ '0x89': {
+ chainId: '0x89',
+ caipChainId: 'eip155:137',
+ name: 'Polygon Mainnet',
+ },
+ },
+ },
+ MultichainNetworkController: {
+ ...mockState.engine.backgroundState.MultichainNetworkController,
+ multichainNetworkConfigurationsByChainId: {},
+ },
+ },
+ },
+ };
+
+ const { getByTestId } = renderWithProvider(
+ ,
+ { state: networkAddedState },
+ false,
+ );
+
+ const tokenRow = getByTestId(
+ 'trending-token-row-item-eip155:137/slip44:966',
+ );
+ fireEvent.press(tokenRow);
+
+ expect(mockNavigate).toHaveBeenCalledWith('Asset', {
+ chainId: '0x89',
+ address: '0x0000000000000000000000000000000000000000',
+ symbol: 'MATIC',
+ name: 'Polygon',
+ decimals: 18,
+ image:
+ 'https://static.cx.metamask.io/api/v2/tokenIcons/assets/eip155/137/slip44/966.png',
+ pricePercentChange1d: 3.44,
+ isNative: true,
+ isETH: false,
});
});
diff --git a/app/components/UI/Trending/components/TrendingTokenRowItem/TrendingTokenRowItem.tsx b/app/components/UI/Trending/components/TrendingTokenRowItem/TrendingTokenRowItem.tsx
index 6e11503ba16..cf164178427 100644
--- a/app/components/UI/Trending/components/TrendingTokenRowItem/TrendingTokenRowItem.tsx
+++ b/app/components/UI/Trending/components/TrendingTokenRowItem/TrendingTokenRowItem.tsx
@@ -1,5 +1,5 @@
-import React, { useCallback, useState } from 'react';
-import { TouchableOpacity, View } from 'react-native';
+import React, { useCallback, useMemo, useState } from 'react';
+import { ImageSourcePropType, TouchableOpacity, View } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { useSelector } from 'react-redux';
import Text, {
@@ -22,6 +22,7 @@ import {
Hex,
isCaipChainId,
} from '@metamask/utils';
+import { NATIVE_SWAPS_TOKEN_ADDRESS } from '../../../../../constants/bridge';
import {
getDefaultNetworkByChainId,
getTestNetImageByChainId,
@@ -42,6 +43,74 @@ import { selectNetworkConfigurationsByCaipChainId } from '../../../../../selecto
import type { Network } from '../../../../Views/Settings/NetworksSettings/NetworkSettings/CustomNetworkView/CustomNetwork.types';
import { getTrendingTokenImageUrl } from '../../utils/getTrendingTokenImageUrl';
+/**
+ * Extracts CAIP chain ID from asset ID
+ */
+const getCaipChainIdFromAssetId = (assetId: string): CaipChainId =>
+ assetId.split('/')[0] as CaipChainId;
+
+/**
+ * Converts CAIP chain ID to hex chain ID
+ */
+const caipChainIdToHex = (caipChainId: CaipChainId): Hex => {
+ const { namespace, reference } = parseCaipChainId(caipChainId);
+ return namespace === 'eip155'
+ ? (`0x${Number(reference).toString(16)}` as Hex)
+ : (caipChainId as Hex);
+};
+
+/**
+ * Gets network badge image source for a given CAIP chain ID
+ */
+const getNetworkBadgeSource = (
+ caipChainId: CaipChainId,
+): ImageSourcePropType | undefined => {
+ const hexChainId = caipChainIdToHex(caipChainId);
+
+ if (isTestNet(hexChainId)) {
+ return getTestNetImageByChainId(hexChainId);
+ }
+
+ const defaultNetwork = getDefaultNetworkByChainId(hexChainId) as
+ | { imageSource: ImageSourcePropType }
+ | undefined;
+
+ if (defaultNetwork) {
+ return defaultNetwork.imageSource;
+ }
+
+ const unpopularNetwork = UnpopularNetworkList.find(
+ (networkConfig) => networkConfig.chainId === hexChainId,
+ );
+
+ const customNetworkImg = CustomNetworkImgMapping[hexChainId];
+
+ const popularNetwork = PopularList.find(
+ (networkConfig) => networkConfig.chainId === hexChainId,
+ );
+
+ const network = unpopularNetwork || popularNetwork;
+ if (network) {
+ return network.rpcPrefs.imageSource;
+ }
+ if (isCaipChainId(caipChainId)) {
+ return getNonEvmNetworkImageSourceByChainId(caipChainId);
+ }
+ if (customNetworkImg) {
+ return customNetworkImg as ImageSourcePropType;
+ }
+
+ return undefined;
+};
+
+/**
+ * Gets the text color for price percentage change
+ */
+const getPriceChangeColor = (priceChange: number): TextColor => {
+ if (priceChange === 0) return TextColor.Default;
+ return priceChange > 0 ? TextColor.Success : TextColor.Error;
+};
+
/**
* Maps TimeOption to the corresponding priceChangePct field key
*/
@@ -67,6 +136,37 @@ interface TrendingTokenRowItemProps {
iconSize?: number;
selectedTimeOption?: TimeOption;
}
+
+/**
+ * Converts a TrendingAsset to Asset navigation params
+ */
+const getAssetNavigationParams = (token: TrendingAsset) => {
+ const [caipChainId, assetIdentifier] = token.assetId.split('/');
+ if (!isCaipChainId(caipChainId)) return null;
+
+ const isEvmChain = caipChainId.startsWith('eip155:');
+ const isNativeToken = assetIdentifier?.startsWith('slip44:');
+ const address = (
+ isNativeToken ? NATIVE_SWAPS_TOKEN_ADDRESS : assetIdentifier?.split(':')[1]
+ ) as Hex | undefined;
+
+ const hexChainId = caipChainIdToHex(caipChainId);
+
+ return {
+ chainId: hexChainId,
+ address: isEvmChain ? address : token.assetId,
+ symbol: token.symbol,
+ name: token.name,
+ decimals: token.decimals,
+ image: getTrendingTokenImageUrl(token.assetId),
+ pricePercentChange1d: token.priceChangePct?.h24
+ ? parseFloat(token.priceChangePct.h24)
+ : undefined,
+ isNative: isNativeToken,
+ isETH: isNativeToken && hexChainId === '0x1',
+ };
+};
+
const TrendingTokenRowItem = ({
token,
iconSize = 44,
@@ -74,52 +174,24 @@ const TrendingTokenRowItem = ({
}: TrendingTokenRowItemProps) => {
const { styles } = useStyles(styleSheet, {});
const navigation = useNavigation();
- const chainId = token.assetId.split('/')[0] as CaipChainId;
const networkConfigurations = useSelector(
selectNetworkConfigurationsByCaipChainId,
);
const [isNetworkModalVisible, setIsNetworkModalVisible] = useState(false);
const [selectedNetwork, setSelectedNetwork] = useState(null);
- const networkBadgeSource = useCallback((currentChainId: CaipChainId) => {
- const { reference } = parseCaipChainId(currentChainId);
- const hexChainId = `0x${Number(reference).toString(16)}` as Hex;
-
- if (isTestNet(hexChainId)) {
- return getTestNetImageByChainId(hexChainId);
- }
-
- const defaultNetwork = getDefaultNetworkByChainId(hexChainId) as
- | {
- imageSource: string;
- }
- | undefined;
-
- if (defaultNetwork) {
- return defaultNetwork.imageSource;
- }
-
- const unpopularNetwork = UnpopularNetworkList.find(
- (networkConfig) => networkConfig.chainId === hexChainId,
- );
-
- const customNetworkImg = CustomNetworkImgMapping[hexChainId];
+ // Memoize derived values
+ const caipChainId = useMemo(
+ () => getCaipChainIdFromAssetId(token.assetId),
+ [token.assetId],
+ );
- const popularNetwork = PopularList.find(
- (networkConfig) => networkConfig.chainId === hexChainId,
- );
+ const assetParams = useMemo(() => getAssetNavigationParams(token), [token]);
- const network = unpopularNetwork || popularNetwork;
- if (network) {
- return network.rpcPrefs.imageSource;
- }
- if (isCaipChainId(currentChainId)) {
- return getNonEvmNetworkImageSourceByChainId(currentChainId);
- }
- if (customNetworkImg) {
- return customNetworkImg;
- }
- }, []);
+ const networkBadgeImageSource = useMemo(
+ () => getNetworkBadgeSource(caipChainId),
+ [caipChainId],
+ );
// Parse price change percentage from API (comes as string like "-3.44" or "+0.456")
// Use the correct field based on selected time option
@@ -134,36 +206,15 @@ const TrendingTokenRowItem = ({
const hasPercentageChange =
pricePercentChange !== undefined && !isNaN(pricePercentChange);
const isPositiveChange = hasPercentageChange && pricePercentChange > 0;
- const isNeutralChange = hasPercentageChange && pricePercentChange === 0;
const handlePress = useCallback(() => {
- // Parse assetId to extract chainId and address
- // Format: 'eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48'
- const [caipChainId, assetIdentifier] = token.assetId.split('/');
- // check if caipChainId is evm or non-evm
- const isEvmChain = caipChainId.startsWith('eip155:');
- const address = assetIdentifier?.split(':')[1] as Hex | undefined;
-
- if (!address || !isCaipChainId(caipChainId)) {
- return;
- }
-
- // Convert CAIP chainId to Hex format for EVM networks
- const { namespace, reference } = parseCaipChainId(caipChainId);
- const hexChainId =
- namespace === 'eip155'
- ? (`0x${Number(reference).toString(16)}` as Hex)
- : (caipChainId as Hex);
+ if (!assetParams) return;
- // Check if network is already added by user
- const isNetworkAdded = Boolean(
- networkConfigurations[caipChainId as CaipChainId],
- );
+ const isNetworkAdded = Boolean(networkConfigurations[caipChainId]);
- // If network is not added, show modal to add it
if (!isNetworkAdded) {
const popularNetwork = PopularList.find(
- (network) => network.chainId === hexChainId,
+ (network) => network.chainId === assetParams.chainId,
);
if (popularNetwork) {
@@ -173,25 +224,8 @@ const TrendingTokenRowItem = ({
}
}
- // Construct image URL from assetId
- const imageUrl = getTrendingTokenImageUrl(token.assetId);
-
- // Get 24-hour price change percentage (h24 corresponds to 1 day)
- const priceChange24h = token.priceChangePct?.h24
- ? parseFloat(token.priceChangePct.h24)
- : undefined;
-
- // Navigate to Asset page with token data
- navigation.navigate('Asset', {
- chainId: hexChainId,
- address: isEvmChain ? address : token.assetId,
- symbol: token.symbol,
- name: token.name,
- decimals: token.decimals,
- image: imageUrl,
- pricePercentChange1d: priceChange24h,
- });
- }, [token, navigation, networkConfigurations]);
+ navigation.navigate('Asset', assetParams);
+ }, [assetParams, caipChainId, navigation, networkConfigurations]);
const closeNetworkModal = useCallback(() => {
setIsNetworkModalVisible(false);
@@ -199,34 +233,11 @@ const TrendingTokenRowItem = ({
}, []);
const handleNetworkModalAccept = useCallback(() => {
- // Network has been added by NetworkModals' closeModal function
- // Now navigate to Asset page
- const [caipChainId, assetIdentifier] = token.assetId.split('/');
- const isEvmChain = caipChainId.startsWith('eip155:');
- const address = assetIdentifier?.split(':')[1] as Hex | undefined;
-
- if (address && isCaipChainId(caipChainId)) {
- const { namespace, reference } = parseCaipChainId(caipChainId);
- const hexChainId =
- namespace === 'eip155'
- ? (`0x${Number(reference).toString(16)}` as Hex)
- : (caipChainId as Hex);
-
- navigation.navigate('Asset', {
- chainId: hexChainId,
- address: isEvmChain ? address : token.assetId,
- symbol: token.symbol,
- name: token.name,
- image: getTrendingTokenImageUrl(token.assetId),
- decimals: token.decimals,
- pricePercentChange1d: token.priceChangePct?.h24
- ? parseFloat(token.priceChangePct.h24)
- : undefined,
- });
+ if (assetParams) {
+ navigation.navigate('Asset', assetParams);
}
-
closeNetworkModal();
- }, [token, navigation, closeNetworkModal]);
+ }, [assetParams, navigation, closeNetworkModal]);
return (
<>
@@ -265,7 +276,7 @@ const TrendingTokenRowItem = ({
}
@@ -306,15 +317,9 @@ const TrendingTokenRowItem = ({
{hasPercentageChange && (
- {isNeutralChange ? '' : isPositiveChange ? '+' : '-'}
+ {pricePercentChange === 0 ? '' : isPositiveChange ? '+' : '-'}
{Math.abs(pricePercentChange).toFixed(2)}%
)}
diff --git a/app/components/UI/Trending/components/TrendingTokenSkeleton/TrendingTokensSkeleton.test.tsx b/app/components/UI/Trending/components/TrendingTokenSkeleton/TrendingTokensSkeleton.test.tsx
index bddf11fde0b..587b4acfb76 100644
--- a/app/components/UI/Trending/components/TrendingTokenSkeleton/TrendingTokensSkeleton.test.tsx
+++ b/app/components/UI/Trending/components/TrendingTokenSkeleton/TrendingTokensSkeleton.test.tsx
@@ -7,37 +7,71 @@ jest.mock(
'../../../../../component-library/components/Skeleton/Skeleton',
() => {
const ReactNative = jest.requireActual('react-native');
+ const SkeletonMock = ({
+ height,
+ width,
+ style,
+ }: {
+ height?: number;
+ width?: number | string;
+ style?: object;
+ }) => (
+
+ );
return {
__esModule: true,
- default: jest.fn(({ height, width, style, testID }) => (
-
- )),
+ default: SkeletonMock,
};
},
);
describe('TrendingTokensSkeleton', () => {
- it('renders successfully with default props', () => {
- const { getAllByTestId } = render();
- const skeletons = getAllByTestId('skeleton');
- // Should render 5 skeleton elements: icon, token name, market stats, price, percentage
- expect(skeletons.length).toBe(5);
+ beforeEach(() => {
+ jest.clearAllMocks();
});
- it('renders single skeleton row by default', () => {
- const { getAllByTestId } = render();
- const skeletons = getAllByTestId('skeleton');
- // Should render 5 skeleton elements for one row
- expect(skeletons.length).toBe(5);
- });
+ describe('rendering', () => {
+ it('renders successfully with default props', () => {
+ const { getAllByTestId } = render();
+
+ const skeletons = getAllByTestId('skeleton');
+ expect(skeletons.length).toBe(5);
+ });
+
+ it('renders logo skeleton with correct dimensions', () => {
+ const { getAllByTestId } = render();
+
+ const skeletons = getAllByTestId('skeleton');
+ const logoSkeleton = skeletons[0];
+
+ expect(logoSkeleton.props.style).toEqual([
+ { height: 44, width: 44 },
+ { borderRadius: 100 },
+ ]);
+ });
+
+ it('renders name skeleton with correct dimensions', () => {
+ const { getAllByTestId } = render();
+
+ const skeletons = getAllByTestId('skeleton');
+ const nameSkeleton = skeletons[1];
+
+ expect(nameSkeleton.props.style).toContainEqual({
+ height: 20,
+ width: '60%',
+ });
+ });
+
+ it('renders URL skeleton with correct dimensions', () => {
+ const { getAllByTestId } = render();
+
+ const skeletons = getAllByTestId('skeleton');
+ const urlSkeleton = skeletons[2];
- it('renders multiple skeleton rows when count is provided', () => {
- const { getAllByTestId } = render();
- const skeletons = getAllByTestId('skeleton');
- // Should render 5 skeleton elements per row (3 rows = 15 skeletons)
- expect(skeletons.length).toBe(15);
+ expect(urlSkeleton.props.style).toEqual([
+ { height: 18, width: '80%' },
+ { marginBottom: 0, marginTop: 2 },
+ ]);
+ });
});
});
diff --git a/app/components/UI/Trending/components/TrendingTokenSkeleton/TrendingTokensSkeleton.tsx b/app/components/UI/Trending/components/TrendingTokenSkeleton/TrendingTokensSkeleton.tsx
index f8256ad3774..b7eae3dfa75 100644
--- a/app/components/UI/Trending/components/TrendingTokenSkeleton/TrendingTokensSkeleton.tsx
+++ b/app/components/UI/Trending/components/TrendingTokenSkeleton/TrendingTokensSkeleton.tsx
@@ -2,21 +2,6 @@ import React from 'react';
import { View, StyleSheet, type ViewStyle } from 'react-native';
import Skeleton from '../../../../../component-library/components/Skeleton/Skeleton';
-export interface TrendingTokensSkeletonProps {
- /**
- * Number of skeleton rows to render
- */
- count?: number;
- /**
- * Size of the icon skeleton (defaults to HOME_SCREEN_CONFIG.DEFAULT_ICON_SIZE)
- */
- iconSize?: number;
- /**
- * Optional style for the container
- */
- style?: ViewStyle;
-}
-
const styles = StyleSheet.create({
container: {
display: 'flex',
@@ -61,51 +46,27 @@ const styles = StyleSheet.create({
},
});
-const TrendingTokensSkeleton: React.FC = ({
- count = 1,
- iconSize = 44,
- style,
-}) => {
- // Generate array for count
- const rows = Array.from({ length: count }, (_, i) => i);
-
- return (
- <>
- {rows.map((index) => (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ))}
- >
- );
-};
+const iconSize = 44;
+const TrendingTokensSkeleton: React.FC = () => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+);
export default TrendingTokensSkeleton;
diff --git a/app/components/UI/Trending/hooks/useSearchRequest/useSearchRequest.ts b/app/components/UI/Trending/hooks/useSearchRequest/useSearchRequest.ts
index 56bdc3845b5..bf0c097a02d 100644
--- a/app/components/UI/Trending/hooks/useSearchRequest/useSearchRequest.ts
+++ b/app/components/UI/Trending/hooks/useSearchRequest/useSearchRequest.ts
@@ -8,11 +8,11 @@ import { useStableArray } from '../../../Perps/hooks/useStableArray';
* @returns {Object} An object containing the search results, loading state, and a function to trigger search
*/
export const useSearchRequest = (options: {
- chainIds: CaipChainId[];
+ chainIds?: CaipChainId[];
query: string;
limit: number;
}) => {
- const { chainIds, query, limit } = options;
+ const { chainIds = [], query, limit } = options;
const [results, setResults] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
diff --git a/app/components/UI/Trending/hooks/useTrendingSearch/useTrendingSearch.test.ts b/app/components/UI/Trending/hooks/useTrendingSearch/useTrendingSearch.test.ts
index 8cb4b31f938..8e0ef466d22 100644
--- a/app/components/UI/Trending/hooks/useTrendingSearch/useTrendingSearch.test.ts
+++ b/app/components/UI/Trending/hooks/useTrendingSearch/useTrendingSearch.test.ts
@@ -96,6 +96,7 @@ describe('useTrendingSearch', () => {
});
it('returns combined search and trending results when search query provided', async () => {
+ // Search for 'ETH' which matches one trending result
mockUseSearchRequest.mockReturnValue({
results: mockSearchResults,
isLoading: false,
@@ -104,23 +105,24 @@ describe('useTrendingSearch', () => {
});
const { result } = renderHookWithProvider(() =>
- useTrendingSearch('USDC', 'h24_trending'),
+ useTrendingSearch('ETH', 'h24_trending'),
);
await waitFor(() => {
- expect(result.current.data).toHaveLength(3);
+ expect(result.current.data).toHaveLength(2);
});
+ // Should contain ETH from trending (matches query) and USDC from search
expect(result.current.data).toEqual(
expect.arrayContaining([
- ...mockTrendingResults,
+ expect.objectContaining({ symbol: 'ETH' }),
expect.objectContaining({ symbol: 'USDC' }),
]),
);
});
it('removes duplicate results when combining search and trending', async () => {
- const duplicateResult = mockTrendingResults[0];
+ const duplicateResult = mockTrendingResults[0]; // ETH
mockUseSearchRequest.mockReturnValue({
results: [duplicateResult, mockSearchResults[0]],
isLoading: false,
@@ -133,12 +135,20 @@ describe('useTrendingSearch', () => {
);
await waitFor(() => {
- expect(result.current.data).toHaveLength(3);
+ expect(result.current.data).toHaveLength(2);
});
+ // Should have ETH (deduplicated) and USDC (from search)
+ // DAI is filtered out because it doesn't match 'ETH' query
const assetIds = result.current.data.map((item) => item.assetId);
const uniqueAssetIds = new Set(assetIds);
expect(assetIds.length).toBe(uniqueAssetIds.size);
+ expect(result.current.data).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({ symbol: 'ETH' }),
+ expect.objectContaining({ symbol: 'USDC' }),
+ ]),
+ );
});
it('returns trending loading state when no search query', () => {
@@ -168,4 +178,73 @@ describe('useTrendingSearch', () => {
expect(result.current.isLoading).toBe(true);
});
+
+ describe('filtering trending results by query', () => {
+ it('returns all trending results when query is empty or whitespace', async () => {
+ const sortedResults = mockTrendingResults;
+ mockSortTrendingTokens.mockReturnValue(sortedResults);
+
+ const { result: result1 } = renderHookWithProvider(() =>
+ useTrendingSearch(''),
+ );
+ const { result: result2 } = renderHookWithProvider(() =>
+ useTrendingSearch(' '),
+ );
+
+ await waitFor(() => {
+ expect(result1.current.data).toEqual(sortedResults);
+ expect(result2.current.data).toEqual(sortedResults);
+ });
+ });
+
+ it('filters trending results by symbol case-insensitively', async () => {
+ const { result } = renderHookWithProvider(() => useTrendingSearch('eth'));
+
+ await waitFor(() => {
+ expect(result.current.data).toHaveLength(1);
+ expect(result.current.data[0].symbol).toBe('ETH');
+ });
+ });
+
+ it('filters trending results by name case-insensitively', async () => {
+ const { result } = renderHookWithProvider(() =>
+ useTrendingSearch('ethereum'),
+ );
+
+ await waitFor(() => {
+ expect(result.current.data).toHaveLength(1);
+ expect(result.current.data[0].name).toBe('Ethereum');
+ });
+ });
+
+ it('filters trending results by partial matches', async () => {
+ const { result } = renderHookWithProvider(() => useTrendingSearch('dai'));
+
+ await waitFor(() => {
+ expect(result.current.data).toHaveLength(1);
+ expect(result.current.data[0].symbol).toBe('DAI');
+ });
+ });
+
+ it('returns empty array when no trending results match query', async () => {
+ const { result } = renderHookWithProvider(() =>
+ useTrendingSearch('NonExistent'),
+ );
+
+ await waitFor(() => {
+ expect(result.current.data).toHaveLength(0);
+ });
+ });
+
+ it('trims whitespace from query before filtering', async () => {
+ const { result } = renderHookWithProvider(() =>
+ useTrendingSearch(' ETH '),
+ );
+
+ await waitFor(() => {
+ expect(result.current.data).toHaveLength(1);
+ expect(result.current.data[0].symbol).toBe('ETH');
+ });
+ });
+ });
});
diff --git a/app/components/UI/Trending/hooks/useTrendingSearch/useTrendingSearch.ts b/app/components/UI/Trending/hooks/useTrendingSearch/useTrendingSearch.ts
index e2a9ed53d5c..e05cf5970db 100644
--- a/app/components/UI/Trending/hooks/useTrendingSearch/useTrendingSearch.ts
+++ b/app/components/UI/Trending/hooks/useTrendingSearch/useTrendingSearch.ts
@@ -22,7 +22,6 @@ export const useTrendingSearch = (
useSearchRequest({
query: searchQuery || '',
limit: 20,
- chainIds: [],
});
const {
@@ -35,13 +34,21 @@ export const useTrendingSearch = (
});
const data = useMemo(() => {
- if (!searchQuery) {
+ if (!searchQuery?.trim()) {
return sortTrendingTokens(trendingResults, PriceChangeOption.PriceChange);
}
+ const query = searchQuery.toLowerCase().trim();
+
+ const filteredTrendingResults = trendingResults.filter(
+ (item) =>
+ item.symbol?.toLowerCase().includes(query) ||
+ item.name?.toLowerCase().includes(query),
+ );
+
// Combine trending and search results, avoiding duplicates
const resultMap = new Map(
- trendingResults.map((result) => [result.assetId, result]),
+ filteredTrendingResults.map((result) => [result.assetId, result]),
);
searchResults.forEach((result) => {
diff --git a/app/components/Views/SitesFullView/SitesFullView.test.tsx b/app/components/Views/SitesFullView/SitesFullView.test.tsx
index e94244ce848..0d90f27edae 100644
--- a/app/components/Views/SitesFullView/SitesFullView.test.tsx
+++ b/app/components/Views/SitesFullView/SitesFullView.test.tsx
@@ -158,6 +158,28 @@ describe('SitesFullView', () => {
},
];
+ const setupMockWithSearchFilter = () => {
+ mockUseSitesData.mockImplementation((searchQuery: string) => {
+ let filteredSites = mockSites;
+
+ if (searchQuery?.trim()) {
+ const query = searchQuery.toLowerCase().trim();
+ filteredSites = mockSites.filter(
+ (site) =>
+ site.name.toLowerCase().includes(query) ||
+ site.displayUrl.toLowerCase().includes(query) ||
+ site.url.toLowerCase().includes(query),
+ );
+ }
+
+ return {
+ sites: filteredSites,
+ isLoading: false,
+ refetch: mockRefetch,
+ };
+ });
+ };
+
beforeEach(() => {
jest.clearAllMocks();
mockRefetch.mockClear();
@@ -243,11 +265,7 @@ describe('SitesFullView', () => {
describe('Search Functionality', () => {
it('filters sites by name, URL, and display URL', () => {
- mockUseSitesData.mockReturnValue({
- sites: mockSites,
- isLoading: false,
- refetch: mockRefetch,
- });
+ setupMockWithSearchFilter();
const { getByTestId, queryByTestId } = render();
@@ -272,11 +290,7 @@ describe('SitesFullView', () => {
});
it('shows all sites when search query is empty', () => {
- mockUseSitesData.mockReturnValue({
- sites: mockSites,
- isLoading: false,
- refetch: mockRefetch,
- });
+ setupMockWithSearchFilter();
const { getByTestId } = render();
@@ -373,7 +387,7 @@ describe('SitesFullView', () => {
render();
- expect(mockUseSitesData).toHaveBeenCalledWith({ limit: 100 });
+ expect(mockUseSitesData).toHaveBeenCalledWith('', 100);
});
it('calls refetch when refresh is triggered', async () => {
@@ -442,11 +456,7 @@ describe('SitesFullView', () => {
});
it('performs case-insensitive search', () => {
- mockUseSitesData.mockReturnValue({
- sites: mockSites,
- isLoading: false,
- refetch: mockRefetch,
- });
+ setupMockWithSearchFilter();
const { getByTestId, queryByTestId } = render();
diff --git a/app/components/Views/SitesFullView/SitesFullView.tsx b/app/components/Views/SitesFullView/SitesFullView.tsx
index abc6783059f..55d0a0feca7 100644
--- a/app/components/Views/SitesFullView/SitesFullView.tsx
+++ b/app/components/Views/SitesFullView/SitesFullView.tsx
@@ -47,22 +47,7 @@ const SitesFullView: React.FC = () => {
sites,
isLoading,
refetch: refetchSites,
- } = useSitesData({ limit: 100 });
-
- // Filter sites based on search query
- const filteredSites = useMemo(() => {
- if (!searchQuery.trim()) {
- return sites;
- }
-
- const query = searchQuery.toLowerCase();
- return sites.filter(
- (site) =>
- site.name.toLowerCase().includes(query) ||
- site.displayUrl.toLowerCase().includes(query) ||
- site.url.toLowerCase().includes(query),
- );
- }, [sites, searchQuery]);
+ } = useSitesData(searchQuery, 100);
const handleBackPress = useCallback(() => {
navigation.goBack();
@@ -132,7 +117,7 @@ const SitesFullView: React.FC = () => {
) : (
{
fetch: jest.fn(),
});
- const { getByTestId } = renderWithProvider(
+ const { queryAllByTestId } = renderWithProvider(
,
{ state: mockState },
false,
);
- expect(getByTestId('trending-tokens-skeleton')).toBeOnTheScreen();
+ const skeletons = queryAllByTestId('trending-tokens-skeleton');
+ expect(skeletons.length).toBeGreaterThan(0);
+ expect(skeletons[0]).toBeOnTheScreen();
});
it('displays skeleton loader when results are empty', () => {
@@ -341,13 +343,15 @@ describe('TrendingTokensFullView', () => {
fetch: jest.fn(),
});
- const { getByTestId } = renderWithProvider(
+ const { queryAllByTestId } = renderWithProvider(
,
{ state: mockState },
false,
);
- expect(getByTestId('trending-tokens-skeleton')).toBeOnTheScreen();
+ const skeletons = queryAllByTestId('trending-tokens-skeleton');
+ expect(skeletons.length).toBeGreaterThan(0);
+ expect(skeletons[0]).toBeOnTheScreen();
});
it('displays trending tokens list when data is loaded', () => {
diff --git a/app/components/Views/TrendingTokens/TrendingTokensFullView/TrendingTokensFullView.tsx b/app/components/Views/TrendingTokens/TrendingTokensFullView/TrendingTokensFullView.tsx
index 2ea31d167af..a14fb33b3f9 100644
--- a/app/components/Views/TrendingTokens/TrendingTokensFullView/TrendingTokensFullView.tsx
+++ b/app/components/Views/TrendingTokens/TrendingTokensFullView/TrendingTokensFullView.tsx
@@ -40,7 +40,6 @@ import {
TimeOption,
} from '../../../UI/Trending/components/TrendingTokensBottomSheet';
import { sortTrendingTokens } from '../../../UI/Trending/utils/sortTrendingTokens';
-import { SECTIONS_CONFIG } from '../../TrendingView/config/sections.config';
import { useTrendingSearch } from '../../../UI/Trending/hooks/useTrendingSearch/useTrendingSearch';
interface TrendingTokensNavigationParamList {
@@ -116,8 +115,6 @@ const createStyles = (theme: Theme) =>
},
});
-const MAX_TOKENS = 100;
-
const TrendingTokensFullView = () => {
const navigation =
useNavigation>();
@@ -199,32 +196,11 @@ const TrendingTokensFullView = () => {
// - When no search query: returns trending results from useTrendingRequest
// - When search query exists: returns merged trending + search results
const {
- data: tokensSectionData,
+ data: searchResults,
isLoading,
refetch: refetchTokensSection,
} = useTrendingSearch(searchQuery || undefined, sortBy, selectedNetwork);
- const searchResults = useMemo(() => {
- // When search is not active, use the full section data
- if (!isSearchVisible) {
- return tokensSectionData as TrendingAsset[];
- }
-
- const searchTerm = searchQuery.toLowerCase().trim();
-
- // If search box is empty, still use full section data
- if (!searchTerm) {
- return tokensSectionData as TrendingAsset[];
- }
-
- const tokensSectionConfig = SECTIONS_CONFIG.tokens;
-
- // Filter section data based on searchable text (symbol + name)
- return (tokensSectionData as unknown[]).filter((item) =>
- tokensSectionConfig.getSearchableText(item).includes(searchTerm),
- ) as TrendingAsset[];
- }, [isSearchVisible, searchQuery, tokensSectionData]);
-
// Sort and display tokens based on selected option and direction
const trendingTokens = useMemo(() => {
// Early return if no results
@@ -232,22 +208,20 @@ const TrendingTokensFullView = () => {
return [];
}
- const filteredResults = searchResults;
-
// If no sort option selected, return filtered results as-is (already sorted by API)
if (!selectedPriceChangeOption) {
- return filteredResults.slice(0, MAX_TOKENS);
+ return searchResults;
}
// Sort using the shared utility function
const sorted = sortTrendingTokens(
- filteredResults,
+ searchResults,
selectedPriceChangeOption,
priceChangeSortDirection,
selectedTimeOption,
);
- return sorted.slice(0, MAX_TOKENS);
+ return sorted;
}, [
searchResults,
selectedPriceChangeOption,
@@ -394,7 +368,9 @@ const TrendingTokensFullView = () => {
{isLoading || (searchResults as TrendingAsset[]).length === 0 ? (
-
+ {Array.from({ length: 10 }).map((_, index) => (
+
+ ))}
) : (
diff --git a/app/components/Views/TrendingView/ExploreSearchScreen/components/ExploreSearchResults/ExploreSearchResults.tsx b/app/components/Views/TrendingView/ExploreSearchScreen/components/ExploreSearchResults/ExploreSearchResults.tsx
index a9bce9bcdda..590b05fd217 100644
--- a/app/components/Views/TrendingView/ExploreSearchScreen/components/ExploreSearchResults/ExploreSearchResults.tsx
+++ b/app/components/Views/TrendingView/ExploreSearchScreen/components/ExploreSearchResults/ExploreSearchResults.tsx
@@ -143,7 +143,7 @@ const ExploreSearchResults: React.FC = ({
return `skeleton-${item.sectionId}-${item.index}`;
const section = SECTIONS_CONFIG[item.sectionId];
- return section ? section.keyExtractor(item.data) : `item-${index}`;
+ return section ? `${section.id}-${index}` : `item-${index}`;
}, []);
return (
diff --git a/app/components/Views/TrendingView/ExploreSearchScreen/components/ExploreSearchResults/config/useExploreSearch.test.ts b/app/components/Views/TrendingView/ExploreSearchScreen/components/ExploreSearchResults/config/useExploreSearch.test.ts
index cc4eca8f44c..2fb5233e81f 100644
--- a/app/components/Views/TrendingView/ExploreSearchScreen/components/ExploreSearchResults/config/useExploreSearch.test.ts
+++ b/app/components/Views/TrendingView/ExploreSearchScreen/components/ExploreSearchResults/config/useExploreSearch.test.ts
@@ -46,59 +46,64 @@ const mockSites = [
},
];
-const mockUseTrendingSearch = jest.fn();
-const mockUsePerpsMarkets = jest.fn();
-const mockUsePredictMarketData = jest.fn();
-const mockUseSitesData = jest.fn();
+let mockTrendingData = mockTrendingTokens;
+let mockTrendingLoading = false;
+let mockPerpsData = mockPerpsMarkets;
+let mockPerpsLoading = false;
+let mockPredictionsData = mockPredictionMarkets;
+let mockPredictionsLoading = false;
+let mockSitesData = mockSites;
+let mockSitesLoading = false;
jest.mock(
'../../../../../../UI/Trending/hooks/useTrendingSearch/useTrendingSearch',
() => ({
- useTrendingSearch: () => mockUseTrendingSearch(),
+ useTrendingSearch: () => ({
+ data: mockTrendingData,
+ isLoading: mockTrendingLoading,
+ refetch: jest.fn(),
+ }),
}),
);
jest.mock('../../../../../../UI/Perps/hooks/usePerpsMarkets', () => ({
- usePerpsMarkets: () => mockUsePerpsMarkets(),
+ usePerpsMarkets: () => ({
+ markets: mockPerpsData,
+ isLoading: mockPerpsLoading,
+ refresh: jest.fn(),
+ isRefreshing: false,
+ }),
}));
jest.mock('../../../../../../UI/Predict/hooks/usePredictMarketData', () => ({
- usePredictMarketData: () => mockUsePredictMarketData(),
+ usePredictMarketData: () => ({
+ marketData: mockPredictionsData,
+ isFetching: mockPredictionsLoading,
+ refetch: jest.fn(),
+ }),
}));
jest.mock('../../../../../../UI/Sites/hooks/useSiteData/useSitesData', () => ({
- useSitesData: () => mockUseSitesData(),
+ useSitesData: () => ({
+ sites: mockSitesData,
+ isLoading: mockSitesLoading,
+ refetch: jest.fn(),
+ }),
}));
describe('useExploreSearch', () => {
beforeEach(() => {
- jest.clearAllMocks();
jest.useFakeTimers();
- mockUseTrendingSearch.mockReturnValue({
- data: mockTrendingTokens,
- isLoading: false,
- refetch: jest.fn(),
- });
-
- mockUsePerpsMarkets.mockReturnValue({
- markets: mockPerpsMarkets,
- isLoading: false,
- refresh: jest.fn(),
- isRefreshing: false,
- });
-
- mockUsePredictMarketData.mockReturnValue({
- marketData: mockPredictionMarkets,
- isFetching: false,
- refetch: jest.fn(),
- });
-
- mockUseSitesData.mockReturnValue({
- sites: mockSites,
- isLoading: false,
- refetch: jest.fn(),
- });
+ // Reset to default values
+ mockTrendingData = mockTrendingTokens;
+ mockTrendingLoading = false;
+ mockPerpsData = mockPerpsMarkets;
+ mockPerpsLoading = false;
+ mockPredictionsData = mockPredictionMarkets;
+ mockPredictionsLoading = false;
+ mockSitesData = mockSites;
+ mockSitesLoading = false;
});
afterEach(() => {
@@ -124,113 +129,13 @@ describe('useExploreSearch', () => {
expect(result.current.data.sites).toHaveLength(3);
});
- it('filters tokens by symbol when query matches', async () => {
- const { result, rerender } = renderHook(
- ({ query }) => useExploreSearch(query),
- { initialProps: { query: '' } },
- );
-
- rerender({ query: 'btc' });
-
- await act(async () => {
- jest.advanceTimersByTime(200);
- });
-
- await waitFor(() => {
- expect(result.current.data.tokens).toHaveLength(1);
- expect((result.current.data.tokens[0] as { symbol: string }).symbol).toBe(
- 'BTC',
- );
- });
- });
-
- it('filters tokens by name when query matches', async () => {
- const { result, rerender } = renderHook(
- ({ query }) => useExploreSearch(query),
- { initialProps: { query: '' } },
- );
-
- rerender({ query: 'ethereum' });
-
- await act(async () => {
- jest.advanceTimersByTime(200);
- });
-
- await waitFor(() => {
- expect(result.current.data.tokens).toHaveLength(1);
- expect((result.current.data.tokens[0] as { name: string }).name).toBe(
- 'Ethereum',
- );
- });
- });
-
- it('performs case insensitive search', async () => {
- const { result, rerender } = renderHook(
- ({ query }) => useExploreSearch(query),
- { initialProps: { query: '' } },
- );
-
- rerender({ query: 'BITCOIN' });
-
- await act(async () => {
- jest.advanceTimersByTime(200);
- });
-
- await waitFor(() => {
- expect(result.current.data.tokens.length).toBeGreaterThan(0);
- expect((result.current.data.tokens[0] as { name: string }).name).toBe(
- 'Bitcoin',
- );
- });
- });
-
- it('filters perps markets by symbol', async () => {
- const { result, rerender } = renderHook(
- ({ query }) => useExploreSearch(query),
- { initialProps: { query: '' } },
- );
-
- rerender({ query: 'doge' });
-
- await act(async () => {
- jest.advanceTimersByTime(200);
- });
-
- await waitFor(() => {
- expect(result.current.data.perps).toHaveLength(1);
- expect((result.current.data.perps[0] as { symbol: string }).symbol).toBe(
- 'DOGE-USD',
- );
- });
- });
-
- it('filters predictions by title', async () => {
- const { result, rerender } = renderHook(
- ({ query }) => useExploreSearch(query),
- { initialProps: { query: '' } },
- );
+ it('returns empty arrays when section hooks return no data', async () => {
+ mockTrendingData = [];
+ mockPerpsData = [];
+ mockPredictionsData = [];
+ mockSitesData = [];
- rerender({ query: 'trump' });
-
- await act(async () => {
- jest.advanceTimersByTime(200);
- });
-
- await waitFor(() => {
- expect(result.current.data.predictions).toHaveLength(1);
- expect(
- (result.current.data.predictions[0] as { title: string }).title,
- ).toBe('Trump election results');
- });
- });
-
- it('returns empty arrays when no items match query', async () => {
- const { result, rerender } = renderHook(
- ({ query }) => useExploreSearch(query),
- { initialProps: { query: '' } },
- );
-
- rerender({ query: 'nonexistent' });
+ const { result } = renderHook(() => useExploreSearch('test'));
await act(async () => {
jest.advanceTimersByTime(200);
@@ -254,76 +159,61 @@ describe('useExploreSearch', () => {
rerender({ query: 'btc' });
+ // Before debounce completes, should still show initial count (top 3)
await act(async () => {
jest.advanceTimersByTime(100);
});
expect(result.current.data.tokens.length).toBe(initialTokenCount);
+ // After full debounce time, query should be processed
await act(async () => {
jest.advanceTimersByTime(100);
});
await waitFor(() => {
- expect(result.current.data.tokens.length).toBeLessThan(initialTokenCount);
+ expect(result.current.data.tokens.length).toBeGreaterThan(0);
});
});
- it('returns loading states for each section', () => {
- mockUseTrendingSearch.mockReturnValue({
- data: [],
- isLoading: true,
- refetch: jest.fn(),
- });
-
- mockUsePerpsMarkets.mockReturnValue({
- markets: [],
- isLoading: true,
- refresh: jest.fn(),
- isRefreshing: false,
- });
-
- mockUsePredictMarketData.mockReturnValue({
- marketData: [],
- isFetching: true,
- refetch: jest.fn(),
- });
+ it('shows loading state while debouncing', async () => {
+ const { result, rerender } = renderHook(
+ ({ query }) => useExploreSearch(query),
+ { initialProps: { query: '' } },
+ );
- mockUseSitesData.mockReturnValue({
- sites: [],
- isLoading: true,
- refetch: jest.fn(),
- });
+ expect(result.current.isLoading.tokens).toBe(false);
- const { result } = renderHook(() => useExploreSearch(''));
+ rerender({ query: 'test' });
expect(result.current.isLoading.tokens).toBe(true);
expect(result.current.isLoading.perps).toBe(true);
expect(result.current.isLoading.predictions).toBe(true);
expect(result.current.isLoading.sites).toBe(true);
- });
-
- it('filters across multiple sections simultaneously', async () => {
- const { result, rerender } = renderHook(
- ({ query }) => useExploreSearch(query),
- { initialProps: { query: '' } },
- );
-
- rerender({ query: 'sol' });
await act(async () => {
jest.advanceTimersByTime(200);
});
await waitFor(() => {
- const hasTokenMatch = result.current.data.tokens.length > 0;
- const hasPerpsMatch = result.current.data.perps.length > 0;
- const hasPredictionsMatch = result.current.data.predictions.length > 0;
-
- expect(hasTokenMatch || hasPerpsMatch || hasPredictionsMatch).toBe(true);
+ expect(result.current.isLoading.tokens).toBe(false);
});
});
+ it('aggregates loading states from section hooks', () => {
+ mockTrendingLoading = true;
+ mockPerpsLoading = true;
+ mockPredictionsLoading = true;
+ mockSitesLoading = true;
+
+ const { result } = renderHook(() => useExploreSearch(''));
+
+ expect(result.current.isLoading.tokens).toBe(true);
+ expect(result.current.isLoading.perps).toBe(true);
+ expect(result.current.isLoading.predictions).toBe(true);
+ expect(result.current.isLoading.sites).toBe(true);
+ });
+
it('processes all sections defined in config', () => {
const { result } = renderHook(() => useExploreSearch(''));
diff --git a/app/components/Views/TrendingView/ExploreSearchScreen/components/ExploreSearchResults/config/useExploreSearch.ts b/app/components/Views/TrendingView/ExploreSearchScreen/components/ExploreSearchResults/config/useExploreSearch.ts
index 376d8e53ba5..990f9308e57 100644
--- a/app/components/Views/TrendingView/ExploreSearchScreen/components/ExploreSearchResults/config/useExploreSearch.ts
+++ b/app/components/Views/TrendingView/ExploreSearchScreen/components/ExploreSearchResults/config/useExploreSearch.ts
@@ -50,7 +50,6 @@ export const useExploreSearch = (query: string): ExploreSearchResult => {
>;
const shouldShowTopItems = !debouncedQuery.trim();
- const searchTerm = debouncedQuery.toLowerCase();
// Process each section generically
SECTIONS_ARRAY.forEach((section) => {
@@ -64,9 +63,7 @@ export const useExploreSearch = (query: string): ExploreSearchResult => {
data[section.id] = sectionData.data.slice(0, 3);
} else {
// Filter items based on section's searchable text
- data[section.id] = sectionData.data.filter((item) =>
- section.getSearchableText(item).includes(searchTerm),
- );
+ data[section.id] = sectionData.data;
}
});
diff --git a/app/components/Views/TrendingView/components/SectionCard/SectionCard.tsx b/app/components/Views/TrendingView/components/SectionCard/SectionCard.tsx
index 3fa1d09bbf3..b8e561a9a17 100644
--- a/app/components/Views/TrendingView/components/SectionCard/SectionCard.tsx
+++ b/app/components/Views/TrendingView/components/SectionCard/SectionCard.tsx
@@ -58,7 +58,7 @@ const SectionCard: React.FC = ({
section.keyExtractor(item)}
+ keyExtractor={(_, index) => `${section.id}-${index}`}
keyboardShouldPersistTaps="handled"
testID="perps-tokens-list"
/>
diff --git a/app/components/Views/TrendingView/components/SectionCarrousel/SectionCarrousel.tsx b/app/components/Views/TrendingView/components/SectionCarrousel/SectionCarrousel.tsx
index 858c40355fe..98b770f0095 100644
--- a/app/components/Views/TrendingView/components/SectionCarrousel/SectionCarrousel.tsx
+++ b/app/components/Views/TrendingView/components/SectionCarrousel/SectionCarrousel.tsx
@@ -63,7 +63,7 @@ const SectionCarrousel: React.FC = ({
keyExtractor={
isLoading
? (_, index) => `skeleton-${index}`
- : (item) => section.keyExtractor(item)
+ : (_, index) => `${section.id}-${index}`
}
horizontal
pagingEnabled={false}
diff --git a/app/components/Views/TrendingView/config/sections.config.tsx b/app/components/Views/TrendingView/config/sections.config.tsx
index 45887c5c11d..a5c4669979b 100644
--- a/app/components/Views/TrendingView/config/sections.config.tsx
+++ b/app/components/Views/TrendingView/config/sections.config.tsx
@@ -6,7 +6,6 @@ import { strings } from '../../../../../locales/i18n';
import TrendingTokenRowItem from '../../../UI/Trending/components/TrendingTokenRowItem/TrendingTokenRowItem';
import TrendingTokensSkeleton from '../../../UI/Trending/components/TrendingTokenSkeleton/TrendingTokensSkeleton';
import PerpsMarketRowItem from '../../../UI/Perps/components/PerpsMarketRowItem';
-import PerpsMarketRowSkeleton from '../../../UI/Perps/Views/PerpsMarketListView/components/PerpsMarketRowSkeleton';
import type { PerpsMarketData } from '../../../UI/Perps/controllers/types';
import PredictMarket from '../../../UI/Predict/components/PredictMarket';
import type { PredictMarket as PredictMarketType } from '../../../UI/Predict/types';
@@ -24,6 +23,7 @@ import SiteRowItemWrapper from '../../../UI/Sites/components/SiteRowItemWrapper/
import SiteSkeleton from '../../../UI/Sites/components/SiteSkeleton/SiteSkeleton';
import { useSitesData } from '../../../UI/Sites/hooks/useSiteData/useSitesData';
import { useTrendingSearch } from '../../../UI/Trending/hooks/useTrendingSearch/useTrendingSearch';
+import { filterMarketsByQuery } from '../../../UI/Perps/utils/marketUtils';
export type SectionId = 'predictions' | 'tokens' | 'perps' | 'sites';
@@ -43,8 +43,6 @@ interface SectionConfig {
navigation: NavigationProp;
}>;
Skeleton: React.ComponentType;
- getSearchableText: (item: unknown) => string;
- keyExtractor: (item: unknown) => string;
Section: React.ComponentType<{ refreshTrigger?: number }>;
useSectionData: (searchQuery?: string) => {
data: unknown[];
@@ -81,15 +79,11 @@ export const SECTIONS_CONFIG: Record = {
),
Skeleton: () => ,
- getSearchableText: (item) =>
- `${(item as TrendingAsset).symbol} ${(item as TrendingAsset).name}`.toLowerCase(),
- keyExtractor: (item) => `token-${(item as TrendingAsset).assetId}`,
Section: ({ refreshTrigger }) => (
),
useSectionData: (searchQuery) => {
const { data, isLoading, refetch } = useTrendingSearch(searchQuery);
-
return { data, isLoading, refetch };
},
},
@@ -120,10 +114,8 @@ export const SECTIONS_CONFIG: Record = {
showBadge={false}
/>
),
- Skeleton: () => ,
- getSearchableText: (item) =>
- `${(item as PerpsMarketData).symbol} ${(item as PerpsMarketData).name || ''}`.toLowerCase(),
- keyExtractor: (item) => `perp-${(item as PerpsMarketData).symbol}`,
+ // Using trending skeleton cause PerpsMarketRowSkeleton has too much spacing
+ Skeleton: () => ,
Section: ({ refreshTrigger }) => (
@@ -131,11 +123,15 @@ export const SECTIONS_CONFIG: Record = {
),
- useSectionData: () => {
+ useSectionData: (searchQuery) => {
const { markets, isLoading, refresh, isRefreshing } = usePerpsMarkets();
+ const filteredMarkets = searchQuery
+ ? filterMarketsByQuery(markets, searchQuery)
+ : markets;
+
return {
- data: markets,
+ data: filteredMarkets,
isLoading: isLoading || isRefreshing,
refetch: refresh,
};
@@ -156,9 +152,6 @@ export const SECTIONS_CONFIG: Record = {
),
Skeleton: () => ,
- getSearchableText: (item) =>
- (item as PredictMarketType).title.toLowerCase(),
- keyExtractor: (item) => `prediction-${(item as PredictMarketType).id}`,
Section: ({ refreshTrigger }) => (
= {
),
Skeleton: () => ,
- getSearchableText: (item) =>
- `${(item as SiteData).name} ${(item as SiteData).displayUrl}`.toLowerCase(),
- keyExtractor: (item) => `site-${(item as SiteData).id}`,
Section: ({ refreshTrigger }) => (
),
- useSectionData: () => {
- const { sites, isLoading, refetch } = useSitesData({ limit: 100 });
+ useSectionData: (searchQuery) => {
+ const { sites, isLoading, refetch } = useSitesData(searchQuery, 100);
return { data: sites, isLoading, refetch };
},
},
@@ -224,7 +214,7 @@ export const SECTIONS_ARRAY: (SectionConfig & { id: SectionId })[] = [
* @returns Data and loading state for all sections
*/
export const useSectionsData = (
- searchQuery?: string,
+ searchQuery: string,
): Record => {
const { data: trendingTokens, isLoading: isTokensLoading } =
SECTIONS_CONFIG.tokens.useSectionData(searchQuery);
From 676610eb0be454701c78be39da6d753371cbe7db Mon Sep 17 00:00:00 2001
From: MetaMask Bot <37885440+metamaskbot@users.noreply.github.com>
Date: Fri, 28 Nov 2025 02:07:26 +0100
Subject: [PATCH 2/2] chore: Bump main version to 7.62.0 (#23382)
## Version Bump After Release
This PR bumps the main branch version from 7.61.0 to 7.62.0 after
cutting the release branch.
### Why this is needed:
- **Nightly builds**: Each nightly build needs to be one minor version
ahead of the current release candidate
- **Version conflicts**: Prevents conflicts between nightlies and
release candidates
- **Platform alignment**: Maintains version alignment between MetaMask
mobile and extension
- **Update systems**: Ensures nightlies are accepted by app stores and
browser update systems
### What changed:
- Version bumped from `7.61.0` to `7.62.0`
- Platform: `mobile`
- Files updated by `set-semvar-version.sh` script
### Next steps:
This PR should be **manually reviewed and merged by the release
manager** to maintain proper version flow.
### Related:
- Release version: 7.61.0
- Release branch: release/7.61.0
- Platform: mobile
- Test mode: false
---
*This PR was automatically created by the
`create-platform-release-pr.sh` script.*
Co-authored-by: metamaskbot
---
android/app/build.gradle | 2 +-
bitrise.yml | 4 ++--
ios/MetaMask.xcodeproj/project.pbxproj | 12 ++++++------
package.json | 2 +-
4 files changed, 10 insertions(+), 10 deletions(-)
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 88871c88647..4f06959781d 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -187,7 +187,7 @@ android {
applicationId "io.metamask"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
- versionName "7.61.0"
+ versionName "7.62.0"
versionCode 3092
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
diff --git a/bitrise.yml b/bitrise.yml
index 0563c419dcf..bde73a91159 100644
--- a/bitrise.yml
+++ b/bitrise.yml
@@ -3574,13 +3574,13 @@ app:
PROJECT_LOCATION_IOS: ios
- opts:
is_expand: false
- VERSION_NAME: 7.61.0
+ VERSION_NAME: 7.62.0
- opts:
is_expand: false
VERSION_NUMBER: 3092
- opts:
is_expand: false
- FLASK_VERSION_NAME: 7.61.0
+ FLASK_VERSION_NAME: 7.62.0
- opts:
is_expand: false
FLASK_VERSION_NUMBER: 3092
diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj
index 3a223be784a..4dbf5c3ab3f 100644
--- a/ios/MetaMask.xcodeproj/project.pbxproj
+++ b/ios/MetaMask.xcodeproj/project.pbxproj
@@ -1319,7 +1319,7 @@
"${inherited}",
);
LLVM_LTO = YES;
- MARKETING_VERSION = 7.61.0;
+ MARKETING_VERSION = 7.62.0;
ONLY_ACTIVE_ARCH = YES;
OTHER_CFLAGS = "$(inherited)";
OTHER_LDFLAGS = (
@@ -1385,7 +1385,7 @@
"${inherited}",
);
LLVM_LTO = YES;
- MARKETING_VERSION = 7.61.0;
+ MARKETING_VERSION = 7.62.0;
ONLY_ACTIVE_ARCH = NO;
OTHER_CFLAGS = "$(inherited)";
OTHER_LDFLAGS = (
@@ -1454,7 +1454,7 @@
"\"$(SRCROOT)/MetaMask/System/Library/Frameworks\"",
);
LLVM_LTO = YES;
- MARKETING_VERSION = 7.61.0;
+ MARKETING_VERSION = 7.62.0;
ONLY_ACTIVE_ARCH = YES;
OTHER_CFLAGS = "$(inherited)";
OTHER_LDFLAGS = (
@@ -1518,7 +1518,7 @@
"\"$(SRCROOT)/MetaMask/System/Library/Frameworks\"",
);
LLVM_LTO = YES;
- MARKETING_VERSION = 7.61.0;
+ MARKETING_VERSION = 7.62.0;
ONLY_ACTIVE_ARCH = NO;
OTHER_CFLAGS = "$(inherited)";
OTHER_LDFLAGS = (
@@ -1684,7 +1684,7 @@
"\"$(SRCROOT)/MetaMask/System/Library/Frameworks\"",
);
LLVM_LTO = YES;
- MARKETING_VERSION = 7.61.0;
+ MARKETING_VERSION = 7.62.0;
ONLY_ACTIVE_ARCH = YES;
OTHER_CFLAGS = (
"$(inherited)",
@@ -1751,7 +1751,7 @@
"\"$(SRCROOT)/MetaMask/System/Library/Frameworks\"",
);
LLVM_LTO = YES;
- MARKETING_VERSION = 7.61.0;
+ MARKETING_VERSION = 7.62.0;
ONLY_ACTIVE_ARCH = NO;
OTHER_CFLAGS = (
"$(inherited)",
diff --git a/package.json b/package.json
index 3802e66557c..fb769b492e3 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "metamask",
- "version": "7.61.0",
+ "version": "7.62.0",
"private": true,
"scripts": {
"install:foundryup": "yarn mm-foundryup",