@metamask/ramps-controller 5.1.0 → 6.0.0

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.
@@ -1,18 +1,18 @@
1
1
  "use strict";
2
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
3
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
4
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
5
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
6
+ };
2
7
  var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
3
8
  if (kind === "m") throw new TypeError("Private method is not writable");
4
9
  if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
5
10
  if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
6
11
  return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
7
12
  };
8
- var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
9
- if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
10
- if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
11
- return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
12
- };
13
- var _RampsController_instances, _RampsController_requestCacheTTL, _RampsController_requestCacheMaxSize, _RampsController_pendingRequests, _RampsController_removeRequestState, _RampsController_cleanupState, _RampsController_updateRequestState;
13
+ var _RampsController_instances, _RampsController_requestCacheTTL, _RampsController_requestCacheMaxSize, _RampsController_pendingRequests, _RampsController_pendingResourceCount, _RampsController_clearPendingResourceCountForDependentResources, _RampsController_removeRequestState, _RampsController_cleanupState, _RampsController_fireAndForget, _RampsController_updateResourceField, _RampsController_setResourceLoading, _RampsController_setResourceError, _RampsController_updateRequestState;
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.RampsController = exports.getDefaultRampsControllerState = exports.controllerName = void 0;
15
+ exports.RampsController = exports.getDefaultRampsControllerState = exports.RAMPS_CONTROLLER_REQUIRED_SERVICE_ACTIONS = exports.controllerName = void 0;
16
16
  const base_controller_1 = require("@metamask/base-controller");
17
17
  const RequestCache_1 = require("./RequestCache.cjs");
18
18
  // === GENERAL ===
@@ -22,6 +22,19 @@ const RequestCache_1 = require("./RequestCache.cjs");
22
22
  * when composed with other controllers.
23
23
  */
24
24
  exports.controllerName = 'RampsController';
