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
|
@@ -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",
|
|
@@ -273,9 +300,14 @@ export class SmartComplyFlow {
|
|
|
273
300
|
this.isDestroyed = false;
|
|
274
301
|
this.headerBackBtn = null;
|
|
275
302
|
this._keydownHandler = null;
|
|
303
|
+
this._visibilityHandler = null;
|
|
304
|
+
this._hiddenAtMs = null;
|
|
305
|
+
this._popstateHandler = null;
|
|
306
|
+
this._historyGuardActive = false;
|
|
276
307
|
// Retry safety — max 3 confirmation rejections or liveness failures per session
|
|
277
308
|
this._confirmRetries = 0;
|
|
278
309
|
this._livenessFailures = 0;
|
|
310
|
+
this._livenessAttemptId = 0;
|
|
279
311
|
// IVS result stored for confirm_identity screen
|
|
280
312
|
this._ivsName = "";
|
|
281
313
|
this._ivsGender = "";
|
|
@@ -334,6 +366,26 @@ export class SmartComplyFlow {
|
|
|
334
366
|
document.removeEventListener("keydown", this._keydownHandler);
|
|
335
367
|
this._keydownHandler = null;
|
|
336
368
|
}
|
|
369
|
+
if (this._visibilityHandler) {
|
|
370
|
+
document.removeEventListener("visibilitychange", this._visibilityHandler);
|
|
371
|
+
this._visibilityHandler = null;
|
|
372
|
+
}
|
|
373
|
+
if (this._popstateHandler) {
|
|
374
|
+
window.removeEventListener("popstate", this._popstateHandler);
|
|
375
|
+
this._popstateHandler = null;
|
|
376
|
+
}
|
|
377
|
+
// Closing via the ✕/Escape/completion (not via the back gesture, which
|
|
378
|
+
// handles its own guard entry above) still leaves our one guard entry
|
|
379
|
+
// sitting on top of the host's history — back out of it so the user
|
|
380
|
+
// isn't left pressing back once "into" a widget that's already gone.
|
|
381
|
+
if (this._historyGuardActive) {
|
|
382
|
+
this._historyGuardActive = false;
|
|
383
|
+
try {
|
|
384
|
+
if (history.state?.scFlowGuard)
|
|
385
|
+
history.back();
|
|
386
|
+
}
|
|
387
|
+
catch { /* ignore — best effort only */ }
|
|
388
|
+
}
|
|
337
389
|
this.docCapture?.destroy();
|
|
338
390
|
// Release any pre-warmed camera stream
|
|
339
391
|
if (this._prewarmedStream) {
|
|
@@ -464,6 +516,64 @@ export class SmartComplyFlow {
|
|
|
464
516
|
}
|
|
465
517
|
};
|
|
466
518
|
document.addEventListener("keydown", this._keydownHandler);
|
|
519
|
+
// A tab backgrounded mid-scan (a notification, switching apps to copy a
|
|
520
|
+
// code, a phone call) leaves the camera stream and detection loop in an
|
|
521
|
+
// undefined state on most mobile browsers — rAF throttles/pauses while
|
|
522
|
+
// hidden, and some browsers suspend the camera outright. Rather than let
|
|
523
|
+
// the scan silently continue against stale state and fail confusingly,
|
|
524
|
+
// restart the liveness step cleanly once the tab has been away long
|
|
525
|
+
// enough to plausibly have disrupted an active camera session. A brief
|
|
526
|
+
// glance elsewhere (quick app switch, screen lock tap) is not enough to
|
|
527
|
+
// warrant restarting — only a real return-from-away.
|
|
528
|
+
const REBACKGROUND_RESTART_MS = 3000;
|
|
529
|
+
this._visibilityHandler = () => {
|
|
530
|
+
if (this.isDestroyed)
|
|
531
|
+
return;
|
|
532
|
+
if (document.hidden) {
|
|
533
|
+
this._hiddenAtMs = Date.now();
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
const hiddenAt = this._hiddenAtMs;
|
|
537
|
+
this._hiddenAtMs = null;
|
|
538
|
+
if (hiddenAt === null)
|
|
539
|
+
return;
|
|
540
|
+
const awayMs = Date.now() - hiddenAt;
|
|
541
|
+
if (awayMs >= REBACKGROUND_RESTART_MS && this.currentStep === "liveness") {
|
|
542
|
+
this.showStep("liveness");
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
document.addEventListener("visibilitychange", this._visibilityHandler);
|
|
546
|
+
// Intercept the browser/OS back gesture so it steps back within the flow
|
|
547
|
+
// instead of navigating the host page away and abandoning the session
|
|
548
|
+
// mid-verification (camera left open, timers orphaned). One guard entry
|
|
549
|
+
// is pushed — not one per step — to minimize interference with a host
|
|
550
|
+
// page's own router; popstate re-arms it each time so back can be
|
|
551
|
+
// pressed repeatedly. If the current step has no back target, back
|
|
552
|
+
// closes the widget cleanly instead (same as the header ✕).
|
|
553
|
+
this._historyGuardActive = true;
|
|
554
|
+
try {
|
|
555
|
+
history.pushState({ scFlowGuard: true }, "");
|
|
556
|
+
}
|
|
557
|
+
catch {
|
|
558
|
+
this._historyGuardActive = false; // host disallows pushState — degrade silently
|
|
559
|
+
}
|
|
560
|
+
this._popstateHandler = () => {
|
|
561
|
+
if (this.isDestroyed || !this._historyGuardActive)
|
|
562
|
+
return;
|
|
563
|
+
const backAction = this._getBackAction(this.currentStep);
|
|
564
|
+
if (backAction) {
|
|
565
|
+
backAction();
|
|
566
|
+
}
|
|
567
|
+
else {
|
|
568
|
+
this.close();
|
|
569
|
+
return; // don't re-arm — we're closing, let the real back navigation proceed
|
|
570
|
+
}
|
|
571
|
+
try {
|
|
572
|
+
history.pushState({ scFlowGuard: true }, "");
|
|
573
|
+
}
|
|
574
|
+
catch { /* ignore — nothing more we can do */ }
|
|
575
|
+
};
|
|
576
|
+
window.addEventListener("popstate", this._popstateHandler);
|
|
467
577
|
}
|
|
468
578
|
async initialize() {
|
|
469
579
|
this.showStep("loading");
|
|
@@ -655,7 +765,7 @@ export class SmartComplyFlow {
|
|
|
655
765
|
container.appendChild(topRow);
|
|
656
766
|
// Title + subtitle (compact)
|
|
657
767
|
const titleWrap = document.createElement("div");
|
|
658
|
-
titleWrap.style.cssText = "display:flex;flex-direction:column;gap:5px;";
|
|
768
|
+
titleWrap.style.cssText = "display:flex;flex-direction:column;align-items:center;gap:5px;width:100%;";
|
|
659
769
|
const title = document.createElement("h2");
|
|
660
770
|
title.style.cssText = `
|
|
661
771
|
color:${this.theme.text};font-size:${TYPE.heading}px;font-weight:700;
|
|
@@ -670,7 +780,7 @@ export class SmartComplyFlow {
|
|
|
670
780
|
const desc = document.createElement("p");
|
|
671
781
|
desc.style.cssText = `
|
|
672
782
|
color:${this.theme.textSecondary};font-size:13px;line-height:1.5;
|
|
673
|
-
margin:0;max-width:300px;
|
|
783
|
+
margin:0 auto;max-width:300px;text-align:center;
|
|
674
784
|
`;
|
|
675
785
|
desc.textContent = isDocFlow
|
|
676
786
|
? "You'll need your document and camera access. This takes about 2 minutes."
|
|
@@ -732,14 +842,16 @@ export class SmartComplyFlow {
|
|
|
732
842
|
btn.style.cssText += "margin-top:2px;";
|
|
733
843
|
btn.addEventListener("click", () => this.showStep("country"));
|
|
734
844
|
container.appendChild(btn);
|
|
735
|
-
//
|
|
736
|
-
//
|
|
845
|
+
// Plain-language line on what the camera/photo is for and what happens
|
|
846
|
+
// to it — shown before the "Get Started" tap that leads to the first
|
|
847
|
+
// camera permission prompt, so the browser's own prompt isn't the only
|
|
848
|
+
// context the user gets for why a camera is being requested.
|
|
737
849
|
const trust = document.createElement("div");
|
|
738
850
|
trust.style.cssText = `
|
|
739
|
-
display:flex;align-items:
|
|
740
|
-
color:${this.theme.textMuted};font-size:10.5px;
|
|
851
|
+
display:flex;align-items:flex-start;gap:6px;text-align:left;
|
|
852
|
+
color:${this.theme.textMuted};font-size:10.5px;line-height:1.5;
|
|
741
853
|
`;
|
|
742
|
-
trust.
|
|
854
|
+
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
855
|
container.appendChild(trust);
|
|
744
856
|
}
|
|
745
857
|
// ── Country Select ──────────────────────────────────────────────
|
|
@@ -1290,6 +1402,14 @@ export class SmartComplyFlow {
|
|
|
1290
1402
|
this._renderRetryLimitReached(container);
|
|
1291
1403
|
return;
|
|
1292
1404
|
}
|
|
1405
|
+
// OCR reported "done" but extracted nothing usable — do not let the user
|
|
1406
|
+
// click through to liveness with no verified identity data. This is the
|
|
1407
|
+
// ocr_status==="done"-with-empty-fields sibling of the poll timeout case
|
|
1408
|
+
// (which _renderOcrTimeout already catches in renderOcrGate).
|
|
1409
|
+
if (!this._ocrName && !this._ocrDocNumber && !this._ocrExpiry) {
|
|
1410
|
+
this._renderOcrTimeout(container);
|
|
1411
|
+
return;
|
|
1412
|
+
}
|
|
1293
1413
|
// W13: Pre-warm camera while user reads — but only if the document photo
|
|
1294
1414
|
// itself came from the camera. Pre-warming here when the user uploaded
|
|
1295
1415
|
// from gallery means requesting camera access before they've touched
|
|
@@ -1429,7 +1549,7 @@ export class SmartComplyFlow {
|
|
|
1429
1549
|
// end. 45 x 2s = 90s comfortably covers that worst case while the loop
|
|
1430
1550
|
// still exits immediately once ocrDone is true — the common case
|
|
1431
1551
|
// (OCR done in a few seconds) is exactly as fast as before.
|
|
1432
|
-
const MAX_POLLS = 45; // 45
|
|
1552
|
+
const MAX_POLLS = 45; // 45 x 2s = 90s timeout
|
|
1433
1553
|
let polls = 0;
|
|
1434
1554
|
const poll = async () => {
|
|
1435
1555
|
if (this.isDestroyed)
|
|
@@ -1483,13 +1603,13 @@ export class SmartComplyFlow {
|
|
|
1483
1603
|
this._ocrPollTimer = setTimeout(poll, 2000);
|
|
1484
1604
|
}
|
|
1485
1605
|
else if (!this.isDestroyed) {
|
|
1486
|
-
// Genuinely exhausted the whole window
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1606
|
+
// Genuinely exhausted the whole window with nothing usable read —
|
|
1607
|
+
// stop here instead of letting an unverified identity through to
|
|
1608
|
+
// liveness. document_confirm never blocks on missing fields (by
|
|
1609
|
+
// design, so a partial-but-real OCR read still lets the user
|
|
1610
|
+
// continue), so the gate has to live here, before that screen.
|
|
1611
|
+
container.innerHTML = "";
|
|
1612
|
+
this._renderOcrTimeout(container);
|
|
1493
1613
|
}
|
|
1494
1614
|
}
|
|
1495
1615
|
catch {
|
|
@@ -1558,6 +1678,35 @@ export class SmartComplyFlow {
|
|
|
1558
1678
|
});
|
|
1559
1679
|
container.appendChild(retryBtn);
|
|
1560
1680
|
}
|
|
1681
|
+
/**
|
|
1682
|
+
* OCR ran to completion but never produced a single usable field within
|
|
1683
|
+
* the poll window — do not let the flow reach liveness with no verified
|
|
1684
|
+
* identity data. Distinct from _renderOcrQualityFail (a bad photo) since
|
|
1685
|
+
* this covers a genuinely stuck/slow OCR pass instead of a rejected image.
|
|
1686
|
+
*/
|
|
1687
|
+
_renderOcrTimeout(container) {
|
|
1688
|
+
container.style.cssText += "display:flex;flex-direction:column;align-items:center;text-align:center;gap:14px;padding:20px 0;";
|
|
1689
|
+
const icon = document.createElement("div");
|
|
1690
|
+
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;`;
|
|
1691
|
+
icon.textContent = "⏱️";
|
|
1692
|
+
container.appendChild(icon);
|
|
1693
|
+
const t = document.createElement("h2");
|
|
1694
|
+
t.style.cssText = `color:${this.theme.text};font-size:${TYPE.heading}px;font-weight:700;margin:0;`;
|
|
1695
|
+
t.textContent = "Couldn't Read Your Document";
|
|
1696
|
+
container.appendChild(t);
|
|
1697
|
+
const d = document.createElement("p");
|
|
1698
|
+
d.style.cssText = `color:${this.theme.textSecondary};font-size:13px;margin:0;max-width:280px;line-height:1.6;`;
|
|
1699
|
+
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.";
|
|
1700
|
+
container.appendChild(d);
|
|
1701
|
+
const retryBtn = this.createPrimaryButton("Retake photo");
|
|
1702
|
+
retryBtn.addEventListener("click", () => {
|
|
1703
|
+
this.livenessEntryId = null;
|
|
1704
|
+
this.documentBlob = null;
|
|
1705
|
+
this.documentBackBlob = null;
|
|
1706
|
+
this.showStep("document_capture");
|
|
1707
|
+
});
|
|
1708
|
+
container.appendChild(retryBtn);
|
|
1709
|
+
}
|
|
1561
1710
|
// ── W11: You're all done (end-user final screen) ─────────────────
|
|
1562
1711
|
renderDone(container) {
|
|
1563
1712
|
container.style.cssText += "display:flex;flex-direction:column;align-items:center;text-align:center;gap:16px;padding:24px 0;";
|
|
@@ -1816,6 +1965,11 @@ export class SmartComplyFlow {
|
|
|
1816
1965
|
this._renderRetryLimitReached(container);
|
|
1817
1966
|
return;
|
|
1818
1967
|
}
|
|
1968
|
+
// Each render gets its own id so a stale attempt (e.g. abandoned after
|
|
1969
|
+
// the tab was backgrounded and we restarted the scan on return) can't
|
|
1970
|
+
// resolve/reject into the current one — see the visibilitychange
|
|
1971
|
+
// handler in mount(), which re-renders this step on return-to-foreground.
|
|
1972
|
+
const attemptId = ++this._livenessAttemptId;
|
|
1819
1973
|
const title = this.createStepTitle("Face verification");
|
|
1820
1974
|
container.appendChild(title);
|
|
1821
1975
|
const desc = document.createElement("div");
|
|
@@ -1864,8 +2018,12 @@ export class SmartComplyFlow {
|
|
|
1864
2018
|
error: this.theme.error,
|
|
1865
2019
|
warning: this.theme.warning,
|
|
1866
2020
|
},
|
|
1867
|
-
}, [
|
|
2021
|
+
}, [...DEFAULT_CHALLENGE_ACTIONS], this.livenessEntryId || undefined)
|
|
1868
2022
|
.then((result) => {
|
|
2023
|
+
// A stale attempt (superseded by a restart after the tab was
|
|
2024
|
+
// backgrounded) resolving late must not act on the current screen.
|
|
2025
|
+
if (attemptId !== this._livenessAttemptId)
|
|
2026
|
+
return;
|
|
1869
2027
|
// W11: After submit, end user is done — show "You're all done" screen.
|
|
1870
2028
|
// Backend processing continues in background; client receives webhook.
|
|
1871
2029
|
// Track liveness failures for retry limit (W14).
|
|
@@ -1878,8 +2036,18 @@ export class SmartComplyFlow {
|
|
|
1878
2036
|
this.showStep("done");
|
|
1879
2037
|
})
|
|
1880
2038
|
.catch((err) => {
|
|
1881
|
-
this.
|
|
1882
|
-
|
|
2039
|
+
if (attemptId !== this._livenessAttemptId)
|
|
2040
|
+
return;
|
|
2041
|
+
// Only count a genuine verification failure against the retry limit —
|
|
2042
|
+
// a dropped camera, network blip, or upload failure isn't the user's
|
|
2043
|
+
// fault and shouldn't cost them one of their limited attempts. These
|
|
2044
|
+
// are infrastructure-layer errors (thrown/rejected); a real face-
|
|
2045
|
+
// match/quality failure comes back as a resolved result with
|
|
2046
|
+
// status "failed" instead (handled in .then() above).
|
|
2047
|
+
if (!_isInfrastructureError(err)) {
|
|
2048
|
+
this._livenessFailures++;
|
|
2049
|
+
}
|
|
2050
|
+
this.showErrorStep(_userFacingLivenessError(err));
|
|
1883
2051
|
});
|
|
1884
2052
|
}
|
|
1885
2053
|
// ── Error ───────────────────────────────────────────────────────
|
|
@@ -2072,9 +2240,14 @@ export class SmartComplyFlow {
|
|
|
2072
2240
|
const powered = document.createElement("span");
|
|
2073
2241
|
powered.style.cssText = `font-size:11px;color:${this.theme.textMuted};`;
|
|
2074
2242
|
powered.textContent = "Powered by ";
|
|
2075
|
-
const brand = document.createElement("
|
|
2076
|
-
brand.
|
|
2243
|
+
const brand = document.createElement("a");
|
|
2244
|
+
brand.href = "https://adhere.smartcomply.com/";
|
|
2245
|
+
brand.target = "_blank";
|
|
2246
|
+
brand.rel = "noopener noreferrer";
|
|
2247
|
+
brand.style.cssText = `font-size:11px;font-weight:700;color:${this.theme.primary};text-decoration:none;`;
|
|
2077
2248
|
brand.textContent = "Adhere";
|
|
2249
|
+
brand.addEventListener("mouseenter", () => { brand.style.textDecoration = "underline"; });
|
|
2250
|
+
brand.addEventListener("mouseleave", () => { brand.style.textDecoration = "none"; });
|
|
2078
2251
|
footer.appendChild(powered);
|
|
2079
2252
|
footer.appendChild(brand);
|
|
2080
2253
|
return footer;
|
|
@@ -2111,18 +2284,19 @@ export class SmartComplyFlow {
|
|
|
2111
2284
|
});
|
|
2112
2285
|
card.addEventListener("click", onClick);
|
|
2113
2286
|
const textWrap = document.createElement("div");
|
|
2287
|
+
textWrap.style.cssText = "flex:1 1 auto;min-width:0;";
|
|
2114
2288
|
const titleEl = document.createElement("div");
|
|
2115
|
-
titleEl.style.cssText = `color:${this.theme.text};font-size:14px;font-weight:600;`;
|
|
2289
|
+
titleEl.style.cssText = `color:${this.theme.text};font-size:14px;font-weight:600;line-height:1.35;`;
|
|
2116
2290
|
titleEl.textContent = title;
|
|
2117
2291
|
textWrap.appendChild(titleEl);
|
|
2118
2292
|
if (subtitle) {
|
|
2119
2293
|
const subEl = document.createElement("div");
|
|
2120
|
-
subEl.style.cssText = `color:${this.theme.textMuted};font-size:12px;margin-top:2px;`;
|
|
2294
|
+
subEl.style.cssText = `color:${this.theme.textMuted};font-size:12px;line-height:1.4;margin-top:2px;`;
|
|
2121
2295
|
subEl.textContent = subtitle;
|
|
2122
2296
|
textWrap.appendChild(subEl);
|
|
2123
2297
|
}
|
|
2124
2298
|
const arrow = document.createElement("span");
|
|
2125
|
-
arrow.style.cssText = `color:${this.theme.textMuted};font-size:18px;flex-shrink:0;`;
|
|
2299
|
+
arrow.style.cssText = `color:${this.theme.textMuted};font-size:18px;flex-shrink:0;margin-left:12px;line-height:1;`;
|
|
2126
2300
|
arrow.textContent = "→";
|
|
2127
2301
|
card.appendChild(textWrap);
|
|
2128
2302
|
card.appendChild(arrow);
|
|
@@ -2195,6 +2369,24 @@ export class SmartComplyFlow {
|
|
|
2195
2369
|
.flatMap((ch) => ch.fields || []);
|
|
2196
2370
|
return allFields.some((f) => isUploadFieldType(f.type));
|
|
2197
2371
|
}
|
|
2372
|
+
/**
|
|
2373
|
+
* The "go back one step" target for a given step, if that step is
|
|
2374
|
+
* navigable — shared by the header back button and the browser
|
|
2375
|
+
* back-button (popstate) handler in mount() so both agree on the same
|
|
2376
|
+
* step order.
|
|
2377
|
+
*/
|
|
2378
|
+
_getBackAction(step) {
|
|
2379
|
+
const countries = Object.keys(this.sdkConfig?.channels || {});
|
|
2380
|
+
const backTargets = {
|
|
2381
|
+
id_type: () => this.showStep(countries.length > 1 ? "country" : "welcome"),
|
|
2382
|
+
country: () => this.showStep("welcome"),
|
|
2383
|
+
id_input: () => this.showStep("id_type"),
|
|
2384
|
+
confirm_identity: () => this.showStep("id_input"),
|
|
2385
|
+
document_capture: () => { this.docCapture?.destroy(); this.showStep("id_type"); },
|
|
2386
|
+
document_confirm: () => { this.docCapture?.destroy(); this.showStep("document_capture"); },
|
|
2387
|
+
};
|
|
2388
|
+
return backTargets[step];
|
|
2389
|
+
}
|
|
2198
2390
|
updateProgress() {
|
|
2199
2391
|
const isDocFlow = this.isDocumentFlow();
|
|
2200
2392
|
const countries = Object.keys(this.sdkConfig?.channels || {});
|
|
@@ -2246,15 +2438,7 @@ export class SmartComplyFlow {
|
|
|
2246
2438
|
}
|
|
2247
2439
|
// Back button — show for navigable steps only
|
|
2248
2440
|
if (this.headerBackBtn) {
|
|
2249
|
-
const
|
|
2250
|
-
id_type: () => this.showStep(countries.length > 1 ? "country" : "welcome"),
|
|
2251
|
-
country: () => this.showStep("welcome"),
|
|
2252
|
-
id_input: () => this.showStep("id_type"),
|
|
2253
|
-
confirm_identity: () => this.showStep("id_input"),
|
|
2254
|
-
document_capture: () => { this.docCapture?.destroy(); this.showStep("id_type"); },
|
|
2255
|
-
document_confirm: () => { this.docCapture?.destroy(); this.showStep("document_capture"); },
|
|
2256
|
-
};
|
|
2257
|
-
const backAction = backTargets[this.currentStep];
|
|
2441
|
+
const backAction = this._getBackAction(this.currentStep);
|
|
2258
2442
|
if (backAction) {
|
|
2259
2443
|
this.headerBackBtn.style.display = "flex";
|
|
2260
2444
|
const newBtn = this.headerBackBtn.cloneNode(true);
|