smartcomply-web-sdk 1.0.68 → 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 (33) 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 +82 -4
  4. package/dist/esm/flow/DocumentCapture.js.map +1 -1
  5. package/dist/esm/flow/SmartComplyFlow.d.ts +12 -0
  6. package/dist/esm/flow/SmartComplyFlow.d.ts.map +1 -1
  7. package/dist/esm/flow/SmartComplyFlow.js +158 -17
  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/utils/ImageQuality.d.ts +12 -1
  13. package/dist/esm/utils/ImageQuality.d.ts.map +1 -1
  14. package/dist/esm/utils/ImageQuality.js +12 -1
  15. package/dist/esm/utils/ImageQuality.js.map +1 -1
  16. package/dist/flow/DocumentCapture.d.ts +10 -0
  17. package/dist/flow/DocumentCapture.d.ts.map +1 -1
  18. package/dist/flow/DocumentCapture.js +82 -4
  19. package/dist/flow/DocumentCapture.js.map +1 -1
  20. package/dist/flow/SmartComplyFlow.d.ts +12 -0
  21. package/dist/flow/SmartComplyFlow.d.ts.map +1 -1
  22. package/dist/flow/SmartComplyFlow.js +158 -17
  23. package/dist/flow/SmartComplyFlow.js.map +1 -1
  24. package/dist/modules/liveness/LivenessUI.d.ts.map +1 -1
  25. package/dist/modules/liveness/LivenessUI.js +13 -3
  26. package/dist/modules/liveness/LivenessUI.js.map +1 -1
  27. package/dist/smartcomply.browser.js +305 -28
  28. package/dist/smartcomply.browser.js.map +4 -4
  29. package/dist/utils/ImageQuality.d.ts +12 -1
  30. package/dist/utils/ImageQuality.d.ts.map +1 -1
  31. package/dist/utils/ImageQuality.js +12 -1
  32. package/dist/utils/ImageQuality.js.map +1 -1
  33. package/package.json +1 -1
@@ -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
  `;
@@ -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);
@@ -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");
@@ -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 ──────────────────────────────────────────────
@@ -8270,6 +8532,7 @@ var SmartComplySDK = (() => {
8270
8532
  this._renderRetryLimitReached(container);
8271
8533
  return;
8272
8534
  }
8535
+ const attemptId = ++this._livenessAttemptId;
8273
8536
  const title = this.createStepTitle("Face verification");
8274
8537
  container.appendChild(title);
8275
8538
  const desc = document.createElement("div");
@@ -8311,9 +8574,10 @@ var SmartComplySDK = (() => {
8311
8574
  warning: this.theme.warning
8312
8575
  }
8313
8576
  },
8314
- ["BLINK"],
8577
+ [...DEFAULT_CHALLENGE_ACTIONS],
8315
8578
  this.livenessEntryId || void 0
8316
8579
  ).then((result) => {
8580
+ if (attemptId !== this._livenessAttemptId) return;
8317
8581
  if (result.status === "failed") {
8318
8582
  this._livenessFailures++;
8319
8583
  }
@@ -8322,8 +8586,11 @@ var SmartComplySDK = (() => {
8322
8586
  }
8323
8587
  this.showStep("done");
8324
8588
  }).catch((err) => {
8325
- this._livenessFailures++;
8326
- 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));
8327
8594
  });
8328
8595
  }
8329
8596
  // ── Error ───────────────────────────────────────────────────────
@@ -8605,6 +8872,30 @@ var SmartComplySDK = (() => {
8605
8872
  const allFields = Object.values(channels).flat().flatMap((ch) => ch.fields || []);
8606
8873
  return allFields.some((f2) => isUploadFieldType(f2.type));
8607
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
+ }
8608
8899
  updateProgress() {
8609
8900
  const isDocFlow = this.isDocumentFlow();
8610
8901
  const countries = Object.keys(this.sdkConfig?.channels || {});
@@ -8642,21 +8933,7 @@ var SmartComplySDK = (() => {
8642
8933
  if (dots[1]) dots[1].style.background = stage2Active ? this.theme.primary : this.theme.border;
8643
8934
  }
8644
8935
  if (this.headerBackBtn) {
8645
- const backTargets = {
8646
- id_type: () => this.showStep(countries.length > 1 ? "country" : "welcome"),
8647
- country: () => this.showStep("welcome"),
8648
- id_input: () => this.showStep("id_type"),
8649
- confirm_identity: () => this.showStep("id_input"),
8650
- document_capture: () => {
8651
- this.docCapture?.destroy();
8652
- this.showStep("id_type");
8653
- },
8654
- document_confirm: () => {
8655
- this.docCapture?.destroy();
8656
- this.showStep("document_capture");
8657
- }
8658
- };
8659
- const backAction = backTargets[this.currentStep];
8936
+ const backAction = this._getBackAction(this.currentStep);
8660
8937
  if (backAction) {
8661
8938
  this.headerBackBtn.style.display = "flex";
8662
8939
  const newBtn = this.headerBackBtn.cloneNode(true);