25
+ /**
26
+ * RampsService action types that RampsController calls via the messenger.
27
+ * Any host (e.g. mobile) that creates a RampsController messenger must delegate
28
+ * these actions from the root messenger so the controller can function.
29
+ */
30
+ exports.RAMPS_CONTROLLER_REQUIRED_SERVICE_ACTIONS = [
31
+ 'RampsService:getGeolocation',
32
+ 'RampsService:getCountries',
33
+ 'RampsService:getTokens',
34
+ 'RampsService:getProviders',
35
+ 'RampsService:getPaymentMethods',
36
+ 'RampsService:getQuotes',
37
+ ];
25
38
  /**
26
39
  * Default TTL for quotes requests (15 seconds).
27
40
  * Quotes are time-sensitive and should have a shorter cache duration.
@@ -37,12 +50,6 @@ const rampsControllerMetadata = {
37
50
  includeInStateLogs: true,
38
51
  usedInUi: true,
39
52
  },
40
- selectedProvider: {
41
- persist: false,
42
- includeInDebugSnapshot: true,
43
- includeInStateLogs: true,
44
- usedInUi: true,
45
- },
46
53
  countries: {
47
54
  persist: true,
48
55
  includeInDebugSnapshot: true,
@@ -61,24 +68,12 @@ const rampsControllerMetadata = {
61
68
  includeInStateLogs: true,
62
69
  usedInUi: true,
63
70
  },
64
- selectedToken: {
65
- persist: false,
66
- includeInDebugSnapshot: true,
67
- includeInStateLogs: true,
68
- usedInUi: true,
69
- },
70
71
  paymentMethods: {
71
72
  persist: false,
72
73
  includeInDebugSnapshot: true,
73
74
  includeInStateLogs: true,
74
75
  usedInUi: true,
75
76
  },
76
- selectedPaymentMethod: {
77
- persist: false,
78
- includeInDebugSnapshot: true,
79
- includeInStateLogs: true,
80
- usedInUi: true,
81
- },
82
77
  quotes: {
83
78
  persist: false,
84
79
  includeInDebugSnapshot: true,
@@ -92,6 +87,23 @@ const rampsControllerMetadata = {
92
87
  usedInUi: true,
93
88
  },
94
89
  };
90
+ /**
91
+ * Creates a default resource state object.
92
+ *
93
+ * @template TData - The type of the resource data.
94
+ * @template TSelected - The type of the selected item.
95
+ * @param data - The initial data value.
96
+ * @param selected - The initial selected value.
97
+ * @returns A ResourceState object with default loading and error values.
98
+ */
99
+ function createDefaultResourceState(data, selected = null) {
100
+ return {
101
+ data,
102
+ selected,
103
+ isLoading: false,
104
+ error: null,
105
+ };
106
+ }
95
107
  /**
96
108
  * Constructs the default {@link RampsController} state. This allows
97
109
  * consumers to provide a partial state object when initializing the controller
@@ -103,18 +115,43 @@ const rampsControllerMetadata = {
103
115
  function getDefaultRampsControllerState() {
104
116
  return {
105
117
  userRegion: null,
106
- selectedProvider: null,
107
- countries: [],
108
- providers: [],
109
- tokens: null,
110
- selectedToken: null,
111
- paymentMethods: [],
112
- selectedPaymentMethod: null,
113
- quotes: null,
118
+ countries: createDefaultResourceState([]),
119
+ providers: createDefaultResourceState([], null),
120
+ tokens: createDefaultResourceState(null, null),
121
+ paymentMethods: createDefaultResourceState([], null),
122
+ quotes: createDefaultResourceState(null),
114
123
  requests: {},
115
124
  };
116
125
  }
117
126
  exports.getDefaultRampsControllerState = getDefaultRampsControllerState;
127
+ /**
128
+ * Resets region-dependent resources (userRegion, providers, tokens, paymentMethods, quotes).
129
+ * Mutates state in place; use from within controller update() for atomic updates.
130
+ *
131
+ * @param state - The state object to mutate.
132
+ * @param options - Options for the reset.
133
+ * @param options.clearUserRegionData - When true, sets userRegion to null (e.g. for full cleanup).
134
+ */
135
+ function resetDependentResources(state, options) {
136
+ if (options?.clearUserRegionData) {
137
+ state.userRegion = null;
138
+ }
139
+ state.providers.selected = null;
140
+ state.providers.data = [];
141
+ state.providers.isLoading = false;
142
+ state.providers.error = null;
143
+ state.tokens.selected = null;
144
+ state.tokens.data = null;
145
+ state.tokens.isLoading = false;
146
+ state.tokens.error = null;
147
+ state.paymentMethods.data = [];
148
+ state.paymentMethods.selected = null;
149
+ state.paymentMethods.isLoading = false;
150
+ state.paymentMethods.error = null;
151
+ state.quotes.data = null;
152
+ state.quotes.isLoading = false;
153
+ state.quotes.error = null;
154
+ }
118
155
  // === HELPER FUNCTIONS ===
119
156
  /**
120
157
  * Finds a country and state from a region code string.
@@ -173,6 +210,15 @@ function findRegionFromCode(regionCode, countries) {
173
210
  * Manages cryptocurrency on/off ramps functionality.
174
211
  */
175
212
  class RampsController extends base_controller_1.BaseController {
213
+ /**
214
+ * Clears the pending resource count map. Used only in tests to exercise the
215
+ * defensive path when get() returns undefined in the finally block.
216
+ *
217
+ * @internal
218
+ */
219
+ clearPendingResourceCountForTest() {
220
+ __classPrivateFieldGet(this, _RampsController_pendingResourceCount, "f").clear();
221
+ }
176
222
  /**
177
223
  * Constructs a new {@link RampsController}.
178
224
  *
@@ -209,6 +255,11 @@ class RampsController extends base_controller_1.BaseController {
209
255
  * Key is the cache key, value is the pending request with abort controller.
210
256
  */
211
257
  _RampsController_pendingRequests.set(this, new Map());
258
+ /**
259
+ * Count of in-flight requests per resource type.
260
+ * Used so isLoading is only cleared when the last request for that resource finishes.
261
+ */
262
+ _RampsController_pendingResourceCount.set(this, new Map());
212
263
  __classPrivateFieldSet(this, _RampsController_requestCacheTTL, requestCacheTTL, "f");
213
264
  __classPrivateFieldSet(this, _RampsController_requestCacheMaxSize, requestCacheMaxSize, "f");
214
265
  }
