@metamask-previews/kyc-controller 0.0.0-preview-5bb6dba48 → 0.0.0-preview-efd9e28b2

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
@@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  ### Added
11
11
 
12
+ - Add `KycSumSubSdkStatus` for the status strings SumSub SDKs (and launchers) report, distinct from controller sub-flow `KycSumSubStatus`.
13
+ - Add an `abandoned` member to `KycSumSubStatus`, recorded when the applicant closes the SumSub SDK without submitting. It separates that outcome from `failed`, which now means only that the flow could not run or was rejected.
12
14
  - Add `idosRelayBaseUrl` on `KycService` and `KycService.fetchIdosRelayJwks()` to fetch the idOS relay well-known JWKS used to verify the `ukycCapabilityToken` encryption schema. `encryptionDataKey` continues to verify against idOS enclave JWKS via `fetchIdosEnclaveJwks` / `idosEnclaveBaseUrl`. ([#10008](https://github.com/MetaMask/core/pull/10008))
13
15
  - Add session-scoped disclaimer APIs on `KycService` for the idOS / KYC-provider catalog ([#9979](https://github.com/MetaMask/core/pull/9979)):
14
16
  - `fetchSessionDisclaimers({ sessionId })` calls `GET /sessions/{sessionId}/disclaimers`
@@ -53,6 +55,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
53
55
 
54
56
  ### Fixed
55
57
 
58
+ - Treat native SumSub `Pending` and `Approved` statuses as a completed applicant flow, and fall back to the SDK `launch` result status when no status-change callback was delivered. Previously only the launcher-normalized `Completed` string counted, so a successful native SDK session was recorded as failed. ([#10133](https://github.com/MetaMask/core/pull/10133))
59
+ - Also treat the SumSub `ActionCompleted`, `FinallyRejected`, and `TemporarilyDeclined` statuses as a submitted applicant flow. Rejection and temporary-decline decisions poll UKYC instead of being recorded as `abandoned`. ([#10133](https://github.com/MetaMask/core/pull/10133))
60
+ - Stop recording an `error` when the applicant closes the SumSub SDK without submitting. Backing out is a normal outcome, but it was reported the same way as a genuine failure, so consumers surfaced "SumSub verification did not complete" to an applicant who had simply changed their mind. `acceptTermsAndStartSession` still rewinds to the `terms` phase and drops the spent UKYC session, but leaves `error` unset and reports the outcome through `sumsub.status = 'abandoned'`, which survives the rewind. An SDK that could not run is still recorded as `failed` with an `error`.
61
+ - Pause user-keyed `GET /kyc/status` polling for the duration of the SumSub sub-flow, then resume it from `startSumSub` itself when the sub-flow settles. A tick landing while the SDK was on screen could flip the status to `completed` and publish `KycController:statusChanged`, pulling consumers (and their signing prompts) in front of a flow the applicant had not finished navigating. Restarting inside `startSumSub` covers abandonment, SDK failure, and callers that do not run `refreshKycStatus` (the MoonPay post-auth path).
56
62
  - Stop routing `KycService` write endpoints through the query cache. Every `POST` (`createSession`, `checkKycRequired`, `createVendorCustomer`, `submitVendorDisclaimers`, `submitSessionDisclaimers`, `createUkycSession`, `setAuthorizations`, `createJourney`) now issues its request directly instead of via `fetchQuery`. Previously these were modelled as queries, so two overlapping calls sharing a `queryKey` were deduplicated into a single request — a second `createVendorCustomer` while the first was in flight never reached the API — and their responses were retained in the cache and published on the messenger as `cacheUpdated` payloads, which for these endpoints include session tokens and applicant access tokens. Writes are also no longer retried by the service policy, so a failed non-idempotent request cannot create duplicate records server-side. ([#10007](https://github.com/MetaMask/core/pull/10007))
57
63
  - Stop incorrectly prefixing the session client public key onto wrapped `encryptionDataKey` and `ukycCapabilityToken` values from `wrapEncryptionKey`. `data` is now ciphertext+tag only; the server already has the client public key from session creation. ([#10036](https://github.com/MetaMask/core/pull/10036))
58
64
  - Clear leftover MoonPay `sessionToken`, `accessToken`, and Check/Auth frame credentials when `initialize` or `createVendorCustomer` switches to another vendor, so `buildCheckFrameUrl` cannot return a MoonPay URL for a consents-path session. ([#9908](https://github.com/MetaMask/core/pull/9908))
@@ -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 _KycController_instances, _KycController_sumsubLauncher, _KycController_moonPayFrames, _KycController_generation, _KycController_sessionStatusPollIntervalMs, _KycController_pollTimer, _KycController_pollToken, _KycController_userStatusPollIntervalMs, _KycController_userStatusPollTimer, _KycController_userStatusPolling, _KycController_userStatusPollToken, _KycController_localUserSecretStore, _KycController_startConsentsSession, _KycController_recordSessionDisclaimers, _KycController_createSession, _KycController_clearAcceptedTerms, _KycController_continueAfterAuthentication, _KycController_requireTermsReacceptance, _KycController_buildUkycSessionVendorFields, _KycController_createUkycSession, _KycController_fetchAndApplyUserStatus, _KycController_applyUserStatus, _KycController_ensureUserStatusPolling, _KycController_stopUserStatusPolling, _KycController_startSessionStatusPolling, _KycController_pollSessionStatusOnce, _KycController_stopPolling, _KycController_cancelPendingSession, _KycController_updateIfCurrent, _KycController_applyUpdate, _KycController_assertAttestedServerPublicKey, _KycController_fail;
13
+ var _KycController_instances, _KycController_sumsubLauncher, _KycController_moonPayFrames, _KycController_generation, _KycController_sessionStatusPollIntervalMs, _KycController_pollTimer, _KycController_pollToken, _KycController_userStatusPollIntervalMs, _KycController_userStatusPollTimer, _KycController_userStatusPolling, _KycController_userStatusPollToken, _KycController_localUserSecretStore, _KycController_startConsentsSession, _KycController_rewindConsentsFlow, _KycController_recordSessionDisclaimers, _KycController_createSession, _KycController_clearAcceptedTerms, _KycController_continueAfterAuthentication, _KycController_requireTermsReacceptance, _KycController_buildUkycSessionVendorFields, _KycController_createUkycSession, _KycController_fetchAndApplyUserStatus, _KycController_applyUserStatus, _KycController_ensureUserStatusPolling, _KycController_resumeUserStatusAfterSumSub, _KycController_stopUserStatusPolling, _KycController_startSessionStatusPolling, _KycController_pollSessionStatusOnce, _KycController_stopPolling, _KycController_cancelPendingSession, _KycController_updateIfCurrent, _KycController_applyUpdate, _KycController_assertAttestedServerPublicKey, _KycController_fail;
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.KycController = exports.getDefaultKycControllerState = exports.getDefaultKycProviderDisclaimersAccepted = exports.getDefaultKycVendorDisclaimersAccepted = exports.controllerName = void 0;
16
16
  const base_controller_1 = require("@metamask/base-controller");
@@ -35,10 +35,44 @@ const MOCK_JWT_TOKEN = 'mock-jwt-token';
35
35
  // cover the KYC session's expected lifetime — including the provider journey —
36
36
  // rather than a fixed short window, so this is a session-scoped window.
37
37
  const UKYC_CAPABILITY_TOKEN_TTL_MS = 4 * 60 * 60 * 1000;
38
- // The SumSub SDK status that signals the applicant finished the flow
39
- // successfully. Any other resolution (abandonment, failure, or a non-success
40
- // outcome) must not be recorded as `complete`.
41
- const SUMSUB_COMPLETED_STATUS = 'Completed';
38
+ // SumSub statuses that mean the applicant submitted (see `KycSumSubSdkStatus`
39
+ // for what each one reports). `Completed` covers launchers that normalize the
40
+ // platform status before forwarding it. Review decisions (`Approved`,
41
+ // `FinallyRejected`, `TemporarilyDeclined`) are post-submission outcomes: the
42
+ // applicant finished the SDK, so UKYC is polled for the authoritative
43
+ // decision. Pre-submission statuses (`Ready`, `Initial`, `Incomplete`) must
44
+ // not be recorded as a completed verification.
45
+ const SUMSUB_COMPLETED_STATUSES = new Set([
46
+ 'Completed',
47
+ 'Pending',
48
+ 'Approved',
49
+ 'ActionCompleted',
50
+ 'FinallyRejected',
51
+ 'TemporarilyDeclined',
52
+ ]);
53
+ // The only status meaning the SDK could not run, rather than reporting how far
54
+ // the applicant got before closing it.
55
+ const SUMSUB_FAILED_STATUS = 'Failed';
56
+ const SUMSUB_ABANDONED_MESSAGE = 'Identity verification was not finished — accept the terms to try again.';
57
+ /**
58
+ * Checks whether a SumSub status means the applicant submitted the flow.
59
+ *
60
+ * @param status - Status from a launcher callback or launch result.
61
+ * @returns Whether the applicant submitted, including a review decision.
62
+ */
63
+ function isSumSubFlowCompleted(status) {
64
+ return typeof status === 'string' && SUMSUB_COMPLETED_STATUSES.has(status);
65
+ }
66
+ /**
67
+ * Checks whether the SDK failed to run, as opposed to the applicant closing it
68
+ * early. Only the former is worth reporting as an error.
69
+ *
70
+ * @param result - The result the launcher resolved with.
71
+ * @returns Whether the SDK failed to run.
72
+ */
73
+ function isSumSubLaunchFailure(result) {
74
+ return (result.status === SUMSUB_FAILED_STATUS || typeof result.error === 'string');
75
+ }
42
76
  // Phases that represent an active vendor-session flow (tokens issued and/or
43
77
  // Check/Auth frames in progress). A repeat `initialize` while in one of these
44
78
  // must not restart the session and disrupt the in-flight flow.
@@ -905,145 +939,169 @@ class KycController extends base_controller_1.BaseController {
905
939
  async startSumSub(params) {
906
940
  // A new sub-flow supersedes any polling still running from a prior run.
907
941
  __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_stopPolling).call(this);
908
- if (!__classPrivateFieldGet(this, _KycController_sumsubLauncher, "f").isAvailable()) {
909
- const error = 'SumSub SDK is not available in this runtime.';
910
- __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_applyUpdate).call(this, (state) => {
911
- state.sumsub.status = 'failed';
912
- state.sumsub.result = { error };
913
- });
914
- throw new Error(error);
915
- }
942
+ // Paused for the whole sub-flow: a tick landing while the SDK is on screen
943
+ // publishes `statusChanged`, pulling consumers (and their signing prompts)
944
+ // in front of a flow the applicant has not finished. Resumed in `finally`
945
+ // so abandonment, SDK failure, and callers that do not run
946
+ // `refreshKycStatus` (MoonPay post-auth) still restore the loop.
947
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_stopUserStatusPolling).call(this);
916
948
  // Capture the flow generation so each async step can detect a `reset()`
917
949
  // that lands mid-flight and avoid writing stale sub-flow state (or, worse,
918
950
  // presenting the SDK) on a controller that is now idle.
919
951
  const generation = __classPrivateFieldGet(this, _KycController_generation, "f");
920
952
  try {
921
- if (!this.state.sumsub.sessionId) {
953
+ if (!__classPrivateFieldGet(this, _KycController_sumsubLauncher, "f").isAvailable()) {
954
+ const error = 'SumSub SDK is not available in this runtime.';
922
955
  __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_applyUpdate).call(this, (state) => {
923
- state.sumsub.status = 'creatingSession';
924
- state.sumsub.result = null;
925
- state.sumsub.sessionStatus = null;
956
+ state.sumsub.status = 'failed';
957
+ state.sumsub.result = { error };
958
+ });
959
+ throw new Error(error);
960
+ }
961
+ try {
962
+ if (!this.state.sumsub.sessionId) {
963
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_applyUpdate).call(this, (state) => {
964
+ state.sumsub.status = 'creatingSession';
965
+ state.sumsub.result = null;
966
+ state.sumsub.sessionStatus = null;
967
+ });
968
+ const created = await __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_createUkycSession).call(this, generation);
969
+ if (!created) {
970
+ return {};
971
+ }
972
+ // A user who already finished the journey can return to a session the
973
+ // relay has already approved (`kycStatus`) while the vendor is still
974
+ // finalizing its own decision (`finalStatus`). There is nothing left to
975
+ // verify, so stop here and surface a message rather than launching the
976
+ // SDK again.
977
+ if (created.vendorProcessing) {
978
+ return {
979
+ kycStatus: created.kycStatus,
980
+ finalStatus: created.finalStatus,
981
+ };
982
+ }
983
+ }
984
+ // Empty string is a valid "no id to poll" session id used by tests and
985
+ // must not be coalesced away as missing.
986
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
987
+ const sessionId = this.state.sumsub.sessionId || '';
988
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_updateIfCurrent).call(this, generation, (state) => {
989
+ state.sumsub.status = 'fetchingToken';
990
+ state.sumsub.sessionId = sessionId;
926
991
  });
927
- const created = await __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_createUkycSession).call(this, generation);
928
- if (!created) {
992
+ const { applicantAccessToken } = await this.messenger.call('KycService:createJourney', sessionId);
993
+ // A reset() may have landed while the session/token was being prepared.
994
+ // Gate the `launching` write and the decision to open the SDK behind a
995
+ // single generation check: `#updateIfCurrent` only writes when still
996
+ // current and reports whether it did. Since there is no `await` between
997
+ // this check and `launch` below, a successful result guarantees the SDK
998
+ // is never presented on a flow that a concurrent reset() returned to idle.
999
+ const stillCurrent = __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_updateIfCurrent).call(this, generation, (state) => {
1000
+ state.sumsub.status = 'launching';
1001
+ state.sumsub.applicantAccessToken = applicantAccessToken;
1002
+ });
1003
+ if (!stillCurrent) {
929
1004
  return {};
930
1005
  }
931
- // A user who already finished the journey can return to a session the
932
- // relay has already approved (`kycStatus`) while the vendor is still
933
- // finalizing its own decision (`finalStatus`). There is nothing left to
934
- // verify, so stop here and surface a message rather than launching the
935
- // SDK again.
936
- if (created.vendorProcessing) {
937
- return {
938
- kycStatus: created.kycStatus,
939
- finalStatus: created.finalStatus,
940
- };
1006
+ // Track whether the SDK ever reported a successful completion. A resolved
1007
+ // `launch` alone does not imply success the applicant may have
1008
+ // abandoned the flow or the SDK may have reported a non-success outcome.
1009
+ let reachedCompletion = false;
1010
+ const result = await __classPrivateFieldGet(this, _KycController_sumsubLauncher, "f").launch({
1011
+ applicantAccessToken,
1012
+ onTokenExpiration: async () => {
1013
+ // A reset() may have superseded this flow while the SDK stayed open.
1014
+ // Refuse to refresh against the now-stale UKYC session rather than
1015
+ // silently keeping an orphaned SDK alive.
1016
+ if (__classPrivateFieldGet(this, _KycController_generation, "f") !== generation) {
1017
+ throw new Error('KYC flow was reset; SumSub session is no longer active.');
1018
+ }
1019
+ const refreshed = await this.messenger.call('KycService:createJourney', sessionId);
1020
+ return refreshed.applicantAccessToken;
1021
+ },
1022
+ onStatusChange: (_prev, next) => {
1023
+ if (isSumSubFlowCompleted(next)) {
1024
+ reachedCompletion = true;
1025
+ }
1026
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_updateIfCurrent).call(this, generation, (state) => {
1027
+ state.sumsub.status = isSumSubFlowCompleted(next)
1028
+ ? 'complete'
1029
+ : 'inProgress';
1030
+ });
1031
+ },
1032
+ locale: params?.locale ?? 'en',
1033
+ debug: params?.debug ?? false,
1034
+ });
1035
+ // Some native SDKs resolve with their final status without first
1036
+ // delivering the corresponding state-change callback.
1037
+ reachedCompletion || (reachedCompletion = isSumSubFlowCompleted(result.status));
1038
+ // A resolved `launch` alone is not the final outcome: only a submission
1039
+ // is worth polling for a decision. Without one, the SDK status is the
1040
+ // only way to tell a failure from an applicant who closed the SDK early
1041
+ // — the UKYC session leaves its initial state as soon as the journey is
1042
+ // created, so it cannot stand in for "the applicant finished".
1043
+ let settledStatus = 'abandoned';
1044
+ if (reachedCompletion) {
1045
+ settledStatus = 'polling';
941
1046
  }
1047
+ else if (isSumSubLaunchFailure(result)) {
1048
+ settledStatus = 'failed';
1049
+ }
1050
+ const applied = __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_updateIfCurrent).call(this, generation, (state) => {
1051
+ state.sumsub.status = settledStatus;
1052
+ state.sumsub.result = result;
1053
+ });
1054
+ // Once the SDK completes, the authoritative verification decision comes
1055
+ // from the UKYC backend, not the SDK result. Poll the session status
1056
+ // until it reaches a terminal decision. Guard on `applied` so a `reset()`
1057
+ // that landed during `launch` cannot start polling on an idle flow.
1058
+ if (applied && reachedCompletion) {
1059
+ if (sessionId) {
1060
+ await __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_startSessionStatusPolling).call(this, sessionId);
1061
+ }
1062
+ else {
1063
+ // No session id to poll against; fall back to treating the SDK
1064
+ // completion as the final outcome.
1065
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_updateIfCurrent).call(this, generation, (state) => {
1066
+ state.sumsub.status = 'complete';
1067
+ });
1068
+ }
1069
+ }
1070
+ return result;
942
1071
  }
943
- // Empty string is a valid "no id to poll" session id used by tests and
944
- // must not be coalesced away as missing.
945
- // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
946
- const sessionId = this.state.sumsub.sessionId || '';
947
- __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_updateIfCurrent).call(this, generation, (state) => {
948
- state.sumsub.status = 'fetchingToken';
949
- state.sumsub.sessionId = sessionId;
950
- });
951
- const { applicantAccessToken } = await this.messenger.call('KycService:createJourney', sessionId);
952
- // A reset() may have landed while the session/token was being prepared.
953
- // Gate the `launching` write and the decision to open the SDK behind a
954
- // single generation check: `#updateIfCurrent` only writes when still
955
- // current and reports whether it did. Since there is no `await` between
956
- // this check and `launch` below, a successful result guarantees the SDK
957
- // is never presented on a flow that a concurrent reset() returned to idle.
958
- const stillCurrent = __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_updateIfCurrent).call(this, generation, (state) => {
959
- state.sumsub.status = 'launching';
960
- state.sumsub.applicantAccessToken = applicantAccessToken;
961
- });
962
- if (!stillCurrent) {
963
- return {};
964
- }
965
- // Track whether the SDK ever reported a successful completion. A resolved
966
- // `launch` alone does not imply success — the applicant may have
967
- // abandoned the flow or the SDK may have reported a non-success outcome.
968
- let reachedCompletion = false;
969
- const result = await __classPrivateFieldGet(this, _KycController_sumsubLauncher, "f").launch({
970
- applicantAccessToken,
971
- onTokenExpiration: async () => {
972
- // A reset() may have superseded this flow while the SDK stayed open.
973
- // Refuse to refresh against the now-stale UKYC session rather than
974
- // silently keeping an orphaned SDK alive.
1072
+ catch (error) {
1073
+ // Applicant already finished KYC treat as completed for Money toast.
1074
+ if (isSessionAlreadyCompletedError(error)) {
1075
+ // A reset() may have landed while `launch` was in flight; forcing
1076
+ // `completed` (and publishing `statusChanged`) on an idle controller
1077
+ // would resurrect a flow the consumer already tore down.
975
1078
  if (__classPrivateFieldGet(this, _KycController_generation, "f") !== generation) {
976
- throw new Error('KYC flow was reset; SumSub session is no longer active.');
977
- }
978
- const refreshed = await this.messenger.call('KycService:createJourney', sessionId);
979
- return refreshed.applicantAccessToken;
980
- },
981
- onStatusChange: (_prev, next) => {
982
- if (next === SUMSUB_COMPLETED_STATUS) {
983
- reachedCompletion = true;
1079
+ return { alreadyCompleted: true };
984
1080
  }
985
- __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_updateIfCurrent).call(this, generation, (state) => {
986
- state.sumsub.status =
987
- next === SUMSUB_COMPLETED_STATUS ? 'complete' : 'inProgress';
1081
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_applyUserStatus).call(this, {
1082
+ status: 'completed',
1083
+ sumsubSessionId: null,
1084
+ errorCode: null,
988
1085
  });
989
- },
990
- locale: params?.locale ?? 'en',
991
- debug: params?.debug ?? false,
992
- });
993
- // A resolved `launch` alone is not the final outcome: only a SDK-reported
994
- // completion is worth polling for a verification decision. Anything else
995
- // (abandonment, non-success) is `failed` and must not be polled.
996
- const applied = __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_updateIfCurrent).call(this, generation, (state) => {
997
- state.sumsub.status = reachedCompletion ? 'polling' : 'failed';
998
- state.sumsub.result = result;
999
- });
1000
- // Once the SDK completes, the authoritative verification decision comes
1001
- // from the UKYC backend, not the SDK result. Poll the session status
1002
- // until it reaches a terminal decision. Guard on `applied` so a `reset()`
1003
- // that landed during `launch` cannot start polling on an idle flow.
1004
- if (applied && reachedCompletion) {
1005
- if (sessionId) {
1006
- await __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_startSessionStatusPolling).call(this, sessionId);
1007
- }
1008
- else {
1009
- // No session id to poll against; fall back to treating the SDK
1010
- // completion as the final outcome.
1011
1086
  __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_updateIfCurrent).call(this, generation, (state) => {
1012
1087
  state.sumsub.status = 'complete';
1088
+ state.sumsub.result = { alreadyCompleted: true };
1089
+ state.statusMessage = 'KYC already completed.';
1090
+ state.phase = 'done';
1091
+ state.error = null;
1013
1092
  });
1014
- }
1015
- }
1016
- return result;
1017
- }
1018
- catch (error) {
1019
- // Applicant already finished KYC — treat as completed for Money toast.
1020
- if (isSessionAlreadyCompletedError(error)) {
1021
- // A reset() may have landed while `launch` was in flight; forcing
1022
- // `completed` (and publishing `statusChanged`) on an idle controller
1023
- // would resurrect a flow the consumer already tore down.
1024
- if (__classPrivateFieldGet(this, _KycController_generation, "f") !== generation) {
1025
1093
  return { alreadyCompleted: true };
1026
1094
  }
1027
- __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_applyUserStatus).call(this, {
1028
- status: 'completed',
1029
- sumsubSessionId: null,
1030
- errorCode: null,
1031
- });
1095
+ const result = { error: String(error) };
1032
1096
  __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_updateIfCurrent).call(this, generation, (state) => {
1033
- state.sumsub.status = 'complete';
1034
- state.sumsub.result = { alreadyCompleted: true };
1035
- state.statusMessage = 'KYC already completed.';
1036
- state.phase = 'done';
1037
- state.error = null;
1097
+ state.sumsub.status = 'failed';
1098
+ state.sumsub.result = result;
1038
1099
  });
1039
- return { alreadyCompleted: true };
1100
+ return result;
1040
1101
  }
1041
- const result = { error: String(error) };
1042
- __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_updateIfCurrent).call(this, generation, (state) => {
1043
- state.sumsub.status = 'failed';
1044
- state.sumsub.result = result;
1045
- });
1046
- return result;
1102
+ }
1103
+ finally {
1104
+ await __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_resumeUserStatusAfterSumSub).call(this, generation);
1047
1105
  }
