Skip to content

Commit 6b9885e

Browse files
authored
chore: pass cached security data from token list to token details cp-7.76.0 (#29603)
## **Description** When the token list security badges feature flag is enabled, read the already-fetched security data from the TanStack Query cache on item press and forward it via navigation params, eliminating the on-demand fetch in TokenDetails. ## **Changelog** <!-- If this PR is not End-User-Facing and should not show up in the CHANGELOG, you can choose to either: 1. Write `CHANGELOG entry: null` 2. Label with `no-changelog` If this PR is End-User-Facing, please write a short User-Facing description in the past tense like: `CHANGELOG entry: Added a new tab for users to see their NFTs` `CHANGELOG entry: Fixed a bug that was causing some NFTs to flicker` (This helps the Release Engineer do their job more quickly and accurately) --> CHANGELOG entry: remove on demand api call to get security data when passed from token list and FF is ON ## **Related issues** Fixes: ## **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** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **Pre-merge author checklist** <!-- Every checklist item must be consciously assessed before marking this PR as "Ready for review". A checked box means you deliberately considered that responsibility, not that you literally performed every action listed. Unchecked boxes are ambiguous: they are not an implicit "N/A" and they are not a silent "skip". See `docs/readme/ready-for-review.md` for the full checklist semantics. --> - [ ] 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. #### Performance checks (if applicable) - [ ] I've tested on Android - Ideally on a mid-range device; emulator is acceptable - [ ] I've tested with a power user scenario - Use these [power-user SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93) to import wallets with many accounts and tokens - [ ] I've instrumented key operations with Sentry traces for production performance metrics - See [`trace()`](/app/util/trace.ts) for usage and [`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274) for an example For performance guidelines and tooling, see the [Performance Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers). ## **Pre-merge reviewer checklist** <!-- Reviewer checklist items follow the same semantics as the author checklist: an unchecked box is ambiguous, a checked box means the reviewer consciously assessed that responsibility. See `docs/readme/ready-for-review.md`. --> - [ ] 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. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes the token list tap navigation payload to optionally include cached `TokenSecurityData` from TanStack Query, which could affect the Asset Details screen’s behavior when the security-badges feature flag is enabled. Risk is limited by feature-flag gating but still touches navigation params and cache key usage. > > **Overview** > When `selectTokenListSecurityBadgesEnabled` is on and a CAIP asset id has been resolved, tapping a token in `TokenListItem` now reads `TokenSecurityData` from the TanStack Query cache (`tokenListSecurityBadgeKeys.byAsset(caipId)`) and forwards it to the `Asset` route via `securityData` navigation params. > > Adds/updates unit tests to mock `useQueryClient` + CAIP resolution and assert `securityData` is included only when the flag is enabled and cached data exists. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 224e470. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 484fa2b commit 6b9885e

2 files changed

Lines changed: 197 additions & 4 deletions

File tree

app/components/UI/Tokens/TokenList/TokenListItem/TokenListItem.test.tsx

Lines changed: 174 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import {
2424
selectCurrentCurrency,
2525
} from '../../../../../selectors/currencyRateController';
2626

27+
import { useQuery } from '@tanstack/react-query';
28+
import { selectTokenListSecurityBadgesEnabled } from '../../../../../selectors/featureFlagController/tokenListSecurityBadges';
2729
import { useMusdConversionTokens } from '../../../Earn/hooks/useMusdConversionTokens';
2830
import { useMusdConversionEligibility } from '../../../Earn/hooks/useMusdConversionEligibility';
2931
import {
@@ -53,11 +55,13 @@ jest.mock('../../../Bridge/hooks/useRWAToken', () => ({
5355
}));
5456

5557
// CAIP resolution uses useQuery; these tests use renderWithProvider (no QueryClient).
58+
const mockGetQueryData = jest.fn();
5659
jest.mock('@tanstack/react-query', () => {
5760
const actual = jest.requireActual('@tanstack/react-query');
5861
return {
5962
...actual,
6063
useQuery: jest.fn(() => ({ data: undefined })),
64+
useQueryClient: jest.fn(() => ({ getQueryData: mockGetQueryData })),
6165
};
6266
});
6367

@@ -82,10 +86,11 @@ jest.mock('../../../shared/StockBadge', () => {
8286
});
8387

8488
// Mock dependencies
89+
const mockNavigate = jest.fn();
8590
jest.mock('@react-navigation/native', () => ({
8691
...jest.requireActual('@react-navigation/native'),
8792
useNavigation: () => ({
88-
navigate: jest.fn(),
93+
navigate: mockNavigate,
8994
}),
9095
}));
9196

@@ -1367,4 +1372,172 @@ describe('TokenListItem - Component Rendering Tests for Coverage', () => {
13671372
expect(getByText('TEST')).toBeOnTheScreen();
13681373
});
13691374
});
1375+
1376+
describe('Security data passed to Token Details on press', () => {
1377+
const mockUseQuery = useQuery as jest.MockedFunction<typeof useQuery>;
1378+
1379+
const assetKey: FlashListAssetKey = {
1380+
address: '0x456',
1381+
chainId: '0x1',
1382+
isStaked: false,
1383+
};
1384+
1385+
const mockSecurityData = {
1386+
resultType: 'Verified',
1387+
maliciousScore: '0',
1388+
features: [],
1389+
fees: { transfer: 0, transferFeeMaxAmount: null, buy: 0, sell: null },
1390+
financialStats: {
1391+
supply: 1000,
1392+
topHolders: [],
1393+
holdersCount: 10,
1394+
tradeVolume24h: null,
1395+
lockedLiquidityPct: null,
1396+
markets: [],
1397+
},
1398+
metadata: {
1399+
externalLinks: {
1400+
homepage: null,
1401+
twitterPage: null,
1402+
telegramChannelId: null,
1403+
},
1404+
},
1405+
created: '2023-01-01T00:00:00Z',
1406+
};
1407+
1408+
it('passes cached security data in nav params when feature flag is ON', () => {
1409+
prepareMocks({ asset: defaultAsset });
1410+
1411+
// Enable the feature flag and basicFunctionality
1412+
mockUseSelector.mockImplementation(
1413+
(selector: (state: unknown) => unknown) => {
1414+
if (selector === selectTokenListSecurityBadgesEnabled) {
1415+
return true;
1416+
}
1417+
const selectorString = selector.toString();
1418+
if (selectorString.includes('basicFunctionalityEnabled')) {
1419+
return true;
1420+
}
1421+
if (selectorString.includes('selectAsset')) {
1422+
return defaultAsset;
1423+
}
1424+
return {};
1425+
},
1426+
);
1427+
1428+
// Mock useQuery to return a resolved CAIP asset ID
1429+
const caipId = 'eip155:1/erc20:0x456';
1430+
mockUseQuery.mockReturnValue({ data: caipId } as ReturnType<
1431+
typeof useQuery
1432+
>);
1433+
1434+
// Mock cached security data
1435+
mockGetQueryData.mockReturnValue(mockSecurityData);
1436+
1437+
const { getByText } = renderWithProvider(
1438+
<TokenListItem
1439+
assetKey={assetKey}
1440+
showRemoveMenu={jest.fn()}
1441+
setShowScamWarningModal={jest.fn()}
1442+
privacyMode={false}
1443+
shouldShowTokenListItemCta={mockshouldShowTokenListItemCta}
1444+
/>,
1445+
);
1446+
1447+
fireEvent.press(getByText('Test Token'));
1448+
1449+
expect(mockNavigate).toHaveBeenCalledWith(
1450+
'Asset',
1451+
expect.objectContaining({ securityData: mockSecurityData }),
1452+
);
1453+
});
1454+
1455+
it('does not pass security data in nav params when feature flag is OFF', () => {
1456+
prepareMocks({ asset: defaultAsset });
1457+
1458+
// Feature flag OFF (default)
1459+
mockUseSelector.mockImplementation(
1460+
(selector: (state: unknown) => unknown) => {
1461+
if (selector === selectTokenListSecurityBadgesEnabled) {
1462+
return false;
1463+
}
1464+
const selectorString = selector.toString();
1465+
if (selectorString.includes('basicFunctionalityEnabled')) {
1466+
return true;
1467+
}
1468+
if (selectorString.includes('selectAsset')) {
1469+
return defaultAsset;
1470+
}
1471+
return {};
1472+
},
1473+
);
1474+
1475+
mockUseQuery.mockReturnValue({ data: undefined } as ReturnType<
1476+
typeof useQuery
1477+
>);
1478+
mockGetQueryData.mockReturnValue(mockSecurityData);
1479+
1480+
const { getByText } = renderWithProvider(
1481+
<TokenListItem
1482+
assetKey={assetKey}
1483+
showRemoveMenu={jest.fn()}
1484+
setShowScamWarningModal={jest.fn()}
1485+
privacyMode={false}
1486+
shouldShowTokenListItemCta={mockshouldShowTokenListItemCta}
1487+
/>,
1488+
);
1489+
1490+
fireEvent.press(getByText('Test Token'));
1491+
1492+
expect(mockNavigate).toHaveBeenCalledWith(
1493+
'Asset',
1494+
expect.not.objectContaining({ securityData: expect.anything() }),
1495+
);
1496+
});
1497+
1498+
it('does not pass security data when cache has no data', () => {
1499+
prepareMocks({ asset: defaultAsset });
1500+
1501+
mockUseSelector.mockImplementation(
1502+
(selector: (state: unknown) => unknown) => {
1503+
if (selector === selectTokenListSecurityBadgesEnabled) {
1504+
return true;
1505+
}
1506+
const selectorString = selector.toString();
1507+
if (selectorString.includes('basicFunctionalityEnabled')) {
1508+
return true;
1509+
}
1510+
if (selectorString.includes('selectAsset')) {
1511+
return defaultAsset;
1512+
}
1513+
return {};
1514+
},
1515+
);
1516+
1517+
const caipId = 'eip155:1/erc20:0x456';
1518+
mockUseQuery.mockReturnValue({ data: caipId } as ReturnType<
1519+
typeof useQuery
1520+
>);
1521+
1522+
// No cached data
1523+
mockGetQueryData.mockReturnValue(null);
1524+
1525+
const { getByText } = renderWithProvider(
1526+
<TokenListItem
1527+
assetKey={assetKey}
1528+
showRemoveMenu={jest.fn()}
1529+
setShowScamWarningModal={jest.fn()}
1530+
privacyMode={false}
1531+
shouldShowTokenListItemCta={mockshouldShowTokenListItemCta}
1532+
/>,
1533+
);
1534+
1535+
fireEvent.press(getByText('Test Token'));
1536+
1537+
expect(mockNavigate).toHaveBeenCalledWith(
1538+
'Asset',
1539+
expect.not.objectContaining({ securityData: expect.anything() }),
1540+
);
1541+
});
1542+
});
13701543
});

