smartcomply-web-sdk 1.0.3 → 1.0.4
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/README.md +43 -0
- package/dist/esm/camera/ActionDetector.d.ts +26 -0
- package/dist/esm/camera/ActionDetector.d.ts.map +1 -0
- package/dist/esm/camera/ActionDetector.js +110 -0
- package/dist/esm/camera/ActionDetector.js.map +1 -0
- package/dist/esm/camera/CameraManager.d.ts +7 -0
- package/dist/esm/camera/CameraManager.d.ts.map +1 -0
- package/dist/esm/camera/CameraManager.js +27 -0
- package/dist/esm/camera/CameraManager.js.map +1 -0
- package/dist/esm/camera/FaceDetector.d.ts +12 -0
- package/dist/esm/camera/FaceDetector.d.ts.map +1 -0
- package/dist/esm/camera/FaceDetector.js +51 -0
- package/dist/esm/camera/FaceDetector.js.map +1 -0
- package/dist/esm/camera/Permission.d.ts +42 -0
- package/dist/esm/camera/Permission.d.ts.map +1 -0
- package/dist/esm/camera/Permission.js +101 -0
- package/dist/esm/camera/Permission.js.map +1 -0
- package/dist/esm/camera/VideoRecorder.d.ts +12 -0
- package/dist/esm/camera/VideoRecorder.d.ts.map +1 -0
- package/dist/esm/camera/VideoRecorder.js +47 -0
- package/dist/esm/camera/VideoRecorder.js.map +1 -0
- package/dist/esm/client/Config.d.ts +9 -0
- package/dist/esm/client/Config.d.ts.map +1 -0
- package/dist/esm/client/Config.js +5 -0
- package/dist/esm/client/Config.js.map +1 -0
- package/dist/esm/client/HttpClient.d.ts +44 -0
- package/dist/esm/client/HttpClient.d.ts.map +1 -0
- package/dist/esm/client/HttpClient.js +145 -0
- package/dist/esm/client/HttpClient.js.map +1 -0
- package/dist/esm/client/Smartcomply.d.ts +50 -0
- package/dist/esm/client/Smartcomply.d.ts.map +1 -0
- package/dist/esm/client/Smartcomply.js +73 -0
- package/dist/esm/client/Smartcomply.js.map +1 -0
- package/dist/esm/components/CameraView.d.ts +59 -0
- package/dist/esm/components/CameraView.d.ts.map +1 -0
- package/dist/esm/components/CameraView.js +238 -0
- package/dist/esm/components/CameraView.js.map +1 -0
- package/dist/esm/components/DocumentUpload.d.ts +21 -0
- package/dist/esm/components/DocumentUpload.d.ts.map +1 -0
- package/dist/esm/components/DocumentUpload.js +216 -0
- package/dist/esm/components/DocumentUpload.js.map +1 -0
- package/dist/esm/errors/AuthError.d.ts +8 -0
- package/dist/esm/errors/AuthError.d.ts.map +1 -0
- package/dist/esm/errors/AuthError.js +11 -0
- package/dist/esm/errors/AuthError.js.map +1 -0
- package/dist/esm/errors/NetworkError.d.ts +8 -0
- package/dist/esm/errors/NetworkError.d.ts.map +1 -0
- package/dist/esm/errors/NetworkError.js +11 -0
- package/dist/esm/errors/NetworkError.js.map +1 -0
- package/dist/esm/errors/SDKError.d.ts +7 -0
- package/dist/esm/errors/SDKError.d.ts.map +1 -0
- package/dist/esm/errors/SDKError.js +10 -0
- package/dist/esm/errors/SDKError.js.map +1 -0
- package/dist/esm/flow/DocumentCapture.d.ts +54 -0
- package/dist/esm/flow/DocumentCapture.d.ts.map +1 -0
- package/dist/esm/flow/DocumentCapture.js +797 -0
- package/dist/esm/flow/DocumentCapture.js.map +1 -0
- package/dist/esm/flow/SmartComplyFlow.d.ts +104 -0
- package/dist/esm/flow/SmartComplyFlow.d.ts.map +1 -0
- package/dist/esm/flow/SmartComplyFlow.js +1722 -0
- package/dist/esm/flow/SmartComplyFlow.js.map +1 -0
- package/dist/esm/flow/theme.d.ts +30 -0
- package/dist/esm/flow/theme.d.ts.map +1 -0
- package/dist/esm/flow/theme.js +99 -0
- package/dist/esm/flow/theme.js.map +1 -0
- package/dist/esm/index.d.ts +20 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +15 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/modules/liveness/LivenessUI.d.ts +46 -0
- package/dist/esm/modules/liveness/LivenessUI.d.ts.map +1 -0
- package/dist/esm/modules/liveness/LivenessUI.js +605 -0
- package/dist/esm/modules/liveness/LivenessUI.js.map +1 -0
- package/dist/esm/modules/liveness/index.d.ts +5 -0
- package/dist/esm/modules/liveness/index.d.ts.map +1 -0
- package/dist/esm/modules/liveness/index.js +4 -0
- package/dist/esm/modules/liveness/index.js.map +1 -0
- package/dist/esm/modules/liveness/liveness.d.ts +85 -0
- package/dist/esm/modules/liveness/liveness.d.ts.map +1 -0
- package/dist/esm/modules/liveness/liveness.js +421 -0
- package/dist/esm/modules/liveness/liveness.js.map +1 -0
- package/dist/esm/modules/liveness/uploader.d.ts +34 -0
- package/dist/esm/modules/liveness/uploader.d.ts.map +1 -0
- package/dist/esm/modules/liveness/uploader.js +102 -0
- package/dist/esm/modules/liveness/uploader.js.map +1 -0
- package/dist/esm/modules/onboarding/index.d.ts +2 -0
- package/dist/esm/modules/onboarding/index.d.ts.map +1 -0
- package/dist/esm/modules/onboarding/index.js +2 -0
- package/dist/esm/modules/onboarding/index.js.map +1 -0
- package/dist/esm/modules/onboarding/onboarding.d.ts +25 -0
- package/dist/esm/modules/onboarding/onboarding.d.ts.map +1 -0
- package/dist/esm/modules/onboarding/onboarding.js +29 -0
- package/dist/esm/modules/onboarding/onboarding.js.map +1 -0
- package/dist/esm/types/common.d.ts +27 -0
- package/dist/esm/types/common.d.ts.map +1 -0
- package/dist/esm/types/common.js +2 -0
- package/dist/esm/types/common.js.map +1 -0
- package/dist/esm/types/liveness.d.ts +142 -0
- package/dist/esm/types/liveness.d.ts.map +1 -0
- package/dist/esm/types/liveness.js +2 -0
- package/dist/esm/types/liveness.js.map +1 -0
- package/dist/esm/types/onboarding.d.ts +53 -0
- package/dist/esm/types/onboarding.d.ts.map +1 -0
- package/dist/esm/types/onboarding.js +2 -0
- package/dist/esm/types/onboarding.js.map +1 -0
- package/dist/esm/types/quality.d.ts +23 -0
- package/dist/esm/types/quality.d.ts.map +1 -0
- package/dist/esm/types/quality.js +5 -0
- package/dist/esm/types/quality.js.map +1 -0
- package/dist/esm/utils/ImageQuality.d.ts +48 -0
- package/dist/esm/utils/ImageQuality.d.ts.map +1 -0
- package/dist/esm/utils/ImageQuality.js +177 -0
- package/dist/esm/utils/ImageQuality.js.map +1 -0
- package/dist/esm/utils/constants.d.ts +59 -0
- package/dist/esm/utils/constants.d.ts.map +1 -0
- package/dist/esm/utils/constants.js +67 -0
- package/dist/esm/utils/constants.js.map +1 -0
- package/dist/esm/utils/device.d.ts +14 -0
- package/dist/esm/utils/device.d.ts.map +1 -0
- package/dist/esm/utils/device.js +52 -0
- package/dist/esm/utils/device.js.map +1 -0
- package/dist/esm/utils/logger.d.ts +28 -0
- package/dist/esm/utils/logger.d.ts.map +1 -0
- package/dist/esm/utils/logger.js +64 -0
- package/dist/esm/utils/logger.js.map +1 -0
- package/dist/esm/utils/validator.d.ts +35 -0
- package/dist/esm/utils/validator.d.ts.map +1 -0
- package/dist/esm/utils/validator.js +102 -0
- package/dist/esm/utils/validator.js.map +1 -0
- package/package.json +13 -5
|
@@ -0,0 +1,1722 @@
|
|
|
1
|
+
import { SmartComply } from "../client/Smartcomply";
|
|
2
|
+
import { getTheme } from "./theme";
|
|
3
|
+
import { DocumentCapture } from "./DocumentCapture";
|
|
4
|
+
import { collectDeviceMetadata } from "../utils/device";
|
|
5
|
+
const STEP_ORDER = [
|
|
6
|
+
"loading",
|
|
7
|
+
"welcome",
|
|
8
|
+
"country",
|
|
9
|
+
"id_type",
|
|
10
|
+
"id_input", // or document_capture
|
|
11
|
+
"document_capture",
|
|
12
|
+
"processing",
|
|
13
|
+
"liveness",
|
|
14
|
+
"result",
|
|
15
|
+
];
|
|
16
|
+
const COUNTRY_LABELS = {
|
|
17
|
+
nigeria: "🇳🇬 Nigeria",
|
|
18
|
+
global: "🌍 Other Countries",
|
|
19
|
+
ghana: "🇬🇭 Ghana",
|
|
20
|
+
kenya: "🇰🇪 Kenya",
|
|
21
|
+
south_africa: "🇿🇦 South Africa",
|
|
22
|
+
united_states: "🇺🇸 United States",
|
|
23
|
+
united_kingdom: "🇬🇧 United Kingdom",
|
|
24
|
+
};
|
|
25
|
+
function resolveIdTypeInfo(typeName, isDocumentFlow) {
|
|
26
|
+
const u = typeName.toUpperCase();
|
|
27
|
+
const withFace = u.includes("WITH FACE") || u.includes("FACE");
|
|
28
|
+
const advanced = u.includes("ADVANCED");
|
|
29
|
+
// Helper function to check if document requires back side
|
|
30
|
+
const requiresBackSide = (type) => {
|
|
31
|
+
const t = type.toLowerCase();
|
|
32
|
+
return t.includes("voter") ||
|
|
33
|
+
t.includes("nin") ||
|
|
34
|
+
t.includes("driver") ||
|
|
35
|
+
t.includes("license") ||
|
|
36
|
+
t.includes("national id") ||
|
|
37
|
+
t.includes("national id card");
|
|
38
|
+
};
|
|
39
|
+
// BVN — always 11 digits
|
|
40
|
+
if (u.includes("BVN") || u.includes("BANK VERIFICATION")) {
|
|
41
|
+
const extra = withFace ? " + selfie photo" : "";
|
|
42
|
+
return { icon: "🏦", desc: `Enter your 11-digit BVN${extra}`, requiresBack: false };
|
|
43
|
+
}
|
|
44
|
+
// NIN — always 11 digits
|
|
45
|
+
if (u.includes("NIN") || u.includes("NATIONAL IDENTITY NUMBER")) {
|
|
46
|
+
const extra = withFace ? " + selfie photo" : "";
|
|
47
|
+
return { icon: "🆔", desc: `Enter your 11-digit NIN${extra}`, requiresBack: false };
|
|
48
|
+
}
|
|
49
|
+
// Ghana Card / Ghana National ID
|
|
50
|
+
if (u.includes("GHANA")) {
|
|
51
|
+
if (isDocumentFlow)
|
|
52
|
+
return { icon: "🪪", desc: "Capture or upload your Ghana Card", requiresBack: true };
|
|
53
|
+
const extra = withFace ? " + selfie photo" : "";
|
|
54
|
+
return { icon: "🪪", desc: `Enter your Ghana Card number${extra}`, requiresBack: false };
|
|
55
|
+
}
|
|
56
|
+
// SSNIT (Ghana social security)
|
|
57
|
+
if (u.includes("SSNIT"))
|
|
58
|
+
return { icon: "🏛️", desc: "Enter your SSNIT number", requiresBack: false };
|
|
59
|
+
// Kenya National ID
|
|
60
|
+
if (u.includes("KENYA") || (u.includes("NATIONAL") && u.includes("KENYA")))
|
|
61
|
+
return { icon: "🆔", desc: "Enter your Kenya National ID number", requiresBack: false };
|
|
62
|
+
// TIN
|
|
63
|
+
if (u.includes("TIN") || u.includes("TAX IDENTIFICATION"))
|
|
64
|
+
return { icon: "🧾", desc: "Enter your Tax Identification Number (TIN)", requiresBack: false };
|
|
65
|
+
// CAC / Company registration
|
|
66
|
+
if (u.includes("CAC") || u.includes("CORPORATE AFFAIRS") || u.includes("COMPANY"))
|
|
67
|
+
return { icon: "🏢", desc: advanced ? "Enter RC number, company name & type" : "Enter your company registration number", requiresBack: false };
|
|
68
|
+
// Voter / PVC
|
|
69
|
+
if (u.includes("VOTER") || u.includes("PVC")) {
|
|
70
|
+
const requiresBack = requiresBackSide(typeName);
|
|
71
|
+
return {
|
|
72
|
+
icon: "🗳️",
|
|
73
|
+
desc: isDocumentFlow
|
|
74
|
+
? `Capture or upload your voter's card${requiresBack ? " (front & back)" : ""}`
|
|
75
|
+
: "Enter your voter ID number",
|
|
76
|
+
requiresBack: isDocumentFlow ? requiresBack : false
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
// Passport
|
|
80
|
+
if (u.includes("PASSPORT"))
|
|
81
|
+
return { icon: "📘", desc: isDocumentFlow ? "Capture or upload your passport" : "Enter your passport number", requiresBack: false };
|
|
82
|
+
// Driver's license
|
|
83
|
+
if (u.includes("DRIVER") || u.includes("LICENSE") || u.includes("LICENCE")) {
|
|
84
|
+
const requiresBack = requiresBackSide(typeName);
|
|
85
|
+
return {
|
|
86
|
+
icon: "🚗",
|
|
87
|
+
desc: isDocumentFlow
|
|
88
|
+
? `Capture or upload your driver's license${requiresBack ? " (front & back)" : ""}`
|
|
89
|
+
: "Enter your driver's license number",
|
|
90
|
+
requiresBack: isDocumentFlow ? requiresBack : false
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
// National ID card (generic)
|
|
94
|
+
if (u.includes("NATIONAL ID") || (u.includes("NATIONAL") && u.includes("CARD"))) {
|
|
95
|
+
const requiresBack = requiresBackSide(typeName);
|
|
96
|
+
return {
|
|
97
|
+
icon: "🪪",
|
|
98
|
+
desc: isDocumentFlow
|
|
99
|
+
? `Capture or upload your ID card${requiresBack ? " (front & back)" : ""}`
|
|
100
|
+
: "Enter your National ID number",
|
|
101
|
+
requiresBack: isDocumentFlow ? requiresBack : false
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
// Face liveness / comparison
|
|
105
|
+
if (u.includes("LIVENESS") || u.includes("FACE COMPARISON") || u.includes("FACE MATCH"))
|
|
106
|
+
return { icon: "🤳", desc: "We'll capture a short selfie video to verify you", requiresBack: false };
|
|
107
|
+
return {
|
|
108
|
+
icon: isDocumentFlow ? "📄" : "🆔",
|
|
109
|
+
desc: isDocumentFlow ? "Capture or upload your document" : "Enter your ID number",
|
|
110
|
+
requiresBack: false,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
114
|
+
// SmartComplyFlow
|
|
115
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
116
|
+
// Add CSS animation keyframes for spin animations
|
|
117
|
+
const style = document.createElement('style');
|
|
118
|
+
style.textContent = `
|
|
119
|
+
@keyframes sc-spin {
|
|
120
|
+
0% { transform: rotate(0deg); }
|
|
121
|
+
100% { transform: rotate(360deg); }
|
|
122
|
+
}
|
|
123
|
+
`;
|
|
124
|
+
if (!document.head.querySelector('style[data-sc-animations]')) {
|
|
125
|
+
style.setAttribute('data-sc-animations', 'true');
|
|
126
|
+
document.head.appendChild(style);
|
|
127
|
+
}
|
|
128
|
+
export class SmartComplyFlow {
|
|
129
|
+
constructor(options) {
|
|
130
|
+
this.theme = getTheme("default");
|
|
131
|
+
// DOM
|
|
132
|
+
this.overlay = null;
|
|
133
|
+
this.modal = null;
|
|
134
|
+
this.contentArea = null;
|
|
135
|
+
this.progressBar = null;
|
|
136
|
+
this.stepLabel = null;
|
|
137
|
+
this.brandLabel = null;
|
|
138
|
+
// State
|
|
139
|
+
this.sdkConfig = null;
|
|
140
|
+
this.currentStep = "loading";
|
|
141
|
+
this.selectedCountry = "";
|
|
142
|
+
this.selectedChannelId = 0;
|
|
143
|
+
this.selectedChannelFields = [];
|
|
144
|
+
this.selectedIdType = ""; // Canonical code (e.g., "national_id", "nin_with_face")
|
|
145
|
+
this.selectedIdTypeName = ""; // Display name (e.g., "National Identity Number with Face (NIN)")
|
|
146
|
+
this.collectedFields = {};
|
|
147
|
+
this.idNumber = "";
|
|
148
|
+
this.documentBlob = null;
|
|
149
|
+
this.documentBackBlob = null;
|
|
150
|
+
this.identityCheckId = null;
|
|
151
|
+
this.verificationResult = null;
|
|
152
|
+
this.livenessEntryId = null;
|
|
153
|
+
this.isDestroyed = false;
|
|
154
|
+
this.headerBackBtn = null;
|
|
155
|
+
// Session expiry
|
|
156
|
+
this.sessionExpiryTimer = null;
|
|
157
|
+
// Status polling (result screen)
|
|
158
|
+
this._pollStatusTimer = null;
|
|
159
|
+
// Components
|
|
160
|
+
this.docCapture = null;
|
|
161
|
+
this.options = options;
|
|
162
|
+
this.sdk = new SmartComply({
|
|
163
|
+
apiKey: options.apiKey,
|
|
164
|
+
clientId: options.clientId,
|
|
165
|
+
environment: options.environment || "sandbox",
|
|
166
|
+
timeout: options.timeout,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Open the SmartComply verification flow.
|
|
171
|
+
*
|
|
172
|
+
* Usage:
|
|
173
|
+
* SmartComplyFlow.open({
|
|
174
|
+
* apiKey: "pk_live_...",
|
|
175
|
+
* clientId: "uuid-...",
|
|
176
|
+
* environment: "production",
|
|
177
|
+
* onComplete: (result) => console.log("Done!", result),
|
|
178
|
+
* onError: (err) => console.error(err),
|
|
179
|
+
* onClose: () => console.log("Closed"),
|
|
180
|
+
* });
|
|
181
|
+
*/
|
|
182
|
+
static open(options) {
|
|
183
|
+
const flow = new SmartComplyFlow(options);
|
|
184
|
+
flow.mount();
|
|
185
|
+
flow.initialize();
|
|
186
|
+
return flow;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Close and destroy the flow widget.
|
|
190
|
+
*/
|
|
191
|
+
close() {
|
|
192
|
+
this.isDestroyed = true;
|
|
193
|
+
this.docCapture?.destroy();
|
|
194
|
+
if (this.sessionExpiryTimer) {
|
|
195
|
+
clearTimeout(this.sessionExpiryTimer);
|
|
196
|
+
this.sessionExpiryTimer = null;
|
|
197
|
+
}
|
|
198
|
+
if (this._pollStatusTimer) {
|
|
199
|
+
clearTimeout(this._pollStatusTimer);
|
|
200
|
+
this._pollStatusTimer = null;
|
|
201
|
+
}
|
|
202
|
+
if (this.overlay) {
|
|
203
|
+
this.overlay.style.opacity = "0";
|
|
204
|
+
setTimeout(() => {
|
|
205
|
+
if (this.overlay?.parentElement) {
|
|
206
|
+
this.overlay.parentElement.removeChild(this.overlay);
|
|
207
|
+
}
|
|
208
|
+
this.overlay = null;
|
|
209
|
+
this.modal = null;
|
|
210
|
+
this.contentArea = null;
|
|
211
|
+
}, 300);
|
|
212
|
+
}
|
|
213
|
+
this.options.onClose?.();
|
|
214
|
+
}
|
|
215
|
+
// ─────────────────────────────────────────────────────────────────
|
|
216
|
+
// Initialization
|
|
217
|
+
// ─────────────────────────────────────────────────────────────────
|
|
218
|
+
mount() {
|
|
219
|
+
// Inject keyframes
|
|
220
|
+
this.injectStyles();
|
|
221
|
+
// Overlay
|
|
222
|
+
this.overlay = document.createElement("div");
|
|
223
|
+
this.overlay.id = "sc-flow-overlay";
|
|
224
|
+
this.overlay.style.cssText = `
|
|
225
|
+
position:fixed;inset:0;z-index:99999;
|
|
226
|
+
display:flex;align-items:center;justify-content:center;
|
|
227
|
+
background:${this.theme.overlay};
|
|
228
|
+
backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);
|
|
229
|
+
opacity:0;transition:opacity 0.3s ease;
|
|
230
|
+
font-family:'Inter','Segoe UI',system-ui,-apple-system,sans-serif;
|
|
231
|
+
`;
|
|
232
|
+
// Modal
|
|
233
|
+
this.modal = document.createElement("div");
|
|
234
|
+
this.modal.id = "sc-flow-modal";
|
|
235
|
+
this.modal.style.cssText = `
|
|
236
|
+
width:100%;max-width:440px;max-height:92vh;
|
|
237
|
+
background:${this.theme.bg};
|
|
238
|
+
border-radius:20px;overflow:hidden;
|
|
239
|
+
box-shadow:${this.theme.shadow};
|
|
240
|
+
display:flex;flex-direction:column;
|
|
241
|
+
transform:translateY(20px) scale(0.97);
|
|
242
|
+
transition:transform 0.35s cubic-bezier(0.16,1,0.3,1);
|
|
243
|
+
`;
|
|
244
|
+
// Header
|
|
245
|
+
const header = this.createHeader();
|
|
246
|
+
this.modal.appendChild(header);
|
|
247
|
+
// Progress bar
|
|
248
|
+
const progressWrap = document.createElement("div");
|
|
249
|
+
progressWrap.style.cssText = `height:3px;background:${this.theme.shimmer};`;
|
|
250
|
+
this.progressBar = document.createElement("div");
|
|
251
|
+
this.progressBar.style.cssText = `
|
|
252
|
+
height:100%;width:0%;background:linear-gradient(90deg,${this.theme.primary},${this.theme.primaryHover});
|
|
253
|
+
border-radius:0 2px 2px 0;transition:width 0.5s ease;
|
|
254
|
+
`;
|
|
255
|
+
progressWrap.appendChild(this.progressBar);
|
|
256
|
+
this.modal.appendChild(progressWrap);
|
|
257
|
+
// Content area (scrollable)
|
|
258
|
+
this.contentArea = document.createElement("div");
|
|
259
|
+
this.contentArea.id = "sc-flow-content";
|
|
260
|
+
this.contentArea.style.cssText = `
|
|
261
|
+
flex:1;overflow-y:auto;padding:20px 24px;
|
|
262
|
+
-webkit-overflow-scrolling:touch;
|
|
263
|
+
`;
|
|
264
|
+
this.modal.appendChild(this.contentArea);
|
|
265
|
+
// Footer
|
|
266
|
+
const footer = this.createFooter();
|
|
267
|
+
this.modal.appendChild(footer);
|
|
268
|
+
this.overlay.appendChild(this.modal);
|
|
269
|
+
document.body.appendChild(this.overlay);
|
|
270
|
+
// Animate in
|
|
271
|
+
requestAnimationFrame(() => {
|
|
272
|
+
if (this.overlay)
|
|
273
|
+
this.overlay.style.opacity = "1";
|
|
274
|
+
if (this.modal)
|
|
275
|
+
this.modal.style.transform = "translateY(0) scale(1)";
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
async initialize() {
|
|
279
|
+
this.showStep("loading");
|
|
280
|
+
try {
|
|
281
|
+
// Create session
|
|
282
|
+
await this.sdk.createSession();
|
|
283
|
+
// C4: Start session expiry timer (warn 2 min before 30-min expiry)
|
|
284
|
+
const expiresAt = this.sdk.sessionExpiresAt;
|
|
285
|
+
if (expiresAt) {
|
|
286
|
+
const expiresMs = new Date(expiresAt).getTime() - Date.now();
|
|
287
|
+
const warnMs = Math.max(expiresMs - 120000, 0); // warn 2 min before
|
|
288
|
+
this.sessionExpiryTimer = setTimeout(() => {
|
|
289
|
+
if (!this.isDestroyed && this.currentStep !== "result") {
|
|
290
|
+
this.showErrorStep("Your session is about to expire. Please complete verification quickly or restart.");
|
|
291
|
+
}
|
|
292
|
+
}, warnMs);
|
|
293
|
+
}
|
|
294
|
+
// Load config
|
|
295
|
+
this.sdkConfig = await this.sdk.initializeConfig();
|
|
296
|
+
// Apply theme from config
|
|
297
|
+
if (this.sdkConfig.theme) {
|
|
298
|
+
this.theme = getTheme(this.sdkConfig.theme);
|
|
299
|
+
this.applyTheme();
|
|
300
|
+
}
|
|
301
|
+
// Update brand name
|
|
302
|
+
if (this.brandLabel && this.sdkConfig.brand_name) {
|
|
303
|
+
this.brandLabel.textContent = this.sdkConfig.brand_name;
|
|
304
|
+
}
|
|
305
|
+
if (this.isDestroyed)
|
|
306
|
+
return;
|
|
307
|
+
this.showStep("welcome");
|
|
308
|
+
}
|
|
309
|
+
catch (err) {
|
|
310
|
+
this.showErrorStep(err.message || "Failed to initialize. Please try again.");
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
// ─────────────────────────────────────────────────────────────────
|
|
314
|
+
// Step rendering
|
|
315
|
+
// ─────────────────────────────────────────────────────────────────
|
|
316
|
+
showStep(step) {
|
|
317
|
+
this.currentStep = step;
|
|
318
|
+
if (!this.contentArea)
|
|
319
|
+
return;
|
|
320
|
+
// Update progress
|
|
321
|
+
this.updateProgress();
|
|
322
|
+
// Animate out current content
|
|
323
|
+
const oldContent = this.contentArea.firstChild;
|
|
324
|
+
if (oldContent) {
|
|
325
|
+
oldContent.style.cssText += "opacity:0;transform:translateX(-20px);transition:all 0.2s ease;";
|
|
326
|
+
setTimeout(() => {
|
|
327
|
+
if (this.contentArea)
|
|
328
|
+
this.contentArea.innerHTML = "";
|
|
329
|
+
this.renderStep(step);
|
|
330
|
+
}, 200);
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
this.renderStep(step);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
renderStep(step) {
|
|
337
|
+
if (!this.contentArea)
|
|
338
|
+
return;
|
|
339
|
+
const wrapper = document.createElement("div");
|
|
340
|
+
wrapper.style.cssText = "opacity:0;transform:translateX(20px);transition:all 0.3s ease;";
|
|
341
|
+
switch (step) {
|
|
342
|
+
case "loading":
|
|
343
|
+
this.renderLoading(wrapper);
|
|
344
|
+
break;
|
|
345
|
+
case "welcome":
|
|
346
|
+
this.renderWelcome(wrapper);
|
|
347
|
+
break;
|
|
348
|
+
case "country":
|
|
349
|
+
this.renderCountrySelect(wrapper);
|
|
350
|
+
break;
|
|
351
|
+
case "id_type":
|
|
352
|
+
this.renderIdTypeSelect(wrapper);
|
|
353
|
+
break;
|
|
354
|
+
case "id_input":
|
|
355
|
+
this.renderIdInput(wrapper);
|
|
356
|
+
break;
|
|
357
|
+
case "document_capture":
|
|
358
|
+
this.renderDocumentCapture(wrapper);
|
|
359
|
+
break;
|
|
360
|
+
case "processing":
|
|
361
|
+
this.renderProcessing(wrapper);
|
|
362
|
+
break;
|
|
363
|
+
case "liveness":
|
|
364
|
+
this.renderLiveness(wrapper);
|
|
365
|
+
break;
|
|
366
|
+
case "result":
|
|
367
|
+
this.renderResult(wrapper);
|
|
368
|
+
break;
|
|
369
|
+
case "error":
|
|
370
|
+
break; // handled separately
|
|
371
|
+
}
|
|
372
|
+
this.contentArea.appendChild(wrapper);
|
|
373
|
+
// Animate in
|
|
374
|
+
requestAnimationFrame(() => {
|
|
375
|
+
wrapper.style.opacity = "1";
|
|
376
|
+
wrapper.style.transform = "translateX(0)";
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
// ── Loading ─────────────────────────────────────────────────────
|
|
380
|
+
renderLoading(container) {
|
|
381
|
+
container.style.cssText += "display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:300px;gap:16px;";
|
|
382
|
+
const spinnerWrap = document.createElement("div");
|
|
383
|
+
spinnerWrap.style.cssText = "position:relative;width:60px;height:60px;flex-shrink:0;";
|
|
384
|
+
const ring = document.createElement("div");
|
|
385
|
+
ring.style.cssText = `
|
|
386
|
+
position:absolute;inset:0;border-radius:50%;
|
|
387
|
+
border:3px solid ${this.theme.border};
|
|
388
|
+
border-top-color:${this.theme.primary};
|
|
389
|
+
animation:sc-spin 0.9s linear infinite;
|
|
390
|
+
`;
|
|
391
|
+
const centre = document.createElement("div");
|
|
392
|
+
centre.style.cssText = `
|
|
393
|
+
position:absolute;inset:0;
|
|
394
|
+
display:flex;align-items:center;justify-content:center;
|
|
395
|
+
font-size:22px;
|
|
396
|
+
`;
|
|
397
|
+
centre.textContent = "\uD83D\uDEE1\uFE0F";
|
|
398
|
+
spinnerWrap.appendChild(ring);
|
|
399
|
+
spinnerWrap.appendChild(centre);
|
|
400
|
+
container.appendChild(spinnerWrap);
|
|
401
|
+
const label = document.createElement("div");
|
|
402
|
+
label.style.cssText = `color:${this.theme.text};font-size:15px;font-weight:600;`;
|
|
403
|
+
label.textContent = "Setting up secure session";
|
|
404
|
+
container.appendChild(label);
|
|
405
|
+
const sub = document.createElement("div");
|
|
406
|
+
sub.style.cssText = `color:${this.theme.textMuted};font-size:12px;text-align:center;max-width:220px;line-height:1.5;`;
|
|
407
|
+
sub.textContent = "Connecting to the verification service...";
|
|
408
|
+
container.appendChild(sub);
|
|
409
|
+
}
|
|
410
|
+
// ── Welcome ────────────────────────────────────────────────────
|
|
411
|
+
renderWelcome(container) {
|
|
412
|
+
container.style.cssText += "display:flex;flex-direction:column;align-items:center;text-align:center;gap:14px;";
|
|
413
|
+
const isDocFlow = this.isDocumentFlow();
|
|
414
|
+
// Icon + badge row (side by side to save vertical space)
|
|
415
|
+
const topRow = document.createElement("div");
|
|
416
|
+
topRow.style.cssText = "display:flex;flex-direction:column;align-items:center;gap:10px;";
|
|
417
|
+
const iconWrap = document.createElement("div");
|
|
418
|
+
iconWrap.style.cssText = `
|
|
419
|
+
width:58px;height:58px;border-radius:16px;
|
|
420
|
+
display:flex;align-items:center;justify-content:center;
|
|
421
|
+
font-size:26px;
|
|
422
|
+
background:linear-gradient(135deg,${this.theme.primary}22,${this.theme.primaryGlow});
|
|
423
|
+
border:1.5px solid ${this.theme.primary}33;
|
|
424
|
+
`;
|
|
425
|
+
iconWrap.textContent = isDocFlow ? "🪪" : "🛡️";
|
|
426
|
+
topRow.appendChild(iconWrap);
|
|
427
|
+
const badge = document.createElement("div");
|
|
428
|
+
badge.style.cssText = `
|
|
429
|
+
display:inline-flex;align-items:center;gap:5px;
|
|
430
|
+
padding:3px 11px;border-radius:999px;
|
|
431
|
+
font-size:10.5px;font-weight:600;letter-spacing:0.2px;
|
|
432
|
+
background:${this.theme.primary}15;
|
|
433
|
+
color:${this.theme.primary};
|
|
434
|
+
border:1px solid ${this.theme.primary}30;
|
|
435
|
+
`;
|
|
436
|
+
badge.textContent = isDocFlow ? "📄 Document Verification" : "🔍 Data Verification";
|
|
437
|
+
topRow.appendChild(badge);
|
|
438
|
+
container.appendChild(topRow);
|
|
439
|
+
// Title + subtitle (compact)
|
|
440
|
+
const titleWrap = document.createElement("div");
|
|
441
|
+
titleWrap.style.cssText = "display:flex;flex-direction:column;gap:5px;";
|
|
442
|
+
const title = document.createElement("h2");
|
|
443
|
+
title.style.cssText = `
|
|
444
|
+
color:${this.theme.text};font-size:20px;font-weight:700;
|
|
445
|
+
margin:0;line-height:1.2;
|
|
446
|
+
`;
|
|
447
|
+
title.textContent = "Identity Verification";
|
|
448
|
+
titleWrap.appendChild(title);
|
|
449
|
+
const desc = document.createElement("p");
|
|
450
|
+
desc.style.cssText = `
|
|
451
|
+
color:${this.theme.textSecondary};font-size:13px;line-height:1.5;
|
|
452
|
+
margin:0;max-width:300px;
|
|
453
|
+
`;
|
|
454
|
+
desc.textContent = isDocFlow
|
|
455
|
+
? "Scan your ID document and take a quick selfie to confirm your identity."
|
|
456
|
+
: "Enter your ID details and take a quick selfie to confirm your identity.";
|
|
457
|
+
titleWrap.appendChild(desc);
|
|
458
|
+
container.appendChild(titleWrap);
|
|
459
|
+
// Steps preview — compact rows
|
|
460
|
+
const steps = isDocFlow
|
|
461
|
+
? [
|
|
462
|
+
{ icon: "🪪", text: "Choose your document type", sub: "Passport, National ID, Driver License" },
|
|
463
|
+
{ icon: "📷", text: "Scan your document", sub: "Upload or capture a photo of your ID" },
|
|
464
|
+
{ icon: "🤳", text: "Face check", sub: "Quick selfie to confirm it's you" },
|
|
465
|
+
]
|
|
466
|
+
: [
|
|
467
|
+
{ icon: "🆔", text: "Choose your ID type", sub: "NIN, BVN, Voter's Card and more" },
|
|
468
|
+
{ icon: "📝", text: "Enter your ID details", sub: "Exactly as shown on your ID" },
|
|
469
|
+
{ icon: "🤳", text: "Face check", sub: "Quick selfie to confirm it's you" },
|
|
470
|
+
];
|
|
471
|
+
const stepsWrap = document.createElement("div");
|
|
472
|
+
stepsWrap.style.cssText = `
|
|
473
|
+
width:100%;display:flex;flex-direction:column;gap:0;
|
|
474
|
+
border-radius:12px;overflow:hidden;
|
|
475
|
+
border:1.5px solid ${this.theme.border};
|
|
476
|
+
`;
|
|
477
|
+
steps.forEach((step, i) => {
|
|
478
|
+
const row = document.createElement("div");
|
|
479
|
+
row.style.cssText = `
|
|
480
|
+
display:flex;align-items:center;gap:12px;
|
|
481
|
+
padding:9px 14px;
|
|
482
|
+
background:${this.theme.cardBg};
|
|
483
|
+
${i < steps.length - 1 ? `border-bottom:1px solid ${this.theme.border};` : ""}
|
|
484
|
+
`;
|
|
485
|
+
const iconEl = document.createElement("div");
|
|
486
|
+
iconEl.style.cssText = `
|
|
487
|
+
width:28px;height:28px;border-radius:8px;flex-shrink:0;
|
|
488
|
+
display:flex;align-items:center;justify-content:center;
|
|
489
|
+
font-size:13px;
|
|
490
|
+
background:${this.theme.primary}12;
|
|
491
|
+
border:1px solid ${this.theme.primary}20;
|
|
492
|
+
`;
|
|
493
|
+
iconEl.textContent = step.icon;
|
|
494
|
+
const textWrap = document.createElement("div");
|
|
495
|
+
textWrap.style.cssText = "display:flex;flex-direction:column;gap:1px;text-align:left;";
|
|
496
|
+
const textEl = document.createElement("span");
|
|
497
|
+
textEl.style.cssText = `color:${this.theme.text};font-size:12.5px;font-weight:600;`;
|
|
498
|
+
textEl.textContent = step.text;
|
|
499
|
+
const subEl = document.createElement("span");
|
|
500
|
+
subEl.style.cssText = `color:${this.theme.textMuted};font-size:10.5px;`;
|
|
501
|
+
subEl.textContent = step.sub;
|
|
502
|
+
textWrap.appendChild(textEl);
|
|
503
|
+
textWrap.appendChild(subEl);
|
|
504
|
+
row.appendChild(iconEl);
|
|
505
|
+
row.appendChild(textWrap);
|
|
506
|
+
stepsWrap.appendChild(row);
|
|
507
|
+
});
|
|
508
|
+
container.appendChild(stepsWrap);
|
|
509
|
+
// CTA
|
|
510
|
+
const btn = this.createPrimaryButton("Get Started →");
|
|
511
|
+
btn.style.cssText += "margin-top:2px;";
|
|
512
|
+
btn.addEventListener("click", () => this.showStep("country"));
|
|
513
|
+
container.appendChild(btn);
|
|
514
|
+
// Trust badge
|
|
515
|
+
const trust = document.createElement("div");
|
|
516
|
+
trust.style.cssText = `
|
|
517
|
+
display:flex;align-items:center;gap:6px;
|
|
518
|
+
color:${this.theme.textMuted};font-size:10.5px;
|
|
519
|
+
`;
|
|
520
|
+
trust.textContent = "🔒 End-to-end encrypted · Powered by Adhere";
|
|
521
|
+
container.appendChild(trust);
|
|
522
|
+
}
|
|
523
|
+
// ── Country Select ──────────────────────────────────────────────
|
|
524
|
+
renderCountrySelect(container) {
|
|
525
|
+
container.style.cssText += "display:flex;flex-direction:column;gap:16px;";
|
|
526
|
+
const channels = this.sdkConfig?.channels || {};
|
|
527
|
+
const countries = Object.keys(channels);
|
|
528
|
+
// Auto-skip country selection if only one country available
|
|
529
|
+
if (countries.length === 1) {
|
|
530
|
+
this.selectedCountry = countries[0];
|
|
531
|
+
this.showStep("id_type");
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
const title = this.createStepTitle("Select your country");
|
|
535
|
+
container.appendChild(title);
|
|
536
|
+
const subtitle = document.createElement("div");
|
|
537
|
+
subtitle.style.cssText = `color:${this.theme.textSecondary};font-size:13px;margin-top:-8px;`;
|
|
538
|
+
subtitle.textContent = "Choose the country that issued your ID";
|
|
539
|
+
container.appendChild(subtitle);
|
|
540
|
+
for (const country of countries) {
|
|
541
|
+
const card = this.createOptionCard(COUNTRY_LABELS[country] || country, `${channels[country].length} ID type${channels[country].length > 1 ? "s" : ""} available`, () => {
|
|
542
|
+
this.selectedCountry = country;
|
|
543
|
+
this.showStep("id_type");
|
|
544
|
+
});
|
|
545
|
+
container.appendChild(card);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
// ── ID Type Select ──────────────────────────────────────────────
|
|
549
|
+
renderIdTypeSelect(container) {
|
|
550
|
+
container.style.cssText += "display:flex;flex-direction:column;gap:16px;";
|
|
551
|
+
const globalDocFlow = this.isDocumentFlow();
|
|
552
|
+
const title = this.createStepTitle(globalDocFlow ? "Select document type" : "Select ID type");
|
|
553
|
+
container.appendChild(title);
|
|
554
|
+
const subtitle = document.createElement("div");
|
|
555
|
+
subtitle.style.cssText = `color:${this.theme.textSecondary};font-size:13px;margin-top:-8px;`;
|
|
556
|
+
subtitle.textContent = globalDocFlow
|
|
557
|
+
? "Choose the document you want to scan"
|
|
558
|
+
: "Choose the ID you want to verify";
|
|
559
|
+
container.appendChild(subtitle);
|
|
560
|
+
const channels = this.sdkConfig?.channels || {};
|
|
561
|
+
const idTypes = channels[this.selectedCountry] || [];
|
|
562
|
+
for (const idType of idTypes) {
|
|
563
|
+
const typeName = typeof idType === "string" ? idType : idType.name;
|
|
564
|
+
// Use code (canonical) for API calls, fallback to name for backward compatibility
|
|
565
|
+
const typeCode = typeof idType === "string" ? idType : (idType.code || idType.name);
|
|
566
|
+
// Detect per-channel: does THIS specific channel have upload fields?
|
|
567
|
+
const channelFields = typeof idType !== "string" ? (idType.fields || []) : [];
|
|
568
|
+
const isThisDocFlow = channelFields.some((f) => f.type === "upload") || globalDocFlow;
|
|
569
|
+
const info = resolveIdTypeInfo(typeName, isThisDocFlow);
|
|
570
|
+
// Add back side indicator to subtitle if needed
|
|
571
|
+
let subtitle = info.desc;
|
|
572
|
+
if (info.requiresBack && isThisDocFlow) {
|
|
573
|
+
subtitle += " ⚠️ Both sides required";
|
|
574
|
+
}
|
|
575
|
+
const card = this.createOptionCard(`${info.icon} ${typeName}`, subtitle, () => {
|
|
576
|
+
this.selectedIdType = typeCode; // Store canonical code for API
|
|
577
|
+
this.selectedIdTypeName = typeName; // Keep display name for UI
|
|
578
|
+
// Always reset first to avoid stale state when user goes back and changes selection
|
|
579
|
+
this.selectedChannelId = 0;
|
|
580
|
+
this.selectedChannelFields = [];
|
|
581
|
+
if (typeof idType !== "string") {
|
|
582
|
+
this.selectedChannelId = idType.id;
|
|
583
|
+
this.selectedChannelFields = idType.fields || []; // guard null/undefined from backend
|
|
584
|
+
}
|
|
585
|
+
this.collectedFields = {};
|
|
586
|
+
// Route: use explicit verification_type when set (more reliable than inspecting all channels)
|
|
587
|
+
// Fallback to per-channel upload field check for mixed / unconfigured setups
|
|
588
|
+
const hasUpload = (this.selectedChannelFields).some((f) => f.type === "upload");
|
|
589
|
+
const vType = this.sdkConfig?.verification_type;
|
|
590
|
+
const vTypes = vType ? (Array.isArray(vType) ? vType : [vType]) : [];
|
|
591
|
+
const isDocChannel = hasUpload || (vTypes.length > 0 && vTypes.includes("document_verification"));
|
|
592
|
+
this.showStep(isDocChannel ? "document_capture" : "id_input");
|
|
593
|
+
});
|
|
594
|
+
container.appendChild(card);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
// ── ID Input (data verification — dynamic fields) ─────────────
|
|
598
|
+
renderIdInput(container) {
|
|
599
|
+
container.style.cssText += "display:flex;flex-direction:column;gap:16px;";
|
|
600
|
+
const dataFields = this.selectedChannelFields.filter((f) => f.type !== "upload");
|
|
601
|
+
const info = resolveIdTypeInfo(this.selectedIdTypeName || this.selectedIdType, false);
|
|
602
|
+
const title = this.createStepTitle(`${info.icon} ${this.selectedIdTypeName || this.selectedIdType}`);
|
|
603
|
+
container.appendChild(title);
|
|
604
|
+
const subtitle = document.createElement("div");
|
|
605
|
+
subtitle.style.cssText = `color:${this.theme.textSecondary};font-size:13px;margin-top:-8px;`;
|
|
606
|
+
subtitle.textContent = dataFields.length > 0
|
|
607
|
+
? `Enter your ${this.selectedIdTypeName || this.selectedIdType} details exactly as they appear on your ID.`
|
|
608
|
+
: "Fill in the fields below to continue.";
|
|
609
|
+
container.appendChild(subtitle);
|
|
610
|
+
// Edge case: no data fields configured — show fallback and allow submission
|
|
611
|
+
if (dataFields.length === 0) {
|
|
612
|
+
const empty = document.createElement("div");
|
|
613
|
+
empty.style.cssText = `
|
|
614
|
+
padding:16px;border-radius:10px;
|
|
615
|
+
background:${this.theme.cardBg};border:1px solid ${this.theme.border};
|
|
616
|
+
color:${this.theme.textSecondary};font-size:13px;text-align:center;
|
|
617
|
+
`;
|
|
618
|
+
empty.textContent = "No additional details required. Click below to continue.";
|
|
619
|
+
container.appendChild(empty);
|
|
620
|
+
const autoBtn = this.createPrimaryButton("Continue");
|
|
621
|
+
autoBtn.addEventListener("click", () => this.showStep("liveness"));
|
|
622
|
+
container.appendChild(autoBtn);
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
// Track input elements for validation
|
|
626
|
+
const inputEls = [];
|
|
627
|
+
for (const field of dataFields) {
|
|
628
|
+
const fieldKey = field.label.toLowerCase().replace(/[^a-z0-9]+/g, "_");
|
|
629
|
+
const fieldWrap = document.createElement("div");
|
|
630
|
+
fieldWrap.style.cssText = "display:flex;flex-direction:column;gap:6px;";
|
|
631
|
+
const label = document.createElement("label");
|
|
632
|
+
label.style.cssText = `color:${this.theme.textMuted};font-size:10px;font-weight:700;letter-spacing:0.6px;text-transform:uppercase;`;
|
|
633
|
+
label.textContent = field.label;
|
|
634
|
+
fieldWrap.appendChild(label);
|
|
635
|
+
if (field.type === "dropdown" || field.type === "select") {
|
|
636
|
+
// Dropdown field
|
|
637
|
+
const select = document.createElement("select");
|
|
638
|
+
select.style.cssText = `
|
|
639
|
+
width:100%;padding:13px 16px;border-radius:12px;
|
|
640
|
+
font-size:15px;
|
|
641
|
+
background:${this.theme.inputBg};
|
|
642
|
+
color:${this.theme.text};
|
|
643
|
+
border:2px solid ${this.theme.border};
|
|
644
|
+
outline:none;box-sizing:border-box;
|
|
645
|
+
cursor:pointer;transition:border-color 0.2s ease;
|
|
646
|
+
`;
|
|
647
|
+
const placeholder = document.createElement("option");
|
|
648
|
+
placeholder.value = "";
|
|
649
|
+
placeholder.textContent = `Select ${field.label}`;
|
|
650
|
+
placeholder.disabled = true;
|
|
651
|
+
placeholder.selected = true;
|
|
652
|
+
select.appendChild(placeholder);
|
|
653
|
+
for (const opt of field.options || []) {
|
|
654
|
+
const option = document.createElement("option");
|
|
655
|
+
option.value = opt;
|
|
656
|
+
option.textContent = opt;
|
|
657
|
+
select.appendChild(option);
|
|
658
|
+
}
|
|
659
|
+
select.addEventListener("focus", () => { select.style.borderColor = this.theme.primary; });
|
|
660
|
+
select.addEventListener("blur", () => { select.style.borderColor = this.theme.border; });
|
|
661
|
+
select.addEventListener("change", () => {
|
|
662
|
+
this.collectedFields[fieldKey] = select.value;
|
|
663
|
+
updateSubmitState();
|
|
664
|
+
});
|
|
665
|
+
fieldWrap.appendChild(select);
|
|
666
|
+
inputEls.push({ field, el: select });
|
|
667
|
+
}
|
|
668
|
+
else if (field.type === "image" || field.type === "file") {
|
|
669
|
+
// File upload field — converts to base64 for API submission
|
|
670
|
+
const fileInput = document.createElement("input");
|
|
671
|
+
fileInput.type = "file";
|
|
672
|
+
fileInput.accept = "image/*";
|
|
673
|
+
fileInput.style.cssText = `
|
|
674
|
+
width:100%;padding:12px 16px;border-radius:12px;
|
|
675
|
+
font-size:14px;font-weight:500;
|
|
676
|
+
background:${this.theme.inputBg};
|
|
677
|
+
color:${this.theme.text};
|
|
678
|
+
border:2px dashed ${this.theme.border};
|
|
679
|
+
outline:none;transition:border-color 0.2s ease;
|
|
680
|
+
box-sizing:border-box;cursor:pointer;
|
|
681
|
+
`;
|
|
682
|
+
fileInput.addEventListener("focus", () => {
|
|
683
|
+
fileInput.style.borderColor = this.theme.primary;
|
|
684
|
+
});
|
|
685
|
+
fileInput.addEventListener("blur", () => {
|
|
686
|
+
fileInput.style.borderColor = this.theme.border;
|
|
687
|
+
});
|
|
688
|
+
fileInput.addEventListener("change", () => {
|
|
689
|
+
const file = fileInput.files?.[0];
|
|
690
|
+
if (file) {
|
|
691
|
+
const reader = new FileReader();
|
|
692
|
+
reader.onload = () => {
|
|
693
|
+
const base64 = reader.result.split(",")[1] || "";
|
|
694
|
+
this.collectedFields[fieldKey] = base64;
|
|
695
|
+
updateSubmitState();
|
|
696
|
+
};
|
|
697
|
+
reader.readAsDataURL(file);
|
|
698
|
+
}
|
|
699
|
+
});
|
|
700
|
+
fieldWrap.appendChild(fileInput);
|
|
701
|
+
inputEls.push({ field, el: fileInput });
|
|
702
|
+
}
|
|
703
|
+
else {
|
|
704
|
+
// Text input field
|
|
705
|
+
const input = document.createElement("input");
|
|
706
|
+
input.type = "text";
|
|
707
|
+
input.placeholder = `Enter ${field.label}`;
|
|
708
|
+
input.maxLength = 50;
|
|
709
|
+
input.style.cssText = `
|
|
710
|
+
width:100%;padding:14px 16px;border-radius:12px;
|
|
711
|
+
font-size:16px;font-weight:500;
|
|
712
|
+
background:${this.theme.inputBg};
|
|
713
|
+
color:${this.theme.text};
|
|
714
|
+
border:2px solid ${this.theme.border};
|
|
715
|
+
outline:none;transition:border-color 0.2s ease;
|
|
716
|
+
box-sizing:border-box;
|
|
717
|
+
letter-spacing:0.5px;
|
|
718
|
+
`;
|
|
719
|
+
input.addEventListener("focus", () => {
|
|
720
|
+
input.style.borderColor = this.theme.primary;
|
|
721
|
+
});
|
|
722
|
+
input.addEventListener("blur", () => {
|
|
723
|
+
input.style.borderColor = this.theme.border;
|
|
724
|
+
});
|
|
725
|
+
input.addEventListener("input", () => {
|
|
726
|
+
this.collectedFields[fieldKey] = input.value.trim();
|
|
727
|
+
updateSubmitState();
|
|
728
|
+
});
|
|
729
|
+
fieldWrap.appendChild(input);
|
|
730
|
+
inputEls.push({ field, el: input });
|
|
731
|
+
}
|
|
732
|
+
container.appendChild(fieldWrap);
|
|
733
|
+
}
|
|
734
|
+
// Error area
|
|
735
|
+
const errorEl = document.createElement("div");
|
|
736
|
+
errorEl.id = "sc-id-error";
|
|
737
|
+
errorEl.style.cssText = `
|
|
738
|
+
color:${this.theme.error};font-size:13px;display:none;
|
|
739
|
+
padding:8px 12px;border-radius:8px;background:${this.theme.errorBg};
|
|
740
|
+
`;
|
|
741
|
+
container.appendChild(errorEl);
|
|
742
|
+
// Submit
|
|
743
|
+
const submitBtn = this.createPrimaryButton("Verify & Continue");
|
|
744
|
+
submitBtn.disabled = true;
|
|
745
|
+
submitBtn.style.opacity = "0.5";
|
|
746
|
+
const updateSubmitState = () => {
|
|
747
|
+
const allFilled = inputEls.every(({ el }) => el.value.trim().length > 0);
|
|
748
|
+
submitBtn.disabled = !allFilled;
|
|
749
|
+
submitBtn.style.opacity = allFilled ? "1" : "0.5";
|
|
750
|
+
};
|
|
751
|
+
submitBtn.addEventListener("click", async () => {
|
|
752
|
+
submitBtn.disabled = true;
|
|
753
|
+
submitBtn.innerHTML = `<span style="display:inline-flex;align-items:center;justify-content:center;gap:8px;"><span style="width:13px;height:13px;border-radius:50%;border:2px solid rgba(255,255,255,0.3);border-top-color:#fff;animation:sc-spin 0.6s linear infinite;display:inline-block;flex-shrink:0;"></span>Verifying...</span>`;
|
|
754
|
+
errorEl.style.display = "none";
|
|
755
|
+
try {
|
|
756
|
+
this.verificationResult = await this.sdk.onboarding.verify({
|
|
757
|
+
identity_type_id: this.selectedChannelId,
|
|
758
|
+
fields: { ...this.collectedFields },
|
|
759
|
+
});
|
|
760
|
+
if (this.verificationResult.status === "verified" || this.verificationResult.status === "success") {
|
|
761
|
+
// Save the first collected field value as identifier for liveness
|
|
762
|
+
const fieldValues = Object.values(this.collectedFields);
|
|
763
|
+
this.idNumber = fieldValues[0] || "";
|
|
764
|
+
// A4: Save identity_check_id from verify response to pass to liveness
|
|
765
|
+
this.identityCheckId = this.verificationResult.data?.identity_check_id || null;
|
|
766
|
+
this.showStep("liveness");
|
|
767
|
+
}
|
|
768
|
+
else {
|
|
769
|
+
errorEl.textContent = this.verificationResult.message || "Verification failed. Please check your details.";
|
|
770
|
+
errorEl.style.display = "block";
|
|
771
|
+
submitBtn.textContent = "Verify & Continue";
|
|
772
|
+
submitBtn.disabled = false;
|
|
773
|
+
submitBtn.style.opacity = "1";
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
catch (err) {
|
|
777
|
+
// C3: Show field-level errors if available
|
|
778
|
+
const fieldErrors = err.data?.errors;
|
|
779
|
+
if (Array.isArray(fieldErrors) && fieldErrors.length > 0) {
|
|
780
|
+
const messages = fieldErrors.map((e) => `${e.field}: ${e.message}`).join("\n");
|
|
781
|
+
errorEl.textContent = messages;
|
|
782
|
+
}
|
|
783
|
+
else {
|
|
784
|
+
errorEl.textContent = err.message || "Verification failed";
|
|
785
|
+
}
|
|
786
|
+
errorEl.style.display = "block";
|
|
787
|
+
submitBtn.textContent = "Verify & Continue";
|
|
788
|
+
submitBtn.disabled = false;
|
|
789
|
+
submitBtn.style.opacity = "1";
|
|
790
|
+
}
|
|
791
|
+
});
|
|
792
|
+
container.appendChild(submitBtn);
|
|
793
|
+
// Focus first input
|
|
794
|
+
const firstInput = inputEls[0]?.el;
|
|
795
|
+
if (firstInput && firstInput instanceof HTMLInputElement) {
|
|
796
|
+
setTimeout(() => firstInput.focus(), 350);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
// ── Document Capture ────────────────────────────────────────────
|
|
800
|
+
renderDocumentCapture(container) {
|
|
801
|
+
container.style.cssText += "display:flex;flex-direction:column;gap:16px;";
|
|
802
|
+
const title = this.createStepTitle(`Capture ${this.selectedIdTypeName || this.selectedIdType || "your document"}`);
|
|
803
|
+
container.appendChild(title);
|
|
804
|
+
const subtitle = document.createElement("div");
|
|
805
|
+
subtitle.style.cssText = `color:${this.theme.textSecondary};font-size:13px;margin-top:-8px;`;
|
|
806
|
+
subtitle.textContent = "Upload or capture your document photo. Ensure it's clear and readable.";
|
|
807
|
+
container.appendChild(subtitle);
|
|
808
|
+
const captureContainer = document.createElement("div");
|
|
809
|
+
captureContainer.style.cssText = `
|
|
810
|
+
width: 100%;
|
|
811
|
+
max-width: 400px;
|
|
812
|
+
margin: 0 auto;
|
|
813
|
+
position: relative;
|
|
814
|
+
overflow: hidden;
|
|
815
|
+
border-radius: 12px;
|
|
816
|
+
background: ${this.theme.cardBg};
|
|
817
|
+
border: 2px solid ${this.theme.border};
|
|
818
|
+
`;
|
|
819
|
+
container.appendChild(captureContainer);
|
|
820
|
+
// Use the new DocumentCapture component with front/back support
|
|
821
|
+
this.docCapture = new DocumentCapture();
|
|
822
|
+
this.docCapture
|
|
823
|
+
.capture(captureContainer, this.theme, this.selectedIdTypeName || this.selectedIdType)
|
|
824
|
+
.then(async (result) => {
|
|
825
|
+
await this.processDocumentCaptureResult(result, captureContainer, container);
|
|
826
|
+
})
|
|
827
|
+
.catch((err) => {
|
|
828
|
+
console.error("Document capture failed:", err);
|
|
829
|
+
this.showErrorStep(err.message || "Document capture failed. Please try again.");
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
// ── Processing ────────────────────────────────────────────────────
|
|
833
|
+
renderProcessing(container) {
|
|
834
|
+
container.style.cssText += "display:flex;flex-direction:column;align-items:center;gap:20px;padding:20px 16px;min-height:400px;";
|
|
835
|
+
// Processing animation
|
|
836
|
+
const animationWrap = document.createElement("div");
|
|
837
|
+
animationWrap.style.cssText = "position:relative;width:80px;height:80px;";
|
|
838
|
+
const spinner = document.createElement("div");
|
|
839
|
+
spinner.style.cssText = `
|
|
840
|
+
position:absolute;inset:0;border-radius:50%;
|
|
841
|
+
border:4px solid ${this.theme.border};
|
|
842
|
+
border-top-color:${this.theme.primary};
|
|
843
|
+
animation:sc-spin 1s linear infinite;
|
|
844
|
+
`;
|
|
845
|
+
animationWrap.appendChild(spinner);
|
|
846
|
+
const centerIcon = document.createElement("div");
|
|
847
|
+
centerIcon.style.cssText = `
|
|
848
|
+
position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
|
|
849
|
+
font-size:28px;
|
|
850
|
+
`;
|
|
851
|
+
centerIcon.textContent = "🔍";
|
|
852
|
+
animationWrap.appendChild(centerIcon);
|
|
853
|
+
container.appendChild(animationWrap);
|
|
854
|
+
// Processing text
|
|
855
|
+
const title = document.createElement("div");
|
|
856
|
+
title.style.cssText = `color:${this.theme.text};font-size:18px;font-weight:600;text-align:center;`;
|
|
857
|
+
title.textContent = "Processing Your Documents";
|
|
858
|
+
container.appendChild(title);
|
|
859
|
+
const subtitle = document.createElement("div");
|
|
860
|
+
subtitle.style.cssText = `color:${this.theme.textSecondary};font-size:13px;text-align:center;max-width:280px;line-height:1.5;`;
|
|
861
|
+
subtitle.textContent = "Scanning and verifying your identity documents. This may take a few moments.";
|
|
862
|
+
container.appendChild(subtitle);
|
|
863
|
+
// Processing steps
|
|
864
|
+
const stepsContainer = document.createElement("div");
|
|
865
|
+
stepsContainer.style.cssText = `
|
|
866
|
+
width:100%;max-width:400px;display:flex;flex-direction:column;gap:12px;
|
|
867
|
+
margin-top:16px;
|
|
868
|
+
`;
|
|
869
|
+
const steps = [
|
|
870
|
+
{ id: 'uploading', text: 'Uploading documents', icon: '📤' },
|
|
871
|
+
{ id: 'scanning', text: 'Scanning for information', icon: '🔍' },
|
|
872
|
+
{ id: 'verifying', text: 'Verifying authenticity', icon: '✓' },
|
|
873
|
+
{ id: 'analyzing', text: 'Analyzing document quality', icon: '🔬' }
|
|
874
|
+
];
|
|
875
|
+
steps.forEach((step, index) => {
|
|
876
|
+
const stepEl = document.createElement("div");
|
|
877
|
+
stepEl.style.cssText = `
|
|
878
|
+
display:flex;align-items:center;gap:12px;padding:12px;border-radius:10px;
|
|
879
|
+
background:${this.theme.cardBg};border:1px solid ${this.theme.border};
|
|
880
|
+
transition:all 0.3s ease;
|
|
881
|
+
`;
|
|
882
|
+
stepEl.id = `processing-step-${step.id}`;
|
|
883
|
+
const iconEl = document.createElement("div");
|
|
884
|
+
iconEl.style.cssText = `
|
|
885
|
+
width:32px;height:32px;border-radius:8px;display:flex;align-items:center;justify-content:center;
|
|
886
|
+
font-size:14px;background:${this.theme.shimmer};border:1px solid ${this.theme.border};
|
|
887
|
+
`;
|
|
888
|
+
iconEl.textContent = step.icon;
|
|
889
|
+
iconEl.id = `step-icon-${step.id}`;
|
|
890
|
+
const textEl = document.createElement("div");
|
|
891
|
+
textEl.style.cssText = `flex:1;color:${this.theme.textSecondary};font-size:13px;font-weight:500;`;
|
|
892
|
+
textEl.textContent = step.text;
|
|
893
|
+
textEl.id = `step-text-${step.id}`;
|
|
894
|
+
const statusEl = document.createElement("div");
|
|
895
|
+
statusEl.style.cssText = `
|
|
896
|
+
width:20px;height:20px;border-radius:50%;
|
|
897
|
+
border:2px solid ${this.theme.border};
|
|
898
|
+
position:relative;
|
|
899
|
+
`;
|
|
900
|
+
statusEl.id = `step-status-${step.id}`;
|
|
901
|
+
stepEl.appendChild(iconEl);
|
|
902
|
+
stepEl.appendChild(textEl);
|
|
903
|
+
stepEl.appendChild(statusEl);
|
|
904
|
+
stepsContainer.appendChild(stepEl);
|
|
905
|
+
});
|
|
906
|
+
container.appendChild(stepsContainer);
|
|
907
|
+
// Start processing animation
|
|
908
|
+
this.startProcessingAnimation();
|
|
909
|
+
}
|
|
910
|
+
startProcessingAnimation() {
|
|
911
|
+
const steps = ['uploading', 'scanning', 'verifying', 'analyzing'];
|
|
912
|
+
let currentStepIndex = 0;
|
|
913
|
+
const animateStep = (stepId) => {
|
|
914
|
+
// Update step appearance
|
|
915
|
+
const stepEl = document.getElementById(`processing-step-${stepId}`);
|
|
916
|
+
const iconEl = document.getElementById(`step-icon-${stepId}`);
|
|
917
|
+
const textEl = document.getElementById(`step-text-${stepId}`);
|
|
918
|
+
const statusEl = document.getElementById(`step-status-${stepId}`);
|
|
919
|
+
if (stepEl && iconEl && textEl && statusEl) {
|
|
920
|
+
// Active state
|
|
921
|
+
stepEl.style.borderColor = this.theme.primary;
|
|
922
|
+
stepEl.style.background = `${this.theme.primary}10`;
|
|
923
|
+
iconEl.style.background = this.theme.primary;
|
|
924
|
+
iconEl.style.color = 'white';
|
|
925
|
+
textEl.style.color = this.theme.text;
|
|
926
|
+
textEl.style.fontWeight = '600';
|
|
927
|
+
// Add spinner to status
|
|
928
|
+
statusEl.innerHTML = `
|
|
929
|
+
<div style="
|
|
930
|
+
position:absolute;inset:2px;border-radius:50%;
|
|
931
|
+
border:2px solid ${this.theme.border};
|
|
932
|
+
border-top-color:${this.theme.primary};
|
|
933
|
+
animation:sc-spin 0.8s linear infinite;
|
|
934
|
+
"></div>
|
|
935
|
+
`;
|
|
936
|
+
}
|
|
937
|
+
// Move to next step after delay
|
|
938
|
+
setTimeout(() => {
|
|
939
|
+
// Complete current step
|
|
940
|
+
if (stepEl && iconEl && textEl && statusEl) {
|
|
941
|
+
stepEl.style.borderColor = this.theme.success;
|
|
942
|
+
stepEl.style.background = `${this.theme.success}10`;
|
|
943
|
+
iconEl.style.background = this.theme.success;
|
|
944
|
+
statusEl.innerHTML = `
|
|
945
|
+
<div style="
|
|
946
|
+
position:absolute;inset:3px;border-radius:50%;
|
|
947
|
+
background:${this.theme.success};
|
|
948
|
+
display:flex;align-items:center;justify-content:center;
|
|
949
|
+
color:white;font-size:10px;font-weight:bold;
|
|
950
|
+
">✓</div>
|
|
951
|
+
`;
|
|
952
|
+
}
|
|
953
|
+
currentStepIndex++;
|
|
954
|
+
if (currentStepIndex < steps.length) {
|
|
955
|
+
animateStep(steps[currentStepIndex]);
|
|
956
|
+
}
|
|
957
|
+
}, 2000); // 2 seconds per step
|
|
958
|
+
};
|
|
959
|
+
// Start animation
|
|
960
|
+
setTimeout(() => animateStep(steps[0]), 500);
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
963
|
+
* Process document capture result with front/back support
|
|
964
|
+
*/
|
|
965
|
+
async processDocumentCaptureResult(result, captureContainer, container) {
|
|
966
|
+
// Store both front and back blobs
|
|
967
|
+
this.documentBlob = result.front;
|
|
968
|
+
this.documentBackBlob = result.back || null;
|
|
969
|
+
// Process the front image through the existing flow
|
|
970
|
+
await this.processDocumentBlob(result.front, captureContainer, container);
|
|
971
|
+
// Log back image capture
|
|
972
|
+
if (result.back) {
|
|
973
|
+
console.log("Back image captured, size:", result.back.size);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
/**
|
|
977
|
+
* Process document blob - common method for both CameraView and DocumentCapture
|
|
978
|
+
*/
|
|
979
|
+
async processDocumentBlob(blob, captureContainer, container) {
|
|
980
|
+
const maxRetries = 2;
|
|
981
|
+
let retryCount = 0;
|
|
982
|
+
const attemptProcessing = async () => {
|
|
983
|
+
try {
|
|
984
|
+
// Validate blob
|
|
985
|
+
if (!blob || blob.size === 0) {
|
|
986
|
+
throw new Error("Failed to capture document. Please try again.");
|
|
987
|
+
}
|
|
988
|
+
if (blob.size > 2 * 1024 * 1024) {
|
|
989
|
+
throw new Error("Document image is too large. Please retake in better lighting or use a clearer photo.");
|
|
990
|
+
}
|
|
991
|
+
this.documentBlob = blob;
|
|
992
|
+
// Show scanning state
|
|
993
|
+
captureContainer.innerHTML = `
|
|
994
|
+
<div style="display:flex;flex-direction:column;align-items:center;gap:14px;padding:28px 20px;">
|
|
995
|
+
<div style="position:relative;width:52px;height:52px;">
|
|
996
|
+
<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>
|
|
997
|
+
<div style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:20px;">🔍</div>
|
|
998
|
+
</div>
|
|
999
|
+
<div style="color:${this.theme.text};font-size:14px;font-weight:600;">Scanning document...</div>
|
|
1000
|
+
<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>
|
|
1001
|
+
</div>`;
|
|
1002
|
+
// Create liveness entry with document
|
|
1003
|
+
const identifier = `DOC-${Date.now()}`;
|
|
1004
|
+
const COUNTRY_CODES = {
|
|
1005
|
+
nigeria: "NG", ghana: "GH", kenya: "KE",
|
|
1006
|
+
south_africa: "ZA", united_states: "US", united_kingdom: "GB", global: "US",
|
|
1007
|
+
};
|
|
1008
|
+
const country = COUNTRY_CODES[this.selectedCountry] || this.selectedCountry.toUpperCase().slice(0, 2);
|
|
1009
|
+
const deviceMeta = collectDeviceMetadata();
|
|
1010
|
+
let createResult;
|
|
1011
|
+
try {
|
|
1012
|
+
createResult = await this.sdk.liveness.create({
|
|
1013
|
+
identifier,
|
|
1014
|
+
identifier_type: this.selectedIdType,
|
|
1015
|
+
country,
|
|
1016
|
+
challenge_actions: ["BLINK", "TURN_LEFT", "OPEN_MOUTH"],
|
|
1017
|
+
document: blob,
|
|
1018
|
+
document_back: this.documentBackBlob || undefined,
|
|
1019
|
+
device_type: deviceMeta.device_type,
|
|
1020
|
+
os_name: deviceMeta.os_name,
|
|
1021
|
+
browser_timezone: deviceMeta.browser_timezone,
|
|
1022
|
+
fingerprint_id: deviceMeta.fingerprint_id,
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
catch (apiError) {
|
|
1026
|
+
// Handle specific network and API errors
|
|
1027
|
+
if (apiError.message?.includes('fetch') || apiError.message?.includes('network') || apiError.code === 'NETWORK_ERROR') {
|
|
1028
|
+
throw new Error("Network connection failed. Please check your internet connection and try again.");
|
|
1029
|
+
}
|
|
1030
|
+
else if (apiError.message?.includes('413') || apiError.message?.includes('too large')) {
|
|
1031
|
+
throw new Error("Document image is too large. Please use a smaller image or retake the photo.");
|
|
1032
|
+
}
|
|
1033
|
+
else if (apiError.message?.includes('415') || apiError.message?.includes('format')) {
|
|
1034
|
+
throw new Error("Unsupported image format. Please use JPEG or PNG images.");
|
|
1035
|
+
}
|
|
1036
|
+
else if (apiError.message?.includes('401') || apiError.message?.includes('unauthorized')) {
|
|
1037
|
+
throw new Error("Session expired. Please refresh the page and try again.");
|
|
1038
|
+
}
|
|
1039
|
+
else if (apiError.message?.includes('429') || apiError.message?.includes('rate limit')) {
|
|
1040
|
+
throw new Error("Too many requests. Please wait a moment and try again.");
|
|
1041
|
+
}
|
|
1042
|
+
else {
|
|
1043
|
+
throw new Error(apiError.message || "Failed to process document. Please try again.");
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
this.livenessEntryId = createResult.id;
|
|
1047
|
+
// Check if document was expired
|
|
1048
|
+
const docOcr = createResult.document_ocr;
|
|
1049
|
+
if (docOcr && docOcr.status === "expired") {
|
|
1050
|
+
throw new Error(docOcr.message || "Document has expired. Please use a valid document.");
|
|
1051
|
+
}
|
|
1052
|
+
// Check image quality
|
|
1053
|
+
const imgQuality = docOcr?.image_quality;
|
|
1054
|
+
const isBlurry = imgQuality?.is_blurry === true;
|
|
1055
|
+
const lowConfidence = imgQuality?.ocr_confidence !== undefined && imgQuality.ocr_confidence < 0.45;
|
|
1056
|
+
if (isBlurry || lowConfidence) {
|
|
1057
|
+
throw new Error("Image quality too low. Please ensure the document is well-lit and clearly visible.");
|
|
1058
|
+
}
|
|
1059
|
+
// Show processing step instead of immediate success
|
|
1060
|
+
this.showStep("processing");
|
|
1061
|
+
// Simulate processing time and then proceed to liveness
|
|
1062
|
+
setTimeout(() => {
|
|
1063
|
+
this.showStep("liveness");
|
|
1064
|
+
}, 8000); // 8 seconds total processing time (4 steps × 2 seconds each)
|
|
1065
|
+
}
|
|
1066
|
+
catch (err) {
|
|
1067
|
+
console.error("Document processing failed:", err);
|
|
1068
|
+
if (retryCount < maxRetries) {
|
|
1069
|
+
retryCount++;
|
|
1070
|
+
console.log(`Retrying document processing (attempt ${retryCount}/${maxRetries})`);
|
|
1071
|
+
// Show retry message
|
|
1072
|
+
if (captureContainer) {
|
|
1073
|
+
captureContainer.innerHTML = `
|
|
1074
|
+
<div style="display:flex;flex-direction:column;align-items:center;gap:16px;padding:28px 20px;text-align:center;">
|
|
1075
|
+
<div style="position:relative;width:52px;height:52px;">
|
|
1076
|
+
<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>
|
|
1077
|
+
<div style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:20px;">🔄</div>
|
|
1078
|
+
</div>
|
|
1079
|
+
<div style="color:${this.theme.text};font-size:14px;font-weight:600;">Retrying...</div>
|
|
1080
|
+
<div style="color:${this.theme.textSecondary};font-size:12px;max-width:240px;line-height:1.5;">Attempt ${retryCount} of ${maxRetries}</div>
|
|
1081
|
+
</div>`;
|
|
1082
|
+
}
|
|
1083
|
+
// Wait before retry
|
|
1084
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
1085
|
+
return attemptProcessing();
|
|
1086
|
+
}
|
|
1087
|
+
else {
|
|
1088
|
+
// Final retry failed, show detailed error
|
|
1089
|
+
this.handleDocumentProcessingError(err, captureContainer);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
};
|
|
1093
|
+
await attemptProcessing();
|
|
1094
|
+
}
|
|
1095
|
+
handleDocumentProcessingError(error, captureContainer) {
|
|
1096
|
+
let errorMessage = "Document verification failed. Please try again.";
|
|
1097
|
+
let canRetry = true;
|
|
1098
|
+
if (error.message) {
|
|
1099
|
+
if (error.message.includes("expired")) {
|
|
1100
|
+
errorMessage = "Document has expired. Please use a valid document.";
|
|
1101
|
+
canRetry = false;
|
|
1102
|
+
}
|
|
1103
|
+
else if (error.message.includes("quality too low")) {
|
|
1104
|
+
errorMessage = "Image quality too low. Please ensure the document is well-lit and clearly visible.";
|
|
1105
|
+
}
|
|
1106
|
+
else if (error.message.includes("Network") || error.message.includes("fetch")) {
|
|
1107
|
+
errorMessage = "Network connection failed. Please check your internet connection and try again.";
|
|
1108
|
+
}
|
|
1109
|
+
else if (error.message.includes("413") || error.message.includes("too large")) {
|
|
1110
|
+
errorMessage = "Document image is too large. Please use a smaller image or retake the photo.";
|
|
1111
|
+
}
|
|
1112
|
+
else if (error.message.includes("415") || error.message.includes("format")) {
|
|
1113
|
+
errorMessage = "Unsupported image format. Please use JPEG or PNG images.";
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
if (captureContainer) {
|
|
1117
|
+
captureContainer.innerHTML = `
|
|
1118
|
+
<div style="display:flex;flex-direction:column;align-items:center;gap:16px;padding:28px 20px;text-align:center;">
|
|
1119
|
+
<div style="width:56px;height:56px;border-radius:50%;background:${this.theme.errorBg || '#FEE'};border:3px solid ${this.theme.error || '#DC3545'};display:flex;align-items:center;justify-content:center;font-size:26px;">❌</div>
|
|
1120
|
+
<div style="color:${this.theme.text};font-size:14px;font-weight:700;">Verification Failed</div>
|
|
1121
|
+
<div style="color:${this.theme.textSecondary};font-size:13px;max-width:240px;line-height:1.6;">${errorMessage}</div>
|
|
1122
|
+
${canRetry ? `
|
|
1123
|
+
<button id="retry-btn" style="
|
|
1124
|
+
margin-top:8px;padding:12px 24px;border-radius:8px;
|
|
1125
|
+
background:${this.theme.primary};color:white;
|
|
1126
|
+
border:none;font-size:14px;font-weight:600;cursor:pointer;
|
|
1127
|
+
transition:all 0.2s ease;
|
|
1128
|
+
">Try Again</button>
|
|
1129
|
+
` : ''}
|
|
1130
|
+
</div>`;
|
|
1131
|
+
if (canRetry) {
|
|
1132
|
+
const retryBtn = captureContainer.querySelector("#retry-btn");
|
|
1133
|
+
if (retryBtn) {
|
|
1134
|
+
retryBtn.addEventListener("click", () => {
|
|
1135
|
+
this.showStep("document_capture");
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
else {
|
|
1141
|
+
this.showErrorStep(errorMessage);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
// ── Liveness ────────────────────────────────────────────────────
|
|
1145
|
+
renderLiveness(container) {
|
|
1146
|
+
container.style.cssText += "display:flex;flex-direction:column;gap:16px;";
|
|
1147
|
+
const title = this.createStepTitle("Face verification");
|
|
1148
|
+
container.appendChild(title);
|
|
1149
|
+
const desc = document.createElement("div");
|
|
1150
|
+
desc.style.cssText = `color:${this.theme.textSecondary};font-size:13px;margin-top:-8px;line-height:1.5;`;
|
|
1151
|
+
desc.textContent = "Position your face in the oval and follow the on-screen instructions.";
|
|
1152
|
+
container.appendChild(desc);
|
|
1153
|
+
// Liveness mount point
|
|
1154
|
+
const livenessContainer = document.createElement("div");
|
|
1155
|
+
livenessContainer.style.cssText = "margin:8px 0;";
|
|
1156
|
+
container.appendChild(livenessContainer);
|
|
1157
|
+
const identifier = this.idNumber || `DOC-${Date.now()}`;
|
|
1158
|
+
const COUNTRY_CODES = {
|
|
1159
|
+
nigeria: "NG",
|
|
1160
|
+
ghana: "GH",
|
|
1161
|
+
kenya: "KE",
|
|
1162
|
+
south_africa: "ZA",
|
|
1163
|
+
united_states: "US",
|
|
1164
|
+
united_kingdom: "GB",
|
|
1165
|
+
global: "US",
|
|
1166
|
+
};
|
|
1167
|
+
const country = COUNTRY_CODES[this.selectedCountry] || this.selectedCountry.toUpperCase().slice(0, 2);
|
|
1168
|
+
// Start liveness check (this handles camera, detection, recording, submission)
|
|
1169
|
+
// If livenessEntryId exists (document flow), reuse it instead of creating a new one
|
|
1170
|
+
this.sdk.liveness
|
|
1171
|
+
.startCheck(livenessContainer, {
|
|
1172
|
+
identifier,
|
|
1173
|
+
identifier_type: this.selectedIdType,
|
|
1174
|
+
country,
|
|
1175
|
+
document: this.documentBlob || undefined,
|
|
1176
|
+
document_back: this.documentBackBlob || undefined,
|
|
1177
|
+
identity_check: this.identityCheckId || undefined,
|
|
1178
|
+
}, ["BLINK", "TURN_LEFT", "OPEN_MOUTH"], this.livenessEntryId || undefined)
|
|
1179
|
+
.then((result) => {
|
|
1180
|
+
this.showResult(result);
|
|
1181
|
+
})
|
|
1182
|
+
.catch((err) => {
|
|
1183
|
+
this.showErrorStep(err.message || "Liveness verification failed. Please try again.");
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
// ── Result ──────────────────────────────────────────────────────
|
|
1187
|
+
showResult(submitResult) {
|
|
1188
|
+
this.currentStep = "result";
|
|
1189
|
+
this.updateProgress();
|
|
1190
|
+
if (!this.contentArea)
|
|
1191
|
+
return;
|
|
1192
|
+
this.contentArea.innerHTML = "";
|
|
1193
|
+
const wrapper = document.createElement("div");
|
|
1194
|
+
wrapper.style.cssText = `
|
|
1195
|
+
display:flex;flex-direction:column;align-items:center;
|
|
1196
|
+
text-align:center;gap:16px;padding:20px 0;
|
|
1197
|
+
opacity:0;transform:scale(0.95);transition:all 0.4s ease;
|
|
1198
|
+
`;
|
|
1199
|
+
// Icon — starts as a spinner, updates when status resolves
|
|
1200
|
+
const iconWrap = document.createElement("div");
|
|
1201
|
+
iconWrap.style.cssText = `
|
|
1202
|
+
width:80px;height:80px;border-radius:50%;
|
|
1203
|
+
display:flex;align-items:center;justify-content:center;
|
|
1204
|
+
font-size:36px;
|
|
1205
|
+
background:${this.theme.cardBg};
|
|
1206
|
+
border:3px solid ${this.theme.border};
|
|
1207
|
+
`;
|
|
1208
|
+
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>`;
|
|
1209
|
+
wrapper.appendChild(iconWrap);
|
|
1210
|
+
const title = document.createElement("h2");
|
|
1211
|
+
title.style.cssText = `color:${this.theme.text};font-size:20px;font-weight:700;margin:0;`;
|
|
1212
|
+
title.textContent = "Verification Submitted";
|
|
1213
|
+
wrapper.appendChild(title);
|
|
1214
|
+
const desc = document.createElement("p");
|
|
1215
|
+
desc.style.cssText = `color:${this.theme.textSecondary};font-size:14px;line-height:1.6;margin:0;max-width:300px;`;
|
|
1216
|
+
desc.textContent = "Processing your identity check. This takes around 30 seconds.";
|
|
1217
|
+
wrapper.appendChild(desc);
|
|
1218
|
+
// Details card
|
|
1219
|
+
const details = document.createElement("div");
|
|
1220
|
+
details.style.cssText = `
|
|
1221
|
+
width:100%;padding:16px;border-radius:12px;
|
|
1222
|
+
background:${this.theme.cardBg};border:1px solid ${this.theme.border};
|
|
1223
|
+
text-align:left;
|
|
1224
|
+
`;
|
|
1225
|
+
// Status badge — mutable DOM node so polling can update it
|
|
1226
|
+
const statusBadge = document.createElement("span");
|
|
1227
|
+
statusBadge.style.cssText = `font-size:12px;font-weight:600;color:${this.theme.warning};`;
|
|
1228
|
+
statusBadge.textContent = "Processing…";
|
|
1229
|
+
const statusRow = document.createElement("div");
|
|
1230
|
+
statusRow.style.cssText = "display:flex;justify-content:space-between;margin-bottom:8px;";
|
|
1231
|
+
statusRow.innerHTML = `<span style="color:${this.theme.textMuted};font-size:12px;">Status</span>`;
|
|
1232
|
+
statusRow.appendChild(statusBadge);
|
|
1233
|
+
// Build rows via DOM (not innerHTML+=) to keep statusBadge live in the document
|
|
1234
|
+
const entryRow = document.createElement("div");
|
|
1235
|
+
entryRow.style.cssText = "display:flex;justify-content:space-between;margin-bottom:8px;";
|
|
1236
|
+
entryRow.innerHTML = `
|
|
1237
|
+
<span style="color:${this.theme.textMuted};font-size:12px;">Entry ID</span>
|
|
1238
|
+
<span style="color:${this.theme.text};font-size:12px;font-weight:600;">${submitResult.id}</span>
|
|
1239
|
+
`;
|
|
1240
|
+
details.appendChild(entryRow);
|
|
1241
|
+
details.appendChild(statusRow);
|
|
1242
|
+
const submittedRow = document.createElement("div");
|
|
1243
|
+
submittedRow.style.cssText = "display:flex;justify-content:space-between;margin-top:8px;";
|
|
1244
|
+
submittedRow.innerHTML = `
|
|
1245
|
+
<span style="color:${this.theme.textMuted};font-size:12px;">Submitted</span>
|
|
1246
|
+
<span style="color:${this.theme.text};font-size:12px;">${submitResult.submitted_at ? new Date(submitResult.submitted_at).toLocaleString() : "Just now"}</span>
|
|
1247
|
+
`;
|
|
1248
|
+
details.appendChild(submittedRow);
|
|
1249
|
+
wrapper.appendChild(details);
|
|
1250
|
+
// Failure reason — hidden until status = failed
|
|
1251
|
+
const failureMsg = document.createElement("div");
|
|
1252
|
+
failureMsg.style.cssText = `
|
|
1253
|
+
display:none;width:100%;padding:12px 14px;border-radius:10px;
|
|
1254
|
+
background:${this.theme.errorBg};border:1px solid ${this.theme.error};
|
|
1255
|
+
color:${this.theme.error};font-size:12px;text-align:left;line-height:1.5;
|
|
1256
|
+
`;
|
|
1257
|
+
wrapper.appendChild(failureMsg);
|
|
1258
|
+
// Done button
|
|
1259
|
+
const doneBtn = this.createPrimaryButton("Done");
|
|
1260
|
+
let finalStatus = submitResult.status;
|
|
1261
|
+
doneBtn.addEventListener("click", () => {
|
|
1262
|
+
if (this._pollStatusTimer) {
|
|
1263
|
+
clearTimeout(this._pollStatusTimer);
|
|
1264
|
+
this._pollStatusTimer = null;
|
|
1265
|
+
}
|
|
1266
|
+
this.options.onComplete?.({
|
|
1267
|
+
entryId: submitResult.id,
|
|
1268
|
+
sessionId: this.sdk.sessionToken,
|
|
1269
|
+
status: finalStatus,
|
|
1270
|
+
submittedAt: submitResult.submitted_at,
|
|
1271
|
+
verificationResult: this.verificationResult || undefined,
|
|
1272
|
+
});
|
|
1273
|
+
this.close();
|
|
1274
|
+
});
|
|
1275
|
+
wrapper.appendChild(doneBtn);
|
|
1276
|
+
this.contentArea.appendChild(wrapper);
|
|
1277
|
+
requestAnimationFrame(() => {
|
|
1278
|
+
wrapper.style.opacity = "1";
|
|
1279
|
+
wrapper.style.transform = "scale(1)";
|
|
1280
|
+
});
|
|
1281
|
+
// Start polling for the async result
|
|
1282
|
+
this._startStatusPolling(submitResult.id, iconWrap, statusBadge, title, desc, failureMsg, (resolved) => { finalStatus = resolved; });
|
|
1283
|
+
}
|
|
1284
|
+
_startStatusPolling(entryId, iconWrap, statusBadge, title, desc, failureMsg, onResolved) {
|
|
1285
|
+
const MAX_POLLS = 25; // 25 × 3 s = 75 s + 4 s initial = ~79 s max
|
|
1286
|
+
let polls = 0;
|
|
1287
|
+
// Hard 30 s UI timer — stops spinner even if backend is still processing
|
|
1288
|
+
let uiTimeoutId = setTimeout(() => {
|
|
1289
|
+
if (!this.isDestroyed)
|
|
1290
|
+
applyTimeout();
|
|
1291
|
+
}, 30000);
|
|
1292
|
+
const clearUiTimeout = () => { if (uiTimeoutId) {
|
|
1293
|
+
clearTimeout(uiTimeoutId);
|
|
1294
|
+
uiTimeoutId = null;
|
|
1295
|
+
} };
|
|
1296
|
+
const applyTimeout = () => {
|
|
1297
|
+
iconWrap.innerHTML = "⏱";
|
|
1298
|
+
iconWrap.style.background = this.theme.cardBg;
|
|
1299
|
+
iconWrap.style.border = `3px solid ${this.theme.warning}`;
|
|
1300
|
+
iconWrap.style.color = this.theme.warning;
|
|
1301
|
+
iconWrap.style.fontSize = "30px";
|
|
1302
|
+
statusBadge.style.color = this.theme.textMuted;
|
|
1303
|
+
statusBadge.textContent = "Still processing…";
|
|
1304
|
+
title.textContent = "Verification In Progress";
|
|
1305
|
+
desc.textContent = "Your verification is being processed. Your result will be delivered via your dashboard or webhook. You can safely close this.";
|
|
1306
|
+
};
|
|
1307
|
+
const applyPassed = () => {
|
|
1308
|
+
clearUiTimeout();
|
|
1309
|
+
iconWrap.innerHTML = "✓";
|
|
1310
|
+
iconWrap.style.background = this.theme.successBg;
|
|
1311
|
+
iconWrap.style.border = `3px solid ${this.theme.success}`;
|
|
1312
|
+
iconWrap.style.color = this.theme.success;
|
|
1313
|
+
iconWrap.style.fontSize = "36px";
|
|
1314
|
+
statusBadge.style.color = this.theme.success;
|
|
1315
|
+
statusBadge.textContent = "Verified ✓";
|
|
1316
|
+
title.textContent = "Identity Verified";
|
|
1317
|
+
desc.textContent = "Your identity check passed successfully.";
|
|
1318
|
+
};
|
|
1319
|
+
const applyFailed = (reason) => {
|
|
1320
|
+
clearUiTimeout();
|
|
1321
|
+
iconWrap.innerHTML = "✕";
|
|
1322
|
+
iconWrap.style.background = this.theme.errorBg;
|
|
1323
|
+
iconWrap.style.border = `3px solid ${this.theme.error}`;
|
|
1324
|
+
iconWrap.style.color = this.theme.error;
|
|
1325
|
+
iconWrap.style.fontSize = "32px";
|
|
1326
|
+
statusBadge.style.color = this.theme.error;
|
|
1327
|
+
statusBadge.textContent = "Failed";
|
|
1328
|
+
title.textContent = "Verification Failed";
|
|
1329
|
+
desc.textContent = "Your identity check could not be completed.";
|
|
1330
|
+
if (reason) {
|
|
1331
|
+
failureMsg.textContent = reason;
|
|
1332
|
+
failureMsg.style.display = "block";
|
|
1333
|
+
}
|
|
1334
|
+
};
|
|
1335
|
+
const poll = async () => {
|
|
1336
|
+
if (this.isDestroyed)
|
|
1337
|
+
return;
|
|
1338
|
+
polls++;
|
|
1339
|
+
try {
|
|
1340
|
+
const result = await this.sdk.liveness.pollResult(entryId);
|
|
1341
|
+
if (result.status === "passed") {
|
|
1342
|
+
onResolved("passed");
|
|
1343
|
+
applyPassed();
|
|
1344
|
+
return;
|
|
1345
|
+
}
|
|
1346
|
+
if (result.status === "failed" || result.status === "expired_document") {
|
|
1347
|
+
onResolved(result.status);
|
|
1348
|
+
applyFailed(result.failure_reason);
|
|
1349
|
+
return;
|
|
1350
|
+
}
|
|
1351
|
+
// Still pending/processing — schedule next poll if budget remains
|
|
1352
|
+
if (polls < MAX_POLLS && !this.isDestroyed) {
|
|
1353
|
+
this._pollStatusTimer = setTimeout(poll, 3000);
|
|
1354
|
+
}
|
|
1355
|
+
else if (!this.isDestroyed) {
|
|
1356
|
+
applyTimeout(); // budget exhausted — stop spinner, show timeout state
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
catch {
|
|
1360
|
+
// Network hiccup — retry with a slightly longer delay
|
|
1361
|
+
if (polls < MAX_POLLS && !this.isDestroyed) {
|
|
1362
|
+
this._pollStatusTimer = setTimeout(poll, 5000);
|
|
1363
|
+
}
|
|
1364
|
+
else if (!this.isDestroyed) {
|
|
1365
|
+
applyTimeout(); // budget exhausted — stop spinner
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
};
|
|
1369
|
+
// First poll after 4 s — gives Celery task time to complete
|
|
1370
|
+
this._pollStatusTimer = setTimeout(poll, 4000);
|
|
1371
|
+
}
|
|
1372
|
+
renderResult(_container) {
|
|
1373
|
+
// Handled by showResult() directly
|
|
1374
|
+
}
|
|
1375
|
+
// ── Error ───────────────────────────────────────────────────────
|
|
1376
|
+
showErrorStep(message) {
|
|
1377
|
+
this.currentStep = "error";
|
|
1378
|
+
if (!this.contentArea)
|
|
1379
|
+
return;
|
|
1380
|
+
this.contentArea.innerHTML = "";
|
|
1381
|
+
const wrapper = document.createElement("div");
|
|
1382
|
+
wrapper.style.cssText = `
|
|
1383
|
+
display:flex;flex-direction:column;align-items:center;
|
|
1384
|
+
text-align:center;gap:16px;padding:20px 0;
|
|
1385
|
+
`;
|
|
1386
|
+
const isCameraErr = /camera|permission|notallowed|notfound/i.test(message);
|
|
1387
|
+
const isNetworkErr = /network|timeout|connection|fetch/i.test(message);
|
|
1388
|
+
const isAuthErr = /session|token|expired|authentication/i.test(message);
|
|
1389
|
+
const errorIcon = isCameraErr ? "\uD83D\uDCF7" : isNetworkErr ? "\uD83D\uDCE1" : isAuthErr ? "\u23F1" : "\u2717";
|
|
1390
|
+
const errorTitle = isCameraErr ? "Camera Access Required"
|
|
1391
|
+
: isNetworkErr ? "Connection Problem"
|
|
1392
|
+
: isAuthErr ? "Session Expired"
|
|
1393
|
+
: "Verification Failed";
|
|
1394
|
+
const iconWrap = document.createElement("div");
|
|
1395
|
+
iconWrap.style.cssText = `
|
|
1396
|
+
width:72px;height:72px;border-radius:50%;
|
|
1397
|
+
display:flex;align-items:center;justify-content:center;
|
|
1398
|
+
font-size:${errorIcon === "\u2717" ? "32px" : "28px"};
|
|
1399
|
+
background:${this.theme.errorBg};
|
|
1400
|
+
border:3px solid ${this.theme.error};
|
|
1401
|
+
`;
|
|
1402
|
+
iconWrap.textContent = errorIcon;
|
|
1403
|
+
wrapper.appendChild(iconWrap);
|
|
1404
|
+
const title = document.createElement("h2");
|
|
1405
|
+
title.style.cssText = `color:${this.theme.text};font-size:18px;font-weight:700;margin:0;`;
|
|
1406
|
+
title.textContent = errorTitle;
|
|
1407
|
+
wrapper.appendChild(title);
|
|
1408
|
+
const desc = document.createElement("p");
|
|
1409
|
+
desc.style.cssText = `color:${this.theme.textSecondary};font-size:14px;line-height:1.6;margin:0;max-width:300px;`;
|
|
1410
|
+
desc.textContent = message;
|
|
1411
|
+
wrapper.appendChild(desc);
|
|
1412
|
+
const retryBtn = this.createPrimaryButton("Try Again");
|
|
1413
|
+
retryBtn.addEventListener("click", () => {
|
|
1414
|
+
// If session already exists, go back to welcome without re-initializing
|
|
1415
|
+
// (re-initializing would fail with CLIENT_ID_ALREADY_USED)
|
|
1416
|
+
if (this.sdk.sessionToken && this.sdkConfig) {
|
|
1417
|
+
this.showStep("welcome");
|
|
1418
|
+
}
|
|
1419
|
+
else {
|
|
1420
|
+
// Only re-initialize if session was never created
|
|
1421
|
+
this.initialize();
|
|
1422
|
+
}
|
|
1423
|
+
});
|
|
1424
|
+
wrapper.appendChild(retryBtn);
|
|
1425
|
+
const closeBtn = this.createSecondaryButton("Close");
|
|
1426
|
+
closeBtn.addEventListener("click", () => {
|
|
1427
|
+
this.options.onError?.(new Error(message));
|
|
1428
|
+
this.close();
|
|
1429
|
+
});
|
|
1430
|
+
wrapper.appendChild(closeBtn);
|
|
1431
|
+
this.contentArea.appendChild(wrapper);
|
|
1432
|
+
}
|
|
1433
|
+
// ─────────────────────────────────────────────────────────────────
|
|
1434
|
+
// UI Helpers
|
|
1435
|
+
// ─────────────────────────────────────────────────────────────────
|
|
1436
|
+
createHeader() {
|
|
1437
|
+
const header = document.createElement("div");
|
|
1438
|
+
header.style.cssText = `
|
|
1439
|
+
display:flex;align-items:center;justify-content:space-between;
|
|
1440
|
+
padding:14px 20px;border-bottom:1px solid ${this.theme.border};
|
|
1441
|
+
`;
|
|
1442
|
+
// Left: back button + brand
|
|
1443
|
+
const leftArea = document.createElement("div");
|
|
1444
|
+
leftArea.style.cssText = "display:flex;align-items:center;gap:8px;min-width:0;";
|
|
1445
|
+
this.headerBackBtn = document.createElement("button");
|
|
1446
|
+
this.headerBackBtn.innerHTML = "←";
|
|
1447
|
+
this.headerBackBtn.style.cssText = `
|
|
1448
|
+
width:30px;height:30px;border-radius:8px;flex-shrink:0;
|
|
1449
|
+
display:none;align-items:center;justify-content:center;
|
|
1450
|
+
border:1.5px solid ${this.theme.border};cursor:pointer;
|
|
1451
|
+
font-size:16px;line-height:1;
|
|
1452
|
+
background:${this.theme.cardBg};color:${this.theme.text};
|
|
1453
|
+
transition:all 0.2s ease;
|
|
1454
|
+
`;
|
|
1455
|
+
this.headerBackBtn.addEventListener("mouseenter", () => {
|
|
1456
|
+
if (this.headerBackBtn) {
|
|
1457
|
+
this.headerBackBtn.style.background = this.theme.shimmer;
|
|
1458
|
+
}
|
|
1459
|
+
});
|
|
1460
|
+
this.headerBackBtn.addEventListener("mouseleave", () => {
|
|
1461
|
+
if (this.headerBackBtn) {
|
|
1462
|
+
this.headerBackBtn.style.background = this.theme.cardBg;
|
|
1463
|
+
}
|
|
1464
|
+
});
|
|
1465
|
+
leftArea.appendChild(this.headerBackBtn);
|
|
1466
|
+
this.brandLabel = document.createElement("div");
|
|
1467
|
+
this.brandLabel.style.cssText = `
|
|
1468
|
+
font-size:15px;font-weight:700;color:${this.theme.text};
|
|
1469
|
+
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
|
|
1470
|
+
`;
|
|
1471
|
+
this.brandLabel.textContent = this.sdkConfig?.brand_name || "SmartComply";
|
|
1472
|
+
leftArea.appendChild(this.brandLabel);
|
|
1473
|
+
header.appendChild(leftArea);
|
|
1474
|
+
// Step indicator
|
|
1475
|
+
this.stepLabel = document.createElement("div");
|
|
1476
|
+
this.stepLabel.style.cssText = `
|
|
1477
|
+
font-size:12px;color:${this.theme.textMuted};
|
|
1478
|
+
font-variant-numeric:tabular-nums;flex-shrink:0;
|
|
1479
|
+
`;
|
|
1480
|
+
header.appendChild(this.stepLabel);
|
|
1481
|
+
// Close button
|
|
1482
|
+
const closeBtn = document.createElement("button");
|
|
1483
|
+
closeBtn.innerHTML = "✕";
|
|
1484
|
+
closeBtn.style.cssText = `
|
|
1485
|
+
width:32px;height:32px;border-radius:8px;flex-shrink:0;
|
|
1486
|
+
display:flex;align-items:center;justify-content:center;
|
|
1487
|
+
border:none;cursor:pointer;font-size:16px;
|
|
1488
|
+
background:${this.theme.shimmer};color:${this.theme.textMuted};
|
|
1489
|
+
transition:all 0.2s ease;
|
|
1490
|
+
`;
|
|
1491
|
+
closeBtn.addEventListener("mouseenter", () => {
|
|
1492
|
+
closeBtn.style.background = this.theme.errorBg;
|
|
1493
|
+
closeBtn.style.color = this.theme.error;
|
|
1494
|
+
});
|
|
1495
|
+
closeBtn.addEventListener("mouseleave", () => {
|
|
1496
|
+
closeBtn.style.background = this.theme.shimmer;
|
|
1497
|
+
closeBtn.style.color = this.theme.textMuted;
|
|
1498
|
+
});
|
|
1499
|
+
closeBtn.addEventListener("click", () => this.close());
|
|
1500
|
+
header.appendChild(closeBtn);
|
|
1501
|
+
return header;
|
|
1502
|
+
}
|
|
1503
|
+
createFooter() {
|
|
1504
|
+
const footer = document.createElement("div");
|
|
1505
|
+
footer.style.cssText = `
|
|
1506
|
+
padding:12px 20px;border-top:1px solid ${this.theme.border};
|
|
1507
|
+
display:flex;align-items:center;justify-content:center;gap:6px;
|
|
1508
|
+
`;
|
|
1509
|
+
const powered = document.createElement("span");
|
|
1510
|
+
powered.style.cssText = `font-size:11px;color:${this.theme.textMuted};`;
|
|
1511
|
+
powered.textContent = "Powered by ";
|
|
1512
|
+
const brand = document.createElement("span");
|
|
1513
|
+
brand.style.cssText = `font-size:11px;font-weight:700;color:${this.theme.primary};`;
|
|
1514
|
+
brand.textContent = "Adhere";
|
|
1515
|
+
footer.appendChild(powered);
|
|
1516
|
+
footer.appendChild(brand);
|
|
1517
|
+
return footer;
|
|
1518
|
+
}
|
|
1519
|
+
createStepTitle(text) {
|
|
1520
|
+
const el = document.createElement("h3");
|
|
1521
|
+
el.style.cssText = `
|
|
1522
|
+
color:${this.theme.text};font-size:18px;font-weight:700;
|
|
1523
|
+
margin:0;
|
|
1524
|
+
`;
|
|
1525
|
+
el.textContent = text;
|
|
1526
|
+
return el;
|
|
1527
|
+
}
|
|
1528
|
+
createOptionCard(title, subtitle, onClick) {
|
|
1529
|
+
const card = document.createElement("button");
|
|
1530
|
+
card.style.cssText = `
|
|
1531
|
+
width:100%;padding:16px;border-radius:12px;
|
|
1532
|
+
background:${this.theme.cardBg};
|
|
1533
|
+
border:2px solid ${this.theme.border};
|
|
1534
|
+
cursor:pointer;text-align:left;
|
|
1535
|
+
display:flex;align-items:center;justify-content:space-between;
|
|
1536
|
+
transition:all 0.2s ease;
|
|
1537
|
+
`;
|
|
1538
|
+
card.addEventListener("mouseenter", () => {
|
|
1539
|
+
card.style.borderColor = this.theme.primary;
|
|
1540
|
+
card.style.background = this.theme.shimmer;
|
|
1541
|
+
card.style.transform = "translateY(-1px)";
|
|
1542
|
+
});
|
|
1543
|
+
card.addEventListener("mouseleave", () => {
|
|
1544
|
+
card.style.borderColor = this.theme.border;
|
|
1545
|
+
card.style.background = this.theme.cardBg;
|
|
1546
|
+
card.style.transform = "translateY(0)";
|
|
1547
|
+
});
|
|
1548
|
+
card.addEventListener("click", onClick);
|
|
1549
|
+
const textWrap = document.createElement("div");
|
|
1550
|
+
const titleEl = document.createElement("div");
|
|
1551
|
+
titleEl.style.cssText = `color:${this.theme.text};font-size:14px;font-weight:600;`;
|
|
1552
|
+
titleEl.textContent = title;
|
|
1553
|
+
textWrap.appendChild(titleEl);
|
|
1554
|
+
if (subtitle) {
|
|
1555
|
+
const subEl = document.createElement("div");
|
|
1556
|
+
subEl.style.cssText = `color:${this.theme.textMuted};font-size:12px;margin-top:2px;`;
|
|
1557
|
+
subEl.textContent = subtitle;
|
|
1558
|
+
textWrap.appendChild(subEl);
|
|
1559
|
+
}
|
|
1560
|
+
const arrow = document.createElement("span");
|
|
1561
|
+
arrow.style.cssText = `color:${this.theme.textMuted};font-size:18px;flex-shrink:0;`;
|
|
1562
|
+
arrow.textContent = "→";
|
|
1563
|
+
card.appendChild(textWrap);
|
|
1564
|
+
card.appendChild(arrow);
|
|
1565
|
+
return card;
|
|
1566
|
+
}
|
|
1567
|
+
createPrimaryButton(text) {
|
|
1568
|
+
const btn = document.createElement("button");
|
|
1569
|
+
btn.textContent = text;
|
|
1570
|
+
btn.style.cssText = `
|
|
1571
|
+
width:100%;padding:14px;border-radius:12px;
|
|
1572
|
+
font-size:15px;font-weight:600;
|
|
1573
|
+
background:${this.theme.primary};color:#fff;
|
|
1574
|
+
border:none;cursor:pointer;
|
|
1575
|
+
transition:all 0.2s ease;
|
|
1576
|
+
box-shadow:0 2px 8px ${this.theme.primaryGlow};
|
|
1577
|
+
`;
|
|
1578
|
+
btn.addEventListener("mouseenter", () => {
|
|
1579
|
+
btn.style.background = this.theme.primaryHover;
|
|
1580
|
+
btn.style.transform = "translateY(-1px)";
|
|
1581
|
+
});
|
|
1582
|
+
btn.addEventListener("mouseleave", () => {
|
|
1583
|
+
btn.style.background = this.theme.primary;
|
|
1584
|
+
btn.style.transform = "translateY(0)";
|
|
1585
|
+
});
|
|
1586
|
+
return btn;
|
|
1587
|
+
}
|
|
1588
|
+
createSecondaryButton(text) {
|
|
1589
|
+
const btn = document.createElement("button");
|
|
1590
|
+
btn.textContent = text;
|
|
1591
|
+
btn.style.cssText = `
|
|
1592
|
+
width:100%;padding:12px;border-radius:12px;
|
|
1593
|
+
font-size:14px;font-weight:500;
|
|
1594
|
+
background:transparent;color:${this.theme.textSecondary};
|
|
1595
|
+
border:none;cursor:pointer;
|
|
1596
|
+
transition:all 0.2s ease;
|
|
1597
|
+
`;
|
|
1598
|
+
btn.addEventListener("mouseenter", () => {
|
|
1599
|
+
btn.style.color = this.theme.text;
|
|
1600
|
+
});
|
|
1601
|
+
btn.addEventListener("mouseleave", () => {
|
|
1602
|
+
btn.style.color = this.theme.textSecondary;
|
|
1603
|
+
});
|
|
1604
|
+
return btn;
|
|
1605
|
+
}
|
|
1606
|
+
isDocumentFlow() {
|
|
1607
|
+
// 1. Check verification_type — handle both string and array from backend
|
|
1608
|
+
const vType = this.sdkConfig?.verification_type;
|
|
1609
|
+
if (vType) {
|
|
1610
|
+
const types = Array.isArray(vType) ? vType : [vType];
|
|
1611
|
+
if (types.includes("document_verification"))
|
|
1612
|
+
return true;
|
|
1613
|
+
if (types.includes("data_verification"))
|
|
1614
|
+
return false;
|
|
1615
|
+
}
|
|
1616
|
+
// 2. Fallback: inspect channel fields — any upload field = document flow
|
|
1617
|
+
const channels = this.sdkConfig?.channels || {};
|
|
1618
|
+
const allFields = Object.values(channels)
|
|
1619
|
+
.flat()
|
|
1620
|
+
.flatMap((ch) => ch.fields || []);
|
|
1621
|
+
return allFields.some((f) => f.type === "upload");
|
|
1622
|
+
}
|
|
1623
|
+
updateProgress() {
|
|
1624
|
+
const isDocFlow = this.isDocumentFlow();
|
|
1625
|
+
const countries = Object.keys(this.sdkConfig?.channels || {});
|
|
1626
|
+
const singleCountry = countries.length <= 1;
|
|
1627
|
+
// User-action steps only (what the user actually does — no loading/welcome/result)
|
|
1628
|
+
const actionSteps = ["country", "id_type", isDocFlow ? "document_capture" : "id_input", "liveness"].filter((s) => !(s === "country" && singleCountry));
|
|
1629
|
+
const totalSteps = actionSteps.length;
|
|
1630
|
+
const actionIndex = actionSteps.indexOf(this.currentStep);
|
|
1631
|
+
const pct = this.currentStep === "result" ? 100
|
|
1632
|
+
: actionIndex >= 0 ? ((actionIndex + 1) / totalSteps) * 100
|
|
1633
|
+
: 0;
|
|
1634
|
+
if (this.progressBar) {
|
|
1635
|
+
this.progressBar.style.width = `${pct}%`;
|
|
1636
|
+
}
|
|
1637
|
+
if (this.stepLabel) {
|
|
1638
|
+
if (this.currentStep === "loading" || this.currentStep === "welcome") {
|
|
1639
|
+
this.stepLabel.textContent = "";
|
|
1640
|
+
}
|
|
1641
|
+
else if (this.currentStep === "result") {
|
|
1642
|
+
this.stepLabel.textContent = "Complete ✓";
|
|
1643
|
+
}
|
|
1644
|
+
else if (actionIndex >= 0) {
|
|
1645
|
+
this.stepLabel.textContent = `Step ${actionIndex + 1} of ${totalSteps}`;
|
|
1646
|
+
}
|
|
1647
|
+
else {
|
|
1648
|
+
this.stepLabel.textContent = "";
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
// Back button — show in header for navigable steps, hidden elsewhere
|
|
1652
|
+
if (this.headerBackBtn) {
|
|
1653
|
+
const countries = Object.keys(this.sdkConfig?.channels || {});
|
|
1654
|
+
const backTargets = {
|
|
1655
|
+
id_type: () => this.showStep(countries.length > 1 ? "country" : "welcome"),
|
|
1656
|
+
country: () => this.showStep("welcome"),
|
|
1657
|
+
id_input: () => this.showStep("id_type"),
|
|
1658
|
+
document_capture: () => { this.docCapture?.destroy(); this.showStep("id_type"); },
|
|
1659
|
+
};
|
|
1660
|
+
const backAction = backTargets[this.currentStep];
|
|
1661
|
+
if (backAction) {
|
|
1662
|
+
this.headerBackBtn.style.display = "flex";
|
|
1663
|
+
const newBtn = this.headerBackBtn.cloneNode(true);
|
|
1664
|
+
newBtn.style.cssText = this.headerBackBtn.style.cssText;
|
|
1665
|
+
this.headerBackBtn.parentNode?.replaceChild(newBtn, this.headerBackBtn);
|
|
1666
|
+
this.headerBackBtn = newBtn;
|
|
1667
|
+
this.headerBackBtn.addEventListener("mouseenter", () => {
|
|
1668
|
+
if (this.headerBackBtn)
|
|
1669
|
+
this.headerBackBtn.style.background = this.theme.shimmer;
|
|
1670
|
+
});
|
|
1671
|
+
this.headerBackBtn.addEventListener("mouseleave", () => {
|
|
1672
|
+
if (this.headerBackBtn)
|
|
1673
|
+
this.headerBackBtn.style.background = this.theme.cardBg;
|
|
1674
|
+
});
|
|
1675
|
+
this.headerBackBtn.addEventListener("click", backAction);
|
|
1676
|
+
}
|
|
1677
|
+
else {
|
|
1678
|
+
this.headerBackBtn.style.display = "none";
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
applyTheme() {
|
|
1683
|
+
if (this.overlay) {
|
|
1684
|
+
this.overlay.style.background = this.theme.overlay;
|
|
1685
|
+
}
|
|
1686
|
+
if (this.modal) {
|
|
1687
|
+
this.modal.style.background = this.theme.bg;
|
|
1688
|
+
this.modal.style.boxShadow = this.theme.shadow;
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
injectStyles() {
|
|
1692
|
+
if (document.getElementById("sc-flow-styles"))
|
|
1693
|
+
return;
|
|
1694
|
+
const style = document.createElement("style");
|
|
1695
|
+
style.id = "sc-flow-styles";
|
|
1696
|
+
style.textContent = `
|
|
1697
|
+
@keyframes sc-spin {
|
|
1698
|
+
to { transform: rotate(360deg); }
|
|
1699
|
+
}
|
|
1700
|
+
@keyframes sc-pop {
|
|
1701
|
+
0% { transform: scale(0.8); opacity: 0; }
|
|
1702
|
+
50% { transform: scale(1.05); }
|
|
1703
|
+
100% { transform: scale(1); opacity: 1; }
|
|
1704
|
+
}
|
|
1705
|
+
#sc-flow-modal * {
|
|
1706
|
+
box-sizing: border-box;
|
|
1707
|
+
}
|
|
1708
|
+
#sc-flow-modal::-webkit-scrollbar {
|
|
1709
|
+
width: 4px;
|
|
1710
|
+
}
|
|
1711
|
+
#sc-flow-content::-webkit-scrollbar {
|
|
1712
|
+
width: 4px;
|
|
1713
|
+
}
|
|
1714
|
+
#sc-flow-content::-webkit-scrollbar-thumb {
|
|
1715
|
+
background: rgba(128,128,128,0.3);
|
|
1716
|
+
border-radius: 4px;
|
|
1717
|
+
}
|
|
1718
|
+
`;
|
|
1719
|
+
document.head.appendChild(style);
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
//# sourceMappingURL=SmartComplyFlow.js.map
|