smartcomply-web-sdk 1.0.66 → 1.0.69

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/esm/flow/DocumentCapture.d.ts +10 -0
  2. package/dist/esm/flow/DocumentCapture.d.ts.map +1 -1
  3. package/dist/esm/flow/DocumentCapture.js +83 -5
  4. package/dist/esm/flow/DocumentCapture.js.map +1 -1
  5. package/dist/esm/flow/SmartComplyFlow.d.ts +19 -0
  6. package/dist/esm/flow/SmartComplyFlow.d.ts.map +1 -1
  7. package/dist/esm/flow/SmartComplyFlow.js +216 -32
  8. package/dist/esm/flow/SmartComplyFlow.js.map +1 -1
  9. package/dist/esm/modules/liveness/LivenessUI.d.ts.map +1 -1
  10. package/dist/esm/modules/liveness/LivenessUI.js +13 -3
  11. package/dist/esm/modules/liveness/LivenessUI.js.map +1 -1
  12. package/dist/esm/modules/liveness/liveness.js +1 -1
  13. package/dist/esm/modules/liveness/liveness.js.map +1 -1
  14. package/dist/esm/utils/ImageQuality.d.ts +12 -1
  15. package/dist/esm/utils/ImageQuality.d.ts.map +1 -1
  16. package/dist/esm/utils/ImageQuality.js +12 -1
  17. package/dist/esm/utils/ImageQuality.js.map +1 -1
  18. package/dist/esm/utils/constants.d.ts +9 -6
  19. package/dist/esm/utils/constants.d.ts.map +1 -1
  20. package/dist/esm/utils/constants.js +9 -6
  21. package/dist/esm/utils/constants.js.map +1 -1
  22. package/dist/flow/DocumentCapture.d.ts +10 -0
  23. package/dist/flow/DocumentCapture.d.ts.map +1 -1
  24. package/dist/flow/DocumentCapture.js +83 -5
  25. package/dist/flow/DocumentCapture.js.map +1 -1
  26. package/dist/flow/SmartComplyFlow.d.ts +19 -0
  27. package/dist/flow/SmartComplyFlow.d.ts.map +1 -1
  28. package/dist/flow/SmartComplyFlow.js +216 -32
  29. package/dist/flow/SmartComplyFlow.js.map +1 -1
  30. package/dist/modules/liveness/LivenessUI.d.ts.map +1 -1
  31. package/dist/modules/liveness/LivenessUI.js +13 -3
  32. package/dist/modules/liveness/LivenessUI.js.map +1 -1
  33. package/dist/modules/liveness/liveness.js +1 -1
  34. package/dist/modules/liveness/liveness.js.map +1 -1
  35. package/dist/smartcomply.browser.js +360 -43
  36. package/dist/smartcomply.browser.js.map +4 -4
  37. package/dist/utils/ImageQuality.d.ts +12 -1
  38. package/dist/utils/ImageQuality.d.ts.map +1 -1
  39. package/dist/utils/ImageQuality.js +12 -1
  40. package/dist/utils/ImageQuality.js.map +1 -1
  41. package/dist/utils/constants.d.ts +9 -6
  42. package/dist/utils/constants.d.ts.map +1 -1
  43. package/dist/utils/constants.js +9 -6
  44. package/dist/utils/constants.js.map +1 -1
  45. package/package.json +1 -1
