smartcomply-web-sdk 1.0.80 → 1.0.81

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 (37) hide show
  1. package/dist/camera/ActionDetector.d.ts.map +1 -1
  2. package/dist/camera/ActionDetector.js +7 -5
  3. package/dist/camera/ActionDetector.js.map +1 -1
  4. package/dist/client/Smartcomply.d.ts +1 -1
  5. package/dist/client/Smartcomply.js +1 -1
  6. package/dist/esm/camera/ActionDetector.d.ts.map +1 -1
  7. package/dist/esm/camera/ActionDetector.js +7 -5
  8. package/dist/esm/camera/ActionDetector.js.map +1 -1
  9. package/dist/esm/client/Smartcomply.d.ts +1 -1
  10. package/dist/esm/client/Smartcomply.js +1 -1
  11. package/dist/esm/flow/DocumentCapture.d.ts +18 -0
  12. package/dist/esm/flow/DocumentCapture.d.ts.map +1 -1
  13. package/dist/esm/flow/DocumentCapture.js +118 -4
  14. package/dist/esm/flow/DocumentCapture.js.map +1 -1
  15. package/dist/esm/flow/SmartComplyFlow.d.ts +10 -0
  16. package/dist/esm/flow/SmartComplyFlow.d.ts.map +1 -1
  17. package/dist/esm/flow/SmartComplyFlow.js +53 -0
  18. package/dist/esm/flow/SmartComplyFlow.js.map +1 -1
  19. package/dist/esm/utils/ImageQuality.d.ts +38 -0
  20. package/dist/esm/utils/ImageQuality.d.ts.map +1 -1
  21. package/dist/esm/utils/ImageQuality.js +98 -0
  22. package/dist/esm/utils/ImageQuality.js.map +1 -1
  23. package/dist/flow/DocumentCapture.d.ts +18 -0
  24. package/dist/flow/DocumentCapture.d.ts.map +1 -1
  25. package/dist/flow/DocumentCapture.js +117 -3
  26. package/dist/flow/DocumentCapture.js.map +1 -1
  27. package/dist/flow/SmartComplyFlow.d.ts +10 -0
  28. package/dist/flow/SmartComplyFlow.d.ts.map +1 -1
  29. package/dist/flow/SmartComplyFlow.js +53 -0
  30. package/dist/flow/SmartComplyFlow.js.map +1 -1
  31. package/dist/smartcomply.browser.js +211 -3
  32. package/dist/smartcomply.browser.js.map +3 -3
  33. package/dist/utils/ImageQuality.d.ts +38 -0
  34. package/dist/utils/ImageQuality.d.ts.map +1 -1
  35. package/dist/utils/ImageQuality.js +99 -0
  36. package/dist/utils/ImageQuality.js.map +1 -1
  37. package/package.json +1 -1
@@ -4665,7 +4665,7 @@ var SmartComplySDK = (() => {
4665
4665
  faceDetected,
4666
4666
  blinkDetected: this.blinkDetected,
4667
4667
  motionDetected: this.motionDetected,
4668
- livenessConfirmed: this.blinkDetected || this.motionDetected
4668
+ livenessConfirmed: this.blinkDetected && this.motionDetected
4669
4669
  };
4670
4670
  }
