smartcomply-web-sdk 1.0.51 → 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 (35) hide show
  1. package/dist/camera/ActionDetector.d.ts +0 -5
  2. package/dist/camera/ActionDetector.d.ts.map +1 -1
  3. package/dist/camera/ActionDetector.js +1 -12
  4. package/dist/camera/ActionDetector.js.map +1 -1
  5. package/dist/esm/camera/ActionDetector.d.ts +0 -5
  6. package/dist/esm/camera/ActionDetector.d.ts.map +1 -1
  7. package/dist/esm/camera/ActionDetector.js +1 -12
  8. package/dist/esm/camera/ActionDetector.js.map +1 -1
  9. package/dist/esm/flow/SmartComplyFlow.js +144 -144
  10. package/dist/esm/flow/SmartComplyFlow.js.map +1 -1
  11. package/dist/esm/modules/liveness/LivenessUI.d.ts +0 -2
  12. package/dist/esm/modules/liveness/LivenessUI.d.ts.map +1 -1
  13. package/dist/esm/modules/liveness/LivenessUI.js.map +1 -1
  14. package/dist/esm/modules/liveness/liveness.d.ts.map +1 -1
  15. package/dist/esm/modules/liveness/liveness.js +1 -3
  16. package/dist/esm/modules/liveness/liveness.js.map +1 -1
  17. package/dist/esm/utils/constants.d.ts +0 -2
  18. package/dist/esm/utils/constants.d.ts.map +1 -1
  19. package/dist/esm/utils/constants.js +0 -2
  20. package/dist/esm/utils/constants.js.map +1 -1
  21. package/dist/flow/SmartComplyFlow.js +144 -144
  22. package/dist/flow/SmartComplyFlow.js.map +1 -1
  23. package/dist/modules/liveness/LivenessUI.d.ts +0 -2
  24. package/dist/modules/liveness/LivenessUI.d.ts.map +1 -1
  25. package/dist/modules/liveness/LivenessUI.js.map +1 -1
  26. package/dist/modules/liveness/liveness.d.ts.map +1 -1
  27. package/dist/modules/liveness/liveness.js +1 -3
  28. package/dist/modules/liveness/liveness.js.map +1 -1
  29. package/dist/smartcomply.browser.js +86 -100
  30. package/dist/smartcomply.browser.js.map +2 -2
  31. package/dist/utils/constants.d.ts +0 -2
  32. package/dist/utils/constants.d.ts.map +1 -1
  33. package/dist/utils/constants.js +1 -3
  34. package/dist/utils/constants.js.map +1 -1
  35. package/package.json +1 -1
@@ -20,15 +20,15 @@ const STEP_ORDER = [
20
20
  "result",
21
21
  ];
22
22
  const COUNTRY_LABELS = {
23
- nigeria: "🇳🇬 Nigeria",
24
- global: "🌍 Other Countries",
25
- ghana: "🇬🇭 Ghana",
26
- kenya: "🇰🇪 Kenya",
27
- south_africa: "🇿🇦 South Africa",
28
- united_states: "🇺🇸 United States",
29
- united_kingdom: "🇬🇧 United Kingdom",
23
+ nigeria: "🇳🇬 Nigeria",
24
+ global: "🌍 Other Countries",
25
+ ghana: "🇬🇭 Ghana",
26
+ kenya: "🇰🇪 Kenya",
27
+ south_africa: "🇿🇦 South Africa",
28
+ united_states: "🇺🇸 United States",
29
+ united_kingdom: "🇬🇧 United Kingdom",
30
30
  };
31
- // Backend/admin data tags document-capture fields inconsistently — the field
31
+ // Backend/admin data tags document-capture fields inconsistently — the field
32
32
  // renderer (below) already treats "image" and "file" as file inputs alongside
33
33
  // "upload", so flow-routing decisions must recognize the same set or a channel
34
34
  // tagged "image"/"file" silently falls back to the number-entry screen.
