@metamask/ramps-controller 4.0.0 → 5.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,39 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [5.0.0]
11
+
12
+ ### Added
13
+
14
+ - Add `hydrateState()` method to fetch providers and tokens for user region ([#7707](https://github.com/MetaMask/core/pull/7707))
15
+ - Add `countries` state to RampsController with 24 hour TTL caching ([#7707](https://github.com/MetaMask/core/pull/7707))
16
+ - Add `SupportedActions` type for `{ buy: boolean; sell: boolean }` support info
17
+ - Add `selectedToken` state and `setSelectedToken()` method to RampsController ([#7734](https://github.com/MetaMask/core/pull/7734))
18
+ - Add `RampsEnvironment.Local` option to RampsService for local development ([#7734](https://github.com/MetaMask/core/pull/7734))
19
+
20
+ ### Changed
21
+
22
+ - Reorganize `init()` to only fetch geolocation and countries; remove token and provider fetching ([#7707](https://github.com/MetaMask/core/pull/7707))
23
+ - **BREAKING:** Change `Country.supported` and `State.supported` from `boolean` to `SupportedActions` object. The API now returns buy/sell support info in a single call.
24
+ - **BREAKING:** Remove `action` parameter from `getCountries()`. Countries are no longer fetched separately for buy/sell actions.
25
+ - **BREAKING:** Rename `preferredProvider` to `selectedProvider` and `setPreferredProvider()` to `setSelectedProvider()` in RampsController ([#7734](https://github.com/MetaMask/core/pull/7734))
26
+ - **BREAKING:** Change `getPaymentMethods(options)` to `getPaymentMethods(region, options)` with region as first parameter ([#7734](https://github.com/MetaMask/core/pull/7734))
27
+
28
+ ## [4.1.0]
29
+
30
+ ### Added
31
+
32
+ - Add sync trigger methods to RampsController ([#7662](https://github.com/MetaMask/core/pull/7662))
33
+
34
+ - Export `RampAction` type for `'buy' | 'sell'` ramp actions ([#7663](https://github.com/MetaMask/core/pull/7663))
35
+ - Add payment methods support with `getPaymentMethods()` method, `paymentMethods` and `selectedPaymentMethod` state ([#7665](https://github.com/MetaMask/core/pull/7665))
36
+
37
+ ### Changed
38
+
39
+ - Evict expired cache entries based on TTL in addition to size-based eviction ([#7674](https://github.com/MetaMask/core/pull/7674))
40
+
41
+ - Update `getTokens()` to use v2 API endpoint and support optional provider parameter ([#7664](https://github.com/MetaMask/core/pull/7664))
42
+
10
43
  ## [4.0.0]
11
44
 
12
45
  ### Added
@@ -79,7 +112,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
79
112
  - Add `OnRampService` for interacting with the OnRamp API
80
113
  - Add geolocation detection via IP address lookup
81
114
 
82
- [Unreleased]: https://github.com/MetaMask/core/compare/@metamask/ramps-controller@4.0.0...HEAD
115
+ [Unreleased]: https://github.com/MetaMask/core/compare/@metamask/ramps-controller@5.0.0...HEAD
116
+ [5.0.0]: https://github.com/MetaMask/core/compare/@metamask/ramps-controller@4.1.0...@metamask/ramps-controller@5.0.0
117
+ [4.1.0]: https://github.com/MetaMask/core/compare/@metamask/ramps-controller@4.0.0...@metamask/ramps-controller@4.1.0
83
118
  [4.0.0]: https://github.com/MetaMask/core/compare/@metamask/ramps-controller@3.0.0...@metamask/ramps-controller@4.0.0
84
119
  [3.0.0]: https://github.com/MetaMask/core/compare/@metamask/ramps-controller@2.1.0...@metamask/ramps-controller@3.0.0
85
120
  [2.1.0]: https://github.com/MetaMask/core/compare/@metamask/ramps-controller@2.0.0...@metamask/ramps-controller@2.1.0
@@ -10,7 +10,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
10
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
11
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
12
12
  };
13
- var _RampsController_instances, _RampsController_requestCacheTTL, _RampsController_requestCacheMaxSize, _RampsController_pendingRequests, _RampsController_removeRequestState, _RampsController_updateRequestState;
13
+ var _RampsController_instances, _RampsController_requestCacheTTL, _RampsController_requestCacheMaxSize, _RampsController_pendingRequests, _RampsController_removeRequestState, _RampsController_cleanupState, _RampsController_updateRequestState;
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.RampsController = exports.getDefaultRampsControllerState = exports.controllerName = void 0;
16
16
  const base_controller_1 = require("@metamask/base-controller");
@@ -32,7 +32,13 @@ const rampsControllerMetadata = {
32
32
  includeInStateLogs: true,
33
33
  usedInUi: true,
34
34
  },
35
- preferredProvider: {
35
+ selectedProvider: {
36
+ persist: false,
37
+ includeInDebugSnapshot: true,
38
+ includeInStateLogs: true,
39
+ usedInUi: true,
40
+ },
41
+ countries: {
36
42
  persist: true,
37
43
  includeInDebugSnapshot: true,
38
44
  includeInStateLogs: true,
@@ -50,6 +56,24 @@ const rampsControllerMetadata = {
50
56
  includeInStateLogs: true,
51
57
  usedInUi: true,
52
58
  },
59
+ selectedToken: {
60
+ persist: false,
61
+ includeInDebugSnapshot: true,
62
+ includeInStateLogs: true,
63
+ usedInUi: true,
64
+ },
65
+ paymentMethods: {
66
+ persist: false,
67
+ includeInDebugSnapshot: true,
68
+ includeInStateLogs: true,
69
+ usedInUi: true,
70
+ },
71
+ selectedPaymentMethod: {
72
+ persist: false,
73
+ includeInDebugSnapshot: true,
74
+ includeInStateLogs: true,
75
+ usedInUi: true,
76
+ },
53
77
  requests: {
54
78
  persist: false,
55
79
  includeInDebugSnapshot: true,
@@ -68,9 +92,13 @@ const rampsControllerMetadata = {
68
92
  function getDefaultRampsControllerState() {
69
93
  return {
70
94
  userRegion: null,
71
- preferredProvider: null,
95
+ selectedProvider: null,
96
+ countries: [],
72
97
  providers: [],
73
98
  tokens: null,
99
+ selectedToken: null,
100
+ paymentMethods: [],
101
+ selectedPaymentMethod: null,
74
102
  requests: {},
75
103
  };
76
104
  }
@@ -191,7 +219,6 @@ class RampsController extends base_controller_1.BaseController {
191
219
  if (pending) {
192
220
  return pending.promise;
193
221
  }
194
- // Check cache validity (unless force refresh)
195
222
  if (!options?.forceRefresh) {
196
223
  const cached = this.state.requests[cacheKey];
197
224
  if (cached && !(0, RequestCache_1.isCacheExpired)(cached, ttl)) {
@@ -260,86 +287,6 @@ class RampsController extends base_controller_1.BaseController {
260
287
  getRequestState(cacheKey) {
261
288
  return this.state.requests[cacheKey];
262
289
  }
263
- /**
264
- * Updates the user's region by fetching geolocation.
265
- * This method calls the RampsService to get the geolocation.
266
- *
267
- * @param options - Options for cache behavior.
268
- * @returns The user region object.
269
- */
270
- async updateUserRegion(options) {
271
- // If a userRegion already exists and forceRefresh is not requested,
272
- // return it immediately without fetching geolocation.
273
- // This ensures that once a region is set (either via geolocation or manual selection),
274
- // it will not be overwritten by subsequent geolocation fetches.
275
- if (this.state.userRegion && !options?.forceRefresh) {
276
- return this.state.userRegion;
277
- }
278
- // When forceRefresh is true, clear the existing region, tokens, and providers before fetching
279
- if (options?.forceRefresh) {
280
- this.update((state) => {
281
- state.userRegion = null;
282
- state.tokens = null;
283
- state.providers = [];
284
- });
285
- }
286
- const cacheKey = (0, RequestCache_1.createCacheKey)('updateUserRegion', []);
287
- const regionCode = await this.executeRequest(cacheKey, async () => {
288
- const result = await this.messenger.call('RampsService:getGeolocation');
289
- return result;
290
- }, options);
291
- if (!regionCode) {
292
- this.update((state) => {
293
- state.userRegion = null;
294
- state.tokens = null;
295
- state.providers = [];
296
- });
297
- return null;
298
- }
299
- const normalizedRegion = regionCode.toLowerCase().trim();
300
- try {
301
- const countries = await this.getCountries('buy', options);
302
- const userRegion = findRegionFromCode(normalizedRegion, countries);
303
- if (userRegion) {
304
- this.update((state) => {
305
- const regionChanged = state.userRegion?.regionCode !== userRegion.regionCode;
306
- state.userRegion = userRegion;
307
- // Clear tokens and providers when region changes
308
- if (regionChanged) {
309
- state.tokens = null;
310
- state.providers = [];
311
- }
312
- });
313
- // Fetch providers for the new region
314
- if (userRegion.regionCode) {
315
- try {
316
- await this.getProviders(userRegion.regionCode, options);
317
- }
318
- catch {
319
- // Provider fetch failed - error state will be available via selectors
320
- }
321
- }
322
- return userRegion;
323
- }
324
- // Region not found in countries data
325
- this.update((state) => {
326
- state.userRegion = null;
327
- state.tokens = null;
328
- state.providers = [];
329
- });
330
- return null;
331
- }
332
- catch {
333
- // If countries fetch fails, we can't create a valid UserRegion
334
- // Return null to indicate we don't have valid country data
335
- this.update((state) => {
336
- state.userRegion = null;
337
- state.tokens = null;
338
- state.providers = [];
339
- });
340
- return null;
341
- }
342
- }
343
290
  /**
344
291
  * Sets the user's region manually (without fetching geolocation).
345
292
  * This allows users to override the detected region.
@@ -351,100 +298,129 @@ class RampsController extends base_controller_1.BaseController {
351
298
  async setUserRegion(region, options) {
352
299
  const normalizedRegion = region.toLowerCase().trim();
353
300
  try {
354
- const countries = await this.getCountries('buy', options);
301
+ const { countries } = this.state;
302
+ if (!countries || countries.length === 0) {
303
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_cleanupState).call(this);
304
+ throw new Error('No countries found. Cannot set user region without valid country information.');
305
+ }
355
306
  const userRegion = findRegionFromCode(normalizedRegion, countries);
356
- if (userRegion) {
357
- this.update((state) => {
358
- state.userRegion = userRegion;
307
+ if (!userRegion) {
308
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_cleanupState).call(this);
309
+ throw new Error(`Region "${normalizedRegion}" not found in countries data. Cannot set user region without valid country information.`);
310
+ }
311
+ // Only cleanup state if region is actually changing
312
+ const regionChanged = normalizedRegion !== this.state.userRegion?.regionCode;
313
+ // Set the new region atomically with cleanup to avoid intermediate null state
314
+ this.update((state) => {
315
+ if (regionChanged) {
316
+ state.selectedProvider = null;
317
+ state.selectedToken = null;
359
318
  state.tokens = null;
360
319
  state.providers = [];
361
- });
362
- // Fetch providers for the new region
363
- try {
364
- await this.getProviders(userRegion.regionCode, options);
365
- }
366
- catch {
367
- // Provider fetch failed - error state will be available via selectors
320
+ state.paymentMethods = [];
321
+ state.selectedPaymentMethod = null;
368
322
  }
369
- return userRegion;
370
- }
371
- // Region not found in countries data
372
- this.update((state) => {
373
- state.userRegion = null;
374
- state.tokens = null;
375
- state.providers = [];
323
+ state.userRegion = userRegion;
376
324
  });
377
- throw new Error(`Region "${normalizedRegion}" not found in countries data. Cannot set user region without valid country information.`);
325
+ // Only trigger fetches if region changed or if data is missing
326
+ if (regionChanged || !this.state.tokens) {
327
+ this.triggerGetTokens(userRegion.regionCode, 'buy', options);
328
+ }
329
+ if (regionChanged || this.state.providers.length === 0) {
330
+ this.triggerGetProviders(userRegion.regionCode, options);
331
+ }
332
+ return userRegion;
378
333
  }
379
334
  catch (error) {
380
- // If the error is "not found", re-throw it
381
- // Otherwise, it's from countries fetch failure
382
- if (error instanceof Error && error.message.includes('not found')) {
383
- throw error;
384
- }
385
- // Countries fetch failed
386
- this.update((state) => {
387
- state.userRegion = null;
388
- state.tokens = null;
389
- state.providers = [];
390
- });
391
- throw new Error('Failed to fetch countries data. Cannot set user region without valid country information.');
335
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_cleanupState).call(this);
336
+ throw error;
392
337
  }
393
338
  }
394
339
  /**
395
- * Sets the user's preferred provider.
396
- * This allows users to set their preferred ramp provider.
340
+ * Sets the user's selected provider by ID, or clears the selection.
341
+ * Looks up the provider from the current providers in state and automatically
342
+ * fetches payment methods for that provider.
397
343
  *
398
- * @param provider - The provider object to set.
344
+ * @param providerId - The provider ID (e.g., "/providers/moonpay"), or null to clear.
345
+ * @throws If region is not set, providers are not loaded, or provider is not found.
399
346
  */
400
- setPreferredProvider(provider) {
347
+ setSelectedProvider(providerId) {
348
+ if (providerId === null) {
349
+ this.update((state) => {
350
+ state.selectedProvider = null;
351
+ state.paymentMethods = [];
352
+ state.selectedPaymentMethod = null;
353
+ });
354
+ return;
355
+ }
356
+ const regionCode = this.state.userRegion?.regionCode;
357
+ if (!regionCode) {
358
+ throw new Error('Region is required. Cannot set selected provider without valid region information.');
359
+ }
360
+ const { providers } = this.state;
361
+ if (!providers || providers.length === 0) {
362
+ throw new Error('Providers not loaded. Cannot set selected provider before providers are fetched.');
363
+ }
364
+ const provider = providers.find((prov) => prov.id === providerId);
365
+ if (!provider) {
366
+ throw new Error(`Provider with ID "${providerId}" not found in available providers.`);
367
+ }
401
368
  this.update((state) => {
402
- state.preferredProvider = provider;
369
+ state.selectedProvider = provider;
370
+ state.paymentMethods = [];
371
+ state.selectedPaymentMethod = null;
372
+ });
373
+ // fetch payment methods for the new provider
374
+ // this is needed because you can change providers without changing the token
375
+ // (getPaymentMethods will use state as its default)
376
+ this.triggerGetPaymentMethods(regionCode, {
377
+ provider: provider.id,
403
378
  });
404
379
  }
405
380
  /**
406
381
  * Initializes the controller by fetching the user's region from geolocation.
407
382
  * This should be called once at app startup to set up the initial region.
408
- * After the region is set, tokens are fetched and saved to state.
409
383
  *
410
384
  * If a userRegion already exists (from persistence or manual selection),
411
- * this method will skip geolocation fetch and only fetch tokens if needed.
385
+ * this method will skip geolocation fetch and use the existing region.
412
386
  *
413
387
  * @param options - Options for cache behavior.
414
388
  * @returns Promise that resolves when initialization is complete.
415
389
  */
416
390
  async init(options) {
417
- const userRegion = await this.updateUserRegion(options).catch(() => {
418
- // User region fetch failed - error state will be available via selectors
419
- return null;
420
- });
421
- if (userRegion) {
422
- try {
423
- await this.getTokens(userRegion.regionCode, 'buy', options);
424
- }
425
- catch {
426
- // Token fetch failed - error state will be available via selectors
427
- }
428
- try {
429
- await this.getProviders(userRegion.regionCode, options);
430
- }
431
- catch {
432
- // Provider fetch failed - error state will be available via selectors
433
- }
391
+ await this.getCountries(options);
392
+ let regionCode = this.state.userRegion?.regionCode;
393
+ regionCode ?? (regionCode = await this.messenger.call('RampsService:getGeolocation'));
394
+ if (!regionCode) {
395
+ throw new Error('Failed to fetch geolocation. Cannot initialize controller without valid region information.');
396
+ }
397
+ await this.setUserRegion(regionCode, options);
398
+ }
399
+ hydrateState(options) {
400
+ const regionCode = this.state.userRegion?.regionCode;
401
+ if (!regionCode) {
402
+ throw new Error('Region code is required. Cannot hydrate state without valid region information.');
434
403
  }
404
+ this.triggerGetTokens(regionCode, 'buy', options);
405
+ this.triggerGetProviders(regionCode, options);
435
406
  }
436
407
  /**
437
- * Fetches the list of supported countries for a given ramp action.
408
+ * Fetches the list of supported countries.
409
+ * The API returns countries with support information for both buy and sell actions.
410
+ * The countries are saved in the controller state once fetched.
438
411
  *
439
- * @param action - The ramp action type ('buy' or 'sell').
440
412
  * @param options - Options for cache behavior.
441
413
  * @returns An array of countries.
442
414
  */
443
- async getCountries(action = 'buy', options) {
444
- const cacheKey = (0, RequestCache_1.createCacheKey)('getCountries', [action]);
445
- return this.executeRequest(cacheKey, async () => {
446
- return this.messenger.call('RampsService:getCountries', action);
415
+ async getCountries(options) {
416
+ const cacheKey = (0, RequestCache_1.createCacheKey)('getCountries', []);
417
+ const countries = await this.executeRequest(cacheKey, async () => {
418
+ return this.messenger.call('RampsService:getCountries');
447
419
  }, options);
420
+ this.update((state) => {
421
+ state.countries = countries;
422
+ });
423
+ return countries;
448
424
  }
449
425
  /**
450
426
  * Fetches the list of available tokens for a given region and action.
@@ -452,7 +428,8 @@ class RampsController extends base_controller_1.BaseController {
452
428
  *
453
429
  * @param region - The region code (e.g., "us", "fr", "us-ny"). If not provided, uses the user's region from controller state.
454
430
  * @param action - The ramp action type ('buy' or 'sell').
455
- * @param options - Options for cache behavior.
431
+ * @param options - Options for cache behavior and query filters.
432
+ * @param options.provider - Provider ID(s) to filter by.
456
433
  * @returns The tokens response containing topTokens and allTokens.
457
434
  */
458
435
  async getTokens(region, action = 'buy', options) {
@@ -461,9 +438,15 @@ class RampsController extends base_controller_1.BaseController {
461
438
  throw new Error('Region is required. Either provide a region parameter or ensure userRegion is set in controller state.');
462
439
  }
463
440
  const normalizedRegion = regionToUse.toLowerCase().trim();
464
- const cacheKey = (0, RequestCache_1.createCacheKey)('getTokens', [normalizedRegion, action]);
441
+ const cacheKey = (0, RequestCache_1.createCacheKey)('getTokens', [
442
+ normalizedRegion,
443
+ action,
444
+ options?.provider,
445
+ ]);
465
446
  const tokens = await this.executeRequest(cacheKey, async () => {
466
- return this.messenger.call('RampsService:getTokens', normalizedRegion, action);
447
+ return this.messenger.call('RampsService:getTokens', normalizedRegion, action, {
448
+ provider: options?.provider,
449
+ });
467
450
  }, options);
468
451
  this.update((state) => {
469
452
  const userRegionCode = state.userRegion?.regionCode;
@@ -473,6 +456,45 @@ class RampsController extends base_controller_1.BaseController {
473
456
  });
474
457
  return tokens;
475
458
  }
459
+ /**
460
+ * Sets the user's selected token by asset ID.
461
+ * Looks up the token from the current tokens in state and automatically
462
+ * fetches payment methods for that token.
463
+ *
464
+ * @param assetId - The asset identifier in CAIP-19 format (e.g., "eip155:1/erc20:0x..."), or undefined to clear.
465
+ * @throws If region is not set, tokens are not loaded, or token is not found.
466
+ */
467
+ setSelectedToken(assetId) {
468
+ if (!assetId) {
469
+ this.update((state) => {
470
+ state.selectedToken = null;
471
+ state.paymentMethods = [];
472
+ state.selectedPaymentMethod = null;
473
+ });
474
+ return;
475
+ }
476
+ const regionCode = this.state.userRegion?.regionCode;
477
+ if (!regionCode) {
478
+ throw new Error('Region is required. Cannot set selected token without valid region information.');
479
+ }
480
+ const { tokens } = this.state;
481
+ if (!tokens) {
482
+ throw new Error('Tokens not loaded. Cannot set selected token before tokens are fetched.');
483
+ }
484
+ const token = tokens.allTokens.find((tok) => tok.assetId === assetId) ??
485
+ tokens.topTokens.find((tok) => tok.assetId === assetId);
486
+ if (!token) {
487
+ throw new Error(`Token with asset ID "${assetId}" not found in available tokens.`);
488
+ }
489
+ this.update((state) => {
490
+ state.selectedToken = token;
491
+ state.paymentMethods = [];
492
+ state.selectedPaymentMethod = null;
493
+ });
494
+ this.triggerGetPaymentMethods(regionCode, {
495
+ assetId: token.assetId,
496
+ });
497
+ }
476
498
  /**
477
499
  * Fetches the list of providers for a given region.
478
500
  * The providers are saved in the controller state once fetched.
@@ -514,6 +536,152 @@ class RampsController extends base_controller_1.BaseController {
514
536
  });
515
537
  return { providers };
516
538
  }
539
+ /**
540
+ * Fetches the list of payment methods for a given context.
541
+ * The payment methods are saved in the controller state once fetched.
542
+ *
543
+ * @param region - User's region code (e.g. "fr", "us-ny").
544
+ * @param options - Query parameters for filtering payment methods.
545
+ * @param options.fiat - Fiat currency code (e.g., "usd"). If not provided, uses the user's region currency.
546
+ * @param options.assetId - CAIP-19 cryptocurrency identifier.
547
+ * @param options.provider - Provider ID path.
548
+ * @returns The payment methods response containing payments array.
549
+ */
550
+ async getPaymentMethods(region, options) {
551
+ const regionCode = region ?? this.state.userRegion?.regionCode ?? null;
552
+ const fiatToUse = options?.fiat ?? this.state.userRegion?.country?.currency ?? null;
553
+ const assetIdToUse = options?.assetId ?? this.state.selectedToken?.assetId ?? '';
554
+ const providerToUse = options?.provider ?? this.state.selectedProvider?.id ?? '';
555
+ if (!regionCode) {
556
+ throw new Error('Region is required. Either provide a region parameter or ensure userRegion is set in controller state.');
557
+ }
558
+ if (!fiatToUse) {
559
+ throw new Error('Fiat currency is required. Either provide a fiat parameter or ensure userRegion is set in controller state.');
560
+ }
561
+ const normalizedRegion = regionCode.toLowerCase().trim();
562
+ const normalizedFiat = fiatToUse.toLowerCase().trim();
563
+ const cacheKey = (0, RequestCache_1.createCacheKey)('getPaymentMethods', [
564
+ normalizedRegion,
565
+ normalizedFiat,
566
+ assetIdToUse,
567
+ providerToUse,
568
+ ]);
569
+ const response = await this.executeRequest(cacheKey, async () => {
570
+ return this.messenger.call('RampsService:getPaymentMethods', {
571
+ region: normalizedRegion,
572
+ fiat: normalizedFiat,
573
+ assetId: assetIdToUse,
574
+ provider: providerToUse,
575
+ });
576
+ }, options);
577
+ this.update((state) => {
578
+ const currentAssetId = state.selectedToken?.assetId ?? '';
579
+ const currentProviderId = state.selectedProvider?.id ?? '';
580
+ const tokenSelectionUnchanged = assetIdToUse === currentAssetId;
581
+ const providerSelectionUnchanged = providerToUse === currentProviderId;
582
+ // 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
583
+ // ex: if the user rapidly changes the token or provider, the in-flight payment methods might not be valid
584
+ // so this check will ensure that the payment methods are still valid for the token and provider that were requested
585
+ if (tokenSelectionUnchanged && providerSelectionUnchanged) {
586
+ state.paymentMethods = response.payments;
587
+ // this will auto-select the first payment method if the selected payment method is not in the new payment methods
588
+ const currentSelectionStillValid = response.payments.some((pm) => pm.id === state.selectedPaymentMethod?.id);
589
+ if (!currentSelectionStillValid) {
590
+ state.selectedPaymentMethod = response.payments[0] ?? null;
591
+ }
592
+ }
593
+ });
594
+ return response;
595
+ }
596
+ /**
597
+ * Sets the user's selected payment method by ID.
598
+ * Looks up the payment method from the current payment methods in state.
599
+ *
600
+ * @param paymentMethodId - The payment method ID (e.g., "/payments/debit-credit-card"), or null to clear.
601
+ * @throws If payment methods are not loaded or payment method is not found.
602
+ */
603
+ setSelectedPaymentMethod(paymentMethodId) {
604
+ if (!paymentMethodId) {
605
+ this.update((state) => {
606
+ state.selectedPaymentMethod = null;
607
+ });
608
+ return;
609
+ }
610
+ const { paymentMethods } = this.state;
611
+ if (!paymentMethods || paymentMethods.length === 0) {
612
+ throw new Error('Payment methods not loaded. Cannot set selected payment method before payment methods are fetched.');
613
+ }
614
+ const paymentMethod = paymentMethods.find((pm) => pm.id === paymentMethodId);
615
+ if (!paymentMethod) {
616
+ throw new Error(`Payment method with ID "${paymentMethodId}" not found in available payment methods.`);
617
+ }
618
+ this.update((state) => {
619
+ state.selectedPaymentMethod = paymentMethod;
620
+ });
621
+ }
622
+ // ============================================================
623
+ // Sync Trigger Methods
624
+ // These fire-and-forget methods are for use in React effects.
625
+ // Errors are stored in state and available via selectors.
626
+ // ============================================================
627
+ /**
628
+ * Triggers setting the user region without throwing.
629
+ *
630
+ * @param region - The region code to set (e.g., "US-CA").
631
+ * @param options - Options for cache behavior.
632
+ */
633
+ triggerSetUserRegion(region, options) {
634
+ this.setUserRegion(region, options).catch(() => {
635
+ // Error stored in state
636
+ });
637
+ }
638
+ /**
639
+ * Triggers fetching countries without throwing.
640
+ *
641
+ * @param options - Options for cache behavior.
642
+ */
643
+ triggerGetCountries(options) {
644
+ this.getCountries(options).catch(() => {
645
+ // Error stored in state
646
+ });
647
+ }
648
+ /**
649
+ * Triggers fetching tokens without throwing.
650
+ *
651
+ * @param region - The region code. If not provided, uses userRegion from state.
652
+ * @param action - The ramp action type ('buy' or 'sell').
653
+ * @param options - Options for cache behavior.
654
+ */
655
+ triggerGetTokens(region, action = 'buy', options) {
656
+ this.getTokens(region, action, options).catch(() => {
657
+ // Error stored in state
658
+ });
659
+ }
660
+ /**
661
+ * Triggers fetching providers without throwing.
662
+ *
663
+ * @param region - The region code. If not provided, uses userRegion from state.
664
+ * @param options - Options for cache behavior and query filters.
665
+ */
666
+ triggerGetProviders(region, options) {
667
+ this.getProviders(region, options).catch(() => {
668
+ // Error stored in state
669
+ });
670
+ }
671
+ /**
672
+ * Triggers fetching payment methods without throwing.
673
+ *
674
+ * @param region - User's region code (e.g., "us", "fr", "us-ny").
675
+ * @param options - Query parameters for filtering payment methods.
676
+ * @param options.fiat - Fiat currency code. If not provided, uses userRegion currency.
677
+ * @param options.assetId - CAIP-19 cryptocurrency identifier.
678
+ * @param options.provider - Provider ID path.
679
+ */
680
+ triggerGetPaymentMethods(region, options) {
681
+ this.getPaymentMethods(region, options).catch(() => {
682
+ // Error stored in state
683
+ });
684
+ }
517
685
  }
518
686
  exports.RampsController = RampsController;
519
687
  _RampsController_requestCacheTTL = new WeakMap(), _RampsController_requestCacheMaxSize = new WeakMap(), _RampsController_pendingRequests = new WeakMap(), _RampsController_instances = new WeakSet(), _RampsController_removeRequestState = function _RampsController_removeRequestState(cacheKey) {
@@ -521,22 +689,44 @@ _RampsController_requestCacheTTL = new WeakMap(), _RampsController_requestCacheM
521
689
  const requests = state.requests;
522
690
  delete requests[cacheKey];
523
691
  });
692
+ }, _RampsController_cleanupState = function _RampsController_cleanupState() {
693
+ this.update((state) => {
694
+ state.userRegion = null;
695
+ state.selectedProvider = null;
696
+ state.selectedToken = null;
697
+ state.tokens = null;
698
+ state.providers = [];
699
+ state.paymentMethods = [];
700
+ state.selectedPaymentMethod = null;
701
+ });
524
702
  }, _RampsController_updateRequestState = function _RampsController_updateRequestState(cacheKey, requestState) {
525
703
  const maxSize = __classPrivateFieldGet(this, _RampsController_requestCacheMaxSize, "f");
704
+ const ttl = __classPrivateFieldGet(this, _RampsController_requestCacheTTL, "f");
526
705
  this.update((state) => {
527
706
  const requests = state.requests;
528
707
  requests[cacheKey] = requestState;
529
- // Evict oldest entries if cache exceeds max size
708
+ // Evict expired entries based on TTL
709
+ // Only evict SUCCESS states that have exceeded their TTL
530
710
  const keys = Object.keys(requests);
531
- if (keys.length > maxSize) {
711
+ for (const key of keys) {
712
+ const entry = requests[key];
713
+ if (entry &&
714
+ entry.status === RequestCache_1.RequestStatus.SUCCESS &&
715
+ (0, RequestCache_1.isCacheExpired)(entry, ttl)) {
716
+ delete requests[key];
717
+ }
718
+ }
719
+ // Evict oldest entries if cache still exceeds max size
720
+ const remainingKeys = Object.keys(requests);
721
+ if (remainingKeys.length > maxSize) {
532
722
  // Sort by timestamp (oldest first)
533
- const sortedKeys = keys.sort((a, b) => {
723
+ const sortedKeys = remainingKeys.sort((a, b) => {
534
724
  const aTime = requests[a]?.timestamp ?? 0;
535
725
  const bTime = requests[b]?.timestamp ?? 0;
536
726
  return aTime - bTime;
537
727
  });
538
728
  // Remove oldest entries until we're under the limit
539
- const entriesToRemove = keys.length - maxSize;
729
+ const entriesToRemove = remainingKeys.length - maxSize;
540
730
  for (let i = 0; i < entriesToRemove; i++) {
541
731
  const keyToRemove = sortedKeys[i];
542
732
  if (keyToRemove) {