Skip to content

Commit f8f3356

Browse files
authored
feat(ramp): agg / deposit switcher (#22283)
<!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** This pull request introduces a new "Settings" modal for the buy flow in the Ramp Aggregator, updates the configuration button icon, and improves component flexibility and test coverage. The most significant changes are the addition of the buy settings modal and its integration into the navigation flow. ### Buy Flow Settings Modal * Added a new `SettingsModal` component that appears as a bottom sheet, allowing users to view order history or switch to the new buy experience. This includes navigation logic and UI elements. [[1]](diffhunk://#diff-e763577f4d665c8606263239492930e87bf3d4e279cbbac587936cf66d7bd4d8R1-R67) [[2]](diffhunk://#diff-d6834d985c54e83eea965972a6f2361418b54e52f18fbe81580bba2b09aed830R1) [[3]](diffhunk://#diff-c97ef93052f382820dc15a75c6550cfb58cb2e02701b00954bf6627ee973dae5R1-R150) * Integrated the `SettingsModal` into the Ramp Aggregator's modal navigation stack, making it accessible from the buy flow. [[1]](diffhunk://#diff-ee3146518eeb9c65ca423e7002936990d4ec0c3960098219f127aabbdecca283R18) [[2]](diffhunk://#diff-ee3146518eeb9c65ca423e7002936990d4ec0c3960098219f127aabbdecca283R96-R99) * Provided a utility for generating navigation details for the new modal (`createBuySettingsModalNavigationDetails`). [[1]](diffhunk://#diff-8f431ed27f208e5f873f504bd846413111f1b8554ff5e60d1fb34080fa569b4eR103) [[2]](diffhunk://#diff-e763577f4d665c8606263239492930e87bf3d4e279cbbac587936cf66d7bd4d8R1-R67) ### UI/UX Improvements * Changed the configuration button icon in the deposit navbar and associated snapshots from "MoreHorizontal" to "Setting" for clarity and consistency. [[1]](diffhunk://#diff-f2cb25f3b00b5754b8b022c689f98cdbe6e3a26ce9cf80906f443477cbe40e94L2027-R2027) [[2]](diffhunk://#diff-3c3cfd60ca950883ba19a8996fb24e2f2a5ffd3c4b830dc00d10b5f3510b0d0dL186-R186) [[3]](diffhunk://#diff-3c3cfd60ca950883ba19a8996fb24e2f2a5ffd3c4b830dc00d10b5f3510b0d0dL2060-R2060) [[4]](diffhunk://#diff-3c3cfd60ca950883ba19a8996fb24e2f2a5ffd3c4b830dc00d10b5f3510b0d0dL3934-R3934) [[5]](diffhunk://#diff-3c3cfd60ca950883ba19a8996fb24e2f2a5ffd3c4b830dc00d10b5f3510b0d0dL5808-R5808) [[6]](diffhunk://#diff-3c3cfd60ca950883ba19a8996fb24e2f2a5ffd3c4b830dc00d10b5f3510b0d0dL7621-R7621) [[7]](diffhunk://#diff-3c3cfd60ca950883ba19a8996fb24e2f2a5ffd3c4b830dc00d10b5f3510b0d0dL9433-R9433) [[8]](diffhunk://#diff-3c3cfd60ca950883ba19a8996fb24e2f2a5ffd3c4b830dc00d10b5f3510b0d0dL11246-R11246) [[9]](diffhunk://#diff-3c3cfd60ca950883ba19a8996fb24e2f2a5ffd3c4b830dc00d10b5f3510b0d0dL13152-R13152) [[10]](diffhunk://#diff-3c3cfd60ca950883ba19a8996fb24e2f2a5ffd3c4b830dc00d10b5f3510b0d0dL14920-R14920) [[11]](diffhunk://#diff-3c3cfd60ca950883ba19a8996fb24e2f2a5ffd3c4b830dc00d10b5f3510b0d0dL16733-R16733) [[12]](diffhunk://#diff-3c3cfd60ca950883ba19a8996fb24e2f2a5ffd3c4b830dc00d10b5f3510b0d0dL18546-R18546) [[13]](diffhunk://#diff-3c3cfd60ca950883ba19a8996fb24e2f2a5ffd3c4b830dc00d10b5f3510b0d0dL20359-R20359) [[14]](diffhunk://#diff-3c3cfd60ca950883ba19a8996fb24e2f2a5ffd3c4b830dc00d10b5f3510b0d0dL22172-R22172) * Updated the buy flow to show the configuration/settings button only when appropriate, and wired up the new modal. [[1]](diffhunk://#diff-8f431ed27f208e5f873f504bd846413111f1b8554ff5e60d1fb34080fa569b4eR448-R451) [[2]](diffhunk://#diff-8f431ed27f208e5f873f504bd846413111f1b8554ff5e60d1fb34080fa569b4eR461-R475) ### Testing * Added comprehensive tests for the new `SettingsModal`, verifying rendering, navigation, and user interactions. ### Figma Link - https://www.figma.com/design/ItZzm9CzSAjOWQTUKsOdSk/BUY?node-id=1084-4960&t=Srhw8LAFlFu5bYM6-4 - https://www.figma.com/design/ItZzm9CzSAjOWQTUKsOdSk/BUY?node-id=1225-8519&t=Srhw8LAFlFu5bYM6-4 ## **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: Add settings modal for Buy and a switcher between Buy and Deposit ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/TRAM-2825 ## **Manual testing steps** ```gherkin Feature: Buy and Deposit Scenario: user switches to Deposit Given user is in Buy (Aggregator) flow When user opens the settings And taps on the "Use new buy experience" item Then navigates to Deposit Scenario: user switches to Buy (Aggregator) Given user is in Deposit flow When user opens the settings And taps on the "More ways to buy" item Then navigates to Buy (Aggregator) ``` ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> **Buy** | **Before** | **After** | |--------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------| | <img src="https://github.com/user-attachments/assets/88641694-c07f-4b0b-b5cb-25d9cbf8a0fb" width="250px" /> | <img src="https://github.com/user-attachments/assets/c60c8989-2df6-474a-aabb-080e7d9f4ead" width="250px" /> | **Buy Settings** | **Before** | **After** | |--------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------| | N/A | <img src="https://github.com/user-attachments/assets/98d670a5-949e-48d0-bbcc-67a3e072902a" width="250px" /> | **Deposit** | **Before** | **After** | |--------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------| | <img src="https://github.com/user-attachments/assets/a2488522-fc5e-4649-a867-e4245a530512" width="250px" /> | <img src="https://github.com/user-attachments/assets/0619e279-6a63-4c98-8ae8-7b4f52d92b1f" width="250px" /> | **Deposit Settings** | **Before** | **After** | |--------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------| | <img src="https://github.com/user-attachments/assets/6d0b588c-cb9c-4801-ab3b-37e0e4c4ad5f" width="250px" /> | <img src="https://github.com/user-attachments/assets/92596f1b-fc0c-4537-8033-b2f6c65d9658" width="250px" /> | **Switcher** https://github.com/user-attachments/assets/01d1dcb9-fa83-4b91-98cc-a88264d2ee42 ## **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. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Introduces a new buy Settings bottom sheet and navigation, adds a reusable MenuItem component, updates deposit config UI (incl. new Setting icon) and strings, and wires a switcher between Buy (Aggregator) and Deposit. > > - **Ramp Aggregator (Buy)**: > - **New settings modal**: Adds `SettingsModal` bottom sheet (`Routes.RAMP.MODALS.SETTINGS`), opened from `BuildQuote` when buying; options to view order history and switch to Deposit. > - **Navigation**: Registers modal in `routes/index.tsx` and provides `createBuySettingsModalNavigationDetails`. > - **Deposit**: > - **Config modal refresh**: Uses header, shared `MenuItem`, adds "More ways to buy" to navigate to Aggregator; keeps order history/support/logout; removes old styles file. > - **Navbar icon**: Changes configuration button icon to `IconName.Setting`. > - **Shared Components**: > - **MenuItem**: New reusable list item (`app/components/UI/Ramp/components/MenuItem`) with icon/title/optional description. > - **Localization & Routes**: > - Adds/updates strings for settings, logout label, and new menu items; adds `RampSettingsModal` route key. > - **Tests/Snapshots**: > - Adds tests for `SettingsModal` and `MenuItem`; updates snapshots for icon/name and new UI. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 9c02608. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 61d7fe8 commit f8f3356

File tree

19 files changed

+1886
-206
lines changed

19 files changed

+1886
-206
lines changed

app/components/UI/Navbar/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1962,7 +1962,7 @@ export function getDepositNavbarOptions(
19621962
? () => (
19631963
<ButtonIcon
19641964
onPress={onConfigurationPress}
1965-
iconName={IconName.MoreHorizontal}
1965+
iconName={IconName.Setting}
19661966
size={ButtonIconSize.Lg}
19671967
testID="deposit-configuration-menu-button"
19681968
style={styles.headerLeftButton}

app/components/UI/Ramp/Aggregator/Views/BuildQuote/BuildQuote.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ import { trace, endTrace, TraceName } from '../../../../../../util/trace';
100100
import { CHAIN_IDS } from '@metamask/transaction-controller';
101101
import { createUnsupportedRegionModalNavigationDetails } from '../../components/UnsupportedRegionModal';
102102
import { regex } from '../../../../../../util/regex';
103+
import { createBuySettingsModalNavigationDetails } from '../Modals/Settings/SettingsModal';
103104

104105
// TODO: Replace "any" with type
105106
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -417,6 +418,10 @@ const BuildQuote = () => {
417418
}
418419
}, [screenLocation, isBuy, selectedAsset?.network?.chainId, trackEvent]);
419420

421+
const handleConfigurationPress = useCallback(() => {
422+
navigation.navigate(...createBuySettingsModalNavigationDetails());
423+
}, [navigation]);
424+
420425
useEffect(() => {
421426
navigation.setOptions(
422427
getDepositNavbarOptions(
@@ -426,12 +431,21 @@ const BuildQuote = () => {
426431
? strings('fiat_on_ramp_aggregator.amount_to_buy')
427432
: strings('fiat_on_ramp_aggregator.amount_to_sell'),
428433
showBack: params.showBack,
434+
showConfiguration: isBuy,
435+
onConfigurationPress: handleConfigurationPress,
429436
},
430437
theme,
431438
handleCancelPress,
432439
),
433440
);
434-
}, [navigation, theme, handleCancelPress, params.showBack, isBuy]);
441+
}, [
442+
navigation,
443+
theme,
444+
handleCancelPress,
445+
params.showBack,
446+
isBuy,
447+
handleConfigurationPress,
448+
]);
435449

