@metamask/ramps-controller 12.1.0 → 13.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.
@@ -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_orderPollingMeta, _RampsController_orderPollingTimer, _RampsController_isPolling, _RampsController_initPromise, _RampsController_clearPendingResourceCountForDependentResources, _RampsController_abortDependentRequests, _RampsController_mutateRequests, _RampsController_removeRequestState, _RampsController_cleanupState, _RampsController_fireAndForget, _RampsController_requireRegion, _RampsController_isRegionCurrent, _RampsController_isTokenCurrent, _RampsController_isProviderCurrent, _RampsController_updateResourceField, _RampsController_setResourceLoading, _RampsController_setResourceError, _RampsController_updateRequestState, _RampsController_runInit, _RampsController_refreshOrder, _RampsController_pollPendingOrders, _RampsController_syncTransakAuthOnError;
13
+ var _RampsController_instances, _RampsController_requestCacheTTL, _RampsController_requestCacheMaxSize, _RampsController_pendingRequests, _RampsController_pendingResourceCount, _RampsController_pendingResourceGeneration, _RampsController_orderPollingMeta, _RampsController_orderPollingTimer, _RampsController_isPolling, _RampsController_initPromise, _RampsController_clearPendingResourceCountForDependentResources, _RampsController_abortDependentRequests, _RampsController_mutateRequests, _RampsController_removeRequestState, _RampsController_cleanupState, _RampsController_requireRegion, _RampsController_isRegionCurrent, _RampsController_isTokenCurrent, _RampsController_isProviderCurrent, _RampsController_updateResourceField, _RampsController_setResourceLoading, _RampsController_setResourceError, _RampsController_updateRequestState, _RampsController_runInit, _RampsController_refreshOrder, _RampsController_pollPendingOrders, _RampsController_syncTransakAuthOnError;
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.RampsController = exports.normalizeProviderCode = exports.getDefaultRampsControllerState = exports.RAMPS_CONTROLLER_REQUIRED_SERVICE_ACTIONS = exports.controllerName = void 0;
16
16
  const base_controller_1 = require("@metamask/base-controller");
@@ -180,12 +180,11 @@ const DEPENDENT_RESOURCE_KEYS = [
180
180
  ];
181
181
  const DEPENDENT_RESOURCE_KEYS_SET = new Set(DEPENDENT_RESOURCE_KEYS);
182
182
  function resetResource(state, resourceType, defaultResource) {
183
- const def = defaultResource ?? getDefaultRampsControllerState()[resourceType];
184
183
  const resource = state[resourceType];
185
- resource.data = def.data;
186
- resource.selected = def.selected;
187
- resource.isLoading = def.isLoading;
188
- resource.error = def.error;
184
+ resource.data = defaultResource.data;
185
+ resource.selected = defaultResource.selected;
186
+ resource.isLoading = defaultResource.isLoading;
187
+ resource.error = defaultResource.error;
189
188
  }
190
189
  /**
191
190
  * Resets region-dependent resources (userRegion, providers, tokens, paymentMethods).
@@ -381,6 +380,11 @@ class RampsController extends base_controller_1.BaseController {
381
380
  * Used so isLoading is only cleared when the last request for that resource finishes.
382
381
  */
383
382
  _RampsController_pendingResourceCount.set(this, new Map());
383
+ /**
384
+ * Monotonic generation per resource type used to invalidate stale in-flight
385
+ * requests after region/token/provider dependent-resource resets.
386
+ */
387
+ _RampsController_pendingResourceGeneration.set(this, new Map());
384
388
  _RampsController_orderPollingMeta.set(this, new Map());
385
389
  _RampsController_orderPollingTimer.set(this, null);
386
390
  _RampsController_isPolling.set(this, false);
