Skip to content

Commit c83f25f

Browse files
authored
fix(rewards): modal navigation stack (#32180)
<!-- 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. --> <!-- mms-check directive vocabulary — read by .github/scripts/shared/pr-template-checks.ts at module load to build the validation plan. Directives are invisible in rendered markdown and must NOT be removed or edited without updating the validator registry. type=text Section must contain non-placeholder prose. type=changelog Section must have a valid CHANGELOG entry: line. type=issue-link Section must have a Fixes:/Closes:/Refs: line with a value. type=manual-testing Section must have real testing steps or an explicit N/A. type=screenshot Section must have evidence (image/URL) or an explicit N/A. type=checklist Section must have all checkboxes consciously checked. required=true|false Whether a missing/invalid section runs the validator at all. blocking=true|false Whether a failure of this check fails the CI workflow. Default: false — failures are shown as warnings in the sticky comment but do not block the PR. Sections without a directive are checked for structural presence only. --> ## **Description** <!-- mms-check: type=text required=true --> Fixes rewards bottom sheets that briefly navigate to `RewardsDashboard` (or animate incorrectly) when opened from campaign sub-pages. ### Regression introduced by [32573f0](32573f0) (#31227) That PR migrated Rewards to native stack and **split the navigation tree**: **Before `32573f06`** ``` RewardsHome (Rewards tab) ├── REWARDS_VIEW → RewardsNavigator (dashboard + all sub-pages) └── modal sheets (siblings of REWARDS_VIEW) ``` Campaign pages and modal routes shared the same parent stack. `navigation.navigate('RewardsSelectSheet')` bubbled up one level and opened the sheet on top of the current rewards screen. **After `32573f06`** ``` RewardsHome (Rewards tab) ├── RewardsDashboard / Onboarding └── modal sheets MainNavigator (root) └── REWARDS_FLOW → RewardsNavigator (sub-pages only) ``` Sub-pages moved to root-level `REWARDS_FLOW`, but the five rewards modal routes stayed in `RewardsHome`. From a campaign page, `navigate()` could not resolve the route locally, walked up the tree, found it in the Rewards tab navigator, switched to that tab (showing `RewardsDashboard`), then opened the sheet. The same mismatch affects all five rewards modal routes when opened from `REWARDS_FLOW` (tier selector, settings account group, season-one claim sheets, etc.). ### Fix in this PR Move all five rewards modal screens to **root `MainNavigator`** (alongside `REWARDS_FLOW`) so they are registered once and reachable from both the Rewards tab and campaign sub-pages: - `RewardsBottomSheetModal` - `RewardsClaimBottomSheetModal` - `RewardOptInAccountGroupModal` - `EndOfSeasonClaimBottomSheet` - `RewardsSelectSheet` `RewardsHome` now only registers the dashboard/onboarding entry screens. Modal screen options use `clearNativeStackNavigatorOptions` + `transparentModalScreenOptions`. When modals lived under `RewardsHome`, they inherited that navigator's default `animation: 'none'`, so only the `BottomSheet` component animated. At root without `animation: 'none'`, the native stack slid the entire transparent screen (backdrop included) from the bottom; adding `clearNativeStackNavigatorOptions` restores the expected overlay fade + sheet slide behavior. ## **Changelog** <!-- mms-check: type=changelog required=true blocking=true --> CHANGELOG entry: null ## **Related issues** <!-- mms-check: type=issue-link required=true --> Refs: 32573f0 ## **Manual testing steps** <!-- mms-check: type=manual-testing required=true --> ```gherkin Feature: Rewards modal navigation from REWARDS_FLOW Scenario: Ondo campaign tier selector opens in place Given an opted-in user on an Ondo campaign detail or leaderboard page (REWARDS_FLOW) When the user taps the tier selector Then the tier select bottom sheet opens And the app does not navigate to RewardsDashboard first And the semitransparent backdrop fades in while only the sheet content slides up Scenario: Rewards dashboard modals still work Given an opted-in user on the Rewards dashboard When the user opens a rewards bottom sheet (e.g. dashboard modal, claim sheet) Then the sheet opens with correct backdrop and sheet animation Scenario: Settings account group modal from REWARDS_FLOW Given an opted-in user on Rewards Settings (REWARDS_FLOW) When the user taps to show account group addresses Then the account group modal opens without switching to the Rewards tab first ``` ## **Screenshots/Recordings** <!-- mms-check: type=screenshot required=true --> N/A — navigation/animation fix; manual verification on device recommended. ### **Before** Tier selector on Ondo campaign detail briefly shows `RewardsDashboard`; backdrop slides up with the sheet when opened from `REWARDS_FLOW`. ### **After** Sheet opens in place over the current campaign screen; backdrop fades in, sheet slides up independently. https://github.com/user-attachments/assets/1c94add1-6d7b-41fd-bea9-e892526e2499 ## **Pre-merge author checklist** <!-- mms-check: type=checklist required=true --> - [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) - [x] I've tested on Android - Ideally on a mid-range device; emulator is acceptable - [x] 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 - [x] 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** - [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** > Touches core Rewards navigation and modal presentation; wrong registration could break deeplinks or sheets from tab vs flow, but scope is limited to navigator wiring and covered by updated tests. > > **Overview** > Fixes rewards bottom sheets that briefly jumped to **RewardsDashboard** or used the wrong animation when opened from **REWARDS_FLOW** campaign sub-pages. > > **Navigation:** All five rewards modal routes are removed from the **Rewards** tab stack (`RewardsHome`) and registered on root **MainNavigator** next to `REWARDS_FLOW`, so `navigate()` resolves them without switching tabs. `RewardsHome` now only exposes dashboard vs onboarding. > > **Presentation:** Shared `rewardsModalScreenOptions` is hoisted to module scope and merges `clearNativeStackNavigatorOptions` with `transparentModalScreenOptions` so root-level modals keep `animation: 'none'` and only the **BottomSheet** animates (not the full transparent overlay). > > **Tests:** Assert modals are absent from the tab stack and present on the root navigator. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit fe2ae68. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 6ac63f4 commit c83f25f

2 files changed

Lines changed: 71 additions & 32 deletions

File tree

app/components/Nav/Main/MainNavigator.js

Lines changed: 34 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,15 @@ const styles = StyleSheet.create({
193193
},
194194
});
195195

