Skip to content

Commit 12790c0

Browse files
authored
refactor(predict): use native stack navigators for Predict routes (#30151)
<!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until this PR meets the canonical Definition of Ready For Review in `docs/readme/ready-for-review.md`. In short: the template must be materially complete (not just section titles present), all status checks must be currently passing, and the only expected follow-up commits must be reviewer-driven. --> ## **Description** POC: https://docs.google.com/document/d/1_oDELkGRLUgaAeMs2NSzG9w1UyjsD5VSKQlW7yW5oZU/edit?tab=t.0#heading=h.gba8s4wq9tzu Android build: https://github.com/MetaMask/metamask-mobile/actions/runs/25829982981 Switches Predict’s main and modal navigators from @react-navigation/stack to @react-navigation/native-stack, aligns modal/confirmation options with other feature stacks (e.g. Money), and removes JS-stack-only transition customization that no longer applies. **What changed** PredictScreenStack: createNativeStackNavigator with screenOptions={{ headerShown: false }}; market list uses animation: 'none' instead of animationEnabled: false. PredictModalStack: Uses clearNativeStackNavigatorOptions + transparentModalScreenOptions instead of clearStackNavigatorOptions + per-screen opacity interpolators. Confirmations: REDESIGNED_CONFIRMATIONS uses useEmptyNavHeaderForConfirmations() (same pattern as Money); NO_HEADER uses headerShown: false. Removed: getConfirmationTransitionSpec, PredictConfirmationRouteParams, getPredictConfirmationScreenOptions, slideFromRightInterpolator, and all cardStyleInterpolator blocks on modal screens. **Behavior notes** Push screens (buy/sell preview, market details): rely on native stack default push animation (slide from right on Android; system behavior on iOS) instead of the previous manual translateX interpolator. Transparent modals (unavailable, GTM, add funds): no longer fade the route card via interpolator; modal/sheet UIs own their presentation; outer stack uses transparent modal + animation: 'none' from the shared preset. ## **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:null ## **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** <img width="382" height="768" alt="predict before part 1" src="https://github.com/user-attachments/assets/c494c723-9913-4cbb-a9ff-c91b66d7e172" /> <img width="382" height="768" alt="predict before part 2" src="https://github.com/user-attachments/assets/0897d06f-b842-4f24-abfb-0ac6167b3020" /> <img width="382" height="786" alt="predict before part 3" src="https://github.com/user-attachments/assets/0b9fdefc-d434-47cf-9e41-551fe5f49e68" /> ### **After** <img width="382" height="786" alt="Predict after part 1" src="https://github.com/user-attachments/assets/204b4512-294e-4dbe-8ac6-bad7b96160b2" /> <img width="382" height="786" alt="predict after part 2" src="https://github.com/user-attachments/assets/5fd683bb-e7d8-4bae-b677-1cd698f39368" /> <img width="382" height="786" alt="predict after part 3" src="https://github.com/user-attachments/assets/b8fc7459-6be5-4e4a-8b44-93443b0f25ef" /> <!-- [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. --> - [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. #### 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`. --> - [x] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [x] 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 Predict navigation stacks and screen option presets, which can subtly alter transition behavior and header presentation across multiple screens/modals. Risk is moderate because it’s UI/navigation-only and doesn’t touch business logic, but regressions would impact user flow. > > **Overview** > Migrates Predict’s main and modal navigators from `@react-navigation/stack` to `@react-navigation/native-stack`, standardizing screen options via `clearNativeStackNavigatorOptions` and `transparentModalScreenOptions`. > > Removes JS-stack-only transition customizations (custom interpolators and confirmation transitionSpec) and updates confirmation routes to use `useEmptyNavHeaderForConfirmations` (or `headerShown: false`) while relying on native-stack defaults for push/modal animations. Test mocks were updated to reflect the new navigation option exports and confirmations header hook. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 80b0c82. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 4384427 commit 12790c0

2 files changed

Lines changed: 61 additions & 156 deletions

File tree

app/components/UI/Predict/routes/index.test.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,15 @@ jest.mock('../../../Views/confirmations/components/confirm', () => ({
9898
jest.mock(
9999
'../../../../constants/navigation/clearStackNavigatorOptions',
100100
() => ({
101-
clearStackNavigatorOptions: {},
101+
clearNativeStackNavigatorOptions: {},
102+
transparentModalScreenOptions: {},
103+
}),
104+
);
105+
106+
jest.mock(
107+
'../../../Views/confirmations/hooks/ui/useEmptyNavHeaderForConfirmations',
108+
() => ({
109+
useEmptyNavHeaderForConfirmations: () => ({ headerShown: false }),
102110
}),
103111
);
104112

app/components/UI/Predict/routes/index.tsx

Lines changed: 52 additions & 155 deletions
Original file line numberDiff line numberDiff line change
@@ -1,227 +1,124 @@
1-
import {
2-
createStackNavigator,
3-
type StackNavigationOptions,
4-
} from '@react-navigation/stack';
1+
import { createNativeStackNavigator } from '@react-navigation/native-stack';
52
import React from 'react';
63
import { strings } from '../../../../../locales/i18n';
74
import Routes from '../../../../constants/navigation/Routes';
5+
import {
6+
clearNativeStackNavigatorOptions,
7+
transparentModalScreenOptions,
8+
} from '../../../../constants/navigation/clearStackNavigatorOptions';
89
import { Confirm } from '../../../Views/confirmations/components/confirm';
910
import PredictMarketDetails from '../views/PredictMarketDetails';
1011
import PredictUnavailableModal from '../views/PredictUnavailableModal';
12+
import { useEmptyNavHeaderForConfirmations } from '../../../Views/confirmations/hooks/ui/useEmptyNavHeaderForConfirmations';
1113
import PredictActivityDetail from '../components/PredictActivityDetail/PredictActivityDetail';
1214
import { PredictNavigationParamList } from '../types/navigation';
1315
import PredictAddFundsModal from '../views/PredictAddFundsModal/PredictAddFundsModal';
1416
import PredictFeed from '../views/PredictFeed';
1517
import PredictWorldCup from '../views/PredictWorldCup';
1618
import PredictGTMModal from '../components/PredictGTMModal';
17-
import { Dimensions } from 'react-native';
1819
import { useSelector } from 'react-redux';
1920
import { PredictPreviewSheetProvider } from '../contexts';
2021
import PredictBuyPreview from '../views/PredictBuyPreview/PredictBuyPreview';
2122
import PredictBuyWithAnyToken from '../views/PredictBuyWithAnyToken';
2223
import PredictSellPreview from '../views/PredictSellPreview/PredictSellPreview';
2324
import { selectPredictWithAnyTokenEnabledFlag } from '../selectors/featureFlags';
24-
import { clearStackNavigatorOptions } from '../../../../constants/navigation/clearStackNavigatorOptions';
25-
26-
interface PredictConfirmationRouteParams {
27-
animationEnabled?: boolean;
28-
}
29-
30-
const getConfirmationTransitionSpec = (
31-
disableOpenAnimation: boolean,
32-
): StackNavigationOptions['transitionSpec'] =>
33-
disableOpenAnimation
34-
? {
35-
open: { animation: 'timing' as const, config: { duration: 0 } },
36-
close: { animation: 'timing' as const, config: { duration: 300 } },
37-
}
38-
: undefined;
39-
40-
const getPredictConfirmationScreenOptions = ({
41-
route,
42-
}: {
43-
route: {
44-
params?: PredictConfirmationRouteParams;
45-
};
46-
}): StackNavigationOptions => {
47-
const disableOpenAnimation = route.params?.animationEnabled === false;
48-
49-
return {
50-
headerLeft: () => null,
51-
headerShown: true,
52-
title: '',
53-
transitionSpec: getConfirmationTransitionSpec(disableOpenAnimation),
54-
};
55-
};
5625

57-
const slideFromRightInterpolator: StackNavigationOptions['cardStyleInterpolator'] =
58-
({ current }) => ({
59-
cardStyle: {
60-
transform: [
61-
{
62-
translateX: current.progress.interpolate({
63-
inputRange: [0, 1],
64-
outputRange: [Dimensions.get('window').width, 0],
65-
}),
66-
},
67-
],
68-
},
69-
});
26+
const Stack = createNativeStackNavigator<PredictNavigationParamList>();
27+
const ModalStack = createNativeStackNavigator<PredictNavigationParamList>();
7028

71-
const Stack = createStackNavigator<PredictNavigationParamList>();
72-
const ModalStack = createStackNavigator<PredictNavigationParamList>();
29+
const PredictModalStack = () => {
30+
const emptyNavHeaderOptions = useEmptyNavHeaderForConfirmations();
7331

74-
const PredictModalStack = () => (
75-
<ModalStack.Navigator
76-
screenOptions={{
77-
...clearStackNavigatorOptions,
78-
presentation: 'transparentModal',
79-
}}
80-
>
81-
<ModalStack.Screen
82-
name={Routes.PREDICT.MODALS.UNAVAILABLE}
83-
component={PredictUnavailableModal}
84-
options={{
85-
cardStyleInterpolator: ({ current }) => ({
86-
cardStyle: {
87-
opacity: current.progress,
88-
},
89-
}),
90-
}}
91-
/>
92-
<ModalStack.Screen
93-
name={Routes.PREDICT.MODALS.GTM_MODAL}
94-
component={PredictGTMModal}
95-
options={{
96-
cardStyleInterpolator: ({ current }) => ({
97-
cardStyle: {
98-
opacity: current.progress,
99-
},
100-
}),
101-
}}
102-
/>
103-
<ModalStack.Screen
104-
name={Routes.PREDICT.MODALS.ADD_FUNDS_SHEET}
105-
component={PredictAddFundsModal}
106-
options={{
107-
cardStyleInterpolator: ({ current }) => ({
108-
cardStyle: {
109-
opacity: current.progress,
110-
},
111-
}),
112-
}}
113-
/>
114-
<ModalStack.Screen
115-
name={Routes.PREDICT.ACTIVITY_DETAIL}
116-
component={PredictActivityDetail}
117-
options={{
118-
headerShown: false,
119-
}}
120-
/>
121-
<ModalStack.Screen
122-
name={Routes.FULL_SCREEN_CONFIRMATIONS.REDESIGNED_CONFIRMATIONS}
123-
component={Confirm}
124-
options={getPredictConfirmationScreenOptions}
125-
/>
126-
<ModalStack.Screen
127-
name={Routes.FULL_SCREEN_CONFIRMATIONS.NO_HEADER}
128-
component={Confirm}
129-
options={({
130-
route,
131-
}: {
132-
route: { params?: PredictConfirmationRouteParams };
133-
}) => {
134-
const disableOpenAnimation = route.params?.animationEnabled === false;
135-
136-
return {
137-
headerShown: false,
138-
transitionSpec: getConfirmationTransitionSpec(disableOpenAnimation),
139-
};
32+
return (
33+
<ModalStack.Navigator
34+
screenOptions={{
35+
...clearNativeStackNavigatorOptions,
36+
...transparentModalScreenOptions,
14037
}}
141-
/>
142-
</ModalStack.Navigator>
143-
);
38+
>
39+
<ModalStack.Screen
40+
name={Routes.PREDICT.MODALS.UNAVAILABLE}
41+
component={PredictUnavailableModal}
42+
/>
43+
<ModalStack.Screen
44+
name={Routes.PREDICT.MODALS.GTM_MODAL}
45+
component={PredictGTMModal}
46+
/>
47+
<ModalStack.Screen
48+
name={Routes.PREDICT.MODALS.ADD_FUNDS_SHEET}
49+
component={PredictAddFundsModal}
50+
/>
51+
<ModalStack.Screen
52+
name={Routes.PREDICT.ACTIVITY_DETAIL}
53+
component={PredictActivityDetail}
54+
/>
55+
<ModalStack.Screen
56+
name={Routes.FULL_SCREEN_CONFIRMATIONS.REDESIGNED_CONFIRMATIONS}
57+
component={Confirm}
58+
options={emptyNavHeaderOptions}
59+
/>
60+
<ModalStack.Screen
61+
name={Routes.FULL_SCREEN_CONFIRMATIONS.NO_HEADER}
62+
component={Confirm}
63+
options={{ headerShown: false }}
64+
/>
65+
</ModalStack.Navigator>
66+
);
67+
};
14468

14569
const PredictScreenStack = () => {
14670
const payWithAnyTokenEnabled = useSelector(
14771
selectPredictWithAnyTokenEnabledFlag,
14872
);
73+
const emptyNavHeaderOptions = useEmptyNavHeaderForConfirmations();
14974
const BuyPreviewComponent = payWithAnyTokenEnabled
15075
? PredictBuyWithAnyToken
15176
: PredictBuyPreview;
15277

15378
return (
15479
<PredictPreviewSheetProvider>
155-
<Stack.Navigator initialRouteName={Routes.PREDICT.MARKET_LIST}>
80+
<Stack.Navigator
81+
initialRouteName={Routes.PREDICT.MARKET_LIST}
82+
screenOptions={{ headerShown: false }}
83+
>
15684
<Stack.Screen
15785
name={Routes.PREDICT.MARKET_LIST}
15886
component={PredictFeed}
15987
options={{
16088
title: strings('predict.markets.title'),
161-
headerShown: false,
162-
animationEnabled: false,
89+
animation: 'none',
16390
}}
16491
/>
16592

16693
<Stack.Screen
16794
name={Routes.PREDICT.WORLD_CUP}
16895
component={PredictWorldCup}
169-
options={{
170-
headerShown: false,
171-
cardStyleInterpolator: slideFromRightInterpolator,
172-
}}
17396
/>
17497

17598
<Stack.Screen
17699
name={Routes.PREDICT.MODALS.BUY_PREVIEW}
177100
component={BuyPreviewComponent}
178-
options={{
179-
headerShown: false,
180-
cardStyleInterpolator: slideFromRightInterpolator,
181-
}}
182101
/>
183102

184103
<Stack.Screen
185104
name={Routes.PREDICT.MODALS.SELL_PREVIEW}
186105
component={PredictSellPreview}
187-
options={{
188-
headerShown: false,
189-
cardStyleInterpolator: slideFromRightInterpolator,
190-
}}
191106
/>
192107

193108
<Stack.Screen
194109
name={Routes.FULL_SCREEN_CONFIRMATIONS.REDESIGNED_CONFIRMATIONS}
195110
component={Confirm}
196-
options={getPredictConfirmationScreenOptions}
111+
options={emptyNavHeaderOptions}
197112
/>
198113

199114
<Stack.Screen
200115
name={Routes.FULL_SCREEN_CONFIRMATIONS.NO_HEADER}
201116
component={Confirm}
202-
options={({
203-
route,
204-
}: {
205-
route: { params?: PredictConfirmationRouteParams };
206-
}) => {
207-
const disableOpenAnimation =
208-
route.params?.animationEnabled === false;
209-
210-
return {
211-
headerShown: false,
212-
transitionSpec:
213-
getConfirmationTransitionSpec(disableOpenAnimation),
214-
};
215-
}}
216117
/>
217118

218119
<Stack.Screen
219120
name={Routes.PREDICT.MARKET_DETAILS}
220121
component={PredictMarketDetails}
221-
options={{
222-
headerShown: false,
223-
cardStyleInterpolator: slideFromRightInterpolator,
224-
}}
225122
/>
226123
</Stack.Navigator>
227124
</PredictPreviewSheetProvider>

0 commit comments

Comments
 (0)