@metamask-previews/ramps-controller 5.1.0-preview-40468f94 → 6.0.0-preview-fb7aa07ff

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_quotePollingInterval, _RampsController_quotePollingOptions, _RampsController_clearPendingResourceCountForDependentResources, _RampsController_removeRequestState, _RampsController_cleanupState, _RampsController_fireAndForget, _RampsController_restartPollingIfActive, _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,44 @@ 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, 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.selected = null;
153
+ state.quotes.isLoading = false;
154
+ state.quotes.error = null;
155
+ }
118
156
  // === HELPER FUNCTIONS ===
119
157
  /**
120
158
  * Finds a country and state from a region code string.
@@ -173,6 +211,15 @@ function findRegionFromCode(regionCode, countries) {
173
211
  * Manages cryptocurrency on/off ramps functionality.
174
212
  */
175
213
  class RampsController extends base_controller_1.BaseController {
214
+ /**
215
+ * Clears the pending resource count map. Used only in tests to exercise the
216
+ * defensive path when get() returns undefined in the finally block.
217
+ *
218
+ * @internal
219
+ */
220
+ clearPendingResourceCountForTest() {
221
+ __classPrivateFieldGet(this, _RampsController_pendingResourceCount, "f").clear();
222
+ }
176
223
  /**
177
224
  * Constructs a new {@link RampsController}.
178
225
  *
@@ -209,6 +256,21 @@ class RampsController extends base_controller_1.BaseController {
209
256
  * Key is the cache key, value is the pending request with abort controller.
210
257
  */
211
258
  _RampsController_pendingRequests.set(this, new Map());
259
+ /**
260
+ * Count of in-flight requests per resource type.
261
+ * Used so isLoading is only cleared when the last request for that resource finishes.
262
+ */
263
+ _RampsController_pendingResourceCount.set(this, new Map());
264
+ /**
265
+ * Interval ID for automatic quote polling.
266
+ * Set when startQuotePolling() is called, cleared when stopQuotePolling() is called.
267
+ */
268
+ _RampsController_quotePollingInterval.set(this, null);
269
+ /**
270
+ * Options used for quote polling (walletAddress, amount, redirectUrl).
271
+ * Stored so polling can be restarted when dependencies change.
272
+ */
273
+ _RampsController_quotePollingOptions.set(this, null);
212
274
  __classPrivateFieldSet(this, _RampsController_requestCacheTTL, requestCacheTTL, "f");
213
275
  __classPrivateFieldSet(this, _RampsController_requestCacheMaxSize, requestCacheMaxSize, "f");
214
276
  }
@@ -240,8 +302,18 @@ class RampsController extends base_controller_1.BaseController {
240
302
  // Create abort controller for this request
241
303
  const abortController = new AbortController();
242
304
  const lastFetchedAt = Date.now();
305
+ const { resourceType } = options ?? {};
243
306
  // Update state to loading
244
307
  __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_updateRequestState).call(this, cacheKey, (0, RequestCache_1.createLoadingState)());
308
+ // Set resource-level loading state (only on cache miss). Ref-count so concurrent
309
+ // requests for the same resource type (different cache keys) keep isLoading true.
310
+ if (resourceType) {
311
+ const count = __classPrivateFieldGet(this, _RampsController_pendingResourceCount, "f").get(resourceType) ?? 0;
312
+ __classPrivateFieldGet(this, _RampsController_pendingResourceCount, "f").set(resourceType, count + 1);
313
+ if (count === 0) {
314
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_setResourceLoading).call(this, resourceType, true);
315
+ }
316
+ }
245
317
  // Create the fetch promise
246
318
  const promise = (async () => {
247
319
  try {
@@ -251,6 +323,15 @@ class RampsController extends base_controller_1.BaseController {
251
323
  throw new Error('Request was aborted');
252
324
  }
253
325
  __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_updateRequestState).call(this, cacheKey, (0, RequestCache_1.createSuccessState)(data, lastFetchedAt));
326
+ if (resourceType) {
327
+ // We need the extra logic because there are two situations where we’re allowed to clear the error:
328
+ // No callback → always clear
329
+ // Callback present → clear only when isResultCurrent() returns true.
330
+ const isCurrent = !options?.isResultCurrent || options.isResultCurrent();
331
+ if (isCurrent) {
332
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_setResourceError).call(this, resourceType, null);
333
+ }
334
+ }
254
335
  return data;
255
336
  }
