smartcomply-web-sdk 1.0.50 → 1.0.52

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