Skip to content

Commit 1d9312d

Browse files
authored
feat(ramps): adopts new controller user region api interface (#24280)
<!-- 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? --> ## **Description** This PR adopts the breaking changes from `@metamask/ramps-controller` v3.1.0, which refactors the controller to use "userRegion" terminology instead of "geolocation" and introduces proper initialization patterns. ### **Reason for Change** The ramps-controller package was refactored to: 1. **Improve naming clarity**: "geolocation" implies a read-only, automatically-detected value, but we plan to allow users to manually change their region. The new "userRegion" terminology better reflects that it's a user-selectable region. 2. **Add proper initialization**: Previously, consumers had to manually call `updateGeolocation()` after creating the controller, which was error-prone. The new `init()` method encapsulates initialization logic and automatically fetches the user region at startup. 3. **Enable manual region setting**: The new `setUserRegion()` method allows users to manually set their region without calling the geolocation API, preparing for future UI features. ### **Changes in This PR** #### **Selectors** (`app/selectors/rampsController/index.ts`) - ✅ Removed deprecated `selectGeolocation` selector - ✅ Removed deprecated `selectGeolocationRequest` selector - ✅ Added `selectUserRegion` selector (replaces `selectGeolocation`) - ✅ Added `selectUserRegionRequest` selector (replaces `selectGeolocationRequest`) #### **Hooks** (`app/components/UI/Ramp/hooks/`) - ✅ Renamed `useRampsGeolocation.ts` → `useRampsUserRegion.ts` - ✅ Updated hook to use new `updateUserRegion()` and `setUserRegion()` methods - ✅ Updated hook to use new `selectUserRegion` and `selectUserRegionRequest` selectors - ✅ Updated all references and imports throughout the codebase #### **Controller Initialization** (`app/core/Engine/controllers/ramps-controller/ramps-controller-init.ts`) - ✅ Updated to call `controller.init()` at startup (non-blocking) - ✅ Initialization automatically fetches user region via geolocation API - ✅ Errors are handled gracefully and available via selectors #### **Tests** - ✅ Updated `ramps-controller-init.test.ts` to test new `init()` method - ✅ Updated `useRampsUserRegion.test.ts` to use new API and cache keys - ✅ Updated selector tests to use `userRegion` state property - ✅ All tests updated to use `'updateUserRegion'` cache key instead of `'updateGeolocation'` #### **Yarn Patch** - ✅ Updated ramps-controller patch to include all local changes (userRegion state, updateUserRegion, setUserRegion, init methods) ### **Migration Guide** **Before:** ```ts import { selectGeolocation, selectGeolocationRequest } from './selectors/rampsController'; import { useRampsGeolocation } from './hooks/useRampsGeolocation'; // Manual initialization required Engine.context.RampsController.updateGeolocation(); ``` **After:** ```ts import { selectUserRegion, selectUserRegionRequest } from './selectors/rampsController'; import { useRampsUserRegion } from './hooks/useRampsUserRegion'; // Automatic initialization via controller.init() in ramps-controller-init.ts ``` ### **Breaking Changes Adopted** - ❌ `geolocation` state property → ✅ `userRegion` - ❌ `updateGeolocation()` method → ✅ `updateUserRegion()` - ❌ `selectGeolocation` selector → ✅ `selectUserRegion` - ❌ `selectGeolocationRequest` selector → ✅ `selectUserRegionRequest` - ❌ `useRampsGeolocation()` hook → ✅ `useRampsUserRegion()` ### **Related PRs** - Core package PR: [MetaMask/core#7563](MetaMask/core#7563) - Refactors ramps-controller to use userRegion terminology and adds init() method ## **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: Adopted new ramp controller variable names ## **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** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **Pre-merge author checklist** - [ ] 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). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] 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] > Adopts the new `@metamask/ramps-controller` v3 APIs and init model. > > - Migrate from `geolocation` to `userRegion`: new `useRampsUserRegion` hook (with `fetchUserRegion` and `setUserRegion`), selectors `selectUserRegion`/`selectUserRegionRequest` (retain `selectGeolocation` aliases); update `useRampsGeolocation` to call `updateUserRegion` > - Initialize controller via `controller.init()` at startup (`ramps-controller-init`), replacing `updateGeolocation` side-effect > - Refactor ramps service init: derive environment via `getRampsEnvironment` and platform context via `getRampsContext` (uses `Platform.OS`) > - Update fixtures, snapshots, and tests accordingly (new request keys `updateUserRegion:[]`, added `eligibility`/`tokens` fields) > - Bump dependency to `@metamask/ramps-controller@^3.0.0` > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit c99674b. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 42a758f commit 1d9312d

File tree

15 files changed

+891
-115
lines changed

15 files changed

+891
-115
lines changed

app/components/UI/Ramp/hooks/useRampsGeolocation.test.ts

Lines changed: 299 additions & 23 deletions
Large diffs are not rendered by default.

app/components/UI/Ramp/hooks/useRampsGeolocation.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,14 @@ export interface UseRampsGeolocationResult {
4747
*/
4848
export function useRampsGeolocation(): UseRampsGeolocationResult {
4949
const geolocation = useSelector(selectGeolocation);
50-
const { isFetching, error } = useSelector(selectGeolocationRequest);
50+
const { isFetching, error } = useSelector(selectGeolocationRequest) as {
51+
isFetching: boolean;
52+
error: string | null;
53+
};
5154

5255
const fetchGeolocation = useCallback(
5356
(options?: ExecuteRequestOptions) =>
54-
Engine.context.RampsController.updateGeolocation(options),
57+
Engine.context.RampsController.updateUserRegion(options),
5558
[],
5659
);
5760

@@ -69,4 +72,6 @@ export function useRampsGeolocation(): UseRampsGeolocationResult {
6972
};
7073
}
7174

75+
export { useRampsUserRegion } from './useRampsUserRegion';
76+
7277
export default useRampsGeolocation;
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
import { renderHook, waitFor } from '@testing-library/react-native';
2+
import { Provider } from 'react-redux';
3+
import { configureStore } from '@reduxjs/toolkit';
4+
import React from 'react';
5+
import { useRampsUserRegion } from './useRampsUserRegion';
6+
import { RequestStatus } from '@metamask/ramps-controller';
7+
import Engine from '../../../../core/Engine';
8+
9+
jest.mock('../../../../core/Engine', () => ({
10+
context: {
11+
RampsController: {
12+
updateUserRegion: jest.fn().mockResolvedValue('US'),
13+
setUserRegion: jest.fn().mockResolvedValue({
14+
aggregator: true,
15+
deposit: true,
16+
global: true,
17+
}),
18+
},
19+
},
20+
}));
21+
22+
const createMockStore = (rampsControllerState = {}) =>
23+
configureStore({
24+
reducer: {
25+
engine: () => ({
26+
backgroundState: {
27+
RampsController: {
28+
userRegion: null,
29+
requests: {},
30+
...rampsControllerState,
31+
},
32+
},
33+
}),
34+
},
35+
});
36+
37+
const wrapper = (store: ReturnType<typeof createMockStore>) =>
38+
function Wrapper({ children }: { children: React.ReactNode }) {
39+
return React.createElement(Provider, { store } as never, children);
40+
};
41+
42+
describe('useRampsUserRegion', () => {
43+
beforeEach(() => {
44+
jest.clearAllMocks();
45+
});
46+
47+
describe('return value structure', () => {
48+
it('returns userRegion, isLoading, error, fetchUserRegion, and setUserRegion', () => {
49+
const store = createMockStore();
50+
const { result } = renderHook(() => useRampsUserRegion(), {
51+
wrapper: wrapper(store),
52+
});
53+
expect(result.current).toMatchObject({
54+
userRegion: null,
55+
isLoading: false,
56+
error: null,
57+
});
58+
expect(typeof result.current.fetchUserRegion).toBe('function');
59+
expect(typeof result.current.setUserRegion).toBe('function');
60+
});
61+
});
62+
63+
describe('userRegion state', () => {
64+
it('returns userRegion from state', () => {
65+
const store = createMockStore({ userRegion: 'US-CA' });
66+
const { result } = renderHook(() => useRampsUserRegion(), {
67+
wrapper: wrapper(store),
68+
});
69+
expect(result.current.userRegion).toBe('US-CA');
70+
});
71+
});
72+
73+
describe('loading state', () => {
74+
it('returns isLoading true when request is loading', () => {
75+
const store = createMockStore({
76+
requests: {
77+
'updateUserRegion:[]': {
78+
status: RequestStatus.LOADING,
79+
data: null,
80+
error: null,
81+
timestamp: Date.now(),
82+
lastFetchedAt: Date.now(),
83+
},
84+
},
85+
});
86+
const { result } = renderHook(() => useRampsUserRegion(), {
87+
wrapper: wrapper(store),
88+
});
89+
expect(result.current.isLoading).toBe(true);
90+
});
91+
});
92+
93+
describe('error state', () => {
94+
it('returns error from request state', () => {
95+
const store = createMockStore({
96+
requests: {
97+
'updateUserRegion:[]': {
98+
status: RequestStatus.ERROR,
99+
data: null,
100+
error: 'Network error',
101+
timestamp: Date.now(),
102+
lastFetchedAt: Date.now(),
103+
},
104+
},
105+
});
106+
const { result } = renderHook(() => useRampsUserRegion(), {
107+
wrapper: wrapper(store),
108+
});
109+
expect(result.current.error).toBe('Network error');
110+
});
111+
});
112+
113+
describe('fetchUserRegion', () => {
114+
it('calls updateUserRegion without options when called with no arguments', async () => {
115+
const store = createMockStore();
116+
const { result } = renderHook(() => useRampsUserRegion(), {
117+
wrapper: wrapper(store),
118+
});
119+
await result.current.fetchUserRegion();
120+
expect(
121+
Engine.context.RampsController.updateUserRegion,
122+
).toHaveBeenCalledWith(undefined);
123+
});
124+
125+
it('calls updateUserRegion with forceRefresh true when specified', async () => {
126+
const store = createMockStore();
127+
const { result } = renderHook(() => useRampsUserRegion(), {
128+
wrapper: wrapper(store),
129+
});
130+
await result.current.fetchUserRegion({ forceRefresh: true });
131+
expect(
132+
Engine.context.RampsController.updateUserRegion,
133+
).toHaveBeenCalledWith({
134+
forceRefresh: true,
135+
});
136+
});
137+
138+
it('calls updateUserRegion with forceRefresh false when specified', async () => {
139+
const store = createMockStore();
140+
const { result } = renderHook(() => useRampsUserRegion(), {
141+
wrapper: wrapper(store),
142+
});
143+
await result.current.fetchUserRegion({ forceRefresh: false });
144+
expect(
145+
Engine.context.RampsController.updateUserRegion,
146+
).toHaveBeenCalledWith({
147+
forceRefresh: false,
148+
});
149+
});
150+
151+
it('rejects with error when updateUserRegion fails', async () => {
152+
const store = createMockStore();
153+
const mockUpdateUserRegion = Engine.context.RampsController
154+
.updateUserRegion as jest.Mock;
155+
mockUpdateUserRegion.mockReset();
156+
mockUpdateUserRegion.mockRejectedValue(new Error('Network error'));
157+
158+
const { result } = renderHook(() => useRampsUserRegion(), {
159+
wrapper: wrapper(store),
160+
});
161+
162+
await expect(result.current.fetchUserRegion()).rejects.toThrow(
163+
'Network error',
164+
);
165+
});
166+
});
167+
168+
describe('setUserRegion', () => {
169+
it('calls setUserRegion on controller', async () => {
170+
const store = createMockStore();
171+
const { result } = renderHook(() => useRampsUserRegion(), {
172+
wrapper: wrapper(store),
173+
});
174+
await result.current.setUserRegion('US-CA');
175+
expect(Engine.context.RampsController.setUserRegion).toHaveBeenCalledWith(
176+
'US-CA',
177+
undefined,
178+
);
179+
});
180+
181+
it('calls setUserRegion with options when specified', async () => {
182+
const store = createMockStore();
183+
const { result } = renderHook(() => useRampsUserRegion(), {
184+
wrapper: wrapper(store),
185+
});
186+
await result.current.setUserRegion('US-CA', { forceRefresh: true });
187+
expect(Engine.context.RampsController.setUserRegion).toHaveBeenCalledWith(
188+
'US-CA',
189+
{ forceRefresh: true },
190+
);
191+
});
192+
});
193+
194+
describe('useEffect error handling', () => {
195+
it('returns default state when fetchUserRegion rejects in useEffect', async () => {
196+
const store = createMockStore();
197+
const mockUpdateUserRegion = Engine.context.RampsController
198+
.updateUserRegion as jest.Mock;
199+
mockUpdateUserRegion.mockReset();
200+
mockUpdateUserRegion.mockRejectedValue(new Error('Fetch failed'));
201+
202+
const { result } = renderHook(() => useRampsUserRegion(), {
203+
wrapper: wrapper(store),
204+
});
205+
206+
await waitFor(() => {
207+
expect(mockUpdateUserRegion).toHaveBeenCalled();
208+
});
209+
210+
expect(result.current).toMatchObject({
211+
userRegion: null,
212+
isLoading: false,
213+
error: null,
214+
});
215+
expect(typeof result.current.fetchUserRegion).toBe('function');
216+
});
217+
});
218+
});
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { useCallback, useEffect } from 'react';
2+
import { useSelector } from 'react-redux';
3+
import Engine from '../../../../core/Engine';
4+
import {
5+
selectUserRegion,
6+
selectUserRegionRequest,
7+
} from '../../../../selectors/rampsController';
8+
import {
9+
ExecuteRequestOptions,
10+
RequestSelectorResult,
11+
} from '@metamask/ramps-controller';
12+
13+
/**
14+
* Result returned by the useRampsUserRegion hook.
15+
*/
16+
export interface UseRampsUserRegionResult {
17+
/**
18+
* The user's region code (e.g., "US-CA"), or null if not loaded.
19+
*/
20+
userRegion: string | null;
21+
/**
22+
* Whether the user region request is currently loading.
23+
*/
24+
isLoading: boolean;
25+
/**
26+
* The error message if the request failed, or null.
27+
*/
28+
error: string | null;
29+
/**
30+
* Manually fetch the user region from geolocation.
31+
*/
32+
fetchUserRegion: (options?: ExecuteRequestOptions) => Promise<string>;
33+
/**
34+
* Set the user region manually (without fetching geolocation).
35+
*/
36+
setUserRegion: (
37+
region: string,
38+
options?: ExecuteRequestOptions,
39+
) => Promise<void>;
40+
}
41+
42+
/**
43+
* Hook to get the user's region state from RampsController.
44+
* This hook assumes Engine is already initialized.
45+
*
46+
* @returns User region state and fetch/set functions.
47+
*/
48+
export function useRampsUserRegion(): UseRampsUserRegionResult {
49+
const userRegion = useSelector(selectUserRegion);
50+
const { isFetching, error } = useSelector(
51+
selectUserRegionRequest,
52+
) as RequestSelectorResult<string>;
53+
54+
const fetchUserRegion = useCallback(
55+
(options?: ExecuteRequestOptions) =>
56+
Engine.context.RampsController.updateUserRegion(options),
57+
[],
58+
);
59+
60+
const setUserRegion = useCallback(
61+
async (region: string, options?: ExecuteRequestOptions) => {
62+
await Engine.context.RampsController.setUserRegion(region, options);
63+
},
64+
[],
65+
);
66+
67+
useEffect(() => {
68+
fetchUserRegion().catch(() => {
69+
// Error is stored in state
70+
});
71+
}, [fetchUserRegion]);
72+
73+
return {
74+
userRegion,
75+
isLoading: isFetching,
76+
error,
77+
fetchUserRegion,
78+
setUserRegion,
79+
};
80+
}
81+
82+
export default useRampsUserRegion;

app/core/Engine/Engine.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,7 @@ describe('Engine', () => {
278278
RampsController: {
279279
...backgroundState.RampsController,
280280
eligibility: null,
281+
tokens: null,
281282
},
282283
};
283284

0 commit comments

Comments
 (0)