@metamask/ramps-controller 24.0.0 → 25.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.
@@ -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 = [
@@ -2500,15 +2525,17 @@ export class RampsController extends BaseController {
2500
2525
  }
2501
2526
  }
2502
2527
  /**
2503
- * Hydrates the Mobile-routable VBA onboarding stage from KYC state and
2504
- * completes wallet and autoramp setup after KYC acceptance.
2528
+ * Refreshes KYC session facts and, when Iron has approved KYC, activates the
2529
+ * Money Account (wallet registration + autoramp). Hosts map the returned
2530
+ * {@link VbaOnboardingSnapshot} onto their own funnel; this method does not
2531
+ * name screens.
2505
2532
  *
2506
2533
  * Overlapping calls share one run so polling cannot trigger duplicate wallet
2507
2534
  * signatures or autoramp creation.
2508
2535
  *
2509
2536
  * @param params - VBA onboarding parameters.
2510
2537
  * @param params.walletAddress - Monad Money Account wallet address.
2511
- * @returns The hydrated onboarding stage.
2538
+ * @returns Independent KYC and autoramp facts for the current customer.
2512
2539
  */
2513
2540
  async hydrateVbaOnboarding({ walletAddress, }) {
2514
2541
  if (this.#vbaOnboardingHydrationPromise) {
@@ -2526,20 +2553,21 @@ export class RampsController extends BaseController {
2526
2553
  }
2527
2554
  }
2528
2555
  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
2556
  // 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
2557
+ // latest-status endpoint: after SumSub the backend endpoint can lag, while
2536
2558
  // the controller state reflects the journey/SDK outcome. Fall back to a
2537
2559
  // backend fetch only when the controller has no session in state (e.g. a
2538
2560
  // reinstall/cleared state resuming an existing customer, or a brand-new user
2539
2561
  // with no session at all).
2540
2562
  let session = null;
2563
+ // Whether `session` came from persisted controller state (as opposed to a
2564
+ // fresh backend fetch, which is always scoped to the current user). Only a
2565
+ // persisted session can belong to a previous identity, so only that path
2566
+ // needs the ownership check below.
2567
+ let sessionFromCache = false;
2541
2568
  try {
2542
2569
  session = this.messenger.call('KycController:refreshSessionStatus');
2570
+ sessionFromCache = true;
2543
2571
  }
2544
2572
  catch {
2545
2573
  try {
@@ -2547,48 +2575,43 @@ export class RampsController extends BaseController {
2547
2575
  }
2548
2576
  catch {
2549
2577
  // 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.
2578
+ // ("KYC session not found"), which surfaces as a rejection here.
2552
2579
  session = null;
2553
2580
  }
2554
2581
  }
2555
2582
  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);
2583
+ return { ...EMPTY_VBA_ONBOARDING_SNAPSHOT };
2584
+ }
2585
+ // A persisted session can outlive the identity that created it — e.g. a new
2586
+ // wallet created over an install that still holds a previous customer's
2587
+ // session. Reusing it makes the backend reject every session-scoped call
2588
+ // (owner mismatch), dead-ending the user. Verify ownership up front against
2589
+ // the signed-in profile and, on a mismatch, discard the stale session.
2590
+ if (sessionFromCache &&
2591
+ !(await this.#isVbaSessionOwnedByCurrentProfile(session))) {
2592
+ this.messenger.call('KycController:clearState');
2593
+ return { ...EMPTY_VBA_ONBOARDING_SNAPSHOT };
2594
+ }
2595
+ const vendorDisclaimersComplete = await this.messenger.call('KycController:hasCompletedVendorDisclaimers');
2596
+ const sessionDisclaimersComplete = await this.messenger.call('KycController:hasCompletedSessionDisclaimers');
2597
+ const snapshot = {
2598
+ sessionExists: true,
2599
+ vendorDisclaimersComplete,
2600
+ sessionDisclaimersComplete,
2601
+ kycStatus: toVbaKycStatus(session.finalStatus),
2602
+ autorampStatus: 'not_ready',
2603
+ };
2604
+ if (snapshot.kycStatus !== 'approved') {
2605
+ return snapshot;
2582
2606
  }
2583
2607
  if (!walletAddress.trim()) {
2584
2608
  throw new Error('walletAddress is required after KYC acceptance.');
2585
2609
  }
2586
2610
  // KYC is approved; the remaining work activates the Money account (register
2587
2611
  // 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.
2612
+ // can fail transiently (e.g. an address-list lookup timeout). Surface
2613
+ // `retryable_failure` so the host can keep the user on a pending screen
2614
+ // rather than a fatal error — the KYC decision itself already succeeded.
2592
2615
  try {
2593
2616
  const registration = await this.registerMoneyAccountWallet({
2594
2617
  address: walletAddress,
@@ -2613,17 +2636,31 @@ export class RampsController extends BaseController {
2613
2636
  }
2614
2637
  }
2615
2638
  catch {
2616
- return this.#setVbaOnboardingStage(VbaOnboardingStage.KycPending);
2639
+ return { ...snapshot, autorampStatus: 'retryable_failure' };
2617
2640
  }
2618
- return this.#setVbaOnboardingStage(VbaOnboardingStage.Completed);
2641
+ return { ...snapshot, autorampStatus: 'ready' };
2619
2642
  }
2620
- #setVbaOnboardingStage(stage) {
2621
- if (this.state.vbaOnboardingStage !== stage) {
2622
- this.update((state) => {
2623
- state.vbaOnboardingStage = stage;
2624
- });
2643
+ /**
2644
+ * Whether a persisted KYC session belongs to the currently signed-in profile.
2645
+ * A session's `externalUserId` is the canonical profile id captured when the
2646
+ * session was created, so it must match the current profile for the session
2647
+ * to be reused. When the current identity cannot be resolved, err on the side
2648
+ * of keeping the session (return `true`) so a transient profile-read failure
2649
+ * never discards a valid session.
2650
+ *
2651
+ * @param session - The persisted KYC session status to check.
2652
+ * @returns Whether the session is owned by the current profile.
2653
+ */
2654
+ async #isVbaSessionOwnedByCurrentProfile(session) {
2655
+ const profile = await this.messenger.call('AuthenticationController:getSessionProfile');
2656
+ const canonicalId = typeof profile?.canonicalProfileId === 'string' &&
2657
+ profile.canonicalProfileId.length > 0
2658
+ ? profile.canonicalProfileId
2659
+ : profile?.profileId;
2660
+ if (typeof canonicalId !== 'string' || canonicalId.length === 0) {
2661
+ return true;
2625
2662
  }
2626
- return stage;
2663
+ return session.externalUserId === canonicalId;
2627
2664
  }
2628
2665
  /**
2629
2666
  * Removes a local autoramp last-seen cursor by id.