@metamask/ramps-controller 24.0.0 → 25.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.
@@ -85,24 +85,56 @@ export const RAMPS_CONTROLLER_REQUIRED_CONTROLLER_ACTIONS = [
85
85
  'KycController:refreshSessionStatus',
86
86
  'KycController:hasCompletedVendorDisclaimers',
87
87
  'KycController:hasCompletedSessionDisclaimers',
88
+ 'KycController:clearState',
88
89
  'RemoteFeatureFlagController:getState',
89
90
  'UserStorageController:getState',
90
91
  'UserStorageController:performGetStorageAllFeatureEntries',
91
92
  'UserStorageController:performBatchSetStorage',
92
93
  ];
93
94
  /**
94
- * The Mobile route for the current VBA onboarding step.
95
+ * KYC vocabulary used on the VBA onboarding snapshot. `'none'` means no
96
+ * session exists yet (or the vendor returned an unrecognized status).
95
97
  */
96
- export var VbaOnboardingStage;
97
- (function (VbaOnboardingStage) {
98
- VbaOnboardingStage["EmailOtpRequired"] = "EmailOtpRequired";
99
- VbaOnboardingStage["VendorTermsRequired"] = "VendorTermsRequired";
100
- VbaOnboardingStage["ProviderTermsRequired"] = "ProviderTermsRequired";
101
- VbaOnboardingStage["KycRequired"] = "KycRequired";
102
- VbaOnboardingStage["KycPending"] = "KycPending";
103
- VbaOnboardingStage["KycRejected"] = "KycRejected";
104
- VbaOnboardingStage["Completed"] = "Completed";
105
- })(VbaOnboardingStage || (VbaOnboardingStage = {}));
98
+ export const VBA_KYC_STATUSES = [
99
+ 'none',
100
+ 'new',
101
+ 'retry',
102
+ 'pending',
103
+ 'approved',
104
+ 'rejected',
105
+ ];
106
+ /**
107
+ * Autoramp setup progress after KYC has been approved.
108
+ * `'in_progress'` is reserved for hosts that observe an in-flight hydrate;
109
+ * {@link RampsController.hydrateVbaOnboarding} itself returns `'ready'` or
110
+ * `'retryable_failure'` once the coalesced run settles.
111
+ */
112
+ export const VBA_AUTORAMP_STATUSES = [
113
+ 'not_ready',
114
+ 'in_progress',
115
+ 'ready',
116
+ 'retryable_failure',
117
+ ];
118
+ const EMPTY_VBA_ONBOARDING_SNAPSHOT = {
119
+ sessionExists: false,
120
+ vendorDisclaimersComplete: false,
121
+ sessionDisclaimersComplete: false,
122
+ kycStatus: 'none',
123
+ autorampStatus: 'not_ready',
124
+ };
125
+ const VBA_KYC_STATUS_SET = new Set(VBA_KYC_STATUSES);
126
+ /**
127
+ * Maps a vendor status string onto the snapshot vocabulary.
128
+ *
129
+ * @param value - Raw KYC status from the session.
130
+ * @returns A known {@link VbaKycStatus}, or `'none'` when missing/unknown.
131
+ */
132
+ function toVbaKycStatus(value) {
133
+ if (value && VBA_KYC_STATUS_SET.has(value)) {
134
+ return value;
135
+ }
136
+ return 'none';
137
+ }
106
138
  /**
107
139
  * Distinguishes an already-materialized {@link AutorampAccount} from the
108
140
  * create-fields shape accepted by {@link RampsController.addAutoramp}.
@@ -237,12 +269,6 @@ const rampsControllerMetadata = {
237
269
  includeInStateLogs: true,
238
270
  usedInUi: true,
239
271
  },
240
- vbaOnboardingStage: {
241
- persist: true,
242
- includeInDebugSnapshot: true,
243
- includeInStateLogs: true,
244
- usedInUi: true,
245
- },
246
272
  };
247
273
  /**
248
274
  * Creates a default resource state object.
@@ -288,7 +314,6 @@ export function getDefaultRampsControllerState() {
288
314
  orders: [],
289
315
  autoramps: [],
290
316
  providerAutoSelected: false,
291
- vbaOnboardingStage: null,
292
317
  };
293
318
  }
294
319
  const DEPENDENT_RESOURCE_KEYS = [
@@ -450,6 +475,7 @@ const MESSENGER_EXPOSED_METHODS = [
450
475
  'startOrderPolling',
451
476
  'stopOrderPolling',
452
477
  'getBuyWidgetData',
478
+ 'getFallbackBuyWidgetData',
453
479
  'addPrecreatedOrder',
454
480
  'getOrder',
455
481
  'getOrderFromCallback',
@@ -2500,15 +2526,17 @@ export class RampsController extends BaseController {
2500
2526
  }
2501
2527
  }
2502
2528
  /**
2503
- * Hydrates the Mobile-routable VBA onboarding stage from KYC state and
2504
- * completes wallet and autoramp setup after KYC acceptance.
2529
+ * Refreshes KYC session facts and, when Iron has approved KYC, activates the
2530
+ * Money Account (wallet registration + autoramp). Hosts map the returned
2531
+ * {@link VbaOnboardingSnapshot} onto their own funnel; this method does not
2532
+ * name screens.
2505
2533
  *
2506
2534
  * Overlapping calls share one run so polling cannot trigger duplicate wallet
2507
2535
  * signatures or autoramp creation.
2508
2536
  *
2509
2537
  * @param params - VBA onboarding parameters.
2510
2538
  * @param params.walletAddress - Monad Money Account wallet address.
2511
- * @returns The hydrated onboarding stage.
2539
+ * @returns Independent KYC and autoramp facts for the current customer.
2512
2540
  */
