smartcomply-web-sdk 1.0.68 → 1.0.69

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 (33) hide show
  1. package/dist/esm/flow/DocumentCapture.d.ts +10 -0
  2. package/dist/esm/flow/DocumentCapture.d.ts.map +1 -1
  3. package/dist/esm/flow/DocumentCapture.js +82 -4
  4. package/dist/esm/flow/DocumentCapture.js.map +1 -1
  5. package/dist/esm/flow/SmartComplyFlow.d.ts +12 -0
  6. package/dist/esm/flow/SmartComplyFlow.d.ts.map +1 -1
  7. package/dist/esm/flow/SmartComplyFlow.js +158 -17
  8. package/dist/esm/flow/SmartComplyFlow.js.map +1 -1
  9. package/dist/esm/modules/liveness/LivenessUI.d.ts.map +1 -1
  10. package/dist/esm/modules/liveness/LivenessUI.js +13 -3
  11. package/dist/esm/modules/liveness/LivenessUI.js.map +1 -1
  12. package/dist/esm/utils/ImageQuality.d.ts +12 -1
  13. package/dist/esm/utils/ImageQuality.d.ts.map +1 -1
  14. package/dist/esm/utils/ImageQuality.js +12 -1
  15. package/dist/esm/utils/ImageQuality.js.map +1 -1
  16. package/dist/flow/DocumentCapture.d.ts +10 -0
  17. package/dist/flow/DocumentCapture.d.ts.map +1 -1
  18. package/dist/flow/DocumentCapture.js +82 -4
  19. package/dist/flow/DocumentCapture.js.map +1 -1
  20. package/dist/flow/SmartComplyFlow.d.ts +12 -0
  21. package/dist/flow/SmartComplyFlow.d.ts.map +1 -1
  22. package/dist/flow/SmartComplyFlow.js +158 -17
  23. package/dist/flow/SmartComplyFlow.js.map +1 -1
  24. package/dist/modules/liveness/LivenessUI.d.ts.map +1 -1
  25. package/dist/modules/liveness/LivenessUI.js +13 -3
  26. package/dist/modules/liveness/LivenessUI.js.map +1 -1
  27. package/dist/smartcomply.browser.js +305 -28
  28. package/dist/smartcomply.browser.js.map +4 -4
  29. package/dist/utils/ImageQuality.d.ts +12 -1
  30. package/dist/utils/ImageQuality.d.ts.map +1 -1
  31. package/dist/utils/ImageQuality.js +12 -1
  32. package/dist/utils/ImageQuality.js.map +1 -1
  33. package/package.json +1 -1
@@ -6,8 +6,35 @@ const theme_1 = require("./theme");
6
6
  const DocumentCapture_1 = require("./DocumentCapture");
7
7
  const device_1 = require("../utils/device");
8
8
  const documentRules_1 = require("../utils/documentRules");
9
+ const constants_1 = require("../utils/constants");
9
10
  const icons_1 = require("./icons");
10
11
  const spinner_1 = require("./spinner");
