@metamask-previews/ramps-controller 8.0.0-preview-27e39dd44 → 8.0.0-preview-d7935cb09

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
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ### Added
11
+
12
+ - Add `widgetUrl` resource state that automatically fetches and stores the buy widget URL whenever the selected quote changes ([#7920](https://github.com/MetaMask/core/pull/7920))
13
+
14
+ ### Changed
15
+
16
+ - Refactor: Consolidate reset logic with a shared resetResource helper and fix abort handling for dependent resources ([#7818](https://github.com/MetaMask/core/pull/7818))
17
+
10
18
  ## [8.0.0]
11
19
 
12
20
  ### Changed
@@ -10,7 +10,7 @@ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (
10
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");
11
11
  return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
12
12
  };
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;
13
+ var _RampsController_instances, _RampsController_requestCacheTTL, _RampsController_requestCacheMaxSize, _RampsController_pendingRequests, _RampsController_pendingResourceCount, _RampsController_quotePollingInterval, _RampsController_quotePollingOptions, _RampsController_clearPendingResourceCountForDependentResources, _RampsController_abortDependentRequests, _RampsController_mutateRequests, _RampsController_removeRequestState, _RampsController_cleanupState, _RampsController_fireAndForget, _RampsController_restartPollingIfActive, _RampsController_requireRegion, _RampsController_isRegionCurrent, _RampsController_isTokenCurrent, _RampsController_isProviderCurrent, _RampsController_updateResourceField, _RampsController_setResourceLoading, _RampsController_setResourceError, _RampsController_updateRequestState, _RampsController_syncWidgetUrl;
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
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");
@@ -81,6 +81,12 @@ const rampsControllerMetadata = {
81
81
  includeInStateLogs: false,
82
82
  usedInUi: true,
83
83
  },
84
+ widgetUrl: {
85
+ persist: false,
86
+ includeInDebugSnapshot: true,
87
+ includeInStateLogs: false,
88
+ usedInUi: true,
89
+ },
84
90
  requests: {
85
91
  persist: false,
86
92
  includeInDebugSnapshot: true,
@@ -121,10 +127,39 @@ function getDefaultRampsControllerState() {
121
127
  tokens: createDefaultResourceState(null, null),
122
128
  paymentMethods: createDefaultResourceState([], null),
123
129
  quotes: createDefaultResourceState(null, null),
130
+ widgetUrl: createDefaultResourceState(null),
124
131
  requests: {},
125
132
  };
126
133
  }
127
134
  exports.getDefaultRampsControllerState = getDefaultRampsControllerState;
135
+ const DEPENDENT_RESOURCE_KEYS = [
136
+ 'providers',
137
+ 'tokens',
138
+ 'paymentMethods',
139
+ 'quotes',
140
+ ];
141
+ const DEPENDENT_RESOURCE_KEYS_SET = new Set(DEPENDENT_RESOURCE_KEYS);
142
+ function resetResource(state, resourceType, defaultResource) {
143
+ const def = defaultResource ?? getDefaultRampsControllerState()[resourceType];
144
+ const resource = state[resourceType];
145
+ resource.data = def.data;
146
+ resource.selected = def.selected;
147
+ resource.isLoading = def.isLoading;
148
+ resource.error = def.error;
149
+ }
150
+ /**
151
+ * Resets the widgetUrl resource to its default state.
152
+ * Mutates state in place; use from within controller update() for atomic updates.
153
+ *
154
+ * @param state - The state object to mutate.
155
+ */
156
+ function resetWidgetUrl(state) {
157
+ const def = getDefaultRampsControllerState().widgetUrl;
158
+ state.widgetUrl.data = def.data;
159
+ state.widgetUrl.selected = def.selected;
160
+ state.widgetUrl.isLoading = def.isLoading;
161
+ state.widgetUrl.error = def.error;
162
+ }
128
163
  /**
129
164
  * Resets region-dependent resources (userRegion, providers, tokens, paymentMethods, quotes).
130
165
  * Mutates state in place; use from within controller update() for atomic updates.
@@ -137,22 +172,11 @@ function resetDependentResources(state, options) {
137
172
  if (options?.clearUserRegionData) {
138
173
  state.userRegion = null;
139
174
  }
140
- state.providers.selected = null;
141
- state.providers.data = [];
142
- state.providers.isLoading = false;
143
- state.providers.error = null;
144
- state.tokens.selected = null;
145
- state.tokens.data = null;
146
- state.tokens.isLoading = false;
147
- state.tokens.error = null;
148
- state.paymentMethods.data = [];
149
- state.paymentMethods.selected = null;
150
- state.paymentMethods.isLoading = false;
151
- state.paymentMethods.error = null;
152
- state.quotes.data = null;
153
- state.quotes.selected = null;
154
- state.quotes.isLoading = false;
155
- state.quotes.error = null;
175
+ const defaultState = getDefaultRampsControllerState();
176
+ for (const key of DEPENDENT_RESOURCE_KEYS) {
177
+ resetResource(state, key, defaultState[key]);
178
+ }
179
+ resetWidgetUrl(state);
156
180
  }
157
181
  // === HELPER FUNCTIONS ===
158
182
  /**
@@ -276,31 +300,49 @@ class RampsController extends base_controller_1.BaseController {
276
300
  __classPrivateFieldSet(this, _RampsController_requestCacheMaxSize, requestCacheMaxSize, "f");
277
301
  }
278
302
  /**
279
- * Executes a request with caching and deduplication.
303
+ * Executes a request with caching, deduplication, and at most one in-flight
304
+ * request per resource type.
280
305
  *
281
- * If a request with the same cache key is already in flight, returns the
282
- * existing promise. If valid cached data exists, returns it without making
283
- * a new request.
306
+ * 1. **Same cache key in flight** – If a request with this cache key is
307
+ * already pending, returns that promise (deduplication; no second request).
284
308
  *
285
- * @param cacheKey - Unique identifier for this request.
286
- * @param fetcher - Function that performs the actual fetch. Receives an AbortSignal.
287
- * @param options - Options for cache behavior.
288
- * @returns The result of the request.
309
+ * 2. **Cache hit** – If valid, non-expired data exists in state.requests for
310
+ * this key and forceRefresh is not set, returns that data without fetching.
311
+ *
312
+ * 3. **New request** Creates an AbortController and fires the fetcher.
313
+ * If options.resourceType is set, tags the pending request with that
314
+ * resource type (so #abortDependentRequests can cancel it on region
315
+ * change or cleanup) and ref-counts resource-level loading state.
316
+ * On success or error, updates request state and resource error;
317
+ * in finally, clears resource loading only if this request was not
318
+ * aborted.
319
+ *
320
+ * @param cacheKey - Unique identifier for this request (e.g. from createCacheKey).
321
+ * @param fetcher - Async function that performs the fetch. Receives an AbortSignal
322
+ * that is aborted when this request is superseded by another for the same resource.
323
+ * @param options - Optional forceRefresh, ttl, and resourceType for loading/error state.
324
+ * @returns The result of the request (from cache, joined promise, or fetcher).
289
325
  */
290
326
  async executeRequest(cacheKey, fetcher, options) {
327
+ // Get TTL for verifying cache expiration
291
328
  const ttl = options?.ttl ?? __classPrivateFieldGet(this, _RampsController_requestCacheTTL, "f");
292
- // Check for existing pending request - join it instead of making a duplicate
329
+ // DEDUPLICATION:
330
+ // Check if a request is already in flight for this cache key
331
+ // If so, return the original promise for that request
293
332
  const pending = __classPrivateFieldGet(this, _RampsController_pendingRequests, "f").get(cacheKey);
294
333
  if (pending) {
295
334
  return pending.promise;
296
335
  }
336
+ // CACHE HIT:
337
+ // If cache is not expired, return the cached data
297
338
  if (!options?.forceRefresh) {
298
339
  const cached = this.state.requests[cacheKey];
299
340
  if (cached && !(0, RequestCache_1.isCacheExpired)(cached, ttl)) {
300
341
  return cached.data;
301
342
  }
302
343
  }
303
- // Create abort controller for this request
344
+ // Create a new abort controller for this request
345
+ // Record the time the request was started
304
346
  const abortController = new AbortController();
305
347
  const lastFetchedAt = Date.now();
306
348
  const { resourceType } = options ?? {};
@@ -319,15 +361,11 @@ class RampsController extends base_controller_1.BaseController {
319
361
  const promise = (async () => {
320
362
  try {
321
363
  const data = await fetcher(abortController.signal);
322
- // Don't update state if aborted
323
364
  if (abortController.signal.aborted) {
324
365
  throw new Error('Request was aborted');
325
366
  }
326
367
  __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_updateRequestState).call(this, cacheKey, (0, RequestCache_1.createSuccessState)(data, lastFetchedAt));
327
368
  if (resourceType) {
328
- // We need the extra logic because there are two situations where we’re allowed to clear the error:
329
- // No callback → always clear
330
- // Callback present → clear only when isResultCurrent() returns true.
331
369
  const isCurrent = !options?.isResultCurrent || options.isResultCurrent();
332
370
  if (isCurrent) {
333
371
  __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_setResourceError).call(this, resourceType, null);
@@ -336,7 +374,6 @@ class RampsController extends base_controller_1.BaseController {
336
374
  return data;
337
375
  }
338
376
  catch (error) {
339
- // Don't update state if aborted
340
377
  if (abortController.signal.aborted) {
341
378
  throw error;
342
379
  }
@@ -351,13 +388,12 @@ class RampsController extends base_controller_1.BaseController {
351
388
  throw error;
352
389
  }
353
390
  finally {
354
- // Only delete if this is still our entry (not replaced by a new request)
355
- const currentPending = __classPrivateFieldGet(this, _RampsController_pendingRequests, "f").get(cacheKey);
356
- if (currentPending?.abortController === abortController) {
391
+ if (__classPrivateFieldGet(this, _RampsController_pendingRequests, "f").get(cacheKey)?.abortController ===
392
+ abortController) {
357
393
  __classPrivateFieldGet(this, _RampsController_pendingRequests, "f").delete(cacheKey);
358
394
  }
359
395
  // Clear resource-level loading state only when no requests for this resource remain
360
- if (resourceType) {
396
+ if (resourceType && !abortController.signal.aborted) {
361
397
  const count = __classPrivateFieldGet(this, _RampsController_pendingResourceCount, "f").get(resourceType) ?? 0;
362
398
  const next = Math.max(0, count - 1);
363
399
  if (next === 0) {
@@ -370,8 +406,11 @@ class RampsController extends base_controller_1.BaseController {
370
406
  }
371
407
  }
372
408
  })();
373
- // Store pending request for deduplication
374
- __classPrivateFieldGet(this, _RampsController_pendingRequests, "f").set(cacheKey, { promise, abortController });
409
+ __classPrivateFieldGet(this, _RampsController_pendingRequests, "f").set(cacheKey, {
410
+ promise,
411
+ abortController,
412
+ resourceType,
413
+ });
375
414
  return promise;
376
415
  }
377
416
  /**
@@ -425,9 +464,8 @@ class RampsController extends base_controller_1.BaseController {
425
464
  !this.state.tokens.data ||
426
465
  this.state.providers.data.length === 0;
427
466
  if (regionChanged) {
467
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_abortDependentRequests).call(this);
428
468
  __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_clearPendingResourceCountForDependentResources).call(this);
429
- }
430
- if (regionChanged) {
431
469
  this.stopQuotePolling();
432
470
  }
433
471
  this.update((state) => {
@@ -468,15 +506,11 @@ class RampsController extends base_controller_1.BaseController {
468
506
  this.stopQuotePolling();
469
507
  this.update((state) => {
470
508
  state.providers.selected = null;
471
- state.paymentMethods.data = [];
472
- state.paymentMethods.selected = null;
509
+ resetResource(state, 'paymentMethods');
473
510
  });
474
511
  return;
475
512
  }
476
- const regionCode = this.state.userRegion?.regionCode;
477
- if (!regionCode) {
478
- throw new Error('Region is required. Cannot set selected provider without valid region information.');
479
- }
513
+ const regionCode = __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_requireRegion).call(this);
480
514
  const providers = this.state.providers.data;
481
515
  if (!providers || providers.length === 0) {
482
516
  throw new Error('Providers not loaded. Cannot set selected provider before providers are fetched.');
@@ -487,9 +521,9 @@ class RampsController extends base_controller_1.BaseController {
487
521
  }
488
522
  this.update((state) => {
489
523
  state.providers.selected = provider;
490
- state.paymentMethods.data = [];
491
- state.paymentMethods.selected = null;
524
+ resetResource(state, 'paymentMethods');
492
525
  state.quotes.selected = null;
526
+ resetWidgetUrl(state);
493
527
  });
494
528
  __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_fireAndForget).call(this, this.getPaymentMethods(regionCode, { provider: provider.id }).then(() => {
495
529
  // Restart quote polling after payment methods are fetched
@@ -517,10 +551,7 @@ class RampsController extends base_controller_1.BaseController {
517
551
  await this.setUserRegion(regionCode, options);
518
552
  }
519
553
  hydrateState(options) {
520
- const regionCode = this.state.userRegion?.regionCode;
521
- if (!regionCode) {
522
- throw new Error('Region code is required. Cannot hydrate state without valid region information.');
523
- }
554
+ const regionCode = __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_requireRegion).call(this);
524
555
  __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_fireAndForget).call(this, this.getTokens(regionCode, 'buy', options));
525
556
  __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_fireAndForget).call(this, this.getProviders(regionCode, options));
526
557
  }
@@ -538,7 +569,7 @@ class RampsController extends base_controller_1.BaseController {
538
569
  return this.messenger.call('RampsService:getCountries');
539
570
  }, { ...options, resourceType: 'countries' });
540
571
  this.update((state) => {
541
- state.countries.data = countries;
572
+ state.countries.data = Array.isArray(countries) ? [...countries] : [];
542
573
  });
543
574
  return countries;
544
575
  }
@@ -553,10 +584,7 @@ class RampsController extends base_controller_1.BaseController {
553
584
  * @returns The tokens response containing topTokens and allTokens.
554
585
  */
555
586
  async getTokens(region, action = 'buy', options) {
556
- const regionToUse = region ?? this.state.userRegion?.regionCode;
557
- if (!regionToUse) {
558
- throw new Error('Region is required. Either provide a region parameter or ensure userRegion is set in controller state.');
559
- }
587
+ const regionToUse = region ?? __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_requireRegion).call(this);
560
588
  const normalizedRegion = regionToUse.toLowerCase().trim();
561
589
  const cacheKey = (0, RequestCache_1.createCacheKey)('getTokens', [
562
590
  normalizedRegion,
@@ -570,8 +598,7 @@ class RampsController extends base_controller_1.BaseController {
570
598
  }, {
571
599
  ...options,
572
600
  resourceType: 'tokens',
573
- isResultCurrent: () => this.state.userRegion?.regionCode === undefined ||
574
- this.state.userRegion?.regionCode === normalizedRegion,
601
+ isResultCurrent: () => __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_isRegionCurrent).call(this, normalizedRegion),
575
602
  });
576
603
  this.update((state) => {
577
604
  const userRegionCode = state.userRegion?.regionCode;
@@ -594,15 +621,11 @@ class RampsController extends base_controller_1.BaseController {
594
621
  this.stopQuotePolling();
595
622
  this.update((state) => {
596
623
  state.tokens.selected = null;
597
- state.paymentMethods.data = [];
598
- state.paymentMethods.selected = null;
624
+ resetResource(state, 'paymentMethods');
599
625
  });
600
626
  return;
601
627
  }
602
- const regionCode = this.state.userRegion?.regionCode;
603
- if (!regionCode) {
604
- throw new Error('Region is required. Cannot set selected token without valid region information.');
605
- }
628
+ const regionCode = __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_requireRegion).call(this);
606
629
  const tokens = this.state.tokens.data;
607
630
  if (!tokens) {
608
631
  throw new Error('Tokens not loaded. Cannot set selected token before tokens are fetched.');
@@ -614,9 +637,9 @@ class RampsController extends base_controller_1.BaseController {
614
637
  }
615
638
  this.update((state) => {
616
639
  state.tokens.selected = token;
617
- state.paymentMethods.data = [];
618
- state.paymentMethods.selected = null;
640
+ resetResource(state, 'paymentMethods');
619
641
  state.quotes.selected = null;
642
+ resetWidgetUrl(state);
620
643
  });
621
644
  __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_fireAndForget).call(this, this.getPaymentMethods(regionCode, { assetId: token.assetId }).then(() => {
622
645
  __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_restartPollingIfActive).call(this);
@@ -636,10 +659,7 @@ class RampsController extends base_controller_1.BaseController {
636
659
  * @returns The providers response containing providers array.
637
660
  */
638
661
  async getProviders(region, options) {
639
- const regionToUse = region ?? this.state.userRegion?.regionCode;
640
- if (!regionToUse) {
641
- throw new Error('Region is required. Either provide a region parameter or ensure userRegion is set in controller state.');
642
- }
662
+ const regionToUse = region ?? __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_requireRegion).call(this);
643
663
  const normalizedRegion = regionToUse.toLowerCase().trim();
644
664
  const cacheKey = (0, RequestCache_1.createCacheKey)('getProviders', [
645
665
  normalizedRegion,
@@ -658,8 +678,7 @@ class RampsController extends base_controller_1.BaseController {
658
678
  }, {
659
679
  ...options,
660
680
  resourceType: 'providers',
661
- isResultCurrent: () => this.state.userRegion?.regionCode === undefined ||
662
- this.state.userRegion?.regionCode === normalizedRegion,
681
+ isResultCurrent: () => __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_isRegionCurrent).call(this, normalizedRegion),
663
682
  });
664
683
  this.update((state) => {
665
684
  const userRegionCode = state.userRegion?.regionCode;
@@ -681,13 +700,10 @@ class RampsController extends base_controller_1.BaseController {
681
700
  * @returns The payment methods response containing payments array.
682
701
  */
683
702
  async getPaymentMethods(region, options) {
684
- const regionCode = region ?? this.state.userRegion?.regionCode ?? null;
703
+ const regionCode = region ?? __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_requireRegion).call(this);
685
704
  const fiatToUse = options?.fiat ?? this.state.userRegion?.country?.currency ?? null;
686
705
  const assetIdToUse = options?.assetId ?? this.state.tokens.selected?.assetId ?? '';
687
706
  const providerToUse = options?.provider ?? this.state.providers.selected?.id ?? '';
688
- if (!regionCode) {
689
- throw new Error('Region is required. Either provide a region parameter or ensure userRegion is set in controller state.');
690
- }
691
707
  if (!fiatToUse) {
692
708
  throw new Error('Fiat currency is required. Either provide a fiat parameter or ensure userRegion is set in controller state.');
693
709
  }
@@ -710,10 +726,9 @@ class RampsController extends base_controller_1.BaseController {
710
726
  ...options,
711
727
  resourceType: 'paymentMethods',
712
728
  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;
729
+ const regionMatch = __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_isRegionCurrent).call(this, normalizedRegion);
730
+ const tokenMatch = __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_isTokenCurrent).call(this, assetIdToUse);
731
+ const providerMatch = __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_isProviderCurrent).call(this, providerToUse);
717
732
  return regionMatch && tokenMatch && providerMatch;
718
733
  },
719
734
  });
@@ -783,7 +798,7 @@ class RampsController extends base_controller_1.BaseController {
783
798
  * @returns The quotes response containing success, sorted, error, and customActions.
784
799
  */
785
800
  async getQuotes(options) {
786
- const regionToUse = options.region ?? this.state.userRegion?.regionCode;
801
+ const regionToUse = options.region ?? __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_requireRegion).call(this);
787
802
  const fiatToUse = options.fiat ?? this.state.userRegion?.country?.currency;
788
803
  const paymentMethodsToUse = options.paymentMethods ??
789
804
  this.state.paymentMethods.data.map((pm) => pm.id);
@@ -791,9 +806,6 @@ class RampsController extends base_controller_1.BaseController {
791
806
  this.state.providers.data.map((provider) => provider.id);
792
807
  const action = options.action ?? 'buy';
793
808
  const assetIdToUse = options.assetId ?? this.state.tokens.selected?.assetId;
794
- if (!regionToUse) {
795
- throw new Error('Region is required. Either provide a region parameter or ensure userRegion is set in controller state.');
796
- }
797
809
  if (!fiatToUse) {
798
810
  throw new Error('Fiat currency is required. Either provide a fiat parameter or ensure userRegion is set in controller state.');
799
811
  }
@@ -844,8 +856,7 @@ class RampsController extends base_controller_1.BaseController {
844
856
  forceRefresh: options.forceRefresh,
845
857
  ttl: options.ttl ?? DEFAULT_QUOTES_TTL,
846
858
  resourceType: 'quotes',
847
- isResultCurrent: () => this.state.userRegion?.regionCode === undefined ||
848
- this.state.userRegion?.regionCode === normalizedRegion,
859
+ isResultCurrent: () => __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_isRegionCurrent).call(this, normalizedRegion),
849
860
  });
850
861
  this.update((state) => {
851
862
  const userRegionCode = state.userRegion?.regionCode;
@@ -868,14 +879,10 @@ class RampsController extends base_controller_1.BaseController {
868
879
  * @throws If required dependencies (region, token, provider, payment method) are not set.
869
880
  */
870
881
  startQuotePolling(options) {
871
- // Validate required dependencies
872
- const regionCode = this.state.userRegion?.regionCode;
882
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_requireRegion).call(this);
873
883
  const token = this.state.tokens.selected;
874
884
  const provider = this.state.providers.selected;
875
885
  const paymentMethod = this.state.paymentMethods.selected;
876
- if (!regionCode) {
877
- throw new Error('Region is required. Cannot start quote polling without valid region information.');
878
- }
879
886
  if (!token) {
880
887
  throw new Error('Token is required. Cannot start quote polling without a selected token.');
881
888
  }
@@ -900,10 +907,12 @@ class RampsController extends base_controller_1.BaseController {
900
907
  providers: [provider.id],
901
908
  forceRefresh: true,
902
909
  }).then((response) => {
910
+ let newSelectedQuote = null;
903
911
  // Auto-select logic: only when exactly one quote is returned
904
912
  this.update((state) => {
905
913
  if (response.success.length === 1) {
906
- state.quotes.selected = response.success[0];
914
+ newSelectedQuote = response.success[0];
915
+ state.quotes.selected = newSelectedQuote;
907
916
  }
908
917
  else {
909
918
  // Keep existing selection if still valid, but update with fresh data
@@ -912,11 +921,12 @@ class RampsController extends base_controller_1.BaseController {
912
921
  const freshQuote = response.success.find((quote) => quote.provider === currentSelection.provider &&
913
922
  quote.quote.paymentMethod ===
914
923
  currentSelection.quote.paymentMethod);
915
- // Update with fresh quote data, or clear if no longer valid
916
- state.quotes.selected = freshQuote ?? null;
924
+ newSelectedQuote = freshQuote ?? null;
925
+ state.quotes.selected = newSelectedQuote;
917
926
  }
918
927
  }
919
928
  });
929
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_syncWidgetUrl).call(this, newSelectedQuote);
920
930
  return undefined;
921
931
  }));
922
932
  };
@@ -938,6 +948,7 @@ class RampsController extends base_controller_1.BaseController {
938
948
  }
939
949
  /**
940
950
  * Manually sets the selected quote.
951
+ * Automatically triggers a widget URL fetch for the new quote.
941
952
  *
942
953
  * @param quote - The quote to select, or null to clear the selection.
943
954
  */
@@ -945,6 +956,7 @@ class RampsController extends base_controller_1.BaseController {
945
956
  this.update((state) => {
946
957
  state.quotes.selected = quote;
947
958
  });
959
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_syncWidgetUrl).call(this, quote);
948
960
  }
949
961
  /**
950
962
  * Cleans up controller resources.
@@ -962,6 +974,8 @@ class RampsController extends base_controller_1.BaseController {
962
974
  *
963
975
  * @param quote - The quote to fetch the widget URL from.
964
976
  * @returns Promise resolving to the widget URL string, or null if not available.
977
+ * @deprecated Read `state.widgetUrl` instead. The widget URL is now automatically
978
+ * fetched and stored in state whenever the selected quote changes.
965
979
  */
966
980
  async getWidgetUrl(quote) {
967
981
  const buyUrl = quote.quote?.buyURL;
@@ -980,26 +994,32 @@ class RampsController extends base_controller_1.BaseController {
980
994
  }
981
995
  exports.RampsController = RampsController;
982
996
  _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() {
983
- const types = [
984
- 'providers',
985
- 'tokens',
986
- 'paymentMethods',
987
- 'quotes',
988
- ];
989
- for (const resourceType of types) {
997
+ for (const resourceType of DEPENDENT_RESOURCE_KEYS) {
990
998
  __classPrivateFieldGet(this, _RampsController_pendingResourceCount, "f").delete(resourceType);
991
999
  }
992
- }, _RampsController_removeRequestState = function _RampsController_removeRequestState(cacheKey) {
1000
+ }, _RampsController_abortDependentRequests = function _RampsController_abortDependentRequests() {
1001
+ for (const [cacheKey, pending] of __classPrivateFieldGet(this, _RampsController_pendingRequests, "f").entries()) {
1002
+ if (pending.resourceType &&
1003
+ DEPENDENT_RESOURCE_KEYS_SET.has(pending.resourceType)) {
1004
+ pending.abortController.abort();
1005
+ __classPrivateFieldGet(this, _RampsController_pendingRequests, "f").delete(cacheKey);
1006
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_removeRequestState).call(this, cacheKey);
1007
+ }
1008
+ }
1009
+ }, _RampsController_mutateRequests = function _RampsController_mutateRequests(fn) {
993
1010
  this.update((state) => {
994
1011
  const requests = state.requests;
1012
+ fn(requests);
1013
+ });
1014
+ }, _RampsController_removeRequestState = function _RampsController_removeRequestState(cacheKey) {
1015
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_mutateRequests).call(this, (requests) => {
995
1016
  delete requests[cacheKey];
996
1017
  });
997
1018
  }, _RampsController_cleanupState = function _RampsController_cleanupState() {
998
1019
  this.stopQuotePolling();
1020
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_abortDependentRequests).call(this);
999
1021
  __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_clearPendingResourceCountForDependentResources).call(this);
1000
- this.update((state) => resetDependentResources(state, {
1001
- clearUserRegionData: true,
1002
- }));
1022
+ this.update((state) => resetDependentResources(state, { clearUserRegionData: true }));
1003
1023
  }, _RampsController_fireAndForget = function _RampsController_fireAndForget(promise) {
1004
1024
  promise.catch((_error) => undefined);
1005
1025
  }, _RampsController_restartPollingIfActive = function _RampsController_restartPollingIfActive() {
@@ -1014,6 +1034,21 @@ _RampsController_requestCacheTTL = new WeakMap(), _RampsController_requestCacheM
1014
1034
  // when dependencies are available
1015
1035
  }
1016
1036
  }
1037
+ }, _RampsController_requireRegion = function _RampsController_requireRegion() {
1038
+ const regionCode = this.state.userRegion?.regionCode;
1039
+ if (!regionCode) {
1040
+ throw new Error('Region is required. Cannot proceed without valid region information.');
1041
+ }
1042
+ return regionCode;
1043
+ }, _RampsController_isRegionCurrent = function _RampsController_isRegionCurrent(normalizedRegion) {
1044
+ const current = this.state.userRegion?.regionCode;
1045
+ return current === undefined || current === normalizedRegion;
1046
+ }, _RampsController_isTokenCurrent = function _RampsController_isTokenCurrent(normalizedAssetId) {
1047
+ const current = this.state.tokens.selected?.assetId ?? '';
1048
+ return current === normalizedAssetId;
1049
+ }, _RampsController_isProviderCurrent = function _RampsController_isProviderCurrent(normalizedProviderId) {
1050
+ const current = this.state.providers.selected?.id ?? '';
1051
+ return current === normalizedProviderId;
1017
1052
  }, _RampsController_updateResourceField = function _RampsController_updateResourceField(resourceType, field, value) {
1018
1053
  this.update((state) => {
1019
1054
  const resource = state[resourceType];
@@ -1028,11 +1063,8 @@ _RampsController_requestCacheTTL = new WeakMap(), _RampsController_requestCacheM
1028
1063
  }, _RampsController_updateRequestState = function _RampsController_updateRequestState(cacheKey, requestState) {
1029
1064
  const maxSize = __classPrivateFieldGet(this, _RampsController_requestCacheMaxSize, "f");
1030
1065
  const ttl = __classPrivateFieldGet(this, _RampsController_requestCacheTTL, "f");
1031
- this.update((state) => {
1032
- const requests = state.requests;
1066
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_mutateRequests).call(this, (requests) => {
1033
1067
  requests[cacheKey] = requestState;
1034
- // Evict expired entries based on TTL
1035
- // Only evict SUCCESS states that have exceeded their TTL
1036
1068
  const keys = Object.keys(requests);
1037
1069
  for (const key of keys) {
1038
1070
  const entry = requests[key];
@@ -1041,16 +1073,13 @@ _RampsController_requestCacheTTL = new WeakMap(), _RampsController_requestCacheM
1041
1073
  delete requests[key];
1042
1074
  }
1043
1075
  }
1044
- // Evict oldest entries if cache still exceeds max size
1045
1076
  const remainingKeys = Object.keys(requests);
1046
1077
  if (remainingKeys.length > maxSize) {
1047
- // Sort by timestamp (oldest first)
1048
1078
  const sortedKeys = remainingKeys.sort((a, b) => {
1049
1079
  const aTime = requests[a]?.timestamp ?? 0;
1050
1080
  const bTime = requests[b]?.timestamp ?? 0;
1051
1081
  return aTime - bTime;
1052
1082
  });
1053
- // Remove oldest entries until we're under the limit
1054
1083
  const entriesToRemove = remainingKeys.length - maxSize;
1055
1084
  for (let i = 0; i < entriesToRemove; i++) {
1056
1085
  const keyToRemove = sortedKeys[i];
@@ -1060,5 +1089,38 @@ _RampsController_requestCacheTTL = new WeakMap(), _RampsController_requestCacheM
1060
1089
  }
1061
1090
  }
1062
1091
  });
1092
+ }, _RampsController_syncWidgetUrl = function _RampsController_syncWidgetUrl(quote) {
1093
+ const buyUrl = quote?.quote?.buyURL;
1094
+ if (!buyUrl) {
1095
+ this.update((state) => {
1096
+ resetWidgetUrl(state);
1097
+ });
1098
+ return;
1099
+ }
1100
+ if (this.state.widgetUrl.data === null) {
1101
+ this.update((state) => {
1102
+ state.widgetUrl.isLoading = true;
1103
+ state.widgetUrl.error = null;
1104
+ });
1105
+ }
1106
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_fireAndForget).call(this, this.messenger
1107
+ .call('RampsService:getBuyWidgetUrl', buyUrl)
1108
+ .then((buyWidget) => {
1109
+ this.update((state) => {
1110
+ state.widgetUrl.data = buyWidget;
1111
+ state.widgetUrl.isLoading = false;
1112
+ state.widgetUrl.error = null;
1113
+ });
1114
+ return undefined;
1115
+ })
1116
+ .catch((error) => {
1117
+ this.update((state) => {
1118
+ state.widgetUrl.isLoading = false;
1119
+ state.widgetUrl.error =
1120
+ error instanceof Error
1121
+ ? error.message
1122
+ : 'Failed to fetch widget URL';
1123
+ });
1124
+ }));
1063
1125
  };
1064
1126
  //# sourceMappingURL=RampsController.cjs.map