smartcomply-web-sdk 1.0.45 β 1.0.47
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/flow/SmartComplyFlow.d.ts +22 -5
- package/dist/esm/flow/SmartComplyFlow.d.ts.map +1 -1
- package/dist/esm/flow/SmartComplyFlow.js +638 -504
- package/dist/esm/flow/SmartComplyFlow.js.map +1 -1
- package/dist/esm/flow/theme.d.ts +7 -1
- package/dist/esm/flow/theme.d.ts.map +1 -1
- package/dist/esm/flow/theme.js +27 -2
- package/dist/esm/flow/theme.js.map +1 -1
- package/dist/esm/modules/liveness/LivenessUI.d.ts +1 -1
- package/dist/esm/modules/liveness/LivenessUI.d.ts.map +1 -1
- package/dist/esm/modules/liveness/LivenessUI.js +5 -5
- package/dist/esm/modules/liveness/LivenessUI.js.map +1 -1
- package/dist/esm/modules/liveness/liveness.d.ts +1 -0
- package/dist/esm/modules/liveness/liveness.d.ts.map +1 -1
- package/dist/esm/modules/liveness/liveness.js +51 -33
- package/dist/esm/modules/liveness/liveness.js.map +1 -1
- package/dist/flow/SmartComplyFlow.d.ts +22 -5
- package/dist/flow/SmartComplyFlow.d.ts.map +1 -1
- package/dist/flow/SmartComplyFlow.js +638 -504
- package/dist/flow/SmartComplyFlow.js.map +1 -1
- package/dist/flow/theme.d.ts +7 -1
- package/dist/flow/theme.d.ts.map +1 -1
- package/dist/flow/theme.js +27 -2
- package/dist/flow/theme.js.map +1 -1
- package/dist/modules/liveness/LivenessUI.d.ts +1 -1
- package/dist/modules/liveness/LivenessUI.d.ts.map +1 -1
- package/dist/modules/liveness/LivenessUI.js +5 -5
- package/dist/modules/liveness/LivenessUI.js.map +1 -1
- package/dist/modules/liveness/liveness.d.ts +1 -0
- package/dist/modules/liveness/liveness.d.ts.map +1 -1
- package/dist/modules/liveness/liveness.js +51 -33
- package/dist/modules/liveness/liveness.js.map +1 -1
- package/dist/smartcomply.browser.js +572 -428
- package/dist/smartcomply.browser.js.map +3 -3
- package/package.json +1 -1
|
@@ -10,22 +10,25 @@ const STEP_ORDER = [
|
|
|
10
10
|
"welcome",
|
|
11
11
|
"country",
|
|
12
12
|
"id_type",
|
|
13
|
-
"id_input",
|
|
13
|
+
"id_input",
|
|
14
|
+
"confirm_identity",
|
|
14
15
|
"document_capture",
|
|
15
|
-
"
|
|
16
|
+
"ocr_gate",
|
|
17
|
+
"document_confirm",
|
|
16
18
|
"liveness",
|
|
19
|
+
"done",
|
|
17
20
|
"result",
|
|
18
21
|
];
|
|
19
22
|
const COUNTRY_LABELS = {
|
|
20
|
-
nigeria: "
|
|
21
|
-
global: "
|
|
22
|
-
ghana: "
|
|
23
|
-
kenya: "
|
|
24
|
-
south_africa: "
|
|
25
|
-
united_states: "
|
|
26
|
-
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",
|
|
27
30
|
};
|
|
28
|
-
// Backend/admin data tags document-capture fields inconsistently
|
|
31
|
+
// Backend/admin data tags document-capture fields inconsistently Γ’β¬β the field
|
|
29
32
|
// renderer (below) already treats "image" and "file" as file inputs alongside
|
|
30
33
|
// "upload", so flow-routing decisions must recognize the same set or a channel
|
|
31
34
|
// tagged "image"/"file" silently falls back to the number-entry screen.
|
|
@@ -35,7 +38,7 @@ function isUploadFieldType(type) {
|
|
|
35
38
|
function resolveIdTypeInfo(typeName, isDocumentFlow) {
|
|
36
39
|
const u = typeName.toUpperCase();
|
|
37
40
|
const advanced = u.includes("ADVANCED");
|
|
38
|
-
// Strip UI noise from display labels
|
|
41
|
+
// Strip UI noise from display labels Γ’β¬β users don't need "with face" or "(NIN)" suffixes
|
|
39
42
|
const cleanName = typeName
|
|
40
43
|
.replace(/\s*with\s+face\s*/gi, ' ')
|
|
41
44
|
.replace(/\s*\(NIN\)\s*/gi, ' ')
|
|
@@ -50,41 +53,41 @@ function resolveIdTypeInfo(typeName, isDocumentFlow) {
|
|
|
50
53
|
t.includes("license") ||
|
|
51
54
|
t.includes("national id") ||
|
|
52
55
|
t.includes("national id card");
|
|
53
|
-
// 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 Γ’β¬β
|
|
54
57
|
// the NIN card is single-sided. Only the physical NIN slip has a back but
|
|
55
58
|
// the OCR pipeline only needs the front.
|
|
56
59
|
};
|
|
57
|
-
// BVN
|
|
60
|
+
// BVN Γ’β¬β always 11 digits
|
|
58
61
|
if (u.includes("BVN") || u.includes("BANK VERIFICATION")) {
|
|
59
|
-
return { icon: "
|
|
62
|
+
return { icon: "Γ°ΕΈΒΒ¦", desc: "Enter your 11-digit Bank Verification Number (BVN)", requiresBack: false };
|
|
60
63
|
}
|
|
61
|
-
// NIN
|
|
64
|
+
// NIN Γ’β¬β always 11 digits (covers nin_with_face, NIN slip, etc.)
|
|
62
65
|
if (u.includes("NIN") || u.includes("NATIONAL IDENTITY NUMBER")) {
|
|
63
|
-
return { icon: "
|
|
66
|
+
return { icon: "Γ°ΕΈβ β", desc: "Enter your 11-digit National Identification Number (NIN)", requiresBack: false };
|
|
64
67
|
}
|
|
65
68
|
// Ghana Card / Ghana National ID
|
|
66
69
|
if (u.includes("GHANA")) {
|
|
67
70
|
if (isDocumentFlow)
|
|
68
|
-
return { icon: "
|
|
69
|
-
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 };
|
|
70
73
|
}
|
|
71
74
|
// SSNIT (Ghana social security)
|
|
72
75
|
if (u.includes("SSNIT"))
|
|
73
|
-
return { icon: "
|
|
76
|
+
return { icon: "Γ°ΕΈΒβΊΓ―ΒΈΒ", desc: "Enter your SSNIT number", requiresBack: false };
|
|
74
77
|
// Kenya National ID
|
|
75
78
|
if (u.includes("KENYA") || (u.includes("NATIONAL") && u.includes("KENYA")))
|
|
76
|
-
return { icon: "
|
|
79
|
+
return { icon: "Γ°ΕΈβ β", desc: "Enter your Kenya National ID number", requiresBack: false };
|
|
77
80
|
// TIN
|
|
78
81
|
if (u.includes("TIN") || u.includes("TAX IDENTIFICATION"))
|
|
79
|
-
return { icon: "
|
|
82
|
+
return { icon: "Γ°ΕΈΒ§ΒΎ", desc: "Enter your Tax Identification Number (TIN)", requiresBack: false };
|
|
80
83
|
// CAC / Company registration
|
|
81
84
|
if (u.includes("CAC") || u.includes("CORPORATE AFFAIRS") || u.includes("COMPANY"))
|
|
82
|
-
return { icon: "
|
|
85
|
+
return { icon: "Γ°ΕΈΒΒ’", desc: advanced ? "Enter RC number, company name & type" : "Enter your company registration number", requiresBack: false };
|
|
83
86
|
// Voter / PVC
|
|
84
87
|
if (u.includes("VOTER") || u.includes("PVC")) {
|
|
85
88
|
const requiresBack = requiresBackSide(typeName);
|
|
86
89
|
return {
|
|
87
|
-
icon: "
|
|
90
|
+
icon: "Γ°ΕΈβΒ³Γ―ΒΈΒ",
|
|
88
91
|
desc: isDocumentFlow
|
|
89
92
|
? `Capture or upload your voter's card${requiresBack ? " (front & back)" : ""}`
|
|
90
93
|
: "Enter your voter ID number",
|
|
@@ -93,12 +96,12 @@ function resolveIdTypeInfo(typeName, isDocumentFlow) {
|
|
|
93
96
|
}
|
|
94
97
|
// Passport
|
|
95
98
|
if (u.includes("PASSPORT"))
|
|
96
|
-
return { icon: "
|
|
99
|
+
return { icon: "Γ°ΕΈβΛ", desc: isDocumentFlow ? "Capture or upload your passport" : "Enter your passport number", requiresBack: false };
|
|
97
100
|
// Driver's license
|
|
98
101
|
if (u.includes("DRIVER") || u.includes("LICENSE") || u.includes("LICENCE")) {
|
|
99
102
|
const requiresBack = requiresBackSide(typeName);
|
|
100
103
|
return {
|
|
101
|
-
icon: "
|
|
104
|
+
icon: "Γ°ΕΈΕ‘β",
|
|
102
105
|
desc: isDocumentFlow
|
|
103
106
|
? `Capture or upload your driver's license${requiresBack ? " (front & back)" : ""}`
|
|
104
107
|
: "Enter your driver's license number",
|
|
@@ -109,7 +112,7 @@ function resolveIdTypeInfo(typeName, isDocumentFlow) {
|
|
|
109
112
|
if (u.includes("NATIONAL ID") || (u.includes("NATIONAL") && u.includes("CARD"))) {
|
|
110
113
|
const requiresBack = requiresBackSide(typeName);
|
|
111
114
|
return {
|
|
112
|
-
icon: "
|
|
115
|
+
icon: "Γ°ΕΈΒͺΒͺ",
|
|
113
116
|
desc: isDocumentFlow
|
|
114
117
|
? `Capture or upload your ID card${requiresBack ? " (front & back)" : ""}`
|
|
115
118
|
: "Enter your National ID number",
|
|
@@ -118,15 +121,15 @@ function resolveIdTypeInfo(typeName, isDocumentFlow) {
|
|
|
118
121
|
}
|
|
119
122
|
// Face liveness / comparison
|
|
120
123
|
if (u.includes("LIVENESS") || u.includes("FACE COMPARISON") || u.includes("FACE MATCH"))
|
|
121
|
-
return { icon: "
|
|
124
|
+
return { icon: "🀳", desc: "We'll capture a short selfie video to verify you", requiresBack: false };
|
|
122
125
|
return {
|
|
123
|
-
icon: isDocumentFlow ? "
|
|
126
|
+
icon: isDocumentFlow ? "Γ°ΕΈββ" : "Γ°ΕΈβ β",
|
|
124
127
|
desc: isDocumentFlow ? "Capture or upload your document" : "Enter your ID number",
|
|
125
128
|
requiresBack: false,
|
|
126
129
|
};
|
|
127
130
|
}
|
|
128
|
-
//
|
|
129
|
-
//
|
|
131
|
+
// Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬
|
|
132
|
+
// Γ’ββ¬Γ’ββ¬ Widget-side message sanitizer Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬
|
|
130
133
|
// Strips anything technical before showing text to the end user.
|
|
131
134
|
// Keeps messages that are already written plainly in natural language.
|
|
132
135
|
function _sanitizeUserMessage(raw) {
|
|
@@ -155,7 +158,56 @@ function _sanitizeUserMessage(raw) {
|
|
|
155
158
|
return null;
|
|
156
159
|
return raw.trim();
|
|
157
160
|
}
|
|
158
|
-
//
|
|
161
|
+
// Γ’ββ¬Γ’ββ¬ B8: OCR date string normaliser Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬
|
|
162
|
+
// OCR providers return dates in wildly different formats. new Date(str) is
|
|
163
|
+
// locale-dependent for anything other than ISO 8601 (YYYY-MM-DD), so we
|
|
164
|
+
// attempt a set of known patterns before falling back to the native parser.
|
|
165
|
+
function _parseOcrDate(raw) {
|
|
166
|
+
if (!raw)
|
|
167
|
+
return null;
|
|
168
|
+
const s = raw.trim();
|
|
169
|
+
// ISO 8601 or near-ISO Γ’β¬β try first, always unambiguous
|
|
170
|
+
if (/^\d{4}-\d{2}-\d{2}/.test(s))
|
|
171
|
+
return new Date(s.slice(0, 10));
|
|
172
|
+
// DD/MM/YYYY or DD-MM-YYYY
|
|
173
|
+
const dmy = s.match(/^(\d{2})[\/\-](\d{2})[\/\-](\d{4})/);
|
|
174
|
+
if (dmy)
|
|
175
|
+
return new Date(`${dmy[3]}-${dmy[2]}-${dmy[1]}`);
|
|
176
|
+
// MM/DD/YYYY (US format Γ’β¬β less common from OCR but possible)
|
|
177
|
+
const mdy = s.match(/^(\d{2})\/(\d{2})\/(\d{4})/);
|
|
178
|
+
if (mdy) {
|
|
179
|
+
// Heuristic: if "month" > 12 it must be day-first
|
|
180
|
+
if (parseInt(mdy[1], 10) > 12)
|
|
181
|
+
return new Date(`${mdy[3]}-${mdy[2]}-${mdy[1]}`);
|
|
182
|
+
return new Date(`${mdy[3]}-${mdy[1]}-${mdy[2]}`);
|
|
183
|
+
}
|
|
184
|
+
// DD Mon YYYY e.g. "12 May 2029" or "12 MAY 2029"
|
|
185
|
+
const dMonY = s.match(/^(\d{1,2})\s+([A-Za-z]{3,})\s+(\d{4})/);
|
|
186
|
+
if (dMonY)
|
|
187
|
+
return new Date(`${dMonY[2]} ${dMonY[1]} ${dMonY[3]}`);
|
|
188
|
+
// Last resort: native parser
|
|
189
|
+
const d = new Date(s);
|
|
190
|
+
return isNaN(d.getTime()) ? null : d;
|
|
191
|
+
}
|
|
192
|
+
// Γ’ββ¬Γ’ββ¬ W12: Image compression before document upload Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬
|
|
193
|
+
async function _compressImage(blob, maxWidth = 1600, quality = 0.85) {
|
|
194
|
+
try {
|
|
195
|
+
const bmp = await createImageBitmap(blob);
|
|
196
|
+
const scale = Math.min(1, maxWidth / bmp.width);
|
|
197
|
+
const w = Math.round(bmp.width * scale);
|
|
198
|
+
const h = Math.round(bmp.height * scale);
|
|
199
|
+
const canvas = document.createElement("canvas");
|
|
200
|
+
canvas.width = w;
|
|
201
|
+
canvas.height = h;
|
|
202
|
+
canvas.getContext("2d").drawImage(bmp, 0, 0, w, h);
|
|
203
|
+
bmp.close();
|
|
204
|
+
return await new Promise((res, rej) => canvas.toBlob(b => b ? res(b) : rej(new Error("Compression failed")), "image/jpeg", quality));
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
return blob; // fallback to original on any error
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
// Γ’ββ¬Γ’ββ¬ Document type code Γ’β β human label Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬
|
|
159
211
|
function _formatDocType(code) {
|
|
160
212
|
const map = {
|
|
161
213
|
passport: "Passport",
|
|
@@ -176,7 +228,7 @@ function _formatDocType(code) {
|
|
|
176
228
|
return code.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
177
229
|
}
|
|
178
230
|
// SmartComplyFlow
|
|
179
|
-
//
|
|
231
|
+
// Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬
|
|
180
232
|
// Add CSS animation keyframes for spin animations
|
|
181
233
|
const style = document.createElement('style');
|
|
182
234
|
style.textContent = `
|
|
@@ -217,13 +269,31 @@ class SmartComplyFlow {
|
|
|
217
269
|
this.livenessEntryId = null;
|
|
218
270
|
this.isDestroyed = false;
|
|
219
271
|
this.headerBackBtn = null;
|
|
272
|
+
// Retry safety Γ’β¬β max 3 confirmation rejections or liveness failures per session
|
|
273
|
+
this._confirmRetries = 0;
|
|
274
|
+
this._livenessFailures = 0;
|
|
275
|
+
// IVS result stored for confirm_identity screen
|
|
276
|
+
this._ivsName = "";
|
|
277
|
+
this._ivsGender = "";
|
|
278
|
+
// OCR result stored for document_confirm screen
|
|
279
|
+
this._ocrName = "";
|
|
280
|
+
this._ocrDocNumber = "";
|
|
281
|
+
this._ocrExpiry = "";
|
|
282
|
+
this._ocrFaceImageUrl = "";
|
|
283
|
+
this._ocrIsExpired = false;
|
|
284
|
+
this._ocrExpirySoon = false;
|
|
285
|
+
// Camera pre-warm stream (created during confirmation screen, used in liveness)
|
|
286
|
+
this._prewarmedStream = null;
|
|
287
|
+
// OCR gate poll timer Γ’β¬β stored so it can be cancelled on navigation or close
|
|
288
|
+
this._ocrPollTimer = null;
|
|
289
|
+
// UI timeout from _startStatusPolling Γ’β¬β stored so close() can cancel it
|
|
290
|
+
this._uiTimeoutId = null;
|
|
220
291
|
// Session expiry
|
|
221
292
|
this.sessionExpiryTimer = null;
|
|
222
293
|
// Status polling (result screen)
|
|
223
294
|
this._pollStatusTimer = null;
|
|
224
295
|
// Components
|
|
225
296
|
this.docCapture = null;
|
|
226
|
-
this._livenessSubmitResult = null;
|
|
227
297
|
this.options = options;
|
|
228
298
|
this.sdk = new Smartcomply_1.SmartComply({
|
|
229
299
|
apiKey: options.apiKey,
|
|
@@ -257,6 +327,11 @@ class SmartComplyFlow {
|
|
|
257
327
|
close() {
|
|
258
328
|
this.isDestroyed = true;
|
|
259
329
|
this.docCapture?.destroy();
|
|
330
|
+
// Release any pre-warmed camera stream
|
|
331
|
+
if (this._prewarmedStream) {
|
|
332
|
+
this._prewarmedStream.getTracks().forEach(t => t.stop());
|
|
333
|
+
this._prewarmedStream = null;
|
|
334
|
+
}
|
|
260
335
|
if (this.sessionExpiryTimer) {
|
|
261
336
|
clearTimeout(this.sessionExpiryTimer);
|
|
262
337
|
this.sessionExpiryTimer = null;
|
|
@@ -265,13 +340,23 @@ class SmartComplyFlow {
|
|
|
265
340
|
clearTimeout(this._pollStatusTimer);
|
|
266
341
|
this._pollStatusTimer = null;
|
|
267
342
|
}
|
|
343
|
+
// B6: cancel OCR gate poll timer
|
|
344
|
+
if (this._ocrPollTimer) {
|
|
345
|
+
clearTimeout(this._ocrPollTimer);
|
|
346
|
+
this._ocrPollTimer = null;
|
|
347
|
+
}
|
|
348
|
+
// P4: cancel result-screen UI timeout
|
|
349
|
+
if (this._uiTimeoutId) {
|
|
350
|
+
clearTimeout(this._uiTimeoutId);
|
|
351
|
+
this._uiTimeoutId = null;
|
|
352
|
+
}
|
|
268
353
|
if (this.overlay) {
|
|
269
354
|
this.overlay.style.opacity = "0";
|
|
270
355
|
setTimeout(() => {
|
|
271
356
|
try {
|
|
272
357
|
this.overlay?.remove();
|
|
273
358
|
}
|
|
274
|
-
catch (_) { /* extension interference
|
|
359
|
+
catch (_) { /* extension interference Γ’β¬β safe to ignore */ }
|
|
275
360
|
this.overlay = null;
|
|
276
361
|
this.modal = null;
|
|
277
362
|
this.contentArea = null;
|
|
@@ -279,9 +364,9 @@ class SmartComplyFlow {
|
|
|
279
364
|
}
|
|
280
365
|
this.options.onClose?.();
|
|
281
366
|
}
|
|
282
|
-
//
|
|
367
|
+
// Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬
|
|
283
368
|
// Initialization
|
|
284
|
-
//
|
|
369
|
+
// Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬
|
|
285
370
|
mount() {
|
|
286
371
|
// Inject keyframes
|
|
287
372
|
this.injectStyles();
|
|
@@ -366,12 +451,12 @@ class SmartComplyFlow {
|
|
|
366
451
|
}
|
|
367
452
|
// Load config
|
|
368
453
|
this.sdkConfig = await this.sdk.initializeConfig();
|
|
369
|
-
// Apply theme from config
|
|
454
|
+
// Apply theme from config Γ’β¬β use theme_hex to override primary colour if set
|
|
370
455
|
if (this.sdkConfig.theme) {
|
|
371
|
-
this.theme = (0, theme_1.getTheme)(this.sdkConfig.theme);
|
|
456
|
+
this.theme = (0, theme_1.getTheme)(this.sdkConfig.theme, this.sdkConfig.theme_hex || null);
|
|
372
457
|
this.applyTheme();
|
|
373
458
|
}
|
|
374
|
-
// Update brand name
|
|
459
|
+
// Update brand name in header
|
|
375
460
|
if (this.brandLabel && this.sdkConfig.brand_name) {
|
|
376
461
|
this.brandLabel.textContent = this.sdkConfig.brand_name;
|
|
377
462
|
}
|
|
@@ -383,13 +468,18 @@ class SmartComplyFlow {
|
|
|
383
468
|
this.showErrorStep(err.message || "Failed to initialize. Please try again.");
|
|
384
469
|
}
|
|
385
470
|
}
|
|
386
|
-
//
|
|
471
|
+
// Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬
|
|
387
472
|
// Step rendering
|
|
388
|
-
//
|
|
473
|
+
// Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬
|
|
389
474
|
showStep(step) {
|
|
390
475
|
this.currentStep = step;
|
|
391
476
|
if (!this.contentArea)
|
|
392
477
|
return;
|
|
478
|
+
// P1/B6: cancel any in-flight OCR gate poll when user navigates away
|
|
479
|
+
if (this._ocrPollTimer) {
|
|
480
|
+
clearTimeout(this._ocrPollTimer);
|
|
481
|
+
this._ocrPollTimer = null;
|
|
482
|
+
}
|
|
393
483
|
// Update progress
|
|
394
484
|
this.updateProgress();
|
|
395
485
|
// Animate out current content
|
|
@@ -427,20 +517,32 @@ class SmartComplyFlow {
|
|
|
427
517
|
case "id_input":
|
|
428
518
|
this.renderIdInput(wrapper);
|
|
429
519
|
break;
|
|
520
|
+
case "confirm_identity":
|
|
521
|
+
this.renderConfirmIdentity(wrapper);
|
|
522
|
+
break;
|
|
430
523
|
case "document_capture":
|
|
431
524
|
this.renderDocumentCapture(wrapper);
|
|
432
525
|
break;
|
|
526
|
+
case "ocr_gate":
|
|
527
|
+
this.renderOcrGate(wrapper);
|
|
528
|
+
break;
|
|
529
|
+
case "document_confirm":
|
|
530
|
+
this.renderDocumentConfirm(wrapper);
|
|
531
|
+
break;
|
|
433
532
|
case "processing":
|
|
434
|
-
|
|
533
|
+
// C1: processing step is dead code Γ’β¬β flow now goes to 'done' after submit
|
|
435
534
|
break;
|
|
436
535
|
case "liveness":
|
|
437
536
|
this.renderLiveness(wrapper);
|
|
438
537
|
break;
|
|
538
|
+
case "done":
|
|
539
|
+
this.renderDone(wrapper);
|
|
540
|
+
break;
|
|
439
541
|
case "result":
|
|
440
|
-
|
|
542
|
+
// C2: result step is dead code Γ’β¬β showResult/renderResult removed
|
|
441
543
|
break;
|
|
442
544
|
case "error":
|
|
443
|
-
break;
|
|
545
|
+
break;
|
|
444
546
|
}
|
|
445
547
|
this.contentArea.appendChild(wrapper);
|
|
446
548
|
// Animate in
|
|
@@ -449,7 +551,7 @@ class SmartComplyFlow {
|
|
|
449
551
|
wrapper.style.transform = "translateX(0)";
|
|
450
552
|
});
|
|
451
553
|
}
|
|
452
|
-
//
|
|
554
|
+
// Γ’ββ¬Γ’ββ¬ Loading Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬
|
|
453
555
|
renderLoading(container) {
|
|
454
556
|
container.style.cssText += "display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:300px;gap:16px;";
|
|
455
557
|
const spinnerWrap = document.createElement("div");
|
|
@@ -480,7 +582,7 @@ class SmartComplyFlow {
|
|
|
480
582
|
sub.textContent = "Connecting to the verification service...";
|
|
481
583
|
container.appendChild(sub);
|
|
482
584
|
}
|
|
483
|
-
//
|
|
585
|
+
// Γ’ββ¬Γ’ββ¬ Welcome Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬
|
|
484
586
|
renderWelcome(container) {
|
|
485
587
|
container.style.cssText += "display:flex;flex-direction:column;align-items:center;text-align:center;gap:12px;";
|
|
486
588
|
const isDocFlow = this.isDocumentFlow();
|
|
@@ -495,7 +597,7 @@ class SmartComplyFlow {
|
|
|
495
597
|
background:linear-gradient(135deg,${this.theme.primary}22,${this.theme.primaryGlow});
|
|
496
598
|
border:1.5px solid ${this.theme.primary}33;
|
|
497
599
|
`;
|
|
498
|
-
iconWrap.textContent = isDocFlow ? "
|
|
600
|
+
iconWrap.textContent = isDocFlow ? "Γ°ΕΈΒͺΒͺ" : "Γ°ΕΈβΊΒ‘Γ―ΒΈΒ";
|
|
499
601
|
topRow.appendChild(iconWrap);
|
|
500
602
|
const badge = document.createElement("div");
|
|
501
603
|
badge.style.cssText = `
|
|
@@ -506,7 +608,7 @@ class SmartComplyFlow {
|
|
|
506
608
|
color:${this.theme.primary};
|
|
507
609
|
border:1px solid ${this.theme.primary}30;
|
|
508
610
|
`;
|
|
509
|
-
badge.textContent = isDocFlow ? "
|
|
611
|
+
badge.textContent = isDocFlow ? "Γ°ΕΈββ Document Verification" : "Γ°ΕΈβΒ Data Verification";
|
|
510
612
|
topRow.appendChild(badge);
|
|
511
613
|
container.appendChild(topRow);
|
|
512
614
|
// Title + subtitle (compact)
|
|
@@ -517,7 +619,11 @@ class SmartComplyFlow {
|
|
|
517
619
|
color:${this.theme.text};font-size:20px;font-weight:700;
|
|
518
620
|
margin:0;line-height:1.2;
|
|
519
621
|
`;
|
|
520
|
-
|
|
622
|
+
// W2: Use client brand name in title
|
|
623
|
+
const brandName = this.sdkConfig?.brand_name;
|
|
624
|
+
title.textContent = brandName
|
|
625
|
+
? `Verify your identity for ${brandName}`
|
|
626
|
+
: "Identity Verification";
|
|
521
627
|
titleWrap.appendChild(title);
|
|
522
628
|
const desc = document.createElement("p");
|
|
523
629
|
desc.style.cssText = `
|
|
@@ -525,21 +631,21 @@ class SmartComplyFlow {
|
|
|
525
631
|
margin:0;max-width:300px;
|
|
526
632
|
`;
|
|
527
633
|
desc.textContent = isDocFlow
|
|
528
|
-
? "
|
|
529
|
-
: "
|
|
634
|
+
? "You'll need your document and camera access. This takes about 2 minutes."
|
|
635
|
+
: "You'll need your ID number and camera access. This takes about 2 minutes.";
|
|
530
636
|
titleWrap.appendChild(desc);
|
|
531
637
|
container.appendChild(titleWrap);
|
|
532
|
-
// Steps preview
|
|
638
|
+
// Steps preview Γ’β¬β compact rows
|
|
533
639
|
const steps = isDocFlow
|
|
534
640
|
? [
|
|
535
|
-
{ icon: "
|
|
536
|
-
{ icon: "
|
|
537
|
-
{ icon: "
|
|
641
|
+
{ icon: "Γ°ΕΈΒͺΒͺ", text: "Choose your document type", sub: "Passport, National ID, Driver License" },
|
|
642
|
+
{ icon: "Γ°ΕΈβΒ·", text: "Scan your document", sub: "Upload or capture a photo of your ID" },
|
|
643
|
+
{ icon: "🀳", text: "Face check", sub: "Quick selfie to confirm it's you" },
|
|
538
644
|
]
|
|
539
645
|
: [
|
|
540
|
-
{ icon: "
|
|
541
|
-
{ icon: "
|
|
542
|
-
{ icon: "
|
|
646
|
+
{ icon: "Γ°ΕΈβ β", text: "Choose your ID type", sub: "NIN, BVN, Voter's Card and more" },
|
|
647
|
+
{ icon: "Γ°ΕΈβΒ", text: "Enter your ID details", sub: "Exactly as shown on your ID" },
|
|
648
|
+
{ icon: "🀳", text: "Face check", sub: "Quick selfie to confirm it's you" },
|
|
543
649
|
];
|
|
544
650
|
const stepsWrap = document.createElement("div");
|
|
545
651
|
stepsWrap.style.cssText = `
|
|
@@ -580,21 +686,21 @@ class SmartComplyFlow {
|
|
|
580
686
|
});
|
|
581
687
|
container.appendChild(stepsWrap);
|
|
582
688
|
// CTA
|
|
583
|
-
const btn = this.createPrimaryButton("Get Started
|
|
689
|
+
const btn = this.createPrimaryButton("Get Started Γ’β β");
|
|
584
690
|
btn.style.cssText += "margin-top:2px;";
|
|
585
691
|
btn.addEventListener("click", () => this.showStep("country"));
|
|
586
692
|
container.appendChild(btn);
|
|
587
|
-
// Trust badge
|
|
693
|
+
// Trust badge Γ’β¬β "Powered by Adhere" already lives in the persistent
|
|
588
694
|
// modal footer below; repeating it here just cost vertical space.
|
|
589
695
|
const trust = document.createElement("div");
|
|
590
696
|
trust.style.cssText = `
|
|
591
697
|
display:flex;align-items:center;gap:6px;
|
|
592
698
|
color:${this.theme.textMuted};font-size:10.5px;
|
|
593
699
|
`;
|
|
594
|
-
trust.textContent = "
|
|
700
|
+
trust.textContent = "Γ°ΕΈββ End-to-end encrypted";
|
|
595
701
|
container.appendChild(trust);
|
|
596
702
|
}
|
|
597
|
-
//
|
|
703
|
+
// Γ’ββ¬Γ’ββ¬ Country Select Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬
|
|
598
704
|
renderCountrySelect(container) {
|
|
599
705
|
container.style.cssText += "display:flex;flex-direction:column;gap:14px;";
|
|
600
706
|
const channels = this.sdkConfig?.channels || {};
|
|
@@ -619,7 +725,7 @@ class SmartComplyFlow {
|
|
|
619
725
|
container.appendChild(card);
|
|
620
726
|
}
|
|
621
727
|
}
|
|
622
|
-
//
|
|
728
|
+
// Γ’ββ¬Γ’ββ¬ ID Type Select Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬
|
|
623
729
|
renderIdTypeSelect(container) {
|
|
624
730
|
container.style.cssText += "display:flex;flex-direction:column;gap:14px;";
|
|
625
731
|
const globalDocFlow = this.isDocumentFlow();
|
|
@@ -647,9 +753,9 @@ class SmartComplyFlow {
|
|
|
647
753
|
// Add back side indicator to subtitle if needed
|
|
648
754
|
let subtitle = info.desc;
|
|
649
755
|
if (requiresBack && isThisDocFlow) {
|
|
650
|
-
subtitle += "
|
|
756
|
+
subtitle += " Γ’Ε‘Β Γ―ΒΈΒ Both sides required";
|
|
651
757
|
}
|
|
652
|
-
// Clean display name
|
|
758
|
+
// Clean display name Γ’β¬β strip "with face", "(NIN)", "(BVN)" from user-visible label
|
|
653
759
|
const displayName = typeName
|
|
654
760
|
.replace(/\s*with\s+face\s*/gi, ' ')
|
|
655
761
|
.replace(/\s*\(NIN\)\s*/gi, ' ')
|
|
@@ -686,7 +792,7 @@ class SmartComplyFlow {
|
|
|
686
792
|
container.appendChild(card);
|
|
687
793
|
}
|
|
688
794
|
}
|
|
689
|
-
//
|
|
795
|
+
// Γ’ββ¬Γ’ββ¬ ID Input (data verification Γ’β¬β dynamic fields) Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬
|
|
690
796
|
renderIdInput(container) {
|
|
691
797
|
container.style.cssText += "display:flex;flex-direction:column;gap:14px;";
|
|
692
798
|
const dataFields = this.selectedChannelFields.filter((f) => f.type !== "upload");
|
|
@@ -705,7 +811,7 @@ class SmartComplyFlow {
|
|
|
705
811
|
? `Enter your ${this.selectedIdTypeName || this.selectedIdType} details exactly as they appear on your ID.`
|
|
706
812
|
: "Fill in the fields below to continue.";
|
|
707
813
|
container.appendChild(subtitle);
|
|
708
|
-
// Edge case: no data fields configured
|
|
814
|
+
// Edge case: no data fields configured Γ’β¬β show fallback and allow submission
|
|
709
815
|
if (dataFields.length === 0) {
|
|
710
816
|
const empty = document.createElement("div");
|
|
711
817
|
empty.style.cssText = `
|
|
@@ -724,13 +830,13 @@ class SmartComplyFlow {
|
|
|
724
830
|
{ match: "BVN", rule: { minLen: 11, maxLen: 11, numeric: true, hint: "BVN must be exactly 11 digits" } },
|
|
725
831
|
{ match: "NIN", rule: { minLen: 11, maxLen: 11, numeric: true, hint: "NIN must be exactly 11 digits" } },
|
|
726
832
|
{ match: "NATIONAL IDENTITY", rule: { minLen: 11, maxLen: 11, numeric: true, hint: "NIN must be exactly 11 digits" } },
|
|
727
|
-
{ match: "PASSPORT", rule: { minLen: 6, maxLen: 20, alphanumeric: true, hint: "Passport number must be 6
|
|
728
|
-
{ match: "DRIVER", rule: { minLen: 3, maxLen: 30, alphanumeric: true, hint: "Licence number must be 3
|
|
729
|
-
{ match: "VOTER", rule: { minLen: 6, maxLen: 30, alphanumeric: true, hint: "Voter ID must be 6
|
|
730
|
-
{ match: "GHANA", rule: { minLen: 8, maxLen: 20, alphanumeric: true, hint: "Ghana Card number must be 8
|
|
731
|
-
{ match: "SSNIT", rule: { minLen: 7, maxLen: 15, alphanumeric: true, hint: "SSNIT number must be 7
|
|
732
|
-
{ match: "TIN", rule: { minLen: 8, maxLen: 20, alphanumeric: true, hint: "TIN must be 8
|
|
733
|
-
{ match: "KRA", rule: { minLen: 10, maxLen: 12, alphanumeric: true, hint: "KRA PIN must be 10
|
|
833
|
+
{ match: "PASSPORT", rule: { minLen: 6, maxLen: 20, alphanumeric: true, hint: "Passport number must be 6Γ’β¬β20 alphanumeric characters" } },
|
|
834
|
+
{ match: "DRIVER", rule: { minLen: 3, maxLen: 30, alphanumeric: true, hint: "Licence number must be 3Γ’β¬β30 alphanumeric characters" } },
|
|
835
|
+
{ match: "VOTER", rule: { minLen: 6, maxLen: 30, alphanumeric: true, hint: "Voter ID must be 6Γ’β¬β30 alphanumeric characters" } },
|
|
836
|
+
{ match: "GHANA", rule: { minLen: 8, maxLen: 20, alphanumeric: true, hint: "Ghana Card number must be 8Γ’β¬β20 alphanumeric characters" } },
|
|
837
|
+
{ match: "SSNIT", rule: { minLen: 7, maxLen: 15, alphanumeric: true, hint: "SSNIT number must be 7Γ’β¬β15 alphanumeric characters" } },
|
|
838
|
+
{ match: "TIN", rule: { minLen: 8, maxLen: 20, alphanumeric: true, hint: "TIN must be 8Γ’β¬β20 characters" } },
|
|
839
|
+
{ match: "KRA", rule: { minLen: 10, maxLen: 12, alphanumeric: true, hint: "KRA PIN must be 10Γ’β¬β12 characters" } },
|
|
734
840
|
];
|
|
735
841
|
function getFieldRule(label) {
|
|
736
842
|
const upper = label.toUpperCase();
|
|
@@ -758,7 +864,7 @@ class SmartComplyFlow {
|
|
|
758
864
|
// Track input elements for validation
|
|
759
865
|
const inputEls = [];
|
|
760
866
|
for (const field of dataFields) {
|
|
761
|
-
// Send the raw label as the payload key
|
|
867
|
+
// Send the raw label as the payload key Γ’β¬β the backend's
|
|
762
868
|
// _SDK_FIELD_KEY_MAP looks up the exact label text (e.g. "National ID",
|
|
763
869
|
// "CAC Number") to remap it to the canonical IVS key. Pre-normalizing
|
|
764
870
|
// it here (as this used to do) produced keys like "national_id" that
|
|
@@ -808,7 +914,7 @@ class SmartComplyFlow {
|
|
|
808
914
|
inputEls.push({ field, el: select });
|
|
809
915
|
}
|
|
810
916
|
else if (field.type === "image" || field.type === "file") {
|
|
811
|
-
// File upload field
|
|
917
|
+
// File upload field Γ’β¬β converts to base64 for API submission
|
|
812
918
|
const fileInput = document.createElement("input");
|
|
813
919
|
fileInput.type = "file";
|
|
814
920
|
fileInput.accept = "image/*";
|
|
@@ -948,12 +1054,18 @@ class SmartComplyFlow {
|
|
|
948
1054
|
fields: { ...this.collectedFields },
|
|
949
1055
|
});
|
|
950
1056
|
if (this.verificationResult.status === "success") {
|
|
951
|
-
// Save
|
|
1057
|
+
// Save identifier and identity_check_id for liveness step
|
|
952
1058
|
const fieldValues = Object.values(this.collectedFields);
|
|
953
1059
|
this.idNumber = fieldValues[0] || "";
|
|
954
|
-
// A4: Save identity_check_id from verify response to pass to liveness
|
|
955
1060
|
this.identityCheckId = this.verificationResult.data?.identity_check_id || null;
|
|
956
|
-
|
|
1061
|
+
// W3: Store name + gender from IVS result for confirmation screen
|
|
1062
|
+
const d = this.verificationResult.data || {};
|
|
1063
|
+
const nd = d.normalized_data || d.data || d;
|
|
1064
|
+
this._ivsName = [nd.first_name || nd.firstname || "", nd.last_name || nd.lastname || nd.surname || ""]
|
|
1065
|
+
.map(s => String(s || "").trim()).filter(Boolean).join(" ");
|
|
1066
|
+
this._ivsGender = nd.gender || "";
|
|
1067
|
+
// Route to confirmation screen Γ’β¬β user must confirm before liveness
|
|
1068
|
+
this.showStep("confirm_identity");
|
|
957
1069
|
}
|
|
958
1070
|
else {
|
|
959
1071
|
errorEl.textContent = this.verificationResult.message || "We could not verify your details. Please check and try again.";
|
|
@@ -964,10 +1076,10 @@ class SmartComplyFlow {
|
|
|
964
1076
|
}
|
|
965
1077
|
}
|
|
966
1078
|
catch (err) {
|
|
967
|
-
// Show a single plain-language error
|
|
1079
|
+
// Show a single plain-language error Γ’β¬β never raw field:message format
|
|
968
1080
|
const fieldErrors = err.data?.errors;
|
|
969
1081
|
if (Array.isArray(fieldErrors) && fieldErrors.length > 0) {
|
|
970
|
-
// Pick the first meaningful error message
|
|
1082
|
+
// Pick the first meaningful error message Γ’β¬β strip field prefix
|
|
971
1083
|
const firstMsg = fieldErrors[0]?.message || fieldErrors[0]?.detail || "";
|
|
972
1084
|
errorEl.textContent = _sanitizeUserMessage(firstMsg)
|
|
973
1085
|
|| "Please check your details and try again.";
|
|
@@ -998,7 +1110,7 @@ class SmartComplyFlow {
|
|
|
998
1110
|
setTimeout(() => firstInput.focus(), 350);
|
|
999
1111
|
}
|
|
1000
1112
|
}
|
|
1001
|
-
//
|
|
1113
|
+
// Γ’ββ¬Γ’ββ¬ Document Capture Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬
|
|
1002
1114
|
renderDocumentCapture(container) {
|
|
1003
1115
|
container.style.cssText += "display:flex;flex-direction:column;gap:14px;";
|
|
1004
1116
|
// Strip "with face", "(NIN)", "(BVN)" noise from the user-visible title
|
|
@@ -1039,105 +1151,201 @@ class SmartComplyFlow {
|
|
|
1039
1151
|
this.showErrorStep(err.message || "Document capture failed. Please try again.");
|
|
1040
1152
|
});
|
|
1041
1153
|
}
|
|
1042
|
-
//
|
|
1043
|
-
|
|
1044
|
-
container.style.cssText += "display:flex;flex-direction:column;align-items:center;gap:16px;padding:
|
|
1045
|
-
//
|
|
1154
|
+
// Γ’ββ¬Γ’ββ¬ W3: Identity Confirmation (data verification) Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬
|
|
1155
|
+
renderConfirmIdentity(container) {
|
|
1156
|
+
container.style.cssText += "display:flex;flex-direction:column;align-items:center;text-align:center;gap:16px;padding:8px 0;";
|
|
1157
|
+
// W14: Check retry limit before showing screen
|
|
1158
|
+
if (this._confirmRetries >= SmartComplyFlow.MAX_RETRIES) {
|
|
1159
|
+
this._renderRetryLimitReached(container);
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1162
|
+
// W13: Pre-warm camera silently while user reads this screen
|
|
1163
|
+
this._prewarmCamera();
|
|
1164
|
+
const iconWrap = document.createElement("div");
|
|
1165
|
+
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;`;
|
|
1166
|
+
iconWrap.textContent = "Γ°ΕΈβΊΒ‘Γ―ΒΈΒ";
|
|
1167
|
+
container.appendChild(iconWrap);
|
|
1168
|
+
const title = document.createElement("h2");
|
|
1169
|
+
title.style.cssText = `color:${this.theme.text};font-size:18px;font-weight:700;margin:0;`;
|
|
1170
|
+
title.textContent = this._ivsName ? `Is this you?` : "Identity Confirmed";
|
|
1171
|
+
container.appendChild(title);
|
|
1172
|
+
if (this._ivsName) {
|
|
1173
|
+
// Profile card
|
|
1174
|
+
const card = document.createElement("div");
|
|
1175
|
+
card.style.cssText = `width:100%;padding: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;`;
|
|
1176
|
+
const rows = [
|
|
1177
|
+
{ label: "Name", value: this._ivsName },
|
|
1178
|
+
];
|
|
1179
|
+
if (this._ivsGender) {
|
|
1180
|
+
const gLabel = this._ivsGender.toLowerCase();
|
|
1181
|
+
rows.push({ label: "Gender", value: gLabel === "m" || gLabel === "male" || gLabel === "1" ? "Male" : gLabel === "f" || gLabel === "female" || gLabel === "2" ? "Female" : this._ivsGender });
|
|
1182
|
+
}
|
|
1183
|
+
rows.push({ label: "ID Type", value: _formatDocType(this.selectedIdType) || this.selectedIdTypeName || this.selectedIdType });
|
|
1184
|
+
rows.forEach(({ label, value }) => {
|
|
1185
|
+
const row = document.createElement("div");
|
|
1186
|
+
row.style.cssText = "display:flex;justify-content:space-between;align-items:center;";
|
|
1187
|
+
const lEl = document.createElement("span");
|
|
1188
|
+
lEl.style.cssText = `color:${this.theme.textMuted};font-size:12px;font-weight:500;`;
|
|
1189
|
+
lEl.textContent = label;
|
|
1190
|
+
const vEl = document.createElement("span");
|
|
1191
|
+
vEl.style.cssText = `color:${this.theme.text};font-size:13px;font-weight:600;max-width:60%;text-align:right;`;
|
|
1192
|
+
vEl.textContent = value;
|
|
1193
|
+
row.appendChild(lEl);
|
|
1194
|
+
row.appendChild(vEl);
|
|
1195
|
+
card.appendChild(row);
|
|
1196
|
+
});
|
|
1197
|
+
container.appendChild(card);
|
|
1198
|
+
}
|
|
1199
|
+
else {
|
|
1200
|
+
const sub = document.createElement("p");
|
|
1201
|
+
sub.style.cssText = `color:${this.theme.textSecondary};font-size:13px;margin:0;max-width:280px;line-height:1.6;`;
|
|
1202
|
+
sub.textContent = "Your details have been confirmed. Please continue to the face check.";
|
|
1203
|
+
container.appendChild(sub);
|
|
1204
|
+
}
|
|
1205
|
+
const yesBtn = this.createPrimaryButton("Yes, that's me Γ’β¬β continue");
|
|
1206
|
+
yesBtn.addEventListener("click", () => {
|
|
1207
|
+
// B5: Reset retry counter on successful confirmation
|
|
1208
|
+
this._confirmRetries = 0;
|
|
1209
|
+
this.showStep("liveness");
|
|
1210
|
+
});
|
|
1211
|
+
container.appendChild(yesBtn);
|
|
1212
|
+
const noBtn = this.createSecondaryButton("No, use a different ID");
|
|
1213
|
+
noBtn.addEventListener("click", () => {
|
|
1214
|
+
this._confirmRetries++;
|
|
1215
|
+
this._ivsName = "";
|
|
1216
|
+
this._ivsGender = "";
|
|
1217
|
+
this.identityCheckId = null;
|
|
1218
|
+
this.verificationResult = null;
|
|
1219
|
+
this.showStep("id_input");
|
|
1220
|
+
});
|
|
1221
|
+
container.appendChild(noBtn);
|
|
1222
|
+
}
|
|
1223
|
+
// Γ’ββ¬Γ’ββ¬ W4: Document Confirmation (document verification) Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬
|
|
1224
|
+
renderDocumentConfirm(container) {
|
|
1225
|
+
container.style.cssText += "display:flex;flex-direction:column;align-items:center;text-align:center;gap:14px;padding:8px 0;";
|
|
1226
|
+
// W14: Check retry limit
|
|
1227
|
+
if (this._confirmRetries >= SmartComplyFlow.MAX_RETRIES) {
|
|
1228
|
+
this._renderRetryLimitReached(container);
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
// W13: Pre-warm camera while user reads
|
|
1232
|
+
this._prewarmCamera();
|
|
1233
|
+
const iconWrap = document.createElement("div");
|
|
1234
|
+
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;`;
|
|
1235
|
+
iconWrap.textContent = "Γ°ΕΈββ";
|
|
1236
|
+
container.appendChild(iconWrap);
|
|
1237
|
+
const title = document.createElement("h2");
|
|
1238
|
+
title.style.cssText = `color:${this.theme.text};font-size:18px;font-weight:700;margin:0;`;
|
|
1239
|
+
title.textContent = "Are these your details?";
|
|
1240
|
+
container.appendChild(title);
|
|
1241
|
+
const sub = document.createElement("p");
|
|
1242
|
+
sub.style.cssText = `color:${this.theme.textSecondary};font-size:13px;margin:0;max-width:280px;line-height:1.5;`;
|
|
1243
|
+
sub.textContent = "We read the following from your document. Please confirm they are correct before continuing.";
|
|
1244
|
+
container.appendChild(sub);
|
|
1245
|
+
// Document face thumbnail if available
|
|
1246
|
+
if (this._ocrFaceImageUrl) {
|
|
1247
|
+
const faceWrap = document.createElement("div");
|
|
1248
|
+
faceWrap.style.cssText = `width:80px;height:80px;border-radius:50%;overflow:hidden;border:3px solid ${this.theme.primary};flex-shrink:0;margin:0 auto;`;
|
|
1249
|
+
const faceImg = document.createElement("img");
|
|
1250
|
+
faceImg.src = this._ocrFaceImageUrl;
|
|
1251
|
+
faceImg.alt = "Photo from your document";
|
|
1252
|
+
faceImg.style.cssText = "width:100%;height:100%;object-fit:cover;";
|
|
1253
|
+
faceWrap.appendChild(faceImg);
|
|
1254
|
+
container.appendChild(faceWrap);
|
|
1255
|
+
}
|
|
1256
|
+
// Expiry soon warning
|
|
1257
|
+
if (this._ocrExpirySoon && !this._ocrIsExpired) {
|
|
1258
|
+
const warn = document.createElement("div");
|
|
1259
|
+
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;`;
|
|
1260
|
+
warn.textContent = "Γ’Ε‘Β Γ―ΒΈΒ Your document expires soon. Some services may not accept it, but you can still continue.";
|
|
1261
|
+
container.appendChild(warn);
|
|
1262
|
+
}
|
|
1263
|
+
// Details card Γ’β¬β show whatever OCR returned, never block on missing fields
|
|
1264
|
+
const card = document.createElement("div");
|
|
1265
|
+
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;`;
|
|
1266
|
+
const docRows = [];
|
|
1267
|
+
if (this._ocrName)
|
|
1268
|
+
docRows.push({ label: "Name", value: this._ocrName });
|
|
1269
|
+
if (this._ocrDocNumber)
|
|
1270
|
+
docRows.push({ label: "Document No", value: this._ocrDocNumber });
|
|
1271
|
+
if (this._ocrExpiry)
|
|
1272
|
+
docRows.push({ label: "Expires", value: this._ocrExpiry });
|
|
1273
|
+
docRows.push({ label: "Document Type", value: _formatDocType(this.selectedIdType) || this.selectedIdTypeName });
|
|
1274
|
+
if (docRows.length === 0) {
|
|
1275
|
+
const fallback = document.createElement("div");
|
|
1276
|
+
fallback.style.cssText = `color:${this.theme.textSecondary};font-size:12px;`;
|
|
1277
|
+
fallback.textContent = "Document scanned successfully.";
|
|
1278
|
+
card.appendChild(fallback);
|
|
1279
|
+
}
|
|
1280
|
+
else {
|
|
1281
|
+
docRows.forEach(({ label, value }) => {
|
|
1282
|
+
if (!value)
|
|
1283
|
+
return;
|
|
1284
|
+
const row = document.createElement("div");
|
|
1285
|
+
row.style.cssText = "display:flex;justify-content:space-between;align-items:center;";
|
|
1286
|
+
const lEl = document.createElement("span");
|
|
1287
|
+
lEl.style.cssText = `color:${this.theme.textMuted};font-size:12px;font-weight:500;`;
|
|
1288
|
+
lEl.textContent = label;
|
|
1289
|
+
const vEl = document.createElement("span");
|
|
1290
|
+
vEl.style.cssText = `color:${this.theme.text};font-size:13px;font-weight:600;max-width:60%;text-align:right;`;
|
|
1291
|
+
vEl.textContent = value;
|
|
1292
|
+
row.appendChild(lEl);
|
|
1293
|
+
row.appendChild(vEl);
|
|
1294
|
+
card.appendChild(row);
|
|
1295
|
+
});
|
|
1296
|
+
}
|
|
1297
|
+
container.appendChild(card);
|
|
1298
|
+
const yesBtn = this.createPrimaryButton("Yes, continue");
|
|
1299
|
+
yesBtn.addEventListener("click", () => {
|
|
1300
|
+
// B5: Reset retry counter on successful confirmation
|
|
1301
|
+
this._confirmRetries = 0;
|
|
1302
|
+
this.showStep("liveness");
|
|
1303
|
+
});
|
|
1304
|
+
container.appendChild(yesBtn);
|
|
1305
|
+
const noBtn = this.createSecondaryButton("No, retake document");
|
|
1306
|
+
noBtn.addEventListener("click", () => {
|
|
1307
|
+
this._confirmRetries++;
|
|
1308
|
+
this._ocrName = "";
|
|
1309
|
+
this._ocrDocNumber = "";
|
|
1310
|
+
this._ocrExpiry = "";
|
|
1311
|
+
this._ocrFaceImageUrl = "";
|
|
1312
|
+
this._ocrIsExpired = false;
|
|
1313
|
+
this._ocrExpirySoon = false;
|
|
1314
|
+
this.livenessEntryId = null;
|
|
1315
|
+
this.documentBlob = null;
|
|
1316
|
+
this.documentBackBlob = null;
|
|
1317
|
+
this.showStep("document_capture");
|
|
1318
|
+
});
|
|
1319
|
+
container.appendChild(noBtn);
|
|
1320
|
+
}
|
|
1321
|
+
// Γ’ββ¬Γ’ββ¬ W6+W7+W8: OCR Gate (document verification) Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬
|
|
1322
|
+
renderOcrGate(container) {
|
|
1323
|
+
container.style.cssText += "display:flex;flex-direction:column;align-items:center;gap:14px;padding:20px 0;text-align:center;";
|
|
1046
1324
|
const spinnerWrap = document.createElement("div");
|
|
1047
|
-
spinnerWrap.style.cssText = "position:relative;width:
|
|
1325
|
+
spinnerWrap.style.cssText = "position:relative;width:52px;height:52px;flex-shrink:0;";
|
|
1048
1326
|
const ring = document.createElement("div");
|
|
1049
|
-
ring.id = "sc-proc-ring";
|
|
1050
1327
|
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;`;
|
|
1051
|
-
const
|
|
1052
|
-
|
|
1053
|
-
|
|
1328
|
+
const centre = document.createElement("div");
|
|
1329
|
+
centre.style.cssText = "position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:20px;";
|
|
1330
|
+
centre.textContent = "Γ°ΕΈββ";
|
|
1054
1331
|
spinnerWrap.appendChild(ring);
|
|
1055
|
-
spinnerWrap.appendChild(
|
|
1332
|
+
spinnerWrap.appendChild(centre);
|
|
1056
1333
|
container.appendChild(spinnerWrap);
|
|
1057
|
-
const
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
const
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
// Step checklist β reflects real pipeline stages
|
|
1070
|
-
const stages = [
|
|
1071
|
-
{ id: "ocr", label: "Reading document", icon: "π" },
|
|
1072
|
-
{ id: "expiry", label: "Checking expiry", icon: "π
" },
|
|
1073
|
-
{ id: "face", label: "Face match", icon: "π€³" },
|
|
1074
|
-
{ id: "result", label: "Finalising result", icon: "β" },
|
|
1075
|
-
];
|
|
1076
|
-
const grid = document.createElement("div");
|
|
1077
|
-
grid.style.cssText = "width:100%;display:grid;grid-template-columns:1fr 1fr;gap:8px;";
|
|
1078
|
-
stages.forEach((s) => {
|
|
1079
|
-
const el = document.createElement("div");
|
|
1080
|
-
el.id = `sc-stage-${s.id}`;
|
|
1081
|
-
el.style.cssText = `display:flex;align-items:center;gap:8px;padding:9px 10px;border-radius:10px;background:${this.theme.cardBg};border:1px solid ${this.theme.border};transition:all 0.3s ease;`;
|
|
1082
|
-
el.innerHTML = `
|
|
1083
|
-
<div style="width:26px;height:26px;border-radius:7px;flex-shrink:0;display:flex;align-items:center;justify-content:center;font-size:12px;background:${this.theme.shimmer};">${s.icon}</div>
|
|
1084
|
-
<div style="flex:1;color:${this.theme.textSecondary};font-size:11px;font-weight:500;">${s.label}</div>
|
|
1085
|
-
<div id="sc-stage-status-${s.id}" style="width:14px;height:14px;border-radius:50%;border:2px solid ${this.theme.border};flex-shrink:0;"></div>
|
|
1086
|
-
`;
|
|
1087
|
-
grid.appendChild(el);
|
|
1088
|
-
});
|
|
1089
|
-
container.appendChild(grid);
|
|
1090
|
-
// Start polling immediately β update stages as result comes in
|
|
1091
|
-
if (entryId) {
|
|
1092
|
-
this._pollProcessingStages(entryId, statusLine, grid, stages);
|
|
1334
|
+
const statusEl = document.createElement("div");
|
|
1335
|
+
statusEl.setAttribute("aria-live", "polite");
|
|
1336
|
+
statusEl.style.cssText = `color:${this.theme.text};font-size:15px;font-weight:600;`;
|
|
1337
|
+
statusEl.textContent = "Reading your documentΓ’β¬Β¦";
|
|
1338
|
+
container.appendChild(statusEl);
|
|
1339
|
+
const subEl = document.createElement("div");
|
|
1340
|
+
subEl.style.cssText = `color:${this.theme.textSecondary};font-size:12px;max-width:260px;line-height:1.5;`;
|
|
1341
|
+
subEl.textContent = "This usually takes 5Γ’β¬β15 seconds. Please wait.";
|
|
1342
|
+
container.appendChild(subEl);
|
|
1343
|
+
if (!this.livenessEntryId) {
|
|
1344
|
+
this.showErrorStep("Document upload failed. Please try again.");
|
|
1345
|
+
return;
|
|
1093
1346
|
}
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
// Animate stages sequentially on a timer β OCR takes ~5β15s,
|
|
1097
|
-
// face match another ~5β15s. We can't observe sub-steps from the
|
|
1098
|
-
// public sdk-result endpoint, so we advance stages on elapsed time
|
|
1099
|
-
// AND on the final status transition.
|
|
1100
|
-
const stageTimings = [0, 4000, 10000, 18000]; // ms after poll start
|
|
1101
|
-
const startTime = Date.now();
|
|
1102
|
-
const setStageActive = (stageId) => {
|
|
1103
|
-
const el = document.getElementById(`sc-stage-${stageId}`);
|
|
1104
|
-
const statusEl = document.getElementById(`sc-stage-status-${stageId}`);
|
|
1105
|
-
if (!el || !statusEl)
|
|
1106
|
-
return;
|
|
1107
|
-
el.style.borderColor = this.theme.primary;
|
|
1108
|
-
el.style.background = `${this.theme.primary}10`;
|
|
1109
|
-
statusEl.innerHTML = `<div style="position:absolute;inset:1px;border-radius:50%;border:2px solid ${this.theme.border};border-top-color:${this.theme.primary};animation:sc-spin 0.8s linear infinite;"></div>`;
|
|
1110
|
-
statusEl.style.position = "relative";
|
|
1111
|
-
};
|
|
1112
|
-
const setStageComplete = (stageId) => {
|
|
1113
|
-
const el = document.getElementById(`sc-stage-${stageId}`);
|
|
1114
|
-
const statusEl = document.getElementById(`sc-stage-status-${stageId}`);
|
|
1115
|
-
if (!el || !statusEl)
|
|
1116
|
-
return;
|
|
1117
|
-
el.style.borderColor = this.theme.success || "#22c55e";
|
|
1118
|
-
el.style.background = `${this.theme.success || "#22c55e"}10`;
|
|
1119
|
-
statusEl.innerHTML = `<div style="position:absolute;inset:2px;border-radius:50%;background:${this.theme.success || "#22c55e"};display:flex;align-items:center;justify-content:center;color:#fff;font-size:8px;font-weight:bold;">β</div>`;
|
|
1120
|
-
statusEl.style.position = "relative";
|
|
1121
|
-
};
|
|
1122
|
-
// Advance stage indicators on timer
|
|
1123
|
-
stages.forEach((s, i) => {
|
|
1124
|
-
setTimeout(() => {
|
|
1125
|
-
if (this.isDestroyed)
|
|
1126
|
-
return;
|
|
1127
|
-
setStageActive(s.id);
|
|
1128
|
-
if (i > 0)
|
|
1129
|
-
setStageComplete(stages[i - 1].id);
|
|
1130
|
-
statusLine.textContent = s.id === "ocr"
|
|
1131
|
-
? "Scanning documentβ¦"
|
|
1132
|
-
: s.id === "expiry"
|
|
1133
|
-
? "Checking expiry dateβ¦"
|
|
1134
|
-
: s.id === "face"
|
|
1135
|
-
? "Running face matchβ¦"
|
|
1136
|
-
: "Finalising resultβ¦";
|
|
1137
|
-
}, stageTimings[i]);
|
|
1138
|
-
});
|
|
1139
|
-
// Poll for real result
|
|
1140
|
-
const MAX_POLLS = 30; // 30 Γ 3s = 90s max
|
|
1347
|
+
const entryId = this.livenessEntryId;
|
|
1348
|
+
const MAX_POLLS = 10; // 10 Γβ 2s = 20s timeout
|
|
1141
1349
|
let polls = 0;
|
|
1142
1350
|
const poll = async () => {
|
|
1143
1351
|
if (this.isDestroyed)
|
|
@@ -1145,48 +1353,206 @@ class SmartComplyFlow {
|
|
|
1145
1353
|
polls++;
|
|
1146
1354
|
try {
|
|
1147
1355
|
const result = await this.sdk.liveness.pollResult(entryId);
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1356
|
+
// W7: Expired document Γ’β¬β stop, do not proceed to liveness
|
|
1357
|
+
if (result.is_expired === true) {
|
|
1358
|
+
container.innerHTML = "";
|
|
1359
|
+
this._renderExpiredDocument(container);
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1362
|
+
// W8: OCR quality fail Γ’β¬β offer retake
|
|
1363
|
+
if (result.ocr_quality_failed === true) {
|
|
1364
|
+
container.innerHTML = "";
|
|
1365
|
+
this._renderOcrQualityFail(container, result.ocr_rejection_reason);
|
|
1366
|
+
return;
|
|
1367
|
+
}
|
|
1368
|
+
// OCR complete Γ’β¬β store data and show confirmation screen
|
|
1369
|
+
const ocrDone = result.ocr_status === "done" || result.ocr_status === "ocr_complete"
|
|
1370
|
+
|| result.ocr_name || result.ocr_document_number || result.ocr_expiry_date;
|
|
1371
|
+
if (ocrDone) {
|
|
1372
|
+
this._ocrName = result.ocr_name || "";
|
|
1373
|
+
this._ocrDocNumber = result.ocr_document_number || "";
|
|
1374
|
+
this._ocrExpiry = result.ocr_expiry_date || "";
|
|
1375
|
+
this._ocrFaceImageUrl = result.ocr_face_image_url || "";
|
|
1376
|
+
this._ocrIsExpired = result.is_expired === true;
|
|
1377
|
+
// B8: Normalise expiry date string before parsing Γ’β¬β OCR may return
|
|
1378
|
+
// DD/MM/YYYY, DD-MM-YYYY, YYYY-MM-DD, or "12 May 2029" etc.
|
|
1379
|
+
// new Date() alone is locale-dependent on non-ISO strings.
|
|
1380
|
+
if (this._ocrExpiry) {
|
|
1381
|
+
const expDate = _parseOcrDate(this._ocrExpiry);
|
|
1382
|
+
if (expDate) {
|
|
1383
|
+
const sixMonths = new Date();
|
|
1384
|
+
sixMonths.setMonth(sixMonths.getMonth() + 6);
|
|
1385
|
+
this._ocrExpirySoon = expDate <= sixMonths && expDate > new Date();
|
|
1159
1386
|
}
|
|
1160
|
-
}
|
|
1387
|
+
}
|
|
1388
|
+
this.showStep("document_confirm");
|
|
1161
1389
|
return;
|
|
1162
1390
|
}
|
|
1391
|
+
// Still pending Γ’β¬β keep polling
|
|
1392
|
+
if (polls < MAX_POLLS && !this.isDestroyed) {
|
|
1393
|
+
this._ocrPollTimer = setTimeout(poll, 2000);
|
|
1394
|
+
}
|
|
1395
|
+
else if (!this.isDestroyed) {
|
|
1396
|
+
// Timeout Γ’β¬β proceed anyway with whatever we have
|
|
1397
|
+
statusEl.textContent = "Taking longer than usualΓ’β¬Β¦";
|
|
1398
|
+
subEl.textContent = "We'll continue Γ’β¬β you can confirm your details on the next screen.";
|
|
1399
|
+
this._ocrPollTimer = setTimeout(() => {
|
|
1400
|
+
if (!this.isDestroyed)
|
|
1401
|
+
this.showStep("document_confirm");
|
|
1402
|
+
}, 2000);
|
|
1403
|
+
}
|
|
1163
1404
|
}
|
|
1164
|
-
catch {
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
statusLine.textContent = "Still processing β check back soon";
|
|
1405
|
+
catch {
|
|
1406
|
+
if (polls < MAX_POLLS && !this.isDestroyed) {
|
|
1407
|
+
this._ocrPollTimer = setTimeout(poll, 3000);
|
|
1408
|
+
}
|
|
1409
|
+
else if (!this.isDestroyed) {
|
|
1410
|
+
this.showErrorStep("We could not read your document. Please check your connection and try again.");
|
|
1411
|
+
}
|
|
1172
1412
|
}
|
|
1173
1413
|
};
|
|
1174
|
-
// First poll after
|
|
1175
|
-
this.
|
|
1414
|
+
// First poll after 3s Γ’β¬β give OCR submission time to start
|
|
1415
|
+
this._ocrPollTimer = setTimeout(poll, 3000);
|
|
1416
|
+
}
|
|
1417
|
+
_renderExpiredDocument(container) {
|
|
1418
|
+
container.style.cssText += "display:flex;flex-direction:column;align-items:center;text-align:center;gap:14px;padding:20px 0;";
|
|
1419
|
+
const icon = document.createElement("div");
|
|
1420
|
+
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;`;
|
|
1421
|
+
icon.textContent = "Γ°ΕΈββ¦";
|
|
1422
|
+
container.appendChild(icon);
|
|
1423
|
+
const t = document.createElement("h2");
|
|
1424
|
+
t.style.cssText = `color:${this.theme.text};font-size:18px;font-weight:700;margin:0;`;
|
|
1425
|
+
t.textContent = "Document Expired";
|
|
1426
|
+
container.appendChild(t);
|
|
1427
|
+
const d = document.createElement("p");
|
|
1428
|
+
d.style.cssText = `color:${this.theme.textSecondary};font-size:13px;margin:0;max-width:280px;line-height:1.6;`;
|
|
1429
|
+
d.textContent = "The document you submitted has expired. Please use a valid, current ID.";
|
|
1430
|
+
container.appendChild(d);
|
|
1431
|
+
const retryBtn = this.createPrimaryButton("Try a different document");
|
|
1432
|
+
retryBtn.addEventListener("click", () => {
|
|
1433
|
+
this.livenessEntryId = null;
|
|
1434
|
+
this.documentBlob = null;
|
|
1435
|
+
this.documentBackBlob = null;
|
|
1436
|
+
this.showStep("document_capture");
|
|
1437
|
+
});
|
|
1438
|
+
container.appendChild(retryBtn);
|
|
1439
|
+
const endBtn = this.createSecondaryButton("End session");
|
|
1440
|
+
endBtn.addEventListener("click", () => { this.options.onError?.(new Error("Document expired")); this.close(); });
|
|
1441
|
+
container.appendChild(endBtn);
|
|
1442
|
+
}
|
|
1443
|
+
_renderOcrQualityFail(container, reason) {
|
|
1444
|
+
container.style.cssText += "display:flex;flex-direction:column;align-items:center;text-align:center;gap:14px;padding:20px 0;";
|
|
1445
|
+
const icon = document.createElement("div");
|
|
1446
|
+
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;`;
|
|
1447
|
+
icon.textContent = "Γ°ΕΈβΒ";
|
|
1448
|
+
container.appendChild(icon);
|
|
1449
|
+
const t = document.createElement("h2");
|
|
1450
|
+
t.style.cssText = `color:${this.theme.text};font-size:18px;font-weight:700;margin:0;`;
|
|
1451
|
+
t.textContent = "Document Photo Unclear";
|
|
1452
|
+
container.appendChild(t);
|
|
1453
|
+
const msg = reason === "image_too_blurry" ? "Your document photo is too blurry. Please retake it in good lighting with a steady hand."
|
|
1454
|
+
: reason === "image_too_dark" ? "Your document photo is too dark. Please retake it in better lighting."
|
|
1455
|
+
: reason === "image_too_bright" ? "Your document photo has too much glare. Please reduce reflections and retake."
|
|
1456
|
+
: reason === "document_not_detected" ? "We could not detect a document in the photo. Please ensure the full ID is visible."
|
|
1457
|
+
: "We could not read your document clearly. Please retake the photo in good lighting with no glare.";
|
|
1458
|
+
const d = document.createElement("p");
|
|
1459
|
+
d.style.cssText = `color:${this.theme.textSecondary};font-size:13px;margin:0;max-width:280px;line-height:1.6;`;
|
|
1460
|
+
d.textContent = msg;
|
|
1461
|
+
container.appendChild(d);
|
|
1462
|
+
const retryBtn = this.createPrimaryButton("Retake photo");
|
|
1463
|
+
retryBtn.addEventListener("click", () => {
|
|
1464
|
+
this.livenessEntryId = null;
|
|
1465
|
+
this.documentBlob = null;
|
|
1466
|
+
this.documentBackBlob = null;
|
|
1467
|
+
this.showStep("document_capture");
|
|
1468
|
+
});
|
|
1469
|
+
container.appendChild(retryBtn);
|
|
1470
|
+
}
|
|
1471
|
+
// Γ’ββ¬Γ’ββ¬ W11: You're all done (end-user final screen) Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬
|
|
1472
|
+
renderDone(container) {
|
|
1473
|
+
container.style.cssText += "display:flex;flex-direction:column;align-items:center;text-align:center;gap:16px;padding:24px 0;";
|
|
1474
|
+
container.setAttribute("role", "status");
|
|
1475
|
+
// B7: Fire onComplete immediately when the done screen renders Γ’β¬β the end
|
|
1476
|
+
// user has completed their part regardless of whether they click Close.
|
|
1477
|
+
// Firing here means the integrator always gets the event even if the user
|
|
1478
|
+
// closes the modal via the Γ’Εβ’ button instead of the Close button.
|
|
1479
|
+
this.options.onComplete?.({
|
|
1480
|
+
entryId: this.livenessEntryId || 0,
|
|
1481
|
+
sessionId: this.sdk.sessionToken,
|
|
1482
|
+
status: "processing",
|
|
1483
|
+
submittedAt: new Date().toISOString(),
|
|
1484
|
+
verificationResult: this.verificationResult || undefined,
|
|
1485
|
+
});
|
|
1486
|
+
const iconWrap = document.createElement("div");
|
|
1487
|
+
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;`;
|
|
1488
|
+
iconWrap.textContent = "Γ’Εβ";
|
|
1489
|
+
container.appendChild(iconWrap);
|
|
1490
|
+
const title = document.createElement("h2");
|
|
1491
|
+
title.style.cssText = `color:${this.theme.text};font-size:22px;font-weight:700;margin:0;`;
|
|
1492
|
+
title.textContent = "You're all done!";
|
|
1493
|
+
container.appendChild(title);
|
|
1494
|
+
const brandName = this.sdkConfig?.brand_name || "the service";
|
|
1495
|
+
const desc = document.createElement("p");
|
|
1496
|
+
desc.style.cssText = `color:${this.theme.textSecondary};font-size:14px;line-height:1.6;margin:0;max-width:300px;`;
|
|
1497
|
+
desc.textContent = `Your verification is complete. ${brandName} will review and be in touch shortly.`;
|
|
1498
|
+
container.appendChild(desc);
|
|
1499
|
+
// Subtle info line
|
|
1500
|
+
const info = document.createElement("div");
|
|
1501
|
+
info.style.cssText = `color:${this.theme.textMuted};font-size:11px;display:flex;align-items:center;gap:4px;`;
|
|
1502
|
+
info.textContent = "Γ°ΕΈββ Your data is encrypted and secure";
|
|
1503
|
+
container.appendChild(info);
|
|
1504
|
+
const doneBtn = this.createPrimaryButton("Close");
|
|
1505
|
+
doneBtn.addEventListener("click", () => {
|
|
1506
|
+
// onComplete already fired on render Γ’β¬β just close the modal
|
|
1507
|
+
this.close();
|
|
1508
|
+
});
|
|
1509
|
+
container.appendChild(doneBtn);
|
|
1510
|
+
}
|
|
1511
|
+
// Γ’ββ¬Γ’ββ¬ W14: Retry limit reached Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬
|
|
1512
|
+
_renderRetryLimitReached(container) {
|
|
1513
|
+
container.style.cssText += "display:flex;flex-direction:column;align-items:center;text-align:center;gap:14px;padding:24px 0;";
|
|
1514
|
+
const icon = document.createElement("div");
|
|
1515
|
+
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;`;
|
|
1516
|
+
icon.textContent = "Γ’Ε‘Β Γ―ΒΈΒ";
|
|
1517
|
+
container.appendChild(icon);
|
|
1518
|
+
const t = document.createElement("h2");
|
|
1519
|
+
t.style.cssText = `color:${this.theme.text};font-size:18px;font-weight:700;margin:0;`;
|
|
1520
|
+
t.textContent = "Unable to Complete Verification";
|
|
1521
|
+
container.appendChild(t);
|
|
1522
|
+
const d = document.createElement("p");
|
|
1523
|
+
d.style.cssText = `color:${this.theme.textSecondary};font-size:13px;margin:0;max-width:280px;line-height:1.6;`;
|
|
1524
|
+
d.textContent = "We were unable to complete your verification at this time. Please contact support or try again later.";
|
|
1525
|
+
container.appendChild(d);
|
|
1526
|
+
const closeBtn = this.createPrimaryButton("Close");
|
|
1527
|
+
closeBtn.addEventListener("click", () => { this.options.onError?.(new Error("Retry limit reached")); this.close(); });
|
|
1528
|
+
container.appendChild(closeBtn);
|
|
1529
|
+
}
|
|
1530
|
+
// Γ’ββ¬Γ’ββ¬ W13: Camera pre-warm Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬
|
|
1531
|
+
_prewarmCamera() {
|
|
1532
|
+
if (this._prewarmedStream || this.isDestroyed)
|
|
1533
|
+
return;
|
|
1534
|
+
navigator.mediaDevices?.getUserMedia?.({ video: { facingMode: "user" } })
|
|
1535
|
+
.then(stream => {
|
|
1536
|
+
if (this.isDestroyed) {
|
|
1537
|
+
stream.getTracks().forEach(t => t.stop());
|
|
1538
|
+
return;
|
|
1539
|
+
}
|
|
1540
|
+
this._prewarmedStream = stream;
|
|
1541
|
+
})
|
|
1542
|
+
.catch(() => { });
|
|
1176
1543
|
}
|
|
1177
1544
|
/**
|
|
1178
1545
|
* Process document capture result with front/back support
|
|
1179
1546
|
*/
|
|
1180
1547
|
async processDocumentCaptureResult(result, captureContainer, container) {
|
|
1181
|
-
//
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
//
|
|
1185
|
-
await
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
}
|
|
1548
|
+
// B2+P2: Compress both images once here. Do NOT assign documentBlob twice
|
|
1549
|
+
// (old code set it to the raw blob first, then overwrote with compressed).
|
|
1550
|
+
// processDocumentBlob also ran _compressImage Γ’β¬β that second pass is removed
|
|
1551
|
+
// below so compression only happens once.
|
|
1552
|
+
this.documentBlob = await _compressImage(result.front, 1600, 0.85);
|
|
1553
|
+
this.documentBackBlob = result.back ? await _compressImage(result.back, 1600, 0.85) : null;
|
|
1554
|
+
// Process the front image Γ’β¬β pass the already-compressed blob
|
|
1555
|
+
await this.processDocumentBlob(this.documentBlob, captureContainer, container);
|
|
1190
1556
|
}
|
|
1191
1557
|
/**
|
|
1192
1558
|
* Process document blob - common method for both CameraView and DocumentCapture
|
|
@@ -1196,7 +1562,7 @@ class SmartComplyFlow {
|
|
|
1196
1562
|
let retryCount = 0;
|
|
1197
1563
|
const attemptProcessing = async () => {
|
|
1198
1564
|
try {
|
|
1199
|
-
// Validate blob
|
|
1565
|
+
// Validate blob Γ’β¬β compression already done in processDocumentCaptureResult (P2)
|
|
1200
1566
|
if (!blob || blob.size === 0) {
|
|
1201
1567
|
throw new Error("Failed to capture document. Please try again.");
|
|
1202
1568
|
}
|
|
@@ -1209,7 +1575,7 @@ class SmartComplyFlow {
|
|
|
1209
1575
|
<div style="display:flex;flex-direction:column;align-items:center;gap:14px;padding:28px 20px;">
|
|
1210
1576
|
<div style="position:relative;width:52px;height:52px;">
|
|
1211
1577
|
<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>
|
|
1212
|
-
<div style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:20px;"
|
|
1578
|
+
<div style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:20px;">Γ°ΕΈβΒ</div>
|
|
1213
1579
|
</div>
|
|
1214
1580
|
<div style="color:${this.theme.text};font-size:14px;font-weight:600;">Scanning document...</div>
|
|
1215
1581
|
<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>
|
|
@@ -1217,7 +1583,7 @@ class SmartComplyFlow {
|
|
|
1217
1583
|
// Create liveness entry with document.
|
|
1218
1584
|
// Use the first collected field value as identifier when available
|
|
1219
1585
|
// (document flow has no data-verification step, so collectedFields is
|
|
1220
|
-
// empty
|
|
1586
|
+
// empty Γ’β¬β fall back to the selected ID type code so the webhook
|
|
1221
1587
|
// subject.identifier is at least human-meaningful, not a raw timestamp).
|
|
1222
1588
|
const identifier = Object.values(this.collectedFields)[0]
|
|
1223
1589
|
|| this.idNumber
|
|
@@ -1266,10 +1632,9 @@ class SmartComplyFlow {
|
|
|
1266
1632
|
}
|
|
1267
1633
|
}
|
|
1268
1634
|
this.livenessEntryId = createResult.id;
|
|
1269
|
-
//
|
|
1270
|
-
//
|
|
1271
|
-
|
|
1272
|
-
this.showStep("liveness");
|
|
1635
|
+
// W6: Route to OCR gate Γ’β¬β poll until OCR result is ready, validate
|
|
1636
|
+
// expiry/quality, then show document_confirm before liveness starts.
|
|
1637
|
+
this.showStep("ocr_gate");
|
|
1273
1638
|
}
|
|
1274
1639
|
catch (err) {
|
|
1275
1640
|
console.error("Document processing failed:", err);
|
|
@@ -1282,7 +1647,7 @@ class SmartComplyFlow {
|
|
|
1282
1647
|
<div style="display:flex;flex-direction:column;align-items:center;gap:16px;padding:28px 20px;text-align:center;">
|
|
1283
1648
|
<div style="position:relative;width:52px;height:52px;">
|
|
1284
1649
|
<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>
|
|
1285
|
-
<div style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:20px;"
|
|
1650
|
+
<div style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:20px;">Γ°ΕΈββ</div>
|
|
1286
1651
|
</div>
|
|
1287
1652
|
<div style="color:${this.theme.text};font-size:14px;font-weight:600;">Retrying...</div>
|
|
1288
1653
|
<div style="color:${this.theme.textSecondary};font-size:12px;max-width:240px;line-height:1.5;">Attempt ${retryCount} of ${maxRetries}</div>
|
|
@@ -1301,7 +1666,7 @@ class SmartComplyFlow {
|
|
|
1301
1666
|
await attemptProcessing();
|
|
1302
1667
|
}
|
|
1303
1668
|
handleDocumentProcessingError(error, captureContainer) {
|
|
1304
|
-
// Map technical errors to user-friendly messages
|
|
1669
|
+
// Map technical errors to user-friendly messages Γ’β¬β never pass raw error strings
|
|
1305
1670
|
let userMessage = "We could not verify your document. Please try again.";
|
|
1306
1671
|
let canRetry = true;
|
|
1307
1672
|
const raw = (error?.message || "").toLowerCase();
|
|
@@ -1328,13 +1693,13 @@ class SmartComplyFlow {
|
|
|
1328
1693
|
userMessage = "The document does not match the selected ID type. Please choose the correct ID type.";
|
|
1329
1694
|
}
|
|
1330
1695
|
if (captureContainer) {
|
|
1331
|
-
// Build with DOM
|
|
1696
|
+
// Build with DOM Γ’β¬β never interpolate user-derived content into innerHTML
|
|
1332
1697
|
const wrap = document.createElement("div");
|
|
1333
1698
|
wrap.style.cssText = "display:flex;flex-direction:column;align-items:center;gap:16px;padding:28px 20px;text-align:center;";
|
|
1334
1699
|
wrap.setAttribute("role", "alert");
|
|
1335
1700
|
const icon = document.createElement("div");
|
|
1336
1701
|
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;`;
|
|
1337
|
-
icon.textContent = "
|
|
1702
|
+
icon.textContent = "Γ°ΕΈβΒ";
|
|
1338
1703
|
wrap.appendChild(icon);
|
|
1339
1704
|
const titleEl = document.createElement("div");
|
|
1340
1705
|
titleEl.style.cssText = `color:${this.theme.text};font-size:14px;font-weight:700;`;
|
|
@@ -1359,7 +1724,7 @@ class SmartComplyFlow {
|
|
|
1359
1724
|
this.showErrorStep(userMessage);
|
|
1360
1725
|
}
|
|
1361
1726
|
}
|
|
1362
|
-
//
|
|
1727
|
+
// Γ’ββ¬Γ’ββ¬ Liveness Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬
|
|
1363
1728
|
renderLiveness(container) {
|
|
1364
1729
|
container.style.cssText += "display:flex;flex-direction:column;gap:14px;";
|
|
1365
1730
|
const title = this.createStepTitle("Face verification");
|
|
@@ -1372,18 +1737,14 @@ class SmartComplyFlow {
|
|
|
1372
1737
|
const livenessContainer = document.createElement("div");
|
|
1373
1738
|
livenessContainer.style.cssText = "margin:8px 0;width:100%;";
|
|
1374
1739
|
container.appendChild(livenessContainer);
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1740
|
+
// W10: For data verification, identifier is already held by the backend
|
|
1741
|
+
// via identity_check_id Γ’β¬β do not re-send it in liveness create.
|
|
1742
|
+
const identifier = this.identityCheckId
|
|
1743
|
+
? "" // data verification Γ’β¬β session holds context via identity_check_id
|
|
1744
|
+
: (this.idNumber || Object.values(this.collectedFields)[0] || this.selectedIdType || `DOC-${Date.now()}`);
|
|
1379
1745
|
const COUNTRY_CODES = {
|
|
1380
|
-
nigeria: "NG",
|
|
1381
|
-
|
|
1382
|
-
kenya: "KE",
|
|
1383
|
-
south_africa: "ZA",
|
|
1384
|
-
united_states: "US",
|
|
1385
|
-
united_kingdom: "GB",
|
|
1386
|
-
global: "US",
|
|
1746
|
+
nigeria: "NG", ghana: "GH", kenya: "KE",
|
|
1747
|
+
south_africa: "ZA", united_states: "US", united_kingdom: "GB", global: "US",
|
|
1387
1748
|
};
|
|
1388
1749
|
const country = COUNTRY_CODES[this.selectedCountry] || this.selectedCountry.toUpperCase().slice(0, 2);
|
|
1389
1750
|
// Start liveness check (this handles camera, detection, recording, submission)
|
|
@@ -1396,272 +1757,35 @@ class SmartComplyFlow {
|
|
|
1396
1757
|
document: this.documentBlob || undefined,
|
|
1397
1758
|
document_back: this.documentBackBlob || undefined,
|
|
1398
1759
|
identity_check: this.identityCheckId || undefined,
|
|
1399
|
-
// Tell the module which flow we're in so it can skip video for data verification
|
|
1400
1760
|
verification_type: this.identityCheckId
|
|
1401
1761
|
? "data_verification"
|
|
1402
1762
|
: this.documentBlob
|
|
1403
1763
|
? "document_verification"
|
|
1404
1764
|
: "liveness_only",
|
|
1765
|
+
// B4: pass pre-warmed stream so camera.open() is skipped
|
|
1766
|
+
prewarmedStream: this._prewarmedStream || undefined,
|
|
1405
1767
|
}, ["BLINK", "TURN_HEAD", "OPEN_MOUTH"], this.livenessEntryId || undefined)
|
|
1406
1768
|
.then((result) => {
|
|
1407
|
-
//
|
|
1408
|
-
//
|
|
1409
|
-
//
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
this.livenessEntryId = result.id;
|
|
1413
|
-
this.showStep("processing");
|
|
1769
|
+
// W11: After submit, end user is done Γ’β¬β show "You're all done" screen.
|
|
1770
|
+
// Backend processing continues in background; client receives webhook.
|
|
1771
|
+
// Track liveness failures for retry limit (W14).
|
|
1772
|
+
if (result.status === "failed") {
|
|
1773
|
+
this._livenessFailures++;
|
|
1414
1774
|
}
|
|
1415
|
-
|
|
1416
|
-
this.
|
|
1775
|
+
if (result.id) {
|
|
1776
|
+
this.livenessEntryId = result.id;
|
|
1417
1777
|
}
|
|
1778
|
+
this.showStep("done");
|
|
1418
1779
|
})
|
|
1419
1780
|
.catch((err) => {
|
|
1781
|
+
this._livenessFailures++;
|
|
1420
1782
|
this.showErrorStep(err.message || "Liveness verification failed. Please try again.");
|
|
1421
1783
|
});
|
|
1422
1784
|
}
|
|
1423
|
-
// ββ Result ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
1424
|
-
showResult(submitResult) {
|
|
1425
|
-
this.currentStep = "result";
|
|
1426
|
-
this.updateProgress();
|
|
1427
|
-
if (!this.contentArea)
|
|
1428
|
-
return;
|
|
1429
|
-
this.contentArea.innerHTML = "";
|
|
1430
|
-
const wrapper = document.createElement("div");
|
|
1431
|
-
wrapper.setAttribute("role", "region");
|
|
1432
|
-
wrapper.setAttribute("aria-label", "Verification result");
|
|
1433
|
-
wrapper.style.cssText = `
|
|
1434
|
-
display:flex;flex-direction:column;align-items:center;
|
|
1435
|
-
text-align:center;gap:16px;padding:20px 0;
|
|
1436
|
-
opacity:0;transform:scale(0.95);transition:all 0.4s ease;
|
|
1437
|
-
`;
|
|
1438
|
-
// ββ Icon β spinner until status resolves ββββββββββββββββββββββββ
|
|
1439
|
-
const iconWrap = document.createElement("div");
|
|
1440
|
-
iconWrap.style.cssText = `
|
|
1441
|
-
width:80px;height:80px;border-radius:50%;
|
|
1442
|
-
display:flex;align-items:center;justify-content:center;
|
|
1443
|
-
font-size:36px;
|
|
1444
|
-
background:${this.theme.cardBg};
|
|
1445
|
-
border:3px solid ${this.theme.border};
|
|
1446
|
-
transition:all 0.4s ease;
|
|
1447
|
-
`;
|
|
1448
|
-
iconWrap.innerHTML = `<div style="width:32px;height:32px;border-radius:50%;border:3px solid ${this.theme.border};border-top-color:${this.theme.primary};animation:sc-spin 0.9s linear infinite;"></div>`;
|
|
1449
|
-
wrapper.appendChild(iconWrap);
|
|
1450
|
-
// ββ Title and description β updated by polling βββββββββββββββββββ
|
|
1451
|
-
const title = document.createElement("h2");
|
|
1452
|
-
title.setAttribute("aria-live", "polite");
|
|
1453
|
-
title.style.cssText = `color:${this.theme.text};font-size:20px;font-weight:700;margin:0;`;
|
|
1454
|
-
title.textContent = "Checking your identityβ¦";
|
|
1455
|
-
wrapper.appendChild(title);
|
|
1456
|
-
const desc = document.createElement("p");
|
|
1457
|
-
desc.setAttribute("aria-live", "polite");
|
|
1458
|
-
desc.style.cssText = `color:${this.theme.textSecondary};font-size:14px;line-height:1.6;margin:0;max-width:300px;`;
|
|
1459
|
-
desc.textContent = "This usually takes a few seconds. Please wait.";
|
|
1460
|
-
wrapper.appendChild(desc);
|
|
1461
|
-
// ββ Result card β initially shows status only ββββββββββββββββββββ
|
|
1462
|
-
// Populated with name/doc type when verification completes.
|
|
1463
|
-
const resultCard = document.createElement("div");
|
|
1464
|
-
resultCard.style.cssText = `
|
|
1465
|
-
width:100%;padding:16px;border-radius:12px;
|
|
1466
|
-
background:${this.theme.cardBg};border:1px solid ${this.theme.border};
|
|
1467
|
-
text-align:left;display:flex;flex-direction:column;gap:10px;
|
|
1468
|
-
transition:border-color 0.3s ease;
|
|
1469
|
-
`;
|
|
1470
|
-
// Status row β always shown
|
|
1471
|
-
const statusBadge = document.createElement("span");
|
|
1472
|
-
statusBadge.setAttribute("role", "status");
|
|
1473
|
-
statusBadge.setAttribute("aria-live", "polite");
|
|
1474
|
-
statusBadge.setAttribute("aria-atomic", "true");
|
|
1475
|
-
statusBadge.style.cssText = `font-size:12px;font-weight:600;color:${this.theme.warning};`;
|
|
1476
|
-
statusBadge.textContent = "Processingβ¦";
|
|
1477
|
-
const statusRow = document.createElement("div");
|
|
1478
|
-
statusRow.style.cssText = "display:flex;justify-content:space-between;align-items:center;";
|
|
1479
|
-
statusRow.innerHTML = `<span style="color:${this.theme.textMuted};font-size:12px;font-weight:500;">Status</span>`;
|
|
1480
|
-
statusRow.appendChild(statusBadge);
|
|
1481
|
-
resultCard.appendChild(statusRow);
|
|
1482
|
-
// Verified details β shown only when passed.
|
|
1483
|
-
// nameRow: shows full name from IVS result or OCR
|
|
1484
|
-
// docTypeRow: shows document type verified
|
|
1485
|
-
const nameRow = document.createElement("div");
|
|
1486
|
-
nameRow.style.cssText = "display:none;justify-content:space-between;align-items:center;";
|
|
1487
|
-
nameRow.innerHTML = `<span style="color:${this.theme.textMuted};font-size:12px;font-weight:500;">Name</span><span id="sc-result-name" style="color:${this.theme.text};font-size:12px;font-weight:600;text-align:right;max-width:60%;"></span>`;
|
|
1488
|
-
resultCard.appendChild(nameRow);
|
|
1489
|
-
const docTypeRow = document.createElement("div");
|
|
1490
|
-
docTypeRow.style.cssText = "display:none;justify-content:space-between;align-items:center;";
|
|
1491
|
-
docTypeRow.innerHTML = `<span style="color:${this.theme.textMuted};font-size:12px;font-weight:500;">ID Type</span><span id="sc-result-doctype" style="color:${this.theme.text};font-size:12px;font-weight:600;"></span>`;
|
|
1492
|
-
resultCard.appendChild(docTypeRow);
|
|
1493
|
-
wrapper.appendChild(resultCard);
|
|
1494
|
-
// ββ Failure reason β shown only when failed ββββββββββββββββββββββ
|
|
1495
|
-
const failureMsg = document.createElement("div");
|
|
1496
|
-
failureMsg.setAttribute("role", "alert");
|
|
1497
|
-
failureMsg.style.cssText = `
|
|
1498
|
-
display:none;width:100%;padding:12px 14px;border-radius:10px;
|
|
1499
|
-
background:${this.theme.errorBg};border:1px solid ${this.theme.error};
|
|
1500
|
-
color:${this.theme.error};font-size:13px;text-align:left;line-height:1.6;
|
|
1501
|
-
`;
|
|
1502
|
-
wrapper.appendChild(failureMsg);
|
|
1503
|
-
// ββ Action buttons βββββββββββββββββββββββββββββββββββββββββββββββ
|
|
1504
|
-
const btnRow = document.createElement("div");
|
|
1505
|
-
btnRow.style.cssText = "display:flex;flex-direction:column;gap:8px;width:100%;";
|
|
1506
|
-
const doneBtn = this.createPrimaryButton("Done");
|
|
1507
|
-
doneBtn.setAttribute("aria-label", "Close verification and continue");
|
|
1508
|
-
let finalStatus = submitResult.status;
|
|
1509
|
-
doneBtn.addEventListener("click", () => {
|
|
1510
|
-
if (this._pollStatusTimer) {
|
|
1511
|
-
clearTimeout(this._pollStatusTimer);
|
|
1512
|
-
this._pollStatusTimer = null;
|
|
1513
|
-
}
|
|
1514
|
-
this.options.onComplete?.({
|
|
1515
|
-
entryId: submitResult.id,
|
|
1516
|
-
sessionId: this.sdk.sessionToken,
|
|
1517
|
-
status: finalStatus,
|
|
1518
|
-
submittedAt: submitResult.submitted_at,
|
|
1519
|
-
verificationResult: this.verificationResult || undefined,
|
|
1520
|
-
});
|
|
1521
|
-
this.close();
|
|
1522
|
-
});
|
|
1523
|
-
btnRow.appendChild(doneBtn);
|
|
1524
|
-
// "Try again" button β shown only on failure, hidden initially
|
|
1525
|
-
const retryBtn = this.createSecondaryButton("Try Again");
|
|
1526
|
-
retryBtn.setAttribute("aria-label", "Start verification again");
|
|
1527
|
-
retryBtn.style.display = "none";
|
|
1528
|
-
retryBtn.addEventListener("click", () => {
|
|
1529
|
-
if (this._pollStatusTimer) {
|
|
1530
|
-
clearTimeout(this._pollStatusTimer);
|
|
1531
|
-
this._pollStatusTimer = null;
|
|
1532
|
-
}
|
|
1533
|
-
this.initialize();
|
|
1534
|
-
});
|
|
1535
|
-
btnRow.appendChild(retryBtn);
|
|
1536
|
-
wrapper.appendChild(btnRow);
|
|
1537
|
-
this.contentArea.appendChild(wrapper);
|
|
1538
|
-
requestAnimationFrame(() => {
|
|
1539
|
-
wrapper.style.opacity = "1";
|
|
1540
|
-
wrapper.style.transform = "scale(1)";
|
|
1541
|
-
});
|
|
1542
|
-
// ββ Start polling ββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
1543
|
-
this._startStatusPolling(submitResult.id, iconWrap, statusBadge, title, desc, resultCard, nameRow, docTypeRow, failureMsg, retryBtn, (resolved) => { finalStatus = resolved; });
|
|
1544
|
-
}
|
|
1545
|
-
_startStatusPolling(entryId, iconWrap, statusBadge, title, desc, resultCard, nameRow, docTypeRow, failureMsg, retryBtn, onResolved) {
|
|
1546
|
-
const MAX_POLLS = 25; // 25 Γ 3s = 75s + 4s initial β 79s max
|
|
1547
|
-
let polls = 0;
|
|
1548
|
-
// Hard 30s UI timer β stop spinner and tell user to wait even if backend is slower
|
|
1549
|
-
let uiTimeoutId = setTimeout(() => {
|
|
1550
|
-
if (!this.isDestroyed)
|
|
1551
|
-
applyTimeout();
|
|
1552
|
-
}, 30000);
|
|
1553
|
-
const clearUiTimeout = () => {
|
|
1554
|
-
if (uiTimeoutId) {
|
|
1555
|
-
clearTimeout(uiTimeoutId);
|
|
1556
|
-
uiTimeoutId = null;
|
|
1557
|
-
}
|
|
1558
|
-
};
|
|
1559
|
-
const applyTimeout = () => {
|
|
1560
|
-
if (this._pollStatusTimer) {
|
|
1561
|
-
clearTimeout(this._pollStatusTimer);
|
|
1562
|
-
this._pollStatusTimer = null;
|
|
1563
|
-
}
|
|
1564
|
-
iconWrap.innerHTML = "β³";
|
|
1565
|
-
iconWrap.style.border = `3px solid ${this.theme.warning}`;
|
|
1566
|
-
statusBadge.style.color = this.theme.textMuted;
|
|
1567
|
-
statusBadge.textContent = "Still processingβ¦";
|
|
1568
|
-
title.textContent = "Still verifying";
|
|
1569
|
-
desc.textContent = "This is taking a little longer than usual. You can safely close this β your result will be available in your account.";
|
|
1570
|
-
};
|
|
1571
|
-
const applyPassed = (result) => {
|
|
1572
|
-
clearUiTimeout();
|
|
1573
|
-
iconWrap.innerHTML = "β";
|
|
1574
|
-
iconWrap.style.cssText += `background:${this.theme.successBg};border:3px solid ${this.theme.success};color:${this.theme.success};font-size:36px;font-weight:700;`;
|
|
1575
|
-
resultCard.style.borderColor = this.theme.success;
|
|
1576
|
-
statusBadge.style.color = this.theme.success;
|
|
1577
|
-
statusBadge.textContent = "Verified β";
|
|
1578
|
-
title.textContent = "Identity Verified";
|
|
1579
|
-
desc.textContent = "Your identity has been confirmed successfully.";
|
|
1580
|
-
// ββ Show name if available βββββββββββββββββββββββββββββββββββββββ
|
|
1581
|
-
// Pull from IVS customer_profile (data verification) or
|
|
1582
|
-
// document OCR result (document verification) β whichever has data.
|
|
1583
|
-
const profile = result?.customer_profile || result?.data || {};
|
|
1584
|
-
const docResult = result?.document || {};
|
|
1585
|
-
const ocr = docResult?.ocr_result || {};
|
|
1586
|
-
const firstName = (profile?.first_name || ocr?.first_name || docResult?.first_name || "").trim();
|
|
1587
|
-
const lastName = (profile?.last_name || profile?.surname ||
|
|
1588
|
-
ocr?.last_name || ocr?.surname || docResult?.last_name || "").trim();
|
|
1589
|
-
const fullName = [firstName, lastName].filter(Boolean).join(" ");
|
|
1590
|
-
const nameEl = document.getElementById("sc-result-name");
|
|
1591
|
-
if (fullName && nameEl) {
|
|
1592
|
-
nameEl.textContent = fullName;
|
|
1593
|
-
nameRow.style.display = "flex";
|
|
1594
|
-
}
|
|
1595
|
-
// ββ Show ID type βββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
1596
|
-
const rawDocType = (docResult?.document_type ||
|
|
1597
|
-
this.selectedIdType ||
|
|
1598
|
-
result?.identifier_type ||
|
|
1599
|
-
"");
|
|
1600
|
-
const docTypeLabel = _formatDocType(rawDocType);
|
|
1601
|
-
const docTypeEl = document.getElementById("sc-result-doctype");
|
|
1602
|
-
if (docTypeLabel && docTypeEl) {
|
|
1603
|
-
docTypeEl.textContent = docTypeLabel;
|
|
1604
|
-
docTypeRow.style.display = "flex";
|
|
1605
|
-
}
|
|
1606
|
-
};
|
|
1607
|
-
const applyFailed = (reason) => {
|
|
1608
|
-
clearUiTimeout();
|
|
1609
|
-
iconWrap.innerHTML = "β";
|
|
1610
|
-
iconWrap.style.cssText += `background:${this.theme.errorBg};border:3px solid ${this.theme.error};color:${this.theme.error};font-size:32px;`;
|
|
1611
|
-
resultCard.style.borderColor = this.theme.error;
|
|
1612
|
-
statusBadge.style.color = this.theme.error;
|
|
1613
|
-
statusBadge.textContent = "Not verified";
|
|
1614
|
-
title.textContent = "Verification Unsuccessful";
|
|
1615
|
-
desc.textContent = "We were unable to confirm your identity this time.";
|
|
1616
|
-
// Show a clean, actionable reason β never raw technical strings
|
|
1617
|
-
const safeReason = _sanitizeUserMessage(reason);
|
|
1618
|
-
if (safeReason) {
|
|
1619
|
-
failureMsg.textContent = safeReason;
|
|
1620
|
-
failureMsg.style.display = "block";
|
|
1621
|
-
}
|
|
1622
|
-
// Show retry button on failure
|
|
1623
|
-
retryBtn.style.display = "block";
|
|
1624
|
-
};
|
|
1625
|
-
const poll = async () => {
|
|
1626
|
-
if (this.isDestroyed)
|
|
1627
|
-
return;
|
|
1628
|
-
polls++;
|
|
1629
|
-
try {
|
|
1630
|
-
const result = await this.sdk.liveness.pollResult(entryId);
|
|
1631
|
-
if (result.status === "passed") {
|
|
1632
|
-
onResolved("passed");
|
|
1633
|
-
applyPassed(result);
|
|
1634
|
-
return;
|
|
1635
|
-
}
|
|
1636
|
-
if (result.status === "failed") {
|
|
1637
|
-
onResolved("failed");
|
|
1638
|
-
applyFailed(result.failure_reason || null);
|
|
1639
|
-
return;
|
|
1640
|
-
}
|
|
1641
|
-
// Still processing β schedule next poll
|
|
1642
|
-
if (polls < MAX_POLLS && !this.isDestroyed) {
|
|
1643
|
-
this._pollStatusTimer = setTimeout(poll, 3000);
|
|
1644
|
-
}
|
|
1645
|
-
else if (!this.isDestroyed) {
|
|
1646
|
-
applyTimeout();
|
|
1647
|
-
}
|
|
1648
|
-
}
|
|
1649
|
-
catch {
|
|
1650
|
-
if (polls < MAX_POLLS && !this.isDestroyed) {
|
|
1651
|
-
this._pollStatusTimer = setTimeout(poll, 5000);
|
|
1652
|
-
}
|
|
1653
|
-
else if (!this.isDestroyed) {
|
|
1654
|
-
applyTimeout();
|
|
1655
|
-
}
|
|
1656
|
-
}
|
|
1657
|
-
};
|
|
1658
|
-
// First poll after 4s β gives Celery task time to start
|
|
1659
|
-
this._pollStatusTimer = setTimeout(poll, 4000);
|
|
1660
|
-
}
|
|
1661
1785
|
renderResult(_container) {
|
|
1662
|
-
//
|
|
1786
|
+
// C2: Dead code Γ’β¬β flow routes to 'done' step, not 'result'. Kept as stub.
|
|
1663
1787
|
}
|
|
1664
|
-
//
|
|
1788
|
+
// Γ’ββ¬Γ’ββ¬ Error Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬
|
|
1665
1789
|
showErrorStep(message) {
|
|
1666
1790
|
this.currentStep = "error";
|
|
1667
1791
|
if (!this.contentArea)
|
|
@@ -1680,13 +1804,13 @@ class SmartComplyFlow {
|
|
|
1680
1804
|
const isDocQuality = /blurry|blur|quality|glare|dark|bright|lighting/i.test(message);
|
|
1681
1805
|
const isExpiredDoc = /document.*expired|expired.*document/i.test(message);
|
|
1682
1806
|
const isFaceMatch = /face.*match|selfie.*match|does not match/i.test(message);
|
|
1683
|
-
const errorIcon = isCameraErr ? "
|
|
1684
|
-
: isNetworkErr ? "
|
|
1685
|
-
: isAuthErr ? "
|
|
1686
|
-
: isDocQuality ? "
|
|
1687
|
-
: isExpiredDoc ? "
|
|
1688
|
-
: isFaceMatch ? "
|
|
1689
|
-
: "
|
|
1807
|
+
const errorIcon = isCameraErr ? "Γ°ΕΈβΒ·"
|
|
1808
|
+
: isNetworkErr ? "Γ°ΕΈβΒ‘"
|
|
1809
|
+
: isAuthErr ? "Γ’ΒΒ±"
|
|
1810
|
+
: isDocQuality ? "Γ°ΕΈβΒ"
|
|
1811
|
+
: isExpiredDoc ? "Γ°ΕΈββ¦"
|
|
1812
|
+
: isFaceMatch ? "🀳"
|
|
1813
|
+
: "Γ’Εβ’";
|
|
1690
1814
|
const errorTitle = isCameraErr ? "Camera Access Required"
|
|
1691
1815
|
: isNetworkErr ? "Connection Problem"
|
|
1692
1816
|
: isAuthErr ? "Session Expired"
|
|
@@ -1694,7 +1818,7 @@ class SmartComplyFlow {
|
|
|
1694
1818
|
: isExpiredDoc ? "Document Expired"
|
|
1695
1819
|
: isFaceMatch ? "Face Match Failed"
|
|
1696
1820
|
: "Verification Unsuccessful";
|
|
1697
|
-
// Map to user-facing guidance
|
|
1821
|
+
// Map to user-facing guidance Γ’β¬β never show raw internal message
|
|
1698
1822
|
const userMessage = isCameraErr
|
|
1699
1823
|
? "Please allow camera access in your browser settings and try again."
|
|
1700
1824
|
: isNetworkErr
|
|
@@ -1741,9 +1865,9 @@ class SmartComplyFlow {
|
|
|
1741
1865
|
wrapper.appendChild(closeBtn);
|
|
1742
1866
|
this.contentArea.appendChild(wrapper);
|
|
1743
1867
|
}
|
|
1744
|
-
//
|
|
1868
|
+
// Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬
|
|
1745
1869
|
// UI Helpers
|
|
1746
|
-
//
|
|
1870
|
+
// Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬
|
|
1747
1871
|
createHeader() {
|
|
1748
1872
|
const header = document.createElement("div");
|
|
1749
1873
|
header.setAttribute("role", "banner");
|
|
@@ -1797,7 +1921,7 @@ class SmartComplyFlow {
|
|
|
1797
1921
|
header.appendChild(this.stepLabel);
|
|
1798
1922
|
// Close button
|
|
1799
1923
|
const closeBtn = document.createElement("button");
|
|
1800
|
-
closeBtn.innerHTML = "
|
|
1924
|
+
closeBtn.innerHTML = "Γ’Εβ’";
|
|
1801
1925
|
closeBtn.setAttribute("aria-label", "Close identity verification");
|
|
1802
1926
|
closeBtn.style.cssText = `
|
|
1803
1927
|
width:32px;height:32px;border-radius:8px;flex-shrink:0;
|
|
@@ -1878,7 +2002,7 @@ class SmartComplyFlow {
|
|
|
1878
2002
|
}
|
|
1879
2003
|
const arrow = document.createElement("span");
|
|
1880
2004
|
arrow.style.cssText = `color:${this.theme.textMuted};font-size:18px;flex-shrink:0;`;
|
|
1881
|
-
arrow.textContent = "
|
|
2005
|
+
arrow.textContent = "Γ’β β";
|
|
1882
2006
|
card.appendChild(textWrap);
|
|
1883
2007
|
card.appendChild(arrow);
|
|
1884
2008
|
return card;
|
|
@@ -1923,7 +2047,7 @@ class SmartComplyFlow {
|
|
|
1923
2047
|
return btn;
|
|
1924
2048
|
}
|
|
1925
2049
|
isDocumentFlow() {
|
|
1926
|
-
// 1. Check verification_type
|
|
2050
|
+
// 1. Check verification_type Γ’β¬β handle both string and array from backend
|
|
1927
2051
|
const vType = this.sdkConfig?.verification_type;
|
|
1928
2052
|
if (vType) {
|
|
1929
2053
|
const types = Array.isArray(vType) ? vType : [vType];
|
|
@@ -1932,7 +2056,7 @@ class SmartComplyFlow {
|
|
|
1932
2056
|
if (types.includes("data_verification"))
|
|
1933
2057
|
return false;
|
|
1934
2058
|
}
|
|
1935
|
-
// 2. Fallback: inspect channel fields
|
|
2059
|
+
// 2. Fallback: inspect channel fields Γ’β¬β any upload field = document flow
|
|
1936
2060
|
const channels = this.sdkConfig?.channels || {};
|
|
1937
2061
|
const allFields = Object.values(channels)
|
|
1938
2062
|
.flat()
|
|
@@ -1943,38 +2067,47 @@ class SmartComplyFlow {
|
|
|
1943
2067
|
const isDocFlow = this.isDocumentFlow();
|
|
1944
2068
|
const countries = Object.keys(this.sdkConfig?.channels || {});
|
|
1945
2069
|
const singleCountry = countries.length <= 1;
|
|
1946
|
-
//
|
|
1947
|
-
const
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
const
|
|
2070
|
+
// Two-stage action steps Γ’β¬β Stage 1: validate identity, Stage 2: liveness
|
|
2071
|
+
const stage1Steps = isDocFlow
|
|
2072
|
+
? ["country", "id_type", "document_capture", "ocr_gate", "document_confirm"].filter(s => !(s === "country" && singleCountry))
|
|
2073
|
+
: ["country", "id_type", "id_input", "confirm_identity"].filter(s => !(s === "country" && singleCountry));
|
|
2074
|
+
const stage2Steps = ["liveness"];
|
|
2075
|
+
const allActionSteps = [...stage1Steps, ...stage2Steps];
|
|
2076
|
+
const totalSteps = allActionSteps.length;
|
|
2077
|
+
const actionIndex = allActionSteps.indexOf(this.currentStep);
|
|
2078
|
+
const pct = this.currentStep === "done" || this.currentStep === "result" ? 100
|
|
1951
2079
|
: actionIndex >= 0 ? ((actionIndex + 1) / totalSteps) * 100
|
|
1952
2080
|
: 0;
|
|
1953
2081
|
if (this.progressBar) {
|
|
1954
2082
|
this.progressBar.style.width = `${pct}%`;
|
|
1955
2083
|
}
|
|
2084
|
+
// Stage label: "Stage 1 of 2: Verify Identity" / "Stage 2 of 2: Face Check"
|
|
1956
2085
|
if (this.stepLabel) {
|
|
1957
2086
|
if (this.currentStep === "loading" || this.currentStep === "welcome") {
|
|
1958
2087
|
this.stepLabel.textContent = "";
|
|
1959
2088
|
}
|
|
1960
|
-
else if (this.currentStep === "result") {
|
|
1961
|
-
this.stepLabel.textContent = "Complete
|
|
2089
|
+
else if (this.currentStep === "done" || this.currentStep === "result") {
|
|
2090
|
+
this.stepLabel.textContent = "Complete Γ’Εβ";
|
|
2091
|
+
}
|
|
2092
|
+
else if (stage2Steps.includes(this.currentStep)) {
|
|
2093
|
+
this.stepLabel.textContent = "Stage 2 of 2: Face Check";
|
|
1962
2094
|
}
|
|
1963
|
-
else if (
|
|
1964
|
-
this.stepLabel.textContent =
|
|
2095
|
+
else if (stage1Steps.includes(this.currentStep)) {
|
|
2096
|
+
this.stepLabel.textContent = isDocFlow ? "Stage 1 of 2: Scan Document" : "Stage 1 of 2: Verify Identity";
|
|
1965
2097
|
}
|
|
1966
2098
|
else {
|
|
1967
2099
|
this.stepLabel.textContent = "";
|
|
1968
2100
|
}
|
|
1969
2101
|
}
|
|
1970
|
-
// Back button
|
|
2102
|
+
// Back button Γ’β¬β show for navigable steps only
|
|
1971
2103
|
if (this.headerBackBtn) {
|
|
1972
|
-
const countries = Object.keys(this.sdkConfig?.channels || {});
|
|
1973
2104
|
const backTargets = {
|
|
1974
2105
|
id_type: () => this.showStep(countries.length > 1 ? "country" : "welcome"),
|
|
1975
2106
|
country: () => this.showStep("welcome"),
|
|
1976
2107
|
id_input: () => this.showStep("id_type"),
|
|
2108
|
+
confirm_identity: () => this.showStep("id_input"),
|
|
1977
2109
|
document_capture: () => { this.docCapture?.destroy(); this.showStep("id_type"); },
|
|
2110
|
+
document_confirm: () => { this.docCapture?.destroy(); this.showStep("document_capture"); },
|
|
1978
2111
|
};
|
|
1979
2112
|
const backAction = backTargets[this.currentStep];
|
|
1980
2113
|
if (backAction) {
|
|
@@ -2045,4 +2178,5 @@ class SmartComplyFlow {
|
|
|
2045
2178
|
}
|
|
2046
2179
|
}
|
|
2047
2180
|
exports.SmartComplyFlow = SmartComplyFlow;
|
|
2181
|
+
SmartComplyFlow.MAX_RETRIES = 3;
|
|
2048
2182
|
//# sourceMappingURL=SmartComplyFlow.js.map
|