smartcomply-web-sdk 1.0.68 → 1.0.70

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 (41) hide show
  1. package/dist/esm/flow/DocumentCapture.d.ts +20 -0
  2. package/dist/esm/flow/DocumentCapture.d.ts.map +1 -1
  3. package/dist/esm/flow/DocumentCapture.js +173 -19
  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 +165 -22
  8. package/dist/esm/flow/SmartComplyFlow.js.map +1 -1
  9. package/dist/esm/flow/icons.d.ts +1 -1
  10. package/dist/esm/flow/icons.d.ts.map +1 -1
  11. package/dist/esm/flow/icons.js +1 -0
  12. package/dist/esm/flow/icons.js.map +1 -1
  13. package/dist/esm/modules/liveness/LivenessUI.d.ts.map +1 -1
  14. package/dist/esm/modules/liveness/LivenessUI.js +13 -3
  15. package/dist/esm/modules/liveness/LivenessUI.js.map +1 -1
  16. package/dist/esm/utils/ImageQuality.d.ts +12 -1
  17. package/dist/esm/utils/ImageQuality.d.ts.map +1 -1
  18. package/dist/esm/utils/ImageQuality.js +12 -1
  19. package/dist/esm/utils/ImageQuality.js.map +1 -1
  20. package/dist/flow/DocumentCapture.d.ts +20 -0
  21. package/dist/flow/DocumentCapture.d.ts.map +1 -1
  22. package/dist/flow/DocumentCapture.js +173 -19
  23. package/dist/flow/DocumentCapture.js.map +1 -1
  24. package/dist/flow/SmartComplyFlow.d.ts +12 -0
  25. package/dist/flow/SmartComplyFlow.d.ts.map +1 -1
  26. package/dist/flow/SmartComplyFlow.js +165 -22
  27. package/dist/flow/SmartComplyFlow.js.map +1 -1
  28. package/dist/flow/icons.d.ts +1 -1
  29. package/dist/flow/icons.d.ts.map +1 -1
  30. package/dist/flow/icons.js +1 -0
  31. package/dist/flow/icons.js.map +1 -1
  32. package/dist/modules/liveness/LivenessUI.d.ts.map +1 -1
  33. package/dist/modules/liveness/LivenessUI.js +13 -3
  34. package/dist/modules/liveness/LivenessUI.js.map +1 -1
  35. package/dist/smartcomply.browser.js +393 -44
  36. package/dist/smartcomply.browser.js.map +4 -4
  37. package/dist/utils/ImageQuality.d.ts +12 -1
  38. package/dist/utils/ImageQuality.d.ts.map +1 -1
  39. package/dist/utils/ImageQuality.js +12 -1
  40. package/dist/utils/ImageQuality.js.map +1 -1
  41. 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",
@@ -52,15 +79,17 @@ function resolveIdTypeInfo(typeName, isDocumentFlow) {
52
79
  return { icon: "🏦", desc: "Enter your 11-digit Bank Verification Number (BVN)", requiresBack: false };
53
80
  }
54
81
  // NIN — always 11 digits (covers nin_with_face, NIN slip, etc.). Not every