@@ -436,6 +440,9 @@ class RampsController extends base_controller_1.BaseController {
436
440
  const abortController = new AbortController();
437
441
  const lastFetchedAt = Date.now();
438
442
  const { resourceType } = options ?? {};
443
+ const resourceGeneration = resourceType
444
+ ? (__classPrivateFieldGet(this, _RampsController_pendingResourceGeneration, "f").get(resourceType) ?? 0)
445
+ : undefined;
439
446
  // Update state to loading
440
447
  __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_updateRequestState).call(this, cacheKey, (0, RequestCache_1.createLoadingState)());
441
448
  // Set resource-level loading state (only on cache miss). Ref-count so concurrent
@@ -484,14 +491,17 @@ class RampsController extends base_controller_1.BaseController {
484
491
  }
485
492
  // Clear resource-level loading state only when no requests for this resource remain
486
493
  if (resourceType && !abortController.signal.aborted) {
487
- const count = __classPrivateFieldGet(this, _RampsController_pendingResourceCount, "f").get(resourceType) ?? 0;
488
- const next = Math.max(0, count - 1);
489
- if (next === 0) {
490
- __classPrivateFieldGet(this, _RampsController_pendingResourceCount, "f").delete(resourceType);
491
- __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_setResourceLoading).call(this, resourceType, false);
492
- }
493
- else {
494
- __classPrivateFieldGet(this, _RampsController_pendingResourceCount, "f").set(resourceType, next);
494
+ const currentGeneration = __classPrivateFieldGet(this, _RampsController_pendingResourceGeneration, "f").get(resourceType) ?? 0;
495
+ if (currentGeneration === resourceGeneration) {
496
+ const count = __classPrivateFieldGet(this, _RampsController_pendingResourceCount, "f").get(resourceType) ?? 0;
497
+ const next = Math.max(0, count - 1);
498
+ if (next === 0) {
499
+ __classPrivateFieldGet(this, _RampsController_pendingResourceCount, "f").delete(resourceType);
500
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_setResourceLoading).call(this, resourceType, false);
501
+ }
502
+ else {
503
+ __classPrivateFieldGet(this, _RampsController_pendingResourceCount, "f").set(resourceType, next);
504
+ }
495
505
  }
496
506
  }
497
507
  }
@@ -549,12 +559,14 @@ class RampsController extends base_controller_1.BaseController {
549
559
  __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_cleanupState).call(this);
550
560
  throw new Error(`Region "${normalizedRegion}" not found in countries data. Cannot set user region without valid country information.`);
551
561
  }
552
- const regionChanged = normalizedRegion !== this.state.userRegion?.regionCode;
553
- const needsRefetch = regionChanged ||
554
- !this.state.tokens.data ||
555
- this.state.providers.data.length === 0;
562
+ const regionChanged = Boolean(options?.forceRefresh) ||
563
+ normalizedRegion !== this.state.userRegion?.regionCode;
556
564
  if (regionChanged) {
557
- __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_abortDependentRequests).call(this);
565
+ // Note: we intentionally do NOT abort in-flight requests here.
566
+ // Aborting causes data loss during rapid region switching (e.g.
567
+ // user taps France → Finland → France quickly). Instead we let
568
+ // old requests complete naturally; isResultCurrent guards in
569
+ // getProviders/getPaymentMethods discard stale results.
558
570
  __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_clearPendingResourceCountForDependentResources).call(this);
559
571
  }
560
572
  this.update((state) => {
@@ -563,18 +575,6 @@ class RampsController extends base_controller_1.BaseController {
563
575
  }
564
576
  state.userRegion = userRegion;
565
577
  });
566
- if (needsRefetch) {
567
- const refetchPromises = [];
568
- if (regionChanged || !this.state.tokens.data) {
569
- refetchPromises.push(this.getTokens(userRegion.regionCode, 'buy', options));
570
- }
571
- if (regionChanged || this.state.providers.data.length === 0) {
572
- refetchPromises.push(this.getProviders(userRegion.regionCode, options));
573
- }
574
- if (refetchPromises.length > 0) {
575
- __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_fireAndForget).call(this, Promise.all(refetchPromises));
576
- }
577
- }
578
578
  return userRegion;