@@ -240,8 +291,18 @@ class RampsController extends base_controller_1.BaseController {
240
291
  // Create abort controller for this request
241
292
  const abortController = new AbortController();
242
293
  const lastFetchedAt = Date.now();
294
+ const { resourceType } = options ?? {};
243
295
  // Update state to loading
244
296
  __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_updateRequestState).call(this, cacheKey, (0, RequestCache_1.createLoadingState)());
297
+ // Set resource-level loading state (only on cache miss). Ref-count so concurrent
298
+ // requests for the same resource type (different cache keys) keep isLoading true.
299
+ if (resourceType) {
300
+ const count = __classPrivateFieldGet(this, _RampsController_pendingResourceCount, "f").get(resourceType) ?? 0;
301
+ __classPrivateFieldGet(this, _RampsController_pendingResourceCount, "f").set(resourceType, count + 1);
302
+ if (count === 0) {
303
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_setResourceLoading).call(this, resourceType, true);
304
+ }
305
+ }
245
306
  // Create the fetch promise
246
307
  const promise = (async () => {
247
308
  try {
@@ -251,6 +312,15 @@ class RampsController extends base_controller_1.BaseController {
251
312
  throw new Error('Request was aborted');
252
313
  }
253
314
  __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_updateRequestState).call(this, cacheKey, (0, RequestCache_1.createSuccessState)(data, lastFetchedAt));
315
+ if (resourceType) {
316
+ // We need the extra logic because there are two situations where we’re allowed to clear the error:
317
+ // No callback → always clear
318
+ // Callback present → clear only when isResultCurrent() returns true.
319
+ const isCurrent = !options?.isResultCurrent || options.isResultCurrent();
320
+ if (isCurrent) {
321
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_setResourceError).call(this, resourceType, null);
322
+ }
323
+ }
254
324
  return data;
255
325
  }
256
326
  catch (error) {
@@ -258,8 +328,14 @@ class RampsController extends base_controller_1.BaseController {
258
328
  if (abortController.signal.aborted) {
259
329
  throw error;
260
330
  }
261
- const errorMessage = error?.message;
262
- __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_updateRequestState).call(this, cacheKey, (0, RequestCache_1.createErrorState)(errorMessage ?? 'Unknown error', lastFetchedAt));
331
+ const errorMessage = error?.message ?? 'Unknown error';
332
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_updateRequestState).call(this, cacheKey, (0, RequestCache_1.createErrorState)(errorMessage, lastFetchedAt));
333
+ if (resourceType) {
334
+ const isCurrent = !options?.isResultCurrent || options.isResultCurrent();
335
+ if (isCurrent) {
336
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_setResourceError).call(this, resourceType, errorMessage);
337
+ }
338
+ }
263
339
  throw error;
264
340
  }
265
341
  finally {
@@ -268,6 +344,18 @@ class RampsController extends base_controller_1.BaseController {
268
344
  if (currentPending?.abortController === abortController) {
269
345
  __classPrivateFieldGet(this, _RampsController_pendingRequests, "f").delete(cacheKey);
270
346
  }
347
+ // Clear resource-level loading state only when no requests for this resource remain
348
+ if (resourceType) {
349
+ const count = __classPrivateFieldGet(this, _RampsController_pendingResourceCount, "f").get(resourceType) ?? 0;
350
+ const next = Math.max(0, count - 1);
351
+ if (next === 0) {
352
+ __classPrivateFieldGet(this, _RampsController_pendingResourceCount, "f").delete(resourceType);
353
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_setResourceLoading).call(this, resourceType, false);
354
+ }
355
+ else {
356
+ __classPrivateFieldGet(this, _RampsController_pendingResourceCount, "f").set(resourceType, next);
357
+ }
358
+ }
271
359
  }
272
360
  })();
273
361
  // Store pending request for deduplication