256
337
  catch (error) {
@@ -258,8 +339,14 @@ class RampsController extends base_controller_1.BaseController {
258
339
  if (abortController.signal.aborted) {
259
340
  throw error;
260
341
  }
261
- const errorMessage = error?.message;
262
- __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_updateRequestState).call(this, cacheKey, (0, RequestCache_1.createErrorState)(errorMessage ?? 'Unknown error', lastFetchedAt));
342
+ const errorMessage = error?.message ?? 'Unknown error';
343
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_updateRequestState).call(this, cacheKey, (0, RequestCache_1.createErrorState)(errorMessage, lastFetchedAt));
344
+ if (resourceType) {
345
+ const isCurrent = !options?.isResultCurrent || options.isResultCurrent();
346
+ if (isCurrent) {
347
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_setResourceError).call(this, resourceType, errorMessage);
348
+ }
349
+ }
263
350
  throw error;
264
351
  }
265
352
  finally {
@@ -268,6 +355,18 @@ class RampsController extends base_controller_1.BaseController {
268
355
  if (currentPending?.abortController === abortController) {
269
356
  __classPrivateFieldGet(this, _RampsController_pendingRequests, "f").delete(cacheKey);
270
357
  }
358
+ // Clear resource-level loading state only when no requests for this resource remain
359
+ if (resourceType) {
360
+ const count = __classPrivateFieldGet(this, _RampsController_pendingResourceCount, "f").get(resourceType) ?? 0;
361
+ const next = Math.max(0, count - 1);
362
+ if (next === 0) {
363
+ __classPrivateFieldGet(this, _RampsController_pendingResourceCount, "f").delete(resourceType);
364
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_setResourceLoading).call(this, resourceType, false);
365
+ }
366
+ else {
367
+ __classPrivateFieldGet(this, _RampsController_pendingResourceCount, "f").set(resourceType, next);
368
+ }
369
+ }
271
370
  }
272
371
  })();
273
372
  // Store pending request for deduplication
@@ -310,37 +409,43 @@ class RampsController extends base_controller_1.BaseController {
310
409
  async setUserRegion(region, options) {
311
410
  const normalizedRegion = region.toLowerCase().trim();
312
411
  try {
313
- const { countries } = this.state;
314
- if (!countries || countries.length === 0) {
412
+ const countriesData = this.state.countries.data;
413
+ if (!countriesData || countriesData.length === 0) {
315
414
  __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_cleanupState).call(this);
316
415
  throw new Error('No countries found. Cannot set user region without valid country information.');
317
416
  }
318
- const userRegion = findRegionFromCode(normalizedRegion, countries);
417
+ const userRegion = findRegionFromCode(normalizedRegion, countriesData);
319
418
  if (!userRegion) {
320
419
  __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_cleanupState).call(this);
321
420
  throw new Error(`Region "${normalizedRegion}" not found in countries data. Cannot set user region without valid country information.`);
322
421
  }
323
- // Only cleanup state if region is actually changing
324
422
  const regionChanged = normalizedRegion !== this.state.userRegion?.regionCode;
325
- // Set the new region atomically with cleanup to avoid intermediate null state
423
+ const needsRefetch = regionChanged ||
424
+ !this.state.tokens.data ||
425
+ this.state.providers.data.length === 0;
426
+ if (regionChanged) {
427
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_clearPendingResourceCountForDependentResources).call(this);
428
+ }
429
+ if (regionChanged) {
430
+ this.stopQuotePolling();
431
+ }
326
432
  this.update((state) => {
327
433
  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;
434
+ resetDependentResources(state);
335
435
  }
336
436
  state.userRegion = userRegion;
337
437
  });
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);
438
+ if (needsRefetch) {
439
+ const refetchPromises = [];
440
+ if (regionChanged || !this.state.tokens.data) {
441
+ refetchPromises.push(this.getTokens(userRegion.regionCode, 'buy', options));
442
+ }
443
+ if (regionChanged || this.state.providers.data.length === 0) {
444
+ refetchPromises.push(this.getProviders(userRegion.regionCode, options));
445
+ }
446
+ if (refetchPromises.length > 0) {
447
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_fireAndForget).call(this, Promise.all(refetchPromises));
448
+ }
344
449
  }
