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
|
@@ -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
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
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
|
|
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
|
-
//
|
|
739
|
-
//
|
|
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:
|
|
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.
|
|
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
|
-
}, [
|
|
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.
|
|
1922
|
-
|
|
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
|
|
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);
|