@@ -310,37 +398,40 @@ class RampsController extends base_controller_1.BaseController {
310
398
  async setUserRegion(region, options) {
311
399
  const normalizedRegion = region.toLowerCase().trim();
312
400
  try {
313
- const { countries } = this.state;
314
- if (!countries || countries.length === 0) {
401
+ const countriesData = this.state.countries.data;
402
+ if (!countriesData || countriesData.length === 0) {
315
403
  __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_cleanupState).call(this);
316
404
  throw new Error('No countries found. Cannot set user region without valid country information.');
317
405
  }
318
- const userRegion = findRegionFromCode(normalizedRegion, countries);
406
+ const userRegion = findRegionFromCode(normalizedRegion, countriesData);
319
407
  if (!userRegion) {
320
408
  __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_cleanupState).call(this);
321
409
  throw new Error(`Region "${normalizedRegion}" not found in countries data. Cannot set user region without valid country information.`);
322
410
  }
323
- // Only cleanup state if region is actually changing
324
411
  const regionChanged = normalizedRegion !== this.state.userRegion?.regionCode;
325
- // Set the new region atomically with cleanup to avoid intermediate null state
412
+ const needsRefetch = regionChanged ||
413
+ !this.state.tokens.data ||
414
+ this.state.providers.data.length === 0;
415
+ if (regionChanged) {
416
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_clearPendingResourceCountForDependentResources).call(this);
417
+ }
326
418
  this.update((state) => {
327
419
  if (regionChanged) {
328
- state.selectedProvider = null;
329
- state.selectedToken = null;
330
- state.tokens = null;
331
- state.providers = [];
332
- state.paymentMethods = [];
333
- state.selectedPaymentMethod = null;
334
- state.quotes = null;
420
+ resetDependentResources(state);
335
421
  }
336
422
  state.userRegion = userRegion;
337
423
  });
338
- // Only trigger fetches if region changed or if data is missing
339
- if (regionChanged || !this.state.tokens) {
340
- this.triggerGetTokens(userRegion.regionCode, 'buy', options);
341
- }
342
- if (regionChanged || this.state.providers.length === 0) {
343
- this.triggerGetProviders(userRegion.regionCode, options);
424
+ if (needsRefetch) {
425
+ const refetchPromises = [];
426
+ if (regionChanged || !this.state.tokens.data) {
427
+ refetchPromises.push(this.getTokens(userRegion.regionCode, 'buy', options));
428
+ }
429
+ if (regionChanged || this.state.providers.data.length === 0) {
430
+ refetchPromises.push(this.getProviders(userRegion.regionCode, options));
431
+ }
432
+ if (refetchPromises.length > 0) {
433
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_fireAndForget).call(this, Promise.all(refetchPromises));
434
+ }
344
435
  }
345
436
  return userRegion;
346
437
  }