579
579
  }
580
580
  catch (error) {
@@ -583,50 +583,44 @@ class RampsController extends base_controller_1.BaseController {
583
583
  }
584
584
  }
585
585
  /**
586
- * Sets the user's selected provider by ID, or clears the selection.
587
- * Looks up the provider from the current providers in state and automatically
588
- * fetches payment methods for that provider.
586
+ * Sets the user's selected provider.
589
587
  *
590
- * @param providerId - The provider ID (e.g., "/providers/moonpay"), or null to clear.
588
+ * Accepts either a Provider object (stored directly) or a provider ID
589
+ * string (looked up from state). The object form is preferred when the
590
+ * caller already has the full data (e.g. from React Query cache).
591
+ *
592
+ * @param providerOrId - A Provider object, a provider ID string (e.g., "/providers/moonpay"), or null to clear.
591
593
  * @param options - Optional settings for the selection.
592
594
  * @param options.autoSelected - When true, marks the provider as system-guessed
593
595
  * (soft selection). The UI will silently auto-switch on token conflict instead
594
596
  * of showing the "Token Not Available" modal. Defaults to false.
595
- * @throws If region is not set, providers are not loaded, or provider is not found.
596
597
  */
597
- setSelectedProvider(providerId, options) {
598
- if (providerId === null) {
598
+ setSelectedProvider(providerOrId, options) {
599
+ if (providerOrId === null) {
599
600
  this.update((state) => {
600
601
  state.providers.selected = null;
601
602
  state.providerAutoSelected = false;
602
- resetResource(state, 'paymentMethods');
603
603
  });
604
604
  return;
605
605
  }
606
- const regionCode = __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_requireRegion).call(this);
606
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_requireRegion).call(this);
607
+ // If a full Provider object is passed, store it directly (avoids
608
+ // depending on state.providers.data being populated).
609
+ if (typeof providerOrId !== 'string') {
610
+ this.update((state) => {
611
+ state.providers.selected = providerOrId;
612
+ state.providerAutoSelected = options?.autoSelected ?? false;
613
+ });
614
+ return;
615
+ }
616
+ // ID string: look up from state
607
617
  const providers = this.state.providers.data;
608
- if (!providers || providers.length === 0) {
609
- throw new Error('Providers not loaded. Cannot set selected provider before providers are fetched.');
610
- }
611
- const provider = providers.find((prov) => prov.id === providerId);
612
- if (!provider) {
613
- throw new Error(`Provider with ID "${providerId}" not found in available providers.`);
614
- }
615
- const selectedToken = this.state.tokens.selected;
616
- const supportedCryptos = provider.supportedCryptoCurrencies;
617
- // Only fetch payment methods if the selected token is supported by the new
618
- // provider. If it isn't, the payment methods request would fail or return
619
- // empty for the wrong reason; the UI will show the Token Not Available modal
620
- // so the user can change token or pick a different provider.
621
- const assetId = selectedToken?.assetId;
622
- const tokenSupportedByProvider = !(assetId && supportedCryptos?.[assetId] === false);
623
- this.update((state) => {
624
- state.providers.selected = provider;
625
- state.providerAutoSelected = options?.autoSelected ?? false;
626
- resetResource(state, 'paymentMethods');
627
- });
628
- if (tokenSupportedByProvider) {
629
- __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_fireAndForget).call(this, this.getPaymentMethods(regionCode, { provider: provider.id }));
618
+ const provider = providers?.find((prov) => prov.id === providerOrId);
619
+ if (provider) {
620
+ this.update((state) => {
621
+ state.providers.selected = provider;
622
+ state.providerAutoSelected = options?.autoSelected ?? false;
623
+ });
630
624
  }
631
625
  }
