diff --git a/packages/ramps-controller/CHANGELOG.md b/packages/ramps-controller/CHANGELOG.md index 180893facd6..b6ce122cb47 100644 --- a/packages/ramps-controller/CHANGELOG.md +++ b/packages/ramps-controller/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- **BREAKING:** Rename `geolocation` to `userRegion` and `updateGeolocation()` to `updateUserRegion()` in RampsController ([#7563](https://github.com/MetaMask/core/pull/7563)) + - Bump `@metamask/controller-utils` from `^11.17.0` to `^11.18.0` ([#7583](https://github.com/MetaMask/core/pull/7583)) ## [2.1.0] @@ -21,6 +23,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add request caching infrastructure with TTL, deduplication, and abort support ([#7536](https://github.com/MetaMask/core/pull/7536)) +- Add `init()` and `setUserRegion()` methods to RampsController ([#7563](https://github.com/MetaMask/core/pull/7563)) + ### Changed - Bump `@metamask/controller-utils` from `^11.16.0` to `^11.17.0` ([#7534](https://github.com/MetaMask/core/pull/7534)) diff --git a/packages/ramps-controller/src/RampsController.test.ts b/packages/ramps-controller/src/RampsController.test.ts index 1e8e541e56e..45f76c1f232 100644 --- a/packages/ramps-controller/src/RampsController.test.ts +++ b/packages/ramps-controller/src/RampsController.test.ts @@ -23,8 +23,8 @@ describe('RampsController', () => { expect(controller.state).toMatchInlineSnapshot(` Object { "eligibility": null, - "geolocation": null, "requests": Object {}, + "userRegion": null, } `); }); @@ -32,7 +32,7 @@ describe('RampsController', () => { it('accepts initial state', async () => { const givenState = { - geolocation: 'US', + userRegion: 'US', }; await withController( @@ -40,7 +40,7 @@ describe('RampsController', () => { ({ controller }) => { expect(controller.state).toStrictEqual({ eligibility: null, - geolocation: 'US', + userRegion: 'US', requests: {}, }); }, @@ -52,8 +52,8 @@ describe('RampsController', () => { expect(controller.state).toMatchInlineSnapshot(` Object { "eligibility": null, - "geolocation": null, "requests": Object {}, + "userRegion": null, } `); }); @@ -61,7 +61,7 @@ describe('RampsController', () => { it('always resets requests cache on initialization', async () => { const givenState = { - geolocation: 'US', + userRegion: 'US', requests: { someKey: { status: RequestStatus.SUCCESS, @@ -94,8 +94,8 @@ describe('RampsController', () => { ).toMatchInlineSnapshot(` Object { "eligibility": null, - "geolocation": null, "requests": Object {}, + "userRegion": null, } `); }); @@ -112,7 +112,7 @@ describe('RampsController', () => { ).toMatchInlineSnapshot(` Object { "eligibility": null, - "geolocation": null, + "userRegion": null, } `); }); @@ -129,7 +129,7 @@ describe('RampsController', () => { ).toMatchInlineSnapshot(` Object { "eligibility": null, - "geolocation": null, + "userRegion": null, } `); }); @@ -146,20 +146,20 @@ describe('RampsController', () => { ).toMatchInlineSnapshot(` Object { "eligibility": null, - "geolocation": null, "requests": Object {}, + "userRegion": null, } `); }); }); }); - describe('updateGeolocation', () => { - it('updates geolocation state when geolocation is fetched', async () => { + describe('updateUserRegion', () => { + it('updates user region state when region is fetched', async () => { await withController(async ({ controller, rootMessenger }) => { rootMessenger.registerActionHandler( 'RampsService:getGeolocation', - async () => 'US', + async () => 'US-CA', ); rootMessenger.registerActionHandler( 'RampsService:getEligibility', @@ -170,9 +170,9 @@ describe('RampsController', () => { }), ); - await controller.updateGeolocation(); + await controller.updateUserRegion(); - expect(controller.state.geolocation).toBe('US'); + expect(controller.state.userRegion).toBe('us-ca'); }); }); @@ -191,9 +191,9 @@ describe('RampsController', () => { }), ); - await controller.updateGeolocation(); + await controller.updateUserRegion(); - const cacheKey = createCacheKey('updateGeolocation', []); + const cacheKey = createCacheKey('updateUserRegion', []); const requestState = controller.state.requests[cacheKey]; expect(requestState).toBeDefined(); @@ -222,8 +222,8 @@ describe('RampsController', () => { }), ); - await controller.updateGeolocation(); - await controller.updateGeolocation(); + await controller.updateUserRegion(); + await controller.updateUserRegion(); expect(callCount).toBe(1); }); @@ -248,12 +248,42 @@ describe('RampsController', () => { }), ); - await controller.updateGeolocation(); - await controller.updateGeolocation({ forceRefresh: true }); + await controller.updateUserRegion(); + await controller.updateUserRegion({ forceRefresh: true }); expect(callCount).toBe(2); }); }); + + it('handles null geolocation result', async () => { + await withController(async ({ controller, rootMessenger }) => { + rootMessenger.registerActionHandler( + 'RampsService:getGeolocation', + async () => null as unknown as string, + ); + + const result = await controller.updateUserRegion(); + + expect(result).toBeNull(); + expect(controller.state.userRegion).toBeNull(); + expect(controller.state.eligibility).toBeNull(); + }); + }); + + it('handles undefined geolocation result', async () => { + await withController(async ({ controller, rootMessenger }) => { + rootMessenger.registerActionHandler( + 'RampsService:getGeolocation', + async () => undefined as unknown as string, + ); + + const result = await controller.updateUserRegion(); + + expect(result).toBeUndefined(); + expect(controller.state.userRegion).toBeUndefined(); + expect(controller.state.eligibility).toBeNull(); + }); + }); }); describe('executeRequest', () => { @@ -749,10 +779,197 @@ describe('RampsController', () => { expect(requestState?.status).toBe('success'); }); }); + + it('updates eligibility when userRegion matches the ISO code', async () => { + await withController( + { options: { state: { userRegion: 'us' } } }, + async ({ controller, rootMessenger }) => { + const mockEligibility = { + aggregator: true, + deposit: true, + global: true, + }; + + rootMessenger.registerActionHandler( + 'RampsService:getEligibility', + async (isoCode) => { + expect(isoCode).toBe('us'); + return mockEligibility; + }, + ); + + expect(controller.state.userRegion).toBe('us'); + expect(controller.state.eligibility).toBeNull(); + + await controller.updateEligibility('US'); + + expect(controller.state.eligibility).toStrictEqual(mockEligibility); + }, + ); + }); + + it('does not update eligibility when userRegion does not match the ISO code', async () => { + const existingEligibility = { + aggregator: false, + deposit: false, + global: false, + }; + + await withController( + { + options: { + state: { userRegion: 'us', eligibility: existingEligibility }, + }, + }, + async ({ controller, rootMessenger }) => { + const newEligibility = { + aggregator: true, + deposit: true, + global: true, + }; + + rootMessenger.registerActionHandler( + 'RampsService:getEligibility', + async (isoCode) => { + expect(isoCode).toBe('fr'); + return newEligibility; + }, + ); + + expect(controller.state.userRegion).toBe('us'); + expect(controller.state.eligibility).toStrictEqual( + existingEligibility, + ); + + await controller.updateEligibility('fr'); + + expect(controller.state.eligibility).toStrictEqual( + existingEligibility, + ); + }, + ); + }); + }); + + describe('init', () => { + it('initializes controller by fetching user region', async () => { + await withController(async ({ controller, rootMessenger }) => { + rootMessenger.registerActionHandler( + 'RampsService:getGeolocation', + async () => 'US', + ); + rootMessenger.registerActionHandler( + 'RampsService:getEligibility', + async () => ({ + aggregator: true, + deposit: true, + global: true, + }), + ); + + await controller.init(); + + expect(controller.state.userRegion).toBe('us'); + }); + }); + + it('handles initialization failure gracefully', async () => { + await withController(async ({ controller, rootMessenger }) => { + rootMessenger.registerActionHandler( + 'RampsService:getGeolocation', + async () => { + throw new Error('Network error'); + }, + ); + + await controller.init(); + + expect(controller.state.userRegion).toBeNull(); + }); + }); + }); + + describe('setUserRegion', () => { + it('sets user region manually and fetches eligibility', async () => { + await withController(async ({ controller, rootMessenger }) => { + rootMessenger.registerActionHandler( + 'RampsService:getEligibility', + async (isoCode) => { + expect(isoCode).toBe('us-ca'); + return { + aggregator: true, + deposit: true, + global: true, + }; + }, + ); + + await controller.setUserRegion('US-CA'); + + expect(controller.state.userRegion).toBe('us-ca'); + expect(controller.state.eligibility).toStrictEqual({ + aggregator: true, + deposit: true, + global: true, + }); + }); + }); + + it('updates user region state and clears eligibility when eligibility fetch fails', async () => { + await withController(async ({ controller, rootMessenger }) => { + rootMessenger.registerActionHandler( + 'RampsService:getEligibility', + async () => { + throw new Error('Eligibility API error'); + }, + ); + + expect(controller.state.userRegion).toBeNull(); + expect(controller.state.eligibility).toBeNull(); + + await expect(controller.setUserRegion('US-CA')).rejects.toThrow( + 'Eligibility API error', + ); + + expect(controller.state.userRegion).toBe('us-ca'); + expect(controller.state.eligibility).toBeNull(); + }); + }); + + it('clears stale eligibility when new user region is set but eligibility fails', async () => { + await withController(async ({ controller, rootMessenger }) => { + const usEligibility = { + aggregator: true, + deposit: true, + global: true, + }; + + rootMessenger.registerActionHandler( + 'RampsService:getEligibility', + async (isoCode) => { + if (isoCode === 'us') { + return usEligibility; + } + throw new Error('Eligibility API error'); + }, + ); + + await controller.setUserRegion('US'); + expect(controller.state.userRegion).toBe('us'); + expect(controller.state.eligibility).toStrictEqual(usEligibility); + + await expect(controller.setUserRegion('FR')).rejects.toThrow( + 'Eligibility API error', + ); + + expect(controller.state.userRegion).toBe('fr'); + expect(controller.state.eligibility).toBeNull(); + }); + }); }); - describe('updateGeolocation with automatic eligibility', () => { - it('automatically fetches eligibility after getting geolocation', async () => { + describe('updateUserRegion with automatic eligibility', () => { + it('automatically fetches eligibility after getting user region', async () => { await withController(async ({ controller, rootMessenger }) => { const mockEligibility = { aggregator: true, @@ -772,17 +989,17 @@ describe('RampsController', () => { }, ); - expect(controller.state.geolocation).toBeNull(); + expect(controller.state.userRegion).toBeNull(); expect(controller.state.eligibility).toBeNull(); - await controller.updateGeolocation(); + await controller.updateUserRegion(); - expect(controller.state.geolocation).toBe('fr'); + expect(controller.state.userRegion).toBe('fr'); expect(controller.state.eligibility).toStrictEqual(mockEligibility); }); }); - it('updates geolocation state even when eligibility fetch fails', async () => { + it('updates user region state even when eligibility fetch fails', async () => { await withController(async ({ controller, rootMessenger }) => { rootMessenger.registerActionHandler( 'RampsService:getGeolocation', @@ -795,17 +1012,17 @@ describe('RampsController', () => { }, ); - expect(controller.state.geolocation).toBeNull(); + expect(controller.state.userRegion).toBeNull(); expect(controller.state.eligibility).toBeNull(); - await controller.updateGeolocation(); + await controller.updateUserRegion(); - expect(controller.state.geolocation).toBe('us-ny'); + expect(controller.state.userRegion).toBe('us-ny'); expect(controller.state.eligibility).toBeNull(); }); }); - it('clears stale eligibility when new geolocation is fetched but eligibility fails', async () => { + it('clears stale eligibility when new user region is fetched but eligibility fails', async () => { await withController(async ({ controller, rootMessenger }) => { const usEligibility = { aggregator: true, @@ -834,14 +1051,14 @@ describe('RampsController', () => { }, ); - await controller.updateGeolocation(); + await controller.updateUserRegion(); - expect(controller.state.geolocation).toBe('us'); + expect(controller.state.userRegion).toBe('us'); expect(controller.state.eligibility).toStrictEqual(usEligibility); - await controller.updateGeolocation({ forceRefresh: true }); + await controller.updateUserRegion({ forceRefresh: true }); - expect(controller.state.geolocation).toBe('fr'); + expect(controller.state.userRegion).toBe('fr'); expect(controller.state.eligibility).toBeNull(); }); }); @@ -880,13 +1097,13 @@ describe('RampsController', () => { }, ); - const promise1 = controller.updateGeolocation(); + const promise1 = controller.updateUserRegion(); await new Promise((resolve) => setTimeout(resolve, 20)); - const promise2 = controller.updateGeolocation({ forceRefresh: true }); + const promise2 = controller.updateUserRegion({ forceRefresh: true }); await Promise.all([promise1, promise2]); - expect(controller.state.geolocation).toBe('fr'); + expect(controller.state.userRegion).toBe('fr'); expect(controller.state.eligibility).toStrictEqual(frEligibility); }); }); diff --git a/packages/ramps-controller/src/RampsController.ts b/packages/ramps-controller/src/RampsController.ts index 1e855443fe5..f13955daa73 100644 --- a/packages/ramps-controller/src/RampsController.ts +++ b/packages/ramps-controller/src/RampsController.ts @@ -45,9 +45,10 @@ export const controllerName = 'RampsController'; */ export type RampsControllerState = { /** - * The user's country code determined by geolocation. + * The user's selected region code (e.g., "US-CA"). + * Initially set via geolocation fetch, but can be manually changed by the user. */ - geolocation: string | null; + userRegion: string | null; /** * Eligibility information for the user's current region. */ @@ -63,7 +64,7 @@ export type RampsControllerState = { * The metadata for each property in {@link RampsControllerState}. */ const rampsControllerMetadata = { - geolocation: { + userRegion: { persist: true, includeInDebugSnapshot: true, includeInStateLogs: true, @@ -93,7 +94,7 @@ const rampsControllerMetadata = { */ export function getDefaultRampsControllerState(): RampsControllerState { return { - geolocation: null, + userRegion: null, eligibility: null, requests: {}, }; @@ -387,17 +388,17 @@ export class RampsController extends BaseController< } /** - * Updates the user's geolocation and eligibility. + * Updates the user's region by fetching geolocation and eligibility. * This method calls the RampsService to get the geolocation, * then automatically fetches eligibility for that region. * * @param options - Options for cache behavior. - * @returns The geolocation string. + * @returns The user region string. */ - async updateGeolocation(options?: ExecuteRequestOptions): Promise { - const cacheKey = createCacheKey('updateGeolocation', []); + async updateUserRegion(options?: ExecuteRequestOptions): Promise { + const cacheKey = createCacheKey('updateUserRegion', []); - const geolocation = await this.executeRequest( + const userRegion = await this.executeRequest( cacheKey, async () => { const result = await this.messenger.call('RampsService:getGeolocation'); @@ -406,24 +407,77 @@ export class RampsController extends BaseController< options, ); + const normalizedRegion = userRegion + ? userRegion.toLowerCase().trim() + : userRegion; + this.update((state) => { - state.geolocation = geolocation; + state.userRegion = normalizedRegion; }); - if (geolocation) { + if (normalizedRegion) { try { - await this.updateEligibility(geolocation, options); + await this.updateEligibility(normalizedRegion, options); } catch { - // Eligibility fetch failed, but geolocation was successfully fetched and cached. - // Don't let eligibility errors prevent geolocation state from being updated. - // Clear eligibility state to avoid showing stale data from a previous location. this.update((state) => { - state.eligibility = null; + const currentUserRegion = state.userRegion?.toLowerCase().trim(); + if (currentUserRegion === normalizedRegion) { + state.eligibility = null; + } }); } } - return geolocation; + return normalizedRegion; + } + + /** + * Sets the user's region manually (without fetching geolocation). + * This allows users to override the detected region. + * + * @param region - The region code to set (e.g., "US-CA"). + * @param options - Options for cache behavior when fetching eligibility. + * @returns The eligibility information for the region. + */ + async setUserRegion( + region: string, + options?: ExecuteRequestOptions, + ): Promise { + const normalizedRegion = region.toLowerCase().trim(); + + this.update((state) => { + state.userRegion = normalizedRegion; + }); + + try { + return await this.updateEligibility(normalizedRegion, options); + } catch (error) { + // Eligibility fetch failed, but user region was successfully set. + // Don't let eligibility errors prevent user region state from being updated. + // Clear eligibility state to avoid showing stale data from a previous location. + // Only clear if the region still matches to avoid race conditions where a newer + // region change has already succeeded. + this.update((state) => { + const currentUserRegion = state.userRegion?.toLowerCase().trim(); + if (currentUserRegion === normalizedRegion) { + state.eligibility = null; + } + }); + throw error; + } + } + + /** + * Initializes the controller by fetching the user's region from geolocation. + * This should be called once at app startup to set up the initial region. + * + * @param options - Options for cache behavior. + * @returns Promise that resolves when initialization is complete. + */ + async init(options?: ExecuteRequestOptions): Promise { + await this.updateUserRegion(options).catch(() => { + // User region fetch failed - error state will be available via selectors + }); } /** @@ -452,10 +506,9 @@ export class RampsController extends BaseController< ); this.update((state) => { - if ( - state.geolocation === null || - state.geolocation.toLowerCase().trim() === normalizedIsoCode - ) { + const userRegion = state.userRegion?.toLowerCase().trim(); + + if (userRegion === undefined || userRegion === normalizedIsoCode) { state.eligibility = eligibility; } }); diff --git a/packages/ramps-controller/src/RampsService.test.ts b/packages/ramps-controller/src/RampsService.test.ts index 007f644d9af..34113af2d0f 100644 --- a/packages/ramps-controller/src/RampsService.test.ts +++ b/packages/ramps-controller/src/RampsService.test.ts @@ -584,7 +584,6 @@ describe('RampsService', () => { expect(countriesResponse[0]?.supported).toBe(false); expect(countriesResponse[0]?.states?.[0]?.supported).toBe(true); }); - it('throws if the countries API returns an error', async () => { nock('https://on-ramp-cache.uat-api.cx.metamask.io') .get('/regions/countries') diff --git a/packages/ramps-controller/src/RequestCache.test.ts b/packages/ramps-controller/src/RequestCache.test.ts index 4752ca9fb12..2a8e4fc45b9 100644 --- a/packages/ramps-controller/src/RequestCache.test.ts +++ b/packages/ramps-controller/src/RequestCache.test.ts @@ -11,8 +11,8 @@ import { describe('RequestCache', () => { describe('createCacheKey', () => { it('creates a cache key from method and empty params', () => { - const key = createCacheKey('updateGeolocation', []); - expect(key).toBe('updateGeolocation:[]'); + const key = createCacheKey('updateUserRegion', []); + expect(key).toBe('updateUserRegion:[]'); }); it('creates a cache key from method and params', () => { diff --git a/packages/ramps-controller/src/selectors.test.ts b/packages/ramps-controller/src/selectors.test.ts index 034d6b79012..dfba6a4f472 100644 --- a/packages/ramps-controller/src/selectors.test.ts +++ b/packages/ramps-controller/src/selectors.test.ts @@ -24,7 +24,7 @@ describe('createRequestSelector', () => { const loadingRequest = createLoadingState(); const state: TestRootState = { ramps: { - geolocation: null, + userRegion: null, eligibility: null, requests: { 'getCryptoCurrencies:["US"]': loadingRequest, @@ -53,7 +53,7 @@ describe('createRequestSelector', () => { const successRequest = createSuccessState(['ETH', 'BTC'], Date.now()); const state: TestRootState = { ramps: { - geolocation: null, + userRegion: null, eligibility: null, requests: { 'getCryptoCurrencies:["US"]': successRequest, @@ -85,7 +85,7 @@ describe('createRequestSelector', () => { const errorRequest = createErrorState('Network error', Date.now()); const state: TestRootState = { ramps: { - geolocation: null, + userRegion: null, eligibility: null, requests: { 'getCryptoCurrencies:["US"]': errorRequest, @@ -113,7 +113,7 @@ describe('createRequestSelector', () => { const state: TestRootState = { ramps: { - geolocation: null, + userRegion: null, eligibility: null, requests: {}, }, @@ -164,7 +164,7 @@ describe('createRequestSelector', () => { const successRequest = createSuccessState(['ETH', 'BTC'], Date.now()); const state: TestRootState = { ramps: { - geolocation: null, + userRegion: null, eligibility: null, requests: { 'getCryptoCurrencies:["US"]': successRequest, @@ -188,7 +188,7 @@ describe('createRequestSelector', () => { const successRequest1 = createSuccessState(['ETH'], Date.now()); const state1: TestRootState = { ramps: { - geolocation: null, + userRegion: null, eligibility: null, requests: { 'getCryptoCurrencies:["US"]': successRequest1, @@ -201,7 +201,7 @@ describe('createRequestSelector', () => { const successRequest2 = createSuccessState(['ETH', 'BTC'], Date.now()); const state2: TestRootState = { ramps: { - geolocation: null, + userRegion: null, eligibility: null, requests: { 'getCryptoCurrencies:["US"]': successRequest2, @@ -226,7 +226,7 @@ describe('createRequestSelector', () => { const successRequest = createSuccessState(largeArray, Date.now()); const state: TestRootState = { ramps: { - geolocation: null, + userRegion: null, eligibility: null, requests: { 'getCryptoCurrencies:["US"]': successRequest, @@ -254,7 +254,7 @@ describe('createRequestSelector', () => { const successRequest = createSuccessState(complexData, Date.now()); const state: TestRootState = { ramps: { - geolocation: null, + userRegion: null, eligibility: null, requests: { 'getData:[]': successRequest, @@ -281,7 +281,7 @@ describe('createRequestSelector', () => { const loadingRequest = createLoadingState(); const loadingState: TestRootState = { ramps: { - geolocation: null, + userRegion: null, eligibility: null, requests: { 'getCryptoCurrencies:["US"]': loadingRequest, @@ -296,7 +296,7 @@ describe('createRequestSelector', () => { const successRequest = createSuccessState(['ETH'], Date.now()); const successState: TestRootState = { ramps: { - geolocation: null, + userRegion: null, eligibility: null, requests: { 'getCryptoCurrencies:["US"]': successRequest, @@ -319,7 +319,7 @@ describe('createRequestSelector', () => { const successRequest = createSuccessState(['ETH'], Date.now()); const successState: TestRootState = { ramps: { - geolocation: null, + userRegion: null, eligibility: null, requests: { 'getCryptoCurrencies:["US"]': successRequest, @@ -333,7 +333,7 @@ describe('createRequestSelector', () => { const errorRequest = createErrorState('Failed to fetch', Date.now()); const errorState: TestRootState = { ramps: { - geolocation: null, + userRegion: null, eligibility: null, requests: { 'getCryptoCurrencies:["US"]': errorRequest, @@ -362,7 +362,7 @@ describe('createRequestSelector', () => { const state: TestRootState = { ramps: { - geolocation: null, + userRegion: null, eligibility: null, requests: { 'getCryptoCurrencies:["US"]': createSuccessState( @@ -395,7 +395,7 @@ describe('createRequestSelector', () => { const state: TestRootState = { ramps: { - geolocation: null, + userRegion: null, eligibility: null, requests: { 'getCryptoCurrencies:["US"]': createSuccessState( diff --git a/packages/ramps-controller/src/selectors.ts b/packages/ramps-controller/src/selectors.ts index fe84be19e7e..a616f9bf625 100644 --- a/packages/ramps-controller/src/selectors.ts +++ b/packages/ramps-controller/src/selectors.ts @@ -31,7 +31,7 @@ export type RequestSelectorResult = { * * @param getState - Function that extracts RampsControllerState from the root state. * Typically a reselect selector like `selectRampsControllerState`. - * @param method - The controller method name (e.g., 'updateGeolocation'). + * @param method - The controller method name (e.g., 'updateUserRegion'). * @param params - The parameters passed to the method, used to generate the cache key. * Must match the params used when calling the controller method. * @returns A selector function that returns `{ data, isFetching, error }`. @@ -47,14 +47,14 @@ export type RequestSelectorResult = { * (rampsControllerState) => rampsControllerState, * ); * - * export const selectGeolocationRequest = createRequestSelector< + * export const selectUserRegionRequest = createRequestSelector< * RootState, * string - * >(selectRampsControllerState, 'updateGeolocation', []); + * >(selectRampsControllerState, 'updateUserRegion', []); * * // In hook - use directly with useSelector, no shallowEqual needed - * export function useRampsGeolocation() { - * const { isFetching, error } = useSelector(selectGeolocationRequest); + * export function useRampsUserRegion() { + * const { isFetching, error } = useSelector(selectUserRegionRequest); * // ... rest of hook * } * ```