smartcomply-web-sdk 1.0.51 → 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 +0 -5
- package/dist/camera/ActionDetector.d.ts.map +1 -1
- package/dist/camera/ActionDetector.js +1 -12
- package/dist/camera/ActionDetector.js.map +1 -1
- package/dist/esm/camera/ActionDetector.d.ts +0 -5
- package/dist/esm/camera/ActionDetector.d.ts.map +1 -1
- package/dist/esm/camera/ActionDetector.js +1 -12
- package/dist/esm/camera/ActionDetector.js.map +1 -1
- package/dist/esm/flow/SmartComplyFlow.js +144 -144
- package/dist/esm/flow/SmartComplyFlow.js.map +1 -1
- package/dist/esm/modules/liveness/LivenessUI.d.ts +0 -2
- package/dist/esm/modules/liveness/LivenessUI.d.ts.map +1 -1
- package/dist/esm/modules/liveness/LivenessUI.js.map +1 -1
- package/dist/esm/modules/liveness/liveness.d.ts.map +1 -1
- package/dist/esm/modules/liveness/liveness.js +1 -3
- package/dist/esm/modules/liveness/liveness.js.map +1 -1
- package/dist/esm/utils/constants.d.ts +0 -2
- package/dist/esm/utils/constants.d.ts.map +1 -1
- package/dist/esm/utils/constants.js +0 -2
- package/dist/esm/utils/constants.js.map +1 -1
- package/dist/flow/SmartComplyFlow.js +144 -144
- package/dist/flow/SmartComplyFlow.js.map +1 -1
- package/dist/modules/liveness/LivenessUI.d.ts +0 -2
- package/dist/modules/liveness/LivenessUI.d.ts.map +1 -1
- package/dist/modules/liveness/LivenessUI.js.map +1 -1
- package/dist/modules/liveness/liveness.d.ts.map +1 -1
- package/dist/modules/liveness/liveness.js +1 -3
- package/dist/modules/liveness/liveness.js.map +1 -1
- package/dist/smartcomply.browser.js +86 -100
- package/dist/smartcomply.browser.js.map +2 -2
- package/dist/utils/constants.d.ts +0 -2
- package/dist/utils/constants.d.ts.map +1 -1
- package/dist/utils/constants.js +1 -3
- 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,7 +1501,7 @@ 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;`;
|
|
@@ -1514,21 +1514,21 @@ class SmartComplyFlow {
|
|
|
1514
1514
|
// Subtle info line
|
|
1515
1515
|
const info = document.createElement("div");
|
|
1516
1516
|
info.style.cssText = `color:${this.theme.textMuted};font-size:11px;display:flex;align-items:center;gap:4px;`;
|
|
1517
|
-
info.textContent = "
|
|
1517
|
+
info.textContent = "🔒 Your data is encrypted and secure";
|
|
1518
1518
|
container.appendChild(info);
|
|
1519
1519
|
const doneBtn = this.createPrimaryButton("Close");
|
|
1520
1520
|
doneBtn.addEventListener("click", () => {
|
|
1521
|
-
// onComplete already fired on render
|
|
1521
|
+
// onComplete already fired on render — just close the modal
|
|
1522
1522
|
this.close();
|
|
1523
1523
|
});
|
|
1524
1524
|
container.appendChild(doneBtn);
|
|
1525
1525
|
}
|
|
1526
|
-
//
|
|
1526
|
+
// ── W14: Retry limit reached ─────────────────────────────────────
|
|
1527
1527
|
_renderRetryLimitReached(container) {
|
|
1528
1528
|
container.style.cssText += "display:flex;flex-direction:column;align-items:center;text-align:center;gap:14px;padding:24px 0;";
|
|
1529
1529
|
const icon = document.createElement("div");
|
|
1530
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;`;
|
|
1531
|
-
icon.textContent = "
|
|
1531
|
+
icon.textContent = "⚠️";
|
|
1532
1532
|
container.appendChild(icon);
|
|
1533
1533
|
const t = document.createElement("h2");
|
|
1534
1534
|
t.style.cssText = `color:${this.theme.text};font-size:18px;font-weight:700;margin:0;`;
|
|
@@ -1542,7 +1542,7 @@ class SmartComplyFlow {
|
|
|
1542
1542
|
closeBtn.addEventListener("click", () => { this.options.onError?.(new Error("Retry limit reached")); this.close(); });
|
|
1543
1543
|
container.appendChild(closeBtn);
|
|
1544
1544
|
}
|
|
1545
|
-
//
|
|
1545
|
+
// ── W13: Camera pre-warm ─────────────────────────────────────────
|
|
1546
1546
|
_prewarmCamera() {
|
|
1547
1547
|
if (this._prewarmedStream || this.isDestroyed)
|
|
1548
1548
|
return;
|
|
@@ -1562,11 +1562,11 @@ class SmartComplyFlow {
|
|
|
1562
1562
|
async processDocumentCaptureResult(result, captureContainer, container) {
|
|
1563
1563
|
// B2+P2: Compress both images once here. Do NOT assign documentBlob twice
|
|
1564
1564
|
// (old code set it to the raw blob first, then overwrote with compressed).
|
|
1565
|
-
// processDocumentBlob also ran _compressImage
|
|
1565
|
+
// processDocumentBlob also ran _compressImage — that second pass is removed
|
|
1566
1566
|
// below so compression only happens once.
|
|
1567
1567
|
this.documentBlob = await _compressImage(result.front, 1600, 0.85);
|
|
1568
1568
|
this.documentBackBlob = result.back ? await _compressImage(result.back, 1600, 0.85) : null;
|
|
1569
|
-
// Process the front image
|
|
1569
|
+
// Process the front image — pass the already-compressed blob
|
|
1570
1570
|
await this.processDocumentBlob(this.documentBlob, captureContainer, container);
|
|
1571
1571
|
}
|
|
1572
1572
|
/**
|
|
@@ -1577,7 +1577,7 @@ class SmartComplyFlow {
|
|
|
1577
1577
|
let retryCount = 0;
|
|
1578
1578
|
const attemptProcessing = async () => {
|
|
1579
1579
|
try {
|
|
1580
|
-
// Validate blob
|
|
1580
|
+
// Validate blob — compression already done in processDocumentCaptureResult (P2)
|
|
1581
1581
|
if (!blob || blob.size === 0) {
|
|
1582
1582
|
throw new Error("Failed to capture document. Please try again.");
|
|
1583
1583
|
}
|
|
@@ -1590,7 +1590,7 @@ class SmartComplyFlow {
|
|
|
1590
1590
|
<div style="display:flex;flex-direction:column;align-items:center;gap:14px;padding:28px 20px;">
|
|
1591
1591
|
<div style="position:relative;width:52px;height:52px;">
|
|
1592
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>
|
|
1593
|
-
<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>
|
|
1594
1594
|
</div>
|
|
1595
1595
|
<div style="color:${this.theme.text};font-size:14px;font-weight:600;">Scanning document...</div>
|
|
1596
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>
|
|
@@ -1598,7 +1598,7 @@ class SmartComplyFlow {
|
|
|
1598
1598
|
// Create liveness entry with document.
|
|
1599
1599
|
// Use the first collected field value as identifier when available
|
|
1600
1600
|
// (document flow has no data-verification step, so collectedFields is
|
|
1601
|
-
// empty
|
|
1601
|
+
// empty — fall back to the selected ID type code so the webhook
|
|
1602
1602
|
// subject.identifier is at least human-meaningful, not a raw timestamp).
|
|
1603
1603
|
const identifier = Object.values(this.collectedFields)[0]
|
|
1604
1604
|
|| this.idNumber
|
|
@@ -1647,7 +1647,7 @@ class SmartComplyFlow {
|
|
|
1647
1647
|
}
|
|
1648
1648
|
}
|
|
1649
1649
|
this.livenessEntryId = createResult.id;
|
|
1650
|
-
// W6: Route to OCR gate
|
|
1650
|
+
// W6: Route to OCR gate — poll until OCR result is ready, validate
|
|
1651
1651
|
// expiry/quality, then show document_confirm before liveness starts.
|
|
1652
1652
|
this.showStep("ocr_gate");
|
|
1653
1653
|
}
|
|
@@ -1662,7 +1662,7 @@ class SmartComplyFlow {
|
|
|
1662
1662
|
<div style="display:flex;flex-direction:column;align-items:center;gap:16px;padding:28px 20px;text-align:center;">
|
|
1663
1663
|
<div style="position:relative;width:52px;height:52px;">
|
|
1664
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>
|
|
1665
|
-
<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>
|
|
1666
1666
|
</div>
|
|
1667
1667
|
<div style="color:${this.theme.text};font-size:14px;font-weight:600;">Retrying...</div>
|
|
1668
1668
|
<div style="color:${this.theme.textSecondary};font-size:12px;max-width:240px;line-height:1.5;">Attempt ${retryCount} of ${maxRetries}</div>
|
|
@@ -1681,7 +1681,7 @@ class SmartComplyFlow {
|
|
|
1681
1681
|
await attemptProcessing();
|
|
1682
1682
|
}
|
|
1683
1683
|
handleDocumentProcessingError(error, captureContainer) {
|
|
1684
|
-
// Map technical errors to user-friendly messages
|
|
1684
|
+
// Map technical errors to user-friendly messages — never pass raw error strings
|
|
1685
1685
|
let userMessage = "We could not verify your document. Please try again.";
|
|
1686
1686
|
let canRetry = true;
|
|
1687
1687
|
const raw = (error?.message || "").toLowerCase();
|
|
@@ -1708,13 +1708,13 @@ class SmartComplyFlow {
|
|
|
1708
1708
|
userMessage = "The document does not match the selected ID type. Please choose the correct ID type.";
|
|
1709
1709
|
}
|
|
1710
1710
|
if (captureContainer) {
|
|
1711
|
-
// Build with DOM
|
|
1711
|
+
// Build with DOM — never interpolate user-derived content into innerHTML
|
|
1712
1712
|
const wrap = document.createElement("div");
|
|
1713
1713
|
wrap.style.cssText = "display:flex;flex-direction:column;align-items:center;gap:16px;padding:28px 20px;text-align:center;";
|
|
1714
1714
|
wrap.setAttribute("role", "alert");
|
|
1715
1715
|
const icon = document.createElement("div");
|
|
1716
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;`;
|
|
1717
|
-
icon.textContent = "
|
|
1717
|
+
icon.textContent = "🔍";
|
|
1718
1718
|
wrap.appendChild(icon);
|
|
1719
1719
|
const titleEl = document.createElement("div");
|
|
1720
1720
|
titleEl.style.cssText = `color:${this.theme.text};font-size:14px;font-weight:700;`;
|
|
@@ -1739,7 +1739,7 @@ class SmartComplyFlow {
|
|
|
1739
1739
|
this.showErrorStep(userMessage);
|
|
1740
1740
|
}
|
|
1741
1741
|
}
|
|
1742
|
-
//
|
|
1742
|
+
// ── Liveness ────────────────────────────────────────────────────
|
|
1743
1743
|
renderLiveness(container) {
|
|
1744
1744
|
container.style.cssText += "display:flex;flex-direction:column;gap:14px;";
|
|
1745
1745
|
// W13: Check retry limit before starting another liveness attempt
|
|
@@ -1758,7 +1758,7 @@ class SmartComplyFlow {
|
|
|
1758
1758
|
livenessContainer.style.cssText = "margin:8px 0;width:100%;";
|
|
1759
1759
|
container.appendChild(livenessContainer);
|
|
1760
1760
|
// W9: For data verification, identity is already held by the backend via
|
|
1761
|
-
// identity_check_id
|
|
1761
|
+
// identity_check_id — the real ID number/type must not be re-sent, and the
|
|
1762
1762
|
// backend now accepts liveness/create without them when identity_check is
|
|
1763
1763
|
// set (see liveness.ts create(), which omits both fields entirely in that
|
|
1764
1764
|
// case). Only build a real identifier for the non-data-verification path.
|
|
@@ -1791,7 +1791,7 @@ class SmartComplyFlow {
|
|
|
1791
1791
|
prewarmedStream: this._prewarmedStream || undefined,
|
|
1792
1792
|
}, ["BLINK"], this.livenessEntryId || undefined)
|
|
1793
1793
|
.then((result) => {
|
|
1794
|
-
// W11: After submit, end user is done
|
|
1794
|
+
// W11: After submit, end user is done — show "You're all done" screen.
|
|
1795
1795
|
// Backend processing continues in background; client receives webhook.
|
|
1796
1796
|
// Track liveness failures for retry limit (W14).
|
|
1797
1797
|
if (result.status === "failed") {
|
|
@@ -1808,9 +1808,9 @@ class SmartComplyFlow {
|
|
|
1808
1808
|
});
|
|
1809
1809
|
}
|
|
1810
1810
|
renderResult(_container) {
|
|
1811
|
-
// C2: Dead code
|
|
1811
|
+
// C2: Dead code — flow routes to 'done' step, not 'result'. Kept as stub.
|
|
1812
1812
|
}
|
|
1813
|
-
//
|
|
1813
|
+
// ── Error ───────────────────────────────────────────────────────
|
|
1814
1814
|
showErrorStep(message) {
|
|
1815
1815
|
this.currentStep = "error";
|
|
1816
1816
|
if (!this.contentArea)
|
|
@@ -1829,13 +1829,13 @@ class SmartComplyFlow {
|
|
|
1829
1829
|
const isDocQuality = /blurry|blur|quality|glare|dark|bright|lighting/i.test(message);
|
|
1830
1830
|
const isExpiredDoc = /document.*expired|expired.*document/i.test(message);
|
|
1831
1831
|
const isFaceMatch = /face.*match|selfie.*match|does not match/i.test(message);
|
|
1832
|
-
const errorIcon = isCameraErr ? "
|
|
1833
|
-
: isNetworkErr ? "
|
|
1834
|
-
: isAuthErr ? "
|
|
1835
|
-
: isDocQuality ? "
|
|
1836
|
-
: isExpiredDoc ? "
|
|
1837
|
-
: isFaceMatch ? "
|
|
1838
|
-
: "
|
|
1832
|
+
const errorIcon = isCameraErr ? "📷"
|
|
1833
|
+
: isNetworkErr ? "📡"
|
|
1834
|
+
: isAuthErr ? "⏱"
|
|
1835
|
+
: isDocQuality ? "🔍"
|
|
1836
|
+
: isExpiredDoc ? "📅"
|
|
1837
|
+
: isFaceMatch ? "🤳"
|
|
1838
|
+
: "✕";
|
|
1839
1839
|
const errorTitle = isCameraErr ? "Camera Access Required"
|
|
1840
1840
|
: isNetworkErr ? "Connection Problem"
|
|
1841
1841
|
: isAuthErr ? "Session Expired"
|
|
@@ -1843,7 +1843,7 @@ class SmartComplyFlow {
|
|
|
1843
1843
|
: isExpiredDoc ? "Document Expired"
|
|
1844
1844
|
: isFaceMatch ? "Face Match Failed"
|
|
1845
1845
|
: "Verification Unsuccessful";
|
|
1846
|
-
// Map to user-facing guidance
|
|
1846
|
+
// Map to user-facing guidance — never show raw internal message
|
|
1847
1847
|
const userMessage = isCameraErr
|
|
1848
1848
|
? "Please allow camera access in your browser settings and try again."
|
|
1849
1849
|
: isNetworkErr
|
|
@@ -1890,9 +1890,9 @@ class SmartComplyFlow {
|
|
|
1890
1890
|
wrapper.appendChild(closeBtn);
|
|
1891
1891
|
this.contentArea.appendChild(wrapper);
|
|
1892
1892
|
}
|
|
1893
|
-
//
|
|
1893
|
+
// ─────────────────────────────────────────────────────────────────
|
|
1894
1894
|
// UI Helpers
|
|
1895
|
-
//
|
|
1895
|
+
// ─────────────────────────────────────────────────────────────────
|
|
1896
1896
|
createHeader() {
|
|
1897
1897
|
const header = document.createElement("div");
|
|
1898
1898
|
header.setAttribute("role", "banner");
|
|
@@ -1962,7 +1962,7 @@ class SmartComplyFlow {
|
|
|
1962
1962
|
header.appendChild(stepIndicator);
|
|
1963
1963
|
// Close button
|
|
1964
1964
|
const closeBtn = document.createElement("button");
|
|
1965
|
-
closeBtn.innerHTML = "
|
|
1965
|
+
closeBtn.innerHTML = "✕";
|
|
1966
1966
|
closeBtn.setAttribute("aria-label", "Close identity verification");
|
|
1967
1967
|
closeBtn.style.cssText = `
|
|
1968
1968
|
width:32px;height:32px;border-radius:8px;flex-shrink:0;
|
|
@@ -2043,7 +2043,7 @@ class SmartComplyFlow {
|
|
|
2043
2043
|
}
|
|
2044
2044
|
const arrow = document.createElement("span");
|
|
2045
2045
|
arrow.style.cssText = `color:${this.theme.textMuted};font-size:18px;flex-shrink:0;`;
|
|
2046
|
-
arrow.textContent = "
|
|
2046
|
+
arrow.textContent = "→";
|
|
2047
2047
|
card.appendChild(textWrap);
|
|
2048
2048
|
card.appendChild(arrow);
|
|
2049
2049
|
return card;
|
|
@@ -2088,7 +2088,7 @@ class SmartComplyFlow {
|
|
|
2088
2088
|
return btn;
|
|
2089
2089
|
}
|
|
2090
2090
|
isDocumentFlow() {
|
|
2091
|
-
// 1. Check verification_type
|
|
2091
|
+
// 1. Check verification_type — handle both string and array from backend
|
|
2092
2092
|
const vType = this.sdkConfig?.verification_type;
|
|
2093
2093
|
if (vType) {
|
|
2094
2094
|
const types = Array.isArray(vType) ? vType : [vType];
|
|
@@ -2097,7 +2097,7 @@ class SmartComplyFlow {
|
|
|
2097
2097
|
if (types.includes("data_verification"))
|
|
2098
2098
|
return false;
|
|
2099
2099
|
}
|
|
2100
|
-
// 2. Fallback: inspect channel fields
|
|
2100
|
+
// 2. Fallback: inspect channel fields — any upload field = document flow
|
|
2101
2101
|
const channels = this.sdkConfig?.channels || {};
|
|
2102
2102
|
const allFields = Object.values(channels)
|
|
2103
2103
|
.flat()
|
|
@@ -2108,7 +2108,7 @@ class SmartComplyFlow {
|
|
|
2108
2108
|
const isDocFlow = this.isDocumentFlow();
|
|
2109
2109
|
const countries = Object.keys(this.sdkConfig?.channels || {});
|
|
2110
2110
|
const singleCountry = countries.length <= 1;
|
|
2111
|
-
// Two-stage action steps
|
|
2111
|
+
// Two-stage action steps — Stage 1: validate identity, Stage 2: liveness
|
|
2112
2112
|
const stage1Steps = isDocFlow
|
|
2113
2113
|
? ["country", "id_type", "document_capture", "ocr_gate", "document_confirm"].filter(s => !(s === "country" && singleCountry))
|
|
2114
2114
|
: ["country", "id_type", "id_input", "confirm_identity"].filter(s => !(s === "country" && singleCountry));
|
|
@@ -2128,7 +2128,7 @@ class SmartComplyFlow {
|
|
|
2128
2128
|
this.stepLabel.textContent = "";
|
|
2129
2129
|
}
|
|
2130
2130
|
else if (this.currentStep === "done" || this.currentStep === "result") {
|
|
2131
|
-
this.stepLabel.textContent = "Complete
|
|
2131
|
+
this.stepLabel.textContent = "Complete ✓";
|
|
2132
2132
|
}
|
|
2133
2133
|
else if (stage2Steps.includes(this.currentStep)) {
|
|
2134
2134
|
this.stepLabel.textContent = "Stage 2 of 2: Face Check";
|
|
@@ -2140,7 +2140,7 @@ class SmartComplyFlow {
|
|
|
2140
2140
|
this.stepLabel.textContent = "";
|
|
2141
2141
|
}
|
|
2142
2142
|
}
|
|
2143
|
-
// W3: Stage dots
|
|
2143
|
+
// W3: Stage dots — dot 1 lights once Stage 1 is reached, dot 2 once Stage 2 is reached
|
|
2144
2144
|
if (this.stageDots) {
|
|
2145
2145
|
const dots = Array.from(this.stageDots.children);
|
|
2146
2146
|
const inStage1 = stage1Steps.includes(this.currentStep);
|
|
@@ -2153,7 +2153,7 @@ class SmartComplyFlow {
|
|
|
2153
2153
|
if (dots[1])
|
|
2154
2154
|
dots[1].style.background = stage2Active ? this.theme.primary : this.theme.border;
|
|
2155
2155
|
}
|
|
2156
|
-
// Back button
|
|
2156
|
+
// Back button — show for navigable steps only
|
|
2157
2157
|
if (this.headerBackBtn) {
|
|
2158
2158
|
const backTargets = {
|
|
2159
2159
|
id_type: () => this.showStep(countries.length > 1 ? "country" : "welcome"),
|