196+
// Registered on the root MainNavigator so sheets are reachable from both the
197+
// Rewards tab (dashboard/onboarding) and REWARDS_FLOW sub-pages. Use
198+
// animation: 'none' so only the BottomSheet's internal slide runs — without it
199+
// the native stack slides the entire screen (overlay included) from the bottom.
200+
const rewardsModalScreenOptions = {
201+
...clearNativeStackNavigatorOptions,
202+
...transparentModalScreenOptions,
203+
};
204+
196205
/* eslint-disable react/prop-types */
197206
const AssetStackFlow = (props) => (
198207
<NativeStack.Navigator
@@ -331,10 +340,6 @@ const RewardsHome = () => {
331340
// 'pending' state). Only RewardsHome mounts for non-opted-in users, so fetching at
332341
// this shared entry point prevents the onboarding tab from loading indefinitely.
333342
useCandidateSubscriptionId();
334-
const rewardsModalScreenOptions = {
335-
...transparentModalScreenOptions,
336-
contentStyle: { backgroundColor: 'transparent' },
337-
};
338343

339344
if (isVersionBlocked) {
340345
return <RewardsUpdateRequired />;
@@ -359,31 +364,6 @@ const RewardsHome = () => {
359364
component={RewardsOnboardingNavigator}
360365
/>
361366
)}
362-
<NativeStack.Screen
363-
name={Routes.MODAL.REWARDS_BOTTOM_SHEET_MODAL}
364-
component={RewardsBottomSheetModal}
365-
options={rewardsModalScreenOptions}
366-
/>
367-
<NativeStack.Screen
368-
name={Routes.MODAL.REWARDS_CLAIM_BOTTOM_SHEET_MODAL}
369-
component={RewardsClaimBottomSheetModal}
370-
options={rewardsModalScreenOptions}
371-
/>
372-
<NativeStack.Screen
373-
name={Routes.MODAL.REWARDS_OPTIN_ACCOUNT_GROUP_MODAL}
374-
component={RewardOptInAccountGroupModal}
375-
options={rewardsModalScreenOptions}
376-
/>
377-
<NativeStack.Screen
378-
name={Routes.MODAL.REWARDS_END_OF_SEASON_CLAIM_BOTTOM_SHEET}
379-
component={EndOfSeasonClaimBottomSheet}
380-
options={rewardsModalScreenOptions}
381-
/>
382-
<NativeStack.Screen
383-
name={Routes.MODAL.REWARDS_SELECT_SHEET}
384-
component={RewardsSelectSheet}
385-
options={rewardsModalScreenOptions}
386-
/>
387367
</NativeStack.Navigator>
388368
);
389369
};
@@ -1050,6 +1030,31 @@ const MainNavigator = () => {
10501030
component={RewardsNavigator}
10511031
options={{ headerShown: false }}
10521032
/>
1033+
<NativeStack.Screen
1034+
name={Routes.MODAL.REWARDS_BOTTOM_SHEET_MODAL}
1035+
component={RewardsBottomSheetModal}
1036+
options={rewardsModalScreenOptions}
1037+
/>
1038+
<NativeStack.Screen
1039+
name={Routes.MODAL.REWARDS_CLAIM_BOTTOM_SHEET_MODAL}
1040+
component={RewardsClaimBottomSheetModal}
1041+
options={rewardsModalScreenOptions}
1042+
/>
1043+
<NativeStack.Screen
1044+
name={Routes.MODAL.REWARDS_OPTIN_ACCOUNT_GROUP_MODAL}
1045+
component={RewardOptInAccountGroupModal}
1046+
options={rewardsModalScreenOptions}
1047+
/>
1048+
<NativeStack.Screen
1049+
name={Routes.MODAL.REWARDS_END_OF_SEASON_CLAIM_BOTTOM_SHEET}
1050+
component={EndOfSeasonClaimBottomSheet}
1051+
options={rewardsModalScreenOptions}
1052+
/>
1053+
<NativeStack.Screen
1054+
name={Routes.MODAL.REWARDS_SELECT_SHEET}
1055+
component={RewardsSelectSheet}
1056+
options={rewardsModalScreenOptions}
1057+
/>
10531058
<NativeStack.Screen
10541059
name={Routes.DEPRECATED_NETWORK_DETAILS}
10551060
component={DeprecatedNetworkDetails}

app/components/Nav/Main/MainNavigator.test.tsx

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1571,11 +1571,45 @@ describe('MainNavigator', () => {
15711571
expect(screenNames).not.toContain(Routes.REWARDS_DASHBOARD);
15721572
});
15731573

1574-
it('registers the rewards modal screens regardless of subscription state', () => {
1575-
// The modal screens are siblings of the dashboard/onboarding branch and
1576-
// must always be available so deeplinks/bottom sheets can open.
1574+
it('does not register rewards modal screens in the Rewards tab stack', () => {
1575+
// Modal sheets live on the root MainNavigator so they are reachable from
1576+
// both the Rewards tab and REWARDS_FLOW without switching tabs.
15771577
const screenNames = findRewardsHomeScreenNames('test-subscription-id');
15781578

1579+
expect(screenNames).not.toContain(
1580+
Routes.MODAL.REWARDS_BOTTOM_SHEET_MODAL,
1581+
);
1582+
expect(screenNames).not.toContain(
1583+
Routes.MODAL.REWARDS_CLAIM_BOTTOM_SHEET_MODAL,
1584+
);
1585+
expect(screenNames).not.toContain(
1586+
Routes.MODAL.REWARDS_OPTIN_ACCOUNT_GROUP_MODAL,
1587+
);
1588+
expect(screenNames).not.toContain(
1589+
Routes.MODAL.REWARDS_END_OF_SEASON_CLAIM_BOTTOM_SHEET,
1590+
);
1591+
expect(screenNames).not.toContain(Routes.MODAL.REWARDS_SELECT_SHEET);
1592+
});
1593+
});
1594+
1595+
describe('Root rewards modal screens', () => {
1596+
const findRootScreenNames = (): string[] => {
1597+
const { root } = renderWithProvider(<MainNavigator />, {
1598+
state: initialRootState,
1599+
});
1600+
1601+
return root
1602+
.findAll(
1603+
(node: ReactTestInstance) =>
1604+
node.type?.toString?.() === 'Screen' &&
1605+
typeof node.props?.name === 'string',
1606+
)
1607+
.map((node) => node.props.name as string);
1608+
};
1609+
1610+
it('registers rewards modal screens on the root navigator', () => {
1611+
const screenNames = findRootScreenNames();
1612+
15791613
expect(screenNames).toEqual(
15801614
expect.arrayContaining([
15811615
Routes.MODAL.REWARDS_BOTTOM_SHEET_MODAL,

0 commit comments

Comments
 (0)