345
450
  return userRegion;
346
451
  }
@@ -359,10 +464,11 @@ class RampsController extends base_controller_1.BaseController {
359
464
  */
360
465
  setSelectedProvider(providerId) {
361
466
  if (providerId === null) {
467
+ this.stopQuotePolling();
362
468
  this.update((state) => {
363
- state.selectedProvider = null;
364
- state.paymentMethods = [];
365
- state.selectedPaymentMethod = null;
469
+ state.providers.selected = null;
470
+ state.paymentMethods.data = [];
471
+ state.paymentMethods.selected = null;
366
472
  });
367
473
  return;
368
474
  }
@@ -370,7 +476,7 @@ class RampsController extends base_controller_1.BaseController {
370
476
  if (!regionCode) {
371
477
  throw new Error('Region is required. Cannot set selected provider without valid region information.');
372
478
  }
373
- const { providers } = this.state;
479
+ const providers = this.state.providers.data;
374
480
  if (!providers || providers.length === 0) {
375
481
  throw new Error('Providers not loaded. Cannot set selected provider before providers are fetched.');
376
482
  }
@@ -379,16 +485,16 @@ class RampsController extends base_controller_1.BaseController {
379
485
  throw new Error(`Provider with ID "${providerId}" not found in available providers.`);
380
486
  }
381
487
  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,
488
+ state.providers.selected = provider;
489
+ state.paymentMethods.data = [];
490
+ state.paymentMethods.selected = null;
491
+ state.quotes.selected = null;
391
492
  });
493
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_fireAndForget).call(this, this.getPaymentMethods(regionCode, { provider: provider.id }).then(() => {
494
+ // Restart quote polling after payment methods are fetched
495
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_restartPollingIfActive).call(this);
496
+ return undefined;
497
+ }));
392
498
  }
393
499
  /**
394
500
  * Initializes the controller by fetching the user's region from geolocation.
@@ -414,8 +520,8 @@ class RampsController extends base_controller_1.BaseController {
414
520
  if (!regionCode) {
415
521
  throw new Error('Region code is required. Cannot hydrate state without valid region information.');
416
522
  }
417
- this.triggerGetTokens(regionCode, 'buy', options);
418
- this.triggerGetProviders(regionCode, options);
523
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_fireAndForget).call(this, this.getTokens(regionCode, 'buy', options));
524
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_fireAndForget).call(this, this.getProviders(regionCode, options));
419
525
  }
420
526
  /**
421
527
  * Fetches the list of supported countries.
@@ -429,9 +535,9 @@ class RampsController extends base_controller_1.BaseController {
429
535
  const cacheKey = (0, RequestCache_1.createCacheKey)('getCountries', []);
430
536
  const countries = await this.executeRequest(cacheKey, async () => {
431
537
  return this.messenger.call('RampsService:getCountries');
432
- }, options);
538
+ }, { ...options, resourceType: 'countries' });
433
539
  this.update((state) => {
434
- state.countries = countries;
540
+ state.countries.data = countries;
435
541
  });
436
542
  return countries;
437
543
  }
@@ -460,11 +566,16 @@ class RampsController extends base_controller_1.BaseController {
460
566
  return this.messenger.call('RampsService:getTokens', normalizedRegion, action, {
461
567
  provider: options?.provider,
462
568
  });
463
- }, options);
569
+ }, {
570
+ ...options,
571
+ resourceType: 'tokens',
572
+ isResultCurrent: () => this.state.userRegion?.regionCode === undefined ||
573
+ this.state.userRegion?.regionCode === normalizedRegion,
574
+ });
464
575
  this.update((state) => {
465
576
  const userRegionCode = state.userRegion?.regionCode;
466
577
  if (userRegionCode === undefined || userRegionCode === normalizedRegion) {
467
- state.tokens = tokens;
578
+ state.tokens.data = tokens;
468
579
  }
469
580
  });
470
581
  return tokens;
@@ -479,10 +590,11 @@ class RampsController extends base_controller_1.BaseController {
479
590
  */
