Commit 03597ba
feat: Integrate Geolocation Controller (MetaMask#26730)
## **Description**
Register the new `@metamask/geolocation-controller` in the mobile Engine
and migrate all 4 existing geolocation consumers (Ramp, Perps, Rewards,
Card) to use the centralised controller instead of their individual
implementations.
### Problem
Four separate features independently fetch geolocation from the same
API, each with their own URL constants, caching logic, and error
handling:
- **Ramp** — `useDetectGeolocation` hook with `GEOLOCATION_URLS`
constant
- **Perps** — `EligibilityService.fetchGeoLocation()` with
`ON_RAMP_GEO_BLOCKING_URLS`, TTL caching, and promise deduplication
- **Rewards** — `RewardsDataService.fetchGeoLocation()` with
`GEOLOCATION_URLS` constant
- **Card** — `CardSDK.getGeoLocation()` with a hardcoded URL
This results in 2–4 redundant API calls on app load and duplicated code
across the codebase.
### Solution
The core `@metamask/geolocation-controller` package provides two
components:
- **`GeolocationApiService`** — handles HTTP fetch, TTL caching (5 min),
and promise deduplication
- **`GeolocationController`** — manages UI-facing state (`location`,
`status`, `lastFetchedAt`, `error`) and delegates to the ApiService via
messenger
This PR:
1. **Registers both components** in the Engine following the established
`RewardsController`/`RewardsDataService` pattern
2. **Eagerly fetches** geolocation on Engine start so the value is
available before any consumer needs it
3. **Migrates all 4 consumers** to read from the centralised controller:
- **Ramp**: Deleted `useDetectGeolocation` hook; redefined
`getDetectedGeolocation` selector to read from `GeolocationController`
state (preserves the 10+ downstream consumer files unchanged)
- **Perps**: `refreshEligibility()` calls
`GeolocationController:getGeolocation` via messenger and passes the
result to `checkEligibility()` as a parameter; removed all
geolocation-fetching code from `EligibilityService`
- **Rewards**: Replaced `RewardsDataService:fetchGeoLocation` messenger
call with `GeolocationController:getGeolocation` in
`RewardsController.getGeoRewardsMetadata()`; removed
`fetchGeoLocation()` method from `RewardsDataService`
- **Card**: Replaced `cardSDK.getGeoLocation()` with
`Engine.context.GeolocationController?.state?.location` in
`getCardholder.ts`; removed `getGeoLocation()` from `CardSDK`
4. **Removes all duplicate** `GEOLOCATION_URLS`,
`ON_RAMP_GEO_BLOCKING_URLS`, and hardcoded geolocation URL constants
### Files changed summary
**Created (6 files):**
| File | Purpose |
|------|---------|
| `app/core/Engine/controllers/geolocation-api-service-init.ts` | Init
function with environment-aware `SDK.Env` mapping |
| `app/core/Engine/messengers/geolocation-api-service-messenger.ts` |
Simple messenger factory (no delegated actions) |
| `app/core/Engine/controllers/geolocation-controller/index.ts` | Init
function with eager geolocation fetch |
| `app/core/Engine/messengers/geolocation-controller-messenger/index.ts`
| Messenger delegating `GeolocationApiService:fetchGeolocation` |
| `app/selectors/geolocationController/index.ts` | Redux selectors for
controller state |
| `app/core/Engine/controllers/geolocation-api-service-init.test.ts` |
Unit tests for env mapping |
**Created (test, 1 file):**
| File | Purpose |
|------|---------|
| `app/core/Engine/controllers/geolocation-controller/index.test.ts` |
Unit tests for init + eager fetch |
**Deleted (2 files):**
| File | Reason |
|------|--------|
| `app/components/UI/Ramp/hooks/useDetectGeolocation.ts` | Replaced by
centralised controller |
| `app/components/UI/Ramp/hooks/useDetectGeolocation.test.ts` | Tests
for deleted hook |
**Modified — Engine registration (5 files):**
- `package.json` — added `@metamask/geolocation-controller` dependency
- `app/core/Engine/Engine.ts` — registered both init functions
- `app/core/Engine/types.ts` — added to `Controllers`, `EngineState`,
`ControllersToInitialize`, `GlobalActions`, `GlobalEvents`
- `app/core/Engine/constants.ts` — added
`GeolocationController:stateChange`
- `app/core/Engine/messengers/index.ts` — registered both messenger
factories
**Modified — Consumer migration (9 files):**
- `app/components/UI/Ramp/index.tsx` — removed `useDetectGeolocation()`
call
- `app/reducers/fiatOrders/index.ts` — redefined
`getDetectedGeolocation` to read from controller
- `app/controllers/perps/PerpsController.ts` — calls
`GeolocationController:getGeolocation` via messenger
- `app/controllers/perps/services/EligibilityService.ts` — removed all
geolocation-fetching code
- `app/controllers/perps/types/index.ts` — added `geoLocation` to
`CheckEligibilityParams`
- `app/core/Engine/messengers/perps-controller-messenger/index.ts` —
delegated `GeolocationController:getGeolocation`
-
`app/core/Engine/controllers/rewards-controller/services/rewards-data-service.ts`
— removed `fetchGeoLocation()`, `GEOLOCATION_URLS`, action type
- `app/core/Engine/controllers/rewards-controller/services/index.ts` —
removed re-export
- `app/core/Engine/controllers/rewards-controller/RewardsController.ts`
— calls `GeolocationController:getGeolocation`
- `app/core/Engine/messengers/rewards-controller-messenger/index.ts` —
added/removed delegated actions
- `app/components/UI/Card/sdk/CardSDK.ts` — removed `getGeoLocation()`
method
- `app/components/UI/Card/util/getCardholder.ts` — reads from
`Engine.context`
**Modified — Tests (5 files):**
- `app/controllers/perps/services/EligibilityService.test.ts` — tests
`checkEligibility` with `geoLocation` param
- `app/controllers/perps/PerpsController.test.ts` — updated mock
-
`app/core/Engine/controllers/rewards-controller/services/rewards-data-service.test.ts`
— removed `fetchGeoLocation` tests
-
`app/core/Engine/controllers/rewards-controller/RewardsController.test.ts`
— updated messenger call references
- `app/components/UI/Card/sdk/CardSDK.test.ts` — removed
`getGeoLocation` tests
- `app/components/UI/Card/util/getCardholder.test.ts` — mocks
`Engine.context` instead of `cardSDK.getGeoLocation()`
- `app/reducers/fiatOrders/index.test.ts` — updated
`getDetectedGeolocation` tests
## **Changelog**
CHANGELOG entry: null
## **Related issues**
Refs: https://consensyssoftware.atlassian.net/browse/MCWP-350
## **Manual testing steps**
```gherkin
Feature: Centralised geolocation via GeolocationController
Scenario: Only one geolocation API call is made on app load
Given the app is freshly launched
When the Engine initialises
Then exactly 1 request to on-ramp.api.cx.metamask.io/geolocation is made
And no duplicate geolocation requests are made by Ramp, Perps, Rewards, or Card
Scenario: Ramp region detection uses controller-sourced geolocation
Given the user opens the Buy/Sell flow
When the Ramp UI loads
Then the detected region matches the user's actual location
And no separate geolocation fetch is triggered by the Ramp hook
Scenario: Perps eligibility check uses controller-sourced geolocation
Given the user navigates to the Perps feature
When eligibility is checked
Then the eligibility result is correct for the user's region
And EligibilityService does not make its own geolocation API call
Scenario: Rewards geo-blocking uses controller-sourced geolocation
Given the user accesses the Rewards feature
When getGeoRewardsMetadata is called
Then geo-blocking works correctly (UK/GB/GI users are blocked from opt-in)
And RewardsDataService does not make its own geolocation API call
Scenario: Card country detection uses controller-sourced geolocation
Given the user accesses the Card feature
When cardholder accounts are loaded
Then the correct country code is returned
And CardSDK does not make its own geolocation API call
Scenario: Geolocation re-fetches after TTL expires
Given geolocation was fetched more than 5 minutes ago
When a consumer requests geolocation
Then a fresh API call is made
And the cached value is updated
```
## **Screenshots/Recordings**
### **Before**
N/A — no UI changes, backend/controller wiring only.
### **After**
https://github.com/user-attachments/assets/793c67b5-fbbd-48b9-a8e3-4e4924257c0b
N/A — no UI changes, backend/controller wiring only.
## **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**
- [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 Engine initialization and multiple feature flows
(Ramp/Perps/Rewards/Card) by changing the geolocation source and state
shape, so regressions could impact region-gating and routing decisions.
Logic is mostly wiring/migration, but it changes when/where geolocation
is fetched (eager on Engine start).
>
> **Overview**
> Integrates `@metamask/geolocation-controller` into the mobile Engine
by registering `GeolocationApiService` and `GeolocationController`,
adding messengers/types/background-state plumbing, and **eagerly
fetching** geolocation on Engine startup.
>
> Migrates geolocation consumers to the centralized controller: Ramp
drops `useDetectGeolocation` and `fiatOrders.detectedGeolocation`
(selector now reads
`engine.backgroundState.GeolocationController.location`), Perps passes
controller-provided `geoLocation` into
`EligibilityService.checkEligibility`, Rewards switches
`RewardsController` to call `GeolocationController:getGeolocation` and
removes `RewardsDataService.fetchGeoLocation`, and Card removes
`CardSDK.getGeoLocation` in favor of
`Engine.controllerMessenger.call('GeolocationController:getGeolocation')`
in `getCardholder`.
>
> Updates unit/E2E tests, fixtures, and API mocks to the new state
location and ISO 3166-2 location code expectations.
>
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
f9d0630. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
---------
Co-authored-by: metamaskbot <metamaskbot@users.noreply.github.com>1 parent 44f7fd2 commit 03597ba
53 files changed
Lines changed: 624 additions & 1173 deletions
File tree
- app
- components
- UI
- Card
- sdk
- util
- Earn/components/Musd/MusdConversionAssetListCta
- Ramp
- Views/TokenSelection
- hooks
- Trending/hooks/useRwaTokens
- Views/AccountsMenu
- controllers/perps
- services
- types
- core/Engine
- controllers
- geolocation-controller
- rewards-controller
- services
- messengers
- geolocation-controller-messenger
- perps-controller-messenger
- rewards-controller-messenger
- reducers/fiatOrders
- selectors/geolocationController
- util
- logs/__snapshots__
- test
- tests
- api-mocking/mock-responses
- ramps/responses
- framework
- fixtures
- json
- smoke/ramps
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
624 | 624 | | |
625 | 625 | | |
626 | 626 | | |
627 | | - | |
628 | | - | |
629 | | - | |
630 | | - | |
631 | | - | |
632 | | - | |
633 | | - | |
634 | | - | |
635 | | - | |
636 | | - | |
637 | | - | |
638 | | - | |
639 | | - | |
640 | | - | |
641 | | - | |
642 | | - | |
643 | | - | |
644 | | - | |
645 | | - | |
646 | | - | |
647 | | - | |
648 | | - | |
649 | | - | |
650 | | - | |
651 | | - | |
652 | | - | |
653 | | - | |
654 | | - | |
655 | | - | |
656 | | - | |
657 | | - | |
658 | | - | |
659 | | - | |
660 | | - | |
661 | | - | |
662 | | - | |
663 | | - | |
664 | | - | |
665 | | - | |
666 | | - | |
667 | | - | |
668 | | - | |
669 | | - | |
670 | | - | |
671 | | - | |
672 | | - | |
673 | | - | |
674 | | - | |
675 | | - | |
676 | | - | |
677 | | - | |
678 | | - | |
679 | | - | |
680 | | - | |
681 | | - | |
682 | | - | |
683 | | - | |
684 | | - | |
685 | | - | |
686 | | - | |
687 | | - | |
688 | | - | |
689 | | - | |
690 | | - | |
691 | | - | |
692 | | - | |
693 | | - | |
694 | | - | |
695 | | - | |
696 | | - | |
697 | | - | |
698 | | - | |
699 | | - | |
700 | | - | |
701 | | - | |
702 | | - | |
703 | | - | |
704 | | - | |
705 | | - | |
706 | | - | |
707 | | - | |
708 | | - | |
709 | | - | |
710 | | - | |
711 | 627 | | |
712 | 628 | | |
713 | 629 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
465 | 465 | | |
466 | 466 | | |
467 | 467 | | |
468 | | - | |
469 | | - | |
470 | | - | |
471 | | - | |
472 | | - | |
473 | | - | |
474 | | - | |
475 | | - | |
476 | | - | |
477 | | - | |
478 | | - | |
479 | | - | |
480 | | - | |
481 | | - | |
482 | | - | |
483 | | - | |
484 | | - | |
485 | | - | |
486 | | - | |
487 | | - | |
488 | | - | |
489 | | - | |
490 | | - | |
491 | 468 | | |
492 | 469 | | |
493 | 470 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
40 | 40 | | |
41 | 41 | | |
42 | 42 | | |
43 | | - | |
44 | 43 | | |
45 | 44 | | |
46 | 45 | | |
| |||
171 | 170 | | |
172 | 171 | | |
173 | 172 | | |
174 | | - | |
175 | 173 | | |
176 | 174 | | |
177 | 175 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
7 | | - | |
8 | 7 | | |
9 | 8 | | |
10 | 9 | | |
11 | 10 | | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
12 | 18 | | |
13 | 19 | | |
14 | 20 | | |
| |||
54 | 60 | | |
55 | 61 | | |
56 | 62 | | |
57 | | - | |
58 | 63 | | |
59 | 64 | | |
60 | 65 | | |
61 | 66 | | |
62 | | - | |
63 | 67 | | |
64 | 68 | | |
65 | | - | |
66 | | - | |
| 69 | + | |
67 | 70 | | |
68 | 71 | | |
69 | 72 | | |
| |||
74 | 77 | | |
75 | 78 | | |
76 | 79 | | |
77 | | - | |
78 | 80 | | |
79 | 81 | | |
80 | 82 | | |
| |||
94 | 96 | | |
95 | 97 | | |
96 | 98 | | |
97 | | - | |
98 | 99 | | |
99 | 100 | | |
100 | 101 | | |
101 | 102 | | |
102 | 103 | | |
103 | 104 | | |
104 | 105 | | |
| 106 | + | |
105 | 107 | | |
106 | | - | |
107 | 108 | | |
108 | 109 | | |
109 | 110 | | |
| |||
117 | 118 | | |
118 | 119 | | |
119 | 120 | | |
| 121 | + | |
120 | 122 | | |
121 | | - | |
122 | 123 | | |
123 | 124 | | |
124 | 125 | | |
| |||
310 | 311 | | |
311 | 312 | | |
312 | 313 | | |
| 314 | + | |
313 | 315 | | |
314 | | - | |
315 | 316 | | |
316 | 317 | | |
317 | 318 | | |
| |||
335 | 336 | | |
336 | 337 | | |
337 | 338 | | |
| 339 | + | |
338 | 340 | | |
339 | | - | |
340 | 341 | | |
341 | 342 | | |
342 | 343 | | |
| |||
356 | 357 | | |
357 | 358 | | |
358 | 359 | | |
| 360 | + | |
359 | 361 | | |
360 | | - | |
361 | 362 | | |
362 | 363 | | |
363 | 364 | | |
| |||
379 | 380 | | |
380 | 381 | | |
381 | 382 | | |
382 | | - | |
383 | | - | |
384 | | - | |
385 | | - | |
386 | | - | |
387 | | - | |
388 | | - | |
389 | | - | |
390 | | - | |
| 383 | + | |
| 384 | + | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
391 | 389 | | |
392 | | - | |
393 | | - | |
394 | | - | |
395 | | - | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
396 | 394 | | |
397 | | - | |
398 | | - | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
399 | 399 | | |
400 | 400 | | |
401 | | - | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
402 | 405 | | |
403 | | - | |
404 | 406 | | |
405 | | - | |
| 407 | + | |
406 | 408 | | |
407 | 409 | | |
408 | 410 | | |
409 | 411 | | |
410 | | - | |
| 412 | + | |
411 | 413 | | |
412 | 414 | | |
413 | 415 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3 | 3 | | |
4 | 4 | | |
5 | 5 | | |
| 6 | + | |
6 | 7 | | |
7 | 8 | | |
8 | 9 | | |
| |||
26 | 27 | | |
27 | 28 | | |
28 | 29 | | |
29 | | - | |
30 | | - | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
31 | 36 | | |
32 | 37 | | |
33 | 38 | | |
| |||
Lines changed: 0 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
10 | 10 | | |
11 | 11 | | |
12 | 12 | | |
13 | | - | |
14 | 13 | | |
15 | 14 | | |
16 | 15 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
5 | | - | |
6 | 5 | | |
7 | 6 | | |
8 | 7 | | |
9 | | - | |
10 | | - | |
11 | | - | |
12 | | - | |
13 | | - | |
14 | 8 | | |
15 | 9 | | |
16 | 10 | | |
| |||
26 | 20 | | |
27 | 21 | | |
28 | 22 | | |
29 | | - | |
30 | | - | |
31 | | - | |
32 | | - | |
33 | | - | |
34 | | - | |
35 | 23 | | |
36 | 24 | | |
37 | 25 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | | - | |
2 | 1 | | |
3 | 2 | | |
4 | 3 | | |
5 | 4 | | |
6 | | - | |
7 | | - | |
8 | | - | |
9 | | - | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
10 | 11 | | |
11 | 12 | | |
12 | 13 | | |
13 | 14 | | |
14 | 15 | | |
15 | | - | |
16 | 16 | | |
17 | 17 | | |
18 | 18 | | |
| |||
Lines changed: 0 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
48 | 48 | | |
49 | 49 | | |
50 | 50 | | |
51 | | - | |
52 | 51 | | |
53 | 52 | | |
54 | 53 | | |
| |||
0 commit comments