smartcomply-web-sdk 1.0.3

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