@@ -38,7 +38,7 @@ function isUploadFieldType(type) {
38
38
  function resolveIdTypeInfo(typeName, isDocumentFlow) {
39
39
  const u = typeName.toUpperCase();
40
40
  const advanced = u.includes("ADVANCED");
41
- // Strip UI noise from display labels — users don't need "with face" or "(NIN)" suffixes
41
+ // Strip UI noise from display labels — users don't need "with face" or "(NIN)" suffixes
42
42
  const cleanName = typeName
43
43
  .replace(/\s*with\s+face\s*/gi, ' ')
44
44
  .replace(/\s*\(NIN\)\s*/gi, ' ')
@@ -53,41 +53,41 @@ function resolveIdTypeInfo(typeName, isDocumentFlow) {
53
53
  t.includes("license") ||
54
54
  t.includes("national id") ||
55
55
  t.includes("national id card");
56
- // Note: NIN document (nin_with_face, nin_slip) does NOT require back side —
56
+ // Note: NIN document (nin_with_face, nin_slip) does NOT require back side —
57
57
  // the NIN card is single-sided. Only the physical NIN slip has a back but
58
58
  // the OCR pipeline only needs the front.
59
59
  };
60
- // BVN — always 11 digits
60
+ // BVN — always 11 digits
61
61
  if (u.includes("BVN") || u.includes("BANK VERIFICATION")) {
62
- return { icon: "🏦", desc: "Enter your 11-digit Bank Verification Number (BVN)", requiresBack: false };
62
+ return { icon: "🏦", desc: "Enter your 11-digit Bank Verification Number (BVN)", requiresBack: false };
63
63
  }
64
- // NIN — always 11 digits (covers nin_with_face, NIN slip, etc.)
64
+ // NIN — always 11 digits (covers nin_with_face, NIN slip, etc.)
65
65
  if (u.includes("NIN") || u.includes("NATIONAL IDENTITY NUMBER")) {
66
- return { icon: "🆔", desc: "Enter your 11-digit National Identification Number (NIN)", requiresBack: false };
66
+ return { icon: "🆔", desc: "Enter your 11-digit National Identification Number (NIN)", requiresBack: false };
67
67
  }
68
68
  // Ghana Card / Ghana National ID
69
69
  if (u.includes("GHANA")) {
70
70
  if (isDocumentFlow)
71
- return { icon: "🪪", desc: "Capture or upload your Ghana Card", requiresBack: true };
72
- return { icon: "🪪", desc: "Enter your Ghana Card number", requiresBack: false };
71
+ return { icon: "🪪", desc: "Capture or upload your Ghana Card", requiresBack: true };
72
+ return { icon: "🪪", desc: "Enter your Ghana Card number", requiresBack: false };
73
73
  }
74
74
  // SSNIT (Ghana social security)
75
75
  if (u.includes("SSNIT"))
76
- return { icon: "🏛️", desc: "Enter your SSNIT number", requiresBack: false };
76
+ return { icon: "🏛️", desc: "Enter your SSNIT number", requiresBack: false };
77
77
  // Kenya National ID
78
78
  if (u.includes("KENYA") || (u.includes("NATIONAL") && u.includes("KENYA")))
79
- return { icon: "🆔", desc: "Enter your Kenya National ID number", requiresBack: false };
79
+ return { icon: "🆔", desc: "Enter your Kenya National ID number", requiresBack: false };
80
80
  // TIN
81
81
  if (u.includes("TIN") || u.includes("TAX IDENTIFICATION"))
82
- return { icon: "🧾", desc: "Enter your Tax Identification Number (TIN)", requiresBack: false };
82
+ return { icon: "🧾", desc: "Enter your Tax Identification Number (TIN)", requiresBack: false };
83
83
  // CAC / Company registration
84
84
  if (u.includes("CAC") || u.includes("CORPORATE AFFAIRS") || u.includes("COMPANY"))
85
- return { icon: "🏢", desc: advanced ? "Enter RC number, company name & type" : "Enter your company registration number", requiresBack: false };
85
+ return { icon: "🏢", desc: advanced ? "Enter RC number, company name & type" : "Enter your company registration number", requiresBack: false };
86
86
  // Voter / PVC
87
87
  if (u.includes("VOTER") || u.includes("PVC")) {
88
88
  const requiresBack = requiresBackSide(typeName);
89
89
  return {
90
- icon: "🗳️",
90
+ icon: "🗳️",
91
91
  desc: isDocumentFlow
92
92
  ? `Capture or upload your voter's card${requiresBack ? " (front & back)" : ""}`
93
93
  : "Enter your voter ID number",
@@ -96,12 +96,12 @@ function resolveIdTypeInfo(typeName, isDocumentFlow) {
96
96
  }
97
97
  // Passport
98
98
  if (u.includes("PASSPORT"))
99
- return { icon: "📘", desc: isDocumentFlow ? "Capture or upload your passport" : "Enter your passport number", requiresBack: false };
99
+ return { icon: "📘", desc: isDocumentFlow ? "Capture or upload your passport" : "Enter your passport number", requiresBack: false };
100
100
  // Driver's license
101
101
  if (u.includes("DRIVER") || u.includes("LICENSE") || u.includes("LICENCE")) {
102
102
  const requiresBack = requiresBackSide(typeName);
103
103
  return {
104
- icon: "🚗",
104
+ icon: "🚗",
105
105
  desc: isDocumentFlow
106
106
  ? `Capture or upload your driver's license${requiresBack ? " (front & back)" : ""}`
107
107
  : "Enter your driver's license number",
@@ -112,7 +112,7 @@ function resolveIdTypeInfo(typeName, isDocumentFlow) {
112
112
  if (u.includes("NATIONAL ID") || (u.includes("NATIONAL") && u.includes("CARD"))) {
113
113
  const requiresBack = requiresBackSide(typeName);
114
114
  return {
115
- icon: "🪪",
115
+ icon: "🪪",
116
116
  desc: isDocumentFlow
117
117
  ? `Capture or upload your ID card${requiresBack ? " (front & back)" : ""}`
118
118
  : "Enter your National ID number",
@@ -121,15 +121,15 @@ function resolveIdTypeInfo(typeName, isDocumentFlow) {
121
121
  }
122
122
  // Face liveness / comparison
123
123
  if (u.includes("LIVENESS") || u.includes("FACE COMPARISON") || u.includes("FACE MATCH"))
124
- return { icon: "🤳", desc: "We'll capture a short selfie video to verify you", requiresBack: false };
124
+ return { icon: "🤳", desc: "We'll capture a short selfie video to verify you", requiresBack: false };
125
125
  return {
126
- icon: isDocumentFlow ? "📄" : "🆔",
126
+ icon: isDocumentFlow ? "📄" : "🆔",
127
127
  desc: isDocumentFlow ? "Capture or upload your document" : "Enter your ID number",
128
128
  requiresBack: false,
129
129
  };
130
130
  }
131
- // ─────────────────────────────────────────────────────────────────────
132
- // ── Widget-side message sanitizer ─────────────────────────────────────────────
131
+ // ─────────────────────────────────────────────────────────────────────
132
+ // ── Widget-side message sanitizer ─────────────────────────────────────────────
133
133
  // Strips anything technical before showing text to the end user.
134
134
  // Keeps messages that are already written plainly in natural language.
135
135
  function _sanitizeUserMessage(raw) {
@@ -158,7 +158,7 @@ function _sanitizeUserMessage(raw) {
158
158
  return null;
159
159
  return raw.trim();
160
160
  }
161
- // ── B8: OCR date string normaliser ───────────────────────────────────────────
161
+ // ── B8: OCR date string normaliser ───────────────────────────────────────────
162
162
  // OCR providers return dates in wildly different formats. new Date(str) is
163
163
  // locale-dependent for anything other than ISO 8601 (YYYY-MM-DD), so we
164
164
  // attempt a set of known patterns before falling back to the native parser.
@@ -166,14 +166,14 @@ function _parseOcrDate(raw) {
166
166
  if (!raw)
167
167
  return null;
168
168
  const s = raw.trim();
169
- // ISO 8601 or near-ISO — try first, always unambiguous
169
+ // ISO 8601 or near-ISO — try first, always unambiguous
170
170
  if (/^\d{4}-\d{2}-\d{2}/.test(s))
171
171
  return new Date(s.slice(0, 10));
172
172
  // DD/MM/YYYY or DD-MM-YYYY
173
173
  const dmy = s.match(/^(\d{2})[\/\-](\d{2})[\/\-](\d{4})/);
174
174
  if (dmy)
175
175
  return new Date(`${dmy[3]}-${dmy[2]}-${dmy[1]}`);
176
- // MM/DD/YYYY (US format — less common from OCR but possible)
176
+ // MM/DD/YYYY (US format — less common from OCR but possible)
177
177
  const mdy = s.match(/^(\d{2})\/(\d{2})\/(\d{4})/);
178
178
  if (mdy) {
179
179
  // Heuristic: if "month" > 12 it must be day-first
@@ -189,7 +189,7 @@ function _parseOcrDate(raw) {
189
189
  const d = new Date(s);
190
190
  return isNaN(d.getTime()) ? null : d;
191
191
  }
192
- // ── W12: Image compression before document upload ────────────────────────────
192
+ // ── W12: Image compression before document upload ────────────────────────────
193
193
  async function _compressImage(blob, maxWidth = 1600, quality = 0.85) {
194
194
  try {
195
195
  const bmp = await createImageBitmap(blob);
@@ -207,7 +207,7 @@ async function _compressImage(blob, maxWidth = 1600, quality = 0.85) {
207
207
  return blob; // fallback to original on any error
208
208
  }
209
209
  }
210
- // ── Document type code → human label ─────────────────────────────────────────
210
+ // ── Document type code → human label ─────────────────────────────────────────
211
211
  function _formatDocType(code) {
212
212
  const map = {
213
213
  passport: "Passport",
@@ -228,7 +228,7 @@ function _formatDocType(code) {
228
228
  return code.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
229
229
  }
230
230
  // SmartComplyFlow
231
- // ─────────────────────────────────────────────────────────────────────
231
+ // ─────────────────────────────────────────────────────────────────────
232
232
  // Add CSS animation keyframes for spin animations
233
233
  const style = document.createElement('style');
234
234
  style.textContent = `
@@ -270,7 +270,7 @@ class SmartComplyFlow {
270
270
  this.livenessEntryId = null;
271
271
  this.isDestroyed = false;
272
272
  this.headerBackBtn = null;
273
- // Retry safety — max 3 confirmation rejections or liveness failures per session
273
+ // Retry safety — max 3 confirmation rejections or liveness failures per session
274
274
  this._confirmRetries = 0;
275
275
  this._livenessFailures = 0;
276
276
  // IVS result stored for confirm_identity screen
@@ -285,9 +285,9 @@ class SmartComplyFlow {
285
285
  this._ocrExpirySoon = false;
286
286
  // Camera pre-warm stream (created during confirmation screen, used in liveness)
287
287
  this._prewarmedStream = null;
288
- // OCR gate poll timer — stored so it can be cancelled on navigation or close
288
+ // OCR gate poll timer — stored so it can be cancelled on navigation or close
289
289
  this._ocrPollTimer = null;
290
- // UI timeout from _startStatusPolling — stored so close() can cancel it
290
+ // UI timeout from _startStatusPolling — stored so close() can cancel it
291
291
  this._uiTimeoutId = null;
292
292
  // Session expiry
293
293
  this.sessionExpiryTimer = null;
@@ -357,7 +357,7 @@ class SmartComplyFlow {
357
357
  try {
358
358
  this.overlay?.remove();
359
359
  }
360
- catch (_) { /* extension interference — safe to ignore */ }
360
+ catch (_) { /* extension interference — safe to ignore */ }
361
361
  this.overlay = null;
362
362
  this.modal = null;
363
363
  this.contentArea = null;
@@ -365,9 +365,9 @@ class SmartComplyFlow {
365
365
  }
366
366
  this.options.onClose?.();
367
367
  }
368
- // ─────────────────────────────────────────────────────────────────
368
+ // ─────────────────────────────────────────────────────────────────
369
369
  // Initialization
370
- // ─────────────────────────────────────────────────────────────────
370
+ // ─────────────────────────────────────────────────────────────────
371
371
  mount() {
372
372
  // Inject keyframes
373
373
  this.injectStyles();
@@ -452,7 +452,7 @@ class SmartComplyFlow {
452
452
  }
453
453
  // Load config
454
454
  this.sdkConfig = await this.sdk.initializeConfig();
455
- // Apply theme from config — use theme_hex to override primary colour if set
455
+ // Apply theme from config — use theme_hex to override primary colour if set
456
456
  if (this.sdkConfig.theme) {
457
457
  this.theme = (0, theme_1.getTheme)(this.sdkConfig.theme, this.sdkConfig.theme_hex || null);
458
458
  this.applyTheme();
@@ -469,9 +469,9 @@ class SmartComplyFlow {
469
469
  this.showErrorStep(err.message || "Failed to initialize. Please try again.");
470
470
  }
471
471
  }
472
- // ─────────────────────────────────────────────────────────────────
472
+ // ─────────────────────────────────────────────────────────────────
473
473
  // Step rendering
474
- // ─────────────────────────────────────────────────────────────────
474
+ // ─────────────────────────────────────────────────────────────────
475
475
  showStep(step) {
476
476
  this.currentStep = step;
477
477
  if (!this.contentArea)
@@ -482,7 +482,7 @@ class SmartComplyFlow {
482
482
  this._ocrPollTimer = null;
483
483
  }
484
484
  // Release a pre-warmed camera stream if we're navigating anywhere other
485
- // than liveness (the only step that consumes it) — otherwise "No, use a
485
+ // than liveness (the only step that consumes it) — otherwise "No, use a
486
486
  // different ID" / "No, retake document" / header back-button leave the
487
487
  // camera held open indefinitely with no consumer.
488
488
  if (step !== "liveness" && this._prewarmedStream) {
@@ -539,7 +539,7 @@ class SmartComplyFlow {
539
539
  this.renderDocumentConfirm(wrapper);
540
540
  break;
541
541
  case "processing":
542
- // C1: processing step is dead code — flow now goes to 'done' after submit
542
+ // C1: processing step is dead code — flow now goes to 'done' after submit
543
543
  break;
544
544
  case "liveness":
545
545
  this.renderLiveness(wrapper);
@@ -548,7 +548,7 @@ class SmartComplyFlow {
548
548
  this.renderDone(wrapper);
549
549
  break;
550
550
  case "result":
551
- // C2: result step is dead code — showResult/renderResult removed
551
+ // C2: result step is dead code — showResult/renderResult removed
552
552
  break;
553
553
  case "error":
554
554
  break;
@@ -560,7 +560,7 @@ class SmartComplyFlow {
560
560
  wrapper.style.transform = "translateX(0)";
561
561
  });
562
562
  }
563
- // ── Loading ─────────────────────────────────────────────────────
563
+ // ── Loading ─────────────────────────────────────────────────────
564
564
  renderLoading(container) {
565
565
  container.style.cssText += "display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:300px;gap:16px;";
566
566
  const spinnerWrap = document.createElement("div");
@@ -591,7 +591,7 @@ class SmartComplyFlow {
591
591
  sub.textContent = "Connecting to the verification service...";
592
592
  container.appendChild(sub);
593
593
  }
594
- // ── Welcome ────────────────────────────────────────────────────
594
+ // ── Welcome ────────────────────────────────────────────────────
595
595
  renderWelcome(container) {
596
596
  container.style.cssText += "display:flex;flex-direction:column;align-items:center;text-align:center;gap:12px;";
597
597
  const isDocFlow = this.isDocumentFlow();
@@ -606,7 +606,7 @@ class SmartComplyFlow {
606
606
  background:linear-gradient(135deg,${this.theme.primary}22,${this.theme.primaryGlow});
607
607
  border:1.5px solid ${this.theme.primary}33;
608
608
  `;
609
- iconWrap.textContent = isDocFlow ? "🪪" : "🛡️";
609
+ iconWrap.textContent = isDocFlow ? "🪪" : "🛡️";
610
610
  topRow.appendChild(iconWrap);
611
611
  const badge = document.createElement("div");
612
612
  badge.style.cssText = `
@@ -617,7 +617,7 @@ class SmartComplyFlow {
617
617
  color:${this.theme.primary};
618
618
  border:1px solid ${this.theme.primary}30;
619
619
  `;
620
- badge.textContent = isDocFlow ? "📄 Document Verification" : "🔍 Data Verification";
620
+ badge.textContent = isDocFlow ? "📄 Document Verification" : "🔍 Data Verification";
621
621
  topRow.appendChild(badge);
622
622
  container.appendChild(topRow);
623
623
  // Title + subtitle (compact)
@@ -644,17 +644,17 @@ class SmartComplyFlow {
644
644
  : "You'll need your ID number and camera access. This takes about 2 minutes.";
645
645
  titleWrap.appendChild(desc);
646
646
  container.appendChild(titleWrap);
647
- // Steps preview — compact rows
647
+ // Steps preview — compact rows
648
648
  const steps = isDocFlow
649
649
  ? [
650
- { icon: "🪪", text: "Choose your document type", sub: "Passport, National ID, Driver License" },
651
- { icon: "📷", text: "Scan your document", sub: "Upload or capture a photo of your ID" },
652
- { icon: "🤳", text: "Face check", sub: "Quick selfie to confirm it's you" },
650
+ { icon: "🪪", text: "Choose your document type", sub: "Passport, National ID, Driver License" },
651
+ { icon: "📷", text: "Scan your document", sub: "Upload or capture a photo of your ID" },
652
+ { icon: "🤳", text: "Face check", sub: "Quick selfie to confirm it's you" },
653
653
  ]
654
654
  : [
655
- { icon: "🆔", text: "Choose your ID type", sub: "NIN, BVN, Voter's Card and more" },
656
- { icon: "📝", text: "Enter your ID details", sub: "Exactly as shown on your ID" },
657
- { icon: "🤳", text: "Face check", sub: "Quick selfie to confirm it's you" },
655
+ { icon: "🆔", text: "Choose your ID type", sub: "NIN, BVN, Voter's Card and more" },
656
+ { icon: "📝", text: "Enter your ID details", sub: "Exactly as shown on your ID" },
657
+ { icon: "🤳", text: "Face check", sub: "Quick selfie to confirm it's you" },
658
658
  ];
659
659
  const stepsWrap = document.createElement("div");
660
660
  stepsWrap.style.cssText = `
@@ -695,21 +695,21 @@ class SmartComplyFlow {
695
695
  });
696
696
  container.appendChild(stepsWrap);
697
697
  // CTA
698
- const btn = this.createPrimaryButton("Get Started →");
698
+ const btn = this.createPrimaryButton("Get Started →");
699
699
  btn.style.cssText += "margin-top:2px;";
700
700
  btn.addEventListener("click", () => this.showStep("country"));
701
701
  container.appendChild(btn);
702
- // Trust badge — "Powered by Adhere" already lives in the persistent
702
+ // Trust badge — "Powered by Adhere" already lives in the persistent
703
703
  // modal footer below; repeating it here just cost vertical space.
704
704
  const trust = document.createElement("div");
705
705
  trust.style.cssText = `
706
706
  display:flex;align-items:center;gap:6px;
707
707
  color:${this.theme.textMuted};font-size:10.5px;
708
708
  `;
709
- trust.textContent = "🔒 End-to-end encrypted";
709
+ trust.textContent = "🔒 End-to-end encrypted";
710
710
  container.appendChild(trust);
711
711
  }
712
- // ── Country Select ──────────────────────────────────────────────
712
+ // ── Country Select ──────────────────────────────────────────────
713
713
  renderCountrySelect(container) {
714
714
  container.style.cssText += "display:flex;flex-direction:column;gap:14px;";
715
715
  const channels = this.sdkConfig?.channels || {};
@@ -734,7 +734,7 @@ class SmartComplyFlow {
734
734
  container.appendChild(card);
735
735
  }
736
736
  }
737
- // ── ID Type Select ──────────────────────────────────────────────
737
+ // ── ID Type Select ──────────────────────────────────────────────
738
738
  renderIdTypeSelect(container) {
739
739
  container.style.cssText += "display:flex;flex-direction:column;gap:14px;";
740
740
  const globalDocFlow = this.isDocumentFlow();
@@ -762,9 +762,9 @@ class SmartComplyFlow {
762
762
  // Add back side indicator to subtitle if needed
763
763
  let subtitle = info.desc;
764
764
  if (requiresBack && isThisDocFlow) {
765
- subtitle += " ⚠️ Both sides required";
765
+ subtitle += " ⚠️ Both sides required";
766
766
  }
767
- // Clean display name — strip "with face", "(NIN)", "(BVN)" from user-visible label
767
+ // Clean display name — strip "with face", "(NIN)", "(BVN)" from user-visible label
768
768
  const displayName = typeName
769
769
  .replace(/\s*with\s+face\s*/gi, ' ')
770
770
  .replace(/\s*\(NIN\)\s*/gi, ' ')
@@ -801,7 +801,7 @@ class SmartComplyFlow {
801
801
  container.appendChild(card);
802
802
  }
803
803
  }
804
- // ── ID Input (data verification — dynamic fields) ─────────────
804
+ // ── ID Input (data verification — dynamic fields) ─────────────
805
805
  renderIdInput(container) {
806
806
  container.style.cssText += "display:flex;flex-direction:column;gap:14px;";
807
807
  const dataFields = this.selectedChannelFields.filter((f) => f.type !== "upload");
@@ -820,7 +820,7 @@ class SmartComplyFlow {
820
820
  ? `Enter your ${this.selectedIdTypeName || this.selectedIdType} details exactly as they appear on your ID.`
821
821
  : "Fill in the fields below to continue.";
822
822
  container.appendChild(subtitle);
823
- // Edge case: no data fields configured — show fallback and allow submission
823
+ // Edge case: no data fields configured — show fallback and allow submission
824
824
  if (dataFields.length === 0) {
825
825
  const empty = document.createElement("div");
826
826
  empty.style.cssText = `
@@ -839,13 +839,13 @@ class SmartComplyFlow {
839
839
  { match: "BVN", rule: { minLen: 11, maxLen: 11, numeric: true, hint: "BVN must be exactly 11 digits" } },
840
840
  { match: "NIN", rule: { minLen: 11, maxLen: 11, numeric: true, hint: "NIN must be exactly 11 digits" } },
841
841
  { match: "NATIONAL IDENTITY", rule: { minLen: 11, maxLen: 11, numeric: true, hint: "NIN must be exactly 11 digits" } },
842
- { match: "PASSPORT", rule: { minLen: 6, maxLen: 20, alphanumeric: true, hint: "Passport number must be 6–20 alphanumeric characters" } },
843
- { match: "DRIVER", rule: { minLen: 3, maxLen: 30, alphanumeric: true, hint: "Licence number must be 3–30 alphanumeric characters" } },
844
- { match: "VOTER", rule: { minLen: 6, maxLen: 30, alphanumeric: true, hint: "Voter ID must be 6–30 alphanumeric characters" } },
845
- { match: "GHANA", rule: { minLen: 8, maxLen: 20, alphanumeric: true, hint: "Ghana Card number must be 8–20 alphanumeric characters" } },
846
- { match: "SSNIT", rule: { minLen: 7, maxLen: 15, alphanumeric: true, hint: "SSNIT number must be 7–15 alphanumeric characters" } },
847
- { match: "TIN", rule: { minLen: 8, maxLen: 20, alphanumeric: true, hint: "TIN must be 8–20 characters" } },
848
- { match: "KRA", rule: { minLen: 10, maxLen: 12, alphanumeric: true, hint: "KRA PIN must be 10–12 characters" } },
842
+ { match: "PASSPORT", rule: { minLen: 6, maxLen: 20, alphanumeric: true, hint: "Passport number must be 6–20 alphanumeric characters" } },
843
+ { match: "DRIVER", rule: { minLen: 3, maxLen: 30, alphanumeric: true, hint: "Licence number must be 3–30 alphanumeric characters" } },
844
+ { match: "VOTER", rule: { minLen: 6, maxLen: 30, alphanumeric: true, hint: "Voter ID must be 6–30 alphanumeric characters" } },
845
+ { match: "GHANA", rule: { minLen: 8, maxLen: 20, alphanumeric: true, hint: "Ghana Card number must be 8–20 alphanumeric characters" } },
846
+ { match: "SSNIT", rule: { minLen: 7, maxLen: 15, alphanumeric: true, hint: "SSNIT number must be 7–15 alphanumeric characters" } },
847
+ { match: "TIN", rule: { minLen: 8, maxLen: 20, alphanumeric: true, hint: "TIN must be 8–20 characters" } },
848
+ { match: "KRA", rule: { minLen: 10, maxLen: 12, alphanumeric: true, hint: "KRA PIN must be 10–12 characters" } },
849
849
  ];
850
850
  function getFieldRule(label) {
851
851
  const upper = label.toUpperCase();
@@ -873,7 +873,7 @@ class SmartComplyFlow {
873
873
  // Track input elements for validation
874
874
  const inputEls = [];
875
875
  for (const field of dataFields) {
876
- // Send the raw label as the payload key — the backend's
876
+ // Send the raw label as the payload key — the backend's
877
877
  // _SDK_FIELD_KEY_MAP looks up the exact label text (e.g. "National ID",
878
878
  // "CAC Number") to remap it to the canonical IVS key. Pre-normalizing
879
879
  // it here (as this used to do) produced keys like "national_id" that
@@ -923,7 +923,7 @@ class SmartComplyFlow {
923
923
  inputEls.push({ field, el: select });
924
924
  }
925
925
  else if (field.type === "image" || field.type === "file") {
926
- // File upload field — converts to base64 for API submission
926
+ // File upload field — converts to base64 for API submission
927
927
  const fileInput = document.createElement("input");
928
928
  fileInput.type = "file";
929
929
  fileInput.accept = "image/*";
@@ -1073,14 +1073,14 @@ class SmartComplyFlow {
1073
1073
  this._ivsName = [nd.first_name || nd.firstname || "", nd.last_name || nd.lastname || nd.surname || ""]
1074
1074
  .map(s => String(s || "").trim()).filter(Boolean).join(" ");
1075
1075
  this._ivsGender = nd.gender || "";
1076
- // W4: If IVS returned no name, there is nothing for the user to confirm —
1076
+ // W4: If IVS returned no name, there is nothing for the user to confirm —
1077
1077
  // auto-proceed straight to Stage 2 (liveness) instead of showing a
1078
1078
  // confirmation screen with no identity details to check.
1079
1079
  if (!this._ivsName) {
1080
1080
  this.showStep("liveness");
1081
1081
  return;
1082
1082
  }
1083
- // Route to confirmation screen — user must confirm before liveness
1083
+ // Route to confirmation screen — user must confirm before liveness
1084
1084
  this.showStep("confirm_identity");
1085
1085
  }
1086
1086
  else {
@@ -1092,10 +1092,10 @@ class SmartComplyFlow {
1092
1092
  }
1093
1093
  }
1094
1094
  catch (err) {
1095
- // Show a single plain-language error — never raw field:message format
1095
+ // Show a single plain-language error — never raw field:message format
1096
1096
  const fieldErrors = err.data?.errors;
1097
1097
  if (Array.isArray(fieldErrors) && fieldErrors.length > 0) {
1098
- // Pick the first meaningful error message — strip field prefix
1098
+ // Pick the first meaningful error message — strip field prefix
1099
1099
  const firstMsg = fieldErrors[0]?.message || fieldErrors[0]?.detail || "";
1100
1100
  errorEl.textContent = _sanitizeUserMessage(firstMsg)
1101
1101
  || "Please check your details and try again.";
@@ -1126,7 +1126,7 @@ class SmartComplyFlow {
1126
1126
  setTimeout(() => firstInput.focus(), 350);
1127
1127
  }
1128
1128
  }
1129
- // ── Document Capture ────────────────────────────────────────────
1129
+ // ── Document Capture ────────────────────────────────────────────
1130
1130
  renderDocumentCapture(container) {
1131
1131
  container.style.cssText += "display:flex;flex-direction:column;gap:14px;";
1132
1132
  // Strip "with face", "(NIN)", "(BVN)" noise from the user-visible title
@@ -1167,7 +1167,7 @@ class SmartComplyFlow {
1167
1167
  this.showErrorStep(err.message || "Document capture failed. Please try again.");
1168
1168
  });
1169
1169
  }
1170
- // ── W3: Identity Confirmation (data verification) ───────────────
1170
+ // ── W3: Identity Confirmation (data verification) ───────────────
1171
1171
  renderConfirmIdentity(container) {
1172
1172
  container.style.cssText += "display:flex;flex-direction:column;align-items:center;text-align:center;gap:16px;padding:8px 0;";
1173
1173
  // W14: Check retry limit before showing screen
@@ -1179,7 +1179,7 @@ class SmartComplyFlow {
1179
1179
  this._prewarmCamera();
1180
1180
  const iconWrap = document.createElement("div");
1181
1181
  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;`;
1182
- iconWrap.textContent = "🛡️";
1182
+ iconWrap.textContent = "🛡️";
1183
1183
  container.appendChild(iconWrap);
1184
1184
  const title = document.createElement("h2");
1185
1185
  title.style.cssText = `color:${this.theme.text};font-size:18px;font-weight:700;margin:0;`;
@@ -1218,7 +1218,7 @@ class SmartComplyFlow {
1218
1218
  sub.textContent = "Your details have been confirmed. Please continue to the face check.";
1219
1219
  container.appendChild(sub);
1220
1220
  }
1221
- const yesBtn = this.createPrimaryButton("Yes, that's me — continue");
1221
+ const yesBtn = this.createPrimaryButton("Yes, that's me — continue");
1222
1222
  yesBtn.addEventListener("click", () => {
1223
1223
  // B5: Reset retry counter on successful confirmation
1224
1224
  this._confirmRetries = 0;
@@ -1236,7 +1236,7 @@ class SmartComplyFlow {
1236
1236
  });
1237
1237
  container.appendChild(noBtn);
1238
1238
  }
1239
- // ── W4: Document Confirmation (document verification) ────────────
1239
+ // ── W4: Document Confirmation (document verification) ────────────
1240
1240
  renderDocumentConfirm(container) {
1241
1241
  container.style.cssText += "display:flex;flex-direction:column;align-items:center;text-align:center;gap:14px;padding:8px 0;";
1242
1242
  // W14: Check retry limit
@@ -1248,7 +1248,7 @@ class SmartComplyFlow {
1248
1248
  this._prewarmCamera();
1249
1249
  const iconWrap = document.createElement("div");
1250
1250
  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;`;
1251
- iconWrap.textContent = "📄";
1251
+ iconWrap.textContent = "📄";
1252
1252
  container.appendChild(iconWrap);
1253
1253
  const title = document.createElement("h2");
1254
1254
  title.style.cssText = `color:${this.theme.text};font-size:18px;font-weight:700;margin:0;`;
@@ -1273,10 +1273,10 @@ class SmartComplyFlow {
1273
1273
  if (this._ocrExpirySoon && !this._ocrIsExpired) {
1274
1274
  const warn = document.createElement("div");
1275
1275
  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;`;
1276
- warn.textContent = "⚠️ Your document expires soon. Some services may not accept it, but you can still continue.";
1276
+ warn.textContent = "⚠️ Your document expires soon. Some services may not accept it, but you can still continue.";
1277
1277
  container.appendChild(warn);
1278
1278
  }
1279
- // Details card — show whatever OCR returned, never block on missing fields
1279
+ // Details card — show whatever OCR returned, never block on missing fields
1280
1280
  const card = document.createElement("div");
1281
1281
  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;`;
1282
1282
  const docRows = [];
@@ -1334,7 +1334,7 @@ class SmartComplyFlow {
1334
1334
  });
1335
1335
  container.appendChild(noBtn);
1336
1336
  }
1337
- // ── W6+W7+W8: OCR Gate (document verification) ───────────────────
1337
+ // ── W6+W7+W8: OCR Gate (document verification) ───────────────────
1338
1338
  renderOcrGate(container) {
1339
1339
  container.style.cssText += "display:flex;flex-direction:column;align-items:center;gap:14px;padding:20px 0;text-align:center;";
1340
1340
  const spinnerWrap = document.createElement("div");
@@ -1343,18 +1343,18 @@ class SmartComplyFlow {
1343
1343
  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;`;
1344
1344
  const centre = document.createElement("div");
1345
1345
  centre.style.cssText = "position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:20px;";
1346
- centre.textContent = "📄";
1346
+ centre.textContent = "📄";
1347
1347
  spinnerWrap.appendChild(ring);
1348
1348
  spinnerWrap.appendChild(centre);
1349
1349
  container.appendChild(spinnerWrap);
1350
1350
  const statusEl = document.createElement("div");
1351
1351
  statusEl.setAttribute("aria-live", "polite");
1352
1352
  statusEl.style.cssText = `color:${this.theme.text};font-size:15px;font-weight:600;`;
1353
- statusEl.textContent = "Reading your document…";
1353
+ statusEl.textContent = "Reading your document…";
1354
1354
  container.appendChild(statusEl);
1355
1355
  const subEl = document.createElement("div");
1356
1356
  subEl.style.cssText = `color:${this.theme.textSecondary};font-size:12px;max-width:260px;line-height:1.5;`;
1357
- subEl.textContent = "This usually takes 5–15 seconds. Please wait.";
1357
+ subEl.textContent = "This usually takes 5–15 seconds. Please wait.";
1358
1358
  container.appendChild(subEl);
1359
1359
  if (!this.livenessEntryId) {
1360
1360
  this.showErrorStep("Document upload failed. Please try again.");
@@ -1369,19 +1369,19 @@ class SmartComplyFlow {
1369
1369
  polls++;
1370
1370
  try {
1371
1371
  const result = await this.sdk.liveness.pollResult(entryId);
1372
- // W7: Expired document — stop, do not proceed to liveness
1372
+ // W7: Expired document — stop, do not proceed to liveness
1373
1373
  if (result.is_expired === true) {
1374
1374
  container.innerHTML = "";
1375
1375
  this._renderExpiredDocument(container);
1376
1376
  return;
1377
1377
  }
1378
- // W8: OCR quality fail — offer retake
1378
+ // W8: OCR quality fail — offer retake
1379
1379
  if (result.ocr_quality_failed === true) {
1380
1380
  container.innerHTML = "";
1381
1381
  this._renderOcrQualityFail(container, result.ocr_rejection_reason);
1382
1382
  return;
1383
1383
  }
1384
- // OCR complete — store data and show confirmation screen
1384
+ // OCR complete — store data and show confirmation screen
1385
1385
  const ocrDone = result.ocr_status === "done" || result.ocr_status === "ocr_complete"
1386
1386
  || result.ocr_name || result.ocr_document_number || result.ocr_expiry_date;
1387
1387
  if (ocrDone) {
@@ -1390,7 +1390,7 @@ class SmartComplyFlow {
1390
1390
  this._ocrExpiry = result.ocr_expiry_date || "";
1391
1391
  this._ocrFaceImageUrl = result.ocr_face_image_url || "";
1392
1392
  this._ocrIsExpired = result.is_expired === true;
1393
- // B8: Normalise expiry date string before parsing — OCR may return
1393
+ // B8: Normalise expiry date string before parsing — OCR may return
1394
1394
  // DD/MM/YYYY, DD-MM-YYYY, YYYY-MM-DD, or "12 May 2029" etc.
1395
1395
  // new Date() alone is locale-dependent on non-ISO strings.
1396
1396
  if (this._ocrExpiry) {
@@ -1404,14 +1404,14 @@ class SmartComplyFlow {
1404
1404
  this.showStep("document_confirm");
1405
1405
  return;
1406
1406
  }
1407
- // Still pending — keep polling
1407
+ // Still pending — keep polling
1408
1408
  if (polls < MAX_POLLS && !this.isDestroyed) {
1409
1409
  this._ocrPollTimer = setTimeout(poll, 2000);
1410
1410
  }
1411
1411
  else if (!this.isDestroyed) {
1412
- // Timeout — proceed anyway with whatever we have
1413
- statusEl.textContent = "Taking longer than usual…";
1414
- subEl.textContent = "We'll continue — you can confirm your details on the next screen.";
1412
+ // Timeout — proceed anyway with whatever we have
1413
+ statusEl.textContent = "Taking longer than usual…";
1414
+ subEl.textContent = "We'll continue — you can confirm your details on the next screen.";
1415
1415
  this._ocrPollTimer = setTimeout(() => {
1416
1416
  if (!this.isDestroyed)
1417
1417
  this.showStep("document_confirm");
@@ -1427,14 +1427,14 @@ class SmartComplyFlow {
1427
1427
  }
1428
1428
  }
1429
1429
  };
1430
- // First poll after 3s — give OCR submission time to start
1430
+ // First poll after 3s — give OCR submission time to start
1431
1431
  this._ocrPollTimer = setTimeout(poll, 3000);
1432
1432
  }
1433
1433
  _renderExpiredDocument(container) {
1434
1434
  container.style.cssText += "display:flex;flex-direction:column;align-items:center;text-align:center;gap:14px;padding:20px 0;";
1435
1435
  const icon = document.createElement("div");
1436
1436
  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;`;
1437
- icon.textContent = "📅";
1437
+ icon.textContent = "📅";
1438
1438
  container.appendChild(icon);
1439
1439
  const t = document.createElement("h2");
1440
1440
  t.style.cssText = `color:${this.theme.text};font-size:18px;font-weight:700;margin:0;`;
@@ -1460,7 +1460,7 @@ class SmartComplyFlow {
1460
1460
  container.style.cssText += "display:flex;flex-direction:column;align-items:center;text-align:center;gap:14px;padding:20px 0;";
1461
1461
  const icon = document.createElement("div");
1462
1462
  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;`;
1463
- icon.textContent = "🔍";
1463
+ icon.textContent = "🔍";
1464
1464
  container.appendChild(icon);
1465
1465
  const t = document.createElement("h2");
1466
1466
  t.style.cssText = `color:${this.theme.text};font-size:18px;font-weight:700;margin:0;`;
@@ -1484,14 +1484,14 @@ class SmartComplyFlow {
1484
1484
  });
1485
1485
  container.appendChild(retryBtn);
1486
1486
  }
1487
- // ── W11: You're all done (end-user final screen) ─────────────────
1487
+ // ── W11: You're all done (end-user final screen) ─────────────────
1488
1488
  renderDone(container) {
1489
1489
  container.style.cssText += "display:flex;flex-direction:column;align-items:center;text-align:center;gap:16px;padding:24px 0;";
1490
1490
  container.setAttribute("role", "status");
1491
- // B7: Fire onComplete immediately when the done screen renders — the end
1491
+ // B7: Fire onComplete immediately when the done screen renders — the end
1492
1492
  // user has completed their part regardless of whether they click Close.
1493
1493
  // Firing here means the integrator always gets the event even if the user
1494
- // closes the modal via the ✕ button instead of the Close button.
1494
+ // closes the modal via the ✕ button instead of the Close button.
1495
1495
  this.options.onComplete?.({
1496
1496
  entryId: this.livenessEntryId || 0,
1497
1497
  sessionId: this.sdk.sessionToken,
@@ -1501,7 +1501,7 @@ class SmartComplyFlow {
1501
1501
  });
1502
1502
  const iconWrap = document.createElement("div");
1503
1503
  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;`;
1504
- iconWrap.textContent = "✓";
1504
+ iconWrap.textContent = "✓";
1505
1505
  container.appendChild(iconWrap);
1506
1506
  const title = document.createElement("h2");
1507
1507
  title.style.cssText = `color:${this.theme.text};font-size:22px;font-weight:700;margin:0;`;
@@ -1514,21 +1514,21 @@ class SmartComplyFlow {
1514
1514
  // Subtle info line
1515
1515
  const info = document.createElement("div");
1516
1516
  info.style.cssText = `color:${this.theme.textMuted};font-size:11px;display:flex;align-items:center;gap:4px;`;
1517
- info.textContent = "🔒 Your data is encrypted and secure";
1517
+ info.textContent = "🔒 Your data is encrypted and secure";
1518
1518
  container.appendChild(info);
1519
1519
  const doneBtn = this.createPrimaryButton("Close");
1520
1520
  doneBtn.addEventListener("click", () => {
1521
- // onComplete already fired on render — just close the modal
1521
+ // onComplete already fired on render — just close the modal
1522
1522
  this.close();
1523
1523
  });
1524
1524
  container.appendChild(doneBtn);
1525
1525
  }
1526
- // ── W14: Retry limit reached ─────────────────────────────────────
1526
+ // ── W14: Retry limit reached ─────────────────────────────────────
1527
1527
  _renderRetryLimitReached(container) {
1528
1528
  container.style.cssText += "display:flex;flex-direction:column;align-items:center;text-align:center;gap:14px;padding:24px 0;";
1529
1529
  const icon = document.createElement("div");
1530
1530
  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;`;
1531
- icon.textContent = "⚠️";
1531
+ icon.textContent = "⚠️";
1532
1532
  container.appendChild(icon);
1533
1533
  const t = document.createElement("h2");
1534
1534
  t.style.cssText = `color:${this.theme.text};font-size:18px;font-weight:700;margin:0;`;
@@ -1542,7 +1542,7 @@ class SmartComplyFlow {
1542
1542
  closeBtn.addEventListener("click", () => { this.options.onError?.(new Error("Retry limit reached")); this.close(); });
1543
1543
  container.appendChild(closeBtn);
1544
1544
  }
1545
- // ── W13: Camera pre-warm ─────────────────────────────────────────
1545
+ // ── W13: Camera pre-warm ─────────────────────────────────────────
1546
1546
  _prewarmCamera() {
1547
1547
  if (this._prewarmedStream || this.isDestroyed)
1548
1548
  return;
@@ -1562,11 +1562,11 @@ class SmartComplyFlow {
1562
1562
  async processDocumentCaptureResult(result, captureContainer, container) {
1563
1563
  // B2+P2: Compress both images once here. Do NOT assign documentBlob twice
1564
1564
  // (old code set it to the raw blob first, then overwrote with compressed).
1565
- // processDocumentBlob also ran _compressImage — that second pass is removed
1565
+ // processDocumentBlob also ran _compressImage — that second pass is removed
1566
1566
  // below so compression only happens once.
1567
1567
  this.documentBlob = await _compressImage(result.front, 1600, 0.85);
1568
1568
  this.documentBackBlob = result.back ? await _compressImage(result.back, 1600, 0.85) : null;
1569
- // Process the front image — pass the already-compressed blob
1569
+ // Process the front image — pass the already-compressed blob
1570
1570
  await this.processDocumentBlob(this.documentBlob, captureContainer, container);
1571
1571
  }
1572
1572
  /**
@@ -1577,7 +1577,7 @@ class SmartComplyFlow {
1577
1577
  let retryCount = 0;
1578
1578
  const attemptProcessing = async () => {
1579
1579
  try {
1580
- // Validate blob — compression already done in processDocumentCaptureResult (P2)
1580
+ // Validate blob — compression already done in processDocumentCaptureResult (P2)
1581
1581
  if (!blob || blob.size === 0) {
1582
1582
  throw new Error("Failed to capture document. Please try again.");
1583
1583
  }
@@ -1590,7 +1590,7 @@ class SmartComplyFlow {
1590
1590
  <div style="display:flex;flex-direction:column;align-items:center;gap:14px;padding:28px 20px;">
1591
1591
  <div style="position:relative;width:52px;height:52px;">
1592
1592
  <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>
1593
- <div style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:20px;">🔍</div>
1593
+ <div style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:20px;">🔍</div>
1594
1594
  </div>
1595
1595
  <div style="color:${this.theme.text};font-size:14px;font-weight:600;">Scanning document...</div>
1596
1596
  <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>
@@ -1598,7 +1598,7 @@ class SmartComplyFlow {
1598
1598
  // Create liveness entry with document.
1599
1599
  // Use the first collected field value as identifier when available
1600
1600
  // (document flow has no data-verification step, so collectedFields is
1601
- // empty — fall back to the selected ID type code so the webhook
1601
+ // empty — fall back to the selected ID type code so the webhook
1602
1602
  // subject.identifier is at least human-meaningful, not a raw timestamp).
1603
1603
  const identifier = Object.values(this.collectedFields)[0]
1604
1604
  || this.idNumber
@@ -1647,7 +1647,7 @@ class SmartComplyFlow {
1647
1647
  }
1648
1648
  }
1649
1649
  this.livenessEntryId = createResult.id;
1650
- // W6: Route to OCR gate — poll until OCR result is ready, validate
1650
+ // W6: Route to OCR gate — poll until OCR result is ready, validate
1651
1651
  // expiry/quality, then show document_confirm before liveness starts.
1652
1652
  this.showStep("ocr_gate");
1653
1653
  }
@@ -1662,7 +1662,7 @@ class SmartComplyFlow {
1662
1662
  <div style="display:flex;flex-direction:column;align-items:center;gap:16px;padding:28px 20px;text-align:center;">
1663
1663
  <div style="position:relative;width:52px;height:52px;">
1664
1664
  <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>
1665
- <div style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:20px;">🔄</div>
1665
+ <div style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:20px;">🔄</div>
1666
1666
  </div>
1667
1667
  <div style="color:${this.theme.text};font-size:14px;font-weight:600;">Retrying...</div>
1668
1668
  <div style="color:${this.theme.textSecondary};font-size:12px;max-width:240px;line-height:1.5;">Attempt ${retryCount} of ${maxRetries}</div>
@@ -1681,7 +1681,7 @@ class SmartComplyFlow {
1681
1681
  await attemptProcessing();
1682
1682
  }
1683
1683
  handleDocumentProcessingError(error, captureContainer) {
1684
- // Map technical errors to user-friendly messages — never pass raw error strings
1684
+ // Map technical errors to user-friendly messages — never pass raw error strings
1685
1685
  let userMessage = "We could not verify your document. Please try again.";
1686
1686
  let canRetry = true;
1687
1687
  const raw = (error?.message || "").toLowerCase();
@@ -1708,13 +1708,13 @@ class SmartComplyFlow {
1708
1708
  userMessage = "The document does not match the selected ID type. Please choose the correct ID type.";
1709
1709
  }
1710
1710
  if (captureContainer) {
1711
- // Build with DOM — never interpolate user-derived content into innerHTML
1711
+ // Build with DOM — never interpolate user-derived content into innerHTML
1712
1712
  const wrap = document.createElement("div");
1713
1713
  wrap.style.cssText = "display:flex;flex-direction:column;align-items:center;gap:16px;padding:28px 20px;text-align:center;";
1714
1714
  wrap.setAttribute("role", "alert");
1715
1715
  const icon = document.createElement("div");
1716
1716
  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;`;
1717
- icon.textContent = "🔍";
1717
+ icon.textContent = "🔍";
1718
1718
  wrap.appendChild(icon);
1719
1719
  const titleEl = document.createElement("div");
1720
1720
  titleEl.style.cssText = `color:${this.theme.text};font-size:14px;font-weight:700;`;
@@ -1739,7 +1739,7 @@ class SmartComplyFlow {
1739
1739
  this.showErrorStep(userMessage);
1740
1740
  }
1741
1741
  }
1742
- // ── Liveness ────────────────────────────────────────────────────
1742
+ // ── Liveness ────────────────────────────────────────────────────
1743
1743
  renderLiveness(container) {
1744
1744
  container.style.cssText += "display:flex;flex-direction:column;gap:14px;";
1745
1745
  // W13: Check retry limit before starting another liveness attempt
@@ -1758,7 +1758,7 @@ class SmartComplyFlow {
1758
1758
  livenessContainer.style.cssText = "margin:8px 0;width:100%;";
1759
1759
  container.appendChild(livenessContainer);
1760
1760
  // W9: For data verification, identity is already held by the backend via
1761
- // identity_check_id — the real ID number/type must not be re-sent, and the
1761
+ // identity_check_id — the real ID number/type must not be re-sent, and the
1762
1762
  // backend now accepts liveness/create without them when identity_check is
1763
1763
  // set (see liveness.ts create(), which omits both fields entirely in that
1764
1764
  // case). Only build a real identifier for the non-data-verification path.
@@ -1791,7 +1791,7 @@ class SmartComplyFlow {
1791
1791
  prewarmedStream: this._prewarmedStream || undefined,
1792
1792
  }, ["BLINK"], this.livenessEntryId || undefined)
1793
1793
  .then((result) => {
1794
- // W11: After submit, end user is done — show "You're all done" screen.
1794
+ // W11: After submit, end user is done — show "You're all done" screen.
1795
1795
  // Backend processing continues in background; client receives webhook.
1796
1796
  // Track liveness failures for retry limit (W14).
1797
1797
  if (result.status === "failed") {
@@ -1808,9 +1808,9 @@ class SmartComplyFlow {
1808
1808
  });
1809
1809
  }
1810
1810
  renderResult(_container) {
1811
- // C2: Dead code — flow routes to 'done' step, not 'result'. Kept as stub.
1811
+ // C2: Dead code — flow routes to 'done' step, not 'result'. Kept as stub.
1812
1812
  }
1813
- // ── Error ───────────────────────────────────────────────────────
1813
+ // ── Error ───────────────────────────────────────────────────────
1814
1814
  showErrorStep(message) {
1815
1815
  this.currentStep = "error";
1816
1816
  if (!this.contentArea)
@@ -1829,13 +1829,13 @@ class SmartComplyFlow {
1829
1829
  const isDocQuality = /blurry|blur|quality|glare|dark|bright|lighting/i.test(message);
1830
1830
  const isExpiredDoc = /document.*expired|expired.*document/i.test(message);
1831
1831
  const isFaceMatch = /face.*match|selfie.*match|does not match/i.test(message);
1832
- const errorIcon = isCameraErr ? "📷"
1833
- : isNetworkErr ? "📡"
1834
- : isAuthErr ? "⏱"
1835
- : isDocQuality ? "🔍"
1836
- : isExpiredDoc ? "📅"
1837
- : isFaceMatch ? "🤳"
1838
- : "✕";
1832
+ const errorIcon = isCameraErr ? "📷"
1833
+ : isNetworkErr ? "📡"
1834
+ : isAuthErr ? "⏱"
1835
+ : isDocQuality ? "🔍"
1836
+ : isExpiredDoc ? "📅"
1837
+ : isFaceMatch ? "🤳"
1838
+ : "✕";
1839
1839
  const errorTitle = isCameraErr ? "Camera Access Required"
1840
1840
  : isNetworkErr ? "Connection Problem"
1841
1841
  : isAuthErr ? "Session Expired"
@@ -1843,7 +1843,7 @@ class SmartComplyFlow {
1843
1843
  : isExpiredDoc ? "Document Expired"
1844
1844
  : isFaceMatch ? "Face Match Failed"
1845
1845
  : "Verification Unsuccessful";
1846
- // Map to user-facing guidance — never show raw internal message
1846
+ // Map to user-facing guidance — never show raw internal message
1847
1847
  const userMessage = isCameraErr
1848
1848
  ? "Please allow camera access in your browser settings and try again."
1849
1849
  : isNetworkErr
@@ -1890,9 +1890,9 @@ class SmartComplyFlow {
1890
1890
  wrapper.appendChild(closeBtn);
1891
1891
  this.contentArea.appendChild(wrapper);
1892
1892
  }
1893
- // ─────────────────────────────────────────────────────────────────
1893
+ // ─────────────────────────────────────────────────────────────────
1894
1894
  // UI Helpers
1895
- // ─────────────────────────────────────────────────────────────────
1895
+ // ─────────────────────────────────────────────────────────────────
1896
1896
  createHeader() {
1897
1897
  const header = document.createElement("div");
1898
1898
  header.setAttribute("role", "banner");
@@ -1962,7 +1962,7 @@ class SmartComplyFlow {
1962
1962
  header.appendChild(stepIndicator);
1963
1963
  // Close button
1964
1964
  const closeBtn = document.createElement("button");
1965
- closeBtn.innerHTML = "✕";
1965
+ closeBtn.innerHTML = "✕";
1966
1966
  closeBtn.setAttribute("aria-label", "Close identity verification");
1967
1967
  closeBtn.style.cssText = `
1968
1968
  width:32px;height:32px;border-radius:8px;flex-shrink:0;
@@ -2043,7 +2043,7 @@ class SmartComplyFlow {
2043
2043
  }
2044
2044
  const arrow = document.createElement("span");
2045
2045
  arrow.style.cssText = `color:${this.theme.textMuted};font-size:18px;flex-shrink:0;`;
2046
- arrow.textContent = "→";
2046
+ arrow.textContent = "→";
2047
2047
  card.appendChild(textWrap);
2048
2048
  card.appendChild(arrow);
2049
2049
  return card;
@@ -2088,7 +2088,7 @@ class SmartComplyFlow {
2088
2088
  return btn;
2089
2089
  }
2090
2090
  isDocumentFlow() {
2091
- // 1. Check verification_type — handle both string and array from backend
2091
+ // 1. Check verification_type — handle both string and array from backend
2092
2092
  const vType = this.sdkConfig?.verification_type;
2093
2093
  if (vType) {
2094
2094
  const types = Array.isArray(vType) ? vType : [vType];
@@ -2097,7 +2097,7 @@ class SmartComplyFlow {
2097
2097
  if (types.includes("data_verification"))
2098
2098
  return false;
2099
2099
  }
2100
- // 2. Fallback: inspect channel fields — any upload field = document flow
2100
+ // 2. Fallback: inspect channel fields — any upload field = document flow
2101
2101
  const channels = this.sdkConfig?.channels || {};
2102
2102
  const allFields = Object.values(channels)
2103
2103
  .flat()
@@ -2108,7 +2108,7 @@ class SmartComplyFlow {
2108
2108
  const isDocFlow = this.isDocumentFlow();
2109
2109
  const countries = Object.keys(this.sdkConfig?.channels || {});
2110
2110
  const singleCountry = countries.length <= 1;
2111
- // Two-stage action steps — Stage 1: validate identity, Stage 2: liveness
2111
+ // Two-stage action steps — Stage 1: validate identity, Stage 2: liveness
2112
2112
  const stage1Steps = isDocFlow
2113
2113
  ? ["country", "id_type", "document_capture", "ocr_gate", "document_confirm"].filter(s => !(s === "country" && singleCountry))
2114
2114
  : ["country", "id_type", "id_input", "confirm_identity"].filter(s => !(s === "country" && singleCountry));
@@ -2128,7 +2128,7 @@ class SmartComplyFlow {
2128
2128
  this.stepLabel.textContent = "";
2129
2129
  }
2130
2130
  else if (this.currentStep === "done" || this.currentStep === "result") {
2131
- this.stepLabel.textContent = "Complete ✓";
2131
+ this.stepLabel.textContent = "Complete ✓";
2132
2132
  }
2133
2133
  else if (stage2Steps.includes(this.currentStep)) {
2134
2134
  this.stepLabel.textContent = "Stage 2 of 2: Face Check";
@@ -2140,7 +2140,7 @@ class SmartComplyFlow {
2140
2140
  this.stepLabel.textContent = "";
2141
2141
  }
2142
2142
  }
2143
- // W3: Stage dots — dot 1 lights once Stage 1 is reached, dot 2 once Stage 2 is reached
2143
+ // W3: Stage dots — dot 1 lights once Stage 1 is reached, dot 2 once Stage 2 is reached
2144
2144
  if (this.stageDots) {
2145
2145
  const dots = Array.from(this.stageDots.children);
2146
2146
  const inStage1 = stage1Steps.includes(this.currentStep);
@@ -2153,7 +2153,7 @@ class SmartComplyFlow {
2153
2153
  if (dots[1])
2154
2154
  dots[1].style.background = stage2Active ? this.theme.primary : this.theme.border;
2155
2155
  }
2156
- // Back button — show for navigable steps only
2156
+ // Back button — show for navigable steps only
2157
2157
  if (this.headerBackBtn) {
2158
2158
  const backTargets = {
2159
2159
  id_type: () => this.showStep(countries.length > 1 ? "country" : "welcome"),