@@ -360,9 +451,9 @@ class RampsController extends base_controller_1.BaseController {
360
451
  setSelectedProvider(providerId) {
361
452
  if (providerId === null) {
362
453
  this.update((state) => {
363
- state.selectedProvider = null;
364
- state.paymentMethods = [];
365
- state.selectedPaymentMethod = null;
454
+ state.providers.selected = null;
455
+ state.paymentMethods.data = [];
456
+ state.paymentMethods.selected = null;
366
457
  });
367
458
  return;
368
459
  }
@@ -370,7 +461,7 @@ class RampsController extends base_controller_1.BaseController {
370
461
  if (!regionCode) {
371
462
  throw new Error('Region is required. Cannot set selected provider without valid region information.');
372
463
  }
373
- const { providers } = this.state;
464
+ const providers = this.state.providers.data;
374
465
  if (!providers || providers.length === 0) {
375
466
  throw new Error('Providers not loaded. Cannot set selected provider before providers are fetched.');
376
467
  }
@@ -379,16 +470,11 @@ class RampsController extends base_controller_1.BaseController {
379
470
  throw new Error(`Provider with ID "${providerId}" not found in available providers.`);
380
471
  }
381
472
  this.update((state) => {
382
- state.selectedProvider = provider;
383
- state.paymentMethods = [];
384
- state.selectedPaymentMethod = null;
385
- });
386
- // fetch payment methods for the new provider
387
- // this is needed because you can change providers without changing the token
388
- // (getPaymentMethods will use state as its default)
389
- this.triggerGetPaymentMethods(regionCode, {
390
- provider: provider.id,
473
+ state.providers.selected = provider;
474
+ state.paymentMethods.data = [];
475
+ state.paymentMethods.selected = null;
391
476
  });
477
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_fireAndForget).call(this, this.getPaymentMethods(regionCode, { provider: provider.id }));
392
478
  }
393
479
  /**
394
480
  * Initializes the controller by fetching the user's region from geolocation.
@@ -414,8 +500,8 @@ class RampsController extends base_controller_1.BaseController {
414
500
  if (!regionCode) {
415
501
  throw new Error('Region code is required. Cannot hydrate state without valid region information.');
416
502
  }
417
- this.triggerGetTokens(regionCode, 'buy', options);
418
- this.triggerGetProviders(regionCode, options);
503
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_fireAndForget).call(this, this.getTokens(regionCode, 'buy', options));
504
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_fireAndForget).call(this, this.getProviders(regionCode, options));
419
505
  }
420
506
  /**
421
507
  * Fetches the list of supported countries.
@@ -429,9 +515,9 @@ class RampsController extends base_controller_1.BaseController {
429
515
  const cacheKey = (0, RequestCache_1.createCacheKey)('getCountries', []);
430
516
  const countries = await this.executeRequest(cacheKey, async () => {
431
517
  return this.messenger.call('RampsService:getCountries');
432
- }, options);
518
+ }, { ...options, resourceType: 'countries' });
433
519
  this.update((state) => {
434
- state.countries = countries;
520
+ state.countries.data = countries;
435
521
  });
436
522
  return countries;
437
523
  }
@@ -460,11 +546,16 @@ class RampsController extends base_controller_1.BaseController {
460
546
  return this.messenger.call('RampsService:getTokens', normalizedRegion, action, {
461
547
  provider: options?.provider,
462
548
  });
463
- }, options);
549
+ }, {
550
+ ...options,
551
+ resourceType: 'tokens',
552
+ isResultCurrent: () => this.state.userRegion?.regionCode === undefined ||
553
+ this.state.userRegion?.regionCode === normalizedRegion,
554
+ });
464
555
  this.update((state) => {
465
556
  const userRegionCode = state.userRegion?.regionCode;
466
557
  if (userRegionCode === undefined || userRegionCode === normalizedRegion) {
467
- state.tokens = tokens;
558
+ state.tokens.data = tokens;
468
559
  }
469
560
  });
470
561
  return tokens;
@@ -480,9 +571,9 @@ class RampsController extends base_controller_1.BaseController {
480
571
  setSelectedToken(assetId) {
481
572
  if (!assetId) {
482
573
  this.update((state) => {
483
- state.selectedToken = null;
484
- state.paymentMethods = [];
485
- state.selectedPaymentMethod = null;
574
+ state.tokens.selected = null;
575
+ state.paymentMethods.data = [];
576
+ state.paymentMethods.selected = null;
486
577
  });
487
578
  return;
488
579
  }
@@ -490,7 +581,7 @@ class RampsController extends base_controller_1.BaseController {
490
581
  if (!regionCode) {
491
582
  throw new Error('Region is required. Cannot set selected token without valid region information.');
492
583
  }
493
- const { tokens } = this.state;
584
+ const tokens = this.state.tokens.data;
494
585
  if (!tokens) {
495
586
  throw new Error('Tokens not loaded. Cannot set selected token before tokens are fetched.');
496
587
  }
@@ -500,13 +591,11 @@ class RampsController extends base_controller_1.BaseController {
500
591
  throw new Error(`Token with asset ID "${assetId}" not found in available tokens.`);
501
592
  }
502
593
  this.update((state) => {
503
- state.selectedToken = token;
504
- state.paymentMethods = [];
505
- state.selectedPaymentMethod = null;
506
- });
507
- this.triggerGetPaymentMethods(regionCode, {
508
- assetId: token.assetId,
594
+ state.tokens.selected = token;
595
+ state.paymentMethods.data = [];
596
+ state.paymentMethods.selected = null;
509
597
  });
598
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_fireAndForget).call(this, this.getPaymentMethods(regionCode, { assetId: token.assetId }));
510
599
  }
511
600
  /**
512
601
  * Fetches the list of providers for a given region.
@@ -540,11 +629,16 @@ class RampsController extends base_controller_1.BaseController {
540
629
  fiat: options?.fiat,
541
630
  payments: options?.payments,
542
631
  });
543
- }, options);
632
+ }, {
633
+ ...options,
634
+ resourceType: 'providers',
635
+ isResultCurrent: () => this.state.userRegion?.regionCode === undefined ||
636
+ this.state.userRegion?.regionCode === normalizedRegion,
637
+ });
544
638
  this.update((state) => {
545
639
  const userRegionCode = state.userRegion?.regionCode;
546
640
  if (userRegionCode === undefined || userRegionCode === normalizedRegion) {
547
- state.providers = providers;
641
+ state.providers.data = providers;
548
642
  }
549
643
  });
550
644
  return { providers };
@@ -563,8 +657,8 @@ class RampsController extends base_controller_1.BaseController {
563
657
  async getPaymentMethods(region, options) {
564
658
  const regionCode = region ?? this.state.userRegion?.regionCode ?? null;
565
659
  const fiatToUse = options?.fiat ?? this.state.userRegion?.country?.currency ?? null;
566
- const assetIdToUse = options?.assetId ?? this.state.selectedToken?.assetId ?? '';
567
- const providerToUse = options?.provider ?? this.state.selectedProvider?.id ?? '';
660
+ const assetIdToUse = options?.assetId ?? this.state.tokens.selected?.assetId ?? '';
661
+ const providerToUse = options?.provider ?? this.state.providers.selected?.id ?? '';
568
662
  if (!regionCode) {
569
663
  throw new Error('Region is required. Either provide a region parameter or ensure userRegion is set in controller state.');
570
664
  }
@@ -586,21 +680,31 @@ class RampsController extends base_controller_1.BaseController {
586
680
  assetId: assetIdToUse,
587
681
  provider: providerToUse,
588
682
  });
589
- }, options);
683
+ }, {
684
+ ...options,
685
+ resourceType: 'paymentMethods',
686
+ isResultCurrent: () => {
687
+ const regionMatch = this.state.userRegion?.regionCode === undefined ||
688
+ this.state.userRegion?.regionCode === normalizedRegion;
689
+ const tokenMatch = (this.state.tokens.selected?.assetId ?? '') === assetIdToUse;
690
+ const providerMatch = (this.state.providers.selected?.id ?? '') === providerToUse;
691
+ return regionMatch && tokenMatch && providerMatch;
692
+ },
693
+ });
590
694
  this.update((state) => {
591
- const currentAssetId = state.selectedToken?.assetId ?? '';
592
- const currentProviderId = state.selectedProvider?.id ?? '';
695
+ const currentAssetId = state.tokens.selected?.assetId ?? '';
696
+ const currentProviderId = state.providers.selected?.id ?? '';
593
697
  const tokenSelectionUnchanged = assetIdToUse === currentAssetId;
594
698
  const providerSelectionUnchanged = providerToUse === currentProviderId;
595
699
  // this is a race condition check to ensure that the selected token and provider in state are the same as the tokens we're requesting for
596
700
  // ex: if the user rapidly changes the token or provider, the in-flight payment methods might not be valid
597
701
  // so this check will ensure that the payment methods are still valid for the token and provider that were requested
598
702
  if (tokenSelectionUnchanged && providerSelectionUnchanged) {
599
- state.paymentMethods = response.payments;
703
+ state.paymentMethods.data = response.payments;
600
704
  // this will auto-select the first payment method if the selected payment method is not in the new payment methods
601
- const currentSelectionStillValid = response.payments.some((pm) => pm.id === state.selectedPaymentMethod?.id);
705
+ const currentSelectionStillValid = response.payments.some((pm) => pm.id === state.paymentMethods.selected?.id);
602
706
  if (!currentSelectionStillValid) {
603
- state.selectedPaymentMethod = response.payments[0] ?? null;
707
+ state.paymentMethods.selected = response.payments[0] ?? null;
604
708
  }
605
709
  }
606
710
  });
@@ -616,11 +720,11 @@ class RampsController extends base_controller_1.BaseController {
616
720
  setSelectedPaymentMethod(paymentMethodId) {
617
721
  if (!paymentMethodId) {
618
722
  this.update((state) => {
619
- state.selectedPaymentMethod = null;
723
+ state.paymentMethods.selected = null;
620
724
  });
621
725
  return;
622
726
  }
623
- const { paymentMethods } = this.state;
727
+ const paymentMethods = this.state.paymentMethods.data;
624
728
  if (!paymentMethods || paymentMethods.length === 0) {
625
729
  throw new Error('Payment methods not loaded. Cannot set selected payment method before payment methods are fetched.');
626
730
  }
@@ -629,7 +733,7 @@ class RampsController extends base_controller_1.BaseController {
629
733
  throw new Error(`Payment method with ID "${paymentMethodId}" not found in available payment methods.`);
630
734
  }
631
735
  this.update((state) => {
632
- state.selectedPaymentMethod = paymentMethod;
736
+ state.paymentMethods.selected = paymentMethod;
633
737
  });
634
738
  }
635
739
  /**
@@ -654,7 +758,7 @@ class RampsController extends base_controller_1.BaseController {
654
758
  const regionToUse = options.region ?? this.state.userRegion?.regionCode;
655
759
  const fiatToUse = options.fiat ?? this.state.userRegion?.country?.currency;
656
760
  const paymentMethodsToUse = options.paymentMethods ??
657
- this.state.paymentMethods.map((pm) => pm.id);
761
+ this.state.paymentMethods.data.map((pm) => pm.id);
658
762
  const action = options.action ?? 'buy';
659
763
  if (!regionToUse) {
660
764
  throw new Error('Region is required. Either provide a region parameter or ensure userRegion is set in controller state.');
@@ -705,11 +809,14 @@ class RampsController extends base_controller_1.BaseController {
705
809
  }, {
706
810
  forceRefresh: options.forceRefresh,
707
811
  ttl: options.ttl ?? DEFAULT_QUOTES_TTL,
812
+ resourceType: 'quotes',
813
+ isResultCurrent: () => this.state.userRegion?.regionCode === undefined ||
814
+ this.state.userRegion?.regionCode === normalizedRegion,
708
815
  });
709
816
  this.update((state) => {
710
817
  const userRegionCode = state.userRegion?.regionCode;
711
818
  if (userRegionCode === undefined || userRegionCode === normalizedRegion) {
712
- state.quotes = response;
819
+ state.quotes.data = response;
713
820
  }
714
821
  });
715
822
  return response;
@@ -724,108 +831,41 @@ class RampsController extends base_controller_1.BaseController {
724
831
  getWidgetUrl(quote) {
725
832
  return quote.quote?.widgetUrl ?? null;
726
833
  }
727
- // ============================================================
728
- // Sync Trigger Methods
729
- // These fire-and-forget methods are for use in React effects.
730
- // Errors are stored in state and available via selectors.
731
- // ============================================================
732
- /**
733
- * Triggers setting the user region without throwing.
734
- *
735
- * @param region - The region code to set (e.g., "US-CA").
736
- * @param options - Options for cache behavior.
737
- */
738
- triggerSetUserRegion(region, options) {
739
- this.setUserRegion(region, options).catch(() => {
740
- // Error stored in state
741
- });
742
- }
743
- /**
744
- * Triggers fetching countries without throwing.
745
- *
746
- * @param options - Options for cache behavior.
747
- */
748
- triggerGetCountries(options) {
749
- this.getCountries(options).catch(() => {
750
- // Error stored in state
751
- });
752
- }
753
- /**
754
- * Triggers fetching tokens without throwing.
755
- *
756
- * @param region - The region code. If not provided, uses userRegion from state.
757
- * @param action - The ramp action type ('buy' or 'sell').
758
- * @param options - Options for cache behavior.
759
- */
760
- triggerGetTokens(region, action = 'buy', options) {
761
- this.getTokens(region, action, options).catch(() => {
762
- // Error stored in state
763
- });
764
- }
765
- /**
766
- * Triggers fetching providers without throwing.
767
- *
768
- * @param region - The region code. If not provided, uses userRegion from state.
769
- * @param options - Options for cache behavior and query filters.
770
- */
771
- triggerGetProviders(region, options) {
772
- this.getProviders(region, options).catch(() => {
773
- // Error stored in state
774
- });
775
- }
776
- /**
777
- * Triggers fetching payment methods without throwing.
778
- *
779
- * @param region - User's region code (e.g., "us", "fr", "us-ny").
780
- * @param options - Query parameters for filtering payment methods.
781
- * @param options.fiat - Fiat currency code. If not provided, uses userRegion currency.
782
- * @param options.assetId - CAIP-19 cryptocurrency identifier.
783
- * @param options.provider - Provider ID path.
784
- */
785
- triggerGetPaymentMethods(region, options) {
786
- this.getPaymentMethods(region, options).catch(() => {
787
- // Error stored in state
788
- });
789
- }
790
- /**
791
- * Triggers fetching quotes without throwing.
792
- *
793
- * @param options - The parameters for fetching quotes.
794
- * @param options.region - User's region code. If not provided, uses userRegion from state.
795
- * @param options.fiat - Fiat currency code. If not provided, uses userRegion currency.
796
- * @param options.assetId - CAIP-19 cryptocurrency identifier.
797
- * @param options.amount - The amount (in fiat for buy, crypto for sell).
798
- * @param options.walletAddress - The destination wallet address.
799
- * @param options.paymentMethods - Array of payment method IDs. If not provided, uses paymentMethods from state.
800
- * @param options.provider - Optional provider ID to filter quotes.
801
- * @param options.redirectUrl - Optional redirect URL after order completion.
802
- * @param options.action - The ramp action type. Defaults to 'buy'.
803
- * @param options.forceRefresh - Whether to bypass cache.
804
- * @param options.ttl - Custom TTL for this request.
805
- */
806
- triggerGetQuotes(options) {
807
- this.getQuotes(options).catch(() => {
808
- // Error stored in state
809
- });
810
- }
811
834
  }