1048
1106
  }
1049
1107
  /**
@@ -1214,18 +1272,26 @@ async function _KycController_startConsentsSession(consents) {
1214
1272
  if (__classPrivateFieldGet(this, _KycController_generation, "f") !== generation) {
1215
1273
  return;
1216
1274
  }
1217
- // `startSumSub` records `sumsub.status = 'failed'` for thrown steps,
1218
- // an SDK close without Completed, *and* a terminal UKYC rejection
1219
- // after the SDK reported Completed. Only rewind when there is no
1220
- // session-status decision yet (abandonment / thrown step). A
1221
- // Completed-then-rejected poll writes `sessionStatus` and is a
1222
- // finished flow: refresh user status and land on `done`.
1275
+ // The applicant closed the SDK without submitting. Nothing failed, so
1276
+ // rewind with `error` unset and let consumers offer a retry.
1277
+ if (this.state.sumsub.status === 'abandoned') {
1278
+ await __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_rewindConsentsFlow).call(this, {
1279
+ error: null,
1280
+ statusMessage: SUMSUB_ABANDONED_MESSAGE,
1281
+ keepSumSubStatus: 'abandoned',
1282
+ });
1283
+ return;
1284
+ }
1285
+ // `startSumSub` records `failed` for thrown steps, an SDK that could not
1286
+ // run, *and* a terminal UKYC rejection after a submission. Only the first
1287
+ // two rewind. A rejection writes `sessionStatus` and is a finished flow:
1288
+ // refresh user status and land on `done`.
1223
1289
  if (this.state.sumsub.status === 'failed' &&
1224
1290
  this.state.sumsub.sessionStatus === null) {
1225
1291
  const sumsubError = sumsubResult?.error;
1226
1292
  throw new Error(typeof sumsubError === 'string'
1227
1293
  ? sumsubError
1228
- : 'SumSub verification did not complete.');
1294
+ : 'SumSub verification could not run.');
1229
1295
  }
1230
1296
  // After SumSub, refresh user-keyed status for the Money toast and start
1231
1297
  // polling while still pending. Soft-fail: toast refresh must not rewind
@@ -1266,20 +1332,41 @@ async function _KycController_startConsentsSession(consents) {
1266
1332
  if (__classPrivateFieldGet(this, _KycController_generation, "f") !== generation) {
1267
1333
  return;
1268
1334
  }
1269
- __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_applyUpdate).call(this, (state) => {
1270
- __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_clearAcceptedTerms).call(this, state);
1271
- state.activeProduct = null;
1272
- state.sessionDisclaimers = null;
1273
- // Session create ran before recording disclaimers. Drop the leftover
1274
- // UKYC session so a later `startSumSub` cannot skip consent recording.
1275
- state.sumsub = { ...getDefaultKycControllerState().sumsub };
1276
- state.error = `Consents session failed: ${String(error)}`;
1277
- state.statusMessage =
1278
- 'Consent / verification failed — accept the terms to try again.';
1279
- state.phase = 'terms';
1335
+ await __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_rewindConsentsFlow).call(this, {
1336
+ error: `Consents session failed: ${String(error)}`,
1337
+ statusMessage: 'Consent / verification failed — accept the terms to try again.',
1280
1338
  });
1281
- await this.loadDisclaimers();
1282
1339
  }
1340
+ }, _KycController_rewindConsentsFlow =
1341
+ /**
1342
+ * Returns the consents path to the terms phase after a SumSub sub-flow that
1343
+ * produced no verification decision, and reloads the disclaimers the next
1344
+ * attempt has to re-accept.
1345
+ *
1346
+ * @param options - Rewind options.
1347
+ * @param options.error - Message for `error`, or `null` when the rewind is a
1348
+ * normal outcome rather than a failure.
1349
+ * @param options.statusMessage - Message for `statusMessage`.
1350
+ * @param options.keepSumSubStatus - Sub-flow status to survive the rewind,
1351
+ * for an outcome consumers still need once the call resolves. Defaults to the
1352
+ * reset `idle`.
1353
+ */
1354
+ async function _KycController_rewindConsentsFlow({ error, statusMessage, keepSumSubStatus, }) {
1355
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_applyUpdate).call(this, (state) => {
1356
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_clearAcceptedTerms).call(this, state);
1357
+ state.activeProduct = null;
1358
+ state.sessionDisclaimers = null;
1359
+ // Session create ran before recording disclaimers. Drop the leftover
1360
+ // UKYC session so a later `startSumSub` cannot skip consent recording.
1361
+ state.sumsub = { ...getDefaultKycControllerState().sumsub };
1362
+ if (keepSumSubStatus) {
1363
+ state.sumsub.status = keepSumSubStatus;
1364
+ }
1365
+ state.error = error;
1366
+ state.statusMessage = statusMessage;
1367
+ state.phase = 'terms';
1368
+ });
1369
+ await this.loadDisclaimers();
1283
1370
  }, _KycController_recordSessionDisclaimers =