480
591
  setSelectedToken(assetId) {
481
592
  if (!assetId) {
593
+ this.stopQuotePolling();
482
594
  this.update((state) => {
483
- state.selectedToken = null;
484
- state.paymentMethods = [];
485
- state.selectedPaymentMethod = null;
595
+ state.tokens.selected = null;
596
+ state.paymentMethods.data = [];
597
+ state.paymentMethods.selected = null;
486
598
  });
487
599
  return;
488
600
  }
@@ -490,7 +602,7 @@ class RampsController extends base_controller_1.BaseController {
490
602
  if (!regionCode) {
491
603
  throw new Error('Region is required. Cannot set selected token without valid region information.');
492
604
  }
493
- const { tokens } = this.state;
605
+ const tokens = this.state.tokens.data;
494
606
  if (!tokens) {
495
607
  throw new Error('Tokens not loaded. Cannot set selected token before tokens are fetched.');
496
608
  }
@@ -500,13 +612,16 @@ class RampsController extends base_controller_1.BaseController {
500
612
  throw new Error(`Token with asset ID "${assetId}" not found in available tokens.`);
501
613
  }
502
614
  this.update((state) => {
503
- state.selectedToken = token;
504
- state.paymentMethods = [];
505
- state.selectedPaymentMethod = null;
506
- });
507
- this.triggerGetPaymentMethods(regionCode, {
508
- assetId: token.assetId,
615
+ state.tokens.selected = token;
616
+ state.paymentMethods.data = [];
617
+ state.paymentMethods.selected = null;
618
+ state.quotes.selected = null;
509
619
  });
620
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_fireAndForget).call(this, this.getPaymentMethods(regionCode, { assetId: token.assetId }).then(() => {
621
+ // Restart quote polling after payment methods are fetched
622
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_restartPollingIfActive).call(this);
623
+ return undefined;
624
+ }));
510
625
  }
