smartcomply-web-sdk 1.0.42 → 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,
@@ -6566,6 +6590,9 @@ var SmartComplySDK = (() => {
6566
6590
  united_states: "\u{1F1FA}\u{1F1F8} United States",
6567
6591
  united_kingdom: "\u{1F1EC}\u{1F1E7} United Kingdom"
6568
6592
  };
6593
+ function isUploadFieldType(type) {
6594
+ return type === "upload" || type === "image" || type === "file";
6595
+ }
6569
6596
  function resolveIdTypeInfo(typeName, isDocumentFlow) {
6570
6597
  const u2 = typeName.toUpperCase();
6571
6598
  const advanced = u2.includes("ADVANCED");
@@ -6626,6 +6653,62 @@ var SmartComplySDK = (() => {
6626
6653
  requiresBack: false
6627
6654
  };
6628
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
+ }
6629
6712
  var style = document.createElement("style");
6630
6713
  style.textContent = `
6631
6714
  @keyframes sc-spin {
@@ -6674,6 +6757,7 @@ var SmartComplySDK = (() => {
6674
6757
  this._pollStatusTimer = null;
6675
6758
  // Components
6676
6759
  this.docCapture = null;
6760
+ this._livenessSubmitResult = null;
6677
6761
  this.options = options;
6678
6762
  this.sdk = new SmartComply({
6679
6763
  apiKey: options.apiKey,
@@ -6746,6 +6830,9 @@ var SmartComplySDK = (() => {
6746
6830
  `;
6747
6831
  this.modal = document.createElement("div");
6748
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");
6749
6836
  this.modal.style.cssText = `
6750
6837
  width:100%;max-width:580px;max-height:95vh;
6751
6838
  background:${this.theme.bg};
@@ -6768,6 +6855,9 @@ var SmartComplySDK = (() => {
6768
6855
  this.modal.appendChild(progressWrap);
6769
6856
  this.contentArea = document.createElement("div");
6770
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");
6771
6861
  this.contentArea.style.cssText = `
6772
6862
  flex:1;overflow-y:auto;padding:16px 20px;
6773
6863
  -webkit-overflow-scrolling:touch;
@@ -6852,7 +6942,7 @@ var SmartComplySDK = (() => {
6852
6942
  this.renderDocumentCapture(wrapper);
6853
6943
  break;
6854
6944
  case "processing":
6855
- this.renderProcessing(wrapper);
6945
+ this.renderProcessing(wrapper, this.livenessEntryId || void 0);
6856
6946
  break;
6857
6947
  case "liveness":
6858
6948
  this.renderLiveness(wrapper);
@@ -7048,7 +7138,7 @@ var SmartComplySDK = (() => {
7048
7138
  const typeName = typeof idType === "string" ? idType : idType.name;
7049
7139
  const typeCode = typeof idType === "string" ? idType : idType.code || idType.name;
7050
7140
  const channelFields = typeof idType !== "string" ? idType.fields || [] : [];
7051
- const isThisDocFlow = channelFields.some((f2) => f2.type === "upload") || globalDocFlow;
7141
+ const isThisDocFlow = channelFields.some((f2) => isUploadFieldType(f2.type)) || globalDocFlow;
7052
7142
  const info = resolveIdTypeInfo(typeName, isThisDocFlow);
7053
7143
  const requiresBackFromBackend = typeof idType !== "string" ? idType.requires_back_side : void 0;
7054
7144
  const requiresBack = requiresBackFromBackend ?? info.requiresBack;
@@ -7077,7 +7167,7 @@ var SmartComplySDK = (() => {
7077
7167
  this.selectedChannelId = idType.id;
7078
7168
  this.selectedChannelFields = idType.fields || [];
7079
7169
  }
7080
- const hasUpload = this.selectedChannelFields.some((f2) => f2.type === "upload");
7170
+ const hasUpload = this.selectedChannelFields.some((f2) => isUploadFieldType(f2.type));
7081
7171
  const vType = this.sdkConfig?.verification_type;
7082
7172
  const vTypes = vType ? Array.isArray(vType) ? vType : [vType] : [];
7083
7173
  const isDocChannel = hasUpload || vTypes.length > 0 && vTypes.includes("document_verification");
@@ -7113,12 +7203,44 @@ var SmartComplySDK = (() => {
7113
7203
  container.appendChild(autoBtn);
7114
7204
  return;
7115
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
+ }
7116
7235
  const inputEls = [];
7117
7236
  for (const field of dataFields) {
7118
7237
  const fieldKey = field.label;
7119
7238
  const fieldWrap = document.createElement("div");
7120
7239
  fieldWrap.style.cssText = "display:flex;flex-direction:column;gap:6px;";
7240
+ const labelId = `sc-field-label-${fieldKey.replace(/\s+/g, "-").toLowerCase()}`;
7121
7241
  const label = document.createElement("label");
7242
+ label.id = labelId;
7243
+ label.htmlFor = `sc-field-input-${fieldKey.replace(/\s+/g, "-").toLowerCase()}`;
7122
7244
  label.style.cssText = `color:${this.theme.textMuted};font-size:10px;font-weight:700;letter-spacing:0.6px;text-transform:uppercase;`;
7123
7245
  label.textContent = field.label;
7124
7246
  fieldWrap.appendChild(label);
@@ -7191,10 +7313,17 @@ var SmartComplySDK = (() => {
7191
7313
  fieldWrap.appendChild(fileInput);
7192
7314
  inputEls.push({ field, el: fileInput });
7193
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);
7194
7319
  const input = document.createElement("input");
7195
7320
  input.type = "text";
7321
+ input.id = inputId;
7322
+ input.setAttribute("aria-labelledby", labelId);
7323
+ input.setAttribute("aria-describedby", hintId);
7196
7324
  input.placeholder = `Enter ${field.label}`;
7197
- input.maxLength = 50;
7325
+ input.maxLength = rule?.maxLen ? rule.maxLen + 2 : 50;
7326
+ if (rule?.numeric) input.inputMode = "numeric";
7198
7327
  input.style.cssText = `
7199
7328
  width:100%;padding:14px 16px;border-radius:12px;
7200
7329
  font-size:16px;font-weight:500;
@@ -7205,17 +7334,46 @@ var SmartComplySDK = (() => {
7205
7334
  box-sizing:border-box;
7206
7335
  letter-spacing:0.5px;
7207
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
+ }
7208
7345
  input.addEventListener("focus", () => {
7209
7346
  input.style.borderColor = this.theme.primary;
7210
7347
  });
7211
7348
  input.addEventListener("blur", () => {
7212
- 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
+ }
7213
7366
  });
7214
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");
7215
7372
  this.collectedFields[fieldKey] = input.value.trim();
7216
7373
  updateSubmitState();
7217
7374
  });
7218
7375
  fieldWrap.appendChild(input);
7376
+ fieldWrap.appendChild(hintEl);
7219
7377
  inputEls.push({ field, el: input });
7220
7378
  }
7221
7379
  container.appendChild(fieldWrap);
@@ -7232,8 +7390,13 @@ var SmartComplySDK = (() => {
7232
7390
  submitBtn.style.opacity = "0.5";
7233
7391
  const updateSubmitState = () => {
7234
7392
  const allFilled = inputEls.every(({ el }) => el.value.trim().length > 0);
7235
- submitBtn.disabled = !allFilled;
7236
- 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";
7237
7400
  };
7238
7401
  submitBtn.addEventListener("click", async () => {
7239
7402
  submitBtn.disabled = true;
@@ -7250,7 +7413,7 @@ var SmartComplySDK = (() => {
7250
7413
  this.identityCheckId = this.verificationResult.data?.identity_check_id || null;
7251
7414
  this.showStep("liveness");
7252
7415
  } else {
7253
- 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.";
7254
7417
  errorEl.style.display = "block";
7255
7418
  submitBtn.textContent = "Verify & Continue";
7256
7419
  submitBtn.disabled = false;
@@ -7259,10 +7422,16 @@ var SmartComplySDK = (() => {
7259
7422
  } catch (err) {
7260
7423
  const fieldErrors = err.data?.errors;
7261
7424
  if (Array.isArray(fieldErrors) && fieldErrors.length > 0) {
7262
- const messages = fieldErrors.map((e2) => `${e2.field}: ${e2.message}`).join("\n");
7263
- 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.";
7264
7433
  } else {
7265
- errorEl.textContent = err.message || "Verification failed";
7434
+ errorEl.textContent = _sanitizeUserMessage(err.message) || "Something went wrong. Please try again.";
7266
7435
  }
7267
7436
  errorEl.style.display = "block";
7268
7437
  submitBtn.textContent = "Verify & Continue";
@@ -7312,121 +7481,112 @@ var SmartComplySDK = (() => {
7312
7481
  this.showErrorStep(err.message || "Document capture failed. Please try again.");
7313
7482
  });
7314
7483
  }
7315
- // ── Processing ────────────────────────────────────────────────────
7316
- renderProcessing(container) {
7317
- container.style.cssText += "display:flex;flex-direction:column;align-items:center;gap:14px;padding:16px;";
7318
- const animationWrap = document.createElement("div");
7319
- animationWrap.style.cssText = "position:relative;width:52px;height:52px;flex-shrink:0;";
7320
- const spinner = document.createElement("div");
7321
- spinner.style.cssText = `
7322
- position:absolute;inset:0;border-radius:50%;
7323
- border:3px solid ${this.theme.border};
7324
- border-top-color:${this.theme.primary};
7325
- animation:sc-spin 0.9s linear infinite;
7326
- `;
7327
- 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;`;
7328
7492
  const centerIcon = document.createElement("div");
7329
- centerIcon.style.cssText = `
7330
- position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
7331
- font-size:20px;
7332
- `;
7493
+ centerIcon.style.cssText = "position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:22px;";
7333
7494
  centerIcon.textContent = "\u{1F50D}";
7334
- animationWrap.appendChild(centerIcon);
7335
- container.appendChild(animationWrap);
7495
+ spinnerWrap.appendChild(ring);
7496
+ spinnerWrap.appendChild(centerIcon);
7497
+ container.appendChild(spinnerWrap);
7336
7498
  const title = document.createElement("div");
7337
7499
  title.style.cssText = `color:${this.theme.text};font-size:15px;font-weight:700;text-align:center;`;
7338
- title.textContent = "Processing your document";
7500
+ title.textContent = "Verifying your document";
7339
7501
  container.appendChild(title);
7340
- const subtitle = document.createElement("div");
7341
- subtitle.style.cssText = `color:${this.theme.textSecondary};font-size:12px;text-align:center;max-width:260px;line-height:1.5;`;
7342
- subtitle.textContent = "Scanning and verifying your ID. This usually takes a few seconds.";
7343
- container.appendChild(subtitle);
7344
- const stepsContainer = document.createElement("div");
7345
- stepsContainer.style.cssText = `
7346
- width:100%;display:grid;grid-template-columns:1fr 1fr;gap:8px;
7347
- `;
7348
- const steps = [
7349
- { id: "uploading", text: "Uploading", icon: "\u{1F4E4}" },
7350
- { id: "scanning", text: "Scanning", icon: "\u{1F50D}" },
7351
- { id: "verifying", text: "Verifying", icon: "\u2713" },
7352
- { 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" }
7353
7514
  ];
7354
- steps.forEach((step) => {
7355
- const stepEl = document.createElement("div");
7356
- stepEl.style.cssText = `
7357
- display:flex;align-items:center;gap:8px;padding:9px 10px;border-radius:10px;
7358
- background:${this.theme.cardBg};border:1px solid ${this.theme.border};
7359
- transition:all 0.3s ease;
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>
7360
7525
  `;
7361
- stepEl.id = `processing-step-${step.id}`;
7362
- const iconEl = document.createElement("div");
7363
- iconEl.style.cssText = `
7364
- width:26px;height:26px;border-radius:7px;flex-shrink:0;
7365
- display:flex;align-items:center;justify-content:center;
7366
- font-size:12px;background:${this.theme.shimmer};
7367
- `;
7368
- iconEl.textContent = step.icon;
7369
- iconEl.id = `step-icon-${step.id}`;
7370
- const textEl = document.createElement("div");
7371
- textEl.style.cssText = `flex:1;color:${this.theme.textSecondary};font-size:11px;font-weight:500;`;
7372
- textEl.textContent = step.text;
7373
- textEl.id = `step-text-${step.id}`;
7374
- const statusEl = document.createElement("div");
7375
- statusEl.style.cssText = `width:16px;height:16px;border-radius:50%;border:2px solid ${this.theme.border};position:relative;flex-shrink:0;`;
7376
- statusEl.id = `step-status-${step.id}`;
7377
- stepEl.appendChild(iconEl);
7378
- stepEl.appendChild(textEl);
7379
- stepEl.appendChild(statusEl);
7380
- stepsContainer.appendChild(stepEl);
7526
+ grid.appendChild(el);
7381
7527
  });
7382
- container.appendChild(stepsContainer);
7383
- this.startProcessingAnimation();
7384
- }
7385
- startProcessingAnimation() {
7386
- const steps = ["uploading", "scanning", "verifying", "analyzing"];
7387
- let currentStepIndex = 0;
7388
- const animateStep = (stepId) => {
7389
- const stepEl = document.getElementById(`processing-step-${stepId}`);
7390
- const iconEl = document.getElementById(`step-icon-${stepId}`);
7391
- const textEl = document.getElementById(`step-text-${stepId}`);
7392
- const statusEl = document.getElementById(`step-status-${stepId}`);
7393
- if (stepEl && iconEl && textEl && statusEl) {
7394
- stepEl.style.borderColor = this.theme.primary;
7395
- stepEl.style.background = `${this.theme.primary}10`;
7396
- iconEl.style.background = this.theme.primary;
7397
- iconEl.style.color = "white";
7398
- textEl.style.color = this.theme.text;
7399
- textEl.style.fontWeight = "600";
7400
- statusEl.innerHTML = `
7401
- <div style="
7402
- position:absolute;inset:2px;border-radius:50%;
7403
- border:2px solid ${this.theme.border};
7404
- border-top-color:${this.theme.primary};
7405
- animation:sc-spin 0.8s linear infinite;
7406
- "></div>
7407
- `;
7408
- }
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) => {
7409
7555
  setTimeout(() => {
7410
- if (stepEl && iconEl && textEl && statusEl) {
7411
- stepEl.style.borderColor = this.theme.success;
7412
- stepEl.style.background = `${this.theme.success}10`;
7413
- iconEl.style.background = this.theme.success;
7414
- statusEl.innerHTML = `
7415
- <div style="
7416
- position:absolute;inset:3px;border-radius:50%;
7417
- background:${this.theme.success};
7418
- display:flex;align-items:center;justify-content:center;
7419
- color:white;font-size:10px;font-weight:bold;
7420
- ">\u2713</div>
7421
- `;
7422
- }
7423
- currentStepIndex++;
7424
- if (currentStepIndex < steps.length) {
7425
- 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;
7426
7579
  }
7427
- }, 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
+ }
7428
7588
  };
7429
- setTimeout(() => animateStep(steps[0]), 500);
7589
+ this._pollStatusTimer = setTimeout(poll, 5e3);
7430
7590
  }
7431
7591
  /**
7432
7592
  * Process document capture result with front/back support
@@ -7463,7 +7623,7 @@ var SmartComplySDK = (() => {
7463
7623
  <div style="color:${this.theme.text};font-size:14px;font-weight:600;">Scanning document...</div>
7464
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>
7465
7625
  </div>`;
7466
- const identifier = `DOC-${Date.now()}`;
7626
+ const identifier = Object.values(this.collectedFields)[0] || this.idNumber || this.selectedIdType || `DOC-${Date.now()}`;
7467
7627
  const COUNTRY_CODES = {
7468
7628
  nigeria: "NG",
7469
7629
  ghana: "GH",
@@ -7532,47 +7692,53 @@ var SmartComplySDK = (() => {
7532
7692
  await attemptProcessing();
7533
7693
  }
7534
7694
  handleDocumentProcessingError(error, captureContainer) {
7535
- let errorMessage = "Document verification failed. Please try again.";
7695
+ let userMessage = "We could not verify your document. Please try again.";
7536
7696
  let canRetry = true;
7537
- if (error.message) {
7538
- if (error.message.includes("expired")) {
7539
- errorMessage = "Document has expired. Please use a valid document.";
7540
- canRetry = false;
7541
- } else if (error.message.includes("quality too low")) {
7542
- errorMessage = "Image quality too low. Please ensure the document is well-lit and clearly visible.";
7543
- } else if (error.message.includes("Network") || error.message.includes("fetch")) {
7544
- errorMessage = "Network connection failed. Please check your internet connection and try again.";
7545
- } else if (error.message.includes("413") || error.message.includes("too large")) {
7546
- errorMessage = "Document image is too large. Please use a smaller image or retake the photo.";
7547
- } else if (error.message.includes("415") || error.message.includes("format")) {
7548
- errorMessage = "Unsupported image format. Please use JPEG or PNG images.";
7549
- }
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.";
7550
7713
  }
7551
7714
  if (captureContainer) {
7552
- captureContainer.innerHTML = `
7553
- <div style="display:flex;flex-direction:column;align-items:center;gap:16px;padding:28px 20px;text-align:center;">
7554
- <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>
7555
- <div style="color:${this.theme.text};font-size:14px;font-weight:700;">Verification Failed</div>
7556
- <div style="color:${this.theme.textSecondary};font-size:13px;max-width:240px;line-height:1.6;">${errorMessage}</div>
7557
- ${canRetry ? `
7558
- <button id="retry-btn" style="
7559
- margin-top:8px;padding:12px 24px;border-radius:8px;
7560
- background:${this.theme.primary};color:white;
7561
- border:none;font-size:14px;font-weight:600;cursor:pointer;
7562
- transition:all 0.2s ease;
7563
- ">Try Again</button>
7564
- ` : ""}
7565
- </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);
7566
7730
  if (canRetry) {
7567
- const retryBtn = captureContainer.querySelector("#retry-btn");
7568
- if (retryBtn) {
7569
- retryBtn.addEventListener("click", () => {
7570
- this.showStep("document_capture");
7571
- });
7572
- }
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);
7573
7737
  }
7738
+ captureContainer.innerHTML = "";
7739
+ captureContainer.appendChild(wrap);
7574
7740
  } else {
7575
- this.showErrorStep(errorMessage);
7741
+ this.showErrorStep(userMessage);
7576
7742
  }
7577
7743
  }
7578
7744
  // ── Liveness ────────────────────────────────────────────────────
@@ -7587,7 +7753,7 @@ var SmartComplySDK = (() => {
7587
7753
  const livenessContainer = document.createElement("div");
7588
7754
  livenessContainer.style.cssText = "margin:8px 0;width:100%;";
7589
7755
  container.appendChild(livenessContainer);
7590
- const identifier = this.idNumber || `DOC-${Date.now()}`;
7756
+ const identifier = this.idNumber || Object.values(this.collectedFields)[0] || this.selectedIdType || `DOC-${Date.now()}`;
7591
7757
  const COUNTRY_CODES = {
7592
7758
  nigeria: "NG",
7593
7759
  ghana: "GH",
@@ -7606,12 +7772,20 @@ var SmartComplySDK = (() => {
7606
7772
  country,
7607
7773
  document: this.documentBlob || void 0,
7608
7774
  document_back: this.documentBackBlob || void 0,
7609
- 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"
7610
7778
  },
7611
7779
  ["BLINK", "TURN_HEAD", "OPEN_MOUTH"],
7612
7780
  this.livenessEntryId || void 0
7613
7781
  ).then((result) => {
7614
- 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
+ }
7615
7789
  }).catch((err) => {
7616
7790
  this.showErrorStep(err.message || "Liveness verification failed. Please try again.");
7617
7791
  });
@@ -7623,6 +7797,8 @@ var SmartComplySDK = (() => {
7623
7797
  if (!this.contentArea) return;
7624
7798
  this.contentArea.innerHTML = "";
7625
7799
  const wrapper = document.createElement("div");
7800
+ wrapper.setAttribute("role", "region");
7801
+ wrapper.setAttribute("aria-label", "Verification result");
7626
7802
  wrapper.style.cssText = `
7627
7803
  display:flex;flex-direction:column;align-items:center;
7628
7804
  text-align:center;gap:16px;padding:20px 0;
@@ -7635,54 +7811,59 @@ var SmartComplySDK = (() => {
7635
7811
  font-size:36px;
7636
7812
  background:${this.theme.cardBg};
7637
7813
  border:3px solid ${this.theme.border};
7814
+ transition:all 0.4s ease;
7638
7815
  `;
7639
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>`;
7640
7817
  wrapper.appendChild(iconWrap);
7641
7818
  const title = document.createElement("h2");
7819
+ title.setAttribute("aria-live", "polite");
7642
7820
  title.style.cssText = `color:${this.theme.text};font-size:20px;font-weight:700;margin:0;`;
7643
- title.textContent = "Verification Submitted";
7821
+ title.textContent = "Checking your identity\u2026";
7644
7822
  wrapper.appendChild(title);
7645
7823
  const desc = document.createElement("p");
7824
+ desc.setAttribute("aria-live", "polite");
7646
7825
  desc.style.cssText = `color:${this.theme.textSecondary};font-size:14px;line-height:1.6;margin:0;max-width:300px;`;
7647
- desc.textContent = "Processing your identity check. This takes around 30 seconds.";
7826
+ desc.textContent = "This usually takes a few seconds. Please wait.";
7648
7827
  wrapper.appendChild(desc);
7649
- const details = document.createElement("div");
7650
- details.style.cssText = `
7828
+ const resultCard = document.createElement("div");
7829
+ resultCard.style.cssText = `
7651
7830
  width:100%;padding:16px;border-radius:12px;
7652
7831
  background:${this.theme.cardBg};border:1px solid ${this.theme.border};
7653
- text-align:left;
7832
+ text-align:left;display:flex;flex-direction:column;gap:10px;
7833
+ transition:border-color 0.3s ease;
7654
7834
  `;
7655
7835
  const statusBadge = document.createElement("span");
7836
+ statusBadge.setAttribute("role", "status");
7837
+ statusBadge.setAttribute("aria-live", "polite");
7838
+ statusBadge.setAttribute("aria-atomic", "true");
7656
7839
  statusBadge.style.cssText = `font-size:12px;font-weight:600;color:${this.theme.warning};`;
7657
7840
  statusBadge.textContent = "Processing\u2026";
7658
7841
  const statusRow = document.createElement("div");
7659
- statusRow.style.cssText = "display:flex;justify-content:space-between;margin-bottom:8px;";
7660
- 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>`;
7661
7844
  statusRow.appendChild(statusBadge);
7662
- const entryRow = document.createElement("div");
7663
- entryRow.style.cssText = "display:flex;justify-content:space-between;margin-bottom:8px;";
7664
- entryRow.innerHTML = `
7665
- <span style="color:${this.theme.textMuted};font-size:12px;">Entry ID</span>
7666
- <span style="color:${this.theme.text};font-size:12px;font-weight:600;">${submitResult.id}</span>
7667
- `;
7668
- details.appendChild(entryRow);
7669
- details.appendChild(statusRow);
7670
- const submittedRow = document.createElement("div");
7671
- submittedRow.style.cssText = "display:flex;justify-content:space-between;margin-top:8px;";
7672
- submittedRow.innerHTML = `
7673
- <span style="color:${this.theme.textMuted};font-size:12px;">Submitted</span>
7674
- <span style="color:${this.theme.text};font-size:12px;">${submitResult.submitted_at ? new Date(submitResult.submitted_at).toLocaleString() : "Just now"}</span>
7675
- `;
7676
- details.appendChild(submittedRow);
7677
- 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);
7678
7855
  const failureMsg = document.createElement("div");
7856
+ failureMsg.setAttribute("role", "alert");
7679
7857
  failureMsg.style.cssText = `
7680
7858
  display:none;width:100%;padding:12px 14px;border-radius:10px;
7681
7859
  background:${this.theme.errorBg};border:1px solid ${this.theme.error};
7682
- 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;
7683
7861
  `;
7684
7862
  wrapper.appendChild(failureMsg);
7863
+ const btnRow = document.createElement("div");
7864
+ btnRow.style.cssText = "display:flex;flex-direction:column;gap:8px;width:100%;";
7685
7865
  const doneBtn = this.createPrimaryButton("Done");
7866
+ doneBtn.setAttribute("aria-label", "Close verification and continue");
7686
7867
  let finalStatus = submitResult.status;
7687
7868
  doneBtn.addEventListener("click", () => {
7688
7869
  if (this._pollStatusTimer) {
@@ -7698,7 +7879,19 @@ var SmartComplySDK = (() => {
7698
7879
  });
7699
7880
  this.close();
7700
7881
  });
7701
- 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);
7702
7895
  this.contentArea.appendChild(wrapper);
7703
7896
  requestAnimationFrame(() => {
7704
7897
  wrapper.style.opacity = "1";
@@ -7710,13 +7903,17 @@ var SmartComplySDK = (() => {
7710
7903
  statusBadge,
7711
7904
  title,
7712
7905
  desc,
7906
+ resultCard,
7907
+ nameRow,
7908
+ docTypeRow,
7713
7909
  failureMsg,
7910
+ retryBtn,
7714
7911
  (resolved) => {
7715
7912
  finalStatus = resolved;
7716
7913
  }
7717
7914
  );
7718
7915
  }
7719
- _startStatusPolling(entryId, iconWrap, statusBadge, title, desc, failureMsg, onResolved) {
7916
+ _startStatusPolling(entryId, iconWrap, statusBadge, title, desc, resultCard, nameRow, docTypeRow, failureMsg, retryBtn, onResolved) {
7720
7917
  const MAX_POLLS = 25;
7721
7918
  let polls = 0;
7722
7919
  let uiTimeoutId = setTimeout(() => {
@@ -7733,43 +7930,56 @@ var SmartComplySDK = (() => {
7733
7930
  clearTimeout(this._pollStatusTimer);
7734
7931
  this._pollStatusTimer = null;
7735
7932
  }
7736
- iconWrap.innerHTML = "\u23F1";
7737
- iconWrap.style.background = this.theme.cardBg;
7933
+ iconWrap.innerHTML = "\u23F3";
7738
7934
  iconWrap.style.border = `3px solid ${this.theme.warning}`;
7739
- iconWrap.style.color = this.theme.warning;
7740
- iconWrap.style.fontSize = "30px";
7741
7935
  statusBadge.style.color = this.theme.textMuted;
7742
7936
  statusBadge.textContent = "Still processing\u2026";
7743
- title.textContent = "Verification In Progress";
7744
- 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.";
7745
7939
  };
7746
- const applyPassed = () => {
7940
+ const applyPassed = (result) => {
7747
7941
  clearUiTimeout();
7748
7942
  iconWrap.innerHTML = "\u2713";
7749
- iconWrap.style.background = this.theme.successBg;
7750
- iconWrap.style.border = `3px solid ${this.theme.success}`;
7751
- iconWrap.style.color = this.theme.success;
7752
- 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;
7753
7945
  statusBadge.style.color = this.theme.success;
7754
7946
  statusBadge.textContent = "Verified \u2713";
7755
7947
  title.textContent = "Identity Verified";
7756
- 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
+ }
7757
7967
  };
7758
7968
  const applyFailed = (reason) => {
7759
7969
  clearUiTimeout();
7760
7970
  iconWrap.innerHTML = "\u2715";
7761
- iconWrap.style.background = this.theme.errorBg;
7762
- iconWrap.style.border = `3px solid ${this.theme.error}`;
7763
- iconWrap.style.color = this.theme.error;
7764
- 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;
7765
7973
  statusBadge.style.color = this.theme.error;
7766
- statusBadge.textContent = "Failed";
7767
- title.textContent = "Verification Failed";
7768
- desc.textContent = "Your identity check could not be completed.";
7769
- if (reason) {
7770
- 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;
7771
7980
  failureMsg.style.display = "block";
7772
7981
  }
7982
+ retryBtn.style.display = "block";
7773
7983
  };
7774
7984
  const poll = async () => {
7775
7985
  if (this.isDestroyed) return;
@@ -7778,13 +7988,12 @@ var SmartComplySDK = (() => {
7778
7988
  const result = await this.sdk.liveness.pollResult(entryId);
7779
7989
  if (result.status === "passed") {
7780
7990
  onResolved("passed");
7781
- applyPassed();
7991
+ applyPassed(result);
7782
7992
  return;
7783
7993
  }
7784
7994
  if (result.status === "failed") {
7785
- onResolved(result.status);
7786
- const reason = result.failure_reason || "Verification could not be completed. Please try again.";
7787
- applyFailed(reason);
7995
+ onResolved("failed");
7996
+ applyFailed(result.failure_reason || null);
7788
7997
  return;
7789
7998
  }
7790
7999
  if (polls < MAX_POLLS && !this.isDestroyed) {
@@ -7810,20 +8019,25 @@ var SmartComplySDK = (() => {
7810
8019
  if (!this.contentArea) return;
7811
8020
  this.contentArea.innerHTML = "";
7812
8021
  const wrapper = document.createElement("div");
8022
+ wrapper.setAttribute("role", "alert");
7813
8023
  wrapper.style.cssText = `
7814
8024
  display:flex;flex-direction:column;align-items:center;
7815
8025
  text-align:center;gap:16px;padding:20px 0;
7816
8026
  `;
7817
8027
  const isCameraErr = /camera|permission|notallowed|notfound/i.test(message);
7818
8028
  const isNetworkErr = /network|timeout|connection|fetch/i.test(message);
7819
- const isAuthErr = /session|token|expired|authentication/i.test(message);
7820
- const errorIcon = isCameraErr ? "\u{1F4F7}" : isNetworkErr ? "\u{1F4E1}" : isAuthErr ? "\u23F1" : "\u2717";
7821
- 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.";
7822
8036
  const iconWrap = document.createElement("div");
7823
8037
  iconWrap.style.cssText = `
7824
8038
  width:72px;height:72px;border-radius:50%;
7825
8039
  display:flex;align-items:center;justify-content:center;
7826
- font-size:${errorIcon === "\u2717" ? "32px" : "28px"};
8040
+ font-size:28px;
7827
8041
  background:${this.theme.errorBg};
7828
8042
  border:3px solid ${this.theme.error};
7829
8043
  `;
@@ -7835,18 +8049,16 @@ var SmartComplySDK = (() => {
7835
8049
  wrapper.appendChild(title);
7836
8050
  const desc = document.createElement("p");
7837
8051
  desc.style.cssText = `color:${this.theme.textSecondary};font-size:14px;line-height:1.6;margin:0;max-width:300px;`;
7838
- desc.textContent = message;
8052
+ desc.textContent = userMessage;
7839
8053
  wrapper.appendChild(desc);
7840
8054
  const retryBtn = this.createPrimaryButton("Try Again");
8055
+ retryBtn.setAttribute("aria-label", "Try verification again");
7841
8056
  retryBtn.addEventListener("click", () => {
7842
- if (this.sdk.sessionToken && this.sdkConfig) {
7843
- this.showStep("welcome");
7844
- } else {
7845
- this.initialize();
7846
- }
8057
+ this.initialize();
7847
8058
  });
7848
8059
  wrapper.appendChild(retryBtn);
7849
8060
  const closeBtn = this.createSecondaryButton("Close");
8061
+ closeBtn.setAttribute("aria-label", "Close verification widget");
7850
8062
  closeBtn.addEventListener("click", () => {
7851
8063
  this.options.onError?.(new Error(message));
7852
8064
  this.close();
@@ -7859,6 +8071,7 @@ var SmartComplySDK = (() => {
7859
8071
  // ─────────────────────────────────────────────────────────────────
7860
8072
  createHeader() {
7861
8073
  const header = document.createElement("div");
8074
+ header.setAttribute("role", "banner");
7862
8075
  header.style.cssText = `
7863
8076
  display:flex;align-items:center;justify-content:space-between;
7864
8077
  padding:11px 20px;border-bottom:1px solid ${this.theme.border};
@@ -7867,6 +8080,7 @@ var SmartComplySDK = (() => {
7867
8080
  leftArea.style.cssText = "display:flex;align-items:center;gap:8px;min-width:0;";
7868
8081
  this.headerBackBtn = document.createElement("button");
7869
8082
  this.headerBackBtn.innerHTML = "&#8592;";
8083
+ this.headerBackBtn.setAttribute("aria-label", "Go back to previous step");
7870
8084
  this.headerBackBtn.style.cssText = `
7871
8085
  width:30px;height:30px;border-radius:8px;flex-shrink:0;
7872
8086
  display:none;align-items:center;justify-content:center;
@@ -7875,6 +8089,7 @@ var SmartComplySDK = (() => {
7875
8089
  background:${this.theme.cardBg};color:${this.theme.text};
7876
8090
  transition:all 0.2s ease;
7877
8091
  `;
8092
+ this.headerBackBtn.style.cssText += `:focus-visible{outline:2px solid ${this.theme.primary};outline-offset:2px;}`;
7878
8093
  this.headerBackBtn.addEventListener("mouseenter", () => {
7879
8094
  if (this.headerBackBtn) {
7880
8095
  this.headerBackBtn.style.background = this.theme.shimmer;
@@ -7887,6 +8102,7 @@ var SmartComplySDK = (() => {
7887
8102
  });
7888
8103
  leftArea.appendChild(this.headerBackBtn);
7889
8104
  this.brandLabel = document.createElement("div");
8105
+ this.brandLabel.setAttribute("aria-label", "Verification provider");
7890
8106
  this.brandLabel.style.cssText = `
7891
8107
  font-size:15px;font-weight:700;color:${this.theme.text};
7892
8108
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
@@ -7895,6 +8111,8 @@ var SmartComplySDK = (() => {
7895
8111
  leftArea.appendChild(this.brandLabel);
7896
8112
  header.appendChild(leftArea);
7897
8113
  this.stepLabel = document.createElement("div");
8114
+ this.stepLabel.setAttribute("aria-live", "polite");
8115
+ this.stepLabel.setAttribute("aria-atomic", "true");
7898
8116
  this.stepLabel.style.cssText = `
7899
8117
  font-size:12px;color:${this.theme.textMuted};
7900
8118
  font-variant-numeric:tabular-nums;flex-shrink:0;
@@ -7902,6 +8120,7 @@ var SmartComplySDK = (() => {
7902
8120
  header.appendChild(this.stepLabel);
7903
8121
  const closeBtn = document.createElement("button");
7904
8122
  closeBtn.innerHTML = "\u2715";
8123
+ closeBtn.setAttribute("aria-label", "Close identity verification");
7905
8124
  closeBtn.style.cssText = `
7906
8125
  width:32px;height:32px;border-radius:8px;flex-shrink:0;
7907
8126
  display:flex;align-items:center;justify-content:center;
@@ -7948,6 +8167,7 @@ var SmartComplySDK = (() => {
7948
8167
  }
7949
8168
  createOptionCard(title, subtitle, onClick) {
7950
8169
  const card = document.createElement("button");
8170
+ card.setAttribute("aria-label", subtitle ? `${title}: ${subtitle}` : title);
7951
8171
  card.style.cssText = `
7952
8172
  width:100%;padding:13px 16px;border-radius:12px;
7953
8173
  background:${this.theme.cardBg};
@@ -8033,7 +8253,7 @@ var SmartComplySDK = (() => {
8033
8253
  }
8034
8254
  const channels = this.sdkConfig?.channels || {};
8035
8255
  const allFields = Object.values(channels).flat().flatMap((ch) => ch.fields || []);
8036
- return allFields.some((f2) => f2.type === "upload");
8256
+ return allFields.some((f2) => isUploadFieldType(f2.type));
8037
8257
  }
8038
8258
  updateProgress() {
8039
8259
  const isDocFlow = this.isDocumentFlow();
@@ -8112,6 +8332,12 @@ var SmartComplySDK = (() => {
8112
8332
  #sc-flow-modal * {
8113
8333
  box-sizing: border-box;
8114
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
+ }
8115
8341
  #sc-flow-modal::-webkit-scrollbar {
8116
8342
  width: 4px;
8117
8343
  }