@metamask-previews/ramps-controller 2.1.0-preview-8a4f4ea3 → 2.1.0-preview-f5ec6061

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  ### Changed
11
11
 
12
+ - **BREAKING:** Rename `geolocation` to `userRegion` and `updateGeolocation()` to `updateUserRegion()` in RampsController ([#7563](https://github.com/MetaMask/core/pull/7563))
13
+
12
14
  - Bump `@metamask/controller-utils` from `^11.17.0` to `^11.18.0` ([#7583](https://github.com/MetaMask/core/pull/7583))
13
15
 
14
16
  ## [2.1.0]
@@ -21,6 +23,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
21
23
 
22
24
  - Add request caching infrastructure with TTL, deduplication, and abort support ([#7536](https://github.com/MetaMask/core/pull/7536))
23
25
 
26
+ - Add `init()` and `setUserRegion()` methods to RampsController ([#7563](https://github.com/MetaMask/core/pull/7563))
27
+
24
28
  ### Changed
25
29
 
26
30
  - Bump `@metamask/controller-utils` from `^11.16.0` to `^11.17.0` ([#7534](https://github.com/MetaMask/core/pull/7534))
@@ -26,7 +26,7 @@ exports.controllerName = 'RampsController';
26
26
  * The metadata for each property in {@link RampsControllerState}.
27
27
  */
28
28
  const rampsControllerMetadata = {
29
- geolocation: {
29
+ userRegion: {
30
30
  persist: true,
31
31
  includeInDebugSnapshot: true,
32
32
  includeInStateLogs: true,
@@ -55,7 +55,7 @@ const rampsControllerMetadata = {
55
55
  */
56
56
  function getDefaultRampsControllerState() {
57
57
  return {
58
- geolocation: null,
58
+ userRegion: null,
59
59
  eligibility: null,
60
60
  requests: {},
61
61
  };
@@ -194,36 +194,82 @@ class RampsController extends base_controller_1.BaseController {
194
194
  return this.state.requests[cacheKey];
195
195
  }
196
196
  /**
197
- * Updates the user's geolocation and eligibility.
197
+ * Updates the user's region by fetching geolocation and eligibility.
198
198
  * This method calls the RampsService to get the geolocation,
199
199
  * then automatically fetches eligibility for that region.
200
200
  *
201
201
  * @param options - Options for cache behavior.
202
- * @returns The geolocation string.
202
+ * @returns The user region string.
203
203
  */
204
- async updateGeolocation(options) {
205
- const cacheKey = (0, RequestCache_1.createCacheKey)('updateGeolocation', []);
206
- const geolocation = await this.executeRequest(cacheKey, async () => {
204
+ async updateUserRegion(options) {
205
+ const cacheKey = (0, RequestCache_1.createCacheKey)('updateUserRegion', []);
206
+ const userRegion = await this.executeRequest(cacheKey, async () => {
207
207
  const result = await this.messenger.call('RampsService:getGeolocation');
208
208
  return result;
209
209
  }, options);
210
+ const normalizedRegion = userRegion
211
+ ? userRegion.toLowerCase().trim()
212
+ : userRegion;
210
213
  this.update((state) => {
211
- state.geolocation = geolocation;
214
+ state.userRegion = normalizedRegion;
212
215
  });
213
- if (geolocation) {
216
+ if (normalizedRegion) {
214
217
  try {
215
- await this.updateEligibility(geolocation, options);
218
+ await this.updateEligibility(normalizedRegion, options);
216
219
  }
217
220
  catch {
218
- // Eligibility fetch failed, but geolocation was successfully fetched and cached.
219
- // Don't let eligibility errors prevent geolocation state from being updated.
220
- // Clear eligibility state to avoid showing stale data from a previous location.
221
221
  this.update((state) => {
222
- state.eligibility = null;
222
+ const currentUserRegion = state.userRegion?.toLowerCase().trim();
223
+ if (currentUserRegion === normalizedRegion) {
224
+ state.eligibility = null;
225
+ }
223
226
  });
224
227
  }
225
228
  }
226
- return geolocation;
229
+ return normalizedRegion;
230
+ }
231
+ /**
232
+ * Sets the user's region manually (without fetching geolocation).
233
+ * This allows users to override the detected region.
234
+ *
235
+ * @param region - The region code to set (e.g., "US-CA").
236
+ * @param options - Options for cache behavior when fetching eligibility.
237
+ * @returns The eligibility information for the region.
238
+ */
239
+ async setUserRegion(region, options) {
240
+ const normalizedRegion = region.toLowerCase().trim();
241
+ this.update((state) => {
242
+ state.userRegion = normalizedRegion;
243
+ });
244
+ try {
245
+ return await this.updateEligibility(normalizedRegion, options);
246
+ }
247
+ catch (error) {
248
+ // Eligibility fetch failed, but user region was successfully set.
249
+ // Don't let eligibility errors prevent user region state from being updated.
250
+ // Clear eligibility state to avoid showing stale data from a previous location.
251
+ // Only clear if the region still matches to avoid race conditions where a newer
252
+ // region change has already succeeded.
253
+ this.update((state) => {
254
+ const currentUserRegion = state.userRegion?.toLowerCase().trim();
255
+ if (currentUserRegion === normalizedRegion) {
256
+ state.eligibility = null;
257
+ }
258
+ });
259
+ throw error;
260
+ }
261
+ }
262
+ /**
263
+ * Initializes the controller by fetching the user's region from geolocation.
264
+ * This should be called once at app startup to set up the initial region.
265
+ *
266
+ * @param options - Options for cache behavior.
267
+ * @returns Promise that resolves when initialization is complete.
268
+ */
269
+ async init(options) {
270
+ await this.updateUserRegion(options).catch(() => {
271
+ // User region fetch failed - error state will be available via selectors
272
+ });
227
273
  }
228
274
  /**
229
275
  * Updates the eligibility information for a given region.
@@ -239,8 +285,8 @@ class RampsController extends base_controller_1.BaseController {
239
285
  return this.messenger.call('RampsService:getEligibility', normalizedIsoCode);
240
286
  }, options);
241
287
  this.update((state) => {
242
- if (state.geolocation === null ||
243
- state.geolocation.toLowerCase().trim() === normalizedIsoCode) {
288
+ const userRegion = state.userRegion?.toLowerCase().trim();
289
+ if (userRegion === undefined || userRegion === normalizedIsoCode) {
244
290
  state.eligibility = eligibility;
245
291
  }
246
292
  });
@@ -1 +1 @@
1
- {"version":3,"file":"RampsController.cjs","sourceRoot":"","sources":["../src/RampsController.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n ControllerGetStateAction,\n ControllerStateChangeEvent,\n StateMetadata,\n} from '@metamask/base-controller';\nimport { BaseController } from '@metamask/base-controller';\nimport type { Messenger } from '@metamask/messenger';\nimport type { Json } from '@metamask/utils';\n\nimport type { Country, Eligibility } from './RampsService';\nimport type {\n RampsServiceGetGeolocationAction,\n RampsServiceGetCountriesAction,\n RampsServiceGetEligibilityAction,\n} from './RampsService-method-action-types';\nimport type {\n RequestCache as RequestCacheType,\n RequestState,\n ExecuteRequestOptions,\n PendingRequest,\n} from './RequestCache';\nimport {\n DEFAULT_REQUEST_CACHE_TTL,\n DEFAULT_REQUEST_CACHE_MAX_SIZE,\n createCacheKey,\n isCacheExpired,\n createLoadingState,\n createSuccessState,\n createErrorState,\n} from './RequestCache';\n\n// === GENERAL ===\n\n/**\n * The name of the {@link RampsController}, used to namespace the\n * controller's actions and events and to namespace the controller's state data\n * when composed with other controllers.\n */\nexport const controllerName = 'RampsController';\n\n// === STATE ===\n\n/**\n * Describes the shape of the state object for {@link RampsController}.\n */\nexport type RampsControllerState = {\n /**\n * The user's country code determined by geolocation.\n */\n geolocation: string | null;\n /**\n * Eligibility information for the user's current region.\n */\n eligibility: Eligibility | null;\n /**\n * Cache of request states, keyed by cache key.\n * This stores loading, success, and error states for API requests.\n */\n requests: RequestCacheType;\n};\n\n/**\n * The metadata for each property in {@link RampsControllerState}.\n */\nconst rampsControllerMetadata = {\n geolocation: {\n persist: true,\n includeInDebugSnapshot: true,\n includeInStateLogs: true,\n usedInUi: true,\n },\n eligibility: {\n persist: true,\n includeInDebugSnapshot: true,\n includeInStateLogs: true,\n usedInUi: true,\n },\n requests: {\n persist: false,\n includeInDebugSnapshot: true,\n includeInStateLogs: false,\n usedInUi: true,\n },\n} satisfies StateMetadata<RampsControllerState>;\n\n/**\n * Constructs the default {@link RampsController} state. This allows\n * consumers to provide a partial state object when initializing the controller\n * and also helps in constructing complete state objects for this controller in\n * tests.\n *\n * @returns The default {@link RampsController} state.\n */\nexport function getDefaultRampsControllerState(): RampsControllerState {\n return {\n geolocation: null,\n eligibility: null,\n requests: {},\n };\n}\n\n// === MESSENGER ===\n\n/**\n * Retrieves the state of the {@link RampsController}.\n */\nexport type RampsControllerGetStateAction = ControllerGetStateAction<\n typeof controllerName,\n RampsControllerState\n>;\n\n/**\n * Actions that {@link RampsControllerMessenger} exposes to other consumers.\n */\nexport type RampsControllerActions = RampsControllerGetStateAction;\n\n/**\n * Actions from other messengers that {@link RampsController} calls.\n */\ntype AllowedActions =\n | RampsServiceGetGeolocationAction\n | RampsServiceGetCountriesAction\n | RampsServiceGetEligibilityAction;\n\n/**\n * Published when the state of {@link RampsController} changes.\n */\nexport type RampsControllerStateChangeEvent = ControllerStateChangeEvent<\n typeof controllerName,\n RampsControllerState\n>;\n\n/**\n * Events that {@link RampsControllerMessenger} exposes to other consumers.\n */\nexport type RampsControllerEvents = RampsControllerStateChangeEvent;\n\n/**\n * Events from other messengers that {@link RampsController} subscribes to.\n */\ntype AllowedEvents = never;\n\n/**\n * The messenger restricted to actions and events accessed by\n * {@link RampsController}.\n */\nexport type RampsControllerMessenger = Messenger<\n typeof controllerName,\n RampsControllerActions | AllowedActions,\n RampsControllerEvents | AllowedEvents\n>;\n\n/**\n * Configuration options for the RampsController.\n */\nexport type RampsControllerOptions = {\n /** The messenger suited for this controller. */\n messenger: RampsControllerMessenger;\n /** The desired state with which to initialize this controller. */\n state?: Partial<RampsControllerState>;\n /** Time to live for cached requests in milliseconds. Defaults to 15 minutes. */\n requestCacheTTL?: number;\n /** Maximum number of entries in the request cache. Defaults to 250. */\n requestCacheMaxSize?: number;\n};\n\n// === CONTROLLER DEFINITION ===\n\n/**\n * Manages cryptocurrency on/off ramps functionality.\n */\nexport class RampsController extends BaseController<\n typeof controllerName,\n RampsControllerState,\n RampsControllerMessenger\n> {\n /**\n * Default TTL for cached requests.\n */\n readonly #requestCacheTTL: number;\n\n /**\n * Maximum number of entries in the request cache.\n */\n readonly #requestCacheMaxSize: number;\n\n /**\n * Map of pending requests for deduplication.\n * Key is the cache key, value is the pending request with abort controller.\n */\n readonly #pendingRequests: Map<string, PendingRequest> = new Map();\n\n /**\n * Constructs a new {@link RampsController}.\n *\n * @param args - The constructor arguments.\n * @param args.messenger - The messenger suited for this controller.\n * @param args.state - The desired state with which to initialize this\n * controller. Missing properties will be filled in with defaults.\n * @param args.requestCacheTTL - Time to live for cached requests in milliseconds.\n * @param args.requestCacheMaxSize - Maximum number of entries in the request cache.\n */\n constructor({\n messenger,\n state = {},\n requestCacheTTL = DEFAULT_REQUEST_CACHE_TTL,\n requestCacheMaxSize = DEFAULT_REQUEST_CACHE_MAX_SIZE,\n }: RampsControllerOptions) {\n super({\n messenger,\n metadata: rampsControllerMetadata,\n name: controllerName,\n state: {\n ...getDefaultRampsControllerState(),\n ...state,\n // Always reset requests cache on initialization (non-persisted)\n requests: {},\n },\n });\n\n this.#requestCacheTTL = requestCacheTTL;\n this.#requestCacheMaxSize = requestCacheMaxSize;\n }\n\n /**\n * Executes a request with caching and deduplication.\n *\n * If a request with the same cache key is already in flight, returns the\n * existing promise. If valid cached data exists, returns it without making\n * a new request.\n *\n * @param cacheKey - Unique identifier for this request.\n * @param fetcher - Function that performs the actual fetch. Receives an AbortSignal.\n * @param options - Options for cache behavior.\n * @returns The result of the request.\n */\n async executeRequest<TResult>(\n cacheKey: string,\n fetcher: (signal: AbortSignal) => Promise<TResult>,\n options?: ExecuteRequestOptions,\n ): Promise<TResult> {\n const ttl = options?.ttl ?? this.#requestCacheTTL;\n\n // Check for existing pending request - join it instead of making a duplicate\n const pending = this.#pendingRequests.get(cacheKey);\n if (pending) {\n return pending.promise as Promise<TResult>;\n }\n\n // Check cache validity (unless force refresh)\n if (!options?.forceRefresh) {\n const cached = this.state.requests[cacheKey];\n if (cached && !isCacheExpired(cached, ttl)) {\n return cached.data as TResult;\n }\n }\n\n // Create abort controller for this request\n const abortController = new AbortController();\n const lastFetchedAt = Date.now();\n\n // Update state to loading\n this.#updateRequestState(cacheKey, createLoadingState());\n\n // Create the fetch promise\n const promise = (async (): Promise<TResult> => {\n try {\n const data = await fetcher(abortController.signal);\n\n // Don't update state if aborted\n if (abortController.signal.aborted) {\n throw new Error('Request was aborted');\n }\n\n this.#updateRequestState(\n cacheKey,\n createSuccessState(data as Json, lastFetchedAt),\n );\n return data;\n } catch (error) {\n // Don't update state if aborted\n if (abortController.signal.aborted) {\n throw error;\n }\n\n const errorMessage = (error as Error)?.message;\n\n this.#updateRequestState(\n cacheKey,\n createErrorState(errorMessage ?? 'Unknown error', lastFetchedAt),\n );\n throw error;\n } finally {\n // Only delete if this is still our entry (not replaced by a new request)\n const currentPending = this.#pendingRequests.get(cacheKey);\n if (currentPending?.abortController === abortController) {\n this.#pendingRequests.delete(cacheKey);\n }\n }\n })();\n\n // Store pending request for deduplication\n this.#pendingRequests.set(cacheKey, { promise, abortController });\n\n return promise;\n }\n\n /**\n * Aborts a pending request if one exists.\n *\n * @param cacheKey - The cache key of the request to abort.\n * @returns True if a request was aborted.\n */\n abortRequest(cacheKey: string): boolean {\n const pending = this.#pendingRequests.get(cacheKey);\n if (pending) {\n pending.abortController.abort();\n this.#pendingRequests.delete(cacheKey);\n this.#removeRequestState(cacheKey);\n return true;\n }\n return false;\n }\n\n /**\n * Removes a request state from the cache.\n *\n * @param cacheKey - The cache key to remove.\n */\n #removeRequestState(cacheKey: string): void {\n this.update((state) => {\n const requests = state.requests as unknown as Record<\n string,\n RequestState | undefined\n >;\n delete requests[cacheKey];\n });\n }\n\n /**\n * Gets the state of a specific cached request.\n *\n * @param cacheKey - The cache key to look up.\n * @returns The request state, or undefined if not cached.\n */\n getRequestState(cacheKey: string): RequestState | undefined {\n return this.state.requests[cacheKey];\n }\n\n /**\n * Updates the state for a specific request.\n *\n * @param cacheKey - The cache key.\n * @param requestState - The new state for the request.\n */\n #updateRequestState(cacheKey: string, requestState: RequestState): void {\n const maxSize = this.#requestCacheMaxSize;\n\n this.update((state) => {\n const requests = state.requests as unknown as Record<\n string,\n RequestState | undefined\n >;\n requests[cacheKey] = requestState;\n\n // Evict oldest entries if cache exceeds max size\n const keys = Object.keys(requests);\n\n if (keys.length > maxSize) {\n // Sort by timestamp (oldest first)\n const sortedKeys = keys.sort((a, b) => {\n const aTime = requests[a]?.timestamp ?? 0;\n const bTime = requests[b]?.timestamp ?? 0;\n return aTime - bTime;\n });\n\n // Remove oldest entries until we're under the limit\n const entriesToRemove = keys.length - maxSize;\n for (let i = 0; i < entriesToRemove; i++) {\n const keyToRemove = sortedKeys[i];\n if (keyToRemove) {\n delete requests[keyToRemove];\n }\n }\n }\n });\n }\n\n /**\n * Updates the user's geolocation and eligibility.\n * This method calls the RampsService to get the geolocation,\n * then automatically fetches eligibility for that region.\n *\n * @param options - Options for cache behavior.\n * @returns The geolocation string.\n */\n async updateGeolocation(options?: ExecuteRequestOptions): Promise<string> {\n const cacheKey = createCacheKey('updateGeolocation', []);\n\n const geolocation = await this.executeRequest(\n cacheKey,\n async () => {\n const result = await this.messenger.call('RampsService:getGeolocation');\n return result;\n },\n options,\n );\n\n this.update((state) => {\n state.geolocation = geolocation;\n });\n\n if (geolocation) {\n try {\n await this.updateEligibility(geolocation, options);\n } catch {\n // Eligibility fetch failed, but geolocation was successfully fetched and cached.\n // Don't let eligibility errors prevent geolocation state from being updated.\n // Clear eligibility state to avoid showing stale data from a previous location.\n this.update((state) => {\n state.eligibility = null;\n });\n }\n }\n\n return geolocation;\n }\n\n /**\n * Updates the eligibility information for a given region.\n *\n * @param isoCode - The ISO code for the region (e.g., \"us\", \"fr\", \"us-ny\").\n * @param options - Options for cache behavior.\n * @returns The eligibility information.\n */\n async updateEligibility(\n isoCode: string,\n options?: ExecuteRequestOptions,\n ): Promise<Eligibility> {\n const normalizedIsoCode = isoCode.toLowerCase().trim();\n const cacheKey = createCacheKey('updateEligibility', [normalizedIsoCode]);\n\n const eligibility = await this.executeRequest(\n cacheKey,\n async () => {\n return this.messenger.call(\n 'RampsService:getEligibility',\n normalizedIsoCode,\n );\n },\n options,\n );\n\n this.update((state) => {\n if (\n state.geolocation === null ||\n state.geolocation.toLowerCase().trim() === normalizedIsoCode\n ) {\n state.eligibility = eligibility;\n }\n });\n\n return eligibility;\n }\n\n /**\n * Fetches the list of supported countries for a given ramp action.\n *\n * @param action - The ramp action type ('buy' or 'sell').\n * @param options - Options for cache behavior.\n * @returns An array of countries with their eligibility information.\n */\n async getCountries(\n action: 'buy' | 'sell' = 'buy',\n options?: ExecuteRequestOptions,\n ): Promise<Country[]> {\n const cacheKey = createCacheKey('getCountries', [action]);\n\n return this.executeRequest(\n cacheKey,\n async () => {\n return this.messenger.call('RampsService:getCountries', action);\n },\n options,\n );\n }\n}\n"]}
1
+ {"version":3,"file":"RampsController.cjs","sourceRoot":"","sources":["../src/RampsController.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAKA,+DAA2D;AAgB3D,qDAQwB;AAExB,kBAAkB;AAElB;;;;GAIG;AACU,QAAA,cAAc,GAAG,iBAAiB,CAAC;AAwBhD;;GAEG;AACH,MAAM,uBAAuB,GAAG;IAC9B,UAAU,EAAE;QACV,OAAO,EAAE,IAAI;QACb,sBAAsB,EAAE,IAAI;QAC5B,kBAAkB,EAAE,IAAI;QACxB,QAAQ,EAAE,IAAI;KACf;IACD,WAAW,EAAE;QACX,OAAO,EAAE,IAAI;QACb,sBAAsB,EAAE,IAAI;QAC5B,kBAAkB,EAAE,IAAI;QACxB,QAAQ,EAAE,IAAI;KACf;IACD,QAAQ,EAAE;QACR,OAAO,EAAE,KAAK;QACd,sBAAsB,EAAE,IAAI;QAC5B,kBAAkB,EAAE,KAAK;QACzB,QAAQ,EAAE,IAAI;KACf;CAC4C,CAAC;AAEhD;;;;;;;GAOG;AACH,SAAgB,8BAA8B;IAC5C,OAAO;QACL,UAAU,EAAE,IAAI;QAChB,WAAW,EAAE,IAAI;QACjB,QAAQ,EAAE,EAAE;KACb,CAAC;AACJ,CAAC;AAND,wEAMC;AAmED,gCAAgC;AAEhC;;GAEG;AACH,MAAa,eAAgB,SAAQ,gCAIpC;IAiBC;;;;;;;;;OASG;IACH,YAAY,EACV,SAAS,EACT,KAAK,GAAG,EAAE,EACV,eAAe,GAAG,wCAAyB,EAC3C,mBAAmB,GAAG,6CAA8B,GAC7B;QACvB,KAAK,CAAC;YACJ,SAAS;YACT,QAAQ,EAAE,uBAAuB;YACjC,IAAI,EAAE,sBAAc;YACpB,KAAK,EAAE;gBACL,GAAG,8BAA8B,EAAE;gBACnC,GAAG,KAAK;gBACR,gEAAgE;gBAChE,QAAQ,EAAE,EAAE;aACb;SACF,CAAC,CAAC;;QA1CL;;WAEG;QACM,mDAAyB;QAElC;;WAEG;QACM,uDAA6B;QAEtC;;;WAGG;QACM,2CAAgD,IAAI,GAAG,EAAE,EAAC;QA8BjE,uBAAA,IAAI,oCAAoB,eAAe,MAAA,CAAC;QACxC,uBAAA,IAAI,wCAAwB,mBAAmB,MAAA,CAAC;IAClD,CAAC;IAED;;;;;;;;;;;OAWG;IACH,KAAK,CAAC,cAAc,CAClB,QAAgB,EAChB,OAAkD,EAClD,OAA+B;QAE/B,MAAM,GAAG,GAAG,OAAO,EAAE,GAAG,IAAI,uBAAA,IAAI,wCAAiB,CAAC;QAElD,6EAA6E;QAC7E,MAAM,OAAO,GAAG,uBAAA,IAAI,wCAAiB,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACpD,IAAI,OAAO,EAAE,CAAC;YACZ,OAAO,OAAO,CAAC,OAA2B,CAAC;QAC7C,CAAC;QAED,8CAA8C;QAC9C,IAAI,CAAC,OAAO,EAAE,YAAY,EAAE,CAAC;YAC3B,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;YAC7C,IAAI,MAAM,IAAI,CAAC,IAAA,6BAAc,EAAC,MAAM,EAAE,GAAG,CAAC,EAAE,CAAC;gBAC3C,OAAO,MAAM,CAAC,IAAe,CAAC;YAChC,CAAC;QACH,CAAC;QAED,2CAA2C;QAC3C,MAAM,eAAe,GAAG,IAAI,eAAe,EAAE,CAAC;QAC9C,MAAM,aAAa,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAEjC,0BAA0B;QAC1B,uBAAA,IAAI,uEAAoB,MAAxB,IAAI,EAAqB,QAAQ,EAAE,IAAA,iCAAkB,GAAE,CAAC,CAAC;QAEzD,2BAA2B;QAC3B,MAAM,OAAO,GAAG,CAAC,KAAK,IAAsB,EAAE;YAC5C,IAAI,CAAC;gBACH,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC;gBAEnD,gCAAgC;gBAChC,IAAI,eAAe,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;oBACnC,MAAM,IAAI,KAAK,CAAC,qBAAqB,CAAC,CAAC;gBACzC,CAAC;gBAED,uBAAA,IAAI,uEAAoB,MAAxB,IAAI,EACF,QAAQ,EACR,IAAA,iCAAkB,EAAC,IAAY,EAAE,aAAa,CAAC,CAChD,CAAC;gBACF,OAAO,IAAI,CAAC;YACd,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,gCAAgC;gBAChC,IAAI,eAAe,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;oBACnC,MAAM,KAAK,CAAC;gBACd,CAAC;gBAED,MAAM,YAAY,GAAI,KAAe,EAAE,OAAO,CAAC;gBAE/C,uBAAA,IAAI,uEAAoB,MAAxB,IAAI,EACF,QAAQ,EACR,IAAA,+BAAgB,EAAC,YAAY,IAAI,eAAe,EAAE,aAAa,CAAC,CACjE,CAAC;gBACF,MAAM,KAAK,CAAC;YACd,CAAC;oBAAS,CAAC;gBACT,yEAAyE;gBACzE,MAAM,cAAc,GAAG,uBAAA,IAAI,wCAAiB,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;gBAC3D,IAAI,cAAc,EAAE,eAAe,KAAK,eAAe,EAAE,CAAC;oBACxD,uBAAA,IAAI,wCAAiB,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;gBACzC,CAAC;YACH,CAAC;QACH,CAAC,CAAC,EAAE,CAAC;QAEL,0CAA0C;QAC1C,uBAAA,IAAI,wCAAiB,CAAC,GAAG,CAAC,QAAQ,EAAE,EAAE,OAAO,EAAE,eAAe,EAAE,CAAC,CAAC;QAElE,OAAO,OAAO,CAAC;IACjB,CAAC;IAED;;;;;OAKG;IACH,YAAY,CAAC,QAAgB;QAC3B,MAAM,OAAO,GAAG,uBAAA,IAAI,wCAAiB,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACpD,IAAI,OAAO,EAAE,CAAC;YACZ,OAAO,CAAC,eAAe,CAAC,KAAK,EAAE,CAAC;YAChC,uBAAA,IAAI,wCAAiB,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;YACvC,uBAAA,IAAI,uEAAoB,MAAxB,IAAI,EAAqB,QAAQ,CAAC,CAAC;YACnC,OAAO,IAAI,CAAC;QACd,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IAiBD;;;;;OAKG;IACH,eAAe,CAAC,QAAgB;QAC9B,OAAO,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IACvC,CAAC;IAyCD;;;;;;;OAOG;IACH,KAAK,CAAC,gBAAgB,CAAC,OAA+B;QACpD,MAAM,QAAQ,GAAG,IAAA,6BAAc,EAAC,kBAAkB,EAAE,EAAE,CAAC,CAAC;QAExD,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC,cAAc,CAC1C,QAAQ,EACR,KAAK,IAAI,EAAE;YACT,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,6BAA6B,CAAC,CAAC;YACxE,OAAO,MAAM,CAAC;QAChB,CAAC,EACD,OAAO,CACR,CAAC;QAEF,MAAM,gBAAgB,GAAG,UAAU;YACjC,CAAC,CAAC,UAAU,CAAC,WAAW,EAAE,CAAC,IAAI,EAAE;YACjC,CAAC,CAAC,UAAU,CAAC;QAEf,IAAI,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE;YACpB,KAAK,CAAC,UAAU,GAAG,gBAAgB,CAAC;QACtC,CAAC,CAAC,CAAC;QAEH,IAAI,gBAAgB,EAAE,CAAC;YACrB,IAAI,CAAC;gBACH,MAAM,IAAI,CAAC,iBAAiB,CAAC,gBAAgB,EAAE,OAAO,CAAC,CAAC;YAC1D,CAAC;YAAC,MAAM,CAAC;gBACP,IAAI,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE;oBACpB,MAAM,iBAAiB,GAAG,KAAK,CAAC,UAAU,EAAE,WAAW,EAAE,CAAC,IAAI,EAAE,CAAC;oBACjE,IAAI,iBAAiB,KAAK,gBAAgB,EAAE,CAAC;wBAC3C,KAAK,CAAC,WAAW,GAAG,IAAI,CAAC;oBAC3B,CAAC;gBACH,CAAC,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QAED,OAAO,gBAAgB,CAAC;IAC1B,CAAC;IAED;;;;;;;OAOG;IACH,KAAK,CAAC,aAAa,CACjB,MAAc,EACd,OAA+B;QAE/B,MAAM,gBAAgB,GAAG,MAAM,CAAC,WAAW,EAAE,CAAC,IAAI,EAAE,CAAC;QAErD,IAAI,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE;YACpB,KAAK,CAAC,UAAU,GAAG,gBAAgB,CAAC;QACtC,CAAC,CAAC,CAAC;QAEH,IAAI,CAAC;YACH,OAAO,MAAM,IAAI,CAAC,iBAAiB,CAAC,gBAAgB,EAAE,OAAO,CAAC,CAAC;QACjE,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,kEAAkE;YAClE,6EAA6E;YAC7E,gFAAgF;YAChF,gFAAgF;YAChF,uCAAuC;YACvC,IAAI,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE;gBACpB,MAAM,iBAAiB,GAAG,KAAK,CAAC,UAAU,EAAE,WAAW,EAAE,CAAC,IAAI,EAAE,CAAC;gBACjE,IAAI,iBAAiB,KAAK,gBAAgB,EAAE,CAAC;oBAC3C,KAAK,CAAC,WAAW,GAAG,IAAI,CAAC;gBAC3B,CAAC;YACH,CAAC,CAAC,CAAC;YACH,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;IAED;;;;;;OAMG;IACH,KAAK,CAAC,IAAI,CAAC,OAA+B;QACxC,MAAM,IAAI,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE;YAC9C,yEAAyE;QAC3E,CAAC,CAAC,CAAC;IACL,CAAC;IAED;;;;;;OAMG;IACH,KAAK,CAAC,iBAAiB,CACrB,OAAe,EACf,OAA+B;QAE/B,MAAM,iBAAiB,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,IAAI,EAAE,CAAC;QACvD,MAAM,QAAQ,GAAG,IAAA,6BAAc,EAAC,mBAAmB,EAAE,CAAC,iBAAiB,CAAC,CAAC,CAAC;QAE1E,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC,cAAc,CAC3C,QAAQ,EACR,KAAK,IAAI,EAAE;YACT,OAAO,IAAI,CAAC,SAAS,CAAC,IAAI,CACxB,6BAA6B,EAC7B,iBAAiB,CAClB,CAAC;QACJ,CAAC,EACD,OAAO,CACR,CAAC;QAEF,IAAI,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE;YACpB,MAAM,UAAU,GAAG,KAAK,CAAC,UAAU,EAAE,WAAW,EAAE,CAAC,IAAI,EAAE,CAAC;YAE1D,IAAI,UAAU,KAAK,SAAS,IAAI,UAAU,KAAK,iBAAiB,EAAE,CAAC;gBACjE,KAAK,CAAC,WAAW,GAAG,WAAW,CAAC;YAClC,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,OAAO,WAAW,CAAC;IACrB,CAAC;IAED;;;;;;OAMG;IACH,KAAK,CAAC,YAAY,CAChB,SAAyB,KAAK,EAC9B,OAA+B;QAE/B,MAAM,QAAQ,GAAG,IAAA,6BAAc,EAAC,cAAc,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC;QAE1D,OAAO,IAAI,CAAC,cAAc,CACxB,QAAQ,EACR,KAAK,IAAI,EAAE;YACT,OAAO,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,2BAA2B,EAAE,MAAM,CAAC,CAAC;QAClE,CAAC,EACD,OAAO,CACR,CAAC;IACJ,CAAC;CACF;AA/WD,0CA+WC;yRAjNqB,QAAgB;IAClC,IAAI,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE;QACpB,MAAM,QAAQ,GAAG,KAAK,CAAC,QAGtB,CAAC;QACF,OAAO,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAC5B,CAAC,CAAC,CAAC;AACL,CAAC,qFAkBmB,QAAgB,EAAE,YAA0B;IAC9D,MAAM,OAAO,GAAG,uBAAA,IAAI,4CAAqB,CAAC;IAE1C,IAAI,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE;QACpB,MAAM,QAAQ,GAAG,KAAK,CAAC,QAGtB,CAAC;QACF,QAAQ,CAAC,QAAQ,CAAC,GAAG,YAAY,CAAC;QAElC,iDAAiD;QACjD,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAEnC,IAAI,IAAI,CAAC,MAAM,GAAG,OAAO,EAAE,CAAC;YAC1B,mCAAmC;YACnC,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;gBACpC,MAAM,KAAK,GAAG,QAAQ,CAAC,CAAC,CAAC,EAAE,SAAS,IAAI,CAAC,CAAC;gBAC1C,MAAM,KAAK,GAAG,QAAQ,CAAC,CAAC,CAAC,EAAE,SAAS,IAAI,CAAC,CAAC;gBAC1C,OAAO,KAAK,GAAG,KAAK,CAAC;YACvB,CAAC,CAAC,CAAC;YAEH,oDAAoD;YACpD,MAAM,eAAe,GAAG,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC;YAC9C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,eAAe,EAAE,CAAC,EAAE,EAAE,CAAC;gBACzC,MAAM,WAAW,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC;gBAClC,IAAI,WAAW,EAAE,CAAC;oBAChB,OAAO,QAAQ,CAAC,WAAW,CAAC,CAAC;gBAC/B,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC","sourcesContent":["import type {\n ControllerGetStateAction,\n ControllerStateChangeEvent,\n StateMetadata,\n} from '@metamask/base-controller';\nimport { BaseController } from '@metamask/base-controller';\nimport type { Messenger } from '@metamask/messenger';\nimport type { Json } from '@metamask/utils';\n\nimport type { Country, Eligibility } from './RampsService';\nimport type {\n RampsServiceGetGeolocationAction,\n RampsServiceGetCountriesAction,\n RampsServiceGetEligibilityAction,\n} from './RampsService-method-action-types';\nimport type {\n RequestCache as RequestCacheType,\n RequestState,\n ExecuteRequestOptions,\n PendingRequest,\n} from './RequestCache';\nimport {\n DEFAULT_REQUEST_CACHE_TTL,\n DEFAULT_REQUEST_CACHE_MAX_SIZE,\n createCacheKey,\n isCacheExpired,\n createLoadingState,\n createSuccessState,\n createErrorState,\n} from './RequestCache';\n\n// === GENERAL ===\n\n/**\n * The name of the {@link RampsController}, used to namespace the\n * controller's actions and events and to namespace the controller's state data\n * when composed with other controllers.\n */\nexport const controllerName = 'RampsController';\n\n// === STATE ===\n\n/**\n * Describes the shape of the state object for {@link RampsController}.\n */\nexport type RampsControllerState = {\n /**\n * The user's selected region code (e.g., \"US-CA\").\n * Initially set via geolocation fetch, but can be manually changed by the user.\n */\n userRegion: string | null;\n /**\n * Eligibility information for the user's current region.\n */\n eligibility: Eligibility | null;\n /**\n * Cache of request states, keyed by cache key.\n * This stores loading, success, and error states for API requests.\n */\n requests: RequestCacheType;\n};\n\n/**\n * The metadata for each property in {@link RampsControllerState}.\n */\nconst rampsControllerMetadata = {\n userRegion: {\n persist: true,\n includeInDebugSnapshot: true,\n includeInStateLogs: true,\n usedInUi: true,\n },\n eligibility: {\n persist: true,\n includeInDebugSnapshot: true,\n includeInStateLogs: true,\n usedInUi: true,\n },\n requests: {\n persist: false,\n includeInDebugSnapshot: true,\n includeInStateLogs: false,\n usedInUi: true,\n },\n} satisfies StateMetadata<RampsControllerState>;\n\n/**\n * Constructs the default {@link RampsController} state. This allows\n * consumers to provide a partial state object when initializing the controller\n * and also helps in constructing complete state objects for this controller in\n * tests.\n *\n * @returns The default {@link RampsController} state.\n */\nexport function getDefaultRampsControllerState(): RampsControllerState {\n return {\n userRegion: null,\n eligibility: null,\n requests: {},\n };\n}\n\n// === MESSENGER ===\n\n/**\n * Retrieves the state of the {@link RampsController}.\n */\nexport type RampsControllerGetStateAction = ControllerGetStateAction<\n typeof controllerName,\n RampsControllerState\n>;\n\n/**\n * Actions that {@link RampsControllerMessenger} exposes to other consumers.\n */\nexport type RampsControllerActions = RampsControllerGetStateAction;\n\n/**\n * Actions from other messengers that {@link RampsController} calls.\n */\ntype AllowedActions =\n | RampsServiceGetGeolocationAction\n | RampsServiceGetCountriesAction\n | RampsServiceGetEligibilityAction;\n\n/**\n * Published when the state of {@link RampsController} changes.\n */\nexport type RampsControllerStateChangeEvent = ControllerStateChangeEvent<\n typeof controllerName,\n RampsControllerState\n>;\n\n/**\n * Events that {@link RampsControllerMessenger} exposes to other consumers.\n */\nexport type RampsControllerEvents = RampsControllerStateChangeEvent;\n\n/**\n * Events from other messengers that {@link RampsController} subscribes to.\n */\ntype AllowedEvents = never;\n\n/**\n * The messenger restricted to actions and events accessed by\n * {@link RampsController}.\n */\nexport type RampsControllerMessenger = Messenger<\n typeof controllerName,\n RampsControllerActions | AllowedActions,\n RampsControllerEvents | AllowedEvents\n>;\n\n/**\n * Configuration options for the RampsController.\n */\nexport type RampsControllerOptions = {\n /** The messenger suited for this controller. */\n messenger: RampsControllerMessenger;\n /** The desired state with which to initialize this controller. */\n state?: Partial<RampsControllerState>;\n /** Time to live for cached requests in milliseconds. Defaults to 15 minutes. */\n requestCacheTTL?: number;\n /** Maximum number of entries in the request cache. Defaults to 250. */\n requestCacheMaxSize?: number;\n};\n\n// === CONTROLLER DEFINITION ===\n\n/**\n * Manages cryptocurrency on/off ramps functionality.\n */\nexport class RampsController extends BaseController<\n typeof controllerName,\n RampsControllerState,\n RampsControllerMessenger\n> {\n /**\n * Default TTL for cached requests.\n */\n readonly #requestCacheTTL: number;\n\n /**\n * Maximum number of entries in the request cache.\n */\n readonly #requestCacheMaxSize: number;\n\n /**\n * Map of pending requests for deduplication.\n * Key is the cache key, value is the pending request with abort controller.\n */\n readonly #pendingRequests: Map<string, PendingRequest> = new Map();\n\n /**\n * Constructs a new {@link RampsController}.\n *\n * @param args - The constructor arguments.\n * @param args.messenger - The messenger suited for this controller.\n * @param args.state - The desired state with which to initialize this\n * controller. Missing properties will be filled in with defaults.\n * @param args.requestCacheTTL - Time to live for cached requests in milliseconds.\n * @param args.requestCacheMaxSize - Maximum number of entries in the request cache.\n */\n constructor({\n messenger,\n state = {},\n requestCacheTTL = DEFAULT_REQUEST_CACHE_TTL,\n requestCacheMaxSize = DEFAULT_REQUEST_CACHE_MAX_SIZE,\n }: RampsControllerOptions) {\n super({\n messenger,\n metadata: rampsControllerMetadata,\n name: controllerName,\n state: {\n ...getDefaultRampsControllerState(),\n ...state,\n // Always reset requests cache on initialization (non-persisted)\n requests: {},\n },\n });\n\n this.#requestCacheTTL = requestCacheTTL;\n this.#requestCacheMaxSize = requestCacheMaxSize;\n }\n\n /**\n * Executes a request with caching and deduplication.\n *\n * If a request with the same cache key is already in flight, returns the\n * existing promise. If valid cached data exists, returns it without making\n * a new request.\n *\n * @param cacheKey - Unique identifier for this request.\n * @param fetcher - Function that performs the actual fetch. Receives an AbortSignal.\n * @param options - Options for cache behavior.\n * @returns The result of the request.\n */\n async executeRequest<TResult>(\n cacheKey: string,\n fetcher: (signal: AbortSignal) => Promise<TResult>,\n options?: ExecuteRequestOptions,\n ): Promise<TResult> {\n const ttl = options?.ttl ?? this.#requestCacheTTL;\n\n // Check for existing pending request - join it instead of making a duplicate\n const pending = this.#pendingRequests.get(cacheKey);\n if (pending) {\n return pending.promise as Promise<TResult>;\n }\n\n // Check cache validity (unless force refresh)\n if (!options?.forceRefresh) {\n const cached = this.state.requests[cacheKey];\n if (cached && !isCacheExpired(cached, ttl)) {\n return cached.data as TResult;\n }\n }\n\n // Create abort controller for this request\n const abortController = new AbortController();\n const lastFetchedAt = Date.now();\n\n // Update state to loading\n this.#updateRequestState(cacheKey, createLoadingState());\n\n // Create the fetch promise\n const promise = (async (): Promise<TResult> => {\n try {\n const data = await fetcher(abortController.signal);\n\n // Don't update state if aborted\n if (abortController.signal.aborted) {\n throw new Error('Request was aborted');\n }\n\n this.#updateRequestState(\n cacheKey,\n createSuccessState(data as Json, lastFetchedAt),\n );\n return data;\n } catch (error) {\n // Don't update state if aborted\n if (abortController.signal.aborted) {\n throw error;\n }\n\n const errorMessage = (error as Error)?.message;\n\n this.#updateRequestState(\n cacheKey,\n createErrorState(errorMessage ?? 'Unknown error', lastFetchedAt),\n );\n throw error;\n } finally {\n // Only delete if this is still our entry (not replaced by a new request)\n const currentPending = this.#pendingRequests.get(cacheKey);\n if (currentPending?.abortController === abortController) {\n this.#pendingRequests.delete(cacheKey);\n }\n }\n })();\n\n // Store pending request for deduplication\n this.#pendingRequests.set(cacheKey, { promise, abortController });\n\n return promise;\n }\n\n /**\n * Aborts a pending request if one exists.\n *\n * @param cacheKey - The cache key of the request to abort.\n * @returns True if a request was aborted.\n */\n abortRequest(cacheKey: string): boolean {\n const pending = this.#pendingRequests.get(cacheKey);\n if (pending) {\n pending.abortController.abort();\n this.#pendingRequests.delete(cacheKey);\n this.#removeRequestState(cacheKey);\n return true;\n }\n return false;\n }\n\n /**\n * Removes a request state from the cache.\n *\n * @param cacheKey - The cache key to remove.\n */\n #removeRequestState(cacheKey: string): void {\n this.update((state) => {\n const requests = state.requests as unknown as Record<\n string,\n RequestState | undefined\n >;\n delete requests[cacheKey];\n });\n }\n\n /**\n * Gets the state of a specific cached request.\n *\n * @param cacheKey - The cache key to look up.\n * @returns The request state, or undefined if not cached.\n */\n getRequestState(cacheKey: string): RequestState | undefined {\n return this.state.requests[cacheKey];\n }\n\n /**\n * Updates the state for a specific request.\n *\n * @param cacheKey - The cache key.\n * @param requestState - The new state for the request.\n */\n #updateRequestState(cacheKey: string, requestState: RequestState): void {\n const maxSize = this.#requestCacheMaxSize;\n\n this.update((state) => {\n const requests = state.requests as unknown as Record<\n string,\n RequestState | undefined\n >;\n requests[cacheKey] = requestState;\n\n // Evict oldest entries if cache exceeds max size\n const keys = Object.keys(requests);\n\n if (keys.length > maxSize) {\n // Sort by timestamp (oldest first)\n const sortedKeys = keys.sort((a, b) => {\n const aTime = requests[a]?.timestamp ?? 0;\n const bTime = requests[b]?.timestamp ?? 0;\n return aTime - bTime;\n });\n\n // Remove oldest entries until we're under the limit\n const entriesToRemove = keys.length - maxSize;\n for (let i = 0; i < entriesToRemove; i++) {\n const keyToRemove = sortedKeys[i];\n if (keyToRemove) {\n delete requests[keyToRemove];\n }\n }\n }\n });\n }\n\n /**\n * Updates the user's region by fetching geolocation and eligibility.\n * This method calls the RampsService to get the geolocation,\n * then automatically fetches eligibility for that region.\n *\n * @param options - Options for cache behavior.\n * @returns The user region string.\n */\n async updateUserRegion(options?: ExecuteRequestOptions): Promise<string> {\n const cacheKey = createCacheKey('updateUserRegion', []);\n\n const userRegion = await this.executeRequest(\n cacheKey,\n async () => {\n const result = await this.messenger.call('RampsService:getGeolocation');\n return result;\n },\n options,\n );\n\n const normalizedRegion = userRegion\n ? userRegion.toLowerCase().trim()\n : userRegion;\n\n this.update((state) => {\n state.userRegion = normalizedRegion;\n });\n\n if (normalizedRegion) {\n try {\n await this.updateEligibility(normalizedRegion, options);\n } catch {\n this.update((state) => {\n const currentUserRegion = state.userRegion?.toLowerCase().trim();\n if (currentUserRegion === normalizedRegion) {\n state.eligibility = null;\n }\n });\n }\n }\n\n return normalizedRegion;\n }\n\n /**\n * Sets the user's region manually (without fetching geolocation).\n * This allows users to override the detected region.\n *\n * @param region - The region code to set (e.g., \"US-CA\").\n * @param options - Options for cache behavior when fetching eligibility.\n * @returns The eligibility information for the region.\n */\n async setUserRegion(\n region: string,\n options?: ExecuteRequestOptions,\n ): Promise<Eligibility> {\n const normalizedRegion = region.toLowerCase().trim();\n\n this.update((state) => {\n state.userRegion = normalizedRegion;\n });\n\n try {\n return await this.updateEligibility(normalizedRegion, options);\n } catch (error) {\n // Eligibility fetch failed, but user region was successfully set.\n // Don't let eligibility errors prevent user region state from being updated.\n // Clear eligibility state to avoid showing stale data from a previous location.\n // Only clear if the region still matches to avoid race conditions where a newer\n // region change has already succeeded.\n this.update((state) => {\n const currentUserRegion = state.userRegion?.toLowerCase().trim();\n if (currentUserRegion === normalizedRegion) {\n state.eligibility = null;\n }\n });\n throw error;\n }\n }\n\n /**\n * Initializes the controller by fetching the user's region from geolocation.\n * This should be called once at app startup to set up the initial region.\n *\n * @param options - Options for cache behavior.\n * @returns Promise that resolves when initialization is complete.\n */\n async init(options?: ExecuteRequestOptions): Promise<void> {\n await this.updateUserRegion(options).catch(() => {\n // User region fetch failed - error state will be available via selectors\n });\n }\n\n /**\n * Updates the eligibility information for a given region.\n *\n * @param isoCode - The ISO code for the region (e.g., \"us\", \"fr\", \"us-ny\").\n * @param options - Options for cache behavior.\n * @returns The eligibility information.\n */\n async updateEligibility(\n isoCode: string,\n options?: ExecuteRequestOptions,\n ): Promise<Eligibility> {\n const normalizedIsoCode = isoCode.toLowerCase().trim();\n const cacheKey = createCacheKey('updateEligibility', [normalizedIsoCode]);\n\n const eligibility = await this.executeRequest(\n cacheKey,\n async () => {\n return this.messenger.call(\n 'RampsService:getEligibility',\n normalizedIsoCode,\n );\n },\n options,\n );\n\n this.update((state) => {\n const userRegion = state.userRegion?.toLowerCase().trim();\n\n if (userRegion === undefined || userRegion === normalizedIsoCode) {\n state.eligibility = eligibility;\n }\n });\n\n return eligibility;\n }\n\n /**\n * Fetches the list of supported countries for a given ramp action.\n *\n * @param action - The ramp action type ('buy' or 'sell').\n * @param options - Options for cache behavior.\n * @returns An array of countries with their eligibility information.\n */\n async getCountries(\n action: 'buy' | 'sell' = 'buy',\n options?: ExecuteRequestOptions,\n ): Promise<Country[]> {\n const cacheKey = createCacheKey('getCountries', [action]);\n\n return this.executeRequest(\n cacheKey,\n async () => {\n return this.messenger.call('RampsService:getCountries', action);\n },\n options,\n );\n }\n}\n"]}
@@ -15,9 +15,10 @@ export declare const controllerName = "RampsController";
15
15
  */
16
16
  export type RampsControllerState = {
17
17
  /**
18
- * The user's country code determined by geolocation.
18
+ * The user's selected region code (e.g., "US-CA").
19
+ * Initially set via geolocation fetch, but can be manually changed by the user.
19
20
  */
20
- geolocation: string | null;
21
+ userRegion: string | null;
21
22
  /**
22
23
  * Eligibility information for the user's current region.
23
24
  */
@@ -123,14 +124,31 @@ export declare class RampsController extends BaseController<typeof controllerNam
123
124
  */
124
125
  getRequestState(cacheKey: string): RequestState | undefined;
125
126
  /**
126
- * Updates the user's geolocation and eligibility.
127
+ * Updates the user's region by fetching geolocation and eligibility.
127
128
  * This method calls the RampsService to get the geolocation,
128
129
  * then automatically fetches eligibility for that region.
129
130
  *
130
131
  * @param options - Options for cache behavior.
131
- * @returns The geolocation string.
132
+ * @returns The user region string.
132
133
  */
133
- updateGeolocation(options?: ExecuteRequestOptions): Promise<string>;
134
+ updateUserRegion(options?: ExecuteRequestOptions): Promise<string>;
135
+ /**
136
+ * Sets the user's region manually (without fetching geolocation).
137
+ * This allows users to override the detected region.
138
+ *
139
+ * @param region - The region code to set (e.g., "US-CA").
140
+ * @param options - Options for cache behavior when fetching eligibility.
141
+ * @returns The eligibility information for the region.
142
+ */
143
+ setUserRegion(region: string, options?: ExecuteRequestOptions): Promise<Eligibility>;
144
+ /**
145
+ * Initializes the controller by fetching the user's region from geolocation.
146
+ * This should be called once at app startup to set up the initial region.
147
+ *
148
+ * @param options - Options for cache behavior.
149
+ * @returns Promise that resolves when initialization is complete.
150
+ */
151
+ init(options?: ExecuteRequestOptions): Promise<void>;
134
152
  /**
135
153
  * Updates the eligibility information for a given region.
136
154
  *
@@ -1 +1 @@
1
- {"version":3,"file":"RampsController.d.cts","sourceRoot":"","sources":["../src/RampsController.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"RampsController.d.cts","sourceRoot":"","sources":["../src/RampsController.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,wBAAwB,EACxB,0BAA0B,EAE3B,kCAAkC;AACnC,OAAO,EAAE,cAAc,EAAE,kCAAkC;AAC3D,OAAO,KAAK,EAAE,SAAS,EAAE,4BAA4B;AAGrD,OAAO,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,2BAAuB;AAC3D,OAAO,KAAK,EACV,gCAAgC,EAChC,8BAA8B,EAC9B,gCAAgC,EACjC,+CAA2C;AAC5C,OAAO,KAAK,EACV,YAAY,IAAI,gBAAgB,EAChC,YAAY,EACZ,qBAAqB,EAEtB,2BAAuB;AAaxB;;;;GAIG;AACH,eAAO,MAAM,cAAc,oBAAoB,CAAC;AAIhD;;GAEG;AACH,MAAM,MAAM,oBAAoB,GAAG;IACjC;;;OAGG;IACH,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B;;OAEG;IACH,WAAW,EAAE,WAAW,GAAG,IAAI,CAAC;IAChC;;;OAGG;IACH,QAAQ,EAAE,gBAAgB,CAAC;CAC5B,CAAC;AA0BF;;;;;;;GAOG;AACH,wBAAgB,8BAA8B,IAAI,oBAAoB,CAMrE;AAID;;GAEG;AACH,MAAM,MAAM,6BAA6B,GAAG,wBAAwB,CAClE,OAAO,cAAc,EACrB,oBAAoB,CACrB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,sBAAsB,GAAG,6BAA6B,CAAC;AAEnE;;GAEG;AACH,KAAK,cAAc,GACf,gCAAgC,GAChC,8BAA8B,GAC9B,gCAAgC,CAAC;AAErC;;GAEG;AACH,MAAM,MAAM,+BAA+B,GAAG,0BAA0B,CACtE,OAAO,cAAc,EACrB,oBAAoB,CACrB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,qBAAqB,GAAG,+BAA+B,CAAC;AAEpE;;GAEG;AACH,KAAK,aAAa,GAAG,KAAK,CAAC;AAE3B;;;GAGG;AACH,MAAM,MAAM,wBAAwB,GAAG,SAAS,CAC9C,OAAO,cAAc,EACrB,sBAAsB,GAAG,cAAc,EACvC,qBAAqB,GAAG,aAAa,CACtC,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,sBAAsB,GAAG;IACnC,gDAAgD;IAChD,SAAS,EAAE,wBAAwB,CAAC;IACpC,kEAAkE;IAClE,KAAK,CAAC,EAAE,OAAO,CAAC,oBAAoB,CAAC,CAAC;IACtC,gFAAgF;IAChF,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,uEAAuE;IACvE,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B,CAAC;AAIF;;GAEG;AACH,qBAAa,eAAgB,SAAQ,cAAc,CACjD,OAAO,cAAc,EACrB,oBAAoB,EACpB,wBAAwB,CACzB;;IAiBC;;;;;;;;;OASG;gBACS,EACV,SAAS,EACT,KAAU,EACV,eAA2C,EAC3C,mBAAoD,GACrD,EAAE,sBAAsB;IAiBzB;;;;;;;;;;;OAWG;IACG,cAAc,CAAC,OAAO,EAC1B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,OAAO,CAAC,EAClD,OAAO,CAAC,EAAE,qBAAqB,GAC9B,OAAO,CAAC,OAAO,CAAC;IAmEnB;;;;;OAKG;IACH,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO;IA0BvC;;;;;OAKG;IACH,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS;IA2C3D;;;;;;;OAOG;IACG,gBAAgB,CAAC,OAAO,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAAC,MAAM,CAAC;IAoCxE;;;;;;;OAOG;IACG,aAAa,CACjB,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,qBAAqB,GAC9B,OAAO,CAAC,WAAW,CAAC;IAyBvB;;;;;;OAMG;IACG,IAAI,CAAC,OAAO,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC;IAM1D;;;;;;OAMG;IACG,iBAAiB,CACrB,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,qBAAqB,GAC9B,OAAO,CAAC,WAAW,CAAC;IA0BvB;;;;;;OAMG;IACG,YAAY,CAChB,MAAM,GAAE,KAAK,GAAG,MAAc,EAC9B,OAAO,CAAC,EAAE,qBAAqB,GAC9B,OAAO,CAAC,OAAO,EAAE,CAAC;CAWtB"}
@@ -15,9 +15,10 @@ export declare const controllerName = "RampsController";
15
15
  */
16
16
  export type RampsControllerState = {
17
17
  /**
18
- * The user's country code determined by geolocation.
18
+ * The user's selected region code (e.g., "US-CA").
19
+ * Initially set via geolocation fetch, but can be manually changed by the user.
19
20
  */
20
- geolocation: string | null;
21
+ userRegion: string | null;
21
22
  /**
22
23
  * Eligibility information for the user's current region.
23
24
  */
@@ -123,14 +124,31 @@ export declare class RampsController extends BaseController<typeof controllerNam
123
124
  */
124
125
  getRequestState(cacheKey: string): RequestState | undefined;
125
126
  /**
126
- * Updates the user's geolocation and eligibility.
127
+ * Updates the user's region by fetching geolocation and eligibility.
127
128
  * This method calls the RampsService to get the geolocation,
128
129
  * then automatically fetches eligibility for that region.
129
130
  *
130
131
  * @param options - Options for cache behavior.
131
- * @returns The geolocation string.
132
+ * @returns The user region string.
132
133
  */
133
- updateGeolocation(options?: ExecuteRequestOptions): Promise<string>;
134
+ updateUserRegion(options?: ExecuteRequestOptions): Promise<string>;
135
+ /**
136
+ * Sets the user's region manually (without fetching geolocation).
137
+ * This allows users to override the detected region.
138
+ *
139
+ * @param region - The region code to set (e.g., "US-CA").
140
+ * @param options - Options for cache behavior when fetching eligibility.
141
+ * @returns The eligibility information for the region.
142
+ */
143
+ setUserRegion(region: string, options?: ExecuteRequestOptions): Promise<Eligibility>;
144
+ /**
145
+ * Initializes the controller by fetching the user's region from geolocation.
146
+ * This should be called once at app startup to set up the initial region.
147
+ *
148
+ * @param options - Options for cache behavior.
149
+ * @returns Promise that resolves when initialization is complete.
150
+ */
151
+ init(options?: ExecuteRequestOptions): Promise<void>;
134
152
  /**
135
153
  * Updates the eligibility information for a given region.
136
154
  *
@@ -1 +1 @@
1
- {"version":3,"file":"RampsController.d.mts","sourceRoot":"","sources":["../src/RampsController.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"RampsController.d.mts","sourceRoot":"","sources":["../src/RampsController.ts"],"names":[],"mappings":"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"}
@@ -23,7 +23,7 @@ export const controllerName = 'RampsController';
23
23
  * The metadata for each property in {@link RampsControllerState}.
24
24
  */
25
25
  const rampsControllerMetadata = {
26
- geolocation: {
26
+ userRegion: {
27
27
  persist: true,
28
28
  includeInDebugSnapshot: true,
29
29
  includeInStateLogs: true,
@@ -52,7 +52,7 @@ const rampsControllerMetadata = {
52
52
  */
53
53
  export function getDefaultRampsControllerState() {
54
54
  return {
55
- geolocation: null,
55
+ userRegion: null,
56
56
  eligibility: null,
57
57
  requests: {},
58
58
  };
@@ -190,36 +190,82 @@ export class RampsController extends BaseController {
190
190
  return this.state.requests[cacheKey];
191
191
  }
192
192
  /**
193
- * Updates the user's geolocation and eligibility.
193
+ * Updates the user's region by fetching geolocation and eligibility.
194
194
  * This method calls the RampsService to get the geolocation,
195
195
  * then automatically fetches eligibility for that region.
196
196
  *
197
197
  * @param options - Options for cache behavior.
198
- * @returns The geolocation string.
198
+ * @returns The user region string.
199
199
  */
200
- async updateGeolocation(options) {
201
- const cacheKey = createCacheKey('updateGeolocation', []);
202
- const geolocation = await this.executeRequest(cacheKey, async () => {
200
+ async updateUserRegion(options) {
201
+ const cacheKey = createCacheKey('updateUserRegion', []);
202
+ const userRegion = await this.executeRequest(cacheKey, async () => {
203
203
  const result = await this.messenger.call('RampsService:getGeolocation');
204
204
  return result;
205
205
  }, options);
206
+ const normalizedRegion = userRegion
207
+ ? userRegion.toLowerCase().trim()
208
+ : userRegion;
206
209
  this.update((state) => {
207
- state.geolocation = geolocation;
210
+ state.userRegion = normalizedRegion;
208
211
  });
209
- if (geolocation) {
212
+ if (normalizedRegion) {
210
213
  try {
211
- await this.updateEligibility(geolocation, options);
214
+ await this.updateEligibility(normalizedRegion, options);
212
215
  }
213
216
  catch {
214
- // Eligibility fetch failed, but geolocation was successfully fetched and cached.
215
- // Don't let eligibility errors prevent geolocation state from being updated.
216
- // Clear eligibility state to avoid showing stale data from a previous location.
217
217
  this.update((state) => {
218
- state.eligibility = null;
218
+ const currentUserRegion = state.userRegion?.toLowerCase().trim();
219
+ if (currentUserRegion === normalizedRegion) {
220
+ state.eligibility = null;
221
+ }
219
222
  });
220
223
  }
221
224
  }
222
- return geolocation;
225
+ return normalizedRegion;
226
+ }
227
+ /**
228
+ * Sets the user's region manually (without fetching geolocation).
229
+ * This allows users to override the detected region.
230
+ *
231
+ * @param region - The region code to set (e.g., "US-CA").
232
+ * @param options - Options for cache behavior when fetching eligibility.
233
+ * @returns The eligibility information for the region.
234
+ */
235
+ async setUserRegion(region, options) {
236
+ const normalizedRegion = region.toLowerCase().trim();
237
+ this.update((state) => {
238
+ state.userRegion = normalizedRegion;
239
+ });
240
+ try {
241
+ return await this.updateEligibility(normalizedRegion, options);
242
+ }
243
+ catch (error) {
244
+ // Eligibility fetch failed, but user region was successfully set.
245
+ // Don't let eligibility errors prevent user region state from being updated.
246
+ // Clear eligibility state to avoid showing stale data from a previous location.
247
+ // Only clear if the region still matches to avoid race conditions where a newer
248
+ // region change has already succeeded.
249
+ this.update((state) => {
250
+ const currentUserRegion = state.userRegion?.toLowerCase().trim();
251
+ if (currentUserRegion === normalizedRegion) {
252
+ state.eligibility = null;
253
+ }
254
+ });
255
+ throw error;
256
+ }
257
+ }
258
+ /**
259
+ * Initializes the controller by fetching the user's region from geolocation.
260
+ * This should be called once at app startup to set up the initial region.
261
+ *
262
+ * @param options - Options for cache behavior.
263
+ * @returns Promise that resolves when initialization is complete.
264
+ */
265
+ async init(options) {
266
+ await this.updateUserRegion(options).catch(() => {
267
+ // User region fetch failed - error state will be available via selectors
268
+ });
223
269
  }
224
270
  /**
225
271
  * Updates the eligibility information for a given region.
@@ -235,8 +281,8 @@ export class RampsController extends BaseController {
235
281
  return this.messenger.call('RampsService:getEligibility', normalizedIsoCode);
236
282
  }, options);
237
283
  this.update((state) => {
238
- if (state.geolocation === null ||
239
- state.geolocation.toLowerCase().trim() === normalizedIsoCode) {
284
+ const userRegion = state.userRegion?.toLowerCase().trim();
285
+ if (userRegion === undefined || userRegion === normalizedIsoCode) {
240
286
  state.eligibility = eligibility;
241
287
  }
242
288
  });
@@ -1 +1 @@
1
- {"version":3,"file":"RampsController.mjs","sourceRoot":"","sources":["../src/RampsController.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n ControllerGetStateAction,\n ControllerStateChangeEvent,\n StateMetadata,\n} from '@metamask/base-controller';\nimport { BaseController } from '@metamask/base-controller';\nimport type { Messenger } from '@metamask/messenger';\nimport type { Json } from '@metamask/utils';\n\nimport type { Country, Eligibility } from './RampsService';\nimport type {\n RampsServiceGetGeolocationAction,\n RampsServiceGetCountriesAction,\n RampsServiceGetEligibilityAction,\n} from './RampsService-method-action-types';\nimport type {\n RequestCache as RequestCacheType,\n RequestState,\n ExecuteRequestOptions,\n PendingRequest,\n} from './RequestCache';\nimport {\n DEFAULT_REQUEST_CACHE_TTL,\n DEFAULT_REQUEST_CACHE_MAX_SIZE,\n createCacheKey,\n isCacheExpired,\n createLoadingState,\n createSuccessState,\n createErrorState,\n} from './RequestCache';\n\n// === GENERAL ===\n\n/**\n * The name of the {@link RampsController}, used to namespace the\n * controller's actions and events and to namespace the controller's state data\n * when composed with other controllers.\n */\nexport const controllerName = 'RampsController';\n\n// === STATE ===\n\n/**\n * Describes the shape of the state object for {@link RampsController}.\n */\nexport type RampsControllerState = {\n /**\n * The user's country code determined by geolocation.\n */\n geolocation: string | null;\n /**\n * Eligibility information for the user's current region.\n */\n eligibility: Eligibility | null;\n /**\n * Cache of request states, keyed by cache key.\n * This stores loading, success, and error states for API requests.\n */\n requests: RequestCacheType;\n};\n\n/**\n * The metadata for each property in {@link RampsControllerState}.\n */\nconst rampsControllerMetadata = {\n geolocation: {\n persist: true,\n includeInDebugSnapshot: true,\n includeInStateLogs: true,\n usedInUi: true,\n },\n eligibility: {\n persist: true,\n includeInDebugSnapshot: true,\n includeInStateLogs: true,\n usedInUi: true,\n },\n requests: {\n persist: false,\n includeInDebugSnapshot: true,\n includeInStateLogs: false,\n usedInUi: true,\n },\n} satisfies StateMetadata<RampsControllerState>;\n\n/**\n * Constructs the default {@link RampsController} state. This allows\n * consumers to provide a partial state object when initializing the controller\n * and also helps in constructing complete state objects for this controller in\n * tests.\n *\n * @returns The default {@link RampsController} state.\n */\nexport function getDefaultRampsControllerState(): RampsControllerState {\n return {\n geolocation: null,\n eligibility: null,\n requests: {},\n };\n}\n\n// === MESSENGER ===\n\n/**\n * Retrieves the state of the {@link RampsController}.\n */\nexport type RampsControllerGetStateAction = ControllerGetStateAction<\n typeof controllerName,\n RampsControllerState\n>;\n\n/**\n * Actions that {@link RampsControllerMessenger} exposes to other consumers.\n */\nexport type RampsControllerActions = RampsControllerGetStateAction;\n\n/**\n * Actions from other messengers that {@link RampsController} calls.\n */\ntype AllowedActions =\n | RampsServiceGetGeolocationAction\n | RampsServiceGetCountriesAction\n | RampsServiceGetEligibilityAction;\n\n/**\n * Published when the state of {@link RampsController} changes.\n */\nexport type RampsControllerStateChangeEvent = ControllerStateChangeEvent<\n typeof controllerName,\n RampsControllerState\n>;\n\n/**\n * Events that {@link RampsControllerMessenger} exposes to other consumers.\n */\nexport type RampsControllerEvents = RampsControllerStateChangeEvent;\n\n/**\n * Events from other messengers that {@link RampsController} subscribes to.\n */\ntype AllowedEvents = never;\n\n/**\n * The messenger restricted to actions and events accessed by\n * {@link RampsController}.\n */\nexport type RampsControllerMessenger = Messenger<\n typeof controllerName,\n RampsControllerActions | AllowedActions,\n RampsControllerEvents | AllowedEvents\n>;\n\n/**\n * Configuration options for the RampsController.\n */\nexport type RampsControllerOptions = {\n /** The messenger suited for this controller. */\n messenger: RampsControllerMessenger;\n /** The desired state with which to initialize this controller. */\n state?: Partial<RampsControllerState>;\n /** Time to live for cached requests in milliseconds. Defaults to 15 minutes. */\n requestCacheTTL?: number;\n /** Maximum number of entries in the request cache. Defaults to 250. */\n requestCacheMaxSize?: number;\n};\n\n// === CONTROLLER DEFINITION ===\n\n/**\n * Manages cryptocurrency on/off ramps functionality.\n */\nexport class RampsController extends BaseController<\n typeof controllerName,\n RampsControllerState,\n RampsControllerMessenger\n> {\n /**\n * Default TTL for cached requests.\n */\n readonly #requestCacheTTL: number;\n\n /**\n * Maximum number of entries in the request cache.\n */\n readonly #requestCacheMaxSize: number;\n\n /**\n * Map of pending requests for deduplication.\n * Key is the cache key, value is the pending request with abort controller.\n */\n readonly #pendingRequests: Map<string, PendingRequest> = new Map();\n\n /**\n * Constructs a new {@link RampsController}.\n *\n * @param args - The constructor arguments.\n * @param args.messenger - The messenger suited for this controller.\n * @param args.state - The desired state with which to initialize this\n * controller. Missing properties will be filled in with defaults.\n * @param args.requestCacheTTL - Time to live for cached requests in milliseconds.\n * @param args.requestCacheMaxSize - Maximum number of entries in the request cache.\n */\n constructor({\n messenger,\n state = {},\n requestCacheTTL = DEFAULT_REQUEST_CACHE_TTL,\n requestCacheMaxSize = DEFAULT_REQUEST_CACHE_MAX_SIZE,\n }: RampsControllerOptions) {\n super({\n messenger,\n metadata: rampsControllerMetadata,\n name: controllerName,\n state: {\n ...getDefaultRampsControllerState(),\n ...state,\n // Always reset requests cache on initialization (non-persisted)\n requests: {},\n },\n });\n\n this.#requestCacheTTL = requestCacheTTL;\n this.#requestCacheMaxSize = requestCacheMaxSize;\n }\n\n /**\n * Executes a request with caching and deduplication.\n *\n * If a request with the same cache key is already in flight, returns the\n * existing promise. If valid cached data exists, returns it without making\n * a new request.\n *\n * @param cacheKey - Unique identifier for this request.\n * @param fetcher - Function that performs the actual fetch. Receives an AbortSignal.\n * @param options - Options for cache behavior.\n * @returns The result of the request.\n */\n async executeRequest<TResult>(\n cacheKey: string,\n fetcher: (signal: AbortSignal) => Promise<TResult>,\n options?: ExecuteRequestOptions,\n ): Promise<TResult> {\n const ttl = options?.ttl ?? this.#requestCacheTTL;\n\n // Check for existing pending request - join it instead of making a duplicate\n const pending = this.#pendingRequests.get(cacheKey);\n if (pending) {\n return pending.promise as Promise<TResult>;\n }\n\n // Check cache validity (unless force refresh)\n if (!options?.forceRefresh) {\n const cached = this.state.requests[cacheKey];\n if (cached && !isCacheExpired(cached, ttl)) {\n return cached.data as TResult;\n }\n }\n\n // Create abort controller for this request\n const abortController = new AbortController();\n const lastFetchedAt = Date.now();\n\n // Update state to loading\n this.#updateRequestState(cacheKey, createLoadingState());\n\n // Create the fetch promise\n const promise = (async (): Promise<TResult> => {\n try {\n const data = await fetcher(abortController.signal);\n\n // Don't update state if aborted\n if (abortController.signal.aborted) {\n throw new Error('Request was aborted');\n }\n\n this.#updateRequestState(\n cacheKey,\n createSuccessState(data as Json, lastFetchedAt),\n );\n return data;\n } catch (error) {\n // Don't update state if aborted\n if (abortController.signal.aborted) {\n throw error;\n }\n\n const errorMessage = (error as Error)?.message;\n\n this.#updateRequestState(\n cacheKey,\n createErrorState(errorMessage ?? 'Unknown error', lastFetchedAt),\n );\n throw error;\n } finally {\n // Only delete if this is still our entry (not replaced by a new request)\n const currentPending = this.#pendingRequests.get(cacheKey);\n if (currentPending?.abortController === abortController) {\n this.#pendingRequests.delete(cacheKey);\n }\n }\n })();\n\n // Store pending request for deduplication\n this.#pendingRequests.set(cacheKey, { promise, abortController });\n\n return promise;\n }\n\n /**\n * Aborts a pending request if one exists.\n *\n * @param cacheKey - The cache key of the request to abort.\n * @returns True if a request was aborted.\n */\n abortRequest(cacheKey: string): boolean {\n const pending = this.#pendingRequests.get(cacheKey);\n if (pending) {\n pending.abortController.abort();\n this.#pendingRequests.delete(cacheKey);\n this.#removeRequestState(cacheKey);\n return true;\n }\n return false;\n }\n\n /**\n * Removes a request state from the cache.\n *\n * @param cacheKey - The cache key to remove.\n */\n #removeRequestState(cacheKey: string): void {\n this.update((state) => {\n const requests = state.requests as unknown as Record<\n string,\n RequestState | undefined\n >;\n delete requests[cacheKey];\n });\n }\n\n /**\n * Gets the state of a specific cached request.\n *\n * @param cacheKey - The cache key to look up.\n * @returns The request state, or undefined if not cached.\n */\n getRequestState(cacheKey: string): RequestState | undefined {\n return this.state.requests[cacheKey];\n }\n\n /**\n * Updates the state for a specific request.\n *\n * @param cacheKey - The cache key.\n * @param requestState - The new state for the request.\n */\n #updateRequestState(cacheKey: string, requestState: RequestState): void {\n const maxSize = this.#requestCacheMaxSize;\n\n this.update((state) => {\n const requests = state.requests as unknown as Record<\n string,\n RequestState | undefined\n >;\n requests[cacheKey] = requestState;\n\n // Evict oldest entries if cache exceeds max size\n const keys = Object.keys(requests);\n\n if (keys.length > maxSize) {\n // Sort by timestamp (oldest first)\n const sortedKeys = keys.sort((a, b) => {\n const aTime = requests[a]?.timestamp ?? 0;\n const bTime = requests[b]?.timestamp ?? 0;\n return aTime - bTime;\n });\n\n // Remove oldest entries until we're under the limit\n const entriesToRemove = keys.length - maxSize;\n for (let i = 0; i < entriesToRemove; i++) {\n const keyToRemove = sortedKeys[i];\n if (keyToRemove) {\n delete requests[keyToRemove];\n }\n }\n }\n });\n }\n\n /**\n * Updates the user's geolocation and eligibility.\n * This method calls the RampsService to get the geolocation,\n * then automatically fetches eligibility for that region.\n *\n * @param options - Options for cache behavior.\n * @returns The geolocation string.\n */\n async updateGeolocation(options?: ExecuteRequestOptions): Promise<string> {\n const cacheKey = createCacheKey('updateGeolocation', []);\n\n const geolocation = await this.executeRequest(\n cacheKey,\n async () => {\n const result = await this.messenger.call('RampsService:getGeolocation');\n return result;\n },\n options,\n );\n\n this.update((state) => {\n state.geolocation = geolocation;\n });\n\n if (geolocation) {\n try {\n await this.updateEligibility(geolocation, options);\n } catch {\n // Eligibility fetch failed, but geolocation was successfully fetched and cached.\n // Don't let eligibility errors prevent geolocation state from being updated.\n // Clear eligibility state to avoid showing stale data from a previous location.\n this.update((state) => {\n state.eligibility = null;\n });\n }\n }\n\n return geolocation;\n }\n\n /**\n * Updates the eligibility information for a given region.\n *\n * @param isoCode - The ISO code for the region (e.g., \"us\", \"fr\", \"us-ny\").\n * @param options - Options for cache behavior.\n * @returns The eligibility information.\n */\n async updateEligibility(\n isoCode: string,\n options?: ExecuteRequestOptions,\n ): Promise<Eligibility> {\n const normalizedIsoCode = isoCode.toLowerCase().trim();\n const cacheKey = createCacheKey('updateEligibility', [normalizedIsoCode]);\n\n const eligibility = await this.executeRequest(\n cacheKey,\n async () => {\n return this.messenger.call(\n 'RampsService:getEligibility',\n normalizedIsoCode,\n );\n },\n options,\n );\n\n this.update((state) => {\n if (\n state.geolocation === null ||\n state.geolocation.toLowerCase().trim() === normalizedIsoCode\n ) {\n state.eligibility = eligibility;\n }\n });\n\n return eligibility;\n }\n\n /**\n * Fetches the list of supported countries for a given ramp action.\n *\n * @param action - The ramp action type ('buy' or 'sell').\n * @param options - Options for cache behavior.\n * @returns An array of countries with their eligibility information.\n */\n async getCountries(\n action: 'buy' | 'sell' = 'buy',\n options?: ExecuteRequestOptions,\n ): Promise<Country[]> {\n const cacheKey = createCacheKey('getCountries', [action]);\n\n return this.executeRequest(\n cacheKey,\n async () => {\n return this.messenger.call('RampsService:getCountries', action);\n },\n options,\n );\n }\n}\n"]}
1
+ {"version":3,"file":"RampsController.mjs","sourceRoot":"","sources":["../src/RampsController.ts"],"names":[],"mappings":";;;;;;;;;;;;AAKA,OAAO,EAAE,cAAc,EAAE,kCAAkC;AAgB3D,OAAO,EACL,yBAAyB,EACzB,8BAA8B,EAC9B,cAAc,EACd,cAAc,EACd,kBAAkB,EAClB,kBAAkB,EAClB,gBAAgB,EACjB,2BAAuB;AAExB,kBAAkB;AAElB;;;;GAIG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,iBAAiB,CAAC;AAwBhD;;GAEG;AACH,MAAM,uBAAuB,GAAG;IAC9B,UAAU,EAAE;QACV,OAAO,EAAE,IAAI;QACb,sBAAsB,EAAE,IAAI;QAC5B,kBAAkB,EAAE,IAAI;QACxB,QAAQ,EAAE,IAAI;KACf;IACD,WAAW,EAAE;QACX,OAAO,EAAE,IAAI;QACb,sBAAsB,EAAE,IAAI;QAC5B,kBAAkB,EAAE,IAAI;QACxB,QAAQ,EAAE,IAAI;KACf;IACD,QAAQ,EAAE;QACR,OAAO,EAAE,KAAK;QACd,sBAAsB,EAAE,IAAI;QAC5B,kBAAkB,EAAE,KAAK;QACzB,QAAQ,EAAE,IAAI;KACf;CAC4C,CAAC;AAEhD;;;;;;;GAOG;AACH,MAAM,UAAU,8BAA8B;IAC5C,OAAO;QACL,UAAU,EAAE,IAAI;QAChB,WAAW,EAAE,IAAI;QACjB,QAAQ,EAAE,EAAE;KACb,CAAC;AACJ,CAAC;AAmED,gCAAgC;AAEhC;;GAEG;AACH,MAAM,OAAO,eAAgB,SAAQ,cAIpC;IAiBC;;;;;;;;;OASG;IACH,YAAY,EACV,SAAS,EACT,KAAK,GAAG,EAAE,EACV,eAAe,GAAG,yBAAyB,EAC3C,mBAAmB,GAAG,8BAA8B,GAC7B;QACvB,KAAK,CAAC;YACJ,SAAS;YACT,QAAQ,EAAE,uBAAuB;YACjC,IAAI,EAAE,cAAc;YACpB,KAAK,EAAE;gBACL,GAAG,8BAA8B,EAAE;gBACnC,GAAG,KAAK;gBACR,gEAAgE;gBAChE,QAAQ,EAAE,EAAE;aACb;SACF,CAAC,CAAC;;QA1CL;;WAEG;QACM,mDAAyB;QAElC;;WAEG;QACM,uDAA6B;QAEtC;;;WAGG;QACM,2CAAgD,IAAI,GAAG,EAAE,EAAC;QA8BjE,uBAAA,IAAI,oCAAoB,eAAe,MAAA,CAAC;QACxC,uBAAA,IAAI,wCAAwB,mBAAmB,MAAA,CAAC;IAClD,CAAC;IAED;;;;;;;;;;;OAWG;IACH,KAAK,CAAC,cAAc,CAClB,QAAgB,EAChB,OAAkD,EAClD,OAA+B;QAE/B,MAAM,GAAG,GAAG,OAAO,EAAE,GAAG,IAAI,uBAAA,IAAI,wCAAiB,CAAC;QAElD,6EAA6E;QAC7E,MAAM,OAAO,GAAG,uBAAA,IAAI,wCAAiB,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACpD,IAAI,OAAO,EAAE,CAAC;YACZ,OAAO,OAAO,CAAC,OAA2B,CAAC;QAC7C,CAAC;QAED,8CAA8C;QAC9C,IAAI,CAAC,OAAO,EAAE,YAAY,EAAE,CAAC;YAC3B,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;YAC7C,IAAI,MAAM,IAAI,CAAC,cAAc,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,CAAC;gBAC3C,OAAO,MAAM,CAAC,IAAe,CAAC;YAChC,CAAC;QACH,CAAC;QAED,2CAA2C;QAC3C,MAAM,eAAe,GAAG,IAAI,eAAe,EAAE,CAAC;QAC9C,MAAM,aAAa,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAEjC,0BAA0B;QAC1B,uBAAA,IAAI,uEAAoB,MAAxB,IAAI,EAAqB,QAAQ,EAAE,kBAAkB,EAAE,CAAC,CAAC;QAEzD,2BAA2B;QAC3B,MAAM,OAAO,GAAG,CAAC,KAAK,IAAsB,EAAE;YAC5C,IAAI,CAAC;gBACH,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC;gBAEnD,gCAAgC;gBAChC,IAAI,eAAe,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;oBACnC,MAAM,IAAI,KAAK,CAAC,qBAAqB,CAAC,CAAC;gBACzC,CAAC;gBAED,uBAAA,IAAI,uEAAoB,MAAxB,IAAI,EACF,QAAQ,EACR,kBAAkB,CAAC,IAAY,EAAE,aAAa,CAAC,CAChD,CAAC;gBACF,OAAO,IAAI,CAAC;YACd,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,gCAAgC;gBAChC,IAAI,eAAe,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;oBACnC,MAAM,KAAK,CAAC;gBACd,CAAC;gBAED,MAAM,YAAY,GAAI,KAAe,EAAE,OAAO,CAAC;gBAE/C,uBAAA,IAAI,uEAAoB,MAAxB,IAAI,EACF,QAAQ,EACR,gBAAgB,CAAC,YAAY,IAAI,eAAe,EAAE,aAAa,CAAC,CACjE,CAAC;gBACF,MAAM,KAAK,CAAC;YACd,CAAC;oBAAS,CAAC;gBACT,yEAAyE;gBACzE,MAAM,cAAc,GAAG,uBAAA,IAAI,wCAAiB,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;gBAC3D,IAAI,cAAc,EAAE,eAAe,KAAK,eAAe,EAAE,CAAC;oBACxD,uBAAA,IAAI,wCAAiB,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;gBACzC,CAAC;YACH,CAAC;QACH,CAAC,CAAC,EAAE,CAAC;QAEL,0CAA0C;QAC1C,uBAAA,IAAI,wCAAiB,CAAC,GAAG,CAAC,QAAQ,EAAE,EAAE,OAAO,EAAE,eAAe,EAAE,CAAC,CAAC;QAElE,OAAO,OAAO,CAAC;IACjB,CAAC;IAED;;;;;OAKG;IACH,YAAY,CAAC,QAAgB;QAC3B,MAAM,OAAO,GAAG,uBAAA,IAAI,wCAAiB,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACpD,IAAI,OAAO,EAAE,CAAC;YACZ,OAAO,CAAC,eAAe,CAAC,KAAK,EAAE,CAAC;YAChC,uBAAA,IAAI,wCAAiB,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;YACvC,uBAAA,IAAI,uEAAoB,MAAxB,IAAI,EAAqB,QAAQ,CAAC,CAAC;YACnC,OAAO,IAAI,CAAC;QACd,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IAiBD;;;;;OAKG;IACH,eAAe,CAAC,QAAgB;QAC9B,OAAO,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IACvC,CAAC;IAyCD;;;;;;;OAOG;IACH,KAAK,CAAC,gBAAgB,CAAC,OAA+B;QACpD,MAAM,QAAQ,GAAG,cAAc,CAAC,kBAAkB,EAAE,EAAE,CAAC,CAAC;QAExD,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC,cAAc,CAC1C,QAAQ,EACR,KAAK,IAAI,EAAE;YACT,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,6BAA6B,CAAC,CAAC;YACxE,OAAO,MAAM,CAAC;QAChB,CAAC,EACD,OAAO,CACR,CAAC;QAEF,MAAM,gBAAgB,GAAG,UAAU;YACjC,CAAC,CAAC,UAAU,CAAC,WAAW,EAAE,CAAC,IAAI,EAAE;YACjC,CAAC,CAAC,UAAU,CAAC;QAEf,IAAI,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE;YACpB,KAAK,CAAC,UAAU,GAAG,gBAAgB,CAAC;QACtC,CAAC,CAAC,CAAC;QAEH,IAAI,gBAAgB,EAAE,CAAC;YACrB,IAAI,CAAC;gBACH,MAAM,IAAI,CAAC,iBAAiB,CAAC,gBAAgB,EAAE,OAAO,CAAC,CAAC;YAC1D,CAAC;YAAC,MAAM,CAAC;gBACP,IAAI,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE;oBACpB,MAAM,iBAAiB,GAAG,KAAK,CAAC,UAAU,EAAE,WAAW,EAAE,CAAC,IAAI,EAAE,CAAC;oBACjE,IAAI,iBAAiB,KAAK,gBAAgB,EAAE,CAAC;wBAC3C,KAAK,CAAC,WAAW,GAAG,IAAI,CAAC;oBAC3B,CAAC;gBACH,CAAC,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QAED,OAAO,gBAAgB,CAAC;IAC1B,CAAC;IAED;;;;;;;OAOG;IACH,KAAK,CAAC,aAAa,CACjB,MAAc,EACd,OAA+B;QAE/B,MAAM,gBAAgB,GAAG,MAAM,CAAC,WAAW,EAAE,CAAC,IAAI,EAAE,CAAC;QAErD,IAAI,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE;YACpB,KAAK,CAAC,UAAU,GAAG,gBAAgB,CAAC;QACtC,CAAC,CAAC,CAAC;QAEH,IAAI,CAAC;YACH,OAAO,MAAM,IAAI,CAAC,iBAAiB,CAAC,gBAAgB,EAAE,OAAO,CAAC,CAAC;QACjE,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,kEAAkE;YAClE,6EAA6E;YAC7E,gFAAgF;YAChF,gFAAgF;YAChF,uCAAuC;YACvC,IAAI,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE;gBACpB,MAAM,iBAAiB,GAAG,KAAK,CAAC,UAAU,EAAE,WAAW,EAAE,CAAC,IAAI,EAAE,CAAC;gBACjE,IAAI,iBAAiB,KAAK,gBAAgB,EAAE,CAAC;oBAC3C,KAAK,CAAC,WAAW,GAAG,IAAI,CAAC;gBAC3B,CAAC;YACH,CAAC,CAAC,CAAC;YACH,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;IAED;;;;;;OAMG;IACH,KAAK,CAAC,IAAI,CAAC,OAA+B;QACxC,MAAM,IAAI,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE;YAC9C,yEAAyE;QAC3E,CAAC,CAAC,CAAC;IACL,CAAC;IAED;;;;;;OAMG;IACH,KAAK,CAAC,iBAAiB,CACrB,OAAe,EACf,OAA+B;QAE/B,MAAM,iBAAiB,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,IAAI,EAAE,CAAC;QACvD,MAAM,QAAQ,GAAG,cAAc,CAAC,mBAAmB,EAAE,CAAC,iBAAiB,CAAC,CAAC,CAAC;QAE1E,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC,cAAc,CAC3C,QAAQ,EACR,KAAK,IAAI,EAAE;YACT,OAAO,IAAI,CAAC,SAAS,CAAC,IAAI,CACxB,6BAA6B,EAC7B,iBAAiB,CAClB,CAAC;QACJ,CAAC,EACD,OAAO,CACR,CAAC;QAEF,IAAI,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE;YACpB,MAAM,UAAU,GAAG,KAAK,CAAC,UAAU,EAAE,WAAW,EAAE,CAAC,IAAI,EAAE,CAAC;YAE1D,IAAI,UAAU,KAAK,SAAS,IAAI,UAAU,KAAK,iBAAiB,EAAE,CAAC;gBACjE,KAAK,CAAC,WAAW,GAAG,WAAW,CAAC;YAClC,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,OAAO,WAAW,CAAC;IACrB,CAAC;IAED;;;;;;OAMG;IACH,KAAK,CAAC,YAAY,CAChB,SAAyB,KAAK,EAC9B,OAA+B;QAE/B,MAAM,QAAQ,GAAG,cAAc,CAAC,cAAc,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC;QAE1D,OAAO,IAAI,CAAC,cAAc,CACxB,QAAQ,EACR,KAAK,IAAI,EAAE;YACT,OAAO,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,2BAA2B,EAAE,MAAM,CAAC,CAAC;QAClE,CAAC,EACD,OAAO,CACR,CAAC;IACJ,CAAC;CACF;yRAjNqB,QAAgB;IAClC,IAAI,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE;QACpB,MAAM,QAAQ,GAAG,KAAK,CAAC,QAGtB,CAAC;QACF,OAAO,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAC5B,CAAC,CAAC,CAAC;AACL,CAAC,qFAkBmB,QAAgB,EAAE,YAA0B;IAC9D,MAAM,OAAO,GAAG,uBAAA,IAAI,4CAAqB,CAAC;IAE1C,IAAI,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE;QACpB,MAAM,QAAQ,GAAG,KAAK,CAAC,QAGtB,CAAC;QACF,QAAQ,CAAC,QAAQ,CAAC,GAAG,YAAY,CAAC;QAElC,iDAAiD;QACjD,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAEnC,IAAI,IAAI,CAAC,MAAM,GAAG,OAAO,EAAE,CAAC;YAC1B,mCAAmC;YACnC,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;gBACpC,MAAM,KAAK,GAAG,QAAQ,CAAC,CAAC,CAAC,EAAE,SAAS,IAAI,CAAC,CAAC;gBAC1C,MAAM,KAAK,GAAG,QAAQ,CAAC,CAAC,CAAC,EAAE,SAAS,IAAI,CAAC,CAAC;gBAC1C,OAAO,KAAK,GAAG,KAAK,CAAC;YACvB,CAAC,CAAC,CAAC;YAEH,oDAAoD;YACpD,MAAM,eAAe,GAAG,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC;YAC9C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,eAAe,EAAE,CAAC,EAAE,EAAE,CAAC;gBACzC,MAAM,WAAW,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC;gBAClC,IAAI,WAAW,EAAE,CAAC;oBAChB,OAAO,QAAQ,CAAC,WAAW,CAAC,CAAC;gBAC/B,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC","sourcesContent":["import type {\n ControllerGetStateAction,\n ControllerStateChangeEvent,\n StateMetadata,\n} from '@metamask/base-controller';\nimport { BaseController } from '@metamask/base-controller';\nimport type { Messenger } from '@metamask/messenger';\nimport type { Json } from '@metamask/utils';\n\nimport type { Country, Eligibility } from './RampsService';\nimport type {\n RampsServiceGetGeolocationAction,\n RampsServiceGetCountriesAction,\n RampsServiceGetEligibilityAction,\n} from './RampsService-method-action-types';\nimport type {\n RequestCache as RequestCacheType,\n RequestState,\n ExecuteRequestOptions,\n PendingRequest,\n} from './RequestCache';\nimport {\n DEFAULT_REQUEST_CACHE_TTL,\n DEFAULT_REQUEST_CACHE_MAX_SIZE,\n createCacheKey,\n isCacheExpired,\n createLoadingState,\n createSuccessState,\n createErrorState,\n} from './RequestCache';\n\n// === GENERAL ===\n\n/**\n * The name of the {@link RampsController}, used to namespace the\n * controller's actions and events and to namespace the controller's state data\n * when composed with other controllers.\n */\nexport const controllerName = 'RampsController';\n\n// === STATE ===\n\n/**\n * Describes the shape of the state object for {@link RampsController}.\n */\nexport type RampsControllerState = {\n /**\n * The user's selected region code (e.g., \"US-CA\").\n * Initially set via geolocation fetch, but can be manually changed by the user.\n */\n userRegion: string | null;\n /**\n * Eligibility information for the user's current region.\n */\n eligibility: Eligibility | null;\n /**\n * Cache of request states, keyed by cache key.\n * This stores loading, success, and error states for API requests.\n */\n requests: RequestCacheType;\n};\n\n/**\n * The metadata for each property in {@link RampsControllerState}.\n */\nconst rampsControllerMetadata = {\n userRegion: {\n persist: true,\n includeInDebugSnapshot: true,\n includeInStateLogs: true,\n usedInUi: true,\n },\n eligibility: {\n persist: true,\n includeInDebugSnapshot: true,\n includeInStateLogs: true,\n usedInUi: true,\n },\n requests: {\n persist: false,\n includeInDebugSnapshot: true,\n includeInStateLogs: false,\n usedInUi: true,\n },\n} satisfies StateMetadata<RampsControllerState>;\n\n/**\n * Constructs the default {@link RampsController} state. This allows\n * consumers to provide a partial state object when initializing the controller\n * and also helps in constructing complete state objects for this controller in\n * tests.\n *\n * @returns The default {@link RampsController} state.\n */\nexport function getDefaultRampsControllerState(): RampsControllerState {\n return {\n userRegion: null,\n eligibility: null,\n requests: {},\n };\n}\n\n// === MESSENGER ===\n\n/**\n * Retrieves the state of the {@link RampsController}.\n */\nexport type RampsControllerGetStateAction = ControllerGetStateAction<\n typeof controllerName,\n RampsControllerState\n>;\n\n/**\n * Actions that {@link RampsControllerMessenger} exposes to other consumers.\n */\nexport type RampsControllerActions = RampsControllerGetStateAction;\n\n/**\n * Actions from other messengers that {@link RampsController} calls.\n */\ntype AllowedActions =\n | RampsServiceGetGeolocationAction\n | RampsServiceGetCountriesAction\n | RampsServiceGetEligibilityAction;\n\n/**\n * Published when the state of {@link RampsController} changes.\n */\nexport type RampsControllerStateChangeEvent = ControllerStateChangeEvent<\n typeof controllerName,\n RampsControllerState\n>;\n\n/**\n * Events that {@link RampsControllerMessenger} exposes to other consumers.\n */\nexport type RampsControllerEvents = RampsControllerStateChangeEvent;\n\n/**\n * Events from other messengers that {@link RampsController} subscribes to.\n */\ntype AllowedEvents = never;\n\n/**\n * The messenger restricted to actions and events accessed by\n * {@link RampsController}.\n */\nexport type RampsControllerMessenger = Messenger<\n typeof controllerName,\n RampsControllerActions | AllowedActions,\n RampsControllerEvents | AllowedEvents\n>;\n\n/**\n * Configuration options for the RampsController.\n */\nexport type RampsControllerOptions = {\n /** The messenger suited for this controller. */\n messenger: RampsControllerMessenger;\n /** The desired state with which to initialize this controller. */\n state?: Partial<RampsControllerState>;\n /** Time to live for cached requests in milliseconds. Defaults to 15 minutes. */\n requestCacheTTL?: number;\n /** Maximum number of entries in the request cache. Defaults to 250. */\n requestCacheMaxSize?: number;\n};\n\n// === CONTROLLER DEFINITION ===\n\n/**\n * Manages cryptocurrency on/off ramps functionality.\n */\nexport class RampsController extends BaseController<\n typeof controllerName,\n RampsControllerState,\n RampsControllerMessenger\n> {\n /**\n * Default TTL for cached requests.\n */\n readonly #requestCacheTTL: number;\n\n /**\n * Maximum number of entries in the request cache.\n */\n readonly #requestCacheMaxSize: number;\n\n /**\n * Map of pending requests for deduplication.\n * Key is the cache key, value is the pending request with abort controller.\n */\n readonly #pendingRequests: Map<string, PendingRequest> = new Map();\n\n /**\n * Constructs a new {@link RampsController}.\n *\n * @param args - The constructor arguments.\n * @param args.messenger - The messenger suited for this controller.\n * @param args.state - The desired state with which to initialize this\n * controller. Missing properties will be filled in with defaults.\n * @param args.requestCacheTTL - Time to live for cached requests in milliseconds.\n * @param args.requestCacheMaxSize - Maximum number of entries in the request cache.\n */\n constructor({\n messenger,\n state = {},\n requestCacheTTL = DEFAULT_REQUEST_CACHE_TTL,\n requestCacheMaxSize = DEFAULT_REQUEST_CACHE_MAX_SIZE,\n }: RampsControllerOptions) {\n super({\n messenger,\n metadata: rampsControllerMetadata,\n name: controllerName,\n state: {\n ...getDefaultRampsControllerState(),\n ...state,\n // Always reset requests cache on initialization (non-persisted)\n requests: {},\n },\n });\n\n this.#requestCacheTTL = requestCacheTTL;\n this.#requestCacheMaxSize = requestCacheMaxSize;\n }\n\n /**\n * Executes a request with caching and deduplication.\n *\n * If a request with the same cache key is already in flight, returns the\n * existing promise. If valid cached data exists, returns it without making\n * a new request.\n *\n * @param cacheKey - Unique identifier for this request.\n * @param fetcher - Function that performs the actual fetch. Receives an AbortSignal.\n * @param options - Options for cache behavior.\n * @returns The result of the request.\n */\n async executeRequest<TResult>(\n cacheKey: string,\n fetcher: (signal: AbortSignal) => Promise<TResult>,\n options?: ExecuteRequestOptions,\n ): Promise<TResult> {\n const ttl = options?.ttl ?? this.#requestCacheTTL;\n\n // Check for existing pending request - join it instead of making a duplicate\n const pending = this.#pendingRequests.get(cacheKey);\n if (pending) {\n return pending.promise as Promise<TResult>;\n }\n\n // Check cache validity (unless force refresh)\n if (!options?.forceRefresh) {\n const cached = this.state.requests[cacheKey];\n if (cached && !isCacheExpired(cached, ttl)) {\n return cached.data as TResult;\n }\n }\n\n // Create abort controller for this request\n const abortController = new AbortController();\n const lastFetchedAt = Date.now();\n\n // Update state to loading\n this.#updateRequestState(cacheKey, createLoadingState());\n\n // Create the fetch promise\n const promise = (async (): Promise<TResult> => {\n try {\n const data = await fetcher(abortController.signal);\n\n // Don't update state if aborted\n if (abortController.signal.aborted) {\n throw new Error('Request was aborted');\n }\n\n this.#updateRequestState(\n cacheKey,\n createSuccessState(data as Json, lastFetchedAt),\n );\n return data;\n } catch (error) {\n // Don't update state if aborted\n if (abortController.signal.aborted) {\n throw error;\n }\n\n const errorMessage = (error as Error)?.message;\n\n this.#updateRequestState(\n cacheKey,\n createErrorState(errorMessage ?? 'Unknown error', lastFetchedAt),\n );\n throw error;\n } finally {\n // Only delete if this is still our entry (not replaced by a new request)\n const currentPending = this.#pendingRequests.get(cacheKey);\n if (currentPending?.abortController === abortController) {\n this.#pendingRequests.delete(cacheKey);\n }\n }\n })();\n\n // Store pending request for deduplication\n this.#pendingRequests.set(cacheKey, { promise, abortController });\n\n return promise;\n }\n\n /**\n * Aborts a pending request if one exists.\n *\n * @param cacheKey - The cache key of the request to abort.\n * @returns True if a request was aborted.\n */\n abortRequest(cacheKey: string): boolean {\n const pending = this.#pendingRequests.get(cacheKey);\n if (pending) {\n pending.abortController.abort();\n this.#pendingRequests.delete(cacheKey);\n this.#removeRequestState(cacheKey);\n return true;\n }\n return false;\n }\n\n /**\n * Removes a request state from the cache.\n *\n * @param cacheKey - The cache key to remove.\n */\n #removeRequestState(cacheKey: string): void {\n this.update((state) => {\n const requests = state.requests as unknown as Record<\n string,\n RequestState | undefined\n >;\n delete requests[cacheKey];\n });\n }\n\n /**\n * Gets the state of a specific cached request.\n *\n * @param cacheKey - The cache key to look up.\n * @returns The request state, or undefined if not cached.\n */\n getRequestState(cacheKey: string): RequestState | undefined {\n return this.state.requests[cacheKey];\n }\n\n /**\n * Updates the state for a specific request.\n *\n * @param cacheKey - The cache key.\n * @param requestState - The new state for the request.\n */\n #updateRequestState(cacheKey: string, requestState: RequestState): void {\n const maxSize = this.#requestCacheMaxSize;\n\n this.update((state) => {\n const requests = state.requests as unknown as Record<\n string,\n RequestState | undefined\n >;\n requests[cacheKey] = requestState;\n\n // Evict oldest entries if cache exceeds max size\n const keys = Object.keys(requests);\n\n if (keys.length > maxSize) {\n // Sort by timestamp (oldest first)\n const sortedKeys = keys.sort((a, b) => {\n const aTime = requests[a]?.timestamp ?? 0;\n const bTime = requests[b]?.timestamp ?? 0;\n return aTime - bTime;\n });\n\n // Remove oldest entries until we're under the limit\n const entriesToRemove = keys.length - maxSize;\n for (let i = 0; i < entriesToRemove; i++) {\n const keyToRemove = sortedKeys[i];\n if (keyToRemove) {\n delete requests[keyToRemove];\n }\n }\n }\n });\n }\n\n /**\n * Updates the user's region by fetching geolocation and eligibility.\n * This method calls the RampsService to get the geolocation,\n * then automatically fetches eligibility for that region.\n *\n * @param options - Options for cache behavior.\n * @returns The user region string.\n */\n async updateUserRegion(options?: ExecuteRequestOptions): Promise<string> {\n const cacheKey = createCacheKey('updateUserRegion', []);\n\n const userRegion = await this.executeRequest(\n cacheKey,\n async () => {\n const result = await this.messenger.call('RampsService:getGeolocation');\n return result;\n },\n options,\n );\n\n const normalizedRegion = userRegion\n ? userRegion.toLowerCase().trim()\n : userRegion;\n\n this.update((state) => {\n state.userRegion = normalizedRegion;\n });\n\n if (normalizedRegion) {\n try {\n await this.updateEligibility(normalizedRegion, options);\n } catch {\n this.update((state) => {\n const currentUserRegion = state.userRegion?.toLowerCase().trim();\n if (currentUserRegion === normalizedRegion) {\n state.eligibility = null;\n }\n });\n }\n }\n\n return normalizedRegion;\n }\n\n /**\n * Sets the user's region manually (without fetching geolocation).\n * This allows users to override the detected region.\n *\n * @param region - The region code to set (e.g., \"US-CA\").\n * @param options - Options for cache behavior when fetching eligibility.\n * @returns The eligibility information for the region.\n */\n async setUserRegion(\n region: string,\n options?: ExecuteRequestOptions,\n ): Promise<Eligibility> {\n const normalizedRegion = region.toLowerCase().trim();\n\n this.update((state) => {\n state.userRegion = normalizedRegion;\n });\n\n try {\n return await this.updateEligibility(normalizedRegion, options);\n } catch (error) {\n // Eligibility fetch failed, but user region was successfully set.\n // Don't let eligibility errors prevent user region state from being updated.\n // Clear eligibility state to avoid showing stale data from a previous location.\n // Only clear if the region still matches to avoid race conditions where a newer\n // region change has already succeeded.\n this.update((state) => {\n const currentUserRegion = state.userRegion?.toLowerCase().trim();\n if (currentUserRegion === normalizedRegion) {\n state.eligibility = null;\n }\n });\n throw error;\n }\n }\n\n /**\n * Initializes the controller by fetching the user's region from geolocation.\n * This should be called once at app startup to set up the initial region.\n *\n * @param options - Options for cache behavior.\n * @returns Promise that resolves when initialization is complete.\n */\n async init(options?: ExecuteRequestOptions): Promise<void> {\n await this.updateUserRegion(options).catch(() => {\n // User region fetch failed - error state will be available via selectors\n });\n }\n\n /**\n * Updates the eligibility information for a given region.\n *\n * @param isoCode - The ISO code for the region (e.g., \"us\", \"fr\", \"us-ny\").\n * @param options - Options for cache behavior.\n * @returns The eligibility information.\n */\n async updateEligibility(\n isoCode: string,\n options?: ExecuteRequestOptions,\n ): Promise<Eligibility> {\n const normalizedIsoCode = isoCode.toLowerCase().trim();\n const cacheKey = createCacheKey('updateEligibility', [normalizedIsoCode]);\n\n const eligibility = await this.executeRequest(\n cacheKey,\n async () => {\n return this.messenger.call(\n 'RampsService:getEligibility',\n normalizedIsoCode,\n );\n },\n options,\n );\n\n this.update((state) => {\n const userRegion = state.userRegion?.toLowerCase().trim();\n\n if (userRegion === undefined || userRegion === normalizedIsoCode) {\n state.eligibility = eligibility;\n }\n });\n\n return eligibility;\n }\n\n /**\n * Fetches the list of supported countries for a given ramp action.\n *\n * @param action - The ramp action type ('buy' or 'sell').\n * @param options - Options for cache behavior.\n * @returns An array of countries with their eligibility information.\n */\n async getCountries(\n action: 'buy' | 'sell' = 'buy',\n options?: ExecuteRequestOptions,\n ): Promise<Country[]> {\n const cacheKey = createCacheKey('getCountries', [action]);\n\n return this.executeRequest(\n cacheKey,\n async () => {\n return this.messenger.call('RampsService:getCountries', action);\n },\n options,\n );\n }\n}\n"]}
@@ -15,7 +15,7 @@ const RequestCache_1 = require("./RequestCache.cjs");
15
15
  *
16
16
  * @param getState - Function that extracts RampsControllerState from the root state.
17
17
  * Typically a reselect selector like `selectRampsControllerState`.
18
- * @param method - The controller method name (e.g., 'updateGeolocation').
18
+ * @param method - The controller method name (e.g., 'updateUserRegion').
19
19
  * @param params - The parameters passed to the method, used to generate the cache key.
20
20
  * Must match the params used when calling the controller method.
21
21
  * @returns A selector function that returns `{ data, isFetching, error }`.
@@ -31,14 +31,14 @@ const RequestCache_1 = require("./RequestCache.cjs");
31
31
  * (rampsControllerState) => rampsControllerState,
32
32
  * );
33
33
  *
34
- * export const selectGeolocationRequest = createRequestSelector<
34
+ * export const selectUserRegionRequest = createRequestSelector<
35
35
  * RootState,
36
36
  * string
37
- * >(selectRampsControllerState, 'updateGeolocation', []);
37
+ * >(selectRampsControllerState, 'updateUserRegion', []);
38
38
  *
39
39
  * // In hook - use directly with useSelector, no shallowEqual needed
40
- * export function useRampsGeolocation() {
41
- * const { isFetching, error } = useSelector(selectGeolocationRequest);
40
+ * export function useRampsUserRegion() {
41
+ * const { isFetching, error } = useSelector(selectUserRegionRequest);
42
42
  * // ... rest of hook
43
43
  * }
44
44
  * ```
@@ -1 +1 @@
1
- {"version":3,"file":"selectors.cjs","sourceRoot":"","sources":["../src/selectors.ts"],"names":[],"mappings":";;;AAEA,qDAA+D;AAkB/D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwDG;AACH,SAAgB,qBAAqB,CACnC,QAAqE,EACrE,MAAc,EACd,MAAiB;IAEjB,MAAM,QAAQ,GAAG,IAAA,6BAAc,EAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAEhD,IAAI,WAAqC,CAAC;IAC1C,IAAI,UAAU,GAAwC,IAAI,CAAC;IAE3D,OAAO,CAAC,KAAiB,EAAgC,EAAE;QACzD,MAAM,OAAO,GAAG,QAAQ,CAAC,KAAK,CAAC,EAAE,QAAQ,EAAE,CAAC,QAAQ,CAAC,CAAC;QAEtD,IAAI,OAAO,KAAK,WAAW,IAAI,UAAU,KAAK,IAAI,EAAE,CAAC;YACnD,OAAO,UAAU,CAAC;QACpB,CAAC;QAED,WAAW,GAAG,OAAO,CAAC;QACtB,UAAU,GAAG;YACX,IAAI,EAAG,OAAO,EAAE,IAAc,IAAI,IAAI;YACtC,UAAU,EAAE,OAAO,EAAE,MAAM,KAAK,4BAAa,CAAC,OAAO;YACrD,KAAK,EAAE,OAAO,EAAE,KAAK,IAAI,IAAI;SAC9B,CAAC;QAEF,OAAO,UAAU,CAAC;IACpB,CAAC,CAAC;AACJ,CAAC;AA1BD,sDA0BC","sourcesContent":["import type { RampsControllerState } from './RampsController';\nimport type { RequestState } from './RequestCache';\nimport { RequestStatus, createCacheKey } from './RequestCache';\n\n/**\n * Result shape returned by request selectors.\n *\n * This object is memoized - the same reference is returned when the underlying\n * request state hasn't changed, making it safe to use with React Redux's\n * `useSelector` without `shallowEqual`.\n */\nexport type RequestSelectorResult<TData> = {\n /** The data returned by the request, or null if not yet loaded or on error. */\n data: TData | null;\n /** Whether the request is currently in progress. */\n isFetching: boolean;\n /** Error message if the request failed, or null if successful or not yet attempted. */\n error: string | null;\n};\n\n/**\n * Creates a memoized selector for a controller method's request state.\n *\n * This selector tracks the loading, error, and data state for a specific\n * controller method call. It's optimized for use with React Redux's `useSelector`\n * hook - the selector returns the same object reference when the underlying\n * request state hasn't changed, so no `shallowEqual` is needed.\n *\n * The selector uses reference equality on the request object itself, so it\n * works correctly with arrays and objects without expensive deep equality checks.\n *\n * @param getState - Function that extracts RampsControllerState from the root state.\n * Typically a reselect selector like `selectRampsControllerState`.\n * @param method - The controller method name (e.g., 'updateGeolocation').\n * @param params - The parameters passed to the method, used to generate the cache key.\n * Must match the params used when calling the controller method.\n * @returns A selector function that returns `{ data, isFetching, error }`.\n *\n * @example\n * ```ts\n * // In selectors file - create once at module level\n * import { createRequestSelector } from '@metamask/ramps-controller';\n * import { createSelector } from 'reselect';\n *\n * const selectRampsControllerState = createSelector(\n * (state: RootState) => state.engine.backgroundState.RampsController,\n * (rampsControllerState) => rampsControllerState,\n * );\n *\n * export const selectGeolocationRequest = createRequestSelector<\n * RootState,\n * string\n * >(selectRampsControllerState, 'updateGeolocation', []);\n *\n * // In hook - use directly with useSelector, no shallowEqual needed\n * export function useRampsGeolocation() {\n * const { isFetching, error } = useSelector(selectGeolocationRequest);\n * // ... rest of hook\n * }\n * ```\n *\n * @example\n * ```ts\n * // For methods with parameters\n * export const selectCryptoCurrenciesRequest = (region: string) =>\n * createRequestSelector<RootState, CryptoCurrency[]>(\n * selectRampsControllerState,\n * 'getCryptoCurrencies',\n * [region],\n * );\n *\n * // In component\n * const { data, isFetching, error } = useSelector(\n * selectCryptoCurrenciesRequest('US')\n * );\n * ```\n */\nexport function createRequestSelector<TRootState, TData>(\n getState: (rootState: TRootState) => RampsControllerState | undefined,\n method: string,\n params: unknown[],\n): (state: TRootState) => RequestSelectorResult<TData> {\n const cacheKey = createCacheKey(method, params);\n\n let lastRequest: RequestState | undefined;\n let lastResult: RequestSelectorResult<TData> | null = null;\n\n return (state: TRootState): RequestSelectorResult<TData> => {\n const request = getState(state)?.requests?.[cacheKey];\n\n if (request === lastRequest && lastResult !== null) {\n return lastResult;\n }\n\n lastRequest = request;\n lastResult = {\n data: (request?.data as TData) ?? null,\n isFetching: request?.status === RequestStatus.LOADING,\n error: request?.error ?? null,\n };\n\n return lastResult;\n };\n}\n"]}
1
+ {"version":3,"file":"selectors.cjs","sourceRoot":"","sources":["../src/selectors.ts"],"names":[],"mappings":";;;AAEA,qDAA+D;AAkB/D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwDG;AACH,SAAgB,qBAAqB,CACnC,QAAqE,EACrE,MAAc,EACd,MAAiB;IAEjB,MAAM,QAAQ,GAAG,IAAA,6BAAc,EAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAEhD,IAAI,WAAqC,CAAC;IAC1C,IAAI,UAAU,GAAwC,IAAI,CAAC;IAE3D,OAAO,CAAC,KAAiB,EAAgC,EAAE;QACzD,MAAM,OAAO,GAAG,QAAQ,CAAC,KAAK,CAAC,EAAE,QAAQ,EAAE,CAAC,QAAQ,CAAC,CAAC;QAEtD,IAAI,OAAO,KAAK,WAAW,IAAI,UAAU,KAAK,IAAI,EAAE,CAAC;YACnD,OAAO,UAAU,CAAC;QACpB,CAAC;QAED,WAAW,GAAG,OAAO,CAAC;QACtB,UAAU,GAAG;YACX,IAAI,EAAG,OAAO,EAAE,IAAc,IAAI,IAAI;YACtC,UAAU,EAAE,OAAO,EAAE,MAAM,KAAK,4BAAa,CAAC,OAAO;YACrD,KAAK,EAAE,OAAO,EAAE,KAAK,IAAI,IAAI;SAC9B,CAAC;QAEF,OAAO,UAAU,CAAC;IACpB,CAAC,CAAC;AACJ,CAAC;AA1BD,sDA0BC","sourcesContent":["import type { RampsControllerState } from './RampsController';\nimport type { RequestState } from './RequestCache';\nimport { RequestStatus, createCacheKey } from './RequestCache';\n\n/**\n * Result shape returned by request selectors.\n *\n * This object is memoized - the same reference is returned when the underlying\n * request state hasn't changed, making it safe to use with React Redux's\n * `useSelector` without `shallowEqual`.\n */\nexport type RequestSelectorResult<TData> = {\n /** The data returned by the request, or null if not yet loaded or on error. */\n data: TData | null;\n /** Whether the request is currently in progress. */\n isFetching: boolean;\n /** Error message if the request failed, or null if successful or not yet attempted. */\n error: string | null;\n};\n\n/**\n * Creates a memoized selector for a controller method's request state.\n *\n * This selector tracks the loading, error, and data state for a specific\n * controller method call. It's optimized for use with React Redux's `useSelector`\n * hook - the selector returns the same object reference when the underlying\n * request state hasn't changed, so no `shallowEqual` is needed.\n *\n * The selector uses reference equality on the request object itself, so it\n * works correctly with arrays and objects without expensive deep equality checks.\n *\n * @param getState - Function that extracts RampsControllerState from the root state.\n * Typically a reselect selector like `selectRampsControllerState`.\n * @param method - The controller method name (e.g., 'updateUserRegion').\n * @param params - The parameters passed to the method, used to generate the cache key.\n * Must match the params used when calling the controller method.\n * @returns A selector function that returns `{ data, isFetching, error }`.\n *\n * @example\n * ```ts\n * // In selectors file - create once at module level\n * import { createRequestSelector } from '@metamask/ramps-controller';\n * import { createSelector } from 'reselect';\n *\n * const selectRampsControllerState = createSelector(\n * (state: RootState) => state.engine.backgroundState.RampsController,\n * (rampsControllerState) => rampsControllerState,\n * );\n *\n * export const selectUserRegionRequest = createRequestSelector<\n * RootState,\n * string\n * >(selectRampsControllerState, 'updateUserRegion', []);\n *\n * // In hook - use directly with useSelector, no shallowEqual needed\n * export function useRampsUserRegion() {\n * const { isFetching, error } = useSelector(selectUserRegionRequest);\n * // ... rest of hook\n * }\n * ```\n *\n * @example\n * ```ts\n * // For methods with parameters\n * export const selectCryptoCurrenciesRequest = (region: string) =>\n * createRequestSelector<RootState, CryptoCurrency[]>(\n * selectRampsControllerState,\n * 'getCryptoCurrencies',\n * [region],\n * );\n *\n * // In component\n * const { data, isFetching, error } = useSelector(\n * selectCryptoCurrenciesRequest('US')\n * );\n * ```\n */\nexport function createRequestSelector<TRootState, TData>(\n getState: (rootState: TRootState) => RampsControllerState | undefined,\n method: string,\n params: unknown[],\n): (state: TRootState) => RequestSelectorResult<TData> {\n const cacheKey = createCacheKey(method, params);\n\n let lastRequest: RequestState | undefined;\n let lastResult: RequestSelectorResult<TData> | null = null;\n\n return (state: TRootState): RequestSelectorResult<TData> => {\n const request = getState(state)?.requests?.[cacheKey];\n\n if (request === lastRequest && lastResult !== null) {\n return lastResult;\n }\n\n lastRequest = request;\n lastResult = {\n data: (request?.data as TData) ?? null,\n isFetching: request?.status === RequestStatus.LOADING,\n error: request?.error ?? null,\n };\n\n return lastResult;\n };\n}\n"]}
@@ -27,7 +27,7 @@ export type RequestSelectorResult<TData> = {
27
27
  *
28
28
  * @param getState - Function that extracts RampsControllerState from the root state.
29
29
  * Typically a reselect selector like `selectRampsControllerState`.
30
- * @param method - The controller method name (e.g., 'updateGeolocation').
30
+ * @param method - The controller method name (e.g., 'updateUserRegion').
31
31
  * @param params - The parameters passed to the method, used to generate the cache key.
32
32
  * Must match the params used when calling the controller method.
33
33
  * @returns A selector function that returns `{ data, isFetching, error }`.
@@ -43,14 +43,14 @@ export type RequestSelectorResult<TData> = {
43
43
  * (rampsControllerState) => rampsControllerState,
44
44
  * );
45
45
  *
46
- * export const selectGeolocationRequest = createRequestSelector<
46
+ * export const selectUserRegionRequest = createRequestSelector<
47
47
  * RootState,
48
48
  * string
49
- * >(selectRampsControllerState, 'updateGeolocation', []);
49
+ * >(selectRampsControllerState, 'updateUserRegion', []);
50
50
  *
51
51
  * // In hook - use directly with useSelector, no shallowEqual needed
52
- * export function useRampsGeolocation() {
53
- * const { isFetching, error } = useSelector(selectGeolocationRequest);
52
+ * export function useRampsUserRegion() {
53
+ * const { isFetching, error } = useSelector(selectUserRegionRequest);
54
54
  * // ... rest of hook
55
55
  * }
56
56
  * ```
@@ -27,7 +27,7 @@ export type RequestSelectorResult<TData> = {
27
27
  *
28
28
  * @param getState - Function that extracts RampsControllerState from the root state.
29
29
  * Typically a reselect selector like `selectRampsControllerState`.
30
- * @param method - The controller method name (e.g., 'updateGeolocation').
30
+ * @param method - The controller method name (e.g., 'updateUserRegion').
31
31
  * @param params - The parameters passed to the method, used to generate the cache key.
32
32
  * Must match the params used when calling the controller method.
33
33
  * @returns A selector function that returns `{ data, isFetching, error }`.
@@ -43,14 +43,14 @@ export type RequestSelectorResult<TData> = {
43
43
  * (rampsControllerState) => rampsControllerState,
44
44
  * );
45
45
  *
46
- * export const selectGeolocationRequest = createRequestSelector<
46
+ * export const selectUserRegionRequest = createRequestSelector<
47
47
  * RootState,
48
48
  * string
49
- * >(selectRampsControllerState, 'updateGeolocation', []);
49
+ * >(selectRampsControllerState, 'updateUserRegion', []);
50
50
  *
51
51
  * // In hook - use directly with useSelector, no shallowEqual needed
52
- * export function useRampsGeolocation() {
53
- * const { isFetching, error } = useSelector(selectGeolocationRequest);
52
+ * export function useRampsUserRegion() {
53
+ * const { isFetching, error } = useSelector(selectUserRegionRequest);
54
54
  * // ... rest of hook
55
55
  * }
56
56
  * ```
@@ -12,7 +12,7 @@ import { RequestStatus, createCacheKey } from "./RequestCache.mjs";
12
12
  *
13
13
  * @param getState - Function that extracts RampsControllerState from the root state.
14
14
  * Typically a reselect selector like `selectRampsControllerState`.
15
- * @param method - The controller method name (e.g., 'updateGeolocation').
15
+ * @param method - The controller method name (e.g., 'updateUserRegion').
16
16
  * @param params - The parameters passed to the method, used to generate the cache key.
17
17
  * Must match the params used when calling the controller method.
18
18
  * @returns A selector function that returns `{ data, isFetching, error }`.
@@ -28,14 +28,14 @@ import { RequestStatus, createCacheKey } from "./RequestCache.mjs";
28
28
  * (rampsControllerState) => rampsControllerState,
29
29
  * );
30
30
  *
31
- * export const selectGeolocationRequest = createRequestSelector<
31
+ * export const selectUserRegionRequest = createRequestSelector<
32
32
  * RootState,
33
33
  * string
34
- * >(selectRampsControllerState, 'updateGeolocation', []);
34
+ * >(selectRampsControllerState, 'updateUserRegion', []);
35
35
  *
36
36
  * // In hook - use directly with useSelector, no shallowEqual needed
37
- * export function useRampsGeolocation() {
38
- * const { isFetching, error } = useSelector(selectGeolocationRequest);
37
+ * export function useRampsUserRegion() {
38
+ * const { isFetching, error } = useSelector(selectUserRegionRequest);
39
39
  * // ... rest of hook
40
40
  * }
41
41
  * ```
@@ -1 +1 @@
1
- {"version":3,"file":"selectors.mjs","sourceRoot":"","sources":["../src/selectors.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,2BAAuB;AAkB/D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwDG;AACH,MAAM,UAAU,qBAAqB,CACnC,QAAqE,EACrE,MAAc,EACd,MAAiB;IAEjB,MAAM,QAAQ,GAAG,cAAc,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAEhD,IAAI,WAAqC,CAAC;IAC1C,IAAI,UAAU,GAAwC,IAAI,CAAC;IAE3D,OAAO,CAAC,KAAiB,EAAgC,EAAE;QACzD,MAAM,OAAO,GAAG,QAAQ,CAAC,KAAK,CAAC,EAAE,QAAQ,EAAE,CAAC,QAAQ,CAAC,CAAC;QAEtD,IAAI,OAAO,KAAK,WAAW,IAAI,UAAU,KAAK,IAAI,EAAE,CAAC;YACnD,OAAO,UAAU,CAAC;QACpB,CAAC;QAED,WAAW,GAAG,OAAO,CAAC;QACtB,UAAU,GAAG;YACX,IAAI,EAAG,OAAO,EAAE,IAAc,IAAI,IAAI;YACtC,UAAU,EAAE,OAAO,EAAE,MAAM,KAAK,aAAa,CAAC,OAAO;YACrD,KAAK,EAAE,OAAO,EAAE,KAAK,IAAI,IAAI;SAC9B,CAAC;QAEF,OAAO,UAAU,CAAC;IACpB,CAAC,CAAC;AACJ,CAAC","sourcesContent":["import type { RampsControllerState } from './RampsController';\nimport type { RequestState } from './RequestCache';\nimport { RequestStatus, createCacheKey } from './RequestCache';\n\n/**\n * Result shape returned by request selectors.\n *\n * This object is memoized - the same reference is returned when the underlying\n * request state hasn't changed, making it safe to use with React Redux's\n * `useSelector` without `shallowEqual`.\n */\nexport type RequestSelectorResult<TData> = {\n /** The data returned by the request, or null if not yet loaded or on error. */\n data: TData | null;\n /** Whether the request is currently in progress. */\n isFetching: boolean;\n /** Error message if the request failed, or null if successful or not yet attempted. */\n error: string | null;\n};\n\n/**\n * Creates a memoized selector for a controller method's request state.\n *\n * This selector tracks the loading, error, and data state for a specific\n * controller method call. It's optimized for use with React Redux's `useSelector`\n * hook - the selector returns the same object reference when the underlying\n * request state hasn't changed, so no `shallowEqual` is needed.\n *\n * The selector uses reference equality on the request object itself, so it\n * works correctly with arrays and objects without expensive deep equality checks.\n *\n * @param getState - Function that extracts RampsControllerState from the root state.\n * Typically a reselect selector like `selectRampsControllerState`.\n * @param method - The controller method name (e.g., 'updateGeolocation').\n * @param params - The parameters passed to the method, used to generate the cache key.\n * Must match the params used when calling the controller method.\n * @returns A selector function that returns `{ data, isFetching, error }`.\n *\n * @example\n * ```ts\n * // In selectors file - create once at module level\n * import { createRequestSelector } from '@metamask/ramps-controller';\n * import { createSelector } from 'reselect';\n *\n * const selectRampsControllerState = createSelector(\n * (state: RootState) => state.engine.backgroundState.RampsController,\n * (rampsControllerState) => rampsControllerState,\n * );\n *\n * export const selectGeolocationRequest = createRequestSelector<\n * RootState,\n * string\n * >(selectRampsControllerState, 'updateGeolocation', []);\n *\n * // In hook - use directly with useSelector, no shallowEqual needed\n * export function useRampsGeolocation() {\n * const { isFetching, error } = useSelector(selectGeolocationRequest);\n * // ... rest of hook\n * }\n * ```\n *\n * @example\n * ```ts\n * // For methods with parameters\n * export const selectCryptoCurrenciesRequest = (region: string) =>\n * createRequestSelector<RootState, CryptoCurrency[]>(\n * selectRampsControllerState,\n * 'getCryptoCurrencies',\n * [region],\n * );\n *\n * // In component\n * const { data, isFetching, error } = useSelector(\n * selectCryptoCurrenciesRequest('US')\n * );\n * ```\n */\nexport function createRequestSelector<TRootState, TData>(\n getState: (rootState: TRootState) => RampsControllerState | undefined,\n method: string,\n params: unknown[],\n): (state: TRootState) => RequestSelectorResult<TData> {\n const cacheKey = createCacheKey(method, params);\n\n let lastRequest: RequestState | undefined;\n let lastResult: RequestSelectorResult<TData> | null = null;\n\n return (state: TRootState): RequestSelectorResult<TData> => {\n const request = getState(state)?.requests?.[cacheKey];\n\n if (request === lastRequest && lastResult !== null) {\n return lastResult;\n }\n\n lastRequest = request;\n lastResult = {\n data: (request?.data as TData) ?? null,\n isFetching: request?.status === RequestStatus.LOADING,\n error: request?.error ?? null,\n };\n\n return lastResult;\n };\n}\n"]}
1
+ {"version":3,"file":"selectors.mjs","sourceRoot":"","sources":["../src/selectors.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,2BAAuB;AAkB/D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwDG;AACH,MAAM,UAAU,qBAAqB,CACnC,QAAqE,EACrE,MAAc,EACd,MAAiB;IAEjB,MAAM,QAAQ,GAAG,cAAc,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAEhD,IAAI,WAAqC,CAAC;IAC1C,IAAI,UAAU,GAAwC,IAAI,CAAC;IAE3D,OAAO,CAAC,KAAiB,EAAgC,EAAE;QACzD,MAAM,OAAO,GAAG,QAAQ,CAAC,KAAK,CAAC,EAAE,QAAQ,EAAE,CAAC,QAAQ,CAAC,CAAC;QAEtD,IAAI,OAAO,KAAK,WAAW,IAAI,UAAU,KAAK,IAAI,EAAE,CAAC;YACnD,OAAO,UAAU,CAAC;QACpB,CAAC;QAED,WAAW,GAAG,OAAO,CAAC;QACtB,UAAU,GAAG;YACX,IAAI,EAAG,OAAO,EAAE,IAAc,IAAI,IAAI;YACtC,UAAU,EAAE,OAAO,EAAE,MAAM,KAAK,aAAa,CAAC,OAAO;YACrD,KAAK,EAAE,OAAO,EAAE,KAAK,IAAI,IAAI;SAC9B,CAAC;QAEF,OAAO,UAAU,CAAC;IACpB,CAAC,CAAC;AACJ,CAAC","sourcesContent":["import type { RampsControllerState } from './RampsController';\nimport type { RequestState } from './RequestCache';\nimport { RequestStatus, createCacheKey } from './RequestCache';\n\n/**\n * Result shape returned by request selectors.\n *\n * This object is memoized - the same reference is returned when the underlying\n * request state hasn't changed, making it safe to use with React Redux's\n * `useSelector` without `shallowEqual`.\n */\nexport type RequestSelectorResult<TData> = {\n /** The data returned by the request, or null if not yet loaded or on error. */\n data: TData | null;\n /** Whether the request is currently in progress. */\n isFetching: boolean;\n /** Error message if the request failed, or null if successful or not yet attempted. */\n error: string | null;\n};\n\n/**\n * Creates a memoized selector for a controller method's request state.\n *\n * This selector tracks the loading, error, and data state for a specific\n * controller method call. It's optimized for use with React Redux's `useSelector`\n * hook - the selector returns the same object reference when the underlying\n * request state hasn't changed, so no `shallowEqual` is needed.\n *\n * The selector uses reference equality on the request object itself, so it\n * works correctly with arrays and objects without expensive deep equality checks.\n *\n * @param getState - Function that extracts RampsControllerState from the root state.\n * Typically a reselect selector like `selectRampsControllerState`.\n * @param method - The controller method name (e.g., 'updateUserRegion').\n * @param params - The parameters passed to the method, used to generate the cache key.\n * Must match the params used when calling the controller method.\n * @returns A selector function that returns `{ data, isFetching, error }`.\n *\n * @example\n * ```ts\n * // In selectors file - create once at module level\n * import { createRequestSelector } from '@metamask/ramps-controller';\n * import { createSelector } from 'reselect';\n *\n * const selectRampsControllerState = createSelector(\n * (state: RootState) => state.engine.backgroundState.RampsController,\n * (rampsControllerState) => rampsControllerState,\n * );\n *\n * export const selectUserRegionRequest = createRequestSelector<\n * RootState,\n * string\n * >(selectRampsControllerState, 'updateUserRegion', []);\n *\n * // In hook - use directly with useSelector, no shallowEqual needed\n * export function useRampsUserRegion() {\n * const { isFetching, error } = useSelector(selectUserRegionRequest);\n * // ... rest of hook\n * }\n * ```\n *\n * @example\n * ```ts\n * // For methods with parameters\n * export const selectCryptoCurrenciesRequest = (region: string) =>\n * createRequestSelector<RootState, CryptoCurrency[]>(\n * selectRampsControllerState,\n * 'getCryptoCurrencies',\n * [region],\n * );\n *\n * // In component\n * const { data, isFetching, error } = useSelector(\n * selectCryptoCurrenciesRequest('US')\n * );\n * ```\n */\nexport function createRequestSelector<TRootState, TData>(\n getState: (rootState: TRootState) => RampsControllerState | undefined,\n method: string,\n params: unknown[],\n): (state: TRootState) => RequestSelectorResult<TData> {\n const cacheKey = createCacheKey(method, params);\n\n let lastRequest: RequestState | undefined;\n let lastResult: RequestSelectorResult<TData> | null = null;\n\n return (state: TRootState): RequestSelectorResult<TData> => {\n const request = getState(state)?.requests?.[cacheKey];\n\n if (request === lastRequest && lastResult !== null) {\n return lastResult;\n }\n\n lastRequest = request;\n lastResult = {\n data: (request?.data as TData) ?? null,\n isFetching: request?.status === RequestStatus.LOADING,\n error: request?.error ?? null,\n };\n\n return lastResult;\n };\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@metamask-previews/ramps-controller",
3
- "version": "2.1.0-preview-8a4f4ea3",
3
+ "version": "2.1.0-preview-f5ec6061",
4
4
  "description": "A controller for managing cryptocurrency on/off ramps functionality",
5
5
  "keywords": [
6
6
  "MetaMask",