smartcomply-web-sdk 1.0.66 → 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 +83 -5
- package/dist/esm/flow/DocumentCapture.js.map +1 -1
- package/dist/esm/flow/SmartComplyFlow.d.ts +19 -0
- package/dist/esm/flow/SmartComplyFlow.d.ts.map +1 -1
- package/dist/esm/flow/SmartComplyFlow.js +216 -32
- 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/modules/liveness/liveness.js +1 -1
- package/dist/esm/modules/liveness/liveness.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/esm/utils/constants.d.ts +9 -6
- package/dist/esm/utils/constants.d.ts.map +1 -1
- package/dist/esm/utils/constants.js +9 -6
- package/dist/esm/utils/constants.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 +83 -5
- package/dist/flow/DocumentCapture.js.map +1 -1
- package/dist/flow/SmartComplyFlow.d.ts +19 -0
- package/dist/flow/SmartComplyFlow.d.ts.map +1 -1
- package/dist/flow/SmartComplyFlow.js +216 -32
- 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/modules/liveness/liveness.js +1 -1
- package/dist/modules/liveness/liveness.js.map +1 -1
- package/dist/smartcomply.browser.js +360 -43
- 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/dist/utils/constants.d.ts +9 -6
- package/dist/utils/constants.d.ts.map +1 -1
- package/dist/utils/constants.js +9 -6
- package/dist/utils/constants.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");
|
|
@@ -658,7 +768,7 @@ class SmartComplyFlow {
|
|
|
658
768
|
container.appendChild(topRow);
|
|
659
769
|
// Title + subtitle (compact)
|
|
660
770
|
const titleWrap = document.createElement("div");
|
|
661
|
-
titleWrap.style.cssText = "display:flex;flex-direction:column;gap:5px;";
|
|
771
|
+
titleWrap.style.cssText = "display:flex;flex-direction:column;align-items:center;gap:5px;width:100%;";
|
|
662
772
|
const title = document.createElement("h2");
|
|
663
773
|
title.style.cssText = `
|
|
664
774
|
color:${this.theme.text};font-size:${theme_1.TYPE.heading}px;font-weight:700;
|
|
@@ -673,7 +783,7 @@ class SmartComplyFlow {
|
|
|
673
783
|
const desc = document.createElement("p");
|
|
674
784
|
desc.style.cssText = `
|
|
675
785
|
color:${this.theme.textSecondary};font-size:13px;line-height:1.5;
|
|
676
|
-
margin:0;max-width:300px;
|
|
786
|
+
margin:0 auto;max-width:300px;text-align:center;
|
|
677
787
|
`;
|
|
678
788
|
desc.textContent = isDocFlow
|
|
679
789
|
? "You'll need your document and camera access. This takes about 2 minutes."
|
|
@@ -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 ──────────────────────────────────────────────
|
|
@@ -1293,6 +1405,14 @@ class SmartComplyFlow {
|
|
|
1293
1405
|
this._renderRetryLimitReached(container);
|
|
1294
1406
|
return;
|
|
1295
1407
|
}
|
|
1408
|
+
// OCR reported "done" but extracted nothing usable — do not let the user
|
|
1409
|
+
// click through to liveness with no verified identity data. This is the
|
|
1410
|
+
// ocr_status==="done"-with-empty-fields sibling of the poll timeout case
|
|
1411
|
+
// (which _renderOcrTimeout already catches in renderOcrGate).
|
|
1412
|
+
if (!this._ocrName && !this._ocrDocNumber && !this._ocrExpiry) {
|
|
1413
|
+
this._renderOcrTimeout(container);
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1296
1416
|
// W13: Pre-warm camera while user reads — but only if the document photo
|
|
1297
1417
|
// itself came from the camera. Pre-warming here when the user uploaded
|
|
1298
1418
|
// from gallery means requesting camera access before they've touched
|
|
@@ -1432,7 +1552,7 @@ class SmartComplyFlow {
|
|
|
1432
1552
|
// end. 45 x 2s = 90s comfortably covers that worst case while the loop
|
|
1433
1553
|
// still exits immediately once ocrDone is true — the common case
|
|
1434
1554
|
// (OCR done in a few seconds) is exactly as fast as before.
|
|
1435
|
-
const MAX_POLLS = 45; // 45
|
|
1555
|
+
const MAX_POLLS = 45; // 45 x 2s = 90s timeout
|
|
1436
1556
|
let polls = 0;
|
|
1437
1557
|
const poll = async () => {
|
|
1438
1558
|
if (this.isDestroyed)
|
|
@@ -1486,13 +1606,13 @@ class SmartComplyFlow {
|
|
|
1486
1606
|
this._ocrPollTimer = setTimeout(poll, 2000);
|
|
1487
1607
|
}
|
|
1488
1608
|
else if (!this.isDestroyed) {
|
|
1489
|
-
// Genuinely exhausted the whole window
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1609
|
+
// Genuinely exhausted the whole window with nothing usable read —
|
|
1610
|
+
// stop here instead of letting an unverified identity through to
|
|
1611
|
+
// liveness. document_confirm never blocks on missing fields (by
|
|
1612
|
+
// design, so a partial-but-real OCR read still lets the user
|
|
1613
|
+
// continue), so the gate has to live here, before that screen.
|
|
1614
|
+
container.innerHTML = "";
|
|
1615
|
+
this._renderOcrTimeout(container);
|
|
1496
1616
|
}
|
|
1497
1617
|
}
|
|
1498
1618
|
catch {
|
|
@@ -1561,6 +1681,35 @@ class SmartComplyFlow {
|
|
|
1561
1681
|
});
|
|
1562
1682
|
container.appendChild(retryBtn);
|
|
1563
1683
|
}
|
|
1684
|
+
/**
|
|
1685
|
+
* OCR ran to completion but never produced a single usable field within
|
|
1686
|
+
* the poll window — do not let the flow reach liveness with no verified
|
|
1687
|
+
* identity data. Distinct from _renderOcrQualityFail (a bad photo) since
|
|
1688
|
+
* this covers a genuinely stuck/slow OCR pass instead of a rejected image.
|
|
1689
|
+
*/
|
|
1690
|
+
_renderOcrTimeout(container) {
|
|
1691
|
+
container.style.cssText += "display:flex;flex-direction:column;align-items:center;text-align:center;gap:14px;padding:20px 0;";
|
|
1692
|
+
const icon = document.createElement("div");
|
|
1693
|
+
icon.style.cssText = `width:72px;height:72px;border-radius:50%;background:${this.theme.errorBg};border:3px solid ${this.theme.error};display:flex;align-items:center;justify-content:center;font-size:28px;`;
|
|
1694
|
+
icon.textContent = "⏱️";
|
|
1695
|
+
container.appendChild(icon);
|
|
1696
|
+
const t = document.createElement("h2");
|
|
1697
|
+
t.style.cssText = `color:${this.theme.text};font-size:${theme_1.TYPE.heading}px;font-weight:700;margin:0;`;
|
|
1698
|
+
t.textContent = "Couldn't Read Your Document";
|
|
1699
|
+
container.appendChild(t);
|
|
1700
|
+
const d = document.createElement("p");
|
|
1701
|
+
d.style.cssText = `color:${this.theme.textSecondary};font-size:13px;margin:0;max-width:280px;line-height:1.6;`;
|
|
1702
|
+
d.textContent = "We weren't able to read any details from your document. Please retake the photo in good lighting, with all four corners visible.";
|
|
1703
|
+
container.appendChild(d);
|
|
1704
|
+
const retryBtn = this.createPrimaryButton("Retake photo");
|
|
1705
|
+
retryBtn.addEventListener("click", () => {
|
|
1706
|
+
this.livenessEntryId = null;
|
|
1707
|
+
this.documentBlob = null;
|
|
1708
|
+
this.documentBackBlob = null;
|
|
1709
|
+
this.showStep("document_capture");
|
|
1710
|
+
});
|
|
1711
|
+
container.appendChild(retryBtn);
|
|
1712
|
+
}
|
|
1564
1713
|
// ── W11: You're all done (end-user final screen) ─────────────────
|
|
1565
1714
|
renderDone(container) {
|
|
1566
1715
|
container.style.cssText += "display:flex;flex-direction:column;align-items:center;text-align:center;gap:16px;padding:24px 0;";
|
|
@@ -1819,6 +1968,11 @@ class SmartComplyFlow {
|
|
|
1819
1968
|
this._renderRetryLimitReached(container);
|
|
1820
1969
|
return;
|
|
1821
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;
|
|
1822
1976
|
const title = this.createStepTitle("Face verification");
|
|
1823
1977
|
container.appendChild(title);
|
|
1824
1978
|
const desc = document.createElement("div");
|
|
@@ -1867,8 +2021,12 @@ class SmartComplyFlow {
|
|
|
1867
2021
|
error: this.theme.error,
|
|
1868
2022
|
warning: this.theme.warning,
|
|
1869
2023
|
},
|
|
1870
|
-
}, [
|
|
2024
|
+
}, [...constants_1.DEFAULT_CHALLENGE_ACTIONS], this.livenessEntryId || undefined)
|
|
1871
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;
|
|
1872
2030
|
// W11: After submit, end user is done — show "You're all done" screen.
|
|
1873
2031
|
// Backend processing continues in background; client receives webhook.
|
|
1874
2032
|
// Track liveness failures for retry limit (W14).
|
|
@@ -1881,8 +2039,18 @@ class SmartComplyFlow {
|
|
|
1881
2039
|
this.showStep("done");
|
|
1882
2040
|
})
|
|
1883
2041
|
.catch((err) => {
|
|
1884
|
-
this.
|
|
1885
|
-
|
|
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));
|
|
1886
2054
|
});
|
|
1887
2055
|
}
|
|
1888
2056
|
// ── Error ───────────────────────────────────────────────────────
|
|
@@ -2075,9 +2243,14 @@ class SmartComplyFlow {
|
|
|
2075
2243
|
const powered = document.createElement("span");
|
|
2076
2244
|
powered.style.cssText = `font-size:11px;color:${this.theme.textMuted};`;
|
|
2077
2245
|
powered.textContent = "Powered by ";
|
|
2078
|
-
const brand = document.createElement("
|
|
2079
|
-
brand.
|
|
2246
|
+
const brand = document.createElement("a");
|
|
2247
|
+
brand.href = "https://adhere.smartcomply.com/";
|
|
2248
|
+
brand.target = "_blank";
|
|
2249
|
+
brand.rel = "noopener noreferrer";
|
|
2250
|
+
brand.style.cssText = `font-size:11px;font-weight:700;color:${this.theme.primary};text-decoration:none;`;
|
|
2080
2251
|
brand.textContent = "Adhere";
|
|
2252
|
+
brand.addEventListener("mouseenter", () => { brand.style.textDecoration = "underline"; });
|
|
2253
|
+
brand.addEventListener("mouseleave", () => { brand.style.textDecoration = "none"; });
|
|
2081
2254
|
footer.appendChild(powered);
|
|
2082
2255
|
footer.appendChild(brand);
|
|
2083
2256
|
return footer;
|
|
@@ -2114,18 +2287,19 @@ class SmartComplyFlow {
|
|
|
2114
2287
|
});
|
|
2115
2288
|
card.addEventListener("click", onClick);
|
|
2116
2289
|
const textWrap = document.createElement("div");
|
|
2290
|
+
textWrap.style.cssText = "flex:1 1 auto;min-width:0;";
|
|
2117
2291
|
const titleEl = document.createElement("div");
|
|
2118
|
-
titleEl.style.cssText = `color:${this.theme.text};font-size:14px;font-weight:600;`;
|
|
2292
|
+
titleEl.style.cssText = `color:${this.theme.text};font-size:14px;font-weight:600;line-height:1.35;`;
|
|
2119
2293
|
titleEl.textContent = title;
|
|
2120
2294
|
textWrap.appendChild(titleEl);
|
|
2121
2295
|
if (subtitle) {
|
|
2122
2296
|
const subEl = document.createElement("div");
|
|
2123
|
-
subEl.style.cssText = `color:${this.theme.textMuted};font-size:12px;margin-top:2px;`;
|
|
2297
|
+
subEl.style.cssText = `color:${this.theme.textMuted};font-size:12px;line-height:1.4;margin-top:2px;`;
|
|
2124
2298
|
subEl.textContent = subtitle;
|
|
2125
2299
|
textWrap.appendChild(subEl);
|
|
2126
2300
|
}
|
|
2127
2301
|
const arrow = document.createElement("span");
|
|
2128
|
-
arrow.style.cssText = `color:${this.theme.textMuted};font-size:18px;flex-shrink:0;`;
|
|
2302
|
+
arrow.style.cssText = `color:${this.theme.textMuted};font-size:18px;flex-shrink:0;margin-left:12px;line-height:1;`;
|
|
2129
2303
|
arrow.textContent = "→";
|
|
2130
2304
|
card.appendChild(textWrap);
|
|
2131
2305
|
card.appendChild(arrow);
|
|
@@ -2198,6 +2372,24 @@ class SmartComplyFlow {
|
|
|
2198
2372
|
.flatMap((ch) => ch.fields || []);
|
|
2199
2373
|
return allFields.some((f) => isUploadFieldType(f.type));
|
|
2200
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
|
+
}
|
|
2201
2393
|
updateProgress() {
|
|
2202
2394
|
const isDocFlow = this.isDocumentFlow();
|
|
2203
2395
|
const countries = Object.keys(this.sdkConfig?.channels || {});
|
|
@@ -2249,15 +2441,7 @@ class SmartComplyFlow {
|
|
|
2249
2441
|
}
|
|
2250
2442
|
// Back button — show for navigable steps only
|
|
2251
2443
|
if (this.headerBackBtn) {
|
|
2252
|
-
const
|
|
2253
|
-
id_type: () => this.showStep(countries.length > 1 ? "country" : "welcome"),
|
|
2254
|
-
country: () => this.showStep("welcome"),
|
|
2255
|
-
id_input: () => this.showStep("id_type"),
|
|
2256
|
-
confirm_identity: () => this.showStep("id_input"),
|
|
2257
|
-
document_capture: () => { this.docCapture?.destroy(); this.showStep("id_type"); },
|
|
2258
|
-
document_confirm: () => { this.docCapture?.destroy(); this.showStep("document_capture"); },
|
|
2259
|
-
};
|
|
2260
|
-
const backAction = backTargets[this.currentStep];
|
|
2444
|
+
const backAction = this._getBackAction(this.currentStep);
|
|
2261
2445
|
if (backAction) {
|
|
2262
2446
|
this.headerBackBtn.style.display = "flex";
|
|
2263
2447
|
const newBtn = this.headerBackBtn.cloneNode(true);
|