@metamask/ramps-controller 14.3.0 → 15.1.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.
Files changed (36) hide show
  1. package/CHANGELOG.md +24 -1
  2. package/dist/RampsController-method-action-types.cjs.map +1 -1
  3. package/dist/RampsController-method-action-types.d.cts +4 -4
  4. package/dist/RampsController-method-action-types.d.cts.map +1 -1
  5. package/dist/RampsController-method-action-types.d.mts +4 -4
  6. package/dist/RampsController-method-action-types.d.mts.map +1 -1
  7. package/dist/RampsController-method-action-types.mjs.map +1 -1
  8. package/dist/RampsController.cjs +185 -29
  9. package/dist/RampsController.cjs.map +1 -1
  10. package/dist/RampsController.d.cts +37 -7
  11. package/dist/RampsController.d.cts.map +1 -1
  12. package/dist/RampsController.d.mts +37 -7
  13. package/dist/RampsController.d.mts.map +1 -1
  14. package/dist/RampsController.mjs +185 -29
  15. package/dist/RampsController.mjs.map +1 -1
  16. package/dist/RampsService-method-action-types.cjs.map +1 -1
  17. package/dist/RampsService-method-action-types.d.cts +4 -3
  18. package/dist/RampsService-method-action-types.d.cts.map +1 -1
  19. package/dist/RampsService-method-action-types.d.mts +4 -3
  20. package/dist/RampsService-method-action-types.d.mts.map +1 -1
  21. package/dist/RampsService-method-action-types.mjs.map +1 -1
  22. package/dist/RampsService.cjs +4 -10
  23. package/dist/RampsService.cjs.map +1 -1
  24. package/dist/RampsService.d.cts +4 -5
  25. package/dist/RampsService.d.cts.map +1 -1
  26. package/dist/RampsService.d.mts +4 -5
  27. package/dist/RampsService.d.mts.map +1 -1
  28. package/dist/RampsService.mjs +4 -10
  29. package/dist/RampsService.mjs.map +1 -1
  30. package/dist/index.cjs.map +1 -1
  31. package/dist/index.d.cts +1 -1
  32. package/dist/index.d.cts.map +1 -1
  33. package/dist/index.d.mts +1 -1
  34. package/dist/index.d.mts.map +1 -1
  35. package/dist/index.mjs.map +1 -1
  36. package/package.json +3 -3
@@ -9,7 +9,7 @@ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (
9
9
  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");
10
10
  return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
11
11
  };
12
- 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_getSupportingProvidersForRegion, _RampsController_filterProviderIdsBySupport, _RampsController_resolveProviderIdsForQuote, _RampsController_getPreferredProviderIdsFromOrders, _RampsController_refreshOrder, _RampsController_pollPendingOrders, _RampsController_syncTransakAuthOnError, _RampsController_getNormalizedTransakError;
12
+ var _RampsController_instances, _RampsController_requestCacheTTL, _RampsController_requestCacheMaxSize, _RampsController_getProviderScope, _RampsController_getDefaultRedirectUrl, _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_syncUserRegionFromCountriesCatalog, _RampsController_pickInAppQuote, _RampsController_getSupportingProvidersForRegion, _RampsController_filterProviderIdsBySupport, _RampsController_resolveProviderIdsForQuote, _RampsController_getPreferredProviderIdsFromOrders, _RampsController_refreshOrder, _RampsController_pollPendingOrders, _RampsController_syncTransakAuthOnError, _RampsController_getNormalizedTransakError;
13
13
  import { BaseController } from "@metamask/base-controller";
14
14
  import { BrokenCircuitError } from "@metamask/controller-utils";
15
15
  import { RAMPS_ERROR_CODES } from "./rampsErrorCodes.mjs";
@@ -132,7 +132,7 @@ const rampsControllerMetadata = {
132
132
  usedInUi: true,
133
133
  },