4671
4671
  getHeadYaw(matrix) {
@@ -5780,7 +5780,7 @@ var SmartComplySDK = (() => {
5780
5780
  * CDN tag they installed with (e.g. @1, which floats to the latest
5781
5781
  * matching release) actually resolved to the version they expect.
5782
5782
  */
5783
- _SmartComply.VERSION = "1.0.80";
5783
+ _SmartComply.VERSION = "1.0.81";
5784
5784
  var SmartComply = _SmartComply;
5785
5785
 
5786
5786
  // src/flow/theme.ts
@@ -6040,6 +6040,67 @@ var SmartComplySDK = (() => {
6040
6040
  quality: metrics
6041
6041
  };
6042
6042
  }
6043
+ function assessFraming(imageData, guideBoxPct) {
6044
+ const { width: W2, height: H2, data } = imageData;
6045
+ const gray = new Uint8Array(W2 * H2);
6046
+ for (let i2 = 0; i2 < W2 * H2; i2++) {
6047
+ const idx = i2 * 4;
6048
+ gray[i2] = Math.round(0.299 * data[idx] + 0.587 * data[idx + 1] + 0.114 * data[idx + 2]);
6049
+ }
6050
+ const gx0 = Math.round(guideBoxPct.x * W2);
6051
+ const gy0 = Math.round(guideBoxPct.y * H2);
6052
+ const gx1 = Math.round((guideBoxPct.x + guideBoxPct.width) * W2);
6053
+ const gy1 = Math.round((guideBoxPct.y + guideBoxPct.height) * H2);
6054
+ const edgeAt = (x2, y2) => {
6055
+ if (x2 < 1 || y2 < 1 || x2 >= W2 - 1 || y2 >= H2 - 1) return 0;
6056
+ const gxVal = gray[y2 * W2 + x2 + 1] - gray[y2 * W2 + x2 - 1];
6057
+ const gyVal = gray[(y2 + 1) * W2 + x2] - gray[(y2 - 1) * W2 + x2];
6058
+ return Math.sqrt(gxVal * gxVal + gyVal * gyVal);
6059
+ };
6060
+ const EDGE_THRESHOLD = 40;
6061
+ const BORDER_BAND = Math.max(4, Math.round(Math.min(W2, H2) * 0.015));
6062
+ let borderPoints = 0;
6063
+ let borderHits = 0;
6064
+ const SAMPLES_PER_SIDE = 24;
6065
+ const sampleBand = (fixed, isX, start, end) => {
6066
+ for (let s2 = 0; s2 < SAMPLES_PER_SIDE; s2++) {
6067
+ const t2 = start + (end - start) * s2 / (SAMPLES_PER_SIDE - 1);
6068
+ let bestEdge = 0;
6069
+ for (let b2 = -BORDER_BAND; b2 <= BORDER_BAND; b2++) {
6070
+ const x2 = isX ? fixed + b2 : Math.round(t2);
6071
+ const y2 = isX ? Math.round(t2) : fixed + b2;
6072
+ bestEdge = Math.max(bestEdge, edgeAt(x2, y2));
6073
+ }
6074
+ borderPoints++;
6075
+ if (bestEdge >= EDGE_THRESHOLD) borderHits++;
6076
+ }
6077
+ };
6078
+ sampleBand(gy0, false, gx0, gx1);
6079
+ sampleBand(gy1, false, gx0, gx1);
6080
+ sampleBand(gx0, true, gy0, gy1);
6081
+ sampleBand(gx1, true, gy0, gy1);
6082
+ const borderEdgeCoverage = borderPoints > 0 ? borderHits / borderPoints : 0;
6083
+ let sum = 0, sumSq = 0, n2 = 0;
6084
+ const GRID = 12;
6085
+ const insetX = Math.round((gx1 - gx0) * 0.1);
6086
+ const insetY = Math.round((gy1 - gy0) * 0.1);
6087
+ for (let iy = 0; iy < GRID; iy++) {
6088
+ for (let ix = 0; ix < GRID; ix++) {
6089
+ const x2 = gx0 + insetX + Math.round((gx1 - gx0 - 2 * insetX) * ix / (GRID - 1));
6090
+ const y2 = gy0 + insetY + Math.round((gy1 - gy0 - 2 * insetY) * iy / (GRID - 1));
6091
+ if (x2 < 0 || y2 < 0 || x2 >= W2 || y2 >= H2) continue;
6092
+ const v2 = gray[y2 * W2 + x2];
6093
+ sum += v2;
6094
+ sumSq += v2 * v2;
6095
+ n2++;
6096
+ }
6097
+ }
6098
+ const mean = n2 > 0 ? sum / n2 : 0;
6099
+ const variance = n2 > 0 ? sumSq / n2 - mean * mean : 0;
6100
+ const interiorVariance = Math.min(1, Math.sqrt(Math.max(0, variance)) / 35);
6101
+ const isFramed = borderEdgeCoverage >= 0.55 && interiorVariance >= 0.25;
6102
+ return { isFramed, borderEdgeCoverage, interiorVariance };
6103
+ }
6043
6104
 
6044
6105
  // src/flow/icons.ts
6045
6106
  var PATHS = {
@@ -6130,6 +6191,9 @@ var SmartComplySDK = (() => {
6130
6191
  this.backBlob = null;
6131
6192
  this.currentSide = "front";
6132
6193
  this.requiresBackOverride = void 0;
6194
+ this.framingTimer = null;
6195
+ this.framingStableCount = 0;
6196
+ this.framingAutoTriggered = false;
6133
6197
  }
6134
6198
  /**
6135
6199
  * Mount the document capture UI and return a promise that resolves
@@ -6414,6 +6478,13 @@ var SmartComplySDK = (() => {
6414
6478
  padding:14px 16px 32px;text-align:center;
6415
6479
  `;
6416
6480
  header.innerHTML = `<div style="color:#fff;font-size:${TYPE.sm}px;font-weight:600;">Align your ${this.getDocumentLabel(documentType)} within the frame</div>`;
6481
+ const framingStatus = document.createElement("div");
6482
+ framingStatus.style.cssText = `
6483
+ margin-top:6px;font-size:${TYPE.xs}px;font-weight:600;
6484
+ color:rgba(255,255,255,0.7);transition:color 0.2s ease;
6485
+ `;
6486
+ framingStatus.textContent = "Fit the document inside the frame";
6487
+ header.appendChild(framingStatus);
6417
6488
  wrapper.appendChild(header);
6418
6489
  const footer = document.createElement("div");
6419
6490
  footer.style.cssText = `
@@ -6446,10 +6517,19 @@ var SmartComplySDK = (() => {
6446
6517
  inner.style.transform = "translate(-50%,-50%) scale(1)";
6447
6518
  });
6448
6519
  captureBtn.addEventListener("click", () => {
6520
+ this.framingAutoTriggered = true;
6449
6521
  this.captureFrame(container, theme, documentType);
6450
6522
  });
6451
6523
  container.appendChild(captureBtn);
6452
- this.openCamera();
6524
+ this.openCamera().then(() => {
6525
+ if (this.videoEl) {
6526
+ this.videoEl.addEventListener(
6527
+ "loadedmetadata",
6528
+ () => this._startFramingLoop(container, theme, documentType, framingStatus),
6529
+ { once: true }
6530
+ );
6531
+ }
6532
+ });
6453
6533
  }
6454
6534
  /**
6455
6535
  * Matches the video box's aspect-ratio CSS to the stream's actual
@@ -6484,6 +6564,90 @@ var SmartComplySDK = (() => {
6484
6564
  this.orientationHandler = null;
6485
6565
  }
6486
6566
  }
6567
+ /**
6568
+ * Auto-capture-on-stability — the same pattern the liveness module already
6569
+ * uses for face-lock (see liveness.ts's checkFace), applied to documents:
6570
+ * poll the raw video frame on an interval, score it against the guide box
6571
+ * with assessFraming() (see utils/ImageQuality.ts), and fire the shutter
6572
+ * automatically once several consecutive samples score as "framed". This
6573
+ * is industry-standard for document capture (Persona/Onfido/Jumio-style
6574
+ * SDKs all auto-capture rather than trust a manual tap against a static
6575
+ * guide overlay) — the guide box alone never blocked a badly cropped tap.
6576
+ *
6577
+ * The manual capture button still works throughout and always wins
6578
+ * (framingAutoTriggered guards against a race between the two).
6579
+ */
6580
+ _startFramingLoop(container, theme, documentType, statusEl) {
6581
+ if (!this.videoEl) return;
6582
+ this.framingStableCount = 0;
6583
+ this.framingAutoTriggered = false;
6584
+ const STABLE_REQUIRED = 3;
6585
+ const SAMPLE_INTERVAL_MS = 350;
6586
+ const MAX_WAIT_MS = 12e3;
6587
+ const startTime = performance.now();
6588
+ const boxWidthPct = 0.84;
6589
+ const boxAspect = 16 / 10;
6590
+ const tick = () => {
6591
+ if (!this.videoEl || this.framingAutoTriggered) return;
6592
+ if (performance.now() - startTime >= MAX_WAIT_MS) {
6593
+ statusEl.textContent = "Fit the document inside the frame";
6594
+ return;
6595
+ }
6596
+ const w2 = this.videoEl.videoWidth;
6597
+ const h2 = this.videoEl.videoHeight;
6598
+ if (!w2 || !h2 || this.videoEl.readyState < 2) {
6599
+ this.framingTimer = setTimeout(tick, SAMPLE_INTERVAL_MS);
6600
+ return;
6601
+ }
6602
+ try {
6603
+ const boxWidthPx = w2 * boxWidthPct;
6604
+ const boxHeightPx = boxWidthPx / boxAspect;
6605
+ const boxXPct = (1 - boxWidthPct) / 2;
6606
+ const boxHeightPct = boxHeightPx / h2;
6607
+ const boxYPct = (1 - boxHeightPct) / 2;
6608
+ const canvas = document.createElement("canvas");
6609
+ const scale = Math.min(1, 480 / w2);
6610
+ canvas.width = Math.round(w2 * scale);
6611
+ canvas.height = Math.round(h2 * scale);
6612
+ const ctx = canvas.getContext("2d", { willReadFrequently: true });
6613
+ if (!ctx) {
6614
+ this.framingTimer = setTimeout(tick, SAMPLE_INTERVAL_MS);
6615
+ return;
6616
+ }
6617
+ ctx.drawImage(this.videoEl, 0, 0, canvas.width, canvas.height);
6618
+ const pixels = ctx.getImageData(0, 0, canvas.width, canvas.height);
6619
+ const result = assessFraming(pixels, {
6620
+ x: boxXPct,
6621
+ y: boxYPct,
6622
+ width: boxWidthPct,
6623
+ height: boxHeightPct
6624
+ });
6625
+ if (result.isFramed) {
6626
+ this.framingStableCount++;
6627
+ statusEl.style.color = theme.primary;
6628
+ statusEl.textContent = this.framingStableCount >= STABLE_REQUIRED ? "Capturing…" : "Hold steady…";
6629
+ } else {
6630
+ this.framingStableCount = 0;
6631
+ statusEl.style.color = "rgba(255,255,255,0.7)";
6632
+ statusEl.textContent = result.interiorVariance < 0.25 ? "Move closer — fill the frame with your document" : "Align all 4 corners within the guide";
6633
+ }
6634
+ if (this.framingStableCount >= STABLE_REQUIRED) {
6635
+ this.framingAutoTriggered = true;
6636
+ this.captureFrame(container, theme, documentType);
6637
+ return;
6638
+ }
6639
+ } catch {
6640
+ }
6641
+ this.framingTimer = setTimeout(tick, SAMPLE_INTERVAL_MS);
6642
+ };
6643
+ this.framingTimer = setTimeout(tick, SAMPLE_INTERVAL_MS);
6644
+ }
6645
+ _stopFramingLoop() {
6646
+ if (this.framingTimer !== null) {
6647
+ clearTimeout(this.framingTimer);
6648
+ this.framingTimer = null;
6649
+ }
6650
+ }
6487
6651
  async openCamera() {
6488
6652
  if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
6489
6653
  this.rejectCapture?.(new Error("Camera access is not supported in this browser. Please use a modern browser like Chrome, Firefox, or Safari."));
@@ -6818,6 +6982,7 @@ var SmartComplySDK = (() => {
6818
6982
  if (input) input.click();
6819
6983
  }
6820
6984
  stopCamera() {
6985
+ this._stopFramingLoop();
6821
6986
  this._unwatchOrientation();
6822
6987
  this.cameraManager.stop();
6823
6988
  if (this.videoEl) {
@@ -8403,6 +8568,11 @@ var SmartComplySDK = (() => {
8403
8568
  this._renderOcrQualityFail(container, result.ocr_rejection_reason);
8404
8569
  return;
8405
8570
  }
8571
+ if (result.status === "failed") {
8572
+ container.innerHTML = "";
8573
+ this._renderDocumentVerificationFailed(container, result.failure_reason);
8574
+ return;
8575
+ }
8406
8576
  const ocrDone = result.ocr_status === "done" || result.ocr_status === "ocr_complete" || result.ocr_name || result.ocr_document_number || result.ocr_expiry_date;
8407
8577
  if (ocrDone) {
8408
8578
  this._ocrName = result.ocr_name || "";
@@ -8495,6 +8665,44 @@ var SmartComplySDK = (() => {
8495
8665
  });
8496
8666
  container.appendChild(retryBtn);
8497
8667
  }
8668
+ /**
8669
+ * The document passed OCR's own quality gate (it was readable) but failed
8670
+ * the government-registry re-check — the ID number either doesn't exist
8671
+ * in the registry, or belongs to a different person than the name on the
8672
+ * document. Distinct from _renderOcrQualityFail: the photo itself may be
8673
+ * perfectly clear, so "retake photo" framing would be misleading — the
8674
+ * fix is a correct, uncropped photo of the actual document, not just a
8675
+ * clearer one.
8676
+ */
8677
+ _renderDocumentVerificationFailed(container, failureReason) {
8678
+ container.style.cssText += "display:flex;flex-direction:column;align-items:center;text-align:center;gap:14px;padding:20px 0;";
8679
+ const icon2 = document.createElement("div");
8680
+ 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;`;
8681
+ icon2.textContent = "⚠️";
8682
+ container.appendChild(icon2);
8683
+ const t2 = document.createElement("h2");
8684
+ t2.style.cssText = `color:${this.theme.text};font-size:${TYPE.heading}px;font-weight:700;margin:0;`;
8685
+ t2.textContent = "We Couldn't Confirm Your Details";
8686
+ container.appendChild(t2);
8687
+ const d2 = document.createElement("p");
8688
+ d2.style.cssText = `color:${this.theme.textSecondary};font-size:13px;margin:0;max-width:280px;line-height:1.6;`;
8689
+ d2.textContent = failureReason || "We couldn't confirm your details. Please re-upload a clear photo of your ID and make sure your details are correct before submitting.";
8690
+ container.appendChild(d2);
8691
+ const retryBtn = this.createPrimaryButton("Re-upload ID");
8692
+ retryBtn.addEventListener("click", () => {
8693
+ this.livenessEntryId = null;
8694
+ this.documentBlob = null;
8695
+ this.documentBackBlob = null;
8696
+ this.showStep("document_capture");
8697
+ });
8698
+ container.appendChild(retryBtn);
8699
+ const endBtn = this.createSecondaryButton("End session");
8700
+ endBtn.addEventListener("click", () => {
8701
+ this.options.onError?.(new Error("Document verification failed"));
8702
+ this.close();
8703
+ });
8704
+ container.appendChild(endBtn);
8705
+ }
8498
8706
  /**
8499
8707
  * OCR ran to completion but never produced a single usable field within
8500
8708
  * the poll window — do not let the flow reach liveness with no verified