@@ -296,7 +296,7 @@ var SmartComplySDK = (() => {
296
296
  var MAX_DOCUMENT_BYTES = 5 * 1024 * 1024;
297
297
  var MAX_VIDEO_BYTES = 50 * 1024 * 1024;
298
298
  var MAX_SNAPSHOT_BYTES = 2 * 1024 * 1024;
299
- var DEFAULT_CHALLENGE_ACTIONS = ["BLINK"];
299
+ var DEFAULT_CHALLENGE_ACTIONS = ["BLINK", "TURN_HEAD"];
300
300
  var LIVENESS_TIME_LIMIT_SECONDS = 12;
301
301
  var DOC_CAMERA_IDEAL_WIDTH = 2e3;
302
302
  var DOC_CAMERA_IDEAL_HEIGHT = 1500;
@@ -4709,6 +4709,7 @@ var SmartComplySDK = (() => {
4709
4709
  this.videoEl.autoplay = true;
4710
4710
  this.videoEl.playsInline = true;
4711
4711
  this.videoEl.muted = true;
4712
+ this.videoEl.setAttribute("aria-label", "Live camera preview for face verification");
4712
4713
  this.videoEl.style.cssText = `
4713
4714
  width:100%;height:100%;display:block;
4714
4715
  filter:brightness(1.03) contrast(1.03);
@@ -4730,6 +4731,7 @@ var SmartComplySDK = (() => {
4730
4731
  this.guideWrapEl = guideWrap;
4731
4732
  const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
4732
4733
  svg.setAttribute("viewBox", "0 0 140 180");
4734
+ svg.setAttribute("aria-hidden", "true");
4733
4735
  svg.style.cssText = "width:100%;height:100%;overflow:visible;";
4734
4736
  const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
4735
4737
  const clip = document.createElementNS("http://www.w3.org/2000/svg", "clipPath");
@@ -4769,12 +4771,12 @@ var SmartComplySDK = (() => {
4769
4771
  confOval.setAttribute("ry", "86");
4770
4772
  confOval.setAttribute("fill", "none");
4771
4773
  confOval.setAttribute("stroke", this.colors.primary);
4772
- confOval.setAttribute("stroke-width", "2.5");
4773
- confOval.setAttribute("stroke-linecap", "round");
4774
+ confOval.setAttribute("stroke-width", "3");
4775
+ confOval.setAttribute("stroke-linecap", "butt");
4774
4776
  const circ = 2 * Math.PI * Math.sqrt((64 * 64 + 86 * 86) / 2);
4775
4777
  confOval.setAttribute("stroke-dasharray", `${circ}`);
4776
4778
  confOval.setAttribute("stroke-dashoffset", `${circ}`);
4777
- confOval.style.cssText = "transition:stroke-dashoffset 0.2s linear,stroke 0.3s ease;transform:rotate(-90deg);transform-origin:70px 90px;";
4779
+ confOval.style.cssText = "transition:stroke-dashoffset 0.2s linear,stroke 0.3s ease;transform:rotate(-90deg);transform-origin:70px 90px;filter:drop-shadow(0 0 3px rgba(0,0,0,0.25));";
4778
4780
  this.confidenceRingEl = confOval;
4779
4781
  const bracketGroup = document.createElementNS("http://www.w3.org/2000/svg", "g");
4780
4782
  bracketGroup.setAttribute("stroke", this.colors.primary);
@@ -4822,6 +4824,8 @@ var SmartComplySDK = (() => {
4822
4824
  padding:14px 16px 32px;
4823
4825
  `;
4824
4826
  this.instructionEl = document.createElement("div");
4827
+ this.instructionEl.setAttribute("aria-live", "polite");
4828
+ this.instructionEl.setAttribute("role", "status");
4825
4829
  this.instructionEl.style.cssText = `
4826
4830
  color:#fff;font-size:14px;font-weight:600;text-align:center;line-height:1.4;
4827
4831
  `;
@@ -5448,7 +5452,7 @@ var SmartComplySDK = (() => {
5448
5452
  tempVideo.remove();
5449
5453
  const uiChallenge = {
5450
5454
  time_limit_seconds: LIVENESS_TIME_LIMIT_SECONDS,
5451
- instruction: "Blink naturally and look slowly left, right, up or down"
5455
+ instruction: "Blink naturally and slowly turn your head from side to side"
5452
5456
  };
5453
5457
  ui2.unmount();
5454
5458
  const videoEl = ui2.mount(container, { ...uiChallenge, instruction: "Starting scan…" });
@@ -5857,6 +5861,126 @@ var SmartComplySDK = (() => {
5857
5861
  return Math.min(maxWidth / srcWidth, maxHeight / srcHeight, 1);
5858
5862
  }
5859
5863
 
5864
+ // src/utils/ImageQuality.ts
5865
+ function calculateSharpness(imageData) {
5866
+ const { width, height, data } = imageData;
5867
+ const gray = new Uint8Array(width * height);
5868
+ for (let i2 = 0; i2 < width * height; i2++) {
5869
+ const idx = i2 * 4;
5870
+ gray[i2] = Math.round(0.299 * data[idx] + 0.587 * data[idx + 1] + 0.114 * data[idx + 2]);
5871
+ }
5872
+ const laplacian = new Float32Array(width * height);
5873
+ let sum = 0;
5874
+ let sumSq = 0;
5875
+ let count = 0;
5876
+ for (let y2 = 1; y2 < height - 1; y2++) {
5877
+ for (let x2 = 1; x2 < width - 1; x2++) {
5878
+ const idx = y2 * width + x2;
5879
+ const val = -1 * gray[idx - width] + -1 * gray[idx - 1] + 4 * gray[idx] + -1 * gray[idx + 1] + -1 * gray[idx + width];
5880
+ laplacian[idx] = val;
5881
+ sum += val;
5882
+ sumSq += val * val;
5883
+ count++;
5884
+ }
5885
+ }
5886
+ const mean = sum / count;
5887
+ const variance = sumSq / count - mean * mean;
5888
+ return Math.abs(variance);
5889
+ }
5890
+ function detectGlare(imageData, threshold = 250) {
5891
+ const { data } = imageData;
5892
+ let overexposed = 0;
5893
+ for (let i2 = 0; i2 < data.length; i2 += 4) {
5894
+ const brightness = (data[i2] + data[i2 + 1] + data[i2 + 2]) / 3;
5895
+ if (brightness > threshold) {
5896
+ overexposed++;
5897
+ }
5898
+ }
5899
+ return overexposed / (data.length / 4);
5900
+ }
5901
+ function calculateContrast(imageData) {
5902
+ const { data } = imageData;
5903
+ let sum = 0;
5904
+ let sumSq = 0;
5905
+ const pixelCount = data.length / 4;
5906
+ for (let i2 = 0; i2 < data.length; i2 += 4) {
5907
+ const gray = 0.299 * data[i2] + 0.587 * data[i2 + 1] + 0.114 * data[i2 + 2];
5908
+ sum += gray;
5909
+ sumSq += gray * gray;
5910
+ }
5911
+ const mean = sum / pixelCount;
5912
+ const variance = sumSq / pixelCount - mean * mean;
5913
+ return Math.sqrt(variance);
5914
+ }
5915
+ function calculateBrightness(imageData) {
5916
+ const { data } = imageData;
5917
+ let sum = 0;
5918
+ for (let i2 = 0; i2 < data.length; i2 += 4) {
5919
+ sum += (data[i2] + data[i2 + 1] + data[i2 + 2]) / 3;
5920
+ }
5921
+ return sum / (data.length / 4);
5922
+ }
5923
+ function assessQuality(imageData) {
5924
+ const sharpness = calculateSharpness(imageData);
5925
+ const glare = detectGlare(imageData);
5926
+ const contrast = calculateContrast(imageData);
5927
+ const brightness = calculateBrightness(imageData);
5928
+ const isBlurry = sharpness < 80;
5929
+ const hasGlare = glare > 0.15;
5930
+ const isTooDark = brightness < 50;
5931
+ const isTooBright = brightness > 200;
5932
+ const lowContrast = contrast < 30;
5933
+ let score = 100;
5934
+ if (isBlurry) score -= 40;
5935
+ if (hasGlare) score -= 25;
5936
+ if (isTooDark) score -= 20;
5937
+ if (isTooBright) score -= 20;
5938
+ if (lowContrast) score -= 15;
5939
+ const metrics = {
5940
+ sharpness: Math.round(sharpness * 10) / 10,
5941
+ contrast: Math.round(contrast * 10) / 10,
5942
+ brightness: Math.round(brightness * 10) / 10,
5943
+ isBlurry,
5944
+ hasGlare,
5945
+ overallScore: Math.max(0, score)
5946
+ };
5947
+ const issues = [];
5948
+ const recommendations = [];
5949
+ if (isBlurry) {
5950
+ issues.push("Too blurry");
5951
+ recommendations.push("Hold camera steady");
5952
+ recommendations.push("Ensure good lighting");
5953
+ }
5954
+ if (hasGlare) {
5955
+ issues.push("Too much glare");
5956
+ recommendations.push("Avoid direct light on document");
5957
+ recommendations.push("Tilt document slightly");
5958
+ }
5959
+ if (isTooDark) {
5960
+ issues.push("Too dark");
5961
+ recommendations.push("Move to brighter area");
5962
+ recommendations.push("Turn on more lights");
5963
+ }
5964
+ if (isTooBright) {
5965
+ issues.push("Too bright");
5966
+ recommendations.push("Reduce lighting");
5967
+ recommendations.push("Move away from direct sun");
5968
+ }
5969
+ if (lowContrast) {
5970
+ issues.push("Low contrast");
5971
+ recommendations.push("Ensure even lighting");
5972
+ }
5973
+ const isValid = score >= 60;
5974
+ const message = issues.length > 0 ? issues.join(", ") : "Good quality";
5975
+ return {
5976
+ isValid,
5977
+ message,
5978
+ recommendations: recommendations.slice(0, 3),
5979
+ // Max 3 recommendations
5980
+ quality: metrics
5981
+ };
5982
+ }
5983
+
5860
5984
  // src/flow/icons.ts
5861
5985
  var PATHS = {
5862
5986
  shield: '<path d="M12 2l8 3v6c0 5-3.5 8.5-8 11-4.5-2.5-8-6-8-11V5l8-3z"/>',
@@ -6400,6 +6524,15 @@ var SmartComplySDK = (() => {
6400
6524
  sublabel.style.cssText = `color:${theme.textMuted};font-size:${TYPE.xs}px;text-align:center;line-height:1.6;`;
6401
6525
  sublabel.textContent = "All text must be readable and all four corners must be visible.";
6402
6526
  this.root.appendChild(sublabel);
6527
+ const qualityNotice = document.createElement("div");
6528
+ qualityNotice.style.cssText = `
6529
+ display:none;width:100%;padding:10px 14px;border-radius:${RADIUS.md}px;
6530
+ background:${theme.warning}15;border:1px solid ${theme.warning}35;
6531
+ color:${theme.text};font-size:12px;text-align:left;line-height:1.5;
6532
+ `;
6533
+ qualityNotice.setAttribute("role", "status");
6534
+ this.root.appendChild(qualityNotice);
6535
+ this._checkPhotoQuality(imageData, qualityNotice, theme);
6403
6536
  const btnRow = document.createElement("div");
6404
6537
  btnRow.style.cssText = "display:flex;gap:12px;width:100%;";
6405
6538
  const retakeBtn = this.createButton("Retake", theme, false);
@@ -6417,7 +6550,7 @@ var SmartComplySDK = (() => {
6417
6550
  if (img._blobUrl) {
6418
6551
  URL.revokeObjectURL(img._blobUrl);
6419
6552
  }
6420
- useBtn.textContent = "Processing...";
6553
+ useBtn.textContent = "Confirming photo…";
6421
6554
  useBtn.disabled = true;
6422
6555
  this.ensureUnderUploadLimit(imageData).then((c2) => this.handleCaptureComplete(c2, container, theme, documentType, viaCamera));
6423
6556
  });
@@ -6425,6 +6558,54 @@ var SmartComplySDK = (() => {
6425
6558
  btnRow.appendChild(useBtn);
6426
6559
  this.root.appendChild(btnRow);
6427
6560
  }
6561
+ /**
6562
+ * Runs the shared client-side quality heuristic (blur/glare/contrast/
6563
+ * brightness — see utils/ImageQuality.ts) against the just-captured photo
6564
+ * and, if it scores poorly, shows a plain-language nudge on the preview
6565
+ * screen. Fails silently on decode error — this is an advisory extra, not
6566
+ * a required step, so a browser that can't run it must not block capture.
6567
+ */
6568
+ async _checkPhotoQuality(imageData, noticeEl, theme) {
6569
+ try {
6570
+ const canvas = await this._blobToCanvas(imageData, 640);
6571
+ const ctx = canvas.getContext("2d");
6572
+ if (!ctx) return;
6573
+ const pixels = ctx.getImageData(0, 0, canvas.width, canvas.height);
6574
+ const result = assessQuality(pixels);
6575
+ if (noticeEl.isConnected && !result.isValid) {
6576
+ noticeEl.style.display = "block";
6577
+ noticeEl.textContent = "This photo might be hard to read — " + (result.recommendations[0] || "try retaking it in good lighting") + ".";
6578
+ }
6579
+ } catch {
6580
+ }
6581
+ }
6582
+ /** Decodes to a canvas, optionally downscaled to maxDim on the long edge (never upscales). */
6583
+ _blobToCanvas(source, maxDim) {
6584
+ return new Promise((resolve, reject) => {
6585
+ const url = URL.createObjectURL(source);
6586
+ const imgEl = document.createElement("img");
6587
+ imgEl.onload = () => {
6588
+ URL.revokeObjectURL(url);
6589
+ const { naturalWidth: w2, naturalHeight: h2 } = imgEl;
6590
+ const ratio = maxDim ? computeScaleRatio(w2, h2, maxDim, maxDim) : 1;
6591
+ const canvas = document.createElement("canvas");
6592
+ canvas.width = Math.round(w2 * ratio);
6593
+ canvas.height = Math.round(h2 * ratio);
6594
+ const ctx = canvas.getContext("2d");
6595
+ if (!ctx) {
6596
+ reject(new Error("Canvas unavailable"));
6597
+ return;
6598
+ }
6599
+ ctx.drawImage(imgEl, 0, 0, canvas.width, canvas.height);
6600
+ resolve(canvas);
6601
+ };
6602
+ imgEl.onerror = () => {
6603
+ URL.revokeObjectURL(url);
6604
+ reject(new Error("Image load failed"));
6605
+ };
6606
+ imgEl.src = url;
6607
+ });
6608
+ }
6428
6609
  handleCaptureComplete(imageData, container, theme, documentType, viaCamera, skipped = false) {
6429
6610
  const requiresBack = this.shouldCaptureBackSide(documentType);
6430
6611
  const wasCapturingFront = this.currentSide === "front";
@@ -6501,6 +6682,8 @@ var SmartComplySDK = (() => {
6501
6682
  if (!this.root) return;
6502
6683
  this.root.innerHTML = "";
6503
6684
  this.root.style.cssText = "display:flex;flex-direction:column;align-items:center;justify-content:center;gap:20px;padding:40px 20px;min-height:300px;";
6685
+ this.root.setAttribute("aria-live", "polite");
6686
+ this.root.setAttribute("role", "status");
6504
6687
  this.root.appendChild(renderSpinner(theme, { size: 80, icon: icon("document", 28) }));
6505
6688
  const loadingText = document.createElement("div");
6506
6689
  loadingText.style.cssText = `color:${theme.text};font-size:${TYPE.heading}px;font-weight:600;text-align:center;`;
@@ -6510,15 +6693,19 @@ var SmartComplySDK = (() => {
6510
6693
  showError(message) {
6511
6694
  if (!this.root) return;
6512
6695
  const errorContainer = document.createElement("div");
6696
+ errorContainer.setAttribute("role", "alert");
6513
6697
  errorContainer.style.cssText = `
6514
6698
  display:flex;align-items:center;gap:12px;padding:16px;
6515
6699
  background:#ff444415;border:1px solid #ff444430;border-radius:${RADIUS.md}px;
6516
6700
  color:#ff4444;font-size:14px;margin-top:16px;animation:shake 0.5s ease-in-out;
6517
6701
  `;
6518
- errorContainer.innerHTML = `
6519
- <span style="font-size:18px;">⚠️</span>
6520
- <span>${message}</span>
6521
- `;
6702
+ const errIcon = document.createElement("span");
6703
+ errIcon.style.fontSize = "18px";
6704
+ errIcon.textContent = "⚠️";
6705
+ const errText = document.createElement("span");
6706
+ errText.textContent = message;
6707
+ errorContainer.appendChild(errIcon);
6708
+ errorContainer.appendChild(errText);
6522
6709
  if (!document.head.querySelector("style[data-shake-animation]")) {
6523
6710
  const style = document.createElement("style");
6524
6711
  style.setAttribute("data-shake-animation", "true");
@@ -6700,6 +6887,24 @@ var SmartComplySDK = (() => {
6700
6887
  };
6701
6888
 
6702
6889
  // src/flow/SmartComplyFlow.ts
6890
+ var _INFRASTRUCTURE_ERROR_PATTERNS = [
6891
+ "camera connection was lost",
6892
+ "network error",
6893
+ "failed after all retries",
6894
+ "camera access is not supported",
6895
+ "camera access failed",
6896
+ "recording failed unexpectedly"
6897
+ ];
6898
+ function _isInfrastructureError(err) {
6899
+ const msg = String(err?.message || "").toLowerCase();
6900
+ return _INFRASTRUCTURE_ERROR_PATTERNS.some((p2) => msg.includes(p2));
6901
+ }
6902
+ function _userFacingLivenessError(err) {
6903
+ if (_isInfrastructureError(err)) {
6904
+ return "We lost connection during your scan — this wasn't anything you did. Please check your camera and internet connection, then try again.";
6905
+ }
6906
+ return err?.message || "We couldn't verify your face. Please try again.";
6907
+ }
6703
6908
  var COUNTRY_LABELS = {
6704
6909
  nigeria: "🇳🇬 Nigeria",
6705
6910
  global: "🌍 Other Countries",
@@ -6907,9 +7112,14 @@ var SmartComplySDK = (() => {
6907
7112
  this.isDestroyed = false;
6908
7113
  this.headerBackBtn = null;
6909
7114
  this._keydownHandler = null;
7115
+ this._visibilityHandler = null;
7116
+ this._hiddenAtMs = null;
7117
+ this._popstateHandler = null;
7118
+ this._historyGuardActive = false;
6910
7119
  // Retry safety — max 3 confirmation rejections or liveness failures per session
6911
7120
  this._confirmRetries = 0;
6912
7121
  this._livenessFailures = 0;
7122
+ this._livenessAttemptId = 0;
6913
7123
  // IVS result stored for confirm_identity screen
6914
7124
  this._ivsName = "";
6915
7125
  this._ivsGender = "";
@@ -6968,6 +7178,21 @@ var SmartComplySDK = (() => {
6968
7178
  document.removeEventListener("keydown", this._keydownHandler);
6969
7179
  this._keydownHandler = null;
6970
7180
  }
7181
+ if (this._visibilityHandler) {
7182
+ document.removeEventListener("visibilitychange", this._visibilityHandler);
7183
+ this._visibilityHandler = null;
7184
+ }
7185
+ if (this._popstateHandler) {
7186
+ window.removeEventListener("popstate", this._popstateHandler);
7187
+ this._popstateHandler = null;
7188
+ }
7189
+ if (this._historyGuardActive) {
7190
+ this._historyGuardActive = false;
7191
+ try {
7192
+ if (history.state?.scFlowGuard) history.back();
7193
+ } catch {
7194
+ }
7195
+ }
6971
7196
  this.docCapture?.destroy();
6972
7197
  if (this._prewarmedStream) {
6973
7198
  this._prewarmedStream.getTracks().forEach((t2) => t2.stop());
@@ -7084,6 +7309,43 @@ var SmartComplySDK = (() => {
7084
7309
  }
7085
7310
  };
7086
7311
  document.addEventListener("keydown", this._keydownHandler);
7312
+ const REBACKGROUND_RESTART_MS = 3e3;
7313
+ this._visibilityHandler = () => {
7314
+ if (this.isDestroyed) return;
7315
+ if (document.hidden) {
7316
+ this._hiddenAtMs = Date.now();
7317
+ return;
7318
+ }
7319
+ const hiddenAt = this._hiddenAtMs;
7320
+ this._hiddenAtMs = null;
7321
+ if (hiddenAt === null) return;
7322
+ const awayMs = Date.now() - hiddenAt;
7323
+ if (awayMs >= REBACKGROUND_RESTART_MS && this.currentStep === "liveness") {
7324
+ this.showStep("liveness");
7325
+ }
7326
+ };
7327
+ document.addEventListener("visibilitychange", this._visibilityHandler);
7328
+ this._historyGuardActive = true;
7329
+ try {
7330
+ history.pushState({ scFlowGuard: true }, "");
7331
+ } catch {
7332
+ this._historyGuardActive = false;
7333
+ }
7334
+ this._popstateHandler = () => {
7335
+ if (this.isDestroyed || !this._historyGuardActive) return;
7336
+ const backAction = this._getBackAction(this.currentStep);
7337
+ if (backAction) {
7338
+ backAction();
7339
+ } else {
7340
+ this.close();
7341
+ return;
7342
+ }
7343
+ try {
7344
+ history.pushState({ scFlowGuard: true }, "");
7345
+ } catch {
7346
+ }
7347
+ };
7348
+ window.addEventListener("popstate", this._popstateHandler);
7087
7349
  }
7088
7350
  async initialize() {
7089
7351
  this.showStep("loading");
@@ -7254,7 +7516,7 @@ var SmartComplySDK = (() => {
7254
7516
  topRow.appendChild(badge);
7255
7517
  container.appendChild(topRow);
7256
7518
  const titleWrap = document.createElement("div");
7257
- titleWrap.style.cssText = "display:flex;flex-direction:column;gap:5px;";
7519
+ titleWrap.style.cssText = "display:flex;flex-direction:column;align-items:center;gap:5px;width:100%;";
7258
7520
  const title = document.createElement("h2");
7259
7521
  title.style.cssText = `
7260
7522
  color:${this.theme.text};font-size:${TYPE.heading}px;font-weight:700;
@@ -7266,7 +7528,7 @@ var SmartComplySDK = (() => {
7266
7528
  const desc = document.createElement("p");
7267
7529
  desc.style.cssText = `
7268
7530
  color:${this.theme.textSecondary};font-size:13px;line-height:1.5;
7269
- margin:0;max-width:300px;
7531
+ margin:0 auto;max-width:300px;text-align:center;
7270
7532
  `;
7271
7533
  desc.textContent = isDocFlow ? "You'll need your document and camera access. This takes about 2 minutes." : "You'll need your ID number and camera access. This takes about 2 minutes.";
7272
7534
  titleWrap.appendChild(desc);
@@ -7324,10 +7586,10 @@ var SmartComplySDK = (() => {
7324
7586
  container.appendChild(btn);
7325
7587
  const trust = document.createElement("div");
7326
7588
  trust.style.cssText = `
7327
- display:flex;align-items:center;gap:6px;
7328
- color:${this.theme.textMuted};font-size:10.5px;
7589
+ display:flex;align-items:flex-start;gap:6px;text-align:left;
7590
+ color:${this.theme.textMuted};font-size:10.5px;line-height:1.5;
7329
7591
  `;
7330
- trust.textContent = "🔒 End-to-end encrypted";
7592
+ trust.innerHTML = `<span style="flex-shrink:0;">🔒</span><span>Your photo and camera scan are used only to verify it's really you, and are kept encrypted.</span>`;
7331
7593
  container.appendChild(trust);
7332
7594
  }
7333
7595
  // ── Country Select ──────────────────────────────────────────────
@@ -7802,6 +8064,10 @@ var SmartComplySDK = (() => {
7802
8064
  this._renderRetryLimitReached(container);
7803
8065
  return;
7804
8066
  }
8067
+ if (!this._ocrName && !this._ocrDocNumber && !this._ocrExpiry) {
8068
+ this._renderOcrTimeout(container);
8069
+ return;
8070
+ }
7805
8071
  if (this._documentViaCamera) this._prewarmCamera();
7806
8072
  const stepBadge = document.createElement("div");
7807
8073
  stepBadge.style.cssText = `
@@ -7952,11 +8218,8 @@ var SmartComplySDK = (() => {
7952
8218
  if (polls < MAX_POLLS && !this.isDestroyed) {
7953
8219
  this._ocrPollTimer = setTimeout(poll, 2e3);
7954
8220
  } else if (!this.isDestroyed) {
7955
- statusEl.textContent = "Taking longer than usual…";
7956
- subEl.textContent = "We'll continue — you can confirm your details on the next screen.";
7957
- this._ocrPollTimer = setTimeout(() => {
7958
- if (!this.isDestroyed) this.showStep("document_confirm");
7959
- }, 2e3);
8221
+ container.innerHTML = "";
8222
+ this._renderOcrTimeout(container);
7960
8223
  }
7961
8224
  } catch {
7962
8225
  if (polls < MAX_POLLS && !this.isDestroyed) {
@@ -8021,6 +8284,35 @@ var SmartComplySDK = (() => {
8021
8284
  });
8022
8285
  container.appendChild(retryBtn);
8023
8286
  }
8287
+ /**
8288
+ * OCR ran to completion but never produced a single usable field within
8289
+ * the poll window — do not let the flow reach liveness with no verified
8290
+ * identity data. Distinct from _renderOcrQualityFail (a bad photo) since
8291
+ * this covers a genuinely stuck/slow OCR pass instead of a rejected image.
8292
+ */
8293
+ _renderOcrTimeout(container) {
8294
+ container.style.cssText += "display:flex;flex-direction:column;align-items:center;text-align:center;gap:14px;padding:20px 0;";
8295
+ const icon2 = document.createElement("div");
8296
+ icon2.style.cssText = `width:72px;height:72px;border-radius:50%;background:${this.theme.errorBg};border:3px solid ${this.theme.error};display:flex;align-items:center;justify-content:center;font-size:28px;`;
8297
+ icon2.textContent = "⏱️";
8298
+ container.appendChild(icon2);
8299
+ const t2 = document.createElement("h2");
8300
+ t2.style.cssText = `color:${this.theme.text};font-size:${TYPE.heading}px;font-weight:700;margin:0;`;
8301
+ t2.textContent = "Couldn't Read Your Document";
8302
+ container.appendChild(t2);
8303
+ const d2 = document.createElement("p");
8304
+ d2.style.cssText = `color:${this.theme.textSecondary};font-size:13px;margin:0;max-width:280px;line-height:1.6;`;
8305
+ d2.textContent = "We weren't able to read any details from your document. Please retake the photo in good lighting, with all four corners visible.";
8306
+ container.appendChild(d2);
8307
+ const retryBtn = this.createPrimaryButton("Retake photo");
8308
+ retryBtn.addEventListener("click", () => {
8309
+ this.livenessEntryId = null;
8310
+ this.documentBlob = null;
8311
+ this.documentBackBlob = null;
8312
+ this.showStep("document_capture");
8313
+ });
8314
+ container.appendChild(retryBtn);
8315
+ }
8024
8316
  // ── W11: You're all done (end-user final screen) ─────────────────
8025
8317
  renderDone(container) {
8026
8318
  container.style.cssText += "display:flex;flex-direction:column;align-items:center;text-align:center;gap:16px;padding:24px 0;";
@@ -8240,6 +8532,7 @@ var SmartComplySDK = (() => {
8240
8532
  this._renderRetryLimitReached(container);
8241
8533
  return;
8242
8534
  }
8535
+ const attemptId = ++this._livenessAttemptId;
8243
8536
  const title = this.createStepTitle("Face verification");
8244
8537
  container.appendChild(title);
8245
8538
  const desc = document.createElement("div");
@@ -8281,9 +8574,10 @@ var SmartComplySDK = (() => {
8281
8574
  warning: this.theme.warning
8282
8575
  }
8283
8576
  },
8284
- ["BLINK"],
8577
+ [...DEFAULT_CHALLENGE_ACTIONS],
8285
8578
  this.livenessEntryId || void 0
8286
8579
  ).then((result) => {
8580
+ if (attemptId !== this._livenessAttemptId) return;
8287
8581
  if (result.status === "failed") {
8288
8582
  this._livenessFailures++;
8289
8583
  }
@@ -8292,8 +8586,11 @@ var SmartComplySDK = (() => {
8292
8586
  }
8293
8587
  this.showStep("done");
8294
8588
  }).catch((err) => {
8295
- this._livenessFailures++;
8296
- this.showErrorStep(err.message || "Liveness verification failed. Please try again.");
8589
+ if (attemptId !== this._livenessAttemptId) return;
8590
+ if (!_isInfrastructureError(err)) {
8591
+ this._livenessFailures++;
8592
+ }
8593
+ this.showErrorStep(_userFacingLivenessError(err));
8297
8594
  });
8298
8595
  }
8299
8596
  // ── Error ───────────────────────────────────────────────────────
@@ -8449,9 +8746,18 @@ var SmartComplySDK = (() => {
8449
8746
  const powered = document.createElement("span");
8450
8747
  powered.style.cssText = `font-size:11px;color:${this.theme.textMuted};`;
8451
8748
  powered.textContent = "Powered by ";
8452
- const brand = document.createElement("span");
8453
- brand.style.cssText = `font-size:11px;font-weight:700;color:${this.theme.primary};`;
8749
+ const brand = document.createElement("a");
8750
+ brand.href = "https://adhere.smartcomply.com/";
8751
+ brand.target = "_blank";
8752
+ brand.rel = "noopener noreferrer";
8753
+ brand.style.cssText = `font-size:11px;font-weight:700;color:${this.theme.primary};text-decoration:none;`;
8454
8754
  brand.textContent = "Adhere";
8755
+ brand.addEventListener("mouseenter", () => {
8756
+ brand.style.textDecoration = "underline";
8757
+ });
8758
+ brand.addEventListener("mouseleave", () => {
8759
+ brand.style.textDecoration = "none";
8760
+ });
8455
8761
  footer.appendChild(powered);
8456
8762
  footer.appendChild(brand);
8457
8763
  return footer;
@@ -8488,18 +8794,19 @@ var SmartComplySDK = (() => {
8488
8794
  });
8489
8795
  card.addEventListener("click", onClick);
8490
8796
  const textWrap = document.createElement("div");
8797
+ textWrap.style.cssText = "flex:1 1 auto;min-width:0;";
8491
8798
  const titleEl = document.createElement("div");
8492
- titleEl.style.cssText = `color:${this.theme.text};font-size:14px;font-weight:600;`;
8799
+ titleEl.style.cssText = `color:${this.theme.text};font-size:14px;font-weight:600;line-height:1.35;`;
8493
8800
  titleEl.textContent = title;
8494
8801
  textWrap.appendChild(titleEl);
8495
8802
  if (subtitle) {
8496
8803
  const subEl = document.createElement("div");
8497
- subEl.style.cssText = `color:${this.theme.textMuted};font-size:12px;margin-top:2px;`;
8804
+ subEl.style.cssText = `color:${this.theme.textMuted};font-size:12px;line-height:1.4;margin-top:2px;`;
8498
8805
  subEl.textContent = subtitle;
8499
8806
  textWrap.appendChild(subEl);
8500
8807
  }
8501
8808
  const arrow = document.createElement("span");
8502
- arrow.style.cssText = `color:${this.theme.textMuted};font-size:18px;flex-shrink:0;`;
8809
+ arrow.style.cssText = `color:${this.theme.textMuted};font-size:18px;flex-shrink:0;margin-left:12px;line-height:1;`;
8503
8810
  arrow.textContent = "→";
8504
8811
  card.appendChild(textWrap);
8505
8812
  card.appendChild(arrow);
@@ -8565,6 +8872,30 @@ var SmartComplySDK = (() => {
8565
8872
  const allFields = Object.values(channels).flat().flatMap((ch) => ch.fields || []);
8566
8873
  return allFields.some((f2) => isUploadFieldType(f2.type));
8567
8874
  }
8875
+ /**
8876
+ * The "go back one step" target for a given step, if that step is
8877
+ * navigable — shared by the header back button and the browser
8878
+ * back-button (popstate) handler in mount() so both agree on the same
8879
+ * step order.
8880
+ */
8881
+ _getBackAction(step) {
8882
+ const countries = Object.keys(this.sdkConfig?.channels || {});
8883
+ const backTargets = {
8884
+ id_type: () => this.showStep(countries.length > 1 ? "country" : "welcome"),
8885
+ country: () => this.showStep("welcome"),
8886
+ id_input: () => this.showStep("id_type"),
8887
+ confirm_identity: () => this.showStep("id_input"),
8888
+ document_capture: () => {
8889
+ this.docCapture?.destroy();
8890
+ this.showStep("id_type");
8891
+ },
8892
+ document_confirm: () => {
8893
+ this.docCapture?.destroy();
8894
+ this.showStep("document_capture");
8895
+ }
8896
+ };
8897
+ return backTargets[step];
8898
+ }
8568
8899
  updateProgress() {
8569
8900
  const isDocFlow = this.isDocumentFlow();
8570
8901
  const countries = Object.keys(this.sdkConfig?.channels || {});
@@ -8602,21 +8933,7 @@ var SmartComplySDK = (() => {
8602
8933
  if (dots[1]) dots[1].style.background = stage2Active ? this.theme.primary : this.theme.border;
8603
8934
  }
8604
8935
  if (this.headerBackBtn) {
8605
- const backTargets = {
8606
- id_type: () => this.showStep(countries.length > 1 ? "country" : "welcome"),
8607
- country: () => this.showStep("welcome"),
8608
- id_input: () => this.showStep("id_type"),
8609
- confirm_identity: () => this.showStep("id_input"),
8610
- document_capture: () => {
8611
- this.docCapture?.destroy();
8612
- this.showStep("id_type");
8613
- },
8614
- document_confirm: () => {
8615
- this.docCapture?.destroy();
8616
- this.showStep("document_capture");
8617
- }
8618
- };
8619
- const backAction = backTargets[this.currentStep];
8936
+ const backAction = this._getBackAction(this.currentStep);
8620
8937
  if (backAction) {
8621
8938
  this.headerBackBtn.style.display = "flex";
8622
8939
  const newBtn = this.headerBackBtn.cloneNode(true);