134
134
  countries: {
135
- persist: true,
135
+ persist: false,
136
136
  includeInDebugSnapshot: true,
137
137
  includeInStateLogs: true,
138
138
  usedInUi: true,
@@ -435,8 +435,13 @@ export class RampsController extends BaseController {
435
435
  * controller. Missing properties will be filled in with defaults.
436
436
  * @param args.requestCacheTTL - Time to live for cached requests in milliseconds.
437
437
  * @param args.requestCacheMaxSize - Maximum number of entries in the request cache.
438
+ * @param args.getProviderScope - Optional callback returning the current
439
+ * provider-class scope for fiat quote widening. Defaults to `off`.
440
+ * @param args.getDefaultRedirectUrl - Optional callback returning the default
441
+ * redirect URL used for the widened in-app quote fetch when the caller omits
442
+ * `redirectUrl`. Defaults to a callback returning `undefined`.
438
443
  */
439
- constructor({ messenger, state = {}, requestCacheTTL = DEFAULT_REQUEST_CACHE_TTL, requestCacheMaxSize = DEFAULT_REQUEST_CACHE_MAX_SIZE, }) {
444
+ constructor({ messenger, state = {}, requestCacheTTL = DEFAULT_REQUEST_CACHE_TTL, requestCacheMaxSize = DEFAULT_REQUEST_CACHE_MAX_SIZE, getProviderScope, getDefaultRedirectUrl, }) {
440
445
  super({
441
446
  messenger,
442
447
  metadata: rampsControllerMetadata,
@@ -457,6 +462,17 @@ export class RampsController extends BaseController {
457
462
  * Maximum number of entries in the request cache.
458
463
  */
459
464
  _RampsController_requestCacheMaxSize.set(this, void 0);
465
+ /**
466
+ * Resolves the current provider-class scope for fiat quote widening. Defaults
467
+ * to `() => 'off'` (native-only) when no callback is injected.
468
+ */
469
+ _RampsController_getProviderScope.set(this, void 0);
470
+ /**
471
+ * Resolves the default redirect URL for the widened in-app quote fetch when
472
+ * the caller omits `redirectUrl`. Defaults to `() => undefined` when no
473
+ * callback is injected.
474
+ */
475
+ _RampsController_getDefaultRedirectUrl.set(this, void 0);
460
476
  /**
461
477
  * Map of pending requests for deduplication.
462
478
  * Key is the cache key, value is the pending request with abort controller.
@@ -478,6 +494,8 @@ export class RampsController extends BaseController {
478
494
  _RampsController_initPromise.set(this, null);
479
495
  __classPrivateFieldSet(this, _RampsController_requestCacheTTL, requestCacheTTL, "f");
480
496
  __classPrivateFieldSet(this, _RampsController_requestCacheMaxSize, requestCacheMaxSize, "f");
497
+ __classPrivateFieldSet(this, _RampsController_getProviderScope, getProviderScope ?? (() => 'off'), "f");
498
+ __classPrivateFieldSet(this, _RampsController_getDefaultRedirectUrl, getDefaultRedirectUrl ?? (() => undefined), "f");
481
499
  this.messenger.registerMethodActionHandlers(this, MESSENGER_EXPOSED_METHODS);
482
500
  }
483
501
  /**
@@ -715,8 +733,10 @@ export class RampsController extends BaseController {
715
733
  * This should be called once at app startup to set up the initial region.
716
734
  *
717
735
  * Idempotent: subsequent calls return the same promise unless forceRefresh is set.
718
- * Skips getCountries when countries are already loaded; skips geolocation when
719
- * userRegion already exists.
736
+ * Force-refetches the countries catalog on startup (bypassing the in-session
737
+ * request cache) so region preset amounts stay current. The catalog is not
738
+ * persisted, so a cold start always re-fetches it regardless. Skips
739
+ * geolocation when userRegion already exists.
720
740
  *
721
741
  * @param options - Options for cache behavior. forceRefresh bypasses idempotency and re-runs the full flow.
722
742
  * @returns Promise that resolves when initialization is complete.
@@ -753,6 +773,7 @@ export class RampsController extends BaseController {
753
773
  this.update((state) => {
754
774
  state.countries.data = Array.isArray(countries) ? [...countries] : [];
755
775
  });
776
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_syncUserRegionFromCountriesCatalog).call(this);
756
777
  return countries;
757
778
  }
758
779
  /**
@@ -827,7 +848,6 @@ export class RampsController extends BaseController {
827
848
  * @param options - Options for cache behavior and query filters.
828
849
  * @param options.provider - Provider ID(s) to filter by.
829
850
  * @param options.crypto - Crypto currency ID(s) to filter by.
830
- * @param options.fiat - Fiat currency ID(s) to filter by.
831
851
  * @param options.payments - Payment method ID(s) to filter by.
832
852
  * @returns The providers response containing providers array.
833
853
  */
@@ -838,14 +858,12 @@ export class RampsController extends BaseController {
838
858
  normalizedRegion,
839
859
  options?.provider,
840
860
  options?.crypto,
841
- options?.fiat,
842
861
  options?.payments,
843
862
  ]);
844
863
  const { providers } = await this.executeRequest(cacheKey, async () => {
845
864
  return this.messenger.call('RampsService:getProviders', normalizedRegion, {
846
865
  provider: options?.provider,
847
866
  crypto: options?.crypto,
848
- fiat: options?.fiat,
849
867
  payments: options?.payments,
850
868
  });
851
869
  }, {
@@ -867,31 +885,23 @@ export class RampsController extends BaseController {
867
885
  *
868
886
  * @param region - User's region code (e.g. "fr", "us-ny").
869
887
  * @param options - Query parameters for filtering payment methods.
870
- * @param options.fiat - Fiat currency code (e.g., "usd"). If not provided, uses the user's region currency.
871
888
  * @param options.assetId - CAIP-19 cryptocurrency identifier.
872
889
  * @param options.provider - Provider ID path.
873
890
  * @returns The payment methods response containing payments array.
874
891
  */
875
892
  async getPaymentMethods(region, options) {
876
893
  const regionCode = region ?? __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_requireRegion).call(this);
877
- const fiatToUse = options?.fiat ?? this.state.userRegion?.country?.currency ?? null;
878
894
  const assetIdToUse = options?.assetId ?? this.state.tokens.selected?.assetId ?? '';
879
895
  const providerToUse = options?.provider ?? this.state.providers.selected?.id ?? '';
880
- if (!fiatToUse) {
881
- throw new Error('Fiat currency is required. Either provide a fiat parameter or ensure userRegion is set in controller state.');
882
- }
883
896
  const normalizedRegion = regionCode.toLowerCase().trim();
884
- const normalizedFiat = fiatToUse.toLowerCase().trim();
885
897
  const cacheKey = createCacheKey('getPaymentMethods', [
886
898
  normalizedRegion,
887
- normalizedFiat,
888
899
  assetIdToUse,
889
900
  providerToUse,
890
901
  ]);
891
902
  const response = await this.executeRequest(cacheKey, async () => {
892
903
  return this.messenger.call('RampsService:getPaymentMethods', {
893
904
  region: normalizedRegion,
894
- fiat: normalizedFiat,
895
905
  assetId: assetIdToUse,
896
906
  provider: providerToUse,
897
907
  });
@@ -1000,7 +1010,19 @@ export class RampsController extends BaseController {
1000
1010
  if (normalizedAssetIdForValidation === '') {
1001
1011
  throw new Error('assetId is required.');
1002
1012
  }
1013
+ // When a non-`off` provider scope is active, widen the native-only
1014
+ // auto-selection path to every supporting provider and pick the best in-app
1015
+ // quote from the results (in-app vs external is only knowable per-quote via
1016
+ // `buyWidget.browser`). Only the auto-select/restrict path that MM Pay's
1017
+ // `getRampsQuote` uses is affected; explicit-`providers` callers and the
1018
+ // plain all-provider path are untouched.
1019
+ const providerScope = __classPrivateFieldGet(this, _RampsController_getProviderScope, "f").call(this);
1020
+ const widenToInAppProviders = providerScope !== 'off' &&
1021
+ !options.providers &&
1022
+ (options.autoSelectProvider === true ||
1023
+ options.restrictToKnownOrNativeProviders === true);
1003
1024
  let providersToUse;
1025
+ let inAppProviderCatalog = this.state.providers.data;
1004
1026
  if (options.providers) {
1005
1027
  providersToUse = options.restrictToKnownOrNativeProviders
1006
1028
  ? await __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_filterProviderIdsBySupport).call(this, {
@@ -1010,6 +1032,17 @@ export class RampsController extends BaseController {
1010
1032
  })
1011
1033
  : options.providers;
1012
1034
  }
1035
+ else if (widenToInAppProviders) {
1036
+ // `#getSupportingProvidersForRegion` also hydrates the provider catalog
1037
+ // when controller state is empty, so all-provider quoting cannot silently
1038
+ // return zero providers here.
1039
+ const { supporting } = await __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_getSupportingProvidersForRegion).call(this, {
1040
+ assetId: normalizedAssetIdForValidation,
1041
+ region: regionToUse,
1042
+ });
1043
+ inAppProviderCatalog = supporting;
1044
+ providersToUse = supporting.map((provider) => provider.id);
1045
+ }
1013
1046
  else if (options.autoSelectProvider ||
1014
1047
  options.restrictToKnownOrNativeProviders) {
1015
1048
  // The restriction flag implies resolution: it must narrow the provider
@@ -1039,8 +1072,12 @@ export class RampsController extends BaseController {
1039
1072
  // Under headless-buy gating, an empty resolved provider list means no
1040
1073
  // eligible (native/supporting) provider exists. Return an empty response
1041
1074
  // rather than passing `[]` to the service, which omits the provider filter
1042
- // and would quote every provider.
1043
- if (options.restrictToKnownOrNativeProviders &&
1075
+ // and would quote every provider. This also guards the widened in-app path:
1076
+ // a caller may trigger widening with `autoSelectProvider` alone (no
1077
+ // `restrictToKnownOrNativeProviders`), and an empty supporting set must not
1078
+ // fall through to unfiltered quotes from providers that do not support the
1079
+ // asset.
1080
+ if ((options.restrictToKnownOrNativeProviders || widenToInAppProviders) &&
1044
1081
  providersToUse.length === 0) {
1045
1082
  return { success: [], sorted: [], error: [], customActions: [] };
1046
1083
  }
@@ -1048,6 +1085,12 @@ export class RampsController extends BaseController {
1048
1085
  const normalizedFiat = fiatToUse.toLowerCase().trim();
1049
1086
  const normalizedAssetId = normalizedAssetIdForValidation;
1050
1087
  const normalizedWalletAddress = options.walletAddress.trim();
1088
+ // The quotes API only embeds a `buyURL`/`buyWidget` when a `redirectUrl` is
1089
+ // present, so on the widened in-app path (where MM Pay omits one) supply the
1090
+ // injected default so aggregator quotes carry a usable widget URL. An
1091
+ // explicit caller `redirectUrl` always wins, and scope `off` never injects.
1092
+ const effectiveRedirectUrl = options.redirectUrl ??
1093
+ (widenToInAppProviders ? __classPrivateFieldGet(this, _RampsController_getDefaultRedirectUrl, "f").call(this) : undefined);
1051
1094
  const cacheKey = createCacheKey('getQuotes', [
1052
1095
  normalizedRegion,
1053
1096
  normalizedFiat,
@@ -1056,7 +1099,7 @@ export class RampsController extends BaseController {
1056
1099
  normalizedWalletAddress,
1057
1100
  [...paymentMethodsToUse].sort().join(','),
1058
1101
  [...providersToUse].sort().join(','),
1059
- options.redirectUrl,
1102
+ effectiveRedirectUrl,
1060
1103
  action,
1061
1104
  ]);
1062
1105
  const params = {
@@ -1067,15 +1110,45 @@ export class RampsController extends BaseController {
1067
1110
  walletAddress: normalizedWalletAddress,
1068
1111
  paymentMethods: paymentMethodsToUse,
1069
1112
  providers: providersToUse,
1070
- redirectUrl: options.redirectUrl,
1113
+ redirectUrl: effectiveRedirectUrl,
1071
1114
  action,
1072
1115
  };
1073
- return this.executeRequest(cacheKey, async () => {
1116
+ const response = await this.executeRequest(cacheKey, async () => {
1074
1117
  return this.messenger.call('RampsService:getQuotes', params);
1075
1118
  }, {
1076
1119
  forceRefresh: options.forceRefresh,
1077
1120
  ttl: options.ttl ?? DEFAULT_QUOTES_TTL,
1078
1121
  });
1122
+ if (!widenToInAppProviders) {
1123
+ return response;
1124
+ }
1125
+ // Reduce the widened multi-provider result to the single best in-app quote
1126
+ // and place it at `success[0]`, since single-pick consumers
1127
+ // (`getRampsQuote` -> `success?.[0]`) rely on index 0 while `success[]`
1128
+ // order is server-defined rather than ranked.
1129
+ const selectedQuote = __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_pickInAppQuote).call(this, response, {
1130
+ scope: providerScope,
1131
+ amount: options.amount,
1132
+ fiat: normalizedFiat,
1133
+ providers: inAppProviderCatalog,
1134
+ });
1135
+ if (!selectedQuote) {
1136
+ // No usable in-app quote: surface "no quote" rather than leaking an
1137
+ // external/custom quote to the single-pick consumer.
1138
+ return {
1139
+ success: [],
1140
+ sorted: response.sorted,
1141
+ error: response.error,
1142
+ customActions: response.customActions,
1143
+ };
1144
+ }
1145
+ return {
1146
+ ...response,
1147
+ success: [
1148
+ selectedQuote,
1149
+ ...response.success.filter((quote) => quote !== selectedQuote),
1150
+ ],
1151
+ };
1079
1152
  }
1080
1153
  // === ORDER MANAGEMENT ===
1081
1154
  /**
@@ -1691,7 +1764,7 @@ export class RampsController extends BaseController {
1691
1764
  }
1692
1765
  }
1693
1766
  }
1694
- _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() {
1767
+ _RampsController_requestCacheTTL = new WeakMap(), _RampsController_requestCacheMaxSize = new WeakMap(), _RampsController_getProviderScope = new WeakMap(), _RampsController_getDefaultRedirectUrl = 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() {
1695
1768
  for (const resourceType of DEPENDENT_RESOURCE_KEYS) {
1696
1769
  __classPrivateFieldGet(this, _RampsController_pendingResourceCount, "f").delete(resourceType);
1697
1770
  const generation = __classPrivateFieldGet(this, _RampsController_pendingResourceGeneration, "f").get(resourceType) ?? 0;
@@ -1777,20 +1850,103 @@ _RampsController_requestCacheTTL = new WeakMap(), _RampsController_requestCacheM
1777
1850
  }
1778
1851
  });
1779
1852
  }, _RampsController_runInit = async function _RampsController_runInit(options) {
1780
- const forceRefresh = options?.forceRefresh === true;
1781
- const hasCountries = this.state.countries.data.length > 0;
1782
- if (forceRefresh || !hasCountries) {
1783
- await this.getCountries(options);
1784
- }
1853
+ // Force-refetch the catalog on startup so region preset amounts stay
1854
+ // current, bypassing the in-session request cache. The catalog is not
1855
+ // persisted, so a cold start always re-fetches it regardless.
1856
+ await this.getCountries({ ...options, forceRefresh: true });
1785
1857
  // Always prefer the user's persisted region. Geolocation is only used to
1786
1858
  // seed the initial value; once the user (or a prior init) has set a region
1787
1859
  // we must respect that choice — even on forceRefresh.
1788
- let regionCode = this.state.userRegion?.regionCode;
1789
- regionCode ?? (regionCode = await this.messenger.call('RampsService:getGeolocation'));
1860
+ const persistedRegionCode = this.state.userRegion?.regionCode;
1861
+ const regionCode = persistedRegionCode ??
1862
+ (await this.messenger.call('RampsService:getGeolocation'));
1790
1863
  if (!regionCode) {
1791
1864
  throw new Error('Failed to fetch geolocation. Cannot initialize controller without valid region information.');
1792
1865
  }
1866
+ // For an already-persisted region, getCountries() has already re-synced it
1867
+ // from the fresh catalog (see #syncUserRegionFromCountriesCatalog). Calling
1868
+ // setUserRegion here would re-validate against that catalog and, if it is
1869
+ // momentarily empty or no longer lists the region (e.g. a transient/partial
1870
+ // catalog response or a region with no current provider coverage), throw and
1871
+ // wipe the persisted region via #cleanupState. Preserve the existing region
1872
+ // instead; only resolve a brand-new region (from geolocation) strictly.
1873
+ if (persistedRegionCode) {
1874
+ return;
1875
+ }
1793
1876
  await this.setUserRegion(regionCode, options);
1877
+ }, _RampsController_syncUserRegionFromCountriesCatalog = function _RampsController_syncUserRegionFromCountriesCatalog() {
1878
+ const regionCode = this.state.userRegion?.regionCode;
1879
+ if (!regionCode) {
1880
+ return;
1881
+ }
1882
+ const countriesData = this.state.countries.data;
1883
+ if (!countriesData.length) {
1884
+ return;
1885
+ }
1886
+ const userRegion = findRegionFromCode(regionCode, countriesData);
1887
+ if (!userRegion) {
1888
+ return;
1889
+ }
1890
+ this.update((state) => {
1891
+ state.userRegion = userRegion;
1892
+ });
1893
+ }, _RampsController_pickInAppQuote = function _RampsController_pickInAppQuote(response, { scope, amount, fiat, providers, }) {
1894
+ const providerByCode = new Map(providers.map((provider) => [
1895
+ normalizeProviderCode(provider.id),
1896
+ provider,
1897
+ ]));
1898
+ const customActionProviderCodes = new Set(response.customActions.map((action) => normalizeProviderCode(action.buy.providerId)));
1899
+ const fitsProviderLimits = (quote) => {
1900
+ const provider = providerByCode.get(normalizeProviderCode(quote.provider));
1901
+ const limit = provider?.limits?.fiat?.[fiat]?.[quote.quote.paymentMethod];
1902
+ if (!limit) {
1903
+ // No published limits for this provider/payment method: treat as
1904
+ // eligible and let the provider enforce limits at checkout.
1905
+ return true;
1906
+ }
1907
+ return amount >= limit.minAmount && amount <= limit.maxAmount;
1908
+ };
1909
+ const isEligible = (quote) => {
1910
+ // `all` (Phase 2) skips the in-app-only exclusions; both scopes still
1911
+ // enforce provider limits up front.
1912
+ if (scope !== 'all') {
1913
+ const providerCode = normalizeProviderCode(quote.provider);
1914
+ if (customActionProviderCodes.has(providerCode)) {
1915
+ return false;
1916
+ }
1917
+ // Defensive: the wire may carry an inline `isCustomAction` flag that is
1918
+ // absent from the published `Quote` type.
1919
+ if (quote.quote.isCustomAction === true) {
1920
+ return false;
1921
+ }
1922
+ if (quote.quote.buyWidget?.browser === 'IN_APP_OS_BROWSER') {
1923
+ return false;
1924
+ }
1925
+ }
1926
+ return fitsProviderLimits(quote);
1927
+ };
1928
+ const candidates = response.success.filter(isEligible);
1929
+ if (candidates.length === 0) {
1930
+ return undefined;
1931
+ }
1932
+ const candidateByCode = new Map(candidates.map((quote) => [normalizeProviderCode(quote.provider), quote]));
1933
+ const pickBySortOrder = (sortBy) => {
1934
+ const order = response.sorted.find((entry) => entry.sortBy === sortBy)?.ids;
1935
+ if (!order) {
1936
+ return undefined;
1937
+ }
1938
+ for (const providerId of order) {
1939
+ const match = candidateByCode.get(normalizeProviderCode(providerId));
1940
+ if (match) {
1941
+ return match;
1942
+ }
1943
+ }
1944
+ return undefined;
1945
+ };
1946
+ // Reliability first, then price, then the first surviving candidate.
1947
+ return (pickBySortOrder('reliability') ??
1948
+ pickBySortOrder('price') ??
1949
+ candidates[0]);
1794
1950
  }, _RampsController_getSupportingProvidersForRegion =
1795
1951
  /**
1796
1952
  * Returns the region's providers that support the given asset, plus the full