812
835
  exports.RampsController = RampsController;
813
- _RampsController_requestCacheTTL = new WeakMap(), _RampsController_requestCacheMaxSize = new WeakMap(), _RampsController_pendingRequests = new WeakMap(), _RampsController_instances = new WeakSet(), _RampsController_removeRequestState = function _RampsController_removeRequestState(cacheKey) {
836
+ _RampsController_requestCacheTTL = new WeakMap(), _RampsController_requestCacheMaxSize = new WeakMap(), _RampsController_pendingRequests = new WeakMap(), _RampsController_pendingResourceCount = new WeakMap(), _RampsController_instances = new WeakSet(), _RampsController_clearPendingResourceCountForDependentResources = function _RampsController_clearPendingResourceCountForDependentResources() {
837
+ const types = [
838
+ 'providers',
839
+ 'tokens',
840
+ 'paymentMethods',
841
+ 'quotes',
842
+ ];
843
+ for (const resourceType of types) {
844
+ __classPrivateFieldGet(this, _RampsController_pendingResourceCount, "f").delete(resourceType);
845
+ }
846
+ }, _RampsController_removeRequestState = function _RampsController_removeRequestState(cacheKey) {
814
847
  this.update((state) => {
815
848
  const requests = state.requests;
816
849
  delete requests[cacheKey];
817
850
  });
818
851
  }, _RampsController_cleanupState = function _RampsController_cleanupState() {
852
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_clearPendingResourceCountForDependentResources).call(this);
853
+ this.update((state) => resetDependentResources(state, {
854
+ clearUserRegionData: true,
855
+ }));
856
+ }, _RampsController_fireAndForget = function _RampsController_fireAndForget(promise) {
857
+ promise.catch((_error) => undefined);
858
+ }, _RampsController_updateResourceField = function _RampsController_updateResourceField(resourceType, field, value) {
819
859
  this.update((state) => {
820
- state.userRegion = null;
821
- state.selectedProvider = null;
822
- state.selectedToken = null;
823
- state.tokens = null;
824
- state.providers = [];
825
- state.paymentMethods = [];
826
- state.selectedPaymentMethod = null;
827
- state.quotes = null;
860
+ const resource = state[resourceType];
861
+ if (resource) {
862
+ resource[field] = value;
863
+ }
828
864
  });
865
+ }, _RampsController_setResourceLoading = function _RampsController_setResourceLoading(resourceType, loading) {
866
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_updateResourceField).call(this, resourceType, 'isLoading', loading);
867
+ }, _RampsController_setResourceError = function _RampsController_setResourceError(resourceType, error) {
868
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_updateResourceField).call(this, resourceType, 'error', error);
829
869
  }, _RampsController_updateRequestState = function _RampsController_updateRequestState(cacheKey, requestState) {
830
870
  const maxSize = __classPrivateFieldGet(this, _RampsController_requestCacheMaxSize, "f");
831
871
  const ttl = __classPrivateFieldGet(this, _RampsController_requestCacheTTL, "f");