Skip to content

Commit fc85dc0

Browse files
authored
fix: use StackActions.push to navigate to trending token details (#27707)
## **Description** <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> **Problem** When the user navigates to a token's details page and then opens Swap from there, the navigation stack looks like: ``` [Home → Asset(TokenA) → Bridge] ``` Tapping a trending token inside the Swap/Bridge screen calls `navigation.navigate('Asset', tokenBParams)`. React Navigation's `navigate` traverses the stack looking for an existing matching route — it finds the already-present `Asset(TokenA)` screen and navigates **back** to it, dismissing the Bridge screen. The user ends up on the original token's details page instead of the tapped trending token's details page. **Fix** Replace `navigation.navigate('Asset', ...)` with `navigation.dispatch(StackActions.push('Asset', ...))` in `TrendingTokenRowItem`. `StackActions.push` always creates a **new** screen instance regardless of what is already on the stack, so the navigation flows correctly forward to the tapped token. When `push` is dispatched from a nested navigator that does not register `'Asset'` (e.g. `BridgeScreenStack` or `ExploreHome`), React Navigation bubbles the action up to the root modal stack which does register it — matching the behaviour already used in `BridgeTokenSelector` for the same reason. Tests are updated to assert that `navigation.dispatch` is called with the correct `StackActions.push` action and that `navigation.navigate` is no longer called. ## **Changelog** CHANGELOG entry: Fixed tapping a trending token from the Swap screen dismissing Swap instead of opening the token details page ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/SWAPS-4271 ## **Manual testing steps** ```gherkin Feature: Trending token navigation from Swap screen Background: Given I am logged into MetaMask Mobile And I have at least one token in my wallet Scenario: user navigates to a trending token from Swap opened via token details page Given I am on a token details page (e.g. mUSD) And I can see the "Swap" action button When user taps "Swap" Then the Swap screen opens And I can see a list of trending tokens When user taps on a trending token (e.g. Sigma) Then the Sigma token details page opens And the Swap screen is no longer visible Scenario: user navigates to a trending token from Swap opened directly (not from token details) Given I am on the Wallet home screen When user opens the Swap screen directly And user taps on a trending token (e.g. Everlyn Token) Then the Everlyn Token details page opens Scenario: user navigates to a trending token from the Explore tab Given I am on the Explore tab When user taps on a trending token Then the token details page for that token opens ``` ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** Tapping a trending token from Swap (opened via token details page) dismisses Swap and returns to the original token details page. ### **After** Tapping a trending token from Swap navigates forward to the tapped token's details page. ## **Pre-merge author checklist** - [x] 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). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] 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. <!-- Generated with the help of the pr-description AI skill --> <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes navigation behavior for trending token taps by forcing a new `Asset` screen onto the stack; risk is mainly UX/regression if the action doesn’t bubble correctly in some navigators. > > **Overview** > Fixes trending-token taps to **always open a new token details screen** by replacing `navigation.navigate('Asset', ...)` with `navigation.dispatch(StackActions.push('Asset', ...))` in `TrendingTokenRowItem`. > > Updates `TrendingTokenRowItem` tests to mock `dispatch` and assert `StackActions.push` is used (and `navigate` is not), including cases where a popular network must be added first or navigation is suppressed on failure. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit e2f50ab. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 4d25a94 commit fc85dc0

2 files changed

Lines changed: 104 additions & 80 deletions

File tree

app/components/UI/Trending/components/TrendingTokenRowItem/TrendingTokenRowItem.test.tsx

Lines changed: 99 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React from 'react';
22
import { fireEvent, waitFor } from '@testing-library/react-native';
3+
import { StackActions } from '@react-navigation/native';
34
import renderWithProvider from '../../../../../util/test/renderWithProvider';
45
import TrendingTokenRowItem from './TrendingTokenRowItem';
56
import type { TrendingAsset } from '@metamask/assets-controllers';
@@ -23,12 +24,14 @@ jest.mock('../../services/TrendingFeedSessionManager', () => ({
2324
}));
2425

2526
const mockNavigate = jest.fn();
27+
const mockDispatch = jest.fn();
2628
const mockAddPopularNetwork = jest.fn();
2729

2830
jest.mock('@react-navigation/native', () => ({
2931
...jest.requireActual('@react-navigation/native'),
3032
useNavigation: () => ({
3133
navigate: mockNavigate,
34+
dispatch: mockDispatch,
3235
}),
3336
createNavigatorFactory: () => ({}),
3437
}));
@@ -798,21 +801,24 @@ describe('TrendingTokenRowItem', () => {
798801
);
799802
fireEvent.press(tokenRow);
800803

801-
expect(mockNavigate).toHaveBeenCalledWith('Asset', {
802-
chainId: '0x1',
803-
address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
804-
symbol: 'USDC',
805-
name: 'USD Coin',
806-
decimals: 6,
807-
image:
808-
'https://static.cx.metamask.io/api/v2/tokenIcons/assets/eip155/1/erc20/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48.png',
809-
pricePercentChange1d: 3.44,
810-
isNative: false,
811-
isETH: false,
812-
isFromTrending: true,
813-
rwaData: undefined,
814-
source: 'trending',
815-
});
804+
expect(mockNavigate).not.toHaveBeenCalled();
805+
expect(mockDispatch).toHaveBeenCalledWith(
806+
StackActions.push('Asset', {
807+
chainId: '0x1',
808+
address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
809+
symbol: 'USDC',
810+
name: 'USD Coin',
811+
decimals: 6,
812+
image:
813+
'https://static.cx.metamask.io/api/v2/tokenIcons/assets/eip155/1/erc20/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48.png',
814+
pricePercentChange1d: 3.44,
815+
isNative: false,
816+
isETH: false,
817+
isFromTrending: true,
818+
rwaData: undefined,
819+
source: 'trending',
820+
}),
821+
);
816822
});
817823

