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