12
+ // A dropped camera/network/upload error is not the user's fault and must not
13
+ // cost them a retry attempt — only a genuine verification outcome should.
14
+ // Classified by message rather than a typed error class since the errors
15
+ // originate from several independent layers (CameraManager, VideoRecorder,
16
+ // LivenessUploader) that don't share a common error type today.
17
+ const _INFRASTRUCTURE_ERROR_PATTERNS = [
18
+ "camera connection was lost",
19
+ "network error",
20
+ "failed after all retries",
21
+ "camera access is not supported",
22
+ "camera access failed",
23
+ "recording failed unexpectedly",
24
+ ];
25
+ function _isInfrastructureError(err) {
26
+ const msg = String(err?.message || "").toLowerCase();
27
+ return _INFRASTRUCTURE_ERROR_PATTERNS.some((p) => msg.includes(p));
28
+ }
29
+ // Keep this plain-language and reassuring for infrastructure hiccups — the
30
+ // user did nothing wrong, so the copy should say so rather than sound like
31
+ // a rejection.
32
+ function _userFacingLivenessError(err) {
33
+ if (_isInfrastructureError(err)) {
34
+ return "We lost connection during your scan — this wasn't anything you did. Please check your camera and internet connection, then try again.";
35
+ }
36
+ return err?.message || "We couldn't verify your face. Please try again.";
37
+ }
11
38
  const STEP_ORDER = [
12
39
  "loading",
13
40
  "welcome",
@@ -276,9 +303,14 @@ class SmartComplyFlow {
276
303
  this.isDestroyed = false;
277
304
  this.headerBackBtn = null;
278
305
  this._keydownHandler = null;
306
+ this._visibilityHandler = null;
307
+ this._hiddenAtMs = null;
308
+ this._popstateHandler = null;
309
+ this._historyGuardActive = false;
279
310
  // Retry safety — max 3 confirmation rejections or liveness failures per session
280
311
  this._confirmRetries = 0;
281
312
  this._livenessFailures = 0;
313
+ this._livenessAttemptId = 0;
282
314
  // IVS result stored for confirm_identity screen
283
315
  this._ivsName = "";
284
316
  this._ivsGender = "";
@@ -337,6 +369,26 @@ class SmartComplyFlow {
337
369
  document.removeEventListener("keydown", this._keydownHandler);
338
370
  this._keydownHandler = null;
339
371
  }
372
+ if (this._visibilityHandler) {
373
+ document.removeEventListener("visibilitychange", this._visibilityHandler);
374
+ this._visibilityHandler = null;
375
+ }
376
+ if (this._popstateHandler) {
377
+ window.removeEventListener("popstate", this._popstateHandler);
378
+ this._popstateHandler = null;
379
+ }
380
+ // Closing via the ✕/Escape/completion (not via the back gesture, which
381
+ // handles its own guard entry above) still leaves our one guard entry
382
+ // sitting on top of the host's history — back out of it so the user
383
+ // isn't left pressing back once "into" a widget that's already gone.
384
+ if (this._historyGuardActive) {
385
+ this._historyGuardActive = false;
386
+ try {
387
+ if (history.state?.scFlowGuard)
388
+ history.back();
389
+ }
390
+ catch { /* ignore — best effort only */ }
391
+ }
340
392
  this.docCapture?.destroy();
341
393
  // Release any pre-warmed camera stream
342
394
  if (this._prewarmedStream) {
@@ -467,6 +519,64 @@ class SmartComplyFlow {
467
519
  }
468
520
  };
469
521
  document.addEventListener("keydown", this._keydownHandler);
522
+ // A tab backgrounded mid-scan (a notification, switching apps to copy a
523
+ // code, a phone call) leaves the camera stream and detection loop in an
524
+ // undefined state on most mobile browsers — rAF throttles/pauses while
525
+ // hidden, and some browsers suspend the camera outright. Rather than let
526
+ // the scan silently continue against stale state and fail confusingly,
527
+ // restart the liveness step cleanly once the tab has been away long
528
+ // enough to plausibly have disrupted an active camera session. A brief
529
+ // glance elsewhere (quick app switch, screen lock tap) is not enough to
530
+ // warrant restarting — only a real return-from-away.
531
+ const REBACKGROUND_RESTART_MS = 3000;
532
+ this._visibilityHandler = () => {
533
+ if (this.isDestroyed)
534
+ return;
535
+ if (document.hidden) {
536
+ this._hiddenAtMs = Date.now();
537
+ return;
538
+ }
539
+ const hiddenAt = this._hiddenAtMs;
540
+ this._hiddenAtMs = null;
541
+ if (hiddenAt === null)
542
+ return;
543
+ const awayMs = Date.now() - hiddenAt;
544
+ if (awayMs >= REBACKGROUND_RESTART_MS && this.currentStep === "liveness") {
545
+ this.showStep("liveness");
546
+ }
547
+ };
548
+ document.addEventListener("visibilitychange", this._visibilityHandler);
549
+ // Intercept the browser/OS back gesture so it steps back within the flow
550
+ // instead of navigating the host page away and abandoning the session
551
+ // mid-verification (camera left open, timers orphaned). One guard entry
552
+ // is pushed — not one per step — to minimize interference with a host
553
+ // page's own router; popstate re-arms it each time so back can be
554
+ // pressed repeatedly. If the current step has no back target, back
555
+ // closes the widget cleanly instead (same as the header ✕).
556
+ this._historyGuardActive = true;
557
+ try {
558
+ history.pushState({ scFlowGuard: true }, "");
559
+ }
560
+ catch {
561
+ this._historyGuardActive = false; // host disallows pushState — degrade silently
562
+ }
563
+ this._popstateHandler = () => {
564
+ if (this.isDestroyed || !this._historyGuardActive)
565
+ return;
566
+ const backAction = this._getBackAction(this.currentStep);
567
+ if (backAction) {
568
+ backAction();
569
+ }
570
+ else {
571
+ this.close();
572
+ return; // don't re-arm — we're closing, let the real back navigation proceed
573
+ }
574
+ try {
575
+ history.pushState({ scFlowGuard: true }, "");
576
+ }
577
+ catch { /* ignore — nothing more we can do */ }
578
+ };
579
+ window.addEventListener("popstate", this._popstateHandler);
470
580
  }
471
581
  async initialize() {
472
582
  this.showStep("loading");
@@ -735,14 +845,16 @@ class SmartComplyFlow {
735
845
  btn.style.cssText += "margin-top:2px;";
736
846
  btn.addEventListener("click", () => this.showStep("country"));
737
847
  container.appendChild(btn);
738
- // Trust badge — "Powered by Adhere" already lives in the persistent
739
- // modal footer below; repeating it here just cost vertical space.
848
+ // Plain-language line on what the camera/photo is for and what happens
849
+ // to it — shown before the "Get Started" tap that leads to the first
850
+ // camera permission prompt, so the browser's own prompt isn't the only
851
+ // context the user gets for why a camera is being requested.
740
852
  const trust = document.createElement("div");
741
853
  trust.style.cssText = `
742
- display:flex;align-items:center;gap:6px;
743
- color:${this.theme.textMuted};font-size:10.5px;
854
+ display:flex;align-items:flex-start;gap:6px;text-align:left;
855
+ color:${this.theme.textMuted};font-size:10.5px;line-height:1.5;
744
856
  `;
745
- trust.textContent = "🔒 End-to-end encrypted";
857
+ trust.innerHTML = `<span style="flex-shrink:0;">🔒</span><span>Your photo and camera scan are used only to verify it's really you, and are kept encrypted.</span>`;
746
858
  container.appendChild(trust);
747
859
  }
748
860
  // ── Country Select ──────────────────────────────────────────────
@@ -1856,6 +1968,11 @@ class SmartComplyFlow {
1856
1968
  this._renderRetryLimitReached(container);
1857
1969
  return;
1858
1970
  }
1971
+ // Each render gets its own id so a stale attempt (e.g. abandoned after
1972
+ // the tab was backgrounded and we restarted the scan on return) can't
1973
+ // resolve/reject into the current one — see the visibilitychange
1974
+ // handler in mount(), which re-renders this step on return-to-foreground.
1975
+ const attemptId = ++this._livenessAttemptId;
1859
1976
  const title = this.createStepTitle("Face verification");
1860
1977
  container.appendChild(title);
1861
1978
  const desc = document.createElement("div");
@@ -1904,8 +2021,12 @@ class SmartComplyFlow {
1904
2021
  error: this.theme.error,
1905
2022
  warning: this.theme.warning,
1906
2023
  },
1907
- }, ["BLINK"], this.livenessEntryId || undefined)
2024
+ }, [...constants_1.DEFAULT_CHALLENGE_ACTIONS], this.livenessEntryId || undefined)
1908
2025
  .then((result) => {
2026
+ // A stale attempt (superseded by a restart after the tab was
2027
+ // backgrounded) resolving late must not act on the current screen.
2028
+ if (attemptId !== this._livenessAttemptId)
2029
+ return;
1909
2030
  // W11: After submit, end user is done — show "You're all done" screen.
1910
2031
  // Backend processing continues in background; client receives webhook.
1911
2032
  // Track liveness failures for retry limit (W14).
@@ -1918,8 +2039,18 @@ class SmartComplyFlow {
1918
2039
  this.showStep("done");
1919
2040
  })
1920
2041
  .catch((err) => {
1921
- this._livenessFailures++;
1922
- this.showErrorStep(err.message || "Liveness verification failed. Please try again.");
2042
+ if (attemptId !== this._livenessAttemptId)
2043
+ return;
2044
+ // Only count a genuine verification failure against the retry limit —
2045
+ // a dropped camera, network blip, or upload failure isn't the user's
2046
+ // fault and shouldn't cost them one of their limited attempts. These
2047
+ // are infrastructure-layer errors (thrown/rejected); a real face-
2048
+ // match/quality failure comes back as a resolved result with
2049
+ // status "failed" instead (handled in .then() above).
2050
+ if (!_isInfrastructureError(err)) {
2051
+ this._livenessFailures++;
2052
+ }
2053
+ this.showErrorStep(_userFacingLivenessError(err));
1923
2054
  });
1924
2055
  }