632
626
  /**
@@ -721,11 +715,10 @@ class RampsController extends base_controller_1.BaseController {
721
715
  if (!assetId) {
722
716
  this.update((state) => {
723
717
  state.tokens.selected = null;
724
- resetResource(state, 'paymentMethods');
725
718
  });
726
719
  return;
727
720
  }
728
- const regionCode = __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_requireRegion).call(this);
721
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_requireRegion).call(this);
729
722
  const tokens = this.state.tokens.data;
730
723
  if (!tokens) {
731
724
  throw new Error('Tokens not loaded. Cannot set selected token before tokens are fetched.');
@@ -737,9 +730,7 @@ class RampsController extends base_controller_1.BaseController {
737
730
  }
738
731
  this.update((state) => {
739
732
  state.tokens.selected = token;
740
- resetResource(state, 'paymentMethods');
741
733
  });
742
- __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_fireAndForget).call(this, this.getPaymentMethods(regionCode, { assetId: token.assetId }));
743
734
  }
744
735
  /**
745
736
  * Fetches the list of providers for a given region.
@@ -847,29 +838,35 @@ class RampsController extends base_controller_1.BaseController {
847
838
  return response;
848
839
  }
849
840
  /**
850
- * Sets the user's selected payment method by ID.
851
- * Looks up the payment method from the current payment methods in state.
841
+ * Sets the user's selected payment method.
852
842
  *
853
- * @param paymentMethodId - The payment method ID (e.g., "/payments/debit-credit-card"), or null to clear.
854
- * @throws If payment methods are not loaded or payment method is not found.
843
+ * Accepts either a payment method ID (looked up from state) or a full
844
+ * PaymentMethod object (stored directly). The object form is preferred
845
+ * when the caller already has the full data (e.g. from React Query cache),
846
+ * as it avoids depending on controller state being populated.
847
+ *
848
+ * @param paymentMethodOrId - A PaymentMethod object, a payment method ID string, or undefined/null to clear.
855
849
  */
