smartcomply-web-sdk 1.0.33 → 1.0.35

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.
@@ -5178,7 +5178,7 @@ var SmartComplySDK = (() => {
5178
5178
  let attempts = 0;
5179
5179
  let stableFrames = 0;
5180
5180
  let missedFrames = 0;
5181
- const STABLE_REQUIRED = 2;
5181
+ const STABLE_REQUIRED = 4;
5182
5182
  const MISS_TOLERANCE = 2;
5183
5183
  const MAX_WAIT_MS = 8e3;
5184
5184
  const startTime = performance.now();
@@ -5210,7 +5210,7 @@ var SmartComplySDK = (() => {
5210
5210
  requestAnimationFrame(checkFace);
5211
5211
  });
5212
5212
  ui2.updateInstruction("Hold still...");
5213
- await new Promise((r2) => setTimeout(r2, 150));
5213
+ await new Promise((r2) => setTimeout(r2, 500));
5214
5214
  const autoshotBlob = await this.captureFrame(videoForDetection);
5215
5215
  tempVideo.pause();
5216
5216
  if (!autoshotBlob || autoshotBlob.size === 0) {
@@ -5352,10 +5352,16 @@ var SmartComplySDK = (() => {
5352
5352
  * Capture a single sharp frame from a video element as a JPEG Blob.
5353
5353
  * Retries up to maxAttempts times (300 ms apart) if the Laplacian blur
5354
5354
  * score is below threshold, then falls back to the best frame captured.
5355
+ *
5356
+ * Threshold calibration:
5357
+ * The server rejects images with OpenCV Laplacian variance < 80.
5358
+ * This SDK uses sum-of-squared Laplacian (different normalisation) —
5359
+ * empirically ~100 here corresponds to ~80 server-side. We aim for 120
5360
+ * to give a comfortable safety margin.
5355
5361
  */
5356
- async captureFrame(video, maxAttempts = 3) {
5357
- const MIN_BLUR_SCORE = 45;
5358
- const RETRY_DELAY_MS2 = 100;
5362
+ async captureFrame(video, maxAttempts = 5) {
5363
+ const MIN_BLUR_SCORE = 120;
5364
+ const RETRY_DELAY_MS2 = 200;
5359
5365
  let bestBlob = null;
5360
5366
  let bestScore = -1;
5361
5367
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
@@ -5382,8 +5388,13 @@ var SmartComplySDK = (() => {
5382
5388
  throw new Error("Failed to capture frame");
5383
5389
  }
5384
5390
  /**
5385
- * Laplacian variance blur metric on a centre-cropped region (320×240 max).
5386
- * Higher score = sharper image. Score < 80 is considered blurry.
5391
+ * Sum-of-squared Laplacian blur metric on a centre-cropped region (320×240 max).
5392
+ * Higher score = sharper image.
5393
+ *
5394
+ * NOTE: This is sum-of-squares, NOT Laplacian variance (which OpenCV uses).
5395
+ * The two scales differ — empirically, a score of ~120 here corresponds to
5396
+ * ~80 on the server side (OpenCV cv2.Laplacian().var()). Keep MIN_BLUR_SCORE
5397
+ * in captureFrame() calibrated against this.
5387
5398
  */
5388
5399
  _computeBlurScore(canvas) {
5389
5400
  const ctx = canvas.getContext("2d");
@@ -5651,126 +5662,128 @@ var SmartComplySDK = (() => {
5651
5662
  }
5652
5663
  renderChoiceScreen(container, theme, documentType) {
5653
5664
  this.root = document.createElement("div");
5654
- this.root.style.cssText = "display:flex;flex-direction:column;align-items:center;gap:24px;padding:24px 20px;max-width:100%;";
5665
+ this.root.style.cssText = `
5666
+ display:flex;flex-direction:column;gap:0;padding:0;width:100%;
5667
+ font-family:system-ui,-apple-system,sans-serif;
5668
+ `;
5655
5669
  const docLabel = this.getDocumentLabel(documentType);
5656
5670
  const requiresBack = this.shouldCaptureBackSide(documentType);
5657
5671
  const isCapturingFront = this.currentSide === "front";
5658
- const header = document.createElement("div");
5659
- header.style.cssText = "display:flex;flex-direction:column;align-items:center;gap:16px;text-align:center;";
5660
- const iconContainer = document.createElement("div");
5661
- iconContainer.style.cssText = `
5662
- width:120px;height:120px;border-radius:24px;
5672
+ const sideLabel = requiresBack ? isCapturingFront ? " \u2014 Front side" : " \u2014 Back side" : "";
5673
+ const headerRow = document.createElement("div");
5674
+ headerRow.style.cssText = `
5675
+ display:flex;align-items:center;gap:14px;
5676
+ padding:16px 20px 14px;
5677
+ `;
5678
+ const iconBox = document.createElement("div");
5679
+ iconBox.style.cssText = `
5680
+ width:48px;height:48px;border-radius:14px;flex-shrink:0;
5663
5681
  display:flex;align-items:center;justify-content:center;
5664
- background:linear-gradient(135deg, ${theme.primary}15, ${theme.primary}05);
5665
- border:3px solid ${theme.primary}30;
5666
- position:relative;overflow:hidden;
5667
- box-shadow:0 8px 32px ${theme.primary}20;
5682
+ font-size:24px;
5683
+ background:${theme.primary}15;
5684
+ border:1.5px solid ${theme.primary}30;
5668
5685
  `;
5669
- const icon = document.createElement("div");
5670
- icon.style.cssText = "font-size:48px;animation:float 3s ease-in-out infinite;";
5671
- icon.textContent = "\u{1FAAA}";
5672
- iconContainer.appendChild(icon);
5673
- if (!document.head.querySelector("style[data-float-animation]")) {
5674
- const style2 = document.createElement("style");
5675
- style2.setAttribute("data-float-animation", "true");
5676
- style2.textContent = `
5677
- @keyframes float {
5678
- 0%, 100% { transform: translateY(0px); }
5679
- 50% { transform: translateY(-10px); }
5680
- }
5681
- `;
5682
- document.head.appendChild(style2);
5683
- }
5684
- header.appendChild(iconContainer);
5685
- const title = document.createElement("div");
5686
- title.style.cssText = `color:${theme.text};font-size:20px;font-weight:700;text-align:center;line-height:1.4;`;
5687
- let labelText = `Capture your ${docLabel}`;
5688
- if (requiresBack) {
5689
- labelText += ` (${isCapturingFront ? "Front Side" : "Back Side"})`;
5690
- }
5691
- title.textContent = labelText;
5692
- header.appendChild(title);
5693
- const subtitle = document.createElement("div");
5694
- subtitle.style.cssText = `color:${theme.textSecondary};font-size:14px;text-align:center;line-height:1.5;max-width:280px;`;
5695
- subtitle.textContent = "Choose how you'd like to provide your document";
5696
- header.appendChild(subtitle);
5697
- this.root.appendChild(header);
5698
- if (requiresBack) {
5699
- const progress = document.createElement("div");
5700
- progress.style.cssText = `
5701
- display:flex;flex-direction:column;align-items:center;gap:12px;
5702
- padding:16px 20px;background:${theme.primary}08;border-radius:16px;
5703
- border:1px solid ${theme.primary}20;width:100%;max-width:320px;
5704
- `;
5705
- progress.innerHTML = `
5706
- <div style="display:flex;gap:8px;align-items:center;">
5707
- <div style="width:12px;height:12px;border-radius:50%;background:${isCapturingFront ? theme.primary : theme.border};transition:all 0.3s ease;"></div>
5708
- <div style="width:32px;height:2px;background:${theme.border};border-radius:1px;"></div>
5709
- <div style="width:12px;height:12px;border-radius:50%;background:${!isCapturingFront ? theme.primary : theme.border};transition:all 0.3s ease;"></div>
5710
- </div>
5711
- <span style="color:${theme.text};font-size:13px;font-weight:600;">${isCapturingFront ? "Step 1 of 2" : "Step 2 of 2"}</span>
5712
- `;
5713
- this.root.appendChild(progress);
5714
- }
5715
- const actionsContainer = document.createElement("div");
5716
- actionsContainer.style.cssText = "display:flex;flex-direction:column;gap:16px;width:100%;max-width:320px;";
5717
- const cameraBtn = this.createEnhancedButton(
5718
- "\u{1F4F7} Take Photo with Camera",
5719
- theme,
5720
- true,
5721
- "Open camera to capture document"
5722
- );
5686
+ iconBox.textContent = this._docIcon(documentType);
5687
+ const titleBlock = document.createElement("div");
5688
+ const titleEl = document.createElement("div");
5689
+ titleEl.style.cssText = `color:${theme.text};font-size:16px;font-weight:700;line-height:1.3;`;
5690
+ titleEl.textContent = `Capture your ${docLabel}`;
5691
+ const subEl = document.createElement("div");
5692
+ subEl.style.cssText = `color:${theme.textSecondary};font-size:12px;margin-top:2px;`;
5693
+ subEl.textContent = requiresBack ? isCapturingFront ? "Step 1 of 2 \u2014 front side" : "Step 2 of 2 \u2014 back side" : "Choose how to provide your document";
5694
+ titleBlock.appendChild(titleEl);
5695
+ titleBlock.appendChild(subEl);
5696
+ headerRow.appendChild(iconBox);
5697
+ headerRow.appendChild(titleBlock);
5698
+ this.root.appendChild(headerRow);
5699
+ const btnRow = document.createElement("div");
5700
+ btnRow.style.cssText = `
5701
+ display:grid;grid-template-columns:1fr 1fr;gap:10px;
5702
+ padding:0 20px 14px;
5703
+ `;
5704
+ const cameraBtn = document.createElement("button");
5705
+ cameraBtn.style.cssText = `
5706
+ display:flex;flex-direction:column;align-items:center;justify-content:center;
5707
+ gap:8px;padding:16px 12px;
5708
+ background:${theme.primary};color:#fff;
5709
+ border:none;border-radius:14px;cursor:pointer;
5710
+ font-size:13px;font-weight:600;font-family:inherit;
5711
+ transition:all 0.15s ease;min-height:80px;
5712
+ box-shadow:0 4px 14px ${theme.primary}30;
5713
+ `;
5714
+ cameraBtn.innerHTML = `<span style="font-size:26px;">\u{1F4F7}</span><span>Take Photo</span><span style="font-size:10px;opacity:0.8;font-weight:500;">Use camera</span>`;
5715
+ cameraBtn.addEventListener("mouseenter", () => {
5716
+ cameraBtn.style.opacity = "0.9";
5717
+ cameraBtn.style.transform = "translateY(-1px)";
5718
+ });
5719
+ cameraBtn.addEventListener("mouseleave", () => {
5720
+ cameraBtn.style.opacity = "1";
5721
+ cameraBtn.style.transform = "none";
5722
+ });
5723
5723
  cameraBtn.addEventListener("click", () => {
5724
- this.showLoadingState("Opening camera...", theme);
5725
5724
  this.root.innerHTML = "";
5726
- setTimeout(() => {
5727
- this.renderCameraView(this.root, theme, documentType);
5728
- }, 500);
5725
+ this.renderCameraView(this.root, theme, documentType);
5726
+ });
5727
+ const uploadBtn = document.createElement("button");
5728
+ uploadBtn.style.cssText = `
5729
+ display:flex;flex-direction:column;align-items:center;justify-content:center;
5730
+ gap:8px;padding:16px 12px;
5731
+ background:${theme.cardBg || theme.inputBg};color:${theme.text};
5732
+ border:2px solid ${theme.border};border-radius:14px;cursor:pointer;
5733
+ font-size:13px;font-weight:600;font-family:inherit;
5734
+ transition:all 0.15s ease;min-height:80px;
5735
+ `;
5736
+ uploadBtn.innerHTML = `<span style="font-size:26px;">\u{1F5BC}\uFE0F</span><span>From Gallery</span><span style="font-size:10px;color:${theme.textMuted || theme.textSecondary};font-weight:500;">Upload file</span>`;
5737
+ uploadBtn.addEventListener("mouseenter", () => {
5738
+ uploadBtn.style.borderColor = theme.primary;
5739
+ uploadBtn.style.transform = "translateY(-1px)";
5740
+ });
5741
+ uploadBtn.addEventListener("mouseleave", () => {
5742
+ uploadBtn.style.borderColor = theme.border;
5743
+ uploadBtn.style.transform = "none";
5729
5744
  });
5730
- actionsContainer.appendChild(cameraBtn);
5731
- const uploadBtn = this.createEnhancedButton(
5732
- "\u{1F4C1} Choose from Gallery",
5733
- theme,
5734
- false,
5735
- "Upload document image from device"
5736
- );
5737
5745
  uploadBtn.addEventListener("click", () => {
5738
5746
  this.handleFileUpload(container, theme);
5739
5747
  });
5740
- actionsContainer.appendChild(uploadBtn);
5741
- this.root.appendChild(actionsContainer);
5748
+ btnRow.appendChild(cameraBtn);
5749
+ btnRow.appendChild(uploadBtn);
5750
+ this.root.appendChild(btnRow);
5742
5751
  const tips = document.createElement("div");
5743
5752
  tips.style.cssText = `
5744
- width:100%;padding:16px 20px;border-radius:16px;
5745
- background:${theme.shimmer};border:1px solid ${theme.border};
5746
- margin-top:8px;
5753
+ margin:0 20px 16px;padding:12px 14px;
5754
+ background:${theme.shimmer || theme.cardBg};
5755
+ border:1px solid ${theme.border};border-radius:12px;
5747
5756
  `;
5748
- const tipsTitle = document.createElement("div");
5749
- tipsTitle.style.cssText = `font-weight:700;color:${theme.text};margin-bottom:12px;font-size:14px;display:flex;align-items:center;gap:8px;`;
5750
- tipsTitle.innerHTML = `\u{1F4A1} Tips for best results`;
5751
- tips.appendChild(tipsTitle);
5752
- const tipsList = document.createElement("div");
5753
- tipsList.style.cssText = `display:grid;gap:8px;font-size:13px;color:${theme.textSecondary};line-height:1.6;`;
5754
- tipsList.innerHTML = `
5755
- <div style="display:flex;gap:8px;align-items:flex-start;">
5756
- <span style="color:${theme.primary};font-weight:bold;">\u2713</span>
5757
- <span>Lay document flat on a solid surface</span>
5758
- </div>
5759
- <div style="display:flex;gap:8px;align-items:flex-start;">
5760
- <span style="color:${theme.primary};font-weight:bold;">\u2713</span>
5761
- <span>Ensure all four corners are visible</span>
5762
- </div>
5763
- <div style="display:flex;gap:8px;align-items:flex-start;">
5764
- <span style="color:${theme.primary};font-weight:bold;">\u2713</span>
5765
- <span>Use good lighting, avoid glare</span>
5766
- </div>
5767
- <div style="display:flex;gap:8px;align-items:flex-start;">
5768
- <span style="color:${theme.primary};font-weight:bold;">\u2713</span>
5769
- <span>Make sure all text is clearly readable</span>
5770
- </div>
5757
+ const tipsGrid = document.createElement("div");
5758
+ tipsGrid.style.cssText = `
5759
+ display:grid;grid-template-columns:1fr 1fr;gap:6px 12px;
5771
5760
  `;
5772
- tips.appendChild(tipsList);
5761
+ const tipItems = [
5762
+ "Lay document flat",
5763
+ "All 4 corners visible",
5764
+ "Good lighting, no glare",
5765
+ "All text clearly readable"
5766
+ ];
5767
+ tipItems.forEach((t2) => {
5768
+ const row = document.createElement("div");
5769
+ row.style.cssText = `display:flex;align-items:center;gap:5px;font-size:11px;color:${theme.textSecondary};`;
5770
+ row.innerHTML = `<span style="color:${theme.primary};font-weight:700;flex-shrink:0;">\u2713</span><span>${t2}</span>`;
5771
+ tipsGrid.appendChild(row);
5772
+ });
5773
+ tips.appendChild(tipsGrid);
5773
5774
  this.root.appendChild(tips);
5775
+ if (requiresBack && isCapturingFront) {
5776
+ const notice = document.createElement("div");
5777
+ notice.style.cssText = `
5778
+ margin:0 20px 16px;padding:10px 14px;
5779
+ background:${theme.primary}0D;
5780
+ border:1px solid ${theme.primary}25;border-radius:10px;
5781
+ display:flex;align-items:center;gap:8px;
5782
+ font-size:12px;color:${theme.textSecondary};line-height:1.5;
5783
+ `;
5784
+ notice.innerHTML = `<span style="font-size:16px;flex-shrink:0;">\u2139\uFE0F</span><span>You'll capture both the <strong style="color:${theme.text}">front</strong> and <strong style="color:${theme.text}">back</strong> of this document.</span>`;
5785
+ this.root.appendChild(notice);
5786
+ }
5774
5787
  const fileInput = document.createElement("input");
5775
5788
  fileInput.type = "file";
5776
5789
  fileInput.accept = "image/jpeg,image/png";
@@ -5779,18 +5792,26 @@ var SmartComplySDK = (() => {
5779
5792
  fileInput.addEventListener("change", (e2) => {
5780
5793
  const file = e2.target.files?.[0];
5781
5794
  if (file) {
5782
- const validationError = this.validateFile(file);
5783
- if (validationError) {
5784
- this.showError(validationError);
5795
+ const err = this.validateFile(file);
5796
+ if (err) {
5797
+ this.showError(err);
5785
5798
  return;
5786
5799
  }
5787
5800
  this.showLoadingState("Processing image...", theme);
5788
- this.compressImage(file, 1280, 960, 0.85).then((compressed) => this.handleFileUploadComplete(compressed, container, theme, documentType)).catch(() => this.handleFileUploadComplete(file, container, theme, documentType));
5801
+ this.compressImage(file, 1280, 960, 0.85).then((c2) => this.handleFileUploadComplete(c2, container, theme, documentType)).catch(() => this.handleFileUploadComplete(file, container, theme, documentType));
5789
5802
  }
5790
5803
  });
5791
5804
  this.root.appendChild(fileInput);
5792
5805
  container.appendChild(this.root);
5793
5806
  }
5807
+ /** Returns an appropriate emoji icon for the document type. */
5808
+ _docIcon(documentType) {
5809
+ const t2 = documentType.toLowerCase();
5810
+ if (t2.includes("passport")) return "\u{1F4D8}";
5811
+ if (t2.includes("driver") || t2.includes("licen")) return "\u{1F697}";
5812
+ if (t2.includes("voter") || t2.includes("pvc")) return "\u{1F5F3}\uFE0F";
5813
+ return "\u{1FAAA}";
5814
+ }
5794
5815
  renderCameraView(container, theme, documentType) {
5795
5816
  const wrapper = document.createElement("div");
5796
5817
  wrapper.style.cssText = "position:relative;width:100%;border-radius:12px;overflow:hidden;background:#000;";