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.
- package/dist/esm/flow/DocumentCapture.d.ts +10 -0
- package/dist/esm/flow/DocumentCapture.d.ts.map +1 -1
- package/dist/esm/flow/DocumentCapture.js +82 -4
- 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 +158 -17
- package/dist/esm/flow/SmartComplyFlow.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 +10 -0
- package/dist/flow/DocumentCapture.d.ts.map +1 -1
- package/dist/flow/DocumentCapture.js +82 -4
- 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 +158 -17
- package/dist/flow/SmartComplyFlow.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 +305 -28
- 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",
|
|
@@ -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
|
-
//
|
|
739
|
-
//
|
|
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:
|
|
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.
|
|
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
|
-
}, [
|
|
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.
|
|
1922
|
-
|
|
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
|
|
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);
|