2513
2541
  async hydrateVbaOnboarding({ walletAddress, }) {
2514
2542
  if (this.#vbaOnboardingHydrationPromise) {
@@ -2526,20 +2554,21 @@ export class RampsController extends BaseController {
2526
2554
  }
2527
2555
  }
2528
2556
  async #hydrateVbaOnboarding(walletAddress) {
2529
- // Fetch the customer's latest session from the vendor account so each stage
2530
- // reflects backend truth (e.g. re-verification required after a new
2531
- // document) rather than only device-local state. A `null` session means no
2532
- // customer/session exists yet, so onboarding starts at the email step.
2533
2557
  // Prefer the in-memory/persisted session status over the backend
2534
- // latest-status endpoint: after SumSub the backend endpoint lags (it still
2535
- // reports kycStatus 'new' right after an 'approved' applicant result), while
2558
+ // latest-status endpoint: after SumSub the backend endpoint can lag, while
2536
2559
  // the controller state reflects the journey/SDK outcome. Fall back to a
2537
2560
  // backend fetch only when the controller has no session in state (e.g. a
2538
2561
  // reinstall/cleared state resuming an existing customer, or a brand-new user
2539
2562
  // with no session at all).
2540
2563
  let session = null;
2564
+ // Whether `session` came from persisted controller state (as opposed to a
2565
+ // fresh backend fetch, which is always scoped to the current user). Only a
2566
+ // persisted session can belong to a previous identity, so only that path
2567
+ // needs the ownership check below.
2568
+ let sessionFromCache = false;
2541
2569
  try {
2542
2570
  session = this.messenger.call('KycController:refreshSessionStatus');
2571
+ sessionFromCache = true;
2543
2572
  }
2544
2573
  catch {
2545
2574
  try {
@@ -2547,48 +2576,43 @@ export class RampsController extends BaseController {
2547
2576
  }
2548
2577
  catch {
2549
2578
  // No session exists for this customer yet: the backend returns 404
2550
- // ("KYC session not found"), which surfaces as a rejection here. Treat
2551
- // it as "start onboarding at the email step" rather than an error.
2579
+ // ("KYC session not found"), which surfaces as a rejection here.
2552
2580
  session = null;
2553
2581
  }
2554
2582
  }
2555
2583
  if (!session) {
2556
- return this.#setVbaOnboardingStage(VbaOnboardingStage.EmailOtpRequired);
2557
- }
2558
- if (!(await this.messenger.call('KycController:hasCompletedVendorDisclaimers'))) {
2559
- return this.#setVbaOnboardingStage(VbaOnboardingStage.VendorTermsRequired);
2560
- }
2561
- if (!(await this.messenger.call('KycController:hasCompletedSessionDisclaimers'))) {
2562
- return this.#setVbaOnboardingStage(VbaOnboardingStage.ProviderTermsRequired);
2563
- }
2564
- // Status fields draw from the KYC vocabulary (new | pending | approved |
2565
- // rejected | retry). `finalStatus` is the vendor's final decision, which
2566
- // stays `pending` until Iron finalizes. `kycStatus` is the SumSub applicant
2567
- // outcome (from the journey/SDK result): `new` before the applicant runs
2568
- // SumSub, moving to `approved`/`pending` once they submit while the vendor
2569
- // finalizes. So gate the SumSub screen on `kycStatus`, and only complete
2570
- // onboarding once `finalStatus` is the terminal `approved`.
2571
- const { finalStatus, kycStatus } = session;
2572
- if (finalStatus === 'rejected' || kycStatus === 'rejected') {
2573
- return this.#setVbaOnboardingStage(VbaOnboardingStage.KycRejected);
2574
- }
2575
- if (finalStatus !== 'approved') {
2576
- if (kycStatus === 'new' || kycStatus === 'retry') {
2577
- // Applicant still has to run (or re-run) SumSub document verification.
2578
- return this.#setVbaOnboardingStage(VbaOnboardingStage.KycRequired);
2579
- }
2580
- // Submitted; vendor is finalizing → "verification in progress".
2581
- return this.#setVbaOnboardingStage(VbaOnboardingStage.KycPending);
2584
+ return { ...EMPTY_VBA_ONBOARDING_SNAPSHOT };
2585
+ }
2586
+ // A persisted session can outlive the identity that created it — e.g. a new
2587
+ // wallet created over an install that still holds a previous customer's
2588
+ // session. Reusing it makes the backend reject every session-scoped call
2589
+ // (owner mismatch), dead-ending the user. Verify ownership up front against
2590
+ // the signed-in profile and, on a mismatch, discard the stale session.
2591
+ if (sessionFromCache &&
2592
+ !(await this.#isVbaSessionOwnedByCurrentProfile(session))) {
2593
+ this.messenger.call('KycController:clearState');
2594
+ return { ...EMPTY_VBA_ONBOARDING_SNAPSHOT };
2595
+ }
2596
+ const vendorDisclaimersComplete = await this.messenger.call('KycController:hasCompletedVendorDisclaimers');
2597
+ const sessionDisclaimersComplete = await this.messenger.call('KycController:hasCompletedSessionDisclaimers');
2598
+ const snapshot = {
2599
+ sessionExists: true,
2600
+ vendorDisclaimersComplete,
2601
+ sessionDisclaimersComplete,
2602
+ kycStatus: toVbaKycStatus(session.finalStatus),
2603
+ autorampStatus: 'not_ready',
2604
+ };
2605
+ if (snapshot.kycStatus !== 'approved') {
2606
+ return snapshot;
2582
2607
  }
2583
2608
  if (!walletAddress.trim()) {
2584
2609
  throw new Error('walletAddress is required after KYC acceptance.');
2585
2610
  }
2586
2611
  // KYC is approved; the remaining work activates the Money account (register
2587
2612
  // the wallet + ensure an autoramp). Those calls hit the neobank backend and
2588
- // can fail transiently (e.g. an address-list lookup timeout). If they do,
2589
- // keep the user on the "verification in progress" screen so a refresh
2590
- // retries the activation, rather than dropping them onto the recoverable-
2591
- // error screen — the KYC decision itself already succeeded.
2613
+ // can fail transiently (e.g. an address-list lookup timeout). Surface
2614
+ // `retryable_failure` so the host can keep the user on a pending screen
2615
+ // rather than a fatal error — the KYC decision itself already succeeded.
2592
2616
  try {
2593
2617
  const registration = await this.registerMoneyAccountWallet({
2594
2618
  address: walletAddress,
@@ -2613,17 +2637,31 @@ export class RampsController extends BaseController {
2613
2637
  }
2614
2638
  }
2615
2639
  catch {
2616
- return this.#setVbaOnboardingStage(VbaOnboardingStage.KycPending);
2640
+ return { ...snapshot, autorampStatus: 'retryable_failure' };
2617
2641
  }
2618
- return this.#setVbaOnboardingStage(VbaOnboardingStage.Completed);
2642
+ return { ...snapshot, autorampStatus: 'ready' };
2619
2643
  }
2620
- #setVbaOnboardingStage(stage) {
2621
- if (this.state.vbaOnboardingStage !== stage) {
2622
- this.update((state) => {
2623
- state.vbaOnboardingStage = stage;
2624
- });
2644
+ /**
2645
+ * Whether a persisted KYC session belongs to the currently signed-in profile.
2646
+ * A session's `externalUserId` is the canonical profile id captured when the
2647
+ * session was created, so it must match the current profile for the session
2648
+ * to be reused. When the current identity cannot be resolved, err on the side
2649
+ * of keeping the session (return `true`) so a transient profile-read failure
2650
+ * never discards a valid session.
2651
+ *
2652
+ * @param session - The persisted KYC session status to check.
2653
+ * @returns Whether the session is owned by the current profile.
2654
+ */
2655
+ async #isVbaSessionOwnedByCurrentProfile(session) {
2656
+ const profile = await this.messenger.call('AuthenticationController:getSessionProfile');
2657
+ const canonicalId = typeof profile?.canonicalProfileId === 'string' &&
2658
+ profile.canonicalProfileId.length > 0
2659
+ ? profile.canonicalProfileId
2660
+ : profile?.profileId;
2661
+ if (typeof canonicalId !== 'string' || canonicalId.length === 0) {
2662
+ return true;
2625
2663
  }
2626
- return stage;
2664
+ return session.externalUserId === canonicalId;
2627
2665
  }
2628
2666
  /**
2629
2667
  * Removes a local autoramp last-seen cursor by id.
@@ -2852,6 +2890,37 @@ export class RampsController extends BaseController {
2852
2890
  if (!buyUrl) {
2853
2891
  return null;
2854
2892
  }
2893
+ return this.#fetchBuyWidget(buyUrl);
2894
+ }
2895
+ /**
2896
+ * Fetches the widget data for a quote's hosted-flow fallback (see
2897
+ * `getBuyWidgetFallback`), used when an embedded checkout turns the user away.
2898
+ *
2899
+ * @param fallback - The buy-widget fallback attached to the quote.
2900
+ * @param options - Optional request options.
2901
+ * @param options.redirectUrl - Where the hosted flow returns to; set as the
2902
+ * `redirectUrl` query parameter, replacing any existing value.
2903
+ * @returns Promise resolving to the hosted BuyWidget, or null if the fallback has no URL or the response has an empty url.
2904
+ * @throws TypeError if the fallback URL is not a valid URL.
2905
+ * @throws Rethrows errors from the RampsService (e.g. HttpError, network failures) so clients can react to fetch failures.
2906
+ */
2907
+ async getFallbackBuyWidgetData(fallback, options) {
2908
+ if (!fallback?.url) {
2909
+ return null;
2910
+ }
2911
+ const buyUrl = new URL(fallback.url);
2912
+ if (options?.redirectUrl) {
2913
+ buyUrl.searchParams.set('redirectUrl', options.redirectUrl);
2914
+ }
2915
+ return this.#fetchBuyWidget(buyUrl.toString());
2916
+ }
2917
+ /**
2918
+ * Resolves a buy-widget request URL into the provider widget via the RampsService.
2919
+ *
2920
+ * @param buyUrl - The buy-widget request URL.
2921
+ * @returns Promise resolving to the BuyWidget, or null if the response has an empty url.
2922
+ */
2923
+ async #fetchBuyWidget(buyUrl) {
2855
2924
  const buyWidget = await this.messenger.call('RampsService:getBuyWidgetUrl', buyUrl);
2856
2925
  if (!buyWidget?.url) {
2857
2926
  return null;