smartcomply-web-sdk 1.0.43 → 1.0.45

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.
@@ -5125,14 +5125,20 @@ var SmartComplySDK = (() => {
5125
5125
  * 1. Take autoshot selfie from camera
5126
5126
  * 2. Create liveness entry on backend (with challenge actions + selfie + optional doc)
5127
5127
  * 3. Run action detection loop (camera + MediaPipe)
5128
- * 4. Record video and submit to backend
5129
- * 5. Return submit response (status: "processing")
5128
+ * 4. Record video (document/liveness flows) OR skip recording (data verification)
5129
+ * 5. Submit to backend and return response (status: "processing")
5130
5130
  *
5131
- * @param container - DOM element to mount the liveness UI into
5132
- * @param params - identifier info and optional document image
5133
- * @param actions - override challenge actions (default: BLINK, TURN_HEAD, OPEN_MOUTH)
5131
+ * For data_verification the backend uses COMPARISON mode (autoshot vs govt DB photo)
5132
+ * so video recording is unnecessary — we skip it to save bandwidth and latency.
5133
+ *
5134
+ * @param container - DOM element to mount the liveness UI into
5135
+ * @param params - identifier info and optional document image
5136
+ * @param actions - override challenge actions (default: BLINK, TURN_HEAD, OPEN_MOUTH)
5137
+ * @param existingEntryId - reuse an already-created LivenessEntry (avoids double charge)
5134
5138
  */
5135
5139
  async startCheck(container, params, actions, existingEntryId) {
5140
+ const isDataVerification = params.verification_type === "data_verification" || !params.document && !params.document_front && !params.id_file && !!params.identity_check;
5141
+ const skipVideo = isDataVerification;
5136
5142
  const camera = new CameraManager();
5137
5143
  const recorder = new VideoRecorder();
5138
5144
  const faceEngine = new FaceDetectorEngine();
@@ -5255,7 +5261,9 @@ var SmartComplySDK = (() => {
5255
5261
  videoEl.srcObject = stream;
5256
5262
  await videoEl.play();
5257
5263
  await new Promise((r2) => setTimeout(r2, 1e3));
5258
- recorder.start(stream);
5264
+ if (!skipVideo) {
5265
+ recorder.start(stream);
5266
+ }
5259
5267
  ui2.updateInstruction(uiChallenge.instruction);
5260
5268
  const actionDetector = new ActionDetector(entry.challenge_actions);
5261
5269
  let lastAction = null;
@@ -5329,14 +5337,30 @@ var SmartComplySDK = (() => {
5329
5337
  faceEngine.destroy();
5330
5338
  camera.stop();
5331
5339
  ui2.unmount();
5332
- this.startCheck(container, { ...params, id_file: capturedDocBlob }, actions, entry.id).then(resolveRetry).catch(rejectRetry);
5340
+ this.startCheck(
5341
+ container,
5342
+ { ...params, id_file: capturedDocBlob },
5343
+ actions,
5344
+ entry.id
5345
+ ).then(resolveRetry).catch(rejectRetry);
5333
5346
  });
5334
5347
  });
5335
5348
  }
5336
5349
  ui2.updateInstruction("Almost done \u2014 hold still...");
5337
5350
  await new Promise((r2) => setTimeout(r2, 800));
5338
5351
  const snapshotBlob = await this.captureFrame(videoEl);
5339
- const { blob: videoBlob, durationSeconds } = await recorder.stop();
5352
+ let videoBlob;
5353
+ let durationSeconds;
5354
+ if (skipVideo) {
5355
+ recorder.stop().catch(() => {
5356
+ });
5357
+ videoBlob = autoshotBlob;
5358
+ durationSeconds = void 0;
5359
+ } else {
5360
+ const recorded = await recorder.stop();
5361
+ videoBlob = recorded.blob;
5362
+ durationSeconds = recorded.durationSeconds;
5363
+ }
5340
5364
  const submitResult = await this.submit(
5341
5365
  entry.id,
5342
5366
  videoBlob,
@@ -6629,6 +6653,62 @@ var SmartComplySDK = (() => {
6629
6653
  requiresBack: false
6630
6654
  };
6631
6655
  }
6656
+ function _sanitizeUserMessage(raw) {
6657
+ if (!raw) return null;
6658
+ const lower = raw.toLowerCase();
6659
+ const blocklist = [
6660
+ "smartcomply",
6661
+ "face comparison service",
6662
+ "session id",
6663
+ "session_id",
6664
+ "provider",
6665
+ "ocr",
6666
+ "textract",
6667
+ "rekognition",
6668
+ "deepface",
6669
+ "ivs",
6670
+ "celery",
6671
+ "exception",
6672
+ "traceback",
6673
+ "str(e)",
6674
+ "error:",
6675
+ "failed:",
6676
+ "django",
6677
+ "python",
6678
+ "http ",
6679
+ "comparison mode",
6680
+ "liveness mode",
6681
+ "callback",
6682
+ "face_comparison",
6683
+ "face_match",
6684
+ "confidence_percentage",
6685
+ "threshold_percentage",
6686
+ "polling_fallback",
6687
+ "resolved_by"
6688
+ ];
6689
+ for (const blocked of blocklist) {
6690
+ if (lower.includes(blocked)) return null;
6691
+ }
6692
+ if (/[a-z]+_[a-z]+/.test(raw) && !/\s/.test(raw.trim())) return null;
6693
+ if (raw.trim()[0] === raw.trim()[0].toLowerCase() && /^[a-z]/.test(raw.trim())) return null;
6694
+ return raw.trim();
6695
+ }
6696
+ function _formatDocType(code) {
6697
+ const map = {
6698
+ passport: "Passport",
6699
+ national_id: "National ID",
6700
+ nin_slip: "NIN Slip",
6701
+ nin_with_face: "National Identity Number",
6702
+ drivers_license: "Driver's Licence",
6703
+ voters_card: "Voter's Card",
6704
+ kenya_id: "Kenya National ID",
6705
+ ghana_id: "Ghana Card"
6706
+ };
6707
+ if (!code) return "";
6708
+ const key = code.toLowerCase().replace(/[\s-]/g, "_");
6709
+ if (map[key]) return map[key];
6710
+ return code.replace(/_/g, " ").replace(/\b\w/g, (c2) => c2.toUpperCase());
6711
+ }
6632
6712
  var style = document.createElement("style");
6633
6713
  style.textContent = `
6634
6714
  @keyframes sc-spin {
@@ -6677,6 +6757,7 @@ var SmartComplySDK = (() => {
6677
6757
  this._pollStatusTimer = null;
6678
6758
  // Components
6679
6759
  this.docCapture = null;
6760
+ this._livenessSubmitResult = null;
6680
6761
  this.options = options;
6681
6762
  this.sdk = new SmartComply({
6682
6763
  apiKey: options.apiKey,
@@ -6749,6 +6830,9 @@ var SmartComplySDK = (() => {
6749
6830
  `;
6750
6831
  this.modal = document.createElement("div");
6751
6832
  this.modal.id = "sc-flow-modal";
6833
+ this.modal.setAttribute("role", "dialog");
6834
+ this.modal.setAttribute("aria-modal", "true");
6835
+ this.modal.setAttribute("aria-label", "Identity verification");
6752
6836
  this.modal.style.cssText = `
6753
6837
  width:100%;max-width:580px;max-height:95vh;
6754
6838
  background:${this.theme.bg};
@@ -6771,6 +6855,9 @@ var SmartComplySDK = (() => {
6771
6855
  this.modal.appendChild(progressWrap);
6772
6856
  this.contentArea = document.createElement("div");
6773
6857
  this.contentArea.id = "sc-flow-content";
6858
+ this.contentArea.setAttribute("role", "main");
6859
+ this.contentArea.setAttribute("aria-live", "polite");
6860
+ this.contentArea.setAttribute("aria-atomic", "false");
6774
6861
  this.contentArea.style.cssText = `
6775
6862
  flex:1;overflow-y:auto;padding:16px 20px;
6776
6863
  -webkit-overflow-scrolling:touch;
@@ -6855,7 +6942,7 @@ var SmartComplySDK = (() => {
6855
6942
  this.renderDocumentCapture(wrapper);
6856
6943
  break;
6857
6944
  case "processing":
6858
- this.renderProcessing(wrapper);
6945
+ this.renderProcessing(wrapper, this.livenessEntryId || void 0);
6859
6946
  break;
6860
6947
  case "liveness":
6861
6948
  this.renderLiveness(wrapper);
@@ -7116,12 +7203,44 @@ var SmartComplySDK = (() => {
7116
7203
  container.appendChild(autoBtn);
7117
7204
  return;
7118
7205
  }
7206
+ const _FIELD_RULES = [
7207
+ { match: "BVN", rule: { minLen: 11, maxLen: 11, numeric: true, hint: "BVN must be exactly 11 digits" } },
7208
+ { match: "NIN", rule: { minLen: 11, maxLen: 11, numeric: true, hint: "NIN must be exactly 11 digits" } },
7209
+ { match: "NATIONAL IDENTITY", rule: { minLen: 11, maxLen: 11, numeric: true, hint: "NIN must be exactly 11 digits" } },
7210
+ { match: "PASSPORT", rule: { minLen: 6, maxLen: 20, alphanumeric: true, hint: "Passport number must be 6\u201320 alphanumeric characters" } },
7211
+ { match: "DRIVER", rule: { minLen: 3, maxLen: 30, alphanumeric: true, hint: "Licence number must be 3\u201330 alphanumeric characters" } },
7212
+ { match: "VOTER", rule: { minLen: 6, maxLen: 30, alphanumeric: true, hint: "Voter ID must be 6\u201330 alphanumeric characters" } },
7213
+ { match: "GHANA", rule: { minLen: 8, maxLen: 20, alphanumeric: true, hint: "Ghana Card number must be 8\u201320 alphanumeric characters" } },
7214
+ { match: "SSNIT", rule: { minLen: 7, maxLen: 15, alphanumeric: true, hint: "SSNIT number must be 7\u201315 alphanumeric characters" } },
7215
+ { match: "TIN", rule: { minLen: 8, maxLen: 20, alphanumeric: true, hint: "TIN must be 8\u201320 characters" } },
7216
+ { match: "KRA", rule: { minLen: 10, maxLen: 12, alphanumeric: true, hint: "KRA PIN must be 10\u201312 characters" } }
7217
+ ];
7218
+ function getFieldRule(label) {
7219
+ const upper = label.toUpperCase();
7220
+ for (const entry of _FIELD_RULES) {
7221
+ if (upper.includes(entry.match)) return entry.rule;
7222
+ }
7223
+ return null;
7224
+ }
7225
+ function validateFieldValue(label, value) {
7226
+ const rule = getFieldRule(label);
7227
+ if (!rule) return null;
7228
+ const v2 = value.trim();
7229
+ if (rule.numeric && !/^\d+$/.test(v2)) return `${label} must contain digits only`;
7230
+ if (rule.alphanumeric && !/^[A-Za-z0-9]+$/.test(v2)) return `${label} must be alphanumeric (letters and digits only)`;
7231
+ if (rule.minLen && v2.length < rule.minLen) return rule.hint;
7232
+ if (rule.maxLen && v2.length > rule.maxLen) return rule.hint;
7233
+ return null;
7234
+ }
7119
7235
  const inputEls = [];
7120
7236
  for (const field of dataFields) {
7121
7237
  const fieldKey = field.label;
7122
7238
  const fieldWrap = document.createElement("div");
7123
7239
  fieldWrap.style.cssText = "display:flex;flex-direction:column;gap:6px;";
7240
+ const labelId = `sc-field-label-${fieldKey.replace(/\s+/g, "-").toLowerCase()}`;
7124
7241
  const label = document.createElement("label");
7242
+ label.id = labelId;
7243
+ label.htmlFor = `sc-field-input-${fieldKey.replace(/\s+/g, "-").toLowerCase()}`;
7125
7244
  label.style.cssText = `color:${this.theme.textMuted};font-size:10px;font-weight:700;letter-spacing:0.6px;text-transform:uppercase;`;
7126
7245
  label.textContent = field.label;
7127
7246
  fieldWrap.appendChild(label);
@@ -7194,10 +7313,17 @@ var SmartComplySDK = (() => {
7194
7313
  fieldWrap.appendChild(fileInput);
7195
7314
  inputEls.push({ field, el: fileInput });
7196
7315
  } else {
7316
+ const inputId = `sc-field-input-${fieldKey.replace(/\s+/g, "-").toLowerCase()}`;
7317
+ const hintId = `sc-field-hint-${fieldKey.replace(/\s+/g, "-").toLowerCase()}`;
7318
+ const rule = getFieldRule(field.label);
7197
7319
  const input = document.createElement("input");
7198
7320
  input.type = "text";
7321
+ input.id = inputId;
7322
+ input.setAttribute("aria-labelledby", labelId);
7323
+ input.setAttribute("aria-describedby", hintId);
7199
7324
  input.placeholder = `Enter ${field.label}`;
7200
- input.maxLength = 50;
7325
+ input.maxLength = rule?.maxLen ? rule.maxLen + 2 : 50;
7326
+ if (rule?.numeric) input.inputMode = "numeric";
7201
7327
  input.style.cssText = `
7202
7328
  width:100%;padding:14px 16px;border-radius:12px;
7203
7329
  font-size:16px;font-weight:500;
@@ -7208,17 +7334,46 @@ var SmartComplySDK = (() => {
7208
7334
  box-sizing:border-box;
7209
7335
  letter-spacing:0.5px;
7210
7336
  `;
7337
+ const hintEl = document.createElement("div");
7338
+ hintEl.id = hintId;
7339
+ hintEl.setAttribute("aria-live", "polite");
7340
+ hintEl.style.cssText = `font-size:11px;min-height:14px;margin-top:-2px;`;
7341
+ if (rule) {
7342
+ hintEl.style.color = this.theme.textMuted;
7343
+ hintEl.textContent = rule.hint;
7344
+ }
7211
7345
  input.addEventListener("focus", () => {
7212
7346
  input.style.borderColor = this.theme.primary;
7213
7347
  });
7214
7348
  input.addEventListener("blur", () => {
7215
- input.style.borderColor = this.theme.border;
7349
+ const v2 = input.value.trim();
7350
+ if (v2) {
7351
+ const err = validateFieldValue(field.label, v2);
7352
+ if (err) {
7353
+ input.style.borderColor = this.theme.error;
7354
+ hintEl.style.color = this.theme.error;
7355
+ hintEl.textContent = err;
7356
+ input.setAttribute("aria-invalid", "true");
7357
+ } else {
7358
+ input.style.borderColor = this.theme.success || this.theme.primary;
7359
+ hintEl.style.color = this.theme.textMuted;
7360
+ hintEl.textContent = rule?.hint || "";
7361
+ input.setAttribute("aria-invalid", "false");
7362
+ }
7363
+ } else {
7364
+ input.style.borderColor = this.theme.border;
7365
+ }
7216
7366
  });
7217
7367
  input.addEventListener("input", () => {
7368
+ input.style.borderColor = this.theme.primary;
7369
+ hintEl.style.color = this.theme.textMuted;
7370
+ hintEl.textContent = rule?.hint || "";
7371
+ input.removeAttribute("aria-invalid");
7218
7372
  this.collectedFields[fieldKey] = input.value.trim();
7219
7373
  updateSubmitState();
7220
7374
  });
7221
7375
  fieldWrap.appendChild(input);
7376
+ fieldWrap.appendChild(hintEl);
7222
7377
  inputEls.push({ field, el: input });
7223
7378
  }
7224
7379
  container.appendChild(fieldWrap);
@@ -7235,8 +7390,13 @@ var SmartComplySDK = (() => {
7235
7390
  submitBtn.style.opacity = "0.5";
7236
7391
  const updateSubmitState = () => {
7237
7392
  const allFilled = inputEls.every(({ el }) => el.value.trim().length > 0);
7238
- submitBtn.disabled = !allFilled;
7239
- submitBtn.style.opacity = allFilled ? "1" : "0.5";
7393
+ const allValid = inputEls.every(({ field, el }) => {
7394
+ if (el.tagName === "SELECT") return el.value.trim().length > 0;
7395
+ return !validateFieldValue(field.label, el.value.trim());
7396
+ });
7397
+ const ok = allFilled && allValid;
7398
+ submitBtn.disabled = !ok;
7399
+ submitBtn.style.opacity = ok ? "1" : "0.5";
7240
7400
  };
7241
7401
  submitBtn.addEventListener("click", async () => {
7242
7402
  submitBtn.disabled = true;
@@ -7253,7 +7413,7 @@ var SmartComplySDK = (() => {
7253
7413
  this.identityCheckId = this.verificationResult.data?.identity_check_id || null;
7254
7414
  this.showStep("liveness");
7255
7415
  } else {
7256
- errorEl.textContent = this.verificationResult.message || "Verification failed. Please check your details.";
7416
+ errorEl.textContent = this.verificationResult.message || "We could not verify your details. Please check and try again.";
7257
7417
  errorEl.style.display = "block";
7258
7418
  submitBtn.textContent = "Verify & Continue";
7259
7419
  submitBtn.disabled = false;
@@ -7262,10 +7422,16 @@ var SmartComplySDK = (() => {
7262
7422
  } catch (err) {
7263
7423
  const fieldErrors = err.data?.errors;
7264
7424
  if (Array.isArray(fieldErrors) && fieldErrors.length > 0) {
7265
- const messages = fieldErrors.map((e2) => `${e2.field}: ${e2.message}`).join("\n");
7266
- errorEl.textContent = messages;
7425
+ const firstMsg = fieldErrors[0]?.message || fieldErrors[0]?.detail || "";
7426
+ errorEl.textContent = _sanitizeUserMessage(firstMsg) || "Please check your details and try again.";
7427
+ } else if (err.code === "VERIFICATION_FAILED") {
7428
+ errorEl.textContent = "We could not verify those details. Please check your ID number and try again.";
7429
+ } else if (err.code === "CHANNEL_NOT_CONFIGURED") {
7430
+ errorEl.textContent = "This ID type is not currently available. Please choose a different option.";
7431
+ } else if (err.code === "INSUFFICIENT_BALANCE") {
7432
+ errorEl.textContent = "Verification is temporarily unavailable. Please contact support.";
7267
7433
  } else {
7268
- errorEl.textContent = err.message || "Verification failed";
7434
+ errorEl.textContent = _sanitizeUserMessage(err.message) || "Something went wrong. Please try again.";
7269
7435
  }
7270
7436
  errorEl.style.display = "block";
7271
7437
  submitBtn.textContent = "Verify & Continue";
@@ -7315,121 +7481,112 @@ var SmartComplySDK = (() => {
7315
7481
  this.showErrorStep(err.message || "Document capture failed. Please try again.");
7316
7482
  });
7317
7483
  }
7318
- // ── Processing ────────────────────────────────────────────────────
7319
- renderProcessing(container) {
7320
- container.style.cssText += "display:flex;flex-direction:column;align-items:center;gap:14px;padding:16px;";
7321
- const animationWrap = document.createElement("div");
7322
- animationWrap.style.cssText = "position:relative;width:52px;height:52px;flex-shrink:0;";
7323
- const spinner = document.createElement("div");
7324
- spinner.style.cssText = `
7325
- position:absolute;inset:0;border-radius:50%;
7326
- border:3px solid ${this.theme.border};
7327
- border-top-color:${this.theme.primary};
7328
- animation:sc-spin 0.9s linear infinite;
7329
- `;
7330
- animationWrap.appendChild(spinner);
7484
+ // ── Processing (real polling-driven, document flow only) ────────────
7485
+ renderProcessing(container, entryId) {
7486
+ container.style.cssText += "display:flex;flex-direction:column;align-items:center;gap:16px;padding:16px;";
7487
+ const spinnerWrap = document.createElement("div");
7488
+ spinnerWrap.style.cssText = "position:relative;width:56px;height:56px;flex-shrink:0;";
7489
+ const ring = document.createElement("div");
7490
+ ring.id = "sc-proc-ring";
7491
+ 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;`;
7331
7492
  const centerIcon = document.createElement("div");
7332
- centerIcon.style.cssText = `
7333
- position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
7334
- font-size:20px;
7335
- `;
7493
+ centerIcon.style.cssText = "position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:22px;";
7336
7494
  centerIcon.textContent = "\u{1F50D}";
7337
- animationWrap.appendChild(centerIcon);
7338
- container.appendChild(animationWrap);
7495
+ spinnerWrap.appendChild(ring);
7496
+ spinnerWrap.appendChild(centerIcon);
7497
+ container.appendChild(spinnerWrap);
7339
7498
  const title = document.createElement("div");
7340
7499
  title.style.cssText = `color:${this.theme.text};font-size:15px;font-weight:700;text-align:center;`;
7341
- title.textContent = "Processing your document";
7500
+ title.textContent = "Verifying your document";
7342
7501
  container.appendChild(title);
7343
- const subtitle = document.createElement("div");
7344
- subtitle.style.cssText = `color:${this.theme.textSecondary};font-size:12px;text-align:center;max-width:260px;line-height:1.5;`;
7345
- subtitle.textContent = "Scanning and verifying your ID. This usually takes a few seconds.";
7346
- container.appendChild(subtitle);
7347
- const stepsContainer = document.createElement("div");
7348
- stepsContainer.style.cssText = `
7349
- width:100%;display:grid;grid-template-columns:1fr 1fr;gap:8px;
7350
- `;
7351
- const steps = [
7352
- { id: "uploading", text: "Uploading", icon: "\u{1F4E4}" },
7353
- { id: "scanning", text: "Scanning", icon: "\u{1F50D}" },
7354
- { id: "verifying", text: "Verifying", icon: "\u2713" },
7355
- { id: "analyzing", text: "Checking quality", icon: "\u{1F52C}" }
7502
+ const statusLine = document.createElement("div");
7503
+ statusLine.id = "sc-proc-status";
7504
+ statusLine.setAttribute("aria-live", "polite");
7505
+ statusLine.setAttribute("aria-atomic", "true");
7506
+ statusLine.style.cssText = `color:${this.theme.textSecondary};font-size:12px;text-align:center;max-width:260px;line-height:1.5;`;
7507
+ statusLine.textContent = "Scanning document\u2026";
7508
+ container.appendChild(statusLine);
7509
+ const stages = [
7510
+ { id: "ocr", label: "Reading document", icon: "\u{1F4C4}" },
7511
+ { id: "expiry", label: "Checking expiry", icon: "\u{1F4C5}" },
7512
+ { id: "face", label: "Face match", icon: "\u{1F933}" },
7513
+ { id: "result", label: "Finalising result", icon: "\u2713" }
7356
7514
  ];
7357
- steps.forEach((step) => {
7358
- const stepEl = document.createElement("div");
7359
- stepEl.style.cssText = `
7360
- display:flex;align-items:center;gap:8px;padding:9px 10px;border-radius:10px;
7361
- background:${this.theme.cardBg};border:1px solid ${this.theme.border};
7362
- transition:all 0.3s ease;
7363
- `;
7364
- stepEl.id = `processing-step-${step.id}`;
7365
- const iconEl = document.createElement("div");
7366
- iconEl.style.cssText = `
7367
- width:26px;height:26px;border-radius:7px;flex-shrink:0;
7368
- display:flex;align-items:center;justify-content:center;
7369
- font-size:12px;background:${this.theme.shimmer};
7515
+ const grid = document.createElement("div");
7516
+ grid.style.cssText = "width:100%;display:grid;grid-template-columns:1fr 1fr;gap:8px;";
7517
+ stages.forEach((s2) => {
7518
+ const el = document.createElement("div");
7519
+ el.id = `sc-stage-${s2.id}`;
7520
+ el.style.cssText = `display:flex;align-items:center;gap:8px;padding:9px 10px;border-radius:10px;background:${this.theme.cardBg};border:1px solid ${this.theme.border};transition:all 0.3s ease;`;
7521
+ el.innerHTML = `
7522
+ <div style="width:26px;height:26px;border-radius:7px;flex-shrink:0;display:flex;align-items:center;justify-content:center;font-size:12px;background:${this.theme.shimmer};">${s2.icon}</div>
7523
+ <div style="flex:1;color:${this.theme.textSecondary};font-size:11px;font-weight:500;">${s2.label}</div>
7524
+ <div id="sc-stage-status-${s2.id}" style="width:14px;height:14px;border-radius:50%;border:2px solid ${this.theme.border};flex-shrink:0;"></div>
7370
7525
  `;
7371
- iconEl.textContent = step.icon;
7372
- iconEl.id = `step-icon-${step.id}`;
7373
- const textEl = document.createElement("div");
7374
- textEl.style.cssText = `flex:1;color:${this.theme.textSecondary};font-size:11px;font-weight:500;`;
7375
- textEl.textContent = step.text;
7376
- textEl.id = `step-text-${step.id}`;
7377
- const statusEl = document.createElement("div");
7378
- statusEl.style.cssText = `width:16px;height:16px;border-radius:50%;border:2px solid ${this.theme.border};position:relative;flex-shrink:0;`;
7379
- statusEl.id = `step-status-${step.id}`;
7380
- stepEl.appendChild(iconEl);
7381
- stepEl.appendChild(textEl);
7382
- stepEl.appendChild(statusEl);
7383
- stepsContainer.appendChild(stepEl);
7526
+ grid.appendChild(el);
7384
7527
  });
7385
- container.appendChild(stepsContainer);
7386
- this.startProcessingAnimation();
7387
- }
7388
- startProcessingAnimation() {
7389
- const steps = ["uploading", "scanning", "verifying", "analyzing"];
7390
- let currentStepIndex = 0;
7391
- const animateStep = (stepId) => {
7392
- const stepEl = document.getElementById(`processing-step-${stepId}`);
7393
- const iconEl = document.getElementById(`step-icon-${stepId}`);
7394
- const textEl = document.getElementById(`step-text-${stepId}`);
7395
- const statusEl = document.getElementById(`step-status-${stepId}`);
7396
- if (stepEl && iconEl && textEl && statusEl) {
7397
- stepEl.style.borderColor = this.theme.primary;
7398
- stepEl.style.background = `${this.theme.primary}10`;
7399
- iconEl.style.background = this.theme.primary;
7400
- iconEl.style.color = "white";
7401
- textEl.style.color = this.theme.text;
7402
- textEl.style.fontWeight = "600";
7403
- statusEl.innerHTML = `
7404
- <div style="
7405
- position:absolute;inset:2px;border-radius:50%;
7406
- border:2px solid ${this.theme.border};
7407
- border-top-color:${this.theme.primary};
7408
- animation:sc-spin 0.8s linear infinite;
7409
- "></div>
7410
- `;
7411
- }
7528
+ container.appendChild(grid);
7529
+ if (entryId) {
7530
+ this._pollProcessingStages(entryId, statusLine, grid, stages);
7531
+ }
7532
+ }
7533
+ _pollProcessingStages(entryId, statusLine, _grid, stages) {
7534
+ const stageTimings = [0, 4e3, 1e4, 18e3];
7535
+ const startTime = Date.now();
7536
+ const setStageActive = (stageId) => {
7537
+ const el = document.getElementById(`sc-stage-${stageId}`);
7538
+ const statusEl = document.getElementById(`sc-stage-status-${stageId}`);
7539
+ if (!el || !statusEl) return;
7540
+ el.style.borderColor = this.theme.primary;
7541
+ el.style.background = `${this.theme.primary}10`;
7542
+ statusEl.innerHTML = `<div style="position:absolute;inset:1px;border-radius:50%;border:2px solid ${this.theme.border};border-top-color:${this.theme.primary};animation:sc-spin 0.8s linear infinite;"></div>`;
7543
+ statusEl.style.position = "relative";
7544
+ };
7545
+ const setStageComplete = (stageId) => {
7546
+ const el = document.getElementById(`sc-stage-${stageId}`);
7547
+ const statusEl = document.getElementById(`sc-stage-status-${stageId}`);
7548
+ if (!el || !statusEl) return;
7549
+ el.style.borderColor = this.theme.success || "#22c55e";
7550
+ el.style.background = `${this.theme.success || "#22c55e"}10`;
7551
+ statusEl.innerHTML = `<div style="position:absolute;inset:2px;border-radius:50%;background:${this.theme.success || "#22c55e"};display:flex;align-items:center;justify-content:center;color:#fff;font-size:8px;font-weight:bold;">\u2713</div>`;
7552
+ statusEl.style.position = "relative";
7553
+ };
7554
+ stages.forEach((s2, i2) => {
7412
7555
  setTimeout(() => {
7413
- if (stepEl && iconEl && textEl && statusEl) {
7414
- stepEl.style.borderColor = this.theme.success;
7415
- stepEl.style.background = `${this.theme.success}10`;
7416
- iconEl.style.background = this.theme.success;
7417
- statusEl.innerHTML = `
7418
- <div style="
7419
- position:absolute;inset:3px;border-radius:50%;
7420
- background:${this.theme.success};
7421
- display:flex;align-items:center;justify-content:center;
7422
- color:white;font-size:10px;font-weight:bold;
7423
- ">\u2713</div>
7424
- `;
7425
- }
7426
- currentStepIndex++;
7427
- if (currentStepIndex < steps.length) {
7428
- animateStep(steps[currentStepIndex]);
7556
+ if (this.isDestroyed) return;
7557
+ setStageActive(s2.id);
7558
+ if (i2 > 0) setStageComplete(stages[i2 - 1].id);
7559
+ statusLine.textContent = s2.id === "ocr" ? "Scanning document\u2026" : s2.id === "expiry" ? "Checking expiry date\u2026" : s2.id === "face" ? "Running face match\u2026" : "Finalising result\u2026";
7560
+ }, stageTimings[i2]);
7561
+ });
7562
+ const MAX_POLLS = 30;
7563
+ let polls = 0;
7564
+ const poll = async () => {
7565
+ if (this.isDestroyed) return;
7566
+ polls++;
7567
+ try {
7568
+ const result = await this.sdk.liveness.pollResult(entryId);
7569
+ if (result.status === "passed" || result.status === "failed") {
7570
+ stages.forEach((s2) => setStageComplete(s2.id));
7571
+ statusLine.textContent = result.status === "passed" ? "Verification complete \u2713" : "Verification finished";
7572
+ setTimeout(() => {
7573
+ if (!this.isDestroyed) {
7574
+ this._livenessSubmitResult = result;
7575
+ this.showResult(result);
7576
+ }
7577
+ }, 800);
7578
+ return;
7429
7579
  }
7430
- }, 2e3);
7580
+ } catch {
7581
+ }
7582
+ if (polls < MAX_POLLS && !this.isDestroyed) {
7583
+ this._pollStatusTimer = setTimeout(poll, 3e3);
7584
+ } else if (!this.isDestroyed) {
7585
+ stages.forEach((s2) => setStageComplete(s2.id));
7586
+ statusLine.textContent = "Still processing \u2014 check back soon";
7587
+ }
7431
7588
  };
7432
- setTimeout(() => animateStep(steps[0]), 500);
7589
+ this._pollStatusTimer = setTimeout(poll, 5e3);
7433
7590
  }
7434
7591
  /**
7435
7592
  * Process document capture result with front/back support
@@ -7466,7 +7623,7 @@ var SmartComplySDK = (() => {
7466
7623
  <div style="color:${this.theme.text};font-size:14px;font-weight:600;">Scanning document...</div>
7467
7624
  <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>
7468
7625
  </div>`;
7469
- const identifier = `DOC-${Date.now()}`;
7626
+ const identifier = Object.values(this.collectedFields)[0] || this.idNumber || this.selectedIdType || `DOC-${Date.now()}`;
7470
7627
  const COUNTRY_CODES = {
7471
7628
  nigeria: "NG",
7472
7629
  ghana: "GH",
@@ -7535,47 +7692,53 @@ var SmartComplySDK = (() => {
7535
7692
  await attemptProcessing();
7536
7693
  }
7537
7694
  handleDocumentProcessingError(error, captureContainer) {
7538
- let errorMessage = "Document verification failed. Please try again.";
7695
+ let userMessage = "We could not verify your document. Please try again.";
7539
7696
  let canRetry = true;
7540
- if (error.message) {
7541
- if (error.message.includes("expired")) {
7542
- errorMessage = "Document has expired. Please use a valid document.";
7543
- canRetry = false;
7544
- } else if (error.message.includes("quality too low")) {
7545
- errorMessage = "Image quality too low. Please ensure the document is well-lit and clearly visible.";
7546
- } else if (error.message.includes("Network") || error.message.includes("fetch")) {
7547
- errorMessage = "Network connection failed. Please check your internet connection and try again.";
7548
- } else if (error.message.includes("413") || error.message.includes("too large")) {
7549
- errorMessage = "Document image is too large. Please use a smaller image or retake the photo.";
7550
- } else if (error.message.includes("415") || error.message.includes("format")) {
7551
- errorMessage = "Unsupported image format. Please use JPEG or PNG images.";
7552
- }
7697
+ const raw = (error?.message || "").toLowerCase();
7698
+ if (raw.includes("expired")) {
7699
+ userMessage = "The document you submitted has expired. Please use a valid, current ID.";
7700
+ canRetry = false;
7701
+ } else if (raw.includes("quality too low") || raw.includes("blurry") || raw.includes("blur")) {
7702
+ userMessage = "The document photo isn't clear enough. Please retake it in good lighting with no glare.";
7703
+ } else if (raw.includes("not detected") || raw.includes("document_not_detected")) {
7704
+ userMessage = "We could not detect a document in the photo. Please ensure the full ID is visible and try again.";
7705
+ } else if (raw.includes("network") || raw.includes("fetch") || raw.includes("connection")) {
7706
+ userMessage = "A connection error occurred. Please check your internet and try again.";
7707
+ } else if (raw.includes("too large") || raw.includes("413")) {
7708
+ userMessage = "The document image is too large. Please retake the photo.";
7709
+ } else if (raw.includes("format") || raw.includes("415")) {
7710
+ userMessage = "Unsupported image format. Please use JPEG or PNG.";
7711
+ } else if (raw.includes("type mismatch") || raw.includes("document_type_mismatch")) {
7712
+ userMessage = "The document does not match the selected ID type. Please choose the correct ID type.";
7553
7713
  }
7554
7714
  if (captureContainer) {
7555
- captureContainer.innerHTML = `
7556
- <div style="display:flex;flex-direction:column;align-items:center;gap:16px;padding:28px 20px;text-align:center;">
7557
- <div style="width:56px;height:56px;border-radius:50%;background:${this.theme.errorBg || "#FEE"};border:3px solid ${this.theme.error || "#DC3545"};display:flex;align-items:center;justify-content:center;font-size:26px;">\u274C</div>
7558
- <div style="color:${this.theme.text};font-size:14px;font-weight:700;">Verification Failed</div>
7559
- <div style="color:${this.theme.textSecondary};font-size:13px;max-width:240px;line-height:1.6;">${errorMessage}</div>
7560
- ${canRetry ? `
7561
- <button id="retry-btn" style="
7562
- margin-top:8px;padding:12px 24px;border-radius:8px;
7563
- background:${this.theme.primary};color:white;
7564
- border:none;font-size:14px;font-weight:600;cursor:pointer;
7565
- transition:all 0.2s ease;
7566
- ">Try Again</button>
7567
- ` : ""}
7568
- </div>`;
7715
+ const wrap = document.createElement("div");
7716
+ wrap.style.cssText = "display:flex;flex-direction:column;align-items:center;gap:16px;padding:28px 20px;text-align:center;";
7717
+ wrap.setAttribute("role", "alert");
7718
+ const icon = document.createElement("div");
7719
+ 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;`;
7720
+ icon.textContent = "\u{1F50D}";
7721
+ wrap.appendChild(icon);
7722
+ const titleEl = document.createElement("div");
7723
+ titleEl.style.cssText = `color:${this.theme.text};font-size:14px;font-weight:700;`;
7724
+ titleEl.textContent = "Document Could Not Be Verified";
7725
+ wrap.appendChild(titleEl);
7726
+ const msgEl = document.createElement("div");
7727
+ msgEl.style.cssText = `color:${this.theme.textSecondary};font-size:13px;max-width:260px;line-height:1.6;`;
7728
+ msgEl.textContent = userMessage;
7729
+ wrap.appendChild(msgEl);
7569
7730
  if (canRetry) {
7570
- const retryBtn = captureContainer.querySelector("#retry-btn");
7571
- if (retryBtn) {
7572
- retryBtn.addEventListener("click", () => {
7573
- this.showStep("document_capture");
7574
- });
7575
- }
7731
+ const retryBtn = document.createElement("button");
7732
+ retryBtn.textContent = "Try Again";
7733
+ retryBtn.setAttribute("aria-label", "Retake document photo");
7734
+ retryBtn.style.cssText = `margin-top:8px;padding:12px 24px;border-radius:8px;background:${this.theme.primary};color:#fff;border:none;font-size:14px;font-weight:600;cursor:pointer;`;
7735
+ retryBtn.addEventListener("click", () => this.showStep("document_capture"));
7736
+ wrap.appendChild(retryBtn);
7576
7737
  }
7738
+ captureContainer.innerHTML = "";
7739
+ captureContainer.appendChild(wrap);
7577
7740
  } else {
7578
- this.showErrorStep(errorMessage);
7741
+ this.showErrorStep(userMessage);
7579
7742
  }
7580
7743
  }
7581
7744
  // ── Liveness ────────────────────────────────────────────────────
@@ -7590,7 +7753,7 @@ var SmartComplySDK = (() => {
7590
7753
  const livenessContainer = document.createElement("div");
7591
7754
  livenessContainer.style.cssText = "margin:8px 0;width:100%;";
7592
7755
  container.appendChild(livenessContainer);
7593
- const identifier = this.idNumber || `DOC-${Date.now()}`;
7756
+ const identifier = this.idNumber || Object.values(this.collectedFields)[0] || this.selectedIdType || `DOC-${Date.now()}`;
7594
7757
  const COUNTRY_CODES = {
7595
7758
  nigeria: "NG",
7596
7759
  ghana: "GH",
@@ -7609,12 +7772,20 @@ var SmartComplySDK = (() => {
7609
7772
  country,
7610
7773
  document: this.documentBlob || void 0,
7611
7774
  document_back: this.documentBackBlob || void 0,
7612
- identity_check: this.identityCheckId || void 0
7775
+ identity_check: this.identityCheckId || void 0,
7776
+ // Tell the module which flow we're in so it can skip video for data verification
7777
+ verification_type: this.identityCheckId ? "data_verification" : this.documentBlob ? "document_verification" : "liveness_only"
7613
7778
  },
7614
7779
  ["BLINK", "TURN_HEAD", "OPEN_MOUTH"],
7615
7780
  this.livenessEntryId || void 0
7616
7781
  ).then((result) => {
7617
- this.showResult(result);
7782
+ const isAsync = !!(this.documentBlob || this.identityCheckId);
7783
+ if (isAsync && result.id) {
7784
+ this.livenessEntryId = result.id;
7785
+ this.showStep("processing");
7786
+ } else {
7787
+ this.showResult(result);
7788
+ }
7618
7789
  }).catch((err) => {
7619
7790
  this.showErrorStep(err.message || "Liveness verification failed. Please try again.");
7620
7791
  });
@@ -7626,6 +7797,8 @@ var SmartComplySDK = (() => {
7626
7797
  if (!this.contentArea) return;
7627
7798
  this.contentArea.innerHTML = "";
7628
7799
  const wrapper = document.createElement("div");
7800
+ wrapper.setAttribute("role", "region");
7801
+ wrapper.setAttribute("aria-label", "Verification result");
7629
7802
  wrapper.style.cssText = `
7630
7803
  display:flex;flex-direction:column;align-items:center;
7631
7804
  text-align:center;gap:16px;padding:20px 0;
@@ -7638,54 +7811,59 @@ var SmartComplySDK = (() => {
7638
7811
  font-size:36px;
7639
7812
  background:${this.theme.cardBg};
7640
7813
  border:3px solid ${this.theme.border};
7814
+ transition:all 0.4s ease;
7641
7815
  `;
7642
7816
  iconWrap.innerHTML = `<div style="width:32px;height:32px;border-radius:50%;border:3px solid ${this.theme.border};border-top-color:${this.theme.primary};animation:sc-spin 0.9s linear infinite;"></div>`;
7643
7817
  wrapper.appendChild(iconWrap);
7644
7818
  const title = document.createElement("h2");
7819
+ title.setAttribute("aria-live", "polite");
7645
7820
  title.style.cssText = `color:${this.theme.text};font-size:20px;font-weight:700;margin:0;`;
7646
- title.textContent = "Verification Submitted";
7821
+ title.textContent = "Checking your identity\u2026";
7647
7822
  wrapper.appendChild(title);
7648
7823
  const desc = document.createElement("p");
7824
+ desc.setAttribute("aria-live", "polite");
7649
7825
  desc.style.cssText = `color:${this.theme.textSecondary};font-size:14px;line-height:1.6;margin:0;max-width:300px;`;
7650
- desc.textContent = "Processing your identity check. This takes around 30 seconds.";
7826
+ desc.textContent = "This usually takes a few seconds. Please wait.";
7651
7827
  wrapper.appendChild(desc);
7652
- const details = document.createElement("div");
7653
- details.style.cssText = `
7828
+ const resultCard = document.createElement("div");
7829
+ resultCard.style.cssText = `
7654
7830
  width:100%;padding:16px;border-radius:12px;
7655
7831
  background:${this.theme.cardBg};border:1px solid ${this.theme.border};
7656
- text-align:left;
7832
+ text-align:left;display:flex;flex-direction:column;gap:10px;
7833
+ transition:border-color 0.3s ease;
7657
7834
  `;
7658
7835
  const statusBadge = document.createElement("span");
7836
+ statusBadge.setAttribute("role", "status");
7837
+ statusBadge.setAttribute("aria-live", "polite");
7838
+ statusBadge.setAttribute("aria-atomic", "true");
7659
7839
  statusBadge.style.cssText = `font-size:12px;font-weight:600;color:${this.theme.warning};`;
7660
7840
  statusBadge.textContent = "Processing\u2026";
7661
7841
  const statusRow = document.createElement("div");
7662
- statusRow.style.cssText = "display:flex;justify-content:space-between;margin-bottom:8px;";
7663
- statusRow.innerHTML = `<span style="color:${this.theme.textMuted};font-size:12px;">Status</span>`;
7842
+ statusRow.style.cssText = "display:flex;justify-content:space-between;align-items:center;";
7843
+ statusRow.innerHTML = `<span style="color:${this.theme.textMuted};font-size:12px;font-weight:500;">Status</span>`;
7664
7844
  statusRow.appendChild(statusBadge);
7665
- const entryRow = document.createElement("div");
7666
- entryRow.style.cssText = "display:flex;justify-content:space-between;margin-bottom:8px;";
7667
- entryRow.innerHTML = `
7668
- <span style="color:${this.theme.textMuted};font-size:12px;">Entry ID</span>
7669
- <span style="color:${this.theme.text};font-size:12px;font-weight:600;">${submitResult.id}</span>
7670
- `;
7671
- details.appendChild(entryRow);
7672
- details.appendChild(statusRow);
7673
- const submittedRow = document.createElement("div");
7674
- submittedRow.style.cssText = "display:flex;justify-content:space-between;margin-top:8px;";
7675
- submittedRow.innerHTML = `
7676
- <span style="color:${this.theme.textMuted};font-size:12px;">Submitted</span>
7677
- <span style="color:${this.theme.text};font-size:12px;">${submitResult.submitted_at ? new Date(submitResult.submitted_at).toLocaleString() : "Just now"}</span>
7678
- `;
7679
- details.appendChild(submittedRow);
7680
- wrapper.appendChild(details);
7845
+ resultCard.appendChild(statusRow);
7846
+ const nameRow = document.createElement("div");
7847
+ nameRow.style.cssText = "display:none;justify-content:space-between;align-items:center;";
7848
+ nameRow.innerHTML = `<span style="color:${this.theme.textMuted};font-size:12px;font-weight:500;">Name</span><span id="sc-result-name" style="color:${this.theme.text};font-size:12px;font-weight:600;text-align:right;max-width:60%;"></span>`;
7849
+ resultCard.appendChild(nameRow);
7850
+ const docTypeRow = document.createElement("div");
7851
+ docTypeRow.style.cssText = "display:none;justify-content:space-between;align-items:center;";
7852
+ docTypeRow.innerHTML = `<span style="color:${this.theme.textMuted};font-size:12px;font-weight:500;">ID Type</span><span id="sc-result-doctype" style="color:${this.theme.text};font-size:12px;font-weight:600;"></span>`;
7853
+ resultCard.appendChild(docTypeRow);
7854
+ wrapper.appendChild(resultCard);
7681
7855
  const failureMsg = document.createElement("div");
7856
+ failureMsg.setAttribute("role", "alert");
7682
7857
  failureMsg.style.cssText = `
7683
7858
  display:none;width:100%;padding:12px 14px;border-radius:10px;
7684
7859
  background:${this.theme.errorBg};border:1px solid ${this.theme.error};
7685
- color:${this.theme.error};font-size:12px;text-align:left;line-height:1.5;
7860
+ color:${this.theme.error};font-size:13px;text-align:left;line-height:1.6;
7686
7861
  `;
7687
7862
  wrapper.appendChild(failureMsg);
7863
+ const btnRow = document.createElement("div");
7864
+ btnRow.style.cssText = "display:flex;flex-direction:column;gap:8px;width:100%;";
7688
7865
  const doneBtn = this.createPrimaryButton("Done");
7866
+ doneBtn.setAttribute("aria-label", "Close verification and continue");
7689
7867
  let finalStatus = submitResult.status;
7690
7868
  doneBtn.addEventListener("click", () => {
7691
7869
  if (this._pollStatusTimer) {
@@ -7701,7 +7879,19 @@ var SmartComplySDK = (() => {
7701
7879
  });
7702
7880
  this.close();
7703
7881
  });
7704
- wrapper.appendChild(doneBtn);
7882
+ btnRow.appendChild(doneBtn);
7883
+ const retryBtn = this.createSecondaryButton("Try Again");
7884
+ retryBtn.setAttribute("aria-label", "Start verification again");
7885
+ retryBtn.style.display = "none";
7886
+ retryBtn.addEventListener("click", () => {
7887
+ if (this._pollStatusTimer) {
7888
+ clearTimeout(this._pollStatusTimer);
7889
+ this._pollStatusTimer = null;
7890
+ }
7891
+ this.initialize();
7892
+ });
7893
+ btnRow.appendChild(retryBtn);
7894
+ wrapper.appendChild(btnRow);
7705
7895
  this.contentArea.appendChild(wrapper);
7706
7896
  requestAnimationFrame(() => {
7707
7897
  wrapper.style.opacity = "1";
@@ -7713,13 +7903,17 @@ var SmartComplySDK = (() => {
7713
7903
  statusBadge,
7714
7904
  title,
7715
7905
  desc,
7906
+ resultCard,
7907
+ nameRow,
7908
+ docTypeRow,
7716
7909
  failureMsg,
7910
+ retryBtn,
7717
7911
  (resolved) => {
7718
7912
  finalStatus = resolved;
7719
7913
  }
7720
7914
  );
7721
7915
  }
7722
- _startStatusPolling(entryId, iconWrap, statusBadge, title, desc, failureMsg, onResolved) {
7916
+ _startStatusPolling(entryId, iconWrap, statusBadge, title, desc, resultCard, nameRow, docTypeRow, failureMsg, retryBtn, onResolved) {
7723
7917
  const MAX_POLLS = 25;
7724
7918
  let polls = 0;
7725
7919
  let uiTimeoutId = setTimeout(() => {
@@ -7736,43 +7930,56 @@ var SmartComplySDK = (() => {
7736
7930
  clearTimeout(this._pollStatusTimer);
7737
7931
  this._pollStatusTimer = null;
7738
7932
  }
7739
- iconWrap.innerHTML = "\u23F1";
7740
- iconWrap.style.background = this.theme.cardBg;
7933
+ iconWrap.innerHTML = "\u23F3";
7741
7934
  iconWrap.style.border = `3px solid ${this.theme.warning}`;
7742
- iconWrap.style.color = this.theme.warning;
7743
- iconWrap.style.fontSize = "30px";
7744
7935
  statusBadge.style.color = this.theme.textMuted;
7745
7936
  statusBadge.textContent = "Still processing\u2026";
7746
- title.textContent = "Verification In Progress";
7747
- desc.textContent = "Your verification is being processed. Your result will be delivered via your dashboard or webhook. You can safely close this.";
7937
+ title.textContent = "Still verifying";
7938
+ desc.textContent = "This is taking a little longer than usual. You can safely close this \u2014 your result will be available in your account.";
7748
7939
  };
7749
- const applyPassed = () => {
7940
+ const applyPassed = (result) => {
7750
7941
  clearUiTimeout();
7751
7942
  iconWrap.innerHTML = "\u2713";
7752
- iconWrap.style.background = this.theme.successBg;
7753
- iconWrap.style.border = `3px solid ${this.theme.success}`;
7754
- iconWrap.style.color = this.theme.success;
7755
- iconWrap.style.fontSize = "36px";
7943
+ iconWrap.style.cssText += `background:${this.theme.successBg};border:3px solid ${this.theme.success};color:${this.theme.success};font-size:36px;font-weight:700;`;
7944
+ resultCard.style.borderColor = this.theme.success;
7756
7945
  statusBadge.style.color = this.theme.success;
7757
7946
  statusBadge.textContent = "Verified \u2713";
7758
7947
  title.textContent = "Identity Verified";
7759
- desc.textContent = "Your identity check passed successfully.";
7948
+ desc.textContent = "Your identity has been confirmed successfully.";
7949
+ const profile = result?.customer_profile || result?.data || {};
7950
+ const docResult = result?.document || {};
7951
+ const ocr = docResult?.ocr_result || {};
7952
+ const firstName = (profile?.first_name || ocr?.first_name || docResult?.first_name || "").trim();
7953
+ const lastName = (profile?.last_name || profile?.surname || ocr?.last_name || ocr?.surname || docResult?.last_name || "").trim();
7954
+ const fullName = [firstName, lastName].filter(Boolean).join(" ");
7955
+ const nameEl = document.getElementById("sc-result-name");
7956
+ if (fullName && nameEl) {
7957
+ nameEl.textContent = fullName;
7958
+ nameRow.style.display = "flex";
7959
+ }
7960
+ const rawDocType = docResult?.document_type || this.selectedIdType || result?.identifier_type || "";
7961
+ const docTypeLabel = _formatDocType(rawDocType);
7962
+ const docTypeEl = document.getElementById("sc-result-doctype");
7963
+ if (docTypeLabel && docTypeEl) {
7964
+ docTypeEl.textContent = docTypeLabel;
7965
+ docTypeRow.style.display = "flex";
7966
+ }
7760
7967
  };
7761
7968
  const applyFailed = (reason) => {
7762
7969
  clearUiTimeout();
7763
7970
  iconWrap.innerHTML = "\u2715";
7764
- iconWrap.style.background = this.theme.errorBg;
7765
- iconWrap.style.border = `3px solid ${this.theme.error}`;
7766
- iconWrap.style.color = this.theme.error;
7767
- iconWrap.style.fontSize = "32px";
7971
+ iconWrap.style.cssText += `background:${this.theme.errorBg};border:3px solid ${this.theme.error};color:${this.theme.error};font-size:32px;`;
7972
+ resultCard.style.borderColor = this.theme.error;
7768
7973
  statusBadge.style.color = this.theme.error;
7769
- statusBadge.textContent = "Failed";
7770
- title.textContent = "Verification Failed";
7771
- desc.textContent = "Your identity check could not be completed.";
7772
- if (reason) {
7773
- failureMsg.textContent = reason;
7974
+ statusBadge.textContent = "Not verified";
7975
+ title.textContent = "Verification Unsuccessful";
7976
+ desc.textContent = "We were unable to confirm your identity this time.";
7977
+ const safeReason = _sanitizeUserMessage(reason);
7978
+ if (safeReason) {
7979
+ failureMsg.textContent = safeReason;
7774
7980
  failureMsg.style.display = "block";
7775
7981
  }
7982
+ retryBtn.style.display = "block";
7776
7983
  };
7777
7984
  const poll = async () => {
7778
7985
  if (this.isDestroyed) return;
@@ -7781,13 +7988,12 @@ var SmartComplySDK = (() => {
7781
7988
  const result = await this.sdk.liveness.pollResult(entryId);
7782
7989
  if (result.status === "passed") {
7783
7990
  onResolved("passed");
7784
- applyPassed();
7991
+ applyPassed(result);
7785
7992
  return;
7786
7993
  }
7787
7994
  if (result.status === "failed") {
7788
- onResolved(result.status);
7789
- const reason = result.failure_reason || "Verification could not be completed. Please try again.";
7790
- applyFailed(reason);
7995
+ onResolved("failed");
7996
+ applyFailed(result.failure_reason || null);
7791
7997
  return;
7792
7998
  }
7793
7999
  if (polls < MAX_POLLS && !this.isDestroyed) {
@@ -7813,20 +8019,25 @@ var SmartComplySDK = (() => {
7813
8019
  if (!this.contentArea) return;
7814
8020
  this.contentArea.innerHTML = "";
7815
8021
  const wrapper = document.createElement("div");
8022
+ wrapper.setAttribute("role", "alert");
7816
8023
  wrapper.style.cssText = `
7817
8024
  display:flex;flex-direction:column;align-items:center;
7818
8025
  text-align:center;gap:16px;padding:20px 0;
7819
8026
  `;
7820
8027
  const isCameraErr = /camera|permission|notallowed|notfound/i.test(message);
7821
8028
  const isNetworkErr = /network|timeout|connection|fetch/i.test(message);
7822
- const isAuthErr = /session|token|expired|authentication/i.test(message);
7823
- const errorIcon = isCameraErr ? "\u{1F4F7}" : isNetworkErr ? "\u{1F4E1}" : isAuthErr ? "\u23F1" : "\u2717";
7824
- const errorTitle = isCameraErr ? "Camera Access Required" : isNetworkErr ? "Connection Problem" : isAuthErr ? "Session Expired" : "Verification Failed";
8029
+ const isAuthErr = /session.*expired|token.*expired|authentication/i.test(message);
8030
+ const isDocQuality = /blurry|blur|quality|glare|dark|bright|lighting/i.test(message);
8031
+ const isExpiredDoc = /document.*expired|expired.*document/i.test(message);
8032
+ const isFaceMatch = /face.*match|selfie.*match|does not match/i.test(message);
8033
+ const errorIcon = isCameraErr ? "\u{1F4F7}" : isNetworkErr ? "\u{1F4E1}" : isAuthErr ? "\u23F1" : isDocQuality ? "\u{1F50D}" : isExpiredDoc ? "\u{1F4C5}" : isFaceMatch ? "\u{1F933}" : "\u2715";
8034
+ const errorTitle = isCameraErr ? "Camera Access Required" : isNetworkErr ? "Connection Problem" : isAuthErr ? "Session Expired" : isDocQuality ? "Document Photo Unclear" : isExpiredDoc ? "Document Expired" : isFaceMatch ? "Face Match Failed" : "Verification Unsuccessful";
8035
+ const userMessage = isCameraErr ? "Please allow camera access in your browser settings and try again." : isNetworkErr ? "Check your internet connection and try again." : isAuthErr ? "Your session has expired. Please start again." : isDocQuality ? _sanitizeUserMessage(message) || "Please retake your document photo in good lighting with no glare." : isExpiredDoc ? "The document you submitted has expired. Please use a valid, current ID." : isFaceMatch ? "Your selfie did not match the photo on your ID. Please ensure you are using your own valid document and face the camera directly." : _sanitizeUserMessage(message) || "We were unable to complete your verification. Please try again.";
7825
8036
  const iconWrap = document.createElement("div");
7826
8037
  iconWrap.style.cssText = `
7827
8038
  width:72px;height:72px;border-radius:50%;
7828
8039
  display:flex;align-items:center;justify-content:center;
7829
- font-size:${errorIcon === "\u2717" ? "32px" : "28px"};
8040
+ font-size:28px;
7830
8041
  background:${this.theme.errorBg};
7831
8042
  border:3px solid ${this.theme.error};
7832
8043
  `;
@@ -7838,18 +8049,16 @@ var SmartComplySDK = (() => {
7838
8049
  wrapper.appendChild(title);
7839
8050
  const desc = document.createElement("p");
7840
8051
  desc.style.cssText = `color:${this.theme.textSecondary};font-size:14px;line-height:1.6;margin:0;max-width:300px;`;
7841
- desc.textContent = message;
8052
+ desc.textContent = userMessage;
7842
8053
  wrapper.appendChild(desc);
7843
8054
  const retryBtn = this.createPrimaryButton("Try Again");
8055
+ retryBtn.setAttribute("aria-label", "Try verification again");
7844
8056
  retryBtn.addEventListener("click", () => {
7845
- if (this.sdk.sessionToken && this.sdkConfig) {
7846
- this.showStep("welcome");
7847
- } else {
7848
- this.initialize();
7849
- }
8057
+ this.initialize();
7850
8058
  });
7851
8059
  wrapper.appendChild(retryBtn);
7852
8060
  const closeBtn = this.createSecondaryButton("Close");
8061
+ closeBtn.setAttribute("aria-label", "Close verification widget");
7853
8062
  closeBtn.addEventListener("click", () => {
7854
8063
  this.options.onError?.(new Error(message));
7855
8064
  this.close();
@@ -7862,6 +8071,7 @@ var SmartComplySDK = (() => {
7862
8071
  // ─────────────────────────────────────────────────────────────────
7863
8072
  createHeader() {
7864
8073
  const header = document.createElement("div");
8074
+ header.setAttribute("role", "banner");
7865
8075
  header.style.cssText = `
7866
8076
  display:flex;align-items:center;justify-content:space-between;
7867
8077
  padding:11px 20px;border-bottom:1px solid ${this.theme.border};
@@ -7870,6 +8080,7 @@ var SmartComplySDK = (() => {
7870
8080
  leftArea.style.cssText = "display:flex;align-items:center;gap:8px;min-width:0;";
7871
8081
  this.headerBackBtn = document.createElement("button");
7872
8082
  this.headerBackBtn.innerHTML = "&#8592;";
8083
+ this.headerBackBtn.setAttribute("aria-label", "Go back to previous step");
7873
8084
  this.headerBackBtn.style.cssText = `
7874
8085
  width:30px;height:30px;border-radius:8px;flex-shrink:0;
7875
8086
  display:none;align-items:center;justify-content:center;
@@ -7878,6 +8089,7 @@ var SmartComplySDK = (() => {
7878
8089
  background:${this.theme.cardBg};color:${this.theme.text};
7879
8090
  transition:all 0.2s ease;
7880
8091
  `;
8092
+ this.headerBackBtn.style.cssText += `:focus-visible{outline:2px solid ${this.theme.primary};outline-offset:2px;}`;
7881
8093
  this.headerBackBtn.addEventListener("mouseenter", () => {
7882
8094
  if (this.headerBackBtn) {
7883
8095
  this.headerBackBtn.style.background = this.theme.shimmer;
@@ -7890,6 +8102,7 @@ var SmartComplySDK = (() => {
7890
8102
  });
7891
8103
  leftArea.appendChild(this.headerBackBtn);
7892
8104
  this.brandLabel = document.createElement("div");
8105
+ this.brandLabel.setAttribute("aria-label", "Verification provider");
7893
8106
  this.brandLabel.style.cssText = `
7894
8107
  font-size:15px;font-weight:700;color:${this.theme.text};
7895
8108
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
@@ -7898,6 +8111,8 @@ var SmartComplySDK = (() => {
7898
8111
  leftArea.appendChild(this.brandLabel);
7899
8112
  header.appendChild(leftArea);
7900
8113
  this.stepLabel = document.createElement("div");
8114
+ this.stepLabel.setAttribute("aria-live", "polite");
8115
+ this.stepLabel.setAttribute("aria-atomic", "true");
7901
8116
  this.stepLabel.style.cssText = `
7902
8117
  font-size:12px;color:${this.theme.textMuted};
7903
8118
  font-variant-numeric:tabular-nums;flex-shrink:0;
@@ -7905,6 +8120,7 @@ var SmartComplySDK = (() => {
7905
8120
  header.appendChild(this.stepLabel);
7906
8121
  const closeBtn = document.createElement("button");
7907
8122
  closeBtn.innerHTML = "\u2715";
8123
+ closeBtn.setAttribute("aria-label", "Close identity verification");
7908
8124
  closeBtn.style.cssText = `
7909
8125
  width:32px;height:32px;border-radius:8px;flex-shrink:0;
7910
8126
  display:flex;align-items:center;justify-content:center;
@@ -7951,6 +8167,7 @@ var SmartComplySDK = (() => {
7951
8167
  }
7952
8168
  createOptionCard(title, subtitle, onClick) {
7953
8169
  const card = document.createElement("button");
8170
+ card.setAttribute("aria-label", subtitle ? `${title}: ${subtitle}` : title);
7954
8171
  card.style.cssText = `
7955
8172
  width:100%;padding:13px 16px;border-radius:12px;
7956
8173
  background:${this.theme.cardBg};
@@ -8115,6 +8332,12 @@ var SmartComplySDK = (() => {
8115
8332
  #sc-flow-modal * {
8116
8333
  box-sizing: border-box;
8117
8334
  }
8335
+ #sc-flow-modal button:focus-visible,
8336
+ #sc-flow-modal select:focus-visible,
8337
+ #sc-flow-modal input:focus-visible {
8338
+ outline: 2px solid #3b82f6;
8339
+ outline-offset: 2px;
8340
+ }
8118
8341
  #sc-flow-modal::-webkit-scrollbar {
8119
8342
  width: 4px;
8120
8343
  }