Skip to content

Commit 609e9a8

Browse files
authored
fix(ramps): ensure Buy tokens load on token selection screen on fresh install cp-7.68.0 (MetaMask#26952)
## **Description** **Problem:** When users tap Buy and reach Token Selection (non-V2) before geolocation and smart routing have run, the screen can show a blank or incorrect state because `detectedGeolocation` and `rampRoutingDecision` are not yet set. Previously, these ran only when the FiatOrders component mounted (e.g. when the user entered a Ramp flow), which could be too late. **Solution:** Introduce a **RampsBootstrap** component that runs at app root as soon as the app mounts: 1. **RampsBootstrap** (`app/components/UI/Ramp/RampsBootstrap.tsx`) – New component that runs three hooks at app root: - `useDetectGeolocation()` – detect user region - `useRampsSmartRouting()` – set ramp routing decision (AGGREGATOR / DEPOSIT / etc.) - `useHydrateRampsController()` – when V2 is enabled, ensure controller init/hydrate runs early 2. **App.tsx** – Mount `<RampsBootstrap />` at the root (direct import from `Ramp/RampsBootstrap`). Marked with a TODO to remove once V2 flag is on for all users. 3. **Ramp index (FiatOrders)** – Remove the three hook calls (`useDetectGeolocation`, `useRampsSmartRouting`, `useHydrateRampsController`) from `FiatOrders` so they run only once at app root in RampsBootstrap. `FiatOrders` continues to run `useFetchRampNetworks()` and order polling. By the time the user reaches Buy → Token Selection, geolocation and routing are usually already set, improving non-V2 behavior. ## **Changelog** CHANGELOG entry: Run Ramps geolocation, smart routing, and controller hydrate at app root via RampsBootstrap so Token Selection (non-V2) has region and routing ready sooner. ## **Related issues** Fixes: MetaMask#26699 ## **Manual testing steps** ```gherkin Feature: Ramps bootstrap at app root Scenario: User opens app and navigates to Buy Token Selection (non-V2) Given the app has just started When the user taps Buy and reaches the Select token screen Then geolocation and smart routing have already run (bootstrap at app root) And the token list loads according to region and routing (no blank/empty due to missing geolocation) Scenario: V2 flow still works Given Ramps V2 is enabled When the user goes to Buy and Token Selection Then tokens load (Engine init plus bootstrap hydrate as before) ``` ## **Screenshots/Recordings** <div> <a href="https://www.loom.com/share/b7b90cdc4a6645719d2bae04f28c2e3d"> <p>Fix buy token loading issue - 26699 - Watch Video</p> </a> <a href="https://www.loom.com/share/b7b90cdc4a6645719d2bae04f28c2e3d"> <img style="max-width:300px;" src="https://cdn.loom.com/sessions/thumbnails/b7b90cdc4a6645719d2bae04f28c2e3d-c643ea7310eb57da-full-play.gif#t=0.1"> </a> </div> ## **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 - [ ] 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] > **Medium Risk** > Moves ramp bootstrap hooks (geolocation, smart routing, controller hydration) to run at app startup, which can affect ramp availability decisions and controller init timing across sessions. Risk is moderate due to new side effects on app mount and potential extra network calls, but changes are localized and covered by tests. > > **Overview** > Ensures ramp prerequisites are initialized *as soon as the app mounts* by introducing `RampsBootstrap`, which runs geolocation detection, smart routing, and ramps controller hydration and renders `null`. > > Mounts `RampsBootstrap` at the `App` root (with a TODO to remove once V2 is fully rolled out) and removes the same hook calls from `FiatOrders`, preventing late/duplicate initialization when users reach non-V2 Buy token selection. Adds unit tests for `RampsBootstrap` and updates existing app/ramp tests to mock the new root-level component. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 0013f0e. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent e84c09e commit 609e9a8

6 files changed

Lines changed: 78 additions & 11 deletions

File tree

app/components/Nav/App/App.test.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ jest.mock('../../UI/Predict/hooks/usePredictToastRegistrations', () => ({
5757
usePredictToastRegistrations: jest.fn().mockReturnValue([]),
5858
}));
5959

60+
jest.mock('../../UI/Ramp/RampsBootstrap', () => () => null);
61+
6062
jest.mock('expo-sensors', () => ({
6163
Accelerometer: {
6264
setUpdateInterval: jest.fn(),

app/components/Nav/App/App.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ import ConfirmTurnOnBackupAndSyncModal from '../../UI/Identity/ConfirmTurnOnBack
126126
import AddNewAccountBottomSheet from '../../Views/AddNewAccount/AddNewAccountBottomSheet';
127127
import EligibilityFailedModal from '../../UI/Ramp/components/EligibilityFailedModal';
128128
import RampUnsupportedModal from '../../UI/Ramp/components/RampUnsupportedModal';
129+
import RampsBootstrap from '../../UI/Ramp/RampsBootstrap';
129130
import SwitchAccountTypeModal from '../../Views/confirmations/components/modals/switch-account-type-modal';
130131
import { AccountDetails } from '../../Views/MultichainAccounts/AccountDetails/AccountDetails';
131132
import { AccountGroupDetails } from '../../Views/MultichainAccounts/AccountGroupDetails/AccountGroupDetails';
@@ -1168,6 +1169,8 @@ const App: React.FC = () => {
11681169

11691170
return (
11701171
<WebSocketHealthToastProvider>
1172+
{/* TODO: Temporary fix for non-V2 Buy token selection; remove RampsBootstrap once V2 flag is on for all users. */}
1173+
<RampsBootstrap />
11711174
<AppFlow />
11721175
<Toast ref={toastRef} />
11731176
<PerpsWebSocketHealthToast />
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import React from 'react';
2+
import { render } from '@testing-library/react-native';
3+
import RampsBootstrap from './RampsBootstrap';
4+
5+
const mockUseDetectGeolocation = jest.fn();
6+
const mockUseRampsSmartRouting = jest.fn();
7+
const mockUseHydrateRampsController = jest.fn();
8+
9+
jest.mock('./hooks/useDetectGeolocation', () => ({
10+
__esModule: true,
11+
default: (...args: unknown[]) => mockUseDetectGeolocation(...args),
12+
}));
13+
14+
jest.mock('./hooks/useRampsSmartRouting', () => ({
15+
__esModule: true,
16+
default: (...args: unknown[]) => mockUseRampsSmartRouting(...args),
17+
}));
18+
19+
jest.mock('./hooks/useHydrateRampsController', () => ({
20+
__esModule: true,
21+
default: (...args: unknown[]) => mockUseHydrateRampsController(...args),
22+
}));
23+
24+
describe('RampsBootstrap', () => {
25+
afterEach(() => {
26+
jest.clearAllMocks();
27+
});
28+
29+
it('calls useDetectGeolocation on mount', () => {
30+
render(<RampsBootstrap />);
31+
32+
expect(mockUseDetectGeolocation).toHaveBeenCalledTimes(1);
33+
});
34+
35+
it('calls useRampsSmartRouting on mount', () => {
36+
render(<RampsBootstrap />);
37+
38+
expect(mockUseRampsSmartRouting).toHaveBeenCalledTimes(1);
39+
});
40+
41+
it('calls useHydrateRampsController on mount', () => {
42+
render(<RampsBootstrap />);
43+
44+
expect(mockUseHydrateRampsController).toHaveBeenCalledTimes(1);
45+
});
46+
47+
it('renders null', () => {
48+
const { toJSON } = render(<RampsBootstrap />);
49+
50+
expect(toJSON()).toBeNull();
51+
});
52+
});
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import useDetectGeolocation from './hooks/useDetectGeolocation';
2+
import useHydrateRampsController from './hooks/useHydrateRampsController';
3+
import useRampsSmartRouting from './hooks/useRampsSmartRouting';
4+
5+
/**
6+
* Ramps app bootstrap: runs geolocation detection, smart routing, and controller
7+
* hydration as soon as the app mounts so that by the time the user reaches Buy →
8+
* Token Selection (non-V2), detectedGeolocation and rampRoutingDecision are
9+
* often already set.
10+
*
11+
* V2: RampsController is initialized by Engine (rampsControllerInit); no UI
12+
* init. Mount at app root.
13+
*/
14+
function RampsBootstrap(): null {
15+
useDetectGeolocation();
16+
useRampsSmartRouting();
17+
useHydrateRampsController();
18+
return null;
19+
}
20+
21+
export default RampsBootstrap;

app/components/UI/Ramp/index.test.tsx

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,6 @@ import getAggregatorAnalyticsPayload from './Aggregator/utils/getAggregatorAnaly
1414

1515
const mockNavigate = jest.fn();
1616

17-
jest.mock('./hooks/useHydrateRampsController', () => ({
18-
__esModule: true,
19-
default: jest.fn(),
20-
}));
21-
2217
jest.mock('@react-navigation/native', () => {
2318
const actual = jest.requireActual('@react-navigation/native');
2419
return {

app/components/UI/Ramp/index.tsx

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,6 @@ import stateHasOrder from './utils/stateHasOrder';
3333
import Routes from '../../../constants/navigation/Routes';
3434
import getOrderAnalyticsPayload from './utils/getOrderAnalyticsPayload';
3535
import { NativeRampsSdk } from '@consensys/native-ramps-sdk';
36-
import useDetectGeolocation from './hooks/useDetectGeolocation';
37-
import useHydrateRampsController from './hooks/useHydrateRampsController';
38-
import useRampsSmartRouting from './hooks/useRampsSmartRouting';
3936
import { RampsOrderStatus } from '@metamask/ramps-controller';
4037
import { isRampsUnifiedV2Enabled } from './utils/isRampsUnifiedV2Enabled';
4138
import { showV2OrderToast } from './utils/v2OrderToast';
@@ -141,10 +138,7 @@ const styles = StyleSheet.create({
141138
});
142139

143140
function FiatOrders() {
144-
useHydrateRampsController();
145141
useFetchRampNetworks();
146-
useDetectGeolocation();
147-
useRampsSmartRouting();
148142
const dispatch = useDispatch();
149143
const dispatchThunk = useThunkDispatch();
150144
const navigation = useNavigation();

0 commit comments

Comments
 (0)