436450
/**
437451
* * Keypad style, handlers and effects
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
// Third party dependencies.
2+
import { fireEvent } from '@testing-library/react-native';
3+
4+
// Internal dependencies.
5+
import SettingsModal from './SettingsModal';
6+
import { renderScreen } from '../../../../../../../util/test/renderWithProvider';
7+
import { backgroundState } from '../../../../../../../util/test/initial-root-state';
8+
import Routes from '../../../../../../../constants/navigation/Routes';
9+
import { createDepositNavigationDetails } from '../../../../Deposit/routes/utils';
10+
11+
const mockNavigate = jest.fn();
12+
const mockGoBack = jest.fn();
13+
const mockDangerouslyGetParent = jest.fn();
14+
15+
jest.mock('@react-navigation/native', () => {
16+
const actualReactNavigation = jest.requireActual('@react-navigation/native');
17+
return {
18+
...actualReactNavigation,
19+
useNavigation: () => ({
20+
...actualReactNavigation.useNavigation(),
21+
navigate: mockNavigate,
22+
goBack: mockGoBack,
23+
dangerouslyGetParent: mockDangerouslyGetParent,
24+
}),
25+
};
26+
});
27+
28+
function render() {
29+
return renderScreen(
30+
SettingsModal,
31+
{
32+
name: 'SettingsModal',
33+
},
34+
{
35+
state: {
36+
engine: {
37+
backgroundState,
38+
},
39+
},
40+
},
41+
);
42+
}
43+
44+
describe('SettingsModal', () => {
45+
beforeEach(() => {
46+
jest.clearAllMocks();
47+
mockDangerouslyGetParent.mockReturnValue({
48+
dangerouslyGetParent: jest.fn().mockReturnValue({
49+
goBack: jest.fn(),
50+
}),
51+
});
52+
});
53+
54+
it('renders snapshot correctly', () => {
55+
const { toJSON } = render();
56+
expect(toJSON()).toMatchSnapshot();
57+
});
58+
59+
it('displays settings title in header', () => {
60+
const { getByText } = render();
61+
62+
expect(getByText('Settings')).toBeTruthy();
63+
});
64+
65+
it('displays view order history menu item', () => {
66+
const { getByText } = render();
67+
68+
expect(getByText('View order history')).toBeTruthy();
69+
});
70+
71+
it('displays use new buy experience menu item', () => {
72+
const { getByText } = render();
73+
74+
expect(getByText('Use new buy experience')).toBeTruthy();
75+
expect(getByText('Try new native on ramp')).toBeTruthy();
76+
});
77+
78+
it('navigates to transactions view when view order history is pressed', () => {
79+
const { getByText } = render();
80+
const viewOrderHistoryButton = getByText('View order history');
81+
82+
fireEvent.press(viewOrderHistoryButton);
83+
84+
expect(mockNavigate).toHaveBeenCalledWith(Routes.TRANSACTIONS_VIEW, {
85+
screen: Routes.TRANSACTIONS_VIEW,
86+
params: {
87+
redirectToOrders: true,
88+
},
89+
});
90+
});
91+
92+
it('navigates to deposit when use new buy experience is pressed', () => {
93+
const { getByText } = render();
94+
const newBuyExperienceButton = getByText('Use new buy experience');
95+
96+
fireEvent.press(newBuyExperienceButton);
97+
98+
expect(mockDangerouslyGetParent).toHaveBeenCalled();
99+
expect(mockNavigate).toHaveBeenCalledWith(
100+
...createDepositNavigationDetails(),
101+
);
102+
});
103+
104+
it('navigates back through parent navigation when deposit is pressed', () => {
105+
const mockParentGoBack = jest.fn();
106+
mockDangerouslyGetParent.mockReturnValue({
107+
dangerouslyGetParent: jest.fn().mockReturnValue({
108+
goBack: mockParentGoBack,
109+
}),
110+
});
111+
112+
const { getByText } = render();
113+
const newBuyExperienceButton = getByText('Use new buy experience');
114+
115+
fireEvent.press(newBuyExperienceButton);
116+
117+
expect(mockParentGoBack).toHaveBeenCalled();
118+
});
119+
120+
describe('bottom sheet behavior', () => {
121+
it('renders bottom sheet with settings content', () => {
122+
const { getByText } = render();
123+
124+
expect(getByText('Settings')).toBeTruthy();
125+
});
126+
});
127+
128+
describe('menu item icons', () => {
129+
it('renders clock icon for view order history', () => {
130+
const { getByText } = render();
131+
132+
expect(getByText('View order history')).toBeTruthy();
133+
});
134+
135+
it('renders add icon for new buy experience', () => {
136+
const { getByText } = render();
137+
138+
expect(getByText('Use new buy experience')).toBeTruthy();
139+
});
140+
});
141+
142+
describe('callback functions', () => {
143+
it('calls navigation callbacks only when menu items are pressed', () => {
144+
render();
145+
146+
expect(mockNavigate).not.toHaveBeenCalled();
147+
expect(mockDangerouslyGetParent).not.toHaveBeenCalled();
148+
});
149+
});
150+
});
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import React, { useCallback, useRef } from 'react';
2+
import { useNavigation } from '@react-navigation/native';
3+
import { strings } from '../../../../../../../../locales/i18n';
4+
import BottomSheet, {
5+
BottomSheetRef,
6+
} from '../../../../../../../component-library/components/BottomSheets/BottomSheet';
7+
import BottomSheetHeader from '../../../../../../../component-library/components/BottomSheets/BottomSheetHeader';
8+
import { IconName } from '../../../../../../../component-library/components/Icons/Icon';
9+
import Routes from '../../../../../../../constants/navigation/Routes';
10+
import { createNavigationDetails } from '../../../../../../../util/navigation/navUtils';
11+
import MenuItem from '../../../../components/MenuItem';
12+
import { createDepositNavigationDetails } from '../../../../Deposit/routes/utils';
13+
14+
export const createBuySettingsModalNavigationDetails = createNavigationDetails(
15+
Routes.RAMP.MODALS.ID,
16+
Routes.RAMP.MODALS.SETTINGS,
17+
);
18+
19+
function SettingsModal() {
20+
const sheetRef = useRef<BottomSheetRef>(null);
21+
const navigation = useNavigation();
22+
23+
const handleNavigateToOrderHistory = useCallback(() => {
24+
sheetRef.current?.onCloseBottomSheet();
25+
navigation.navigate(Routes.TRANSACTIONS_VIEW, {
26+
screen: Routes.TRANSACTIONS_VIEW,
27+
params: {
28+
redirectToOrders: true,
29+
},
30+
});
31+
}, [navigation]);
32+
33+
const handleDepositPress = useCallback(() => {
34+
sheetRef.current?.onCloseBottomSheet();
35+
navigation.dangerouslyGetParent()?.dangerouslyGetParent()?.goBack();
36+
navigation.navigate(...createDepositNavigationDetails());
37+
}, [navigation]);
38+
39+
const handleClosePress = useCallback(() => {
40+
sheetRef.current?.onCloseBottomSheet();
41+
}, []);
42+
43+
return (
44+
<BottomSheet ref={sheetRef} shouldNavigateBack>
45+
<BottomSheetHeader onClose={handleClosePress}>
46+
{strings('fiat_on_ramp_aggregator.settings_modal.title')}
47+
</BottomSheetHeader>
48+
<MenuItem
49+
iconName={IconName.Clock}
50+
title={strings('deposit.configuration_modal.view_order_history')}
51+
onPress={handleNavigateToOrderHistory}
52+
/>
53+
<MenuItem
54+
iconName={IconName.Add}
55+
title={strings(
56+
'fiat_on_ramp_aggregator.settings_modal.use_new_buy_experience',
57+
)}
58+
description={strings(
59+
'fiat_on_ramp_aggregator.settings_modal.use_new_buy_experience_description',
60+
)}
61+
onPress={handleDepositPress}
62+
/>
63+
</BottomSheet>
64+
);
65+
}
66+
67+
export default SettingsModal;

0 commit comments

Comments
 (0)