1925
2056
  // ── Error ───────────────────────────────────────────────────────
@@ -2241,6 +2372,24 @@ class SmartComplyFlow {
2241
2372
  .flatMap((ch) => ch.fields || []);
2242
2373
  return allFields.some((f) => isUploadFieldType(f.type));
2243
2374
  }
2375
+ /**
2376
+ * The "go back one step" target for a given step, if that step is
2377
+ * navigable — shared by the header back button and the browser
2378
+ * back-button (popstate) handler in mount() so both agree on the same
2379
+ * step order.
2380
+ */
2381
+ _getBackAction(step) {
2382
+ const countries = Object.keys(this.sdkConfig?.channels || {});
2383
+ const backTargets = {
2384
+ id_type: () => this.showStep(countries.length > 1 ? "country" : "welcome"),
2385
+ country: () => this.showStep("welcome"),
2386
+ id_input: () => this.showStep("id_type"),
2387
+ confirm_identity: () => this.showStep("id_input"),
2388
+ document_capture: () => { this.docCapture?.destroy(); this.showStep("id_type"); },
2389
+ document_confirm: () => { this.docCapture?.destroy(); this.showStep("document_capture"); },
2390
+ };
2391
+ return backTargets[step];
2392
+ }
2244
2393
  updateProgress() {
2245
2394
  const isDocFlow = this.isDocumentFlow();
2246
2395
  const countries = Object.keys(this.sdkConfig?.channels || {});
@@ -2292,15 +2441,7 @@ class SmartComplyFlow {
2292
2441
  }
2293
2442
  // Back button — show for navigable steps only
2294
2443
  if (this.headerBackBtn) {
2295
- const backTargets = {
2296
- id_type: () => this.showStep(countries.length > 1 ? "country" : "welcome"),
2297
- country: () => this.showStep("welcome"),
2298
- id_input: () => this.showStep("id_type"),
2299
- confirm_identity: () => this.showStep("id_input"),
2300
- document_capture: () => { this.docCapture?.destroy(); this.showStep("id_type"); },
2301
- document_confirm: () => { this.docCapture?.destroy(); this.showStep("document_capture"); },
2302
- };
2303
- const backAction = backTargets[this.currentStep];
2444
+ const backAction = this._getBackAction(this.currentStep);
2304
2445
  if (backAction) {
2305
2446
  this.headerBackBtn.style.display = "flex";
2306
2447
  const newBtn = this.headerBackBtn.cloneNode(true);