@metamask/ramps-controller 10.2.0 → 12.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.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [12.0.0]
11
+
12
+ ### Changed
13
+
14
+ - **BREAKING:** Update state hydration to make `init()` idempotent and remove `hydrateState()` ([#8157](https://github.com/MetaMask/core/pull/8157))
15
+
16
+ ### Removed
17
+
18
+ - Remove `hydrateState()` — use `init()` as the single entry point for controller hydration
19
+
20
+ ## [11.0.0]
21
+
22
+ ### Changed
23
+
24
+ - **BREAKING:** Replace `getWidgetUrl` with `getBuyWidgetData` (returns `BuyWidget | null`); add `addPrecreatedOrder` for custom-action ramp flows (e.g., PayPal, Robinhood, Coinbase) ([#8100](https://github.com/MetaMask/core/pull/8100))
25
+
10
26
  ## [10.2.0]
11
27
 
12
28
  ### Fixed
@@ -198,7 +214,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
198
214
  - Add `OnRampService` for interacting with the OnRamp API
199
215
  - Add geolocation detection via IP address lookup
200
216
 
201
- [Unreleased]: https://github.com/MetaMask/core/compare/@metamask/ramps-controller@10.2.0...HEAD
217
+ [Unreleased]: https://github.com/MetaMask/core/compare/@metamask/ramps-controller@12.0.0...HEAD
218
+ [12.0.0]: https://github.com/MetaMask/core/compare/@metamask/ramps-controller@11.0.0...@metamask/ramps-controller@12.0.0
219
+ [11.0.0]: https://github.com/MetaMask/core/compare/@metamask/ramps-controller@10.2.0...@metamask/ramps-controller@11.0.0
202
220
  [10.2.0]: https://github.com/MetaMask/core/compare/@metamask/ramps-controller@10.1.0...@metamask/ramps-controller@10.2.0
203
221
  [10.1.0]: https://github.com/MetaMask/core/compare/@metamask/ramps-controller@10.0.0...@metamask/ramps-controller@10.1.0
204
222
  [10.0.0]: https://github.com/MetaMask/core/compare/@metamask/ramps-controller@9.0.0...@metamask/ramps-controller@10.0.0
@@ -10,9 +10,9 @@ 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_clearPendingResourceCountForDependentResources, _RampsController_abortDependentRequests, _RampsController_registerActionHandlers, _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_refreshOrder, _RampsController_pollPendingOrders, _RampsController_syncTransakAuthOnError;
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_registerActionHandlers, _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;
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.RampsController = exports.getDefaultRampsControllerState = exports.RAMPS_CONTROLLER_REQUIRED_SERVICE_ACTIONS = exports.controllerName = void 0;
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");
17
17
  const RampsService_1 = require("./RampsService.cjs");
18
18
  const RequestCache_1 = require("./RequestCache.cjs");
@@ -250,6 +250,10 @@ function findRegionFromCode(regionCode, countries) {
250
250
  regionCode: normalizedCode,
251
251
  };
252
252
  }
253
+ function normalizeProviderCode(providerCode) {
254
+ return providerCode.replace(/^\/providers\//u, '');
255
+ }
256
+ exports.normalizeProviderCode = normalizeProviderCode;
253
257
  // === ORDER POLLING CONSTANTS ===
254
258
  const TERMINAL_ORDER_STATUSES = new Set([
255
259
  RampsService_1.RampsOrderStatus.Completed,
@@ -323,6 +327,7 @@ class RampsController extends base_controller_1.BaseController {
323
327
  _RampsController_orderPollingMeta.set(this, new Map());
324
328
  _RampsController_orderPollingTimer.set(this, null);
325
329
  _RampsController_isPolling.set(this, false);
330
+ _RampsController_initPromise.set(this, null);
326
331
  __classPrivateFieldSet(this, _RampsController_requestCacheTTL, requestCacheTTL, "f");
327
332
  __classPrivateFieldSet(this, _RampsController_requestCacheMaxSize, requestCacheMaxSize, "f");
328
333
  __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_registerActionHandlers).call(this);
@@ -565,25 +570,28 @@ class RampsController extends base_controller_1.BaseController {
565
570
  * Initializes the controller by fetching the user's region from geolocation.
566
571
  * This should be called once at app startup to set up the initial region.
567
572
  *
568
- * If a userRegion already exists (from persistence or manual selection),
569
- * this method will skip geolocation fetch and use the existing region.
573
+ * Idempotent: subsequent calls return the same promise unless forceRefresh is set.
574
+ * Skips getCountries when countries are already loaded; skips geolocation when
575
+ * userRegion already exists.
570
576
  *
571
- * @param options - Options for cache behavior.
577
+ * @param options - Options for cache behavior. forceRefresh bypasses idempotency and re-runs the full flow.
572
578
  * @returns Promise that resolves when initialization is complete.
573
579
  */
574
580
  async init(options) {
575
- await this.getCountries(options);
576
- let regionCode = this.state.userRegion?.regionCode;
577
- regionCode ?? (regionCode = await this.messenger.call('RampsService:getGeolocation'));
578
- if (!regionCode) {
579
- throw new Error('Failed to fetch geolocation. Cannot initialize controller without valid region information.');
581
+ if (!options?.forceRefresh && __classPrivateFieldGet(this, _RampsController_initPromise, "f") !== null) {
582
+ return __classPrivateFieldGet(this, _RampsController_initPromise, "f");
580
583
  }
581
- await this.setUserRegion(regionCode, options);
582
- }
583
- hydrateState(options) {
584
- const regionCode = __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_requireRegion).call(this);
585
- __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_fireAndForget).call(this, this.getTokens(regionCode, 'buy', options));
586
- __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_fireAndForget).call(this, this.getProviders(regionCode, options));
584
+ if (options?.forceRefresh) {
585
+ __classPrivateFieldSet(this, _RampsController_initPromise, null, "f");
586
+ }
587
+ const initPromise = __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_runInit).call(this, options).then(() => undefined, (error) => {
588
+ if (__classPrivateFieldGet(this, _RampsController_initPromise, "f") === initPromise) {
589
+ __classPrivateFieldSet(this, _RampsController_initPromise, null, "f");
590
+ }
591
+ throw error;
592
+ });
593
+ __classPrivateFieldSet(this, _RampsController_initPromise, initPromise, "f");
594
+ return initPromise;
587
595
  }
588
596
  /**
589
597
  * Fetches the list of supported countries.
@@ -945,25 +953,74 @@ class RampsController extends base_controller_1.BaseController {
945
953
  super.destroy();
946
954
  }
947
955
  /**
948
- * Fetches the widget URL from a quote for redirect providers.
956
+ * Fetches the widget data from a quote for redirect providers.
949
957
  * Makes a request to the buyURL endpoint via the RampsService to get the
950
- * actual provider widget URL.
958
+ * actual provider widget URL and optional order ID for polling.
951
959
  *
952
960
  * @param quote - The quote to fetch the widget URL from.
953
- * @returns Promise resolving to the widget URL string, or null if not available.
961
+ * @returns Promise resolving to the full BuyWidget (url, browser, orderId), or null if not available (missing buyURL or empty url in response).
962
+ * @throws Rethrows errors from the RampsService (e.g. HttpError, network failures) so clients can react to fetch failures.
954
963
  */
955
- async getWidgetUrl(quote) {
964
+ async getBuyWidgetData(quote) {
956
965
  const buyUrl = quote.quote?.buyURL;
957
966
  if (!buyUrl) {
958
967
  return null;
959
968
  }
960
- try {
961
- const buyWidget = await this.messenger.call('RampsService:getBuyWidgetUrl', buyUrl);
962
- return buyWidget.url ?? null;
963
- }
964
- catch {
969
+ const buyWidget = await this.messenger.call('RampsService:getBuyWidgetUrl', buyUrl);
970
+ if (!buyWidget?.url) {
965
971
  return null;
966
972
  }
973
+ return buyWidget;
974
+ }
975
+ /**
976
+ * Registers an order ID for polling until the order is created or resolved.
977
+ * Adds a minimal stub order to controller state; the existing order polling
978
+ * will fetch the full order when the provider has created it.
979
+ *
980
+ * @param params - Object containing order identifiers and wallet info.
981
+ * @param params.orderId - Full order ID (e.g. "/providers/paypal/orders/abc123") or order code.
982
+ * @param params.providerCode - Provider code (e.g. "paypal", "transak"), with or without /providers/ prefix.
983
+ * @param params.walletAddress - Wallet address for the order.
984
+ * @param params.chainId - Optional chain ID for the order.
985
+ */
986
+ addPrecreatedOrder(params) {
987
+ const { orderId, providerCode, walletAddress, chainId } = params;
988
+ const orderCode = orderId.includes('/orders/')
989
+ ? orderId.split('/orders/')[1]
990
+ : orderId;
991
+ if (!orderCode?.trim()) {
992
+ return;
993
+ }
994
+ const normalizedProviderCode = normalizeProviderCode(providerCode);
995
+ const stubOrder = {
996
+ providerOrderId: orderCode,
997
+ provider: {
998
+ id: `/providers/${normalizedProviderCode}`,
999
+ name: '',
1000
+ environmentType: '',
1001
+ description: '',
1002
+ hqAddress: '',
1003
+ links: [],
1004
+ logos: { light: '', dark: '', height: 0, width: 0 },
1005
+ },
1006
+ walletAddress,
1007
+ status: RampsService_1.RampsOrderStatus.Precreated,
1008
+ orderType: 'buy',
1009
+ createdAt: Date.now(),
1010
+ isOnlyLink: false,
1011
+ success: false,
1012
+ cryptoAmount: 0,
1013
+ fiatAmount: 0,
1014
+ providerOrderLink: '',
1015
+ totalFeesFiat: 0,
1016
+ txHash: '',
1017
+ network: chainId ? { chainId, name: '' } : { chainId: '', name: '' },
1018
+ canBeUpdated: true,
1019
+ idHasExpired: false,
1020
+ excludeFromPurchases: false,
1021
+ timeDescriptionPending: '',
1022
+ };
1023
+ this.addOrder(stubOrder);
967
1024
  }
968
1025
  /**
969
1026
  * Fetches an order from the unified V2 API endpoint.
@@ -978,10 +1035,17 @@ class RampsController extends base_controller_1.BaseController {
978
1035
  const order = await this.messenger.call('RampsService:getOrder', providerCode, orderCode, wallet);
979
1036
  this.update((state) => {
980
1037
  const idx = state.orders.findIndex((existing) => existing.providerOrderId === orderCode);
981
- if (idx !== -1) {
1038
+ if (idx === -1) {
1039
+ state.orders.push({
1040
+ ...order,
1041
+ providerOrderId: orderCode,
1042
+ });
1043
+ }
1044
+ else {
982
1045
  state.orders[idx] = {
983
1046
  ...state.orders[idx],
984
1047
  ...order,
1048
+ providerOrderId: orderCode,
985
1049
  };
986
1050
  }
987
1051
  });
@@ -1397,7 +1461,7 @@ class RampsController extends base_controller_1.BaseController {
1397
1461
  }
1398
1462
  }
1399
1463
  exports.RampsController = RampsController;
1400
- _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_instances = new WeakSet(), _RampsController_clearPendingResourceCountForDependentResources = function _RampsController_clearPendingResourceCountForDependentResources() {
1464
+ _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() {
1401
1465
  for (const resourceType of DEPENDENT_RESOURCE_KEYS) {
1402
1466
  __classPrivateFieldGet(this, _RampsController_pendingResourceCount, "f").delete(resourceType);
1403
1467
  }
@@ -1484,6 +1548,24 @@ _RampsController_requestCacheTTL = new WeakMap(), _RampsController_requestCacheM
1484
1548
  }
1485
1549
  }
1486
1550
  });
1551
+ }, _RampsController_runInit = async function _RampsController_runInit(options) {
1552
+ const forceRefresh = options?.forceRefresh === true;
1553
+ const hasCountries = this.state.countries.data.length > 0;
1554
+ if (forceRefresh || !hasCountries) {
1555
+ await this.getCountries(options);
1556
+ }
1557
+ let regionCode;
1558
+ if (forceRefresh) {
1559
+ regionCode = await this.messenger.call('RampsService:getGeolocation');
1560
+ }
1561
+ else {
1562
+ regionCode = this.state.userRegion?.regionCode;
1563
+ regionCode ?? (regionCode = await this.messenger.call('RampsService:getGeolocation'));
1564
+ }
1565
+ if (!regionCode) {
1566
+ throw new Error('Failed to fetch geolocation. Cannot initialize controller without valid region information.');
1567
+ }
1568
+ await this.setUserRegion(regionCode, options);
1487
1569
  }, _RampsController_refreshOrder =
1488
1570
  /**
1489
1571
  * Refreshes a single order via the V2 API and updates it in state.
@@ -1496,7 +1578,7 @@ async function _RampsController_refreshOrder(order) {
1496
1578
  if (!providerCode || !order.providerOrderId || !order.walletAddress) {
1497
1579
  return;
1498
1580
  }
1499
- const providerCodeSegment = providerCode.replace('/providers/', '');
1581
+ const providerCodeSegment = normalizeProviderCode(providerCode);
1500
1582
  const previousStatus = order.status;
1501
1583
  try {
1502
1584
  const updatedOrder = await this.getOrder(providerCodeSegment, order.providerOrderId, order.walletAddress);