1284
1371
  /**
1285
1372
  * Fetches the session-scoped disclaimer catalog and records consents
@@ -1645,6 +1732,28 @@ async function _KycController_fetchAndApplyUserStatus() {
1645
1732
  tick();
1646
1733
  }, __classPrivateFieldGet(this, _KycController_userStatusPollIntervalMs, "f")), "f");
1647
1734
  __classPrivateFieldGet(this, _KycController_userStatusPollTimer, "f").unref?.();
1735
+ }, _KycController_resumeUserStatusAfterSumSub =
1736
+ /**
1737
+ * Restores user-keyed `GET /kyc/status` polling after {@link startSumSub}
1738
+ * settles. Skipped when a `reset()` superseded the sub-flow.
1739
+ *
1740
+ * @param generation - Flow generation captured when the sub-flow started.
1741
+ */
1742
+ async function _KycController_resumeUserStatusAfterSumSub(generation) {
1743
+ if (__classPrivateFieldGet(this, _KycController_generation, "f") !== generation) {
1744
+ return;
1745
+ }
1746
+ // `session_not_in_valid_state` already wrote `completed`. A follow-up
1747
+ // `GET /kyc/status` can still read a stale `pending` and must not undo it.
1748
+ if (this.state.userStatus === 'completed') {
1749
+ return;
1750
+ }
1751
+ try {
1752
+ await this.refreshKycStatus();
1753
+ }
1754
+ catch (error) {
1755
+ (0, logger_js_1.controllerLog)('KYC status refresh failed:', error);
1756
+ }
1648
1757
  }, _KycController_stopUserStatusPolling = function _KycController_stopUserStatusPolling() {
1649
1758
  __classPrivateFieldSet(this, _KycController_userStatusPollToken, __classPrivateFieldGet(this, _KycController_userStatusPollToken, "f") + 1, "f");
1650
1759
  __classPrivateFieldSet(this, _KycController_userStatusPolling, false, "f");