55
- // physical NIN card in circulation has meaningful back-side content, so
56
- // the back photo is offered but optional rather than required — the
57
- // plain data-verification flow (just typing the 11-digit number, no
58
- // document) never captures a photo at all, so requiresBack stays false there.
82
+ // NIN document in circulation has a back side — a NIN slip typically
83
+ // doesn't, while a NIN card does — so the back photo is offered for
84
+ // either, but optional rather than required: add it if your document
85
+ // has one, skip it if it doesn't. The plain data-verification flow (just
86
+ // typing the 11-digit number, no document) never captures a photo at
87
+ // all, so requiresBack stays false there.
59
88
  if (u.includes("NIN") || u.includes("NATIONAL IDENTITY NUMBER")) {
60
89
  return {
61
90
  icon: "🆔",
62
91
  desc: isDocumentFlow
63
- ? "Capture or upload your NIN card (back optional)"
92
+ ? "Capture or upload your NIN (slip or card) — add the back if your document has one"
64
93
  : "Enter your 11-digit National Identification Number (NIN)",
65
94
  requiresBack: isDocumentFlow ? "optional" : false,
66
95
  };
@@ -276,9 +305,14 @@ class SmartComplyFlow {
276
305
  this.isDestroyed = false;
277
306
  this.headerBackBtn = null;
278
307
  this._keydownHandler = null;
308
+ this._visibilityHandler = null;
309
+ this._hiddenAtMs = null;
310
+ this._popstateHandler = null;
311
+ this._historyGuardActive = false;
279
312
  // Retry safety — max 3 confirmation rejections or liveness failures per session
280
313
  this._confirmRetries = 0;
281
314
  this._livenessFailures = 0;
315
+ this._livenessAttemptId = 0;
282
316
  // IVS result stored for confirm_identity screen
283
317
  this._ivsName = "";
284
318
  this._ivsGender = "";
@@ -337,6 +371,26 @@ class SmartComplyFlow {
337
371
  document.removeEventListener("keydown", this._keydownHandler);
338
372
  this._keydownHandler = null;
339
373
  }
374
+ if (this._visibilityHandler) {
375
+ document.removeEventListener("visibilitychange", this._visibilityHandler);
376
+ this._visibilityHandler = null;
377
+ }
378
+ if (this._popstateHandler) {
379
+ window.removeEventListener("popstate", this._popstateHandler);
380
+ this._popstateHandler = null;
381
+ }
382
+ // Closing via the ✕/Escape/completion (not via the back gesture, which
383
+ // handles its own guard entry above) still leaves our one guard entry
384
+ // sitting on top of the host's history — back out of it so the user
385
+ // isn't left pressing back once "into" a widget that's already gone.
386
+ if (this._historyGuardActive) {
387
+ this._historyGuardActive = false;
388
+ try {
389
+ if (history.state?.scFlowGuard)
390
+ history.back();
391
+ }
392
+ catch { /* ignore — best effort only */ }
393
+ }
340
394
  this.docCapture?.destroy();
341
395
  // Release any pre-warmed camera stream
342
396
  if (this._prewarmedStream) {
@@ -467,6 +521,64 @@ class SmartComplyFlow {
467
521
  }
468
522
  };
469
523
  document.addEventListener("keydown", this._keydownHandler);
524
+ // A tab backgrounded mid-scan (a notification, switching apps to copy a
525
+ // code, a phone call) leaves the camera stream and detection loop in an
526
+ // undefined state on most mobile browsers — rAF throttles/pauses while
527
+ // hidden, and some browsers suspend the camera outright. Rather than let
528
+ // the scan silently continue against stale state and fail confusingly,
529
+ // restart the liveness step cleanly once the tab has been away long
530
+ // enough to plausibly have disrupted an active camera session. A brief
531
+ // glance elsewhere (quick app switch, screen lock tap) is not enough to
532
+ // warrant restarting — only a real return-from-away.
533
+ const REBACKGROUND_RESTART_MS = 3000;
534
+ this._visibilityHandler = () => {
535
+ if (this.isDestroyed)
536
+ return;
537
+ if (document.hidden) {
538
+ this._hiddenAtMs = Date.now();
539
+ return;
540
+ }
541
+ const hiddenAt = this._hiddenAtMs;
542
+ this._hiddenAtMs = null;
543
+ if (hiddenAt === null)
544
+ return;
545
+ const awayMs = Date.now() - hiddenAt;
546
+ if (awayMs >= REBACKGROUND_RESTART_MS && this.currentStep === "liveness") {
547
+ this.showStep("liveness");
548
+ }
549
+ };
550
+ document.addEventListener("visibilitychange", this._visibilityHandler);
551
+ // Intercept the browser/OS back gesture so it steps back within the flow
552
+ // instead of navigating the host page away and abandoning the session
553
+ // mid-verification (camera left open, timers orphaned). One guard entry
554
+ // is pushed — not one per step — to minimize interference with a host
555
+ // page's own router; popstate re-arms it each time so back can be
556
+ // pressed repeatedly. If the current step has no back target, back
557
+ // closes the widget cleanly instead (same as the header ✕).
558
+ this._historyGuardActive = true;
559
+ try {
560
+ history.pushState({ scFlowGuard: true }, "");
561
+ }
562
+ catch {
563
+ this._historyGuardActive = false; // host disallows pushState — degrade silently
564
+ }
565
+ this._popstateHandler = () => {
566
+ if (this.isDestroyed || !this._historyGuardActive)
567
+ return;
568
+ const backAction = this._getBackAction(this.currentStep);
569
+ if (backAction) {
570
+ backAction();
571
+ }
572
+ else {
573
+ this.close();
574
+ return; // don't re-arm — we're closing, let the real back navigation proceed
575
+ }
576
+ try {
577
+ history.pushState({ scFlowGuard: true }, "");
578
+ }
579
+ catch { /* ignore — nothing more we can do */ }
580
+ };
581
+ window.addEventListener("popstate", this._popstateHandler);
470
582
  }
471
583
  async initialize() {
472
584
  this.showStep("loading");
@@ -735,14 +847,16 @@ class SmartComplyFlow {
735
847
  btn.style.cssText += "margin-top:2px;";
736
848
  btn.addEventListener("click", () => this.showStep("country"));
737
849
  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.
850
+ // Plain-language line on what the camera/photo is for and what happens
851
+ // to it — shown before the "Get Started" tap that leads to the first
852
+ // camera permission prompt, so the browser's own prompt isn't the only
853
+ // context the user gets for why a camera is being requested.
740
854
  const trust = document.createElement("div");
741
855
  trust.style.cssText = `
742
- display:flex;align-items:center;gap:6px;
743
- color:${this.theme.textMuted};font-size:10.5px;
856
+ display:flex;align-items:flex-start;gap:6px;text-align:left;
857
+ color:${this.theme.textMuted};font-size:10.5px;line-height:1.5;
744
858
  `;
745
- trust.textContent = "🔒 End-to-end encrypted";
859
+ 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
860
  container.appendChild(trust);
747
861
  }
748
862
  // ── Country Select ──────────────────────────────────────────────
@@ -1856,6 +1970,11 @@ class SmartComplyFlow {
1856
1970
  this._renderRetryLimitReached(container);
1857
1971
  return;
1858
1972
  }
1973
+ // Each render gets its own id so a stale attempt (e.g. abandoned after
1974
+ // the tab was backgrounded and we restarted the scan on return) can't
1975
+ // resolve/reject into the current one — see the visibilitychange
1976
+ // handler in mount(), which re-renders this step on return-to-foreground.
1977
+ const attemptId = ++this._livenessAttemptId;
1859
1978
  const title = this.createStepTitle("Face verification");
1860
1979
  container.appendChild(title);
1861
1980
  const desc = document.createElement("div");
@@ -1904,8 +2023,12 @@ class SmartComplyFlow {
1904
2023
  error: this.theme.error,
1905
2024
  warning: this.theme.warning,
1906
2025
  },
1907
- }, ["BLINK"], this.livenessEntryId || undefined)
2026
+ }, [...constants_1.DEFAULT_CHALLENGE_ACTIONS], this.livenessEntryId || undefined)
1908
2027
  .then((result) => {
2028
+ // A stale attempt (superseded by a restart after the tab was
2029
+ // backgrounded) resolving late must not act on the current screen.
2030
+ if (attemptId !== this._livenessAttemptId)
2031
+ return;
1909
2032
  // W11: After submit, end user is done — show "You're all done" screen.
1910
2033
  // Backend processing continues in background; client receives webhook.
1911
2034
  // Track liveness failures for retry limit (W14).
@@ -1918,8 +2041,18 @@ class SmartComplyFlow {
1918
2041
  this.showStep("done");
1919
2042
  })
1920
2043
  .catch((err) => {
1921
- this._livenessFailures++;
1922
- this.showErrorStep(err.message || "Liveness verification failed. Please try again.");
2044
+ if (attemptId !== this._livenessAttemptId)
2045
+ return;
2046
+ // Only count a genuine verification failure against the retry limit —
2047
+ // a dropped camera, network blip, or upload failure isn't the user's
2048
+ // fault and shouldn't cost them one of their limited attempts. These
2049
+ // are infrastructure-layer errors (thrown/rejected); a real face-
2050
+ // match/quality failure comes back as a resolved result with
2051
+ // status "failed" instead (handled in .then() above).
2052
+ if (!_isInfrastructureError(err)) {
2053
+ this._livenessFailures++;
2054
+ }
2055
+ this.showErrorStep(_userFacingLivenessError(err));
1923
2056
  });
1924
2057
  }