app/components/UI/Tokens/TokenList/TokenListItem/TokenListItem.tsx

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useQuery } from '@tanstack/react-query';
1+
import { useQuery, useQueryClient } from '@tanstack/react-query';
22
import { CaipAssetType, Hex } from '@metamask/utils';
33
import { useNavigation } from '@react-navigation/native';
44
import React, { useCallback, useMemo } from 'react';
@@ -70,7 +70,10 @@ import {
7070
} from '../../../../../selectors/networkController';
7171
import { selectTokenListSecurityBadgesEnabled } from '../../../../../selectors/featureFlagController/tokenListSecurityBadges';
7272
import { selectShowFiatInTestnets } from '../../../../../selectors/settings';
73-
import { getNativeTokenAddress } from '@metamask/assets-controllers';
73+
import {
74+
getNativeTokenAddress,
75+
type TokenSecurityData,
76+
} from '@metamask/assets-controllers';
7477
import { formatPriceWithSubscriptNotation } from '../../../Predict/utils/format';
7578
import { safeToChecksumAddress } from '../../../../../util/address';
7679
import generateTestId from '../../../../../../wdio/utils/generateTestId';
@@ -165,6 +168,7 @@ export const TokenListItem = React.memo(
165168
}: TokenListItemProps) => {
166169
const { trackEvent, createEventBuilder } = useAnalytics();
167170
const navigation = useNavigation();
171+
const queryClient = useQueryClient();
168172
const { colors } = useTheme();
169173
const styles = createStyles(colors);
170174

@@ -405,14 +409,30 @@ export const TokenListItem = React.memo(
405409
const onItemPress = useCallback(
406410
(token: TokenI) => {
407411
trace({ name: TraceName.AssetDetails });
412+
413+
let securityData: TokenSecurityData | undefined;
414+
if (shouldResolveCaipForSecurityBadge && caipAssetIdForSecurity) {
415+
securityData =
416+
queryClient.getQueryData<TokenSecurityData | null>(
417+
tokenListSecurityBadgeKeys.byAsset(caipAssetIdForSecurity),
418+
) ?? undefined;
419+
}
420+
408421
navigation.navigate('Asset', {
409422
...token,
410423
source: isFullView
411424
? TokenDetailsSource.MobileTokenListPage
412425
: TokenDetailsSource.MobileTokenList,
426+
...(securityData !== undefined && { securityData }),
413427
});
414428
},
415-
[isFullView, navigation],
429+
[
430+
isFullView,
431+
navigation,
432+
shouldResolveCaipForSecurityBadge,
433+
caipAssetIdForSecurity,
434+
queryClient,
435+
],
416436
);
417437

418438
const handleLendingRedirect = useStablecoinLendingRedirect({

0 commit comments

Comments
 (0)