511
626
  /**
512
627
  * Fetches the list of providers for a given region.
@@ -540,11 +655,16 @@ class RampsController extends base_controller_1.BaseController {
540
655
  fiat: options?.fiat,
541
656
  payments: options?.payments,
542
657
  });
543
- }, options);
658
+ }, {
659
+ ...options,
660
+ resourceType: 'providers',
661
+ isResultCurrent: () => this.state.userRegion?.regionCode === undefined ||
662
+ this.state.userRegion?.regionCode === normalizedRegion,
663
+ });
544
664
  this.update((state) => {
545
665
  const userRegionCode = state.userRegion?.regionCode;
546
666
  if (userRegionCode === undefined || userRegionCode === normalizedRegion) {
547
- state.providers = providers;
667
+ state.providers.data = providers;
548
668
  }
549
669
  });
550
670
  return { providers };
@@ -563,8 +683,8 @@ class RampsController extends base_controller_1.BaseController {
563
683
  async getPaymentMethods(region, options) {
564
684
  const regionCode = region ?? this.state.userRegion?.regionCode ?? null;
565
685
  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 ?? '';
686
+ const assetIdToUse = options?.assetId ?? this.state.tokens.selected?.assetId ?? '';
687
+ const providerToUse = options?.provider ?? this.state.providers.selected?.id ?? '';
568
688
  if (!regionCode) {
569
689
  throw new Error('Region is required. Either provide a region parameter or ensure userRegion is set in controller state.');
570
690
  }
@@ -586,21 +706,31 @@ class RampsController extends base_controller_1.BaseController {
586
706
  assetId: assetIdToUse,
587
707
  provider: providerToUse,
588
708
  });
589
- }, options);
709
+ }, {
710
+ ...options,
711
+ resourceType: 'paymentMethods',
712
+ isResultCurrent: () => {
713
+ const regionMatch = this.state.userRegion?.regionCode === undefined ||
714
+ this.state.userRegion?.regionCode === normalizedRegion;
715
+ const tokenMatch = (this.state.tokens.selected?.assetId ?? '') === assetIdToUse;
716
+ const providerMatch = (this.state.providers.selected?.id ?? '') === providerToUse;
717
+ return regionMatch && tokenMatch && providerMatch;
718
+ },
719
+ });
590
720
  this.update((state) => {
591
- const currentAssetId = state.selectedToken?.assetId ?? '';
592
- const currentProviderId = state.selectedProvider?.id ?? '';
721
+ const currentAssetId = state.tokens.selected?.assetId ?? '';
722
+ const currentProviderId = state.providers.selected?.id ?? '';
593
723
  const tokenSelectionUnchanged = assetIdToUse === currentAssetId;
594
724
  const providerSelectionUnchanged = providerToUse === currentProviderId;
595
725
  // 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
726
  // ex: if the user rapidly changes the token or provider, the in-flight payment methods might not be valid
597
727
  // so this check will ensure that the payment methods are still valid for the token and provider that were requested
598
728
  if (tokenSelectionUnchanged && providerSelectionUnchanged) {
599
- state.paymentMethods = response.payments;
729
+ state.paymentMethods.data = response.payments;
600
730
  // 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);
731
+ const currentSelectionStillValid = response.payments.some((pm) => pm.id === state.paymentMethods.selected?.id);
602
732
  if (!currentSelectionStillValid) {
603
- state.selectedPaymentMethod = response.payments[0] ?? null;
733
+ state.paymentMethods.selected = response.payments[0] ?? null;
604
734
  }
605
735
  }
606
736
  });
@@ -616,11 +746,11 @@ class RampsController extends base_controller_1.BaseController {
616
746
  setSelectedPaymentMethod(paymentMethodId) {
617
747
  if (!paymentMethodId) {
618
748
  this.update((state) => {
619
- state.selectedPaymentMethod = null;
749
+ state.paymentMethods.selected = null;
620
750
  });
621
751
  return;
622
752
  }
623
- const { paymentMethods } = this.state;
753
+ const paymentMethods = this.state.paymentMethods.data;
624
754
  if (!paymentMethods || paymentMethods.length === 0) {
625
755
  throw new Error('Payment methods not loaded. Cannot set selected payment method before payment methods are fetched.');
626
756
  }
@@ -629,8 +759,10 @@ class RampsController extends base_controller_1.BaseController {
629
759
  throw new Error(`Payment method with ID "${paymentMethodId}" not found in available payment methods.`);
630
760
  }
631
761
  this.update((state) => {
632
- state.selectedPaymentMethod = paymentMethod;
762
+ state.paymentMethods.selected = paymentMethod;
633
763
  });
764
+ // Restart quote polling if active
765
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_restartPollingIfActive).call(this);
634
766
  }
635
767
  /**
636
768
  * Fetches quotes from all providers for a given set of parameters.
@@ -654,7 +786,7 @@ class RampsController extends base_controller_1.BaseController {
654
786
  const regionToUse = options.region ?? this.state.userRegion?.regionCode;
655
787
  const fiatToUse = options.fiat ?? this.state.userRegion?.country?.currency;
656
788
  const paymentMethodsToUse = options.paymentMethods ??
657
- this.state.paymentMethods.map((pm) => pm.id);
789
+ this.state.paymentMethods.data.map((pm) => pm.id);
658
790
  const action = options.action ?? 'buy';
659
791
  if (!regionToUse) {
660
792
  throw new Error('Region is required. Either provide a region parameter or ensure userRegion is set in controller state.');
@@ -705,127 +837,171 @@ class RampsController extends base_controller_1.BaseController {
705
837
  }, {
706
838
  forceRefresh: options.forceRefresh,
707
839
  ttl: options.ttl ?? DEFAULT_QUOTES_TTL,
840
+ resourceType: 'quotes',
841
+ isResultCurrent: () => this.state.userRegion?.regionCode === undefined ||
842
+ this.state.userRegion?.regionCode === normalizedRegion,
708
843
  });
709
844
  this.update((state) => {
710
845
  const userRegionCode = state.userRegion?.regionCode;
711
846
  if (userRegionCode === undefined || userRegionCode === normalizedRegion) {
712
- state.quotes = response;
847
+ state.quotes.data = response;
713
848
  }
714
849
  });
715
850
  return response;
716
851
  }
717
852
  /**
718
- * Extracts the widget URL from a quote for redirect providers.
719
- * Returns the widget URL if available, or null if the quote doesn't have one.
720
- *
721
- * @param quote - The quote to extract the widget URL from.
722
- * @returns The widget URL string, or null if not available.
723
- */
724
- getWidgetUrl(quote) {
725
- return quote.quote?.widgetUrl ?? null;
726
- }
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.
853
+ * Starts automatic quote polling with a 15-second refresh interval.
854
+ * Fetches quotes immediately and then every 15 seconds.
855
+ * If the response contains exactly one quote, it is auto-selected.
856
+ * If multiple quotes are returned, the existing selection is preserved if still valid.
745
857
  *
