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.
- package/dist/esm/flow/DocumentCapture.d.ts +20 -0
- package/dist/esm/flow/DocumentCapture.d.ts.map +1 -1
- package/dist/esm/flow/DocumentCapture.js +173 -19
- package/dist/esm/flow/DocumentCapture.js.map +1 -1
- package/dist/esm/flow/SmartComplyFlow.d.ts +12 -0
- package/dist/esm/flow/SmartComplyFlow.d.ts.map +1 -1
- package/dist/esm/flow/SmartComplyFlow.js +165 -22
- package/dist/esm/flow/SmartComplyFlow.js.map +1 -1
- package/dist/esm/flow/icons.d.ts +1 -1
- package/dist/esm/flow/icons.d.ts.map +1 -1
- package/dist/esm/flow/icons.js +1 -0
- package/dist/esm/flow/icons.js.map +1 -1
- package/dist/esm/modules/liveness/LivenessUI.d.ts.map +1 -1
- package/dist/esm/modules/liveness/LivenessUI.js +13 -3
- package/dist/esm/modules/liveness/LivenessUI.js.map +1 -1
- package/dist/esm/utils/ImageQuality.d.ts +12 -1
- package/dist/esm/utils/ImageQuality.d.ts.map +1 -1
- package/dist/esm/utils/ImageQuality.js +12 -1
- package/dist/esm/utils/ImageQuality.js.map +1 -1
- package/dist/flow/DocumentCapture.d.ts +20 -0
- package/dist/flow/DocumentCapture.d.ts.map +1 -1
- package/dist/flow/DocumentCapture.js +173 -19
- package/dist/flow/DocumentCapture.js.map +1 -1
- package/dist/flow/SmartComplyFlow.d.ts +12 -0
- package/dist/flow/SmartComplyFlow.d.ts.map +1 -1
- package/dist/flow/SmartComplyFlow.js +165 -22
- package/dist/flow/SmartComplyFlow.js.map +1 -1
- package/dist/flow/icons.d.ts +1 -1
- package/dist/flow/icons.d.ts.map +1 -1
- package/dist/flow/icons.js +1 -0
- package/dist/flow/icons.js.map +1 -1
- package/dist/modules/liveness/LivenessUI.d.ts.map +1 -1
- package/dist/modules/liveness/LivenessUI.js +13 -3
- package/dist/modules/liveness/LivenessUI.js.map +1 -1
- package/dist/smartcomply.browser.js +393 -44
- package/dist/smartcomply.browser.js.map +4 -4
- package/dist/utils/ImageQuality.d.ts +12 -1
- package/dist/utils/ImageQuality.d.ts.map +1 -1
- package/dist/utils/ImageQuality.js +12 -1
- package/dist/utils/ImageQuality.js.map +1 -1
- package/package.json +1 -1
|
@@ -3,8 +3,35 @@ import { getTheme, TYPE, RADIUS } from "./theme";
|
|
|
3
3
|
import { DocumentCapture } from "./DocumentCapture";
|
|
4
4
|
import { collectDeviceMetadata } from "../utils/device";
|
|
5
5
|
import { requiresBackSide } from "../utils/documentRules";
|
|
6
|
+
import { DEFAULT_CHALLENGE_ACTIONS } from "../utils/constants";
|
|
6
7
|
import { icon } from "./icons";
|
|
7
8
|
import { renderSpinner, renderInlineSpinner } from "./spinner";
|
|
9
|
+
// A dropped camera/network/upload error is not the user's fault and must not
|
|
10
|
+
// cost them a retry attempt — only a genuine verification outcome should.
|
|
11
|
+
// Classified by message rather than a typed error class since the errors
|
|
12
|
+
// originate from several independent layers (CameraManager, VideoRecorder,
|
|
13
|
+
// LivenessUploader) that don't share a common error type today.
|
|
14
|
+
const _INFRASTRUCTURE_ERROR_PATTERNS = [
|
|
15
|
+
"camera connection was lost",
|
|
16
|
+
"network error",
|
|
17
|
+
"failed after all retries",
|
|
18
|
+
"camera access is not supported",
|
|
19
|
+
"camera access failed",
|
|
20
|
+
"recording failed unexpectedly",
|
|
21
|
+
];
|
|
22
|
+
function _isInfrastructureError(err) {
|
|
23
|
+
const msg = String(err?.message || "").toLowerCase();
|
|
24
|
+
return _INFRASTRUCTURE_ERROR_PATTERNS.some((p) => msg.includes(p));
|
|
25
|
+
}
|
|
26
|
+
// Keep this plain-language and reassuring for infrastructure hiccups — the
|
|
27
|
+
// user did nothing wrong, so the copy should say so rather than sound like
|
|
28
|
+
// a rejection.
|
|
29
|
+
function _userFacingLivenessError(err) {
|
|
30
|
+
if (_isInfrastructureError(err)) {
|
|
31
|
+
return "We lost connection during your scan — this wasn't anything you did. Please check your camera and internet connection, then try again.";
|
|
32
|
+
}
|
|
33
|
+
return err?.message || "We couldn't verify your face. Please try again.";
|
|
34
|
+
}
|
|
8
35
|
const STEP_ORDER = [
|
|
9
36
|
"loading",
|
|
10
37
|
"welcome",
|
|
@@ -49,15 +76,17 @@ function resolveIdTypeInfo(typeName, isDocumentFlow) {
|
|
|
49
76
|
return { icon: "🏦", desc: "Enter your 11-digit Bank Verification Number (BVN)", requiresBack: false };
|
|
50
77
|
}
|
|
51
78
|
// NIN — always 11 digits (covers nin_with_face, NIN slip, etc.). Not every
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
79
|
+
// NIN document in circulation has a back side — a NIN slip typically
|
|
80
|
+
// doesn't, while a NIN card does — so the back photo is offered for
|
|
81
|
+
// either, but optional rather than required: add it if your document
|
|
82
|
+
// has one, skip it if it doesn't. The plain data-verification flow (just
|
|
83
|
+
// typing the 11-digit number, no document) never captures a photo at
|
|
84
|
+
// all, so requiresBack stays false there.
|
|
56
85
|
if (u.includes("NIN") || u.includes("NATIONAL IDENTITY NUMBER")) {
|
|
57
86
|
return {
|
|
58
87
|
icon: "🆔",
|
|
59
88
|
desc: isDocumentFlow
|
|
60
|
-
? "Capture or upload your NIN card
|
|
89
|
+
? "Capture or upload your NIN (slip or card) — add the back if your document has one"
|
|
61
90
|
: "Enter your 11-digit National Identification Number (NIN)",
|
|
62
91
|
requiresBack: isDocumentFlow ? "optional" : false,
|
|
63
92
|
};
|
|
@@ -273,9 +302,14 @@ export class SmartComplyFlow {
|
|
|
273
302
|
this.isDestroyed = false;
|
|
274
303
|
this.headerBackBtn = null;
|
|
275
304
|
this._keydownHandler = null;
|
|
305
|
+
this._visibilityHandler = null;
|
|
306
|
+
this._hiddenAtMs = null;
|
|
307
|
+
this._popstateHandler = null;
|
|
308
|
+
this._historyGuardActive = false;
|
|
276
309
|
// Retry safety — max 3 confirmation rejections or liveness failures per session
|
|
277
310
|
this._confirmRetries = 0;
|
|
278
311
|
this._livenessFailures = 0;
|
|
312
|
+
this._livenessAttemptId = 0;
|
|
279
313
|
// IVS result stored for confirm_identity screen
|
|
280
314
|
this._ivsName = "";
|
|
281
315
|
this._ivsGender = "";
|
|
@@ -334,6 +368,26 @@ export class SmartComplyFlow {
|
|
|
334
368
|
document.removeEventListener("keydown", this._keydownHandler);
|
|
335
369
|
this._keydownHandler = null;
|
|
336
370
|
}
|
|
371
|
+
if (this._visibilityHandler) {
|
|
372
|
+
document.removeEventListener("visibilitychange", this._visibilityHandler);
|
|
373
|
+
this._visibilityHandler = null;
|
|
374
|
+
}
|
|
375
|
+
if (this._popstateHandler) {
|
|
376
|
+
window.removeEventListener("popstate", this._popstateHandler);
|
|
377
|
+
this._popstateHandler = null;
|
|
378
|
+
}
|
|
379
|
+
// Closing via the ✕/Escape/completion (not via the back gesture, which
|
|
380
|
+
// handles its own guard entry above) still leaves our one guard entry
|
|
381
|
+
// sitting on top of the host's history — back out of it so the user
|
|
382
|
+
// isn't left pressing back once "into" a widget that's already gone.
|
|
383
|
+
if (this._historyGuardActive) {
|
|
384
|
+
this._historyGuardActive = false;
|
|
385
|
+
try {
|
|
386
|
+
if (history.state?.scFlowGuard)
|
|
387
|
+
history.back();
|
|
388
|
+
}
|
|
389
|
+
catch { /* ignore — best effort only */ }
|
|
390
|
+
}
|
|
337
391
|
this.docCapture?.destroy();
|
|
338
392
|
// Release any pre-warmed camera stream
|
|
339
393
|
if (this._prewarmedStream) {
|
|
@@ -464,6 +518,64 @@ export class SmartComplyFlow {
|
|
|
464
518
|
}
|
|
465
519
|
};
|
|
466
520
|
document.addEventListener("keydown", this._keydownHandler);
|
|
521
|
+
// A tab backgrounded mid-scan (a notification, switching apps to copy a
|
|
522
|
+
// code, a phone call) leaves the camera stream and detection loop in an
|
|
523
|
+
// undefined state on most mobile browsers — rAF throttles/pauses while
|
|
524
|
+
// hidden, and some browsers suspend the camera outright. Rather than let
|
|
525
|
+
// the scan silently continue against stale state and fail confusingly,
|
|
526
|
+
// restart the liveness step cleanly once the tab has been away long
|
|
527
|
+
// enough to plausibly have disrupted an active camera session. A brief
|
|
528
|
+
// glance elsewhere (quick app switch, screen lock tap) is not enough to
|
|
529
|
+
// warrant restarting — only a real return-from-away.
|
|
530
|
+
const REBACKGROUND_RESTART_MS = 3000;
|
|
531
|
+
this._visibilityHandler = () => {
|
|
532
|
+
if (this.isDestroyed)
|
|
533
|
+
return;
|
|
534
|
+
if (document.hidden) {
|
|
535
|
+
this._hiddenAtMs = Date.now();
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
const hiddenAt = this._hiddenAtMs;
|
|
539
|
+
this._hiddenAtMs = null;
|
|
540
|
+
if (hiddenAt === null)
|
|
541
|
+
return;
|
|
542
|
+
const awayMs = Date.now() - hiddenAt;
|
|
543
|
+
if (awayMs >= REBACKGROUND_RESTART_MS && this.currentStep === "liveness") {
|
|
544
|
+
this.showStep("liveness");
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
document.addEventListener("visibilitychange", this._visibilityHandler);
|
|
548
|
+
// Intercept the browser/OS back gesture so it steps back within the flow
|
|
549
|
+
// instead of navigating the host page away and abandoning the session
|
|
550
|
+
// mid-verification (camera left open, timers orphaned). One guard entry
|
|
551
|
+
// is pushed — not one per step — to minimize interference with a host
|
|
552
|
+
// page's own router; popstate re-arms it each time so back can be
|
|
553
|
+
// pressed repeatedly. If the current step has no back target, back
|
|
554
|
+
// closes the widget cleanly instead (same as the header ✕).
|
|
555
|
+
this._historyGuardActive = true;
|
|
556
|
+
try {
|
|
557
|
+
history.pushState({ scFlowGuard: true }, "");
|
|
558
|
+
}
|
|
559
|
+
catch {
|
|
560
|
+
this._historyGuardActive = false; // host disallows pushState — degrade silently
|
|
561
|
+
}
|
|
562
|
+
this._popstateHandler = () => {
|
|
563
|
+
if (this.isDestroyed || !this._historyGuardActive)
|
|
564
|
+
return;
|
|
565
|
+
const backAction = this._getBackAction(this.currentStep);
|
|
566
|
+
if (backAction) {
|
|
567
|
+
backAction();
|
|
568
|
+
}
|
|
569
|
+
else {
|
|
570
|
+
this.close();
|
|
571
|
+
return; // don't re-arm — we're closing, let the real back navigation proceed
|
|
572
|
+
}
|
|
573
|
+
try {
|
|
574
|
+
history.pushState({ scFlowGuard: true }, "");
|
|
575
|
+
}
|
|
576
|
+
catch { /* ignore — nothing more we can do */ }
|
|
577
|
+
};
|
|
578
|
+
window.addEventListener("popstate", this._popstateHandler);
|
|
467
579
|
}
|
|
468
580
|
async initialize() {
|
|
469
581
|
this.showStep("loading");
|
|
@@ -732,14 +844,16 @@ export class SmartComplyFlow {
|
|
|
732
844
|
btn.style.cssText += "margin-top:2px;";
|
|
733
845
|
btn.addEventListener("click", () => this.showStep("country"));
|
|
734
846
|
container.appendChild(btn);
|
|
735
|
-
//
|
|
736
|
-
//
|
|
847
|
+
// Plain-language line on what the camera/photo is for and what happens
|
|
848
|
+
// to it — shown before the "Get Started" tap that leads to the first
|
|
849
|
+
// camera permission prompt, so the browser's own prompt isn't the only
|
|
850
|
+
// context the user gets for why a camera is being requested.
|
|
737
851
|
const trust = document.createElement("div");
|
|
738
852
|
trust.style.cssText = `
|
|
739
|
-
display:flex;align-items:
|
|
740
|
-
color:${this.theme.textMuted};font-size:10.5px;
|
|
853
|
+
display:flex;align-items:flex-start;gap:6px;text-align:left;
|
|
854
|
+
color:${this.theme.textMuted};font-size:10.5px;line-height:1.5;
|
|
741
855
|
`;
|
|
742
|
-
trust.
|
|
856
|
+
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>`;
|
|
743
857
|
container.appendChild(trust);
|
|
744
858
|
}
|
|
745
859
|
// ── Country Select ──────────────────────────────────────────────
|
|
@@ -1853,6 +1967,11 @@ export class SmartComplyFlow {
|
|
|
1853
1967
|
this._renderRetryLimitReached(container);
|
|
1854
1968
|
return;
|
|
1855
1969
|
}
|
|
1970
|
+
// Each render gets its own id so a stale attempt (e.g. abandoned after
|
|
1971
|
+
// the tab was backgrounded and we restarted the scan on return) can't
|
|
1972
|
+
// resolve/reject into the current one — see the visibilitychange
|
|
1973
|
+
// handler in mount(), which re-renders this step on return-to-foreground.
|
|
1974
|
+
const attemptId = ++this._livenessAttemptId;
|
|
1856
1975
|
const title = this.createStepTitle("Face verification");
|
|
1857
1976
|
container.appendChild(title);
|
|
1858
1977
|
const desc = document.createElement("div");
|
|
@@ -1901,8 +2020,12 @@ export class SmartComplyFlow {
|
|
|
1901
2020
|
error: this.theme.error,
|
|
1902
2021
|
warning: this.theme.warning,
|
|
1903
2022
|
},
|
|
1904
|
-
}, [
|
|
2023
|
+
}, [...DEFAULT_CHALLENGE_ACTIONS], this.livenessEntryId || undefined)
|
|
1905
2024
|
.then((result) => {
|
|
2025
|
+
// A stale attempt (superseded by a restart after the tab was
|
|
2026
|
+
// backgrounded) resolving late must not act on the current screen.
|
|
2027
|
+
if (attemptId !== this._livenessAttemptId)
|
|
2028
|
+
return;
|
|
1906
2029
|
// W11: After submit, end user is done — show "You're all done" screen.
|
|
1907
2030
|
// Backend processing continues in background; client receives webhook.
|
|
1908
2031
|
// Track liveness failures for retry limit (W14).
|
|
@@ -1915,8 +2038,18 @@ export class SmartComplyFlow {
|
|
|
1915
2038
|
this.showStep("done");
|
|
1916
2039
|
})
|
|
1917
2040
|
.catch((err) => {
|
|
1918
|
-
this.
|
|
1919
|
-
|
|
2041
|
+
if (attemptId !== this._livenessAttemptId)
|
|
2042
|
+
return;
|
|
2043
|
+
// Only count a genuine verification failure against the retry limit —
|
|
2044
|
+
// a dropped camera, network blip, or upload failure isn't the user's
|
|
2045
|
+
// fault and shouldn't cost them one of their limited attempts. These
|
|
2046
|
+
// are infrastructure-layer errors (thrown/rejected); a real face-
|
|
2047
|
+
// match/quality failure comes back as a resolved result with
|
|
2048
|
+
// status "failed" instead (handled in .then() above).
|
|
2049
|
+
if (!_isInfrastructureError(err)) {
|
|
2050
|
+
this._livenessFailures++;
|
|
2051
|
+
}
|
|
2052
|
+
this.showErrorStep(_userFacingLivenessError(err));
|
|
1920
2053
|
});
|
|
1921
2054
|
}
|
|
1922
2055
|
// ── Error ───────────────────────────────────────────────────────
|
|
@@ -2238,6 +2371,24 @@ export class SmartComplyFlow {
|
|
|
2238
2371
|
.flatMap((ch) => ch.fields || []);
|
|
2239
2372
|
return allFields.some((f) => isUploadFieldType(f.type));
|
|
2240
2373
|
}
|
|
2374
|
+
/**
|
|
2375
|
+
* The "go back one step" target for a given step, if that step is
|
|
2376
|
+
* navigable — shared by the header back button and the browser
|
|
2377
|
+
* back-button (popstate) handler in mount() so both agree on the same
|
|
2378
|
+
* step order.
|
|
2379
|
+
*/
|
|
2380
|
+
_getBackAction(step) {
|
|
2381
|
+
const countries = Object.keys(this.sdkConfig?.channels || {});
|
|
2382
|
+
const backTargets = {
|
|
2383
|
+
id_type: () => this.showStep(countries.length > 1 ? "country" : "welcome"),
|
|
2384
|
+
country: () => this.showStep("welcome"),
|
|
2385
|
+
id_input: () => this.showStep("id_type"),
|
|
2386
|
+
confirm_identity: () => this.showStep("id_input"),
|
|
2387
|
+
document_capture: () => { this.docCapture?.destroy(); this.showStep("id_type"); },
|
|
2388
|
+
document_confirm: () => { this.docCapture?.destroy(); this.showStep("document_capture"); },
|
|
2389
|
+
};
|
|
2390
|
+
return backTargets[step];
|
|
2391
|
+
}
|
|
2241
2392
|
updateProgress() {
|
|
2242
2393
|
const isDocFlow = this.isDocumentFlow();
|
|
2243
2394
|
const countries = Object.keys(this.sdkConfig?.channels || {});
|
|
@@ -2289,15 +2440,7 @@ export class SmartComplyFlow {
|
|
|
2289
2440
|
}
|
|
2290
2441
|
// Back button — show for navigable steps only
|
|
2291
2442
|
if (this.headerBackBtn) {
|
|
2292
|
-
const
|
|
2293
|
-
id_type: () => this.showStep(countries.length > 1 ? "country" : "welcome"),
|
|
2294
|
-
country: () => this.showStep("welcome"),
|
|
2295
|
-
id_input: () => this.showStep("id_type"),
|
|
2296
|
-
confirm_identity: () => this.showStep("id_input"),
|
|
2297
|
-
document_capture: () => { this.docCapture?.destroy(); this.showStep("id_type"); },
|
|
2298
|
-
document_confirm: () => { this.docCapture?.destroy(); this.showStep("document_capture"); },
|
|
2299
|
-
};
|
|
2300
|
-
const backAction = backTargets[this.currentStep];
|
|
2443
|
+
const backAction = this._getBackAction(this.currentStep);
|
|
2301
2444
|
if (backAction) {
|
|
2302
2445
|
this.headerBackBtn.style.display = "flex";
|
|
2303
2446
|
const newBtn = this.headerBackBtn.cloneNode(true);
|