1925
2058
  // ── Error ───────────────────────────────────────────────────────
@@ -2241,6 +2374,24 @@ class SmartComplyFlow {
2241
2374
  .flatMap((ch) => ch.fields || []);
2242
2375
  return allFields.some((f) => isUploadFieldType(f.type));
2243
2376
  }
2377
+ /**
2378
+ * The "go back one step" target for a given step, if that step is
2379
+ * navigable — shared by the header back button and the browser
2380
+ * back-button (popstate) handler in mount() so both agree on the same
2381
+ * step order.
2382
+ */
2383
+ _getBackAction(step) {
2384
+ const countries = Object.keys(this.sdkConfig?.channels || {});
2385
+ const backTargets = {
2386
+ id_type: () => this.showStep(countries.length > 1 ? "country" : "welcome"),
2387
+ country: () => this.showStep("welcome"),
2388
+ id_input: () => this.showStep("id_type"),
2389
+ confirm_identity: () => this.showStep("id_input"),
2390
+ document_capture: () => { this.docCapture?.destroy(); this.showStep("id_type"); },
2391
+ document_confirm: () => { this.docCapture?.destroy(); this.showStep("document_capture"); },
2392
+ };
2393
+ return backTargets[step];
2394
+ }
2244
2395
  updateProgress() {
2245
2396
  const isDocFlow = this.isDocumentFlow();
2246
2397
  const countries = Object.keys(this.sdkConfig?.channels || {});
@@ -2292,15 +2443,7 @@ class SmartComplyFlow {
2292
2443
  }
2293
2444
  // Back button — show for navigable steps only
2294
2445
  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];
2446
+ const backAction = this._getBackAction(this.currentStep);
2304
2447
  if (backAction) {
2305
2448
  this.headerBackBtn.style.display = "flex";
2306
2449
  const newBtn = this.headerBackBtn.cloneNode(true);