856
- setSelectedPaymentMethod(paymentMethodId) {
857
- if (!paymentMethodId) {
850
+ setSelectedPaymentMethod(paymentMethodOrId) {
851
+ if (!paymentMethodOrId) {
858
852
  this.update((state) => {
859
853
  state.paymentMethods.selected = null;
860
854
  });
861
855
  return;
862
856
  }
863
- const paymentMethods = this.state.paymentMethods.data;
864
- if (!paymentMethods || paymentMethods.length === 0) {
865
- throw new Error('Payment methods not loaded. Cannot set selected payment method before payment methods are fetched.');
866
- }
867
- const paymentMethod = paymentMethods.find((pm) => pm.id === paymentMethodId);
868
- if (!paymentMethod) {
869
- throw new Error(`Payment method with ID "${paymentMethodId}" not found in available payment methods.`);
857
+ // If a full object is passed, store it directly
858
+ if (typeof paymentMethodOrId !== 'string') {
859
+ this.update((state) => {
860
+ state.paymentMethods.selected = paymentMethodOrId;
861
+ });
862
+ return;
870
863
  }
864
+ // ID string: look up from state
865
+ const paymentMethodId = paymentMethodOrId;
866
+ const paymentMethods = this.state.paymentMethods.data;
867
+ const paymentMethod = paymentMethods?.find((pm) => pm.id === paymentMethodId);
871
868
  this.update((state) => {
872
- state.paymentMethods.selected = paymentMethod;
869
+ state.paymentMethods.selected = paymentMethod ?? null;
873
870
  });
874
871
  }
875
872
  /**
@@ -1527,9 +1524,11 @@ class RampsController extends base_controller_1.BaseController {
1527
1524
  }
1528
1525
  }
1529
1526
  exports.RampsController = RampsController;
1530
- _RampsController_requestCacheTTL = new WeakMap(), _RampsController_requestCacheMaxSize = new WeakMap(), _RampsController_pendingRequests = new WeakMap(), _RampsController_pendingResourceCount = new WeakMap(), _RampsController_orderPollingMeta = new WeakMap(), _RampsController_orderPollingTimer = new WeakMap(), _RampsController_isPolling = new WeakMap(), _RampsController_initPromise = new WeakMap(), _RampsController_instances = new WeakSet(), _RampsController_clearPendingResourceCountForDependentResources = function _RampsController_clearPendingResourceCountForDependentResources() {
1527
+ _RampsController_requestCacheTTL = new WeakMap(), _RampsController_requestCacheMaxSize = new WeakMap(), _RampsController_pendingRequests = new WeakMap(), _RampsController_pendingResourceCount = new WeakMap(), _RampsController_pendingResourceGeneration = new WeakMap(), _RampsController_orderPollingMeta = new WeakMap(), _RampsController_orderPollingTimer = new WeakMap(), _RampsController_isPolling = new WeakMap(), _RampsController_initPromise = new WeakMap(), _RampsController_instances = new WeakSet(), _RampsController_clearPendingResourceCountForDependentResources = function _RampsController_clearPendingResourceCountForDependentResources() {
1531
1528
  for (const resourceType of DEPENDENT_RESOURCE_KEYS) {
1532
1529
  __classPrivateFieldGet(this, _RampsController_pendingResourceCount, "f").delete(resourceType);
1530
+ const generation = __classPrivateFieldGet(this, _RampsController_pendingResourceGeneration, "f").get(resourceType) ?? 0;
1531
+ __classPrivateFieldGet(this, _RampsController_pendingResourceGeneration, "f").set(resourceType, generation + 1);
1533
1532
  }
1534
1533
  }, _RampsController_abortDependentRequests = function _RampsController_abortDependentRequests() {
1535
1534
  for (const [cacheKey, pending] of __classPrivateFieldGet(this, _RampsController_pendingRequests, "f").entries()) {
@@ -1553,8 +1552,6 @@ _RampsController_requestCacheTTL = new WeakMap(), _RampsController_requestCacheM
1553
1552
  __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_abortDependentRequests).call(this);
1554
1553
  __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_clearPendingResourceCountForDependentResources).call(this);
1555
1554
  this.update((state) => resetDependentResources(state, { clearUserRegionData: true }));
1556
- }, _RampsController_fireAndForget = function _RampsController_fireAndForget(promise) {
1557
- promise.catch((_error) => undefined);
1558
1555
  }, _RampsController_requireRegion = function _RampsController_requireRegion() {
1559
1556
  const regionCode = this.state.userRegion?.regionCode;
1560
1557
  if (!regionCode) {
@@ -1616,14 +1613,11 @@ _RampsController_requestCacheTTL = new WeakMap(), _RampsController_requestCacheM
1616
1613
  if (forceRefresh || !hasCountries) {
1617
1614
  await this.getCountries(options);
1618
1615
  }
1619
- let regionCode;
1620
- if (forceRefresh) {
1621
- regionCode = await this.messenger.call('RampsService:getGeolocation');
1622
- }
1623
- else {
1624
- regionCode = this.state.userRegion?.regionCode;
1625
- regionCode ?? (regionCode = await this.messenger.call('RampsService:getGeolocation'));
1626
- }
1616
+ // Always prefer the user's persisted region. Geolocation is only used to
1617
+ // seed the initial value; once the user (or a prior init) has set a region
1618
+ // we must respect that choice — even on forceRefresh.
1619
+ let regionCode = this.state.userRegion?.regionCode;
1620
+ regionCode ?? (regionCode = await this.messenger.call('RampsService:getGeolocation'));
1627
1621
  if (!regionCode) {
1628
1622
  throw new Error('Failed to fetch geolocation. Cannot initialize controller without valid region information.');
1629
1623
  }