746
- * @param options - Options for cache behavior.
858
+ * @param options - Parameters for fetching quotes.
859
+ * @param options.walletAddress - The destination wallet address.
860
+ * @param options.amount - The amount (in fiat for buy, crypto for sell).
861
+ * @param options.redirectUrl - Optional redirect URL after order completion.
862
+ * @throws If required dependencies (region, token, payment method) are not set.
747
863
  */
748
- triggerGetCountries(options) {
749
- this.getCountries(options).catch(() => {
750
- // Error stored in state
751
- });
864
+ startQuotePolling(options) {
865
+ // Validate required dependencies
866
+ const regionCode = this.state.userRegion?.regionCode;
867
+ const token = this.state.tokens.selected;
868
+ const paymentMethod = this.state.paymentMethods.selected;
869
+ if (!regionCode) {
870
+ throw new Error('Region is required. Cannot start quote polling without valid region information.');
871
+ }
872
+ if (!token) {
873
+ throw new Error('Token is required. Cannot start quote polling without a selected token.');
874
+ }
875
+ if (!paymentMethod) {
876
+ throw new Error('Payment method is required. Cannot start quote polling without a selected payment method.');
877
+ }
878
+ // Stop any existing polling first
879
+ this.stopQuotePolling();
880
+ // Store options for restarts (must be after stop to avoid being cleared)
881
+ __classPrivateFieldSet(this, _RampsController_quotePollingOptions, options, "f");
882
+ // Define the fetch function
883
+ const fetchQuotes = () => {
884
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_fireAndForget).call(this, this.getQuotes({
885
+ assetId: token.assetId,
886
+ amount: options.amount,
887
+ walletAddress: options.walletAddress,
888
+ redirectUrl: options.redirectUrl,
889
+ paymentMethods: [paymentMethod.id],
890
+ forceRefresh: true,
891
+ }).then((response) => {
892
+ // Auto-select logic: only when exactly one quote is returned
893
+ this.update((state) => {
894
+ if (response.success.length === 1) {
895
+ state.quotes.selected = response.success[0];
896
+ }
897
+ else {
898
+ // Keep existing selection if still valid, but update with fresh data
899
+ const currentSelection = state.quotes.selected;
900
+ if (currentSelection) {
901
+ const freshQuote = response.success.find((quote) => quote.provider === currentSelection.provider &&
902
+ quote.quote.paymentMethod ===
903
+ currentSelection.quote.paymentMethod);
904
+ // Update with fresh quote data, or clear if no longer valid
905
+ state.quotes.selected = freshQuote ?? null;
906
+ }
907
+ }
908
+ });
909
+ return undefined;
910
+ }));
911
+ };
912
+ // Fetch immediately
913
+ fetchQuotes();
914
+ // Set up 15-second polling
915
+ __classPrivateFieldSet(this, _RampsController_quotePollingInterval, setInterval(fetchQuotes, 15000), "f");
752
916
  }
753
917
  /**
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.
918
+ * Stops automatic quote polling.
919
+ * Does not clear quotes data or selection, only stops the interval.
759
920
  */
