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
|
@@ -17,15 +17,15 @@ const STEP_ORDER = [
|
|
|
17
17
|
"result",
|
|
18
18
|
];
|
|
19
19
|
const COUNTRY_LABELS = {
|
|
20
|
-
nigeria: "
|
|
21
|
-
global: "
|
|
22
|
-
ghana: "
|
|
23
|
-
kenya: "
|
|
24
|
-
south_africa: "
|
|
25
|
-
united_states: "
|
|
26
|
-
united_kingdom: "
|
|
20
|
+
nigeria: "🇳🇬 Nigeria",
|
|
21
|
+
global: "🌍 Other Countries",
|
|
22
|
+
ghana: "🇬🇭 Ghana",
|
|
23
|
+
kenya: "🇰🇪 Kenya",
|
|
24
|
+
south_africa: "🇿🇦 South Africa",
|
|
25
|
+
united_states: "🇺🇸 United States",
|
|
26
|
+
united_kingdom: "🇬🇧 United Kingdom",
|
|
27
27
|
};
|
|
28
|
-
// Backend/admin data tags document-capture fields inconsistently
|
|
28
|
+
// Backend/admin data tags document-capture fields inconsistently — the field
|
|
29
29
|
// renderer (below) already treats "image" and "file" as file inputs alongside
|
|
30
30
|
// "upload", so flow-routing decisions must recognize the same set or a channel
|
|
31
31
|
// tagged "image"/"file" silently falls back to the number-entry screen.
|
|
@@ -35,7 +35,7 @@ function isUploadFieldType(type) {
|
|
|
35
35
|
function resolveIdTypeInfo(typeName, isDocumentFlow) {
|
|
36
36
|
const u = typeName.toUpperCase();
|
|
37
37
|
const advanced = u.includes("ADVANCED");
|
|
38
|
-
// Strip UI noise from display labels
|
|
38
|
+
// Strip UI noise from display labels — users don't need "with face" or "(NIN)" suffixes
|
|
39
39
|
const cleanName = typeName
|
|
40
40
|
.replace(/\s*with\s+face\s*/gi, ' ')
|
|
41
41
|
.replace(/\s*\(NIN\)\s*/gi, ' ')
|
|
@@ -50,41 +50,41 @@ function resolveIdTypeInfo(typeName, isDocumentFlow) {
|
|
|
50
50
|
t.includes("license") ||
|
|
51
51
|
t.includes("national id") ||
|
|
52
52
|
t.includes("national id card");
|
|
53
|
-
// Note: NIN document (nin_with_face, nin_slip) does NOT require back side
|
|
53
|
+
// Note: NIN document (nin_with_face, nin_slip) does NOT require back side —
|
|
54
54
|
// the NIN card is single-sided. Only the physical NIN slip has a back but
|
|
55
55
|
// the OCR pipeline only needs the front.
|
|
56
56
|
};
|
|
57
|
-
// BVN
|
|
57
|
+
// BVN — always 11 digits
|
|
58
58
|
if (u.includes("BVN") || u.includes("BANK VERIFICATION")) {
|
|
59
|
-
return { icon: "
|
|
59
|
+
return { icon: "🏦", desc: "Enter your 11-digit Bank Verification Number (BVN)", requiresBack: false };
|
|
60
60
|
}
|
|
61
|
-
// NIN
|
|
61
|
+
// NIN — always 11 digits (covers nin_with_face, NIN slip, etc.)
|
|
62
62
|
if (u.includes("NIN") || u.includes("NATIONAL IDENTITY NUMBER")) {
|
|
63
|
-
return { icon: "
|
|
63
|
+
return { icon: "🆔", desc: "Enter your 11-digit National Identification Number (NIN)", requiresBack: false };
|
|
64
64
|
}
|
|
65
65
|
// Ghana Card / Ghana National ID
|
|
66
66
|
if (u.includes("GHANA")) {
|
|
67
67
|
if (isDocumentFlow)
|
|
68
|
-
return { icon: "
|
|
69
|
-
return { icon: "
|
|
68
|
+
return { icon: "🪪", desc: "Capture or upload your Ghana Card", requiresBack: true };
|
|
69
|
+
return { icon: "🪪", desc: "Enter your Ghana Card number", requiresBack: false };
|
|
70
70
|
}
|
|
71
71
|
// SSNIT (Ghana social security)
|
|
72
72
|
if (u.includes("SSNIT"))
|
|
73
|
-
return { icon: "
|
|
73
|
+
return { icon: "🏛️", desc: "Enter your SSNIT number", requiresBack: false };
|
|
74
74
|
// Kenya National ID
|
|
75
75
|
if (u.includes("KENYA") || (u.includes("NATIONAL") && u.includes("KENYA")))
|
|
76
|
-
return { icon: "
|
|
76
|
+
return { icon: "🆔", desc: "Enter your Kenya National ID number", requiresBack: false };
|
|
77
77
|
// TIN
|
|
78
78
|
if (u.includes("TIN") || u.includes("TAX IDENTIFICATION"))
|
|
79
|
-
return { icon: "
|
|
79
|
+
return { icon: "🧾", desc: "Enter your Tax Identification Number (TIN)", requiresBack: false };
|
|
80
80
|
// CAC / Company registration
|
|
81
81
|
if (u.includes("CAC") || u.includes("CORPORATE AFFAIRS") || u.includes("COMPANY"))
|
|
82
|
-
return { icon: "
|
|
82
|
+
return { icon: "🏢", desc: advanced ? "Enter RC number, company name & type" : "Enter your company registration number", requiresBack: false };
|
|
83
83
|
// Voter / PVC
|
|
84
84
|
if (u.includes("VOTER") || u.includes("PVC")) {
|
|
85
85
|
const requiresBack = requiresBackSide(typeName);
|
|
86
86
|
return {
|
|
87
|
-
icon: "
|
|
87
|
+
icon: "🗳️",
|
|
88
88
|
desc: isDocumentFlow
|
|
89
89
|
? `Capture or upload your voter's card${requiresBack ? " (front & back)" : ""}`
|
|
90
90
|
: "Enter your voter ID number",
|
|
@@ -93,12 +93,12 @@ function resolveIdTypeInfo(typeName, isDocumentFlow) {
|
|
|
93
93
|
}
|
|
94
94
|
// Passport
|
|
95
95
|
if (u.includes("PASSPORT"))
|
|
96
|
-
return { icon: "
|
|
96
|
+
return { icon: "📘", desc: isDocumentFlow ? "Capture or upload your passport" : "Enter your passport number", requiresBack: false };
|
|
97
97
|
// Driver's license
|
|
98
98
|
if (u.includes("DRIVER") || u.includes("LICENSE") || u.includes("LICENCE")) {
|
|
99
99
|
const requiresBack = requiresBackSide(typeName);
|
|
100
100
|
return {
|
|
101
|
-
icon: "
|
|
101
|
+
icon: "🚗",
|
|
102
102
|
desc: isDocumentFlow
|
|
103
103
|
? `Capture or upload your driver's license${requiresBack ? " (front & back)" : ""}`
|
|
104
104
|
: "Enter your driver's license number",
|
|
@@ -109,7 +109,7 @@ function resolveIdTypeInfo(typeName, isDocumentFlow) {
|
|
|
109
109
|
if (u.includes("NATIONAL ID") || (u.includes("NATIONAL") && u.includes("CARD"))) {
|
|
110
110
|
const requiresBack = requiresBackSide(typeName);
|
|
111
111
|
return {
|
|
112
|
-
icon: "
|
|
112
|
+
icon: "🪪",
|
|
113
113
|
desc: isDocumentFlow
|
|
114
114
|
? `Capture or upload your ID card${requiresBack ? " (front & back)" : ""}`
|
|
115
115
|
: "Enter your National ID number",
|
|
@@ -118,15 +118,15 @@ function resolveIdTypeInfo(typeName, isDocumentFlow) {
|
|
|
118
118
|
}
|
|
119
119
|
// Face liveness / comparison
|
|
120
120
|
if (u.includes("LIVENESS") || u.includes("FACE COMPARISON") || u.includes("FACE MATCH"))
|
|
121
|
-
return { icon: "
|
|
121
|
+
return { icon: "🤳", desc: "We'll capture a short selfie video to verify you", requiresBack: false };
|
|
122
122
|
return {
|
|
123
|
-
icon: isDocumentFlow ? "
|
|
123
|
+
icon: isDocumentFlow ? "📄" : "🆔",
|
|
124
124
|
desc: isDocumentFlow ? "Capture or upload your document" : "Enter your ID number",
|
|
125
125
|
requiresBack: false,
|
|
126
126
|
};
|
|
127
127
|
}
|
|
128
|
-
//
|
|
129
|
-
//
|
|
128
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
129
|
+
// ── Widget-side message sanitizer ─────────────────────────────────────────────
|
|
130
130
|
// Strips anything technical before showing text to the end user.
|
|
131
131
|
// Keeps messages that are already written plainly in natural language.
|
|
132
132
|
function _sanitizeUserMessage(raw) {
|
|
@@ -155,7 +155,7 @@ function _sanitizeUserMessage(raw) {
|
|
|
155
155
|
return null;
|
|
156
156
|
return raw.trim();
|
|
157
157
|
}
|
|
158
|
-
//
|
|
158
|
+
// ── B8: OCR date string normaliser ───────────────────────────────────────────
|
|
159
159
|
// OCR providers return dates in wildly different formats. new Date(str) is
|
|
160
160
|
// locale-dependent for anything other than ISO 8601 (YYYY-MM-DD), so we
|
|
161
161
|
// attempt a set of known patterns before falling back to the native parser.
|
|
@@ -163,14 +163,14 @@ function _parseOcrDate(raw) {
|
|
|
163
163
|
if (!raw)
|
|
164
164
|
return null;
|
|
165
165
|
const s = raw.trim();
|
|
166
|
-
// ISO 8601 or near-ISO
|
|
166
|
+
// ISO 8601 or near-ISO — try first, always unambiguous
|
|
167
167
|
if (/^\d{4}-\d{2}-\d{2}/.test(s))
|
|
168
168
|
return new Date(s.slice(0, 10));
|
|
169
169
|
// DD/MM/YYYY or DD-MM-YYYY
|
|
170
170
|
const dmy = s.match(/^(\d{2})[\/\-](\d{2})[\/\-](\d{4})/);
|
|
171
171
|
if (dmy)
|
|
172
172
|
return new Date(`${dmy[3]}-${dmy[2]}-${dmy[1]}`);
|
|
173
|
-
// MM/DD/YYYY (US format
|
|
173
|
+
// MM/DD/YYYY (US format — less common from OCR but possible)
|
|
174
174
|
const mdy = s.match(/^(\d{2})\/(\d{2})\/(\d{4})/);
|
|
175
175
|
if (mdy) {
|
|
176
176
|
// Heuristic: if "month" > 12 it must be day-first
|
|
@@ -186,7 +186,7 @@ function _parseOcrDate(raw) {
|
|
|
186
186
|
const d = new Date(s);
|
|
187
187
|
return isNaN(d.getTime()) ? null : d;
|
|
188
188
|
}
|
|
189
|
-
//
|
|
189
|
+
// ── W12: Image compression before document upload ────────────────────────────
|
|
190
190
|
async function _compressImage(blob, maxWidth = 1600, quality = 0.85) {
|
|
191
191
|
try {
|
|
192
192
|
const bmp = await createImageBitmap(blob);
|
|
@@ -204,7 +204,7 @@ async function _compressImage(blob, maxWidth = 1600, quality = 0.85) {
|
|
|
204
204
|
return blob; // fallback to original on any error
|
|
205
205
|
}
|
|
206
206
|
}
|
|
207
|
-
//
|
|
207
|
+
// ── Document type code → human label ─────────────────────────────────────────
|
|
208
208
|
function _formatDocType(code) {
|
|
209
209
|
const map = {
|
|
210
210
|
passport: "Passport",
|
|
@@ -225,7 +225,7 @@ function _formatDocType(code) {
|
|
|
225
225
|
return code.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
226
226
|
}
|
|
227
227
|
// SmartComplyFlow
|
|
228
|
-
//
|
|
228
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
229
229
|
// Add CSS animation keyframes for spin animations
|
|
230
230
|
const style = document.createElement('style');
|
|
231
231
|
style.textContent = `
|
|
@@ -267,7 +267,7 @@ export class SmartComplyFlow {
|
|
|
267
267
|
this.livenessEntryId = null;
|
|
268
268
|
this.isDestroyed = false;
|
|
269
269
|
this.headerBackBtn = null;
|
|
270
|
-
// Retry safety
|
|
270
|
+
// Retry safety — max 3 confirmation rejections or liveness failures per session
|
|
271
271
|
this._confirmRetries = 0;
|
|
272
272
|
this._livenessFailures = 0;
|
|
273
273
|
// IVS result stored for confirm_identity screen
|
|
@@ -282,9 +282,9 @@ export class SmartComplyFlow {
|
|
|
282
282
|
this._ocrExpirySoon = false;
|
|
283
283
|
// Camera pre-warm stream (created during confirmation screen, used in liveness)
|
|
284
284
|
this._prewarmedStream = null;
|
|
285
|
-
// OCR gate poll timer
|
|
285
|
+
// OCR gate poll timer — stored so it can be cancelled on navigation or close
|
|
286
286
|
this._ocrPollTimer = null;
|
|
287
|
-
// UI timeout from _startStatusPolling
|
|
287
|
+
// UI timeout from _startStatusPolling — stored so close() can cancel it
|
|
288
288
|
this._uiTimeoutId = null;
|
|
289
289
|
// Session expiry
|
|
290
290
|
this.sessionExpiryTimer = null;
|
|
@@ -354,7 +354,7 @@ export class SmartComplyFlow {
|
|
|
354
354
|
try {
|
|
355
355
|
this.overlay?.remove();
|
|
356
356
|
}
|
|
357
|
-
catch (_) { /* extension interference
|
|
357
|
+
catch (_) { /* extension interference — safe to ignore */ }
|
|
358
358
|
this.overlay = null;
|
|
359
359
|
this.modal = null;
|
|
360
360
|
this.contentArea = null;
|
|
@@ -362,9 +362,9 @@ export class SmartComplyFlow {
|
|
|
362
362
|
}
|
|
363
363
|
this.options.onClose?.();
|
|
364
364
|
}
|
|
365
|
-
//
|
|
365
|
+
// ─────────────────────────────────────────────────────────────────
|
|
366
366
|
// Initialization
|
|
367
|
-
//
|
|
367
|
+
// ─────────────────────────────────────────────────────────────────
|
|
368
368
|
mount() {
|
|
369
369
|
// Inject keyframes
|
|
370
370
|
this.injectStyles();
|
|
@@ -449,7 +449,7 @@ export class SmartComplyFlow {
|
|
|
449
449
|
}
|
|
450
450
|
// Load config
|
|
451
451
|
this.sdkConfig = await this.sdk.initializeConfig();
|
|
452
|
-
// Apply theme from config
|
|
452
|
+
// Apply theme from config — use theme_hex to override primary colour if set
|
|
453
453
|
if (this.sdkConfig.theme) {
|
|
454
454
|
this.theme = getTheme(this.sdkConfig.theme, this.sdkConfig.theme_hex || null);
|
|
455
455
|
this.applyTheme();
|
|
@@ -466,9 +466,9 @@ export class SmartComplyFlow {
|
|
|
466
466
|
this.showErrorStep(err.message || "Failed to initialize. Please try again.");
|
|
467
467
|
}
|
|
468
468
|
}
|
|
469
|
-
//
|
|
469
|
+
// ─────────────────────────────────────────────────────────────────
|
|
470
470
|
// Step rendering
|
|
471
|
-
//
|
|
471
|
+
// ─────────────────────────────────────────────────────────────────
|
|
472
472
|
showStep(step) {
|
|
473
473
|
this.currentStep = step;
|
|
474
474
|
if (!this.contentArea)
|
|
@@ -479,7 +479,7 @@ export class SmartComplyFlow {
|
|
|
479
479
|
this._ocrPollTimer = null;
|
|
480
480
|
}
|
|
481
481
|
// Release a pre-warmed camera stream if we're navigating anywhere other
|
|
482
|
-
// than liveness (the only step that consumes it)
|
|
482
|
+
// than liveness (the only step that consumes it) — otherwise "No, use a
|
|
483
483
|
// different ID" / "No, retake document" / header back-button leave the
|
|
484
484
|
// camera held open indefinitely with no consumer.
|
|
485
485
|
if (step !== "liveness" && this._prewarmedStream) {
|
|
@@ -536,7 +536,7 @@ export class SmartComplyFlow {
|
|
|
536
536
|
this.renderDocumentConfirm(wrapper);
|
|
537
537
|
break;
|
|
538
538
|
case "processing":
|
|
539
|
-
// C1: processing step is dead code
|
|
539
|
+
// C1: processing step is dead code — flow now goes to 'done' after submit
|
|
540
540
|
break;
|
|
541
541
|
case "liveness":
|
|
542
542
|
this.renderLiveness(wrapper);
|
|
@@ -545,7 +545,7 @@ export class SmartComplyFlow {
|
|
|
545
545
|
this.renderDone(wrapper);
|
|
546
546
|
break;
|
|
547
547
|
case "result":
|
|
548
|
-
// C2: result step is dead code
|
|
548
|
+
// C2: result step is dead code — showResult/renderResult removed
|
|
549
549
|
break;
|
|
550
550
|
case "error":
|
|
551
551
|
break;
|
|
@@ -557,7 +557,7 @@ export class SmartComplyFlow {
|
|
|
557
557
|
wrapper.style.transform = "translateX(0)";
|
|
558
558
|
});
|
|
559
559
|
}
|
|
560
|
-
//
|
|
560
|
+
// ── Loading ─────────────────────────────────────────────────────
|
|
561
561
|
renderLoading(container) {
|
|
562
562
|
container.style.cssText += "display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:300px;gap:16px;";
|
|
563
563
|
const spinnerWrap = document.createElement("div");
|
|
@@ -588,7 +588,7 @@ export class SmartComplyFlow {
|
|
|
588
588
|
sub.textContent = "Connecting to the verification service...";
|
|
589
589
|
container.appendChild(sub);
|
|
590
590
|
}
|
|
591
|
-
//
|
|
591
|
+
// ── Welcome ────────────────────────────────────────────────────
|
|
592
592
|
renderWelcome(container) {
|
|
593
593
|
container.style.cssText += "display:flex;flex-direction:column;align-items:center;text-align:center;gap:12px;";
|
|
594
594
|
const isDocFlow = this.isDocumentFlow();
|
|
@@ -603,7 +603,7 @@ export class SmartComplyFlow {
|
|
|
603
603
|
background:linear-gradient(135deg,${this.theme.primary}22,${this.theme.primaryGlow});
|
|
604
604
|
border:1.5px solid ${this.theme.primary}33;
|
|
605
605
|
`;
|
|
606
|
-
iconWrap.textContent = isDocFlow ? "
|
|
606
|
+
iconWrap.textContent = isDocFlow ? "🪪" : "🛡️";
|
|
607
607
|
topRow.appendChild(iconWrap);
|
|
608
608
|
const badge = document.createElement("div");
|
|
609
609
|
badge.style.cssText = `
|
|
@@ -614,7 +614,7 @@ export class SmartComplyFlow {
|
|
|
614
614
|
color:${this.theme.primary};
|
|
615
615
|
border:1px solid ${this.theme.primary}30;
|
|
616
616
|
`;
|
|
617
|
-
badge.textContent = isDocFlow ? "
|
|
617
|
+
badge.textContent = isDocFlow ? "📄 Document Verification" : "🔍 Data Verification";
|
|
618
618
|
topRow.appendChild(badge);
|
|
619
619
|
container.appendChild(topRow);
|
|
620
620
|
// Title + subtitle (compact)
|
|
@@ -641,17 +641,17 @@ export class SmartComplyFlow {
|
|
|
641
641
|
: "You'll need your ID number and camera access. This takes about 2 minutes.";
|
|
642
642
|
titleWrap.appendChild(desc);
|
|
643
643
|
container.appendChild(titleWrap);
|
|
644
|
-
// Steps preview
|
|
644
|
+
// Steps preview — compact rows
|
|
645
645
|
const steps = isDocFlow
|
|
646
646
|
? [
|
|
647
|
-
{ icon: "
|
|
648
|
-
{ icon: "
|
|
649
|
-
{ icon: "
|
|
647
|
+
{ icon: "🪪", text: "Choose your document type", sub: "Passport, National ID, Driver License" },
|
|
648
|
+
{ icon: "📷", text: "Scan your document", sub: "Upload or capture a photo of your ID" },
|
|
649
|
+
{ icon: "🤳", text: "Face check", sub: "Quick selfie to confirm it's you" },
|
|
650
650
|
]
|
|
651
651
|
: [
|
|
652
|
-
{ icon: "
|
|
653
|
-
{ icon: "
|
|
654
|
-
{ icon: "
|
|
652
|
+
{ icon: "🆔", text: "Choose your ID type", sub: "NIN, BVN, Voter's Card and more" },
|
|
653
|
+
{ icon: "📝", text: "Enter your ID details", sub: "Exactly as shown on your ID" },
|
|
654
|
+
{ icon: "🤳", text: "Face check", sub: "Quick selfie to confirm it's you" },
|
|
655
655
|
];
|
|
656
656
|
const stepsWrap = document.createElement("div");
|
|
657
657
|
stepsWrap.style.cssText = `
|
|
@@ -692,21 +692,21 @@ export class SmartComplyFlow {
|
|
|
692
692
|
});
|
|
693
693
|
container.appendChild(stepsWrap);
|
|
694
694
|
// CTA
|
|
695
|
-
const btn = this.createPrimaryButton("Get Started
|
|
695
|
+
const btn = this.createPrimaryButton("Get Started →");
|
|
696
696
|
btn.style.cssText += "margin-top:2px;";
|
|
697
697
|
btn.addEventListener("click", () => this.showStep("country"));
|
|
698
698
|
container.appendChild(btn);
|
|
699
|
-
// Trust badge
|
|
699
|
+
// Trust badge — "Powered by Adhere" already lives in the persistent
|
|
700
700
|
// modal footer below; repeating it here just cost vertical space.
|
|
701
701
|
const trust = document.createElement("div");
|
|
702
702
|
trust.style.cssText = `
|
|
703
703
|
display:flex;align-items:center;gap:6px;
|
|
704
704
|
color:${this.theme.textMuted};font-size:10.5px;
|
|
705
705
|
`;
|
|
706
|
-
trust.textContent = "
|
|
706
|
+
trust.textContent = "🔒 End-to-end encrypted";
|
|
707
707
|
container.appendChild(trust);
|
|
708
708
|
}
|
|
709
|
-
//
|
|
709
|
+
// ── Country Select ──────────────────────────────────────────────
|
|
710
710
|
renderCountrySelect(container) {
|
|
711
711
|
container.style.cssText += "display:flex;flex-direction:column;gap:14px;";
|
|
712
712
|
const channels = this.sdkConfig?.channels || {};
|
|
@@ -731,7 +731,7 @@ export class SmartComplyFlow {
|
|
|
731
731
|
container.appendChild(card);
|
|
732
732
|
}
|
|
733
733
|
}
|
|
734
|
-
//
|
|
734
|
+
// ── ID Type Select ──────────────────────────────────────────────
|
|
735
735
|
renderIdTypeSelect(container) {
|
|
736
736
|
container.style.cssText += "display:flex;flex-direction:column;gap:14px;";
|
|
737
737
|
const globalDocFlow = this.isDocumentFlow();
|
|
@@ -759,9 +759,9 @@ export class SmartComplyFlow {
|
|
|
759
759
|
// Add back side indicator to subtitle if needed
|
|
760
760
|
let subtitle = info.desc;
|
|
761
761
|
if (requiresBack && isThisDocFlow) {
|
|
762
|
-
subtitle += "
|
|
762
|
+
subtitle += " ⚠️ Both sides required";
|
|
763
763
|
}
|
|
764
|
-
// Clean display name
|
|
764
|
+
// Clean display name — strip "with face", "(NIN)", "(BVN)" from user-visible label
|
|
765
765
|
const displayName = typeName
|
|
766
766
|
.replace(/\s*with\s+face\s*/gi, ' ')
|
|
767
767
|
.replace(/\s*\(NIN\)\s*/gi, ' ')
|
|
@@ -798,7 +798,7 @@ export class SmartComplyFlow {
|
|
|
798
798
|
container.appendChild(card);
|
|
799
799
|
}
|
|
800
800
|
}
|
|
801
|
-
//
|
|
801
|
+
// ── ID Input (data verification — dynamic fields) ─────────────
|
|
802
802
|
renderIdInput(container) {
|
|
803
803
|
container.style.cssText += "display:flex;flex-direction:column;gap:14px;";
|
|
804
804
|
const dataFields = this.selectedChannelFields.filter((f) => f.type !== "upload");
|
|
@@ -817,7 +817,7 @@ export class SmartComplyFlow {
|
|
|
817
817
|
? `Enter your ${this.selectedIdTypeName || this.selectedIdType} details exactly as they appear on your ID.`
|
|
818
818
|
: "Fill in the fields below to continue.";
|
|
819
819
|
container.appendChild(subtitle);
|
|
820
|
-
// Edge case: no data fields configured
|
|
820
|
+
// Edge case: no data fields configured — show fallback and allow submission
|
|
821
821
|
if (dataFields.length === 0) {
|
|
822
822
|
const empty = document.createElement("div");
|
|
823
823
|
empty.style.cssText = `
|
|
@@ -836,13 +836,13 @@ export class SmartComplyFlow {
|
|
|
836
836
|
{ match: "BVN", rule: { minLen: 11, maxLen: 11, numeric: true, hint: "BVN must be exactly 11 digits" } },
|
|
837
837
|
{ match: "NIN", rule: { minLen: 11, maxLen: 11, numeric: true, hint: "NIN must be exactly 11 digits" } },
|
|
838
838
|
{ match: "NATIONAL IDENTITY", rule: { minLen: 11, maxLen: 11, numeric: true, hint: "NIN must be exactly 11 digits" } },
|
|
839
|
-
{ match: "PASSPORT", rule: { minLen: 6, maxLen: 20, alphanumeric: true, hint: "Passport number must be 6
|
|
840
|
-
{ match: "DRIVER", rule: { minLen: 3, maxLen: 30, alphanumeric: true, hint: "Licence number must be 3
|
|
841
|
-
{ match: "VOTER", rule: { minLen: 6, maxLen: 30, alphanumeric: true, hint: "Voter ID must be 6
|
|
842
|
-
{ match: "GHANA", rule: { minLen: 8, maxLen: 20, alphanumeric: true, hint: "Ghana Card number must be 8
|
|
843
|
-
{ match: "SSNIT", rule: { minLen: 7, maxLen: 15, alphanumeric: true, hint: "SSNIT number must be 7
|
|
844
|
-
{ match: "TIN", rule: { minLen: 8, maxLen: 20, alphanumeric: true, hint: "TIN must be 8
|
|
845
|
-
{ match: "KRA", rule: { minLen: 10, maxLen: 12, alphanumeric: true, hint: "KRA PIN must be 10
|
|
839
|
+
{ match: "PASSPORT", rule: { minLen: 6, maxLen: 20, alphanumeric: true, hint: "Passport number must be 6–20 alphanumeric characters" } },
|
|
840
|
+
{ match: "DRIVER", rule: { minLen: 3, maxLen: 30, alphanumeric: true, hint: "Licence number must be 3–30 alphanumeric characters" } },
|
|
841
|
+
{ match: "VOTER", rule: { minLen: 6, maxLen: 30, alphanumeric: true, hint: "Voter ID must be 6–30 alphanumeric characters" } },
|
|
842
|
+
{ match: "GHANA", rule: { minLen: 8, maxLen: 20, alphanumeric: true, hint: "Ghana Card number must be 8–20 alphanumeric characters" } },
|
|
843
|
+
{ match: "SSNIT", rule: { minLen: 7, maxLen: 15, alphanumeric: true, hint: "SSNIT number must be 7–15 alphanumeric characters" } },
|
|
844
|
+
{ match: "TIN", rule: { minLen: 8, maxLen: 20, alphanumeric: true, hint: "TIN must be 8–20 characters" } },
|
|
845
|
+
{ match: "KRA", rule: { minLen: 10, maxLen: 12, alphanumeric: true, hint: "KRA PIN must be 10–12 characters" } },
|
|
846
846
|
];
|
|
847
847
|
function getFieldRule(label) {
|
|
848
848
|
const upper = label.toUpperCase();
|
|
@@ -870,7 +870,7 @@ export class SmartComplyFlow {
|
|
|
870
870
|
// Track input elements for validation
|
|
871
871
|
const inputEls = [];
|
|
872
872
|
for (const field of dataFields) {
|
|
873
|
-
// Send the raw label as the payload key
|
|
873
|
+
// Send the raw label as the payload key — the backend's
|
|
874
874
|
// _SDK_FIELD_KEY_MAP looks up the exact label text (e.g. "National ID",
|
|
875
875
|
// "CAC Number") to remap it to the canonical IVS key. Pre-normalizing
|
|
876
876
|
// it here (as this used to do) produced keys like "national_id" that
|
|
@@ -920,7 +920,7 @@ export class SmartComplyFlow {
|
|
|
920
920
|
inputEls.push({ field, el: select });
|
|
921
921
|
}
|
|
922
922
|
else if (field.type === "image" || field.type === "file") {
|
|
923
|
-
// File upload field
|
|
923
|
+
// File upload field — converts to base64 for API submission
|
|
924
924
|
const fileInput = document.createElement("input");
|
|
925
925
|
fileInput.type = "file";
|
|
926
926
|
fileInput.accept = "image/*";
|
|
@@ -1070,14 +1070,14 @@ export class SmartComplyFlow {
|
|
|
1070
1070
|
this._ivsName = [nd.first_name || nd.firstname || "", nd.last_name || nd.lastname || nd.surname || ""]
|
|
1071
1071
|
.map(s => String(s || "").trim()).filter(Boolean).join(" ");
|
|
1072
1072
|
this._ivsGender = nd.gender || "";
|
|
1073
|
-
// W4: If IVS returned no name, there is nothing for the user to confirm
|
|
1073
|
+
// W4: If IVS returned no name, there is nothing for the user to confirm —
|
|
1074
1074
|
// auto-proceed straight to Stage 2 (liveness) instead of showing a
|
|
1075
1075
|
// confirmation screen with no identity details to check.
|
|
1076
1076
|
if (!this._ivsName) {
|
|
1077
1077
|
this.showStep("liveness");
|
|
1078
1078
|
return;
|
|
1079
1079
|
}
|
|
1080
|
-
// Route to confirmation screen
|
|
1080
|
+
// Route to confirmation screen — user must confirm before liveness
|
|
1081
1081
|
this.showStep("confirm_identity");
|
|
1082
1082
|
}
|
|
1083
1083
|
else {
|
|
@@ -1089,10 +1089,10 @@ export class SmartComplyFlow {
|
|
|
1089
1089
|
}
|
|
1090
1090
|
}
|
|
1091
1091
|
catch (err) {
|
|
1092
|
-
// Show a single plain-language error
|
|
1092
|
+
// Show a single plain-language error — never raw field:message format
|
|
1093
1093
|
const fieldErrors = err.data?.errors;
|
|
1094
1094
|
if (Array.isArray(fieldErrors) && fieldErrors.length > 0) {
|
|
1095
|
-
// Pick the first meaningful error message
|
|
1095
|
+
// Pick the first meaningful error message — strip field prefix
|
|
1096
1096
|
const firstMsg = fieldErrors[0]?.message || fieldErrors[0]?.detail || "";
|
|
1097
1097
|
errorEl.textContent = _sanitizeUserMessage(firstMsg)
|
|
1098
1098
|
|| "Please check your details and try again.";
|
|
@@ -1123,7 +1123,7 @@ export class SmartComplyFlow {
|
|
|
1123
1123
|
setTimeout(() => firstInput.focus(), 350);
|
|
1124
1124
|
}
|
|
1125
1125
|
}
|
|
1126
|
-
//
|
|
1126
|
+
// ── Document Capture ────────────────────────────────────────────
|
|
1127
1127
|
renderDocumentCapture(container) {
|
|
1128
1128
|
container.style.cssText += "display:flex;flex-direction:column;gap:14px;";
|
|
1129
1129
|
// Strip "with face", "(NIN)", "(BVN)" noise from the user-visible title
|
|
@@ -1164,7 +1164,7 @@ export class SmartComplyFlow {
|
|
|
1164
1164
|
this.showErrorStep(err.message || "Document capture failed. Please try again.");
|
|
1165
1165
|
});
|
|
1166
1166
|
}
|
|
1167
|
-
//
|
|
1167
|
+
// ── W3: Identity Confirmation (data verification) ───────────────
|
|
1168
1168
|
renderConfirmIdentity(container) {
|
|
1169
1169
|
container.style.cssText += "display:flex;flex-direction:column;align-items:center;text-align:center;gap:16px;padding:8px 0;";
|
|
1170
1170
|
// W14: Check retry limit before showing screen
|
|
@@ -1176,7 +1176,7 @@ export class SmartComplyFlow {
|
|
|
1176
1176
|
this._prewarmCamera();
|
|
1177
1177
|
const iconWrap = document.createElement("div");
|
|
1178
1178
|
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;`;
|
|
1179
|
-
iconWrap.textContent = "
|
|
1179
|
+
iconWrap.textContent = "🛡️";
|
|
1180
1180
|
container.appendChild(iconWrap);
|
|
1181
1181
|
const title = document.createElement("h2");
|
|
1182
1182
|
title.style.cssText = `color:${this.theme.text};font-size:18px;font-weight:700;margin:0;`;
|
|
@@ -1215,7 +1215,7 @@ export class SmartComplyFlow {
|
|
|
1215
1215
|
sub.textContent = "Your details have been confirmed. Please continue to the face check.";
|
|
1216
1216
|
container.appendChild(sub);
|
|
1217
1217
|
}
|
|
1218
|
-
const yesBtn = this.createPrimaryButton("Yes, that's me
|
|
1218
|
+
const yesBtn = this.createPrimaryButton("Yes, that's me — continue");
|
|
1219
1219
|
yesBtn.addEventListener("click", () => {
|
|
1220
1220
|
// B5: Reset retry counter on successful confirmation
|
|
1221
1221
|
this._confirmRetries = 0;
|
|
@@ -1233,7 +1233,7 @@ export class SmartComplyFlow {
|
|
|
1233
1233
|
});
|
|
1234
1234
|
container.appendChild(noBtn);
|
|
1235
1235
|
}
|
|
1236
|
-
//
|
|
1236
|
+
// ── W4: Document Confirmation (document verification) ────────────
|
|
1237
1237
|
renderDocumentConfirm(container) {
|
|
1238
1238
|
container.style.cssText += "display:flex;flex-direction:column;align-items:center;text-align:center;gap:14px;padding:8px 0;";
|
|
1239
1239
|
// W14: Check retry limit
|
|
@@ -1245,7 +1245,7 @@ export class SmartComplyFlow {
|
|
|
1245
1245
|
this._prewarmCamera();
|
|
1246
1246
|
const iconWrap = document.createElement("div");
|
|
1247
1247
|
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;`;
|
|
1248
|
-
iconWrap.textContent = "
|
|
1248
|
+
iconWrap.textContent = "📄";
|
|
1249
1249
|
container.appendChild(iconWrap);
|
|
1250
1250
|
const title = document.createElement("h2");
|
|
1251
1251
|
title.style.cssText = `color:${this.theme.text};font-size:18px;font-weight:700;margin:0;`;
|
|
@@ -1270,10 +1270,10 @@ export class SmartComplyFlow {
|
|
|
1270
1270
|
if (this._ocrExpirySoon && !this._ocrIsExpired) {
|
|
1271
1271
|
const warn = document.createElement("div");
|
|
1272
1272
|
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;`;
|
|
1273
|
-
warn.textContent = "
|
|
1273
|
+
warn.textContent = "⚠️ Your document expires soon. Some services may not accept it, but you can still continue.";
|
|
1274
1274
|
container.appendChild(warn);
|
|
1275
1275
|
}
|
|
1276
|
-
// Details card
|
|
1276
|
+
// Details card — show whatever OCR returned, never block on missing fields
|
|
1277
1277
|
const card = document.createElement("div");
|
|
1278
1278
|
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;`;
|
|
1279
1279
|
const docRows = [];
|
|
@@ -1331,7 +1331,7 @@ export class SmartComplyFlow {
|
|
|
1331
1331
|
});
|
|
1332
1332
|
container.appendChild(noBtn);
|
|
1333
1333
|
}
|
|
1334
|
-
//
|
|
1334
|
+
// ── W6+W7+W8: OCR Gate (document verification) ───────────────────
|
|
1335
1335
|
renderOcrGate(container) {
|
|
1336
1336
|
container.style.cssText += "display:flex;flex-direction:column;align-items:center;gap:14px;padding:20px 0;text-align:center;";
|
|
1337
1337
|
const spinnerWrap = document.createElement("div");
|
|
@@ -1340,18 +1340,18 @@ export class SmartComplyFlow {
|
|
|
1340
1340
|
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;`;
|
|
1341
1341
|
const centre = document.createElement("div");
|
|
1342
1342
|
centre.style.cssText = "position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:20px;";
|
|
1343
|
-
centre.textContent = "
|
|
1343
|
+
centre.textContent = "📄";
|
|
1344
1344
|
spinnerWrap.appendChild(ring);
|
|
1345
1345
|
spinnerWrap.appendChild(centre);
|
|
1346
1346
|
container.appendChild(spinnerWrap);
|
|
1347
1347
|
const statusEl = document.createElement("div");
|
|
1348
1348
|
statusEl.setAttribute("aria-live", "polite");
|
|
1349
1349
|
statusEl.style.cssText = `color:${this.theme.text};font-size:15px;font-weight:600;`;
|
|
1350
|
-
statusEl.textContent = "Reading your document
|
|
1350
|
+
statusEl.textContent = "Reading your document…";
|
|
1351
1351
|
container.appendChild(statusEl);
|
|
1352
1352
|
const subEl = document.createElement("div");
|
|
1353
1353
|
subEl.style.cssText = `color:${this.theme.textSecondary};font-size:12px;max-width:260px;line-height:1.5;`;
|
|
1354
|
-
subEl.textContent = "This usually takes 5
|
|
1354
|
+
subEl.textContent = "This usually takes 5–15 seconds. Please wait.";
|
|
1355
1355
|
container.appendChild(subEl);
|
|
1356
1356
|
if (!this.livenessEntryId) {
|
|
1357
1357
|
this.showErrorStep("Document upload failed. Please try again.");
|
|
@@ -1366,19 +1366,19 @@ export class SmartComplyFlow {
|
|
|
1366
1366
|
polls++;
|
|
1367
1367
|
try {
|
|
1368
1368
|
const result = await this.sdk.liveness.pollResult(entryId);
|
|
1369
|
-
// W7: Expired document
|
|
1369
|
+
// W7: Expired document — stop, do not proceed to liveness
|
|
1370
1370
|
if (result.is_expired === true) {
|
|
1371
1371
|
container.innerHTML = "";
|
|
1372
1372
|
this._renderExpiredDocument(container);
|
|
1373
1373
|
return;
|
|
1374
1374
|
}
|
|
1375
|
-
// W8: OCR quality fail
|
|
1375
|
+
// W8: OCR quality fail — offer retake
|
|
1376
1376
|
if (result.ocr_quality_failed === true) {
|
|
1377
1377
|
container.innerHTML = "";
|
|
1378
1378
|
this._renderOcrQualityFail(container, result.ocr_rejection_reason);
|
|
1379
1379
|
return;
|
|
1380
1380
|
}
|
|
1381
|
-
// OCR complete
|
|
1381
|
+
// OCR complete — store data and show confirmation screen
|
|
1382
1382
|
const ocrDone = result.ocr_status === "done" || result.ocr_status === "ocr_complete"
|
|
1383
1383
|
|| result.ocr_name || result.ocr_document_number || result.ocr_expiry_date;
|
|
1384
1384
|
if (ocrDone) {
|
|
@@ -1387,7 +1387,7 @@ export class SmartComplyFlow {
|
|
|
1387
1387
|
this._ocrExpiry = result.ocr_expiry_date || "";
|
|
1388
1388
|
this._ocrFaceImageUrl = result.ocr_face_image_url || "";
|
|
1389
1389
|
this._ocrIsExpired = result.is_expired === true;
|
|
1390
|
-
// B8: Normalise expiry date string before parsing
|
|
1390
|
+
// B8: Normalise expiry date string before parsing — OCR may return
|
|
1391
1391
|
// DD/MM/YYYY, DD-MM-YYYY, YYYY-MM-DD, or "12 May 2029" etc.
|
|
1392
1392
|
// new Date() alone is locale-dependent on non-ISO strings.
|
|
1393
1393
|
if (this._ocrExpiry) {
|
|
@@ -1401,14 +1401,14 @@ export class SmartComplyFlow {
|
|
|
1401
1401
|
this.showStep("document_confirm");
|
|
1402
1402
|
return;
|
|
1403
1403
|
}
|
|
1404
|
-
// Still pending
|
|
1404
|
+
// Still pending — keep polling
|
|
1405
1405
|
if (polls < MAX_POLLS && !this.isDestroyed) {
|
|
1406
1406
|
this._ocrPollTimer = setTimeout(poll, 2000);
|
|
1407
1407
|
}
|
|
1408
1408
|
else if (!this.isDestroyed) {
|
|
1409
|
-
// Timeout
|
|
1410
|
-
statusEl.textContent = "Taking longer than usual
|
|
1411
|
-
subEl.textContent = "We'll continue
|
|
1409
|
+
// Timeout — proceed anyway with whatever we have
|
|
1410
|
+
statusEl.textContent = "Taking longer than usual…";
|
|
1411
|
+
subEl.textContent = "We'll continue — you can confirm your details on the next screen.";
|
|
1412
1412
|
this._ocrPollTimer = setTimeout(() => {
|
|
1413
1413
|
if (!this.isDestroyed)
|
|
1414
1414
|
this.showStep("document_confirm");
|
|
@@ -1424,14 +1424,14 @@ export class SmartComplyFlow {
|
|
|
1424
1424
|
}
|
|
1425
1425
|
}
|
|
1426
1426
|
};
|
|
1427
|
-
// First poll after 3s
|
|
1427
|
+
// First poll after 3s — give OCR submission time to start
|
|
1428
1428
|
this._ocrPollTimer = setTimeout(poll, 3000);
|
|
1429
1429
|
}
|
|
1430
1430
|
_renderExpiredDocument(container) {
|
|
1431
1431
|
container.style.cssText += "display:flex;flex-direction:column;align-items:center;text-align:center;gap:14px;padding:20px 0;";
|
|
1432
1432
|
const icon = document.createElement("div");
|
|
1433
1433
|
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;`;
|
|
1434
|
-
icon.textContent = "
|
|
1434
|
+
icon.textContent = "📅";
|
|
1435
1435
|
container.appendChild(icon);
|
|
1436
1436
|
const t = document.createElement("h2");
|
|
1437
1437
|
t.style.cssText = `color:${this.theme.text};font-size:18px;font-weight:700;margin:0;`;
|
|
@@ -1457,7 +1457,7 @@ export class SmartComplyFlow {
|
|
|
1457
1457
|
container.style.cssText += "display:flex;flex-direction:column;align-items:center;text-align:center;gap:14px;padding:20px 0;";
|
|
1458
1458
|
const icon = document.createElement("div");
|
|
1459
1459
|
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;`;
|
|
1460
|
-
icon.textContent = "
|
|
1460
|
+
icon.textContent = "🔍";
|
|
1461
1461
|
container.appendChild(icon);
|
|
1462
1462
|
const t = document.createElement("h2");
|
|
1463
1463
|
t.style.cssText = `color:${this.theme.text};font-size:18px;font-weight:700;margin:0;`;
|
|
@@ -1481,14 +1481,14 @@ export class SmartComplyFlow {
|
|
|
1481
1481
|
});
|
|
1482
1482
|
container.appendChild(retryBtn);
|
|
1483
1483
|
}
|
|
1484
|
-
//
|
|
1484
|
+
// ── W11: You're all done (end-user final screen) ─────────────────
|
|
1485
1485
|
renderDone(container) {
|
|
1486
1486
|
container.style.cssText += "display:flex;flex-direction:column;align-items:center;text-align:center;gap:16px;padding:24px 0;";
|
|
1487
1487
|
container.setAttribute("role", "status");
|
|
1488
|
-
// B7: Fire onComplete immediately when the done screen renders
|
|
1488
|
+
// B7: Fire onComplete immediately when the done screen renders — the end
|
|
1489
1489
|
// user has completed their part regardless of whether they click Close.
|
|
1490
1490
|
// Firing here means the integrator always gets the event even if the user
|
|
1491
|
-
// closes the modal via the
|
|
1491
|
+
// closes the modal via the ✕ button instead of the Close button.
|
|
1492
1492
|
this.options.onComplete?.({
|
|
1493
1493
|
entryId: this.livenessEntryId || 0,
|
|
1494
1494
|
sessionId: this.sdk.sessionToken,
|
|
@@ -1498,35 +1498,34 @@ export class SmartComplyFlow {
|
|
|
1498
1498
|
});
|
|
1499
1499
|
const iconWrap = document.createElement("div");
|
|
1500
1500
|
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;`;
|
|
1501
|
-
iconWrap.textContent = "
|
|
1501
|
+
iconWrap.textContent = "✓";
|
|
1502
1502
|
container.appendChild(iconWrap);
|
|
1503
1503
|
const title = document.createElement("h2");
|
|
1504
1504
|
title.style.cssText = `color:${this.theme.text};font-size:22px;font-weight:700;margin:0;`;
|
|
1505
1505
|
title.textContent = "You're all done!";
|
|
1506
1506
|
container.appendChild(title);
|
|
1507
|
-
const brandName = this.sdkConfig?.brand_name || "the service";
|
|
1508
1507
|
const desc = document.createElement("p");
|
|
1509
1508
|
desc.style.cssText = `color:${this.theme.textSecondary};font-size:14px;line-height:1.6;margin:0;max-width:300px;`;
|
|
1510
|
-
desc.textContent =
|
|
1509
|
+
desc.textContent = "Your verification is complete.";
|
|
1511
1510
|
container.appendChild(desc);
|
|
1512
1511
|
// Subtle info line
|
|
1513
1512
|
const info = document.createElement("div");
|
|
1514
1513
|
info.style.cssText = `color:${this.theme.textMuted};font-size:11px;display:flex;align-items:center;gap:4px;`;
|
|
1515
|
-
info.textContent = "
|
|
1514
|
+
info.textContent = "🔒 Your data is encrypted and secure";
|
|
1516
1515
|
container.appendChild(info);
|
|
1517
1516
|
const doneBtn = this.createPrimaryButton("Close");
|
|
1518
1517
|
doneBtn.addEventListener("click", () => {
|
|
1519
|
-
// onComplete already fired on render
|
|
1518
|
+
// onComplete already fired on render — just close the modal
|
|
1520
1519
|
this.close();
|
|
1521
1520
|
});
|
|
1522
1521
|
container.appendChild(doneBtn);
|
|
1523
1522
|
}
|
|
1524
|
-
//
|
|
1523
|
+
// ── W14: Retry limit reached ─────────────────────────────────────
|
|
1525
1524
|
_renderRetryLimitReached(container) {
|
|
1526
1525
|
container.style.cssText += "display:flex;flex-direction:column;align-items:center;text-align:center;gap:14px;padding:24px 0;";
|
|
1527
1526
|
const icon = document.createElement("div");
|
|
1528
1527
|
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;`;
|
|
1529
|
-
icon.textContent = "
|
|
1528
|
+
icon.textContent = "⚠️";
|
|
1530
1529
|
container.appendChild(icon);
|
|
1531
1530
|
const t = document.createElement("h2");
|
|
1532
1531
|
t.style.cssText = `color:${this.theme.text};font-size:18px;font-weight:700;margin:0;`;
|
|
@@ -1540,7 +1539,7 @@ export class SmartComplyFlow {
|
|
|
1540
1539
|
closeBtn.addEventListener("click", () => { this.options.onError?.(new Error("Retry limit reached")); this.close(); });
|
|
1541
1540
|
container.appendChild(closeBtn);
|
|
1542
1541
|
}
|
|
1543
|
-
//
|
|
1542
|
+
// ── W13: Camera pre-warm ─────────────────────────────────────────
|
|
1544
1543
|
_prewarmCamera() {
|
|
1545
1544
|
if (this._prewarmedStream || this.isDestroyed)
|
|
1546
1545
|
return;
|
|
@@ -1560,11 +1559,11 @@ export class SmartComplyFlow {
|
|
|
1560
1559
|
async processDocumentCaptureResult(result, captureContainer, container) {
|
|
1561
1560
|
// B2+P2: Compress both images once here. Do NOT assign documentBlob twice
|
|
1562
1561
|
// (old code set it to the raw blob first, then overwrote with compressed).
|
|
1563
|
-
// processDocumentBlob also ran _compressImage
|
|
1562
|
+
// processDocumentBlob also ran _compressImage — that second pass is removed
|
|
1564
1563
|
// below so compression only happens once.
|
|
1565
1564
|
this.documentBlob = await _compressImage(result.front, 1600, 0.85);
|
|
1566
1565
|
this.documentBackBlob = result.back ? await _compressImage(result.back, 1600, 0.85) : null;
|
|
1567
|
-
// Process the front image
|
|
1566
|
+
// Process the front image — pass the already-compressed blob
|
|
1568
1567
|
await this.processDocumentBlob(this.documentBlob, captureContainer, container);
|
|
1569
1568
|
}
|
|
1570
1569
|
/**
|
|
@@ -1575,7 +1574,7 @@ export class SmartComplyFlow {
|
|
|
1575
1574
|
let retryCount = 0;
|
|
1576
1575
|
const attemptProcessing = async () => {
|
|
1577
1576
|
try {
|
|
1578
|
-
// Validate blob
|
|
1577
|
+
// Validate blob — compression already done in processDocumentCaptureResult (P2)
|
|
1579
1578
|
if (!blob || blob.size === 0) {
|
|
1580
1579
|
throw new Error("Failed to capture document. Please try again.");
|
|
1581
1580
|
}
|
|
@@ -1588,7 +1587,7 @@ export class SmartComplyFlow {
|
|
|
1588
1587
|
<div style="display:flex;flex-direction:column;align-items:center;gap:14px;padding:28px 20px;">
|
|
1589
1588
|
<div style="position:relative;width:52px;height:52px;">
|
|
1590
1589
|
<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>
|
|
1591
|
-
<div style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:20px;"
|
|
1590
|
+
<div style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:20px;">🔍</div>
|
|
1592
1591
|
</div>
|
|
1593
1592
|
<div style="color:${this.theme.text};font-size:14px;font-weight:600;">Scanning document...</div>
|
|
1594
1593
|
<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>
|
|
@@ -1596,7 +1595,7 @@ export class SmartComplyFlow {
|
|
|
1596
1595
|
// Create liveness entry with document.
|
|
1597
1596
|
// Use the first collected field value as identifier when available
|
|
1598
1597
|
// (document flow has no data-verification step, so collectedFields is
|
|
1599
|
-
// empty
|
|
1598
|
+
// empty — fall back to the selected ID type code so the webhook
|
|
1600
1599
|
// subject.identifier is at least human-meaningful, not a raw timestamp).
|
|
1601
1600
|
const identifier = Object.values(this.collectedFields)[0]
|
|
1602
1601
|
|| this.idNumber
|
|
@@ -1614,7 +1613,7 @@ export class SmartComplyFlow {
|
|
|
1614
1613
|
identifier,
|
|
1615
1614
|
identifier_type: this.selectedIdType,
|
|
1616
1615
|
country,
|
|
1617
|
-
challenge_actions: ["BLINK"
|
|
1616
|
+
challenge_actions: ["BLINK"],
|
|
1618
1617
|
document: blob,
|
|
1619
1618
|
document_back: this.documentBackBlob || undefined,
|
|
1620
1619
|
device_type: deviceMeta.device_type,
|
|
@@ -1645,7 +1644,7 @@ export class SmartComplyFlow {
|
|
|
1645
1644
|
}
|
|
1646
1645
|
}
|
|
1647
1646
|
this.livenessEntryId = createResult.id;
|
|
1648
|
-
// W6: Route to OCR gate
|
|
1647
|
+
// W6: Route to OCR gate — poll until OCR result is ready, validate
|
|
1649
1648
|
// expiry/quality, then show document_confirm before liveness starts.
|
|
1650
1649
|
this.showStep("ocr_gate");
|
|
1651
1650
|
}
|
|
@@ -1660,7 +1659,7 @@ export class SmartComplyFlow {
|
|
|
1660
1659
|
<div style="display:flex;flex-direction:column;align-items:center;gap:16px;padding:28px 20px;text-align:center;">
|
|
1661
1660
|
<div style="position:relative;width:52px;height:52px;">
|
|
1662
1661
|
<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>
|
|
1663
|
-
<div style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:20px;"
|
|
1662
|
+
<div style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:20px;">🔄</div>
|
|
1664
1663
|
</div>
|
|
1665
1664
|
<div style="color:${this.theme.text};font-size:14px;font-weight:600;">Retrying...</div>
|
|
1666
1665
|
<div style="color:${this.theme.textSecondary};font-size:12px;max-width:240px;line-height:1.5;">Attempt ${retryCount} of ${maxRetries}</div>
|
|
@@ -1679,7 +1678,7 @@ export class SmartComplyFlow {
|
|
|
1679
1678
|
await attemptProcessing();
|
|
1680
1679
|
}
|
|
1681
1680
|
handleDocumentProcessingError(error, captureContainer) {
|
|
1682
|
-
// Map technical errors to user-friendly messages
|
|
1681
|
+
// Map technical errors to user-friendly messages — never pass raw error strings
|
|
1683
1682
|
let userMessage = "We could not verify your document. Please try again.";
|
|
1684
1683
|
let canRetry = true;
|
|
1685
1684
|
const raw = (error?.message || "").toLowerCase();
|
|
@@ -1706,13 +1705,13 @@ export class SmartComplyFlow {
|
|
|
1706
1705
|
userMessage = "The document does not match the selected ID type. Please choose the correct ID type.";
|
|
1707
1706
|
}
|
|
1708
1707
|
if (captureContainer) {
|
|
1709
|
-
// Build with DOM
|
|
1708
|
+
// Build with DOM — never interpolate user-derived content into innerHTML
|
|
1710
1709
|
const wrap = document.createElement("div");
|
|
1711
1710
|
wrap.style.cssText = "display:flex;flex-direction:column;align-items:center;gap:16px;padding:28px 20px;text-align:center;";
|
|
1712
1711
|
wrap.setAttribute("role", "alert");
|
|
1713
1712
|
const icon = document.createElement("div");
|
|
1714
1713
|
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;`;
|
|
1715
|
-
icon.textContent = "
|
|
1714
|
+
icon.textContent = "🔍";
|
|
1716
1715
|
wrap.appendChild(icon);
|
|
1717
1716
|
const titleEl = document.createElement("div");
|
|
1718
1717
|
titleEl.style.cssText = `color:${this.theme.text};font-size:14px;font-weight:700;`;
|
|
@@ -1737,7 +1736,7 @@ export class SmartComplyFlow {
|
|
|
1737
1736
|
this.showErrorStep(userMessage);
|
|
1738
1737
|
}
|
|
1739
1738
|
}
|
|
1740
|
-
//
|
|
1739
|
+
// ── Liveness ────────────────────────────────────────────────────
|
|
1741
1740
|
renderLiveness(container) {
|
|
1742
1741
|
container.style.cssText += "display:flex;flex-direction:column;gap:14px;";
|
|
1743
1742
|
// W13: Check retry limit before starting another liveness attempt
|
|
@@ -1756,7 +1755,7 @@ export class SmartComplyFlow {
|
|
|
1756
1755
|
livenessContainer.style.cssText = "margin:8px 0;width:100%;";
|
|
1757
1756
|
container.appendChild(livenessContainer);
|
|
1758
1757
|
// W9: For data verification, identity is already held by the backend via
|
|
1759
|
-
// identity_check_id
|
|
1758
|
+
// identity_check_id — the real ID number/type must not be re-sent, and the
|
|
1760
1759
|
// backend now accepts liveness/create without them when identity_check is
|
|
1761
1760
|
// set (see liveness.ts create(), which omits both fields entirely in that
|
|
1762
1761
|
// case). Only build a real identifier for the non-data-verification path.
|
|
@@ -1787,9 +1786,9 @@ export class SmartComplyFlow {
|
|
|
1787
1786
|
: "liveness_only",
|
|
1788
1787
|
// B4: pass pre-warmed stream so camera.open() is skipped
|
|
1789
1788
|
prewarmedStream: this._prewarmedStream || undefined,
|
|
1790
|
-
}, ["BLINK"
|
|
1789
|
+
}, ["BLINK"], this.livenessEntryId || undefined)
|
|
1791
1790
|
.then((result) => {
|
|
1792
|
-
// W11: After submit, end user is done
|
|
1791
|
+
// W11: After submit, end user is done — show "You're all done" screen.
|
|
1793
1792
|
// Backend processing continues in background; client receives webhook.
|
|
1794
1793
|
// Track liveness failures for retry limit (W14).
|
|
1795
1794
|
if (result.status === "failed") {
|
|
@@ -1806,9 +1805,9 @@ export class SmartComplyFlow {
|
|
|
1806
1805
|
});
|
|
1807
1806
|
}
|
|
1808
1807
|
renderResult(_container) {
|
|
1809
|
-
// C2: Dead code
|
|
1808
|
+
// C2: Dead code — flow routes to 'done' step, not 'result'. Kept as stub.
|
|
1810
1809
|
}
|
|
1811
|
-
//
|
|
1810
|
+
// ── Error ───────────────────────────────────────────────────────
|
|
1812
1811
|
showErrorStep(message) {
|
|
1813
1812
|
this.currentStep = "error";
|
|
1814
1813
|
if (!this.contentArea)
|
|
@@ -1827,13 +1826,13 @@ export class SmartComplyFlow {
|
|
|
1827
1826
|
const isDocQuality = /blurry|blur|quality|glare|dark|bright|lighting/i.test(message);
|
|
1828
1827
|
const isExpiredDoc = /document.*expired|expired.*document/i.test(message);
|
|
1829
1828
|
const isFaceMatch = /face.*match|selfie.*match|does not match/i.test(message);
|
|
1830
|
-
const errorIcon = isCameraErr ? "
|
|
1831
|
-
: isNetworkErr ? "
|
|
1832
|
-
: isAuthErr ? "
|
|
1833
|
-
: isDocQuality ? "
|
|
1834
|
-
: isExpiredDoc ? "
|
|
1835
|
-
: isFaceMatch ? "
|
|
1836
|
-
: "
|
|
1829
|
+
const errorIcon = isCameraErr ? "📷"
|
|
1830
|
+
: isNetworkErr ? "📡"
|
|
1831
|
+
: isAuthErr ? "⏱"
|
|
1832
|
+
: isDocQuality ? "🔍"
|
|
1833
|
+
: isExpiredDoc ? "📅"
|
|
1834
|
+
: isFaceMatch ? "🤳"
|
|
1835
|
+
: "✕";
|
|
1837
1836
|
const errorTitle = isCameraErr ? "Camera Access Required"
|
|
1838
1837
|
: isNetworkErr ? "Connection Problem"
|
|
1839
1838
|
: isAuthErr ? "Session Expired"
|
|
@@ -1841,7 +1840,7 @@ export class SmartComplyFlow {
|
|
|
1841
1840
|
: isExpiredDoc ? "Document Expired"
|
|
1842
1841
|
: isFaceMatch ? "Face Match Failed"
|
|
1843
1842
|
: "Verification Unsuccessful";
|
|
1844
|
-
// Map to user-facing guidance
|
|
1843
|
+
// Map to user-facing guidance — never show raw internal message
|
|
1845
1844
|
const userMessage = isCameraErr
|
|
1846
1845
|
? "Please allow camera access in your browser settings and try again."
|
|
1847
1846
|
: isNetworkErr
|
|
@@ -1888,9 +1887,9 @@ export class SmartComplyFlow {
|
|
|
1888
1887
|
wrapper.appendChild(closeBtn);
|
|
1889
1888
|
this.contentArea.appendChild(wrapper);
|
|
1890
1889
|
}
|
|
1891
|
-
//
|
|
1890
|
+
// ─────────────────────────────────────────────────────────────────
|
|
1892
1891
|
// UI Helpers
|
|
1893
|
-
//
|
|
1892
|
+
// ─────────────────────────────────────────────────────────────────
|
|
1894
1893
|
createHeader() {
|
|
1895
1894
|
const header = document.createElement("div");
|
|
1896
1895
|
header.setAttribute("role", "banner");
|
|
@@ -1960,7 +1959,7 @@ export class SmartComplyFlow {
|
|
|
1960
1959
|
header.appendChild(stepIndicator);
|
|
1961
1960
|
// Close button
|
|
1962
1961
|
const closeBtn = document.createElement("button");
|
|
1963
|
-
closeBtn.innerHTML = "
|
|
1962
|
+
closeBtn.innerHTML = "✕";
|
|
1964
1963
|
closeBtn.setAttribute("aria-label", "Close identity verification");
|
|
1965
1964
|
closeBtn.style.cssText = `
|
|
1966
1965
|
width:32px;height:32px;border-radius:8px;flex-shrink:0;
|
|
@@ -2041,7 +2040,7 @@ export class SmartComplyFlow {
|
|
|
2041
2040
|
}
|
|
2042
2041
|
const arrow = document.createElement("span");
|
|
2043
2042
|
arrow.style.cssText = `color:${this.theme.textMuted};font-size:18px;flex-shrink:0;`;
|
|
2044
|
-
arrow.textContent = "
|
|
2043
|
+
arrow.textContent = "→";
|
|
2045
2044
|
card.appendChild(textWrap);
|
|
2046
2045
|
card.appendChild(arrow);
|
|
2047
2046
|
return card;
|
|
@@ -2086,7 +2085,7 @@ export class SmartComplyFlow {
|
|
|
2086
2085
|
return btn;
|
|
2087
2086
|
}
|
|
2088
2087
|
isDocumentFlow() {
|
|
2089
|
-
// 1. Check verification_type
|
|
2088
|
+
// 1. Check verification_type — handle both string and array from backend
|
|
2090
2089
|
const vType = this.sdkConfig?.verification_type;
|
|
2091
2090
|
if (vType) {
|
|
2092
2091
|
const types = Array.isArray(vType) ? vType : [vType];
|
|
@@ -2095,7 +2094,7 @@ export class SmartComplyFlow {
|
|
|
2095
2094
|
if (types.includes("data_verification"))
|
|
2096
2095
|
return false;
|
|
2097
2096
|
}
|
|
2098
|
-
// 2. Fallback: inspect channel fields
|
|
2097
|
+
// 2. Fallback: inspect channel fields — any upload field = document flow
|
|
2099
2098
|
const channels = this.sdkConfig?.channels || {};
|
|
2100
2099
|
const allFields = Object.values(channels)
|
|
2101
2100
|
.flat()
|
|
@@ -2106,7 +2105,7 @@ export class SmartComplyFlow {
|
|
|
2106
2105
|
const isDocFlow = this.isDocumentFlow();
|
|
2107
2106
|
const countries = Object.keys(this.sdkConfig?.channels || {});
|
|
2108
2107
|
const singleCountry = countries.length <= 1;
|
|
2109
|
-
// Two-stage action steps
|
|
2108
|
+
// Two-stage action steps — Stage 1: validate identity, Stage 2: liveness
|
|
2110
2109
|
const stage1Steps = isDocFlow
|
|
2111
2110
|
? ["country", "id_type", "document_capture", "ocr_gate", "document_confirm"].filter(s => !(s === "country" && singleCountry))
|
|
2112
2111
|
: ["country", "id_type", "id_input", "confirm_identity"].filter(s => !(s === "country" && singleCountry));
|
|
@@ -2126,7 +2125,7 @@ export class SmartComplyFlow {
|
|
|
2126
2125
|
this.stepLabel.textContent = "";
|
|
2127
2126
|
}
|
|
2128
2127
|
else if (this.currentStep === "done" || this.currentStep === "result") {
|
|
2129
|
-
this.stepLabel.textContent = "Complete
|
|
2128
|
+
this.stepLabel.textContent = "Complete ✓";
|
|
2130
2129
|
}
|
|
2131
2130
|
else if (stage2Steps.includes(this.currentStep)) {
|
|
2132
2131
|
this.stepLabel.textContent = "Stage 2 of 2: Face Check";
|
|
@@ -2138,7 +2137,7 @@ export class SmartComplyFlow {
|
|
|
2138
2137
|
this.stepLabel.textContent = "";
|
|
2139
2138
|
}
|
|
2140
2139
|
}
|
|
2141
|
-
// W3: Stage dots
|
|
2140
|
+
// W3: Stage dots — dot 1 lights once Stage 1 is reached, dot 2 once Stage 2 is reached
|
|
2142
2141
|
if (this.stageDots) {
|
|
2143
2142
|
const dots = Array.from(this.stageDots.children);
|
|
2144
2143
|
const inStage1 = stage1Steps.includes(this.currentStep);
|
|
@@ -2151,7 +2150,7 @@ export class SmartComplyFlow {
|
|
|
2151
2150
|
if (dots[1])
|
|
2152
2151
|
dots[1].style.background = stage2Active ? this.theme.primary : this.theme.border;
|
|
2153
2152
|
}
|
|
2154
|
-
// Back button
|
|
2153
|
+
// Back button — show for navigable steps only
|
|
2155
2154
|
if (this.headerBackBtn) {
|
|
2156
2155
|
const backTargets = {
|
|
2157
2156
|
id_type: () => this.showStep(countries.length > 1 ? "country" : "welcome"),
|