smartcomply-web-sdk 1.0.50 → 1.0.52
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/camera/ActionDetector.d.ts +11 -22
- package/dist/camera/ActionDetector.d.ts.map +1 -1
- package/dist/camera/ActionDetector.js +43 -107
- package/dist/camera/ActionDetector.js.map +1 -1
- package/dist/esm/camera/ActionDetector.d.ts +11 -22
- package/dist/esm/camera/ActionDetector.d.ts.map +1 -1
- package/dist/esm/camera/ActionDetector.js +43 -107
- package/dist/esm/camera/ActionDetector.js.map +1 -1
- package/dist/esm/flow/SmartComplyFlow.d.ts.map +1 -1
- package/dist/esm/flow/SmartComplyFlow.js +147 -148
- package/dist/esm/flow/SmartComplyFlow.js.map +1 -1
- package/dist/esm/modules/liveness/LivenessUI.d.ts +18 -16
- package/dist/esm/modules/liveness/LivenessUI.d.ts.map +1 -1
- package/dist/esm/modules/liveness/LivenessUI.js +99 -311
- package/dist/esm/modules/liveness/LivenessUI.js.map +1 -1
- package/dist/esm/modules/liveness/liveness.d.ts +10 -9
- package/dist/esm/modules/liveness/liveness.d.ts.map +1 -1
- package/dist/esm/modules/liveness/liveness.js +58 -107
- package/dist/esm/modules/liveness/liveness.js.map +1 -1
- package/dist/esm/utils/constants.d.ts +11 -7
- package/dist/esm/utils/constants.d.ts.map +1 -1
- package/dist/esm/utils/constants.js +12 -8
- package/dist/esm/utils/constants.js.map +1 -1
- package/dist/flow/SmartComplyFlow.d.ts.map +1 -1
- package/dist/flow/SmartComplyFlow.js +147 -148
- package/dist/flow/SmartComplyFlow.js.map +1 -1
- package/dist/modules/liveness/LivenessUI.d.ts +18 -16
- package/dist/modules/liveness/LivenessUI.d.ts.map +1 -1
- package/dist/modules/liveness/LivenessUI.js +99 -311
- package/dist/modules/liveness/LivenessUI.js.map +1 -1
- package/dist/modules/liveness/liveness.d.ts +10 -9
- package/dist/modules/liveness/liveness.d.ts.map +1 -1
- package/dist/modules/liveness/liveness.js +57 -106
- package/dist/modules/liveness/liveness.js.map +1 -1
- package/dist/smartcomply.browser.js +202 -516
- package/dist/smartcomply.browser.js.map +3 -3
- package/dist/utils/constants.d.ts +11 -7
- package/dist/utils/constants.d.ts.map +1 -1
- package/dist/utils/constants.js +13 -9
- package/dist/utils/constants.js.map +1 -1
- package/package.json +1 -1
|
@@ -20,15 +20,15 @@ const STEP_ORDER = [
|
|
|
20
20
|
"result",
|
|
21
21
|
];
|
|
22
22
|
const COUNTRY_LABELS = {
|
|
23
|
-
nigeria: "
|
|
24
|
-
global: "
|
|
25
|
-
ghana: "
|
|
26
|
-
kenya: "
|
|
27
|
-
south_africa: "
|
|
28
|
-
united_states: "
|
|
29
|
-
united_kingdom: "
|
|
23
|
+
nigeria: "🇳🇬 Nigeria",
|
|
24
|
+
global: "🌍 Other Countries",
|
|
25
|
+
ghana: "🇬🇭 Ghana",
|
|
26
|
+
kenya: "🇰🇪 Kenya",
|
|
27
|
+
south_africa: "🇿🇦 South Africa",
|
|
28
|
+
united_states: "🇺🇸 United States",
|
|
29
|
+
united_kingdom: "🇬🇧 United Kingdom",
|
|
30
30
|
};
|
|
31
|
-
// Backend/admin data tags document-capture fields inconsistently
|
|
31
|
+
// Backend/admin data tags document-capture fields inconsistently — the field
|
|
32
32
|
// renderer (below) already treats "image" and "file" as file inputs alongside
|
|
33
33
|
// "upload", so flow-routing decisions must recognize the same set or a channel
|
|
34
34
|
// tagged "image"/"file" silently falls back to the number-entry screen.
|
|
@@ -38,7 +38,7 @@ function isUploadFieldType(type) {
|
|
|
38
38
|
function resolveIdTypeInfo(typeName, isDocumentFlow) {
|
|
39
39
|
const u = typeName.toUpperCase();
|
|
40
40
|
const advanced = u.includes("ADVANCED");
|
|
41
|
-
// Strip UI noise from display labels
|
|
41
|
+
// Strip UI noise from display labels — users don't need "with face" or "(NIN)" suffixes
|
|
42
42
|
const cleanName = typeName
|
|
43
43
|
.replace(/\s*with\s+face\s*/gi, ' ')
|
|
44
44
|
.replace(/\s*\(NIN\)\s*/gi, ' ')
|
|
@@ -53,41 +53,41 @@ function resolveIdTypeInfo(typeName, isDocumentFlow) {
|
|
|
53
53
|
t.includes("license") ||
|
|
54
54
|
t.includes("national id") ||
|
|
55
55
|
t.includes("national id card");
|
|
56
|
-
// Note: NIN document (nin_with_face, nin_slip) does NOT require back side
|
|
56
|
+
// Note: NIN document (nin_with_face, nin_slip) does NOT require back side —
|
|
57
57
|
// the NIN card is single-sided. Only the physical NIN slip has a back but
|
|
58
58
|
// the OCR pipeline only needs the front.
|
|
59
59
|
};
|
|
60
|
-
// BVN
|
|
60
|
+
// BVN — always 11 digits
|
|
61
61
|
if (u.includes("BVN") || u.includes("BANK VERIFICATION")) {
|
|
62
|
-
return { icon: "
|
|
62
|
+
return { icon: "🏦", desc: "Enter your 11-digit Bank Verification Number (BVN)", requiresBack: false };
|
|
63
63
|
}
|
|
64
|
-
// NIN
|
|
64
|
+
// NIN — always 11 digits (covers nin_with_face, NIN slip, etc.)
|
|
65
65
|
if (u.includes("NIN") || u.includes("NATIONAL IDENTITY NUMBER")) {
|
|
66
|
-
return { icon: "
|
|
66
|
+
return { icon: "🆔", desc: "Enter your 11-digit National Identification Number (NIN)", requiresBack: false };
|
|
67
67
|
}
|
|
68
68
|
// Ghana Card / Ghana National ID
|
|
69
69
|
if (u.includes("GHANA")) {
|
|
70
70
|
if (isDocumentFlow)
|
|
71
|
-
return { icon: "
|
|
72
|
-
return { icon: "
|
|
71
|
+
return { icon: "🪪", desc: "Capture or upload your Ghana Card", requiresBack: true };
|
|
72
|
+
return { icon: "🪪", desc: "Enter your Ghana Card number", requiresBack: false };
|
|
73
73
|
}
|
|
74
74
|
// SSNIT (Ghana social security)
|
|
75
75
|
if (u.includes("SSNIT"))
|
|
76
|
-
return { icon: "
|
|
76
|
+
return { icon: "🏛️", desc: "Enter your SSNIT number", requiresBack: false };
|
|
77
77
|
// Kenya National ID
|
|
78
78
|
if (u.includes("KENYA") || (u.includes("NATIONAL") && u.includes("KENYA")))
|
|
79
|
-
return { icon: "
|
|
79
|
+
return { icon: "🆔", desc: "Enter your Kenya National ID number", requiresBack: false };
|
|
80
80
|
// TIN
|
|
81
81
|
if (u.includes("TIN") || u.includes("TAX IDENTIFICATION"))
|
|
82
|
-
return { icon: "
|
|
82
|
+
return { icon: "🧾", desc: "Enter your Tax Identification Number (TIN)", requiresBack: false };
|
|
83
83
|
// CAC / Company registration
|
|
84
84
|
if (u.includes("CAC") || u.includes("CORPORATE AFFAIRS") || u.includes("COMPANY"))
|
|
85
|
-
return { icon: "
|
|
85
|
+
return { icon: "🏢", desc: advanced ? "Enter RC number, company name & type" : "Enter your company registration number", requiresBack: false };
|
|
86
86
|
// Voter / PVC
|
|
87
87
|
if (u.includes("VOTER") || u.includes("PVC")) {
|
|
88
88
|
const requiresBack = requiresBackSide(typeName);
|
|
89
89
|
return {
|
|
90
|
-
icon: "
|
|
90
|
+
icon: "🗳️",
|
|
91
91
|
desc: isDocumentFlow
|
|
92
92
|
? `Capture or upload your voter's card${requiresBack ? " (front & back)" : ""}`
|
|
93
93
|
: "Enter your voter ID number",
|
|
@@ -96,12 +96,12 @@ function resolveIdTypeInfo(typeName, isDocumentFlow) {
|
|
|
96
96
|
}
|
|
97
97
|
// Passport
|
|
98
98
|
if (u.includes("PASSPORT"))
|
|
99
|
-
return { icon: "
|
|
99
|
+
return { icon: "📘", desc: isDocumentFlow ? "Capture or upload your passport" : "Enter your passport number", requiresBack: false };
|
|
100
100
|
// Driver's license
|
|
101
101
|
if (u.includes("DRIVER") || u.includes("LICENSE") || u.includes("LICENCE")) {
|
|
102
102
|
const requiresBack = requiresBackSide(typeName);
|
|
103
103
|
return {
|
|
104
|
-
icon: "
|
|
104
|
+
icon: "🚗",
|
|
105
105
|
desc: isDocumentFlow
|
|
106
106
|
? `Capture or upload your driver's license${requiresBack ? " (front & back)" : ""}`
|
|
107
107
|
: "Enter your driver's license number",
|
|
@@ -112,7 +112,7 @@ function resolveIdTypeInfo(typeName, isDocumentFlow) {
|
|
|
112
112
|
if (u.includes("NATIONAL ID") || (u.includes("NATIONAL") && u.includes("CARD"))) {
|
|
113
113
|
const requiresBack = requiresBackSide(typeName);
|
|
114
114
|
return {
|
|
115
|
-
icon: "
|
|
115
|
+
icon: "🪪",
|
|
116
116
|
desc: isDocumentFlow
|
|
117
117
|
? `Capture or upload your ID card${requiresBack ? " (front & back)" : ""}`
|
|
118
118
|
: "Enter your National ID number",
|
|
@@ -121,15 +121,15 @@ function resolveIdTypeInfo(typeName, isDocumentFlow) {
|
|
|
121
121
|
}
|
|
122
122
|
// Face liveness / comparison
|
|
123
123
|
if (u.includes("LIVENESS") || u.includes("FACE COMPARISON") || u.includes("FACE MATCH"))
|
|
124
|
-
return { icon: "
|
|
124
|
+
return { icon: "🤳", desc: "We'll capture a short selfie video to verify you", requiresBack: false };
|
|
125
125
|
return {
|
|
126
|
-
icon: isDocumentFlow ? "
|
|
126
|
+
icon: isDocumentFlow ? "📄" : "🆔",
|
|
127
127
|
desc: isDocumentFlow ? "Capture or upload your document" : "Enter your ID number",
|
|
128
128
|
requiresBack: false,
|
|
129
129
|
};
|
|
130
130
|
}
|
|
131
|
-
//
|
|
132
|
-
//
|
|
131
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
132
|
+
// ── Widget-side message sanitizer ─────────────────────────────────────────────
|
|
133
133
|
// Strips anything technical before showing text to the end user.
|
|
134
134
|
// Keeps messages that are already written plainly in natural language.
|
|
135
135
|
function _sanitizeUserMessage(raw) {
|
|
@@ -158,7 +158,7 @@ function _sanitizeUserMessage(raw) {
|
|
|
158
158
|
return null;
|
|
159
159
|
return raw.trim();
|
|
160
160
|
}
|
|
161
|
-
//
|
|
161
|
+
// ── B8: OCR date string normaliser ───────────────────────────────────────────
|
|
162
162
|
// OCR providers return dates in wildly different formats. new Date(str) is
|
|
163
163
|
// locale-dependent for anything other than ISO 8601 (YYYY-MM-DD), so we
|
|
164
164
|
// attempt a set of known patterns before falling back to the native parser.
|
|
@@ -166,14 +166,14 @@ function _parseOcrDate(raw) {
|
|
|
166
166
|
if (!raw)
|
|
167
167
|
return null;
|
|
168
168
|
const s = raw.trim();
|
|
169
|
-
// ISO 8601 or near-ISO
|
|
169
|
+
// ISO 8601 or near-ISO — try first, always unambiguous
|
|
170
170
|
if (/^\d{4}-\d{2}-\d{2}/.test(s))
|
|
171
171
|
return new Date(s.slice(0, 10));
|
|
172
172
|
// DD/MM/YYYY or DD-MM-YYYY
|
|
173
173
|
const dmy = s.match(/^(\d{2})[\/\-](\d{2})[\/\-](\d{4})/);
|
|
174
174
|
if (dmy)
|
|
175
175
|
return new Date(`${dmy[3]}-${dmy[2]}-${dmy[1]}`);
|
|
176
|
-
// MM/DD/YYYY (US format
|
|
176
|
+
// MM/DD/YYYY (US format — less common from OCR but possible)
|
|
177
177
|
const mdy = s.match(/^(\d{2})\/(\d{2})\/(\d{4})/);
|
|
178
178
|
if (mdy) {
|
|
179
179
|
// Heuristic: if "month" > 12 it must be day-first
|
|
@@ -189,7 +189,7 @@ function _parseOcrDate(raw) {
|
|
|
189
189
|
const d = new Date(s);
|
|
190
190
|
return isNaN(d.getTime()) ? null : d;
|
|
191
191
|
}
|
|
192
|
-
//
|
|
192
|
+
// ── W12: Image compression before document upload ────────────────────────────
|
|
193
193
|
async function _compressImage(blob, maxWidth = 1600, quality = 0.85) {
|
|
194
194
|
try {
|
|
195
195
|
const bmp = await createImageBitmap(blob);
|
|
@@ -207,7 +207,7 @@ async function _compressImage(blob, maxWidth = 1600, quality = 0.85) {
|
|
|
207
207
|
return blob; // fallback to original on any error
|
|
208
208
|
}
|
|
209
209
|
}
|
|
210
|
-
//
|
|
210
|
+
// ── Document type code → human label ─────────────────────────────────────────
|
|
211
211
|
function _formatDocType(code) {
|
|
212
212
|
const map = {
|
|
213
213
|
passport: "Passport",
|
|
@@ -228,7 +228,7 @@ function _formatDocType(code) {
|
|
|
228
228
|
return code.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
229
229
|
}
|
|
230
230
|
// SmartComplyFlow
|
|
231
|
-
//
|
|
231
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
232
232
|
// Add CSS animation keyframes for spin animations
|
|
233
233
|
const style = document.createElement('style');
|
|
234
234
|
style.textContent = `
|
|
@@ -270,7 +270,7 @@ class SmartComplyFlow {
|
|
|
270
270
|
this.livenessEntryId = null;
|
|
271
271
|
this.isDestroyed = false;
|
|
272
272
|
this.headerBackBtn = null;
|
|
273
|
-
// Retry safety
|
|
273
|
+
// Retry safety — max 3 confirmation rejections or liveness failures per session
|
|
274
274
|
this._confirmRetries = 0;
|
|
275
275
|
this._livenessFailures = 0;
|
|
276
276
|
// IVS result stored for confirm_identity screen
|
|
@@ -285,9 +285,9 @@ class SmartComplyFlow {
|
|
|
285
285
|
this._ocrExpirySoon = false;
|
|
286
286
|
// Camera pre-warm stream (created during confirmation screen, used in liveness)
|
|
287
287
|
this._prewarmedStream = null;
|
|
288
|
-
// OCR gate poll timer
|
|
288
|
+
// OCR gate poll timer — stored so it can be cancelled on navigation or close
|
|
289
289
|
this._ocrPollTimer = null;
|
|
290
|
-
// UI timeout from _startStatusPolling
|
|
290
|
+
// UI timeout from _startStatusPolling — stored so close() can cancel it
|
|
291
291
|
this._uiTimeoutId = null;
|
|
292
292
|
// Session expiry
|
|
293
293
|
this.sessionExpiryTimer = null;
|
|
@@ -357,7 +357,7 @@ class SmartComplyFlow {
|
|
|
357
357
|
try {
|
|
358
358
|
this.overlay?.remove();
|
|
359
359
|
}
|
|
360
|
-
catch (_) { /* extension interference
|
|
360
|
+
catch (_) { /* extension interference — safe to ignore */ }
|
|
361
361
|
this.overlay = null;
|
|
362
362
|
this.modal = null;
|
|
363
363
|
this.contentArea = null;
|
|
@@ -365,9 +365,9 @@ class SmartComplyFlow {
|
|
|
365
365
|
}
|
|
366
366
|
this.options.onClose?.();
|
|
367
367
|
}
|
|
368
|
-
//
|
|
368
|
+
// ─────────────────────────────────────────────────────────────────
|
|
369
369
|
// Initialization
|
|
370
|
-
//
|
|
370
|
+
// ─────────────────────────────────────────────────────────────────
|
|
371
371
|
mount() {
|
|
372
372
|
// Inject keyframes
|
|
373
373
|
this.injectStyles();
|
|
@@ -452,7 +452,7 @@ class SmartComplyFlow {
|
|
|
452
452
|
}
|
|
453
453
|
// Load config
|
|
454
454
|
this.sdkConfig = await this.sdk.initializeConfig();
|
|
455
|
-
// Apply theme from config
|
|
455
|
+
// Apply theme from config — use theme_hex to override primary colour if set
|
|
456
456
|
if (this.sdkConfig.theme) {
|
|
457
457
|
this.theme = (0, theme_1.getTheme)(this.sdkConfig.theme, this.sdkConfig.theme_hex || null);
|
|
458
458
|
this.applyTheme();
|
|
@@ -469,9 +469,9 @@ class SmartComplyFlow {
|
|
|
469
469
|
this.showErrorStep(err.message || "Failed to initialize. Please try again.");
|
|
470
470
|
}
|
|
471
471
|
}
|
|
472
|
-
//
|
|
472
|
+
// ─────────────────────────────────────────────────────────────────
|
|
473
473
|
// Step rendering
|
|
474
|
-
//
|
|
474
|
+
// ─────────────────────────────────────────────────────────────────
|
|
475
475
|
showStep(step) {
|
|
476
476
|
this.currentStep = step;
|
|
477
477
|
if (!this.contentArea)
|
|
@@ -482,7 +482,7 @@ class SmartComplyFlow {
|
|
|
482
482
|
this._ocrPollTimer = null;
|
|
483
483
|
}
|
|
484
484
|
// Release a pre-warmed camera stream if we're navigating anywhere other
|
|
485
|
-
// than liveness (the only step that consumes it)
|
|
485
|
+
// than liveness (the only step that consumes it) — otherwise "No, use a
|
|
486
486
|
// different ID" / "No, retake document" / header back-button leave the
|
|
487
487
|
// camera held open indefinitely with no consumer.
|
|
488
488
|
if (step !== "liveness" && this._prewarmedStream) {
|
|
@@ -539,7 +539,7 @@ class SmartComplyFlow {
|
|
|
539
539
|
this.renderDocumentConfirm(wrapper);
|
|
540
540
|
break;
|
|
541
541
|
case "processing":
|
|
542
|
-
// C1: processing step is dead code
|
|
542
|
+
// C1: processing step is dead code — flow now goes to 'done' after submit
|
|
543
543
|
break;
|
|
544
544
|
case "liveness":
|
|
545
545
|
this.renderLiveness(wrapper);
|
|
@@ -548,7 +548,7 @@ class SmartComplyFlow {
|
|
|
548
548
|
this.renderDone(wrapper);
|
|
549
549
|
break;
|
|
550
550
|
case "result":
|
|
551
|
-
// C2: result step is dead code
|
|
551
|
+
// C2: result step is dead code — showResult/renderResult removed
|
|
552
552
|
break;
|
|
553
553
|
case "error":
|
|
554
554
|
break;
|
|
@@ -560,7 +560,7 @@ class SmartComplyFlow {
|
|
|
560
560
|
wrapper.style.transform = "translateX(0)";
|
|
561
561
|
});
|
|
562
562
|
}
|
|
563
|
-
//
|
|
563
|
+
// ── Loading ─────────────────────────────────────────────────────
|
|
564
564
|
renderLoading(container) {
|
|
565
565
|
container.style.cssText += "display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:300px;gap:16px;";
|
|
566
566
|
const spinnerWrap = document.createElement("div");
|
|
@@ -591,7 +591,7 @@ class SmartComplyFlow {
|
|
|
591
591
|
sub.textContent = "Connecting to the verification service...";
|
|
592
592
|
container.appendChild(sub);
|
|
593
593
|
}
|
|
594
|
-
//
|
|
594
|
+
// ── Welcome ────────────────────────────────────────────────────
|
|
595
595
|
renderWelcome(container) {
|
|
596
596
|
container.style.cssText += "display:flex;flex-direction:column;align-items:center;text-align:center;gap:12px;";
|
|
597
597
|
const isDocFlow = this.isDocumentFlow();
|
|
@@ -606,7 +606,7 @@ class SmartComplyFlow {
|
|
|
606
606
|
background:linear-gradient(135deg,${this.theme.primary}22,${this.theme.primaryGlow});
|
|
607
607
|
border:1.5px solid ${this.theme.primary}33;
|
|
608
608
|
`;
|
|
609
|
-
iconWrap.textContent = isDocFlow ? "
|
|
609
|
+
iconWrap.textContent = isDocFlow ? "🪪" : "🛡️";
|
|
610
610
|
topRow.appendChild(iconWrap);
|
|
611
611
|
const badge = document.createElement("div");
|
|
612
612
|
badge.style.cssText = `
|
|
@@ -617,7 +617,7 @@ class SmartComplyFlow {
|
|
|
617
617
|
color:${this.theme.primary};
|
|
618
618
|
border:1px solid ${this.theme.primary}30;
|
|
619
619
|
`;
|
|
620
|
-
badge.textContent = isDocFlow ? "
|
|
620
|
+
badge.textContent = isDocFlow ? "📄 Document Verification" : "🔍 Data Verification";
|
|
621
621
|
topRow.appendChild(badge);
|
|
622
622
|
container.appendChild(topRow);
|
|
623
623
|
// Title + subtitle (compact)
|
|
@@ -644,17 +644,17 @@ class SmartComplyFlow {
|
|
|
644
644
|
: "You'll need your ID number and camera access. This takes about 2 minutes.";
|
|
645
645
|
titleWrap.appendChild(desc);
|
|
646
646
|
container.appendChild(titleWrap);
|
|
647
|
-
// Steps preview
|
|
647
|
+
// Steps preview — compact rows
|
|
648
648
|
const steps = isDocFlow
|
|
649
649
|
? [
|
|
650
|
-
{ icon: "
|
|
651
|
-
{ icon: "
|
|
652
|
-
{ icon: "
|
|
650
|
+
{ icon: "🪪", text: "Choose your document type", sub: "Passport, National ID, Driver License" },
|
|
651
|
+
{ icon: "📷", text: "Scan your document", sub: "Upload or capture a photo of your ID" },
|
|
652
|
+
{ icon: "🤳", text: "Face check", sub: "Quick selfie to confirm it's you" },
|
|
653
653
|
]
|
|
654
654
|
: [
|
|
655
|
-
{ icon: "
|
|
656
|
-
{ icon: "
|
|
657
|
-
{ icon: "
|
|
655
|
+
{ icon: "🆔", text: "Choose your ID type", sub: "NIN, BVN, Voter's Card and more" },
|
|
656
|
+
{ icon: "📝", text: "Enter your ID details", sub: "Exactly as shown on your ID" },
|
|
657
|
+
{ icon: "🤳", text: "Face check", sub: "Quick selfie to confirm it's you" },
|
|
658
658
|
];
|
|
659
659
|
const stepsWrap = document.createElement("div");
|
|
660
660
|
stepsWrap.style.cssText = `
|
|
@@ -695,21 +695,21 @@ class SmartComplyFlow {
|
|
|
695
695
|
});
|
|
696
696
|
container.appendChild(stepsWrap);
|
|
697
697
|
// CTA
|
|
698
|
-
const btn = this.createPrimaryButton("Get Started
|
|
698
|
+
const btn = this.createPrimaryButton("Get Started →");
|
|
699
699
|
btn.style.cssText += "margin-top:2px;";
|
|
700
700
|
btn.addEventListener("click", () => this.showStep("country"));
|
|
701
701
|
container.appendChild(btn);
|
|
702
|
-
// Trust badge
|
|
702
|
+
// Trust badge — "Powered by Adhere" already lives in the persistent
|
|
703
703
|
// modal footer below; repeating it here just cost vertical space.
|
|
704
704
|
const trust = document.createElement("div");
|
|
705
705
|
trust.style.cssText = `
|
|
706
706
|
display:flex;align-items:center;gap:6px;
|
|
707
707
|
color:${this.theme.textMuted};font-size:10.5px;
|
|
708
708
|
`;
|
|
709
|
-
trust.textContent = "
|
|
709
|
+
trust.textContent = "🔒 End-to-end encrypted";
|
|
710
710
|
container.appendChild(trust);
|
|
711
711
|
}
|
|
712
|
-
//
|
|
712
|
+
// ── Country Select ──────────────────────────────────────────────
|
|
713
713
|
renderCountrySelect(container) {
|
|
714
714
|
container.style.cssText += "display:flex;flex-direction:column;gap:14px;";
|
|
715
715
|
const channels = this.sdkConfig?.channels || {};
|
|
@@ -734,7 +734,7 @@ class SmartComplyFlow {
|
|
|
734
734
|
container.appendChild(card);
|
|
735
735
|
}
|
|
736
736
|
}
|
|
737
|
-
//
|
|
737
|
+
// ── ID Type Select ──────────────────────────────────────────────
|
|
738
738
|
renderIdTypeSelect(container) {
|
|
739
739
|
container.style.cssText += "display:flex;flex-direction:column;gap:14px;";
|
|
740
740
|
const globalDocFlow = this.isDocumentFlow();
|
|
@@ -762,9 +762,9 @@ class SmartComplyFlow {
|
|
|
762
762
|
// Add back side indicator to subtitle if needed
|
|
763
763
|
let subtitle = info.desc;
|
|
764
764
|
if (requiresBack && isThisDocFlow) {
|
|
765
|
-
subtitle += "
|
|
765
|
+
subtitle += " ⚠️ Both sides required";
|
|
766
766
|
}
|
|
767
|
-
// Clean display name
|
|
767
|
+
// Clean display name — strip "with face", "(NIN)", "(BVN)" from user-visible label
|
|
768
768
|
const displayName = typeName
|
|
769
769
|
.replace(/\s*with\s+face\s*/gi, ' ')
|
|
770
770
|
.replace(/\s*\(NIN\)\s*/gi, ' ')
|
|
@@ -801,7 +801,7 @@ class SmartComplyFlow {
|
|
|
801
801
|
container.appendChild(card);
|
|
802
802
|
}
|
|
803
803
|
}
|
|
804
|
-
//
|
|
804
|
+
// ── ID Input (data verification — dynamic fields) ─────────────
|
|
805
805
|
renderIdInput(container) {
|
|
806
806
|
container.style.cssText += "display:flex;flex-direction:column;gap:14px;";
|
|
807
807
|
const dataFields = this.selectedChannelFields.filter((f) => f.type !== "upload");
|
|
@@ -820,7 +820,7 @@ class SmartComplyFlow {
|
|
|
820
820
|
? `Enter your ${this.selectedIdTypeName || this.selectedIdType} details exactly as they appear on your ID.`
|
|
821
821
|
: "Fill in the fields below to continue.";
|
|
822
822
|
container.appendChild(subtitle);
|
|
823
|
-
// Edge case: no data fields configured
|
|
823
|
+
// Edge case: no data fields configured — show fallback and allow submission
|
|
824
824
|
if (dataFields.length === 0) {
|
|
825
825
|
const empty = document.createElement("div");
|
|
826
826
|
empty.style.cssText = `
|
|
@@ -839,13 +839,13 @@ class SmartComplyFlow {
|
|
|
839
839
|
{ match: "BVN", rule: { minLen: 11, maxLen: 11, numeric: true, hint: "BVN must be exactly 11 digits" } },
|
|
840
840
|
{ match: "NIN", rule: { minLen: 11, maxLen: 11, numeric: true, hint: "NIN must be exactly 11 digits" } },
|
|
841
841
|
{ match: "NATIONAL IDENTITY", rule: { minLen: 11, maxLen: 11, numeric: true, hint: "NIN must be exactly 11 digits" } },
|
|
842
|
-
{ match: "PASSPORT", rule: { minLen: 6, maxLen: 20, alphanumeric: true, hint: "Passport number must be 6
|
|
843
|
-
{ match: "DRIVER", rule: { minLen: 3, maxLen: 30, alphanumeric: true, hint: "Licence number must be 3
|
|
844
|
-
{ match: "VOTER", rule: { minLen: 6, maxLen: 30, alphanumeric: true, hint: "Voter ID must be 6
|
|
845
|
-
{ match: "GHANA", rule: { minLen: 8, maxLen: 20, alphanumeric: true, hint: "Ghana Card number must be 8
|
|
846
|
-
{ match: "SSNIT", rule: { minLen: 7, maxLen: 15, alphanumeric: true, hint: "SSNIT number must be 7
|
|
847
|
-
{ match: "TIN", rule: { minLen: 8, maxLen: 20, alphanumeric: true, hint: "TIN must be 8
|
|
848
|
-
{ match: "KRA", rule: { minLen: 10, maxLen: 12, alphanumeric: true, hint: "KRA PIN must be 10
|
|
842
|
+
{ match: "PASSPORT", rule: { minLen: 6, maxLen: 20, alphanumeric: true, hint: "Passport number must be 6–20 alphanumeric characters" } },
|
|
843
|
+
{ match: "DRIVER", rule: { minLen: 3, maxLen: 30, alphanumeric: true, hint: "Licence number must be 3–30 alphanumeric characters" } },
|
|
844
|
+
{ match: "VOTER", rule: { minLen: 6, maxLen: 30, alphanumeric: true, hint: "Voter ID must be 6–30 alphanumeric characters" } },
|
|
845
|
+
{ match: "GHANA", rule: { minLen: 8, maxLen: 20, alphanumeric: true, hint: "Ghana Card number must be 8–20 alphanumeric characters" } },
|
|
846
|
+
{ match: "SSNIT", rule: { minLen: 7, maxLen: 15, alphanumeric: true, hint: "SSNIT number must be 7–15 alphanumeric characters" } },
|
|
847
|
+
{ match: "TIN", rule: { minLen: 8, maxLen: 20, alphanumeric: true, hint: "TIN must be 8–20 characters" } },
|
|
848
|
+
{ match: "KRA", rule: { minLen: 10, maxLen: 12, alphanumeric: true, hint: "KRA PIN must be 10–12 characters" } },
|
|
849
849
|
];
|
|
850
850
|
function getFieldRule(label) {
|
|
851
851
|
const upper = label.toUpperCase();
|
|
@@ -873,7 +873,7 @@ class SmartComplyFlow {
|
|
|
873
873
|
// Track input elements for validation
|
|
874
874
|
const inputEls = [];
|
|
875
875
|
for (const field of dataFields) {
|
|
876
|
-
// Send the raw label as the payload key
|
|
876
|
+
// Send the raw label as the payload key — the backend's
|
|
877
877
|
// _SDK_FIELD_KEY_MAP looks up the exact label text (e.g. "National ID",
|
|
878
878
|
// "CAC Number") to remap it to the canonical IVS key. Pre-normalizing
|
|
879
879
|
// it here (as this used to do) produced keys like "national_id" that
|
|
@@ -923,7 +923,7 @@ class SmartComplyFlow {
|
|
|
923
923
|
inputEls.push({ field, el: select });
|
|
924
924
|
}
|
|
925
925
|
else if (field.type === "image" || field.type === "file") {
|
|
926
|
-
// File upload field
|
|
926
|
+
// File upload field — converts to base64 for API submission
|
|
927
927
|
const fileInput = document.createElement("input");
|
|
928
928
|
fileInput.type = "file";
|
|
929
929
|
fileInput.accept = "image/*";
|
|
@@ -1073,14 +1073,14 @@ class SmartComplyFlow {
|
|
|
1073
1073
|
this._ivsName = [nd.first_name || nd.firstname || "", nd.last_name || nd.lastname || nd.surname || ""]
|
|
1074
1074
|
.map(s => String(s || "").trim()).filter(Boolean).join(" ");
|
|
1075
1075
|
this._ivsGender = nd.gender || "";
|
|
1076
|
-
// W4: If IVS returned no name, there is nothing for the user to confirm
|
|
1076
|
+
// W4: If IVS returned no name, there is nothing for the user to confirm —
|
|
1077
1077
|
// auto-proceed straight to Stage 2 (liveness) instead of showing a
|
|
1078
1078
|
// confirmation screen with no identity details to check.
|
|
1079
1079
|
if (!this._ivsName) {
|
|
1080
1080
|
this.showStep("liveness");
|
|
1081
1081
|
return;
|
|
1082
1082
|
}
|
|
1083
|
-
// Route to confirmation screen
|
|
1083
|
+
// Route to confirmation screen — user must confirm before liveness
|
|
1084
1084
|
this.showStep("confirm_identity");
|
|
1085
1085
|
}
|
|
1086
1086
|
else {
|
|
@@ -1092,10 +1092,10 @@ class SmartComplyFlow {
|
|
|
1092
1092
|
}
|
|
1093
1093
|
}
|
|
1094
1094
|
catch (err) {
|
|
1095
|
-
// Show a single plain-language error
|
|
1095
|
+
// Show a single plain-language error — never raw field:message format
|
|
1096
1096
|
const fieldErrors = err.data?.errors;
|
|
1097
1097
|
if (Array.isArray(fieldErrors) && fieldErrors.length > 0) {
|
|
1098
|
-
// Pick the first meaningful error message
|
|
1098
|
+
// Pick the first meaningful error message — strip field prefix
|
|
1099
1099
|
const firstMsg = fieldErrors[0]?.message || fieldErrors[0]?.detail || "";
|
|
1100
1100
|
errorEl.textContent = _sanitizeUserMessage(firstMsg)
|
|
1101
1101
|
|| "Please check your details and try again.";
|
|
@@ -1126,7 +1126,7 @@ class SmartComplyFlow {
|
|
|
1126
1126
|
setTimeout(() => firstInput.focus(), 350);
|
|
1127
1127
|
}
|
|
1128
1128
|
}
|
|
1129
|
-
//
|
|
1129
|
+
// ── Document Capture ────────────────────────────────────────────
|
|
1130
1130
|
renderDocumentCapture(container) {
|
|
1131
1131
|
container.style.cssText += "display:flex;flex-direction:column;gap:14px;";
|
|
1132
1132
|
// Strip "with face", "(NIN)", "(BVN)" noise from the user-visible title
|
|
@@ -1167,7 +1167,7 @@ class SmartComplyFlow {
|
|
|
1167
1167
|
this.showErrorStep(err.message || "Document capture failed. Please try again.");
|
|
1168
1168
|
});
|
|
1169
1169
|
}
|
|
1170
|
-
//
|
|
1170
|
+
// ── W3: Identity Confirmation (data verification) ───────────────
|
|
1171
1171
|
renderConfirmIdentity(container) {
|
|
1172
1172
|
container.style.cssText += "display:flex;flex-direction:column;align-items:center;text-align:center;gap:16px;padding:8px 0;";
|
|
1173
1173
|
// W14: Check retry limit before showing screen
|
|
@@ -1179,7 +1179,7 @@ class SmartComplyFlow {
|
|
|
1179
1179
|
this._prewarmCamera();
|
|
1180
1180
|
const iconWrap = document.createElement("div");
|
|
1181
1181
|
iconWrap.style.cssText = `width:64px;height:64px;border-radius:50%;background:${this.theme.primary}15;border:2px solid ${this.theme.primary}30;display:flex;align-items:center;justify-content:center;font-size:28px;`;
|
|
1182
|
-
iconWrap.textContent = "
|
|
1182
|
+
iconWrap.textContent = "🛡️";
|
|
1183
1183
|
container.appendChild(iconWrap);
|
|
1184
1184
|
const title = document.createElement("h2");
|
|
1185
1185
|
title.style.cssText = `color:${this.theme.text};font-size:18px;font-weight:700;margin:0;`;
|
|
@@ -1218,7 +1218,7 @@ class SmartComplyFlow {
|
|
|
1218
1218
|
sub.textContent = "Your details have been confirmed. Please continue to the face check.";
|
|
1219
1219
|
container.appendChild(sub);
|
|
1220
1220
|
}
|
|
1221
|
-
const yesBtn = this.createPrimaryButton("Yes, that's me
|
|
1221
|
+
const yesBtn = this.createPrimaryButton("Yes, that's me — continue");
|
|
1222
1222
|
yesBtn.addEventListener("click", () => {
|
|
1223
1223
|
// B5: Reset retry counter on successful confirmation
|
|
1224
1224
|
this._confirmRetries = 0;
|
|
@@ -1236,7 +1236,7 @@ class SmartComplyFlow {
|
|
|
1236
1236
|
});
|
|
1237
1237
|
container.appendChild(noBtn);
|
|
1238
1238
|
}
|
|
1239
|
-
//
|
|
1239
|
+
// ── W4: Document Confirmation (document verification) ────────────
|
|
1240
1240
|
renderDocumentConfirm(container) {
|
|
1241
1241
|
container.style.cssText += "display:flex;flex-direction:column;align-items:center;text-align:center;gap:14px;padding:8px 0;";
|
|
1242
1242
|
// W14: Check retry limit
|
|
@@ -1248,7 +1248,7 @@ class SmartComplyFlow {
|
|
|
1248
1248
|
this._prewarmCamera();
|
|
1249
1249
|
const iconWrap = document.createElement("div");
|
|
1250
1250
|
iconWrap.style.cssText = `width:64px;height:64px;border-radius:50%;background:${this.theme.primary}15;border:2px solid ${this.theme.primary}30;display:flex;align-items:center;justify-content:center;font-size:28px;`;
|
|
1251
|
-
iconWrap.textContent = "
|
|
1251
|
+
iconWrap.textContent = "📄";
|
|
1252
1252
|
container.appendChild(iconWrap);
|
|
1253
1253
|
const title = document.createElement("h2");
|
|
1254
1254
|
title.style.cssText = `color:${this.theme.text};font-size:18px;font-weight:700;margin:0;`;
|
|
@@ -1273,10 +1273,10 @@ class SmartComplyFlow {
|
|
|
1273
1273
|
if (this._ocrExpirySoon && !this._ocrIsExpired) {
|
|
1274
1274
|
const warn = document.createElement("div");
|
|
1275
1275
|
warn.style.cssText = `width:100%;padding:10px 14px;border-radius:10px;background:${this.theme.warning}18;border:1px solid ${this.theme.warning}40;color:${this.theme.warning};font-size:12px;text-align:left;line-height:1.5;`;
|
|
1276
|
-
warn.textContent = "
|
|
1276
|
+
warn.textContent = "⚠️ Your document expires soon. Some services may not accept it, but you can still continue.";
|
|
1277
1277
|
container.appendChild(warn);
|
|
1278
1278
|
}
|
|
1279
|
-
// Details card
|
|
1279
|
+
// Details card — show whatever OCR returned, never block on missing fields
|
|
1280
1280
|
const card = document.createElement("div");
|
|
1281
1281
|
card.style.cssText = `width:100%;padding:14px 16px;border-radius:14px;background:${this.theme.cardBg};border:1.5px solid ${this.theme.border};text-align:left;display:flex;flex-direction:column;gap:10px;`;
|
|
1282
1282
|
const docRows = [];
|
|
@@ -1334,7 +1334,7 @@ class SmartComplyFlow {
|
|
|
1334
1334
|
});
|
|
1335
1335
|
container.appendChild(noBtn);
|
|
1336
1336
|
}
|
|
1337
|
-
//
|
|
1337
|
+
// ── W6+W7+W8: OCR Gate (document verification) ───────────────────
|
|
1338
1338
|
renderOcrGate(container) {
|
|
1339
1339
|
container.style.cssText += "display:flex;flex-direction:column;align-items:center;gap:14px;padding:20px 0;text-align:center;";
|
|
1340
1340
|
const spinnerWrap = document.createElement("div");
|
|
@@ -1343,18 +1343,18 @@ class SmartComplyFlow {
|
|
|
1343
1343
|
ring.style.cssText = `position:absolute;inset:0;border-radius:50%;border:3px solid ${this.theme.border};border-top-color:${this.theme.primary};animation:sc-spin 0.9s linear infinite;`;
|
|
1344
1344
|
const centre = document.createElement("div");
|
|
1345
1345
|
centre.style.cssText = "position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:20px;";
|
|
1346
|
-
centre.textContent = "
|
|
1346
|
+
centre.textContent = "📄";
|
|
1347
1347
|
spinnerWrap.appendChild(ring);
|
|
1348
1348
|
spinnerWrap.appendChild(centre);
|
|
1349
1349
|
container.appendChild(spinnerWrap);
|
|
1350
1350
|
const statusEl = document.createElement("div");
|
|
1351
1351
|
statusEl.setAttribute("aria-live", "polite");
|
|
1352
1352
|
statusEl.style.cssText = `color:${this.theme.text};font-size:15px;font-weight:600;`;
|
|
1353
|
-
statusEl.textContent = "Reading your document
|
|
1353
|
+
statusEl.textContent = "Reading your document…";
|
|
1354
1354
|
container.appendChild(statusEl);
|
|
1355
1355
|
const subEl = document.createElement("div");
|
|
1356
1356
|
subEl.style.cssText = `color:${this.theme.textSecondary};font-size:12px;max-width:260px;line-height:1.5;`;
|
|
1357
|
-
subEl.textContent = "This usually takes 5
|
|
1357
|
+
subEl.textContent = "This usually takes 5–15 seconds. Please wait.";
|
|
1358
1358
|
container.appendChild(subEl);
|
|
1359
1359
|
if (!this.livenessEntryId) {
|
|
1360
1360
|
this.showErrorStep("Document upload failed. Please try again.");
|
|
@@ -1369,19 +1369,19 @@ class SmartComplyFlow {
|
|
|
1369
1369
|
polls++;
|
|
1370
1370
|
try {
|
|
1371
1371
|
const result = await this.sdk.liveness.pollResult(entryId);
|
|
1372
|
-
// W7: Expired document
|
|
1372
|
+
// W7: Expired document — stop, do not proceed to liveness
|
|
1373
1373
|
if (result.is_expired === true) {
|
|
1374
1374
|
container.innerHTML = "";
|
|
1375
1375
|
this._renderExpiredDocument(container);
|
|
1376
1376
|
return;
|
|
1377
1377
|
}
|
|
1378
|
-
// W8: OCR quality fail
|
|
1378
|
+
// W8: OCR quality fail — offer retake
|
|
1379
1379
|
if (result.ocr_quality_failed === true) {
|
|
1380
1380
|
container.innerHTML = "";
|
|
1381
1381
|
this._renderOcrQualityFail(container, result.ocr_rejection_reason);
|
|
1382
1382
|
return;
|
|
1383
1383
|
}
|
|
1384
|
-
// OCR complete
|
|
1384
|
+
// OCR complete — store data and show confirmation screen
|
|
1385
1385
|
const ocrDone = result.ocr_status === "done" || result.ocr_status === "ocr_complete"
|
|
1386
1386
|
|| result.ocr_name || result.ocr_document_number || result.ocr_expiry_date;
|
|
1387
1387
|
if (ocrDone) {
|
|
@@ -1390,7 +1390,7 @@ class SmartComplyFlow {
|
|
|
1390
1390
|
this._ocrExpiry = result.ocr_expiry_date || "";
|
|
1391
1391
|
this._ocrFaceImageUrl = result.ocr_face_image_url || "";
|
|
1392
1392
|
this._ocrIsExpired = result.is_expired === true;
|
|
1393
|
-
// B8: Normalise expiry date string before parsing
|
|
1393
|
+
// B8: Normalise expiry date string before parsing — OCR may return
|
|
1394
1394
|
// DD/MM/YYYY, DD-MM-YYYY, YYYY-MM-DD, or "12 May 2029" etc.
|
|
1395
1395
|
// new Date() alone is locale-dependent on non-ISO strings.
|
|
1396
1396
|
if (this._ocrExpiry) {
|
|
@@ -1404,14 +1404,14 @@ class SmartComplyFlow {
|
|
|
1404
1404
|
this.showStep("document_confirm");
|
|
1405
1405
|
return;
|
|
1406
1406
|
}
|
|
1407
|
-
// Still pending
|
|
1407
|
+
// Still pending — keep polling
|
|
1408
1408
|
if (polls < MAX_POLLS && !this.isDestroyed) {
|
|
1409
1409
|
this._ocrPollTimer = setTimeout(poll, 2000);
|
|
1410
1410
|
}
|
|
1411
1411
|
else if (!this.isDestroyed) {
|
|
1412
|
-
// Timeout
|
|
1413
|
-
statusEl.textContent = "Taking longer than usual
|
|
1414
|
-
subEl.textContent = "We'll continue
|
|
1412
|
+
// Timeout — proceed anyway with whatever we have
|
|
1413
|
+
statusEl.textContent = "Taking longer than usual…";
|
|
1414
|
+
subEl.textContent = "We'll continue — you can confirm your details on the next screen.";
|
|
1415
1415
|
this._ocrPollTimer = setTimeout(() => {
|
|
1416
1416
|
if (!this.isDestroyed)
|
|
1417
1417
|
this.showStep("document_confirm");
|
|
@@ -1427,14 +1427,14 @@ class SmartComplyFlow {
|
|
|
1427
1427
|
}
|
|
1428
1428
|
}
|
|
1429
1429
|
};
|
|
1430
|
-
// First poll after 3s
|
|
1430
|
+
// First poll after 3s — give OCR submission time to start
|
|
1431
1431
|
this._ocrPollTimer = setTimeout(poll, 3000);
|
|
1432
1432
|
}
|
|
1433
1433
|
_renderExpiredDocument(container) {
|
|
1434
1434
|
container.style.cssText += "display:flex;flex-direction:column;align-items:center;text-align:center;gap:14px;padding:20px 0;";
|
|
1435
1435
|
const icon = document.createElement("div");
|
|
1436
1436
|
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;`;
|
|
1437
|
-
icon.textContent = "
|
|
1437
|
+
icon.textContent = "📅";
|
|
1438
1438
|
container.appendChild(icon);
|
|
1439
1439
|
const t = document.createElement("h2");
|
|
1440
1440
|
t.style.cssText = `color:${this.theme.text};font-size:18px;font-weight:700;margin:0;`;
|
|
@@ -1460,7 +1460,7 @@ class SmartComplyFlow {
|
|
|
1460
1460
|
container.style.cssText += "display:flex;flex-direction:column;align-items:center;text-align:center;gap:14px;padding:20px 0;";
|
|
1461
1461
|
const icon = document.createElement("div");
|
|
1462
1462
|
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;`;
|
|
1463
|
-
icon.textContent = "
|
|
1463
|
+
icon.textContent = "🔍";
|
|
1464
1464
|
container.appendChild(icon);
|
|
1465
1465
|
const t = document.createElement("h2");
|
|
1466
1466
|
t.style.cssText = `color:${this.theme.text};font-size:18px;font-weight:700;margin:0;`;
|
|
@@ -1484,14 +1484,14 @@ class SmartComplyFlow {
|
|
|
1484
1484
|
});
|
|
1485
1485
|
container.appendChild(retryBtn);
|
|
1486
1486
|
}
|
|
1487
|
-
//
|
|
1487
|
+
// ── W11: You're all done (end-user final screen) ─────────────────
|
|
1488
1488
|
renderDone(container) {
|
|
1489
1489
|
container.style.cssText += "display:flex;flex-direction:column;align-items:center;text-align:center;gap:16px;padding:24px 0;";
|
|
1490
1490
|
container.setAttribute("role", "status");
|
|
1491
|
-
// B7: Fire onComplete immediately when the done screen renders
|
|
1491
|
+
// B7: Fire onComplete immediately when the done screen renders — the end
|
|
1492
1492
|
// user has completed their part regardless of whether they click Close.
|
|
1493
1493
|
// Firing here means the integrator always gets the event even if the user
|
|
1494
|
-
// closes the modal via the
|
|
1494
|
+
// closes the modal via the ✕ button instead of the Close button.
|
|
1495
1495
|
this.options.onComplete?.({
|
|
1496
1496
|
entryId: this.livenessEntryId || 0,
|
|
1497
1497
|
sessionId: this.sdk.sessionToken,
|
|
@@ -1501,35 +1501,34 @@ class SmartComplyFlow {
|
|
|
1501
1501
|
});
|
|
1502
1502
|
const iconWrap = document.createElement("div");
|
|
1503
1503
|
iconWrap.style.cssText = `width:80px;height:80px;border-radius:50%;background:${this.theme.successBg};border:3px solid ${this.theme.success};display:flex;align-items:center;justify-content:center;font-size:36px;font-weight:700;color:${this.theme.success};animation:sc-pop 0.4s ease;`;
|
|
1504
|
-
iconWrap.textContent = "
|
|
1504
|
+
iconWrap.textContent = "✓";
|
|
1505
1505
|
container.appendChild(iconWrap);
|
|
1506
1506
|
const title = document.createElement("h2");
|
|
1507
1507
|
title.style.cssText = `color:${this.theme.text};font-size:22px;font-weight:700;margin:0;`;
|
|
1508
1508
|
title.textContent = "You're all done!";
|
|
1509
1509
|
container.appendChild(title);
|
|
1510
|
-
const brandName = this.sdkConfig?.brand_name || "the service";
|
|
1511
1510
|
const desc = document.createElement("p");
|
|
1512
1511
|
desc.style.cssText = `color:${this.theme.textSecondary};font-size:14px;line-height:1.6;margin:0;max-width:300px;`;
|
|
1513
|
-
desc.textContent =
|
|
1512
|
+
desc.textContent = "Your verification is complete.";
|
|
1514
1513
|
container.appendChild(desc);
|
|
1515
1514
|
// Subtle info line
|
|
1516
1515
|
const info = document.createElement("div");
|
|
1517
1516
|
info.style.cssText = `color:${this.theme.textMuted};font-size:11px;display:flex;align-items:center;gap:4px;`;
|
|
1518
|
-
info.textContent = "
|
|
1517
|
+
info.textContent = "🔒 Your data is encrypted and secure";
|
|
1519
1518
|
container.appendChild(info);
|
|
1520
1519
|
const doneBtn = this.createPrimaryButton("Close");
|
|
1521
1520
|
doneBtn.addEventListener("click", () => {
|
|
1522
|
-
// onComplete already fired on render
|
|
1521
|
+
// onComplete already fired on render — just close the modal
|
|
1523
1522
|
this.close();
|
|
1524
1523
|
});
|
|
1525
1524
|
container.appendChild(doneBtn);
|
|
1526
1525
|
}
|
|
1527
|
-
//
|
|
1526
|
+
// ── W14: Retry limit reached ─────────────────────────────────────
|
|
1528
1527
|
_renderRetryLimitReached(container) {
|
|
1529
1528
|
container.style.cssText += "display:flex;flex-direction:column;align-items:center;text-align:center;gap:14px;padding:24px 0;";
|
|
1530
1529
|
const icon = document.createElement("div");
|
|
1531
1530
|
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;`;
|
|
1532
|
-
icon.textContent = "
|
|
1531
|
+
icon.textContent = "⚠️";
|
|
1533
1532
|
container.appendChild(icon);
|
|
1534
1533
|
const t = document.createElement("h2");
|
|
1535
1534
|
t.style.cssText = `color:${this.theme.text};font-size:18px;font-weight:700;margin:0;`;
|
|
@@ -1543,7 +1542,7 @@ class SmartComplyFlow {
|
|
|
1543
1542
|
closeBtn.addEventListener("click", () => { this.options.onError?.(new Error("Retry limit reached")); this.close(); });
|
|
1544
1543
|
container.appendChild(closeBtn);
|
|
1545
1544
|
}
|
|
1546
|
-
//
|
|
1545
|
+
// ── W13: Camera pre-warm ─────────────────────────────────────────
|
|
1547
1546
|
_prewarmCamera() {
|
|
1548
1547
|
if (this._prewarmedStream || this.isDestroyed)
|
|
1549
1548
|
return;
|
|
@@ -1563,11 +1562,11 @@ class SmartComplyFlow {
|
|
|
1563
1562
|
async processDocumentCaptureResult(result, captureContainer, container) {
|
|
1564
1563
|
// B2+P2: Compress both images once here. Do NOT assign documentBlob twice
|
|
1565
1564
|
// (old code set it to the raw blob first, then overwrote with compressed).
|
|
1566
|
-
// processDocumentBlob also ran _compressImage
|
|
1565
|
+
// processDocumentBlob also ran _compressImage — that second pass is removed
|
|
1567
1566
|
// below so compression only happens once.
|
|
1568
1567
|
this.documentBlob = await _compressImage(result.front, 1600, 0.85);
|
|
1569
1568
|
this.documentBackBlob = result.back ? await _compressImage(result.back, 1600, 0.85) : null;
|
|
1570
|
-
// Process the front image
|
|
1569
|
+
// Process the front image — pass the already-compressed blob
|
|
1571
1570
|
await this.processDocumentBlob(this.documentBlob, captureContainer, container);
|
|
1572
1571
|
}
|
|
1573
1572
|
/**
|
|
@@ -1578,7 +1577,7 @@ class SmartComplyFlow {
|
|
|
1578
1577
|
let retryCount = 0;
|
|
1579
1578
|
const attemptProcessing = async () => {
|
|
1580
1579
|
try {
|
|
1581
|
-
// Validate blob
|
|
1580
|
+
// Validate blob — compression already done in processDocumentCaptureResult (P2)
|
|
1582
1581
|
if (!blob || blob.size === 0) {
|
|
1583
1582
|
throw new Error("Failed to capture document. Please try again.");
|
|
1584
1583
|
}
|
|
@@ -1591,7 +1590,7 @@ class SmartComplyFlow {
|
|
|
1591
1590
|
<div style="display:flex;flex-direction:column;align-items:center;gap:14px;padding:28px 20px;">
|
|
1592
1591
|
<div style="position:relative;width:52px;height:52px;">
|
|
1593
1592
|
<div style="position:absolute;inset:0;border-radius:50%;border:3px solid ${this.theme.border};border-top-color:${this.theme.primary};animation:sc-spin 0.8s linear infinite;"></div>
|
|
1594
|
-
<div style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:20px;"
|
|
1593
|
+
<div style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:20px;">🔍</div>
|
|
1595
1594
|
</div>
|
|
1596
1595
|
<div style="color:${this.theme.text};font-size:14px;font-weight:600;">Scanning document...</div>
|
|
1597
1596
|
<div style="color:${this.theme.textSecondary};font-size:12px;text-align:center;max-width:200px;line-height:1.5;">Running OCR and verifying your identity</div>
|
|
@@ -1599,7 +1598,7 @@ class SmartComplyFlow {
|
|
|
1599
1598
|
// Create liveness entry with document.
|
|
1600
1599
|
// Use the first collected field value as identifier when available
|
|
1601
1600
|
// (document flow has no data-verification step, so collectedFields is
|
|
1602
|
-
// empty
|
|
1601
|
+
// empty — fall back to the selected ID type code so the webhook
|
|
1603
1602
|
// subject.identifier is at least human-meaningful, not a raw timestamp).
|
|
1604
1603
|
const identifier = Object.values(this.collectedFields)[0]
|
|
1605
1604
|
|| this.idNumber
|
|
@@ -1617,7 +1616,7 @@ class SmartComplyFlow {
|
|
|
1617
1616
|
identifier,
|
|
1618
1617
|
identifier_type: this.selectedIdType,
|
|
1619
1618
|
country,
|
|
1620
|
-
challenge_actions: ["BLINK"
|
|
1619
|
+
challenge_actions: ["BLINK"],
|
|
1621
1620
|
document: blob,
|
|
1622
1621
|
document_back: this.documentBackBlob || undefined,
|
|
1623
1622
|
device_type: deviceMeta.device_type,
|
|
@@ -1648,7 +1647,7 @@ class SmartComplyFlow {
|
|
|
1648
1647
|
}
|
|
1649
1648
|
}
|
|
1650
1649
|
this.livenessEntryId = createResult.id;
|
|
1651
|
-
// W6: Route to OCR gate
|
|
1650
|
+
// W6: Route to OCR gate — poll until OCR result is ready, validate
|
|
1652
1651
|
// expiry/quality, then show document_confirm before liveness starts.
|
|
1653
1652
|
this.showStep("ocr_gate");
|
|
1654
1653
|
}
|
|
@@ -1663,7 +1662,7 @@ class SmartComplyFlow {
|
|
|
1663
1662
|
<div style="display:flex;flex-direction:column;align-items:center;gap:16px;padding:28px 20px;text-align:center;">
|
|
1664
1663
|
<div style="position:relative;width:52px;height:52px;">
|
|
1665
1664
|
<div style="position:absolute;inset:0;border-radius:50%;border:3px solid ${this.theme.border};border-top-color:${this.theme.primary};animation:sc-spin 0.8s linear infinite;"></div>
|
|
1666
|
-
<div style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:20px;"
|
|
1665
|
+
<div style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:20px;">🔄</div>
|
|
1667
1666
|
</div>
|
|
1668
1667
|
<div style="color:${this.theme.text};font-size:14px;font-weight:600;">Retrying...</div>
|
|
1669
1668
|
<div style="color:${this.theme.textSecondary};font-size:12px;max-width:240px;line-height:1.5;">Attempt ${retryCount} of ${maxRetries}</div>
|
|
@@ -1682,7 +1681,7 @@ class SmartComplyFlow {
|
|
|
1682
1681
|
await attemptProcessing();
|
|
1683
1682
|
}
|
|
1684
1683
|
handleDocumentProcessingError(error, captureContainer) {
|
|
1685
|
-
// Map technical errors to user-friendly messages
|
|
1684
|
+
// Map technical errors to user-friendly messages — never pass raw error strings
|
|
1686
1685
|
let userMessage = "We could not verify your document. Please try again.";
|
|
1687
1686
|
let canRetry = true;
|
|
1688
1687
|
const raw = (error?.message || "").toLowerCase();
|
|
@@ -1709,13 +1708,13 @@ class SmartComplyFlow {
|
|
|
1709
1708
|
userMessage = "The document does not match the selected ID type. Please choose the correct ID type.";
|
|
1710
1709
|
}
|
|
1711
1710
|
if (captureContainer) {
|
|
1712
|
-
// Build with DOM
|
|
1711
|
+
// Build with DOM — never interpolate user-derived content into innerHTML
|
|
1713
1712
|
const wrap = document.createElement("div");
|
|
1714
1713
|
wrap.style.cssText = "display:flex;flex-direction:column;align-items:center;gap:16px;padding:28px 20px;text-align:center;";
|
|
1715
1714
|
wrap.setAttribute("role", "alert");
|
|
1716
1715
|
const icon = document.createElement("div");
|
|
1717
1716
|
icon.style.cssText = `width:56px;height:56px;border-radius:50%;background:${this.theme.errorBg};border:3px solid ${this.theme.error};display:flex;align-items:center;justify-content:center;font-size:26px;`;
|
|
1718
|
-
icon.textContent = "
|
|
1717
|
+
icon.textContent = "🔍";
|
|
1719
1718
|
wrap.appendChild(icon);
|
|
1720
1719
|
const titleEl = document.createElement("div");
|
|
1721
1720
|
titleEl.style.cssText = `color:${this.theme.text};font-size:14px;font-weight:700;`;
|
|
@@ -1740,7 +1739,7 @@ class SmartComplyFlow {
|
|
|
1740
1739
|
this.showErrorStep(userMessage);
|
|
1741
1740
|
}
|
|
1742
1741
|
}
|
|
1743
|
-
//
|
|
1742
|
+
// ── Liveness ────────────────────────────────────────────────────
|
|
1744
1743
|
renderLiveness(container) {
|
|
1745
1744
|
container.style.cssText += "display:flex;flex-direction:column;gap:14px;";
|
|
1746
1745
|
// W13: Check retry limit before starting another liveness attempt
|
|
@@ -1759,7 +1758,7 @@ class SmartComplyFlow {
|
|
|
1759
1758
|
livenessContainer.style.cssText = "margin:8px 0;width:100%;";
|
|
1760
1759
|
container.appendChild(livenessContainer);
|
|
1761
1760
|
// W9: For data verification, identity is already held by the backend via
|
|
1762
|
-
// identity_check_id
|
|
1761
|
+
// identity_check_id — the real ID number/type must not be re-sent, and the
|
|
1763
1762
|
// backend now accepts liveness/create without them when identity_check is
|
|
1764
1763
|
// set (see liveness.ts create(), which omits both fields entirely in that
|
|
1765
1764
|
// case). Only build a real identifier for the non-data-verification path.
|
|
@@ -1790,9 +1789,9 @@ class SmartComplyFlow {
|
|
|
1790
1789
|
: "liveness_only",
|
|
1791
1790
|
// B4: pass pre-warmed stream so camera.open() is skipped
|
|
1792
1791
|
prewarmedStream: this._prewarmedStream || undefined,
|
|
1793
|
-
}, ["BLINK"
|
|
1792
|
+
}, ["BLINK"], this.livenessEntryId || undefined)
|
|
1794
1793
|
.then((result) => {
|
|
1795
|
-
// W11: After submit, end user is done
|
|
1794
|
+
// W11: After submit, end user is done — show "You're all done" screen.
|
|
1796
1795
|
// Backend processing continues in background; client receives webhook.
|
|
1797
1796
|
// Track liveness failures for retry limit (W14).
|
|
1798
1797
|
if (result.status === "failed") {
|
|
@@ -1809,9 +1808,9 @@ class SmartComplyFlow {
|
|
|
1809
1808
|
});
|
|
1810
1809
|
}
|
|
1811
1810
|
renderResult(_container) {
|
|
1812
|
-
// C2: Dead code
|
|
1811
|
+
// C2: Dead code — flow routes to 'done' step, not 'result'. Kept as stub.
|
|
1813
1812
|
}
|
|
1814
|
-
//
|
|
1813
|
+
// ── Error ───────────────────────────────────────────────────────
|
|
1815
1814
|
showErrorStep(message) {
|
|
1816
1815
|
this.currentStep = "error";
|
|
1817
1816
|
if (!this.contentArea)
|
|
@@ -1830,13 +1829,13 @@ class SmartComplyFlow {
|
|
|
1830
1829
|
const isDocQuality = /blurry|blur|quality|glare|dark|bright|lighting/i.test(message);
|
|
1831
1830
|
const isExpiredDoc = /document.*expired|expired.*document/i.test(message);
|
|
1832
1831
|
const isFaceMatch = /face.*match|selfie.*match|does not match/i.test(message);
|
|
1833
|
-
const errorIcon = isCameraErr ? "
|
|
1834
|
-
: isNetworkErr ? "
|
|
1835
|
-
: isAuthErr ? "
|
|
1836
|
-
: isDocQuality ? "
|
|
1837
|
-
: isExpiredDoc ? "
|
|
1838
|
-
: isFaceMatch ? "
|
|
1839
|
-
: "
|
|
1832
|
+
const errorIcon = isCameraErr ? "📷"
|
|
1833
|
+
: isNetworkErr ? "📡"
|
|
1834
|
+
: isAuthErr ? "⏱"
|
|
1835
|
+
: isDocQuality ? "🔍"
|
|
1836
|
+
: isExpiredDoc ? "📅"
|
|
1837
|
+
: isFaceMatch ? "🤳"
|
|
1838
|
+
: "✕";
|
|
1840
1839
|
const errorTitle = isCameraErr ? "Camera Access Required"
|
|
1841
1840
|
: isNetworkErr ? "Connection Problem"
|
|
1842
1841
|
: isAuthErr ? "Session Expired"
|
|
@@ -1844,7 +1843,7 @@ class SmartComplyFlow {
|
|
|
1844
1843
|
: isExpiredDoc ? "Document Expired"
|
|
1845
1844
|
: isFaceMatch ? "Face Match Failed"
|
|
1846
1845
|
: "Verification Unsuccessful";
|
|
1847
|
-
// Map to user-facing guidance
|
|
1846
|
+
// Map to user-facing guidance — never show raw internal message
|
|
1848
1847
|
const userMessage = isCameraErr
|
|
1849
1848
|
? "Please allow camera access in your browser settings and try again."
|
|
1850
1849
|
: isNetworkErr
|
|
@@ -1891,9 +1890,9 @@ class SmartComplyFlow {
|
|
|
1891
1890
|
wrapper.appendChild(closeBtn);
|
|
1892
1891
|
this.contentArea.appendChild(wrapper);
|
|
1893
1892
|
}
|
|
1894
|
-
//
|
|
1893
|
+
// ─────────────────────────────────────────────────────────────────
|
|
1895
1894
|
// UI Helpers
|
|
1896
|
-
//
|
|
1895
|
+
// ─────────────────────────────────────────────────────────────────
|
|
1897
1896
|
createHeader() {
|
|
1898
1897
|
const header = document.createElement("div");
|
|
1899
1898
|
header.setAttribute("role", "banner");
|
|
@@ -1963,7 +1962,7 @@ class SmartComplyFlow {
|
|
|
1963
1962
|
header.appendChild(stepIndicator);
|
|
1964
1963
|
// Close button
|
|
1965
1964
|
const closeBtn = document.createElement("button");
|
|
1966
|
-
closeBtn.innerHTML = "
|
|
1965
|
+
closeBtn.innerHTML = "✕";
|
|
1967
1966
|
closeBtn.setAttribute("aria-label", "Close identity verification");
|
|
1968
1967
|
closeBtn.style.cssText = `
|
|
1969
1968
|
width:32px;height:32px;border-radius:8px;flex-shrink:0;
|
|
@@ -2044,7 +2043,7 @@ class SmartComplyFlow {
|
|
|
2044
2043
|
}
|
|
2045
2044
|
const arrow = document.createElement("span");
|
|
2046
2045
|
arrow.style.cssText = `color:${this.theme.textMuted};font-size:18px;flex-shrink:0;`;
|
|
2047
|
-
arrow.textContent = "
|
|
2046
|
+
arrow.textContent = "→";
|
|
2048
2047
|
card.appendChild(textWrap);
|
|
2049
2048
|
card.appendChild(arrow);
|
|
2050
2049
|
return card;
|
|
@@ -2089,7 +2088,7 @@ class SmartComplyFlow {
|
|
|
2089
2088
|
return btn;
|
|
2090
2089
|
}
|
|
2091
2090
|
isDocumentFlow() {
|
|
2092
|
-
// 1. Check verification_type
|
|
2091
|
+
// 1. Check verification_type — handle both string and array from backend
|
|
2093
2092
|
const vType = this.sdkConfig?.verification_type;
|
|
2094
2093
|
if (vType) {
|
|
2095
2094
|
const types = Array.isArray(vType) ? vType : [vType];
|
|
@@ -2098,7 +2097,7 @@ class SmartComplyFlow {
|
|
|
2098
2097
|
if (types.includes("data_verification"))
|
|
2099
2098
|
return false;
|
|
2100
2099
|
}
|
|
2101
|
-
// 2. Fallback: inspect channel fields
|
|
2100
|
+
// 2. Fallback: inspect channel fields — any upload field = document flow
|
|
2102
2101
|
const channels = this.sdkConfig?.channels || {};
|
|
2103
2102
|
const allFields = Object.values(channels)
|
|
2104
2103
|
.flat()
|
|
@@ -2109,7 +2108,7 @@ class SmartComplyFlow {
|
|
|
2109
2108
|
const isDocFlow = this.isDocumentFlow();
|
|
2110
2109
|
const countries = Object.keys(this.sdkConfig?.channels || {});
|
|
2111
2110
|
const singleCountry = countries.length <= 1;
|
|
2112
|
-
// Two-stage action steps
|
|
2111
|
+
// Two-stage action steps — Stage 1: validate identity, Stage 2: liveness
|
|
2113
2112
|
const stage1Steps = isDocFlow
|
|
2114
2113
|
? ["country", "id_type", "document_capture", "ocr_gate", "document_confirm"].filter(s => !(s === "country" && singleCountry))
|
|
2115
2114
|
: ["country", "id_type", "id_input", "confirm_identity"].filter(s => !(s === "country" && singleCountry));
|
|
@@ -2129,7 +2128,7 @@ class SmartComplyFlow {
|
|
|
2129
2128
|
this.stepLabel.textContent = "";
|
|
2130
2129
|
}
|
|
2131
2130
|
else if (this.currentStep === "done" || this.currentStep === "result") {
|
|
2132
|
-
this.stepLabel.textContent = "Complete
|
|
2131
|
+
this.stepLabel.textContent = "Complete ✓";
|
|
2133
2132
|
}
|
|
2134
2133
|
else if (stage2Steps.includes(this.currentStep)) {
|
|
2135
2134
|
this.stepLabel.textContent = "Stage 2 of 2: Face Check";
|
|
@@ -2141,7 +2140,7 @@ class SmartComplyFlow {
|
|
|
2141
2140
|
this.stepLabel.textContent = "";
|
|
2142
2141
|
}
|
|
2143
2142
|
}
|
|
2144
|
-
// W3: Stage dots
|
|
2143
|
+
// W3: Stage dots — dot 1 lights once Stage 1 is reached, dot 2 once Stage 2 is reached
|
|
2145
2144
|
if (this.stageDots) {
|
|
2146
2145
|
const dots = Array.from(this.stageDots.children);
|
|
2147
2146
|
const inStage1 = stage1Steps.includes(this.currentStep);
|
|
@@ -2154,7 +2153,7 @@ class SmartComplyFlow {
|
|
|
2154
2153
|
if (dots[1])
|
|
2155
2154
|
dots[1].style.background = stage2Active ? this.theme.primary : this.theme.border;
|
|
2156
2155
|
}
|
|
2157
|
-
// Back button
|
|
2156
|
+
// Back button — show for navigable steps only
|
|
2158
2157
|
if (this.headerBackBtn) {
|
|
2159
2158
|
const backTargets = {
|
|
2160
2159
|
id_type: () => this.showStep(countries.length > 1 ? "country" : "welcome"),
|