760
- triggerGetTokens(region, action = 'buy', options) {
761
- this.getTokens(region, action, options).catch(() => {
762
- // Error stored in state
763
- });
921
+ stopQuotePolling() {
922
+ if (__classPrivateFieldGet(this, _RampsController_quotePollingInterval, "f") !== null) {
923
+ clearInterval(__classPrivateFieldGet(this, _RampsController_quotePollingInterval, "f"));
924
+ __classPrivateFieldSet(this, _RampsController_quotePollingInterval, null, "f");
925
+ }
926
+ __classPrivateFieldSet(this, _RampsController_quotePollingOptions, null, "f");
764
927
  }
765
928
  /**
766
- * Triggers fetching providers without throwing.
929
+ * Manually sets the selected quote.
767
930
  *
768
- * @param region - The region code. If not provided, uses userRegion from state.
769
- * @param options - Options for cache behavior and query filters.
931
+ * @param quote - The quote to select, or null to clear the selection.
770
932
  */
771
- triggerGetProviders(region, options) {
772
- this.getProviders(region, options).catch(() => {
773
- // Error stored in state
933
+ setSelectedQuote(quote) {
934
+ this.update((state) => {
935
+ state.quotes.selected = quote;
774
936
  });
775
937
  }
776
938
  /**
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.
939
+ * Cleans up controller resources.
940
+ * Stops any active quote polling to prevent memory leaks.
941
+ * Should be called when the controller is no longer needed.
784
942
  */
785
- triggerGetPaymentMethods(region, options) {
786
- this.getPaymentMethods(region, options).catch(() => {
787
- // Error stored in state
788
- });
943
+ destroy() {
944
+ this.stopQuotePolling();
945
+ super.destroy();
789
946
  }
790
947
  /**
791
- * Triggers fetching quotes without throwing.
948
+ * Extracts the widget URL from a quote for redirect providers.
949
+ * Returns the widget URL if available, or null if the quote doesn't have one.
792
950
  *
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.
951
+ * @param quote - The quote to extract the widget URL from.
952
+ * @returns The widget URL string, or null if not available.
805
953
  */
806
- triggerGetQuotes(options) {
807
- this.getQuotes(options).catch(() => {
808
- // Error stored in state
809
- });
954
+ getWidgetUrl(quote) {
955
+ return quote.quote?.widgetUrl ?? null;
810
956
  }
811
957
  }
812
958
  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) {
959
+ _RampsController_requestCacheTTL = new WeakMap(), _RampsController_requestCacheMaxSize = new WeakMap(), _RampsController_pendingRequests = new WeakMap(), _RampsController_pendingResourceCount = new WeakMap(), _RampsController_quotePollingInterval = new WeakMap(), _RampsController_quotePollingOptions = new WeakMap(), _RampsController_instances = new WeakSet(), _RampsController_clearPendingResourceCountForDependentResources = function _RampsController_clearPendingResourceCountForDependentResources() {
960
+ const types = [
961
+ 'providers',
962
+ 'tokens',
963
+ 'paymentMethods',
964
+ 'quotes',
965
+ ];
966
+ for (const resourceType of types) {
967
+ __classPrivateFieldGet(this, _RampsController_pendingResourceCount, "f").delete(resourceType);
968
+ }
969
+ }, _RampsController_removeRequestState = function _RampsController_removeRequestState(cacheKey) {
814
970
  this.update((state) => {
815
971
  const requests = state.requests;
816
972
  delete requests[cacheKey];
817
973
  });
818
974
  }, _RampsController_cleanupState = function _RampsController_cleanupState() {
975
+ this.stopQuotePolling();
976
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_clearPendingResourceCountForDependentResources).call(this);
977
+ this.update((state) => resetDependentResources(state, {
978
+ clearUserRegionData: true,
979
+ }));
980
+ }, _RampsController_fireAndForget = function _RampsController_fireAndForget(promise) {
981
+ promise.catch((_error) => undefined);
982
+ }, _RampsController_restartPollingIfActive = function _RampsController_restartPollingIfActive() {
983
+ if (__classPrivateFieldGet(this, _RampsController_quotePollingInterval, "f") !== null && __classPrivateFieldGet(this, _RampsController_quotePollingOptions, "f")) {
984
+ const options = __classPrivateFieldGet(this, _RampsController_quotePollingOptions, "f");
985
+ this.stopQuotePolling();
986
+ try {
987
+ this.startQuotePolling(options);
988
+ }
989
+ catch {
990
+ // Dependencies not met yet, polling will need to be manually restarted
991
+ // when dependencies are available
992
+ }
993
+ }
994
+ }, _RampsController_updateResourceField = function _RampsController_updateResourceField(resourceType, field, value) {
819
995
  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;
996
+ const resource = state[resourceType];
997
+ if (resource) {
998
+ resource[field] = value;
999
+ }
828
1000
  });
1001
+ }, _RampsController_setResourceLoading = function _RampsController_setResourceLoading(resourceType, loading) {
1002
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_updateResourceField).call(this, resourceType, 'isLoading', loading);
1003
+ }, _RampsController_setResourceError = function _RampsController_setResourceError(resourceType, error) {
1004
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_updateResourceField).call(this, resourceType, 'error', error);
829
1005
  }, _RampsController_updateRequestState = function _RampsController_updateRequestState(cacheKey, requestState) {
830
1006
  const maxSize = __classPrivateFieldGet(this, _RampsController_requestCacheMaxSize, "f");
831
1007
  const ttl = __classPrivateFieldGet(this, _RampsController_requestCacheTTL, "f");