818824
it('navigates to Asset page with isETH true for native ETH on Ethereum mainnet', () => {
@@ -858,21 +864,24 @@ describe('TrendingTokenRowItem', () => {
858864
);
859865
fireEvent.press(tokenRow);
860866

861-
expect(mockNavigate).toHaveBeenCalledWith('Asset', {
862-
chainId: '0x1',
863-
address: '0x0000000000000000000000000000000000000000',
864-
symbol: 'ETH',
865-
name: 'Ethereum',
866-
decimals: 18,
867-
image:
868-
'https://static.cx.metamask.io/api/v2/tokenIcons/assets/eip155/1/slip44/60.png',
869-
pricePercentChange1d: 3.44,
870-
isNative: true,
871-
isETH: true,
872-
isFromTrending: true,
873-
rwaData: undefined,
874-
source: 'trending',
875-
});
867+
expect(mockNavigate).not.toHaveBeenCalled();
868+
expect(mockDispatch).toHaveBeenCalledWith(
869+
StackActions.push('Asset', {
870+
chainId: '0x1',
871+
address: '0x0000000000000000000000000000000000000000',
872+
symbol: 'ETH',
873+
name: 'Ethereum',
874+
decimals: 18,
875+
image:
876+
'https://static.cx.metamask.io/api/v2/tokenIcons/assets/eip155/1/slip44/60.png',
877+
pricePercentChange1d: 3.44,
878+
isNative: true,
879+
isETH: true,
880+
isFromTrending: true,
881+
rwaData: undefined,
882+
source: 'trending',
883+
}),
884+
);
876885
});
877886

878887
it('navigates to Asset page with isNative true and isETH false for native token on non-Ethereum chain', () => {
@@ -918,21 +927,24 @@ describe('TrendingTokenRowItem', () => {
918927
);
919928
fireEvent.press(tokenRow);
920929

921-
expect(mockNavigate).toHaveBeenCalledWith('Asset', {
922-
chainId: '0x89',
923-
address: '0x0000000000000000000000000000000000000000',
924-
symbol: 'MATIC',
925-
name: 'Polygon',
926-
decimals: 18,
927-
image:
928-
'https://static.cx.metamask.io/api/v2/tokenIcons/assets/eip155/137/slip44/966.png',
929-
pricePercentChange1d: 3.44,
930-
isNative: true,
931-
isETH: false,
932-
isFromTrending: true,
933-
rwaData: undefined,
934-
source: 'trending',
935-
});
930+
expect(mockNavigate).not.toHaveBeenCalled();
931+
expect(mockDispatch).toHaveBeenCalledWith(
932+
StackActions.push('Asset', {
933+
chainId: '0x89',
934+
address: '0x0000000000000000000000000000000000000000',
935+
symbol: 'MATIC',
936+
name: 'Polygon',
937+
decimals: 18,
938+
image:
939+
'https://static.cx.metamask.io/api/v2/tokenIcons/assets/eip155/137/slip44/966.png',
940+
pricePercentChange1d: 3.44,
941+
isNative: true,
942+
isETH: false,
943+
isFromTrending: true,
944+
rwaData: undefined,
945+
source: 'trending',
946+
}),
947+
);
936948
});
937949

938950
it('adds network directly when network is not added and navigates to asset', async () => {
@@ -995,7 +1007,10 @@ describe('TrendingTokenRowItem', () => {
9951007

9961008
// Wait for the async navigation call
9971009
await waitFor(() => {
998-
expect(mockNavigate).toHaveBeenCalledWith('Asset', expect.any(Object));
1010+
expect(mockNavigate).not.toHaveBeenCalled();
1011+
expect(mockDispatch).toHaveBeenCalledWith(
1012+
StackActions.push('Asset', expect.any(Object)),
1013+
);
9991014
});
10001015
});
10011016

@@ -1054,7 +1069,7 @@ describe('TrendingTokenRowItem', () => {
10541069
});
10551070

10561071
// Navigation should NOT be called when network addition fails
1057-
expect(mockNavigate).not.toHaveBeenCalled();
1072+
expect(mockDispatch).not.toHaveBeenCalled();
10581073

10591074
consoleErrorSpy.mockRestore();
10601075
});
@@ -1077,7 +1092,7 @@ describe('TrendingTokenRowItem', () => {
10771092
);
10781093
fireEvent.press(tokenRow);
10791094

1080-
expect(mockNavigate).not.toHaveBeenCalled();
1095+
expect(mockDispatch).not.toHaveBeenCalled();
10811096
});
10821097

10831098
it('navigates with assetId as address for non-EVM chains', () => {
@@ -1128,21 +1143,24 @@ describe('TrendingTokenRowItem', () => {
11281143
);
11291144
fireEvent.press(tokenRow);
11301145

1131-
expect(mockNavigate).toHaveBeenCalledWith('Asset', {
1132-
chainId: 'bip122:000000000019d6689c085ae165831e93',
1133-
address: 'bip122:000000000019d6689c085ae165831e93/slip44:0',
1134-
symbol: 'BTC',
1135-
name: 'Bitcoin',
1136-
decimals: 8,
1137-
image:
1138-
'https://static.cx.metamask.io/api/v2/tokenIcons/assets/bip122/000000000019d6689c085ae165831e93/slip44/0.png',
1139-
pricePercentChange1d: 3.44,
1140-
isNative: true,
1141-
isETH: false,
1142-
isFromTrending: true,
1143-
rwaData: undefined,
1144-
source: 'trending',
1145-
});
1146+
expect(mockNavigate).not.toHaveBeenCalled();
1147+
expect(mockDispatch).toHaveBeenCalledWith(
1148+
StackActions.push('Asset', {
1149+
chainId: 'bip122:000000000019d6689c085ae165831e93',
1150+
address: 'bip122:000000000019d6689c085ae165831e93/slip44:0',
1151+
symbol: 'BTC',
1152+
name: 'Bitcoin',
1153+
decimals: 8,
1154+
image:
1155+
'https://static.cx.metamask.io/api/v2/tokenIcons/assets/bip122/000000000019d6689c085ae165831e93/slip44/0.png',
1156+
pricePercentChange1d: 3.44,
1157+
isNative: true,
1158+
isETH: false,
1159+
isFromTrending: true,
1160+
rwaData: undefined,
1161+
source: 'trending',
1162+
}),
1163+
);
11461164
});
11471165

11481166
it('navigates directly when network is not popular but is added', () => {
@@ -1194,21 +1212,24 @@ describe('TrendingTokenRowItem', () => {
11941212
fireEvent.press(tokenRow);
11951213

11961214
expect(queryByTestId('network-modal')).toBeNull();
1197-
expect(mockNavigate).toHaveBeenCalledWith('Asset', {
1198-
chainId: '0x3e7',
1199-
address: '0x123',
1200-
symbol: 'TEST',
1201-
name: 'Test Token',
1202-
decimals: 18,
1203-
image:
1204-
'https://static.cx.metamask.io/api/v2/tokenIcons/assets/eip155/999/erc20/0x123.png',
1205-
pricePercentChange1d: 3.44,
1206-
isNative: false,
1207-
isETH: false,
1208-
isFromTrending: true,
1209-
rwaData: undefined,
1210-
source: 'trending',
1211-
});
1215+
expect(mockNavigate).not.toHaveBeenCalled();
1216+
expect(mockDispatch).toHaveBeenCalledWith(
1217+
StackActions.push('Asset', {
1218+
chainId: '0x3e7',
1219+
address: '0x123',
1220+
symbol: 'TEST',
1221+
name: 'Test Token',
1222+
decimals: 18,
1223+
image:
1224+
'https://static.cx.metamask.io/api/v2/tokenIcons/assets/eip155/999/erc20/0x123.png',
1225+
pricePercentChange1d: 3.44,
1226+
isNative: false,
1227+
isETH: false,
1228+
isFromTrending: true,
1229+
rwaData: undefined,
1230+
source: 'trending',
1231+
}),
1232+
);
12121233
});
12131234
});
12141235

app/components/UI/Trending/components/TrendingTokenRowItem/TrendingTokenRowItem.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React, { useCallback, useMemo } from 'react';
22
import { ImageSourcePropType, TouchableOpacity, View } from 'react-native';
3-
import { useNavigation } from '@react-navigation/native';
3+
import { StackActions, useNavigation } from '@react-navigation/native';
44
import { useSelector } from 'react-redux';
55
import Text, {
66
TextColor,
@@ -249,7 +249,10 @@ const TrendingTokenRowItem = ({
249249
}
250250
}
251251

252-
navigation.navigate('Asset', assetParams);
252+
// Use push so we always open a new Asset screen for the tapped token.
253+
// This prevents issues such as dismissing screens like Bridge instead
254+
// of navigating forward to the new token.
255+
navigation.dispatch(StackActions.push('Asset', assetParams));
253256
}, [
254257
assetParams,
255258
caipChainId,

0 commit comments

Comments
 (0)