smartcomply-web-sdk 1.0.64 → 1.0.65

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.
@@ -5372,7 +5372,7 @@ var SmartComplySDK = (() => {
5372
5372
  tempVideo.muted = true;
5373
5373
  tempVideo.playsInline = true;
5374
5374
  await tempVideo.play();
5375
- ui2.mount(container, { time_limit_seconds: 0, instruction: "Centre your face in the oval" });
5375
+ ui2.mount(container, { time_limit_seconds: 0, instruction: "Fit your face inside the oval outline" });
5376
5376
  const uiVideo = ui2.getVideoElement();
5377
5377
  if (uiVideo) {
5378
5378
  uiVideo.srcObject = stream;
@@ -5382,7 +5382,7 @@ var SmartComplySDK = (() => {
5382
5382
  container.appendChild(tempVideo);
5383
5383
  }
5384
5384
  await faceEngine.init();
5385
- ui2.updateInstruction("Centre your face in the oval");
5385
+ ui2.updateInstruction("Fit your face inside the oval outline");
5386
5386
  const videoForDetection = uiVideo || tempVideo;
5387
5387
  await new Promise((resolve) => {
5388
5388
  let stableFrames = 0;
@@ -5402,7 +5402,7 @@ var SmartComplySDK = (() => {
5402
5402
  if (detection.faceDetected) {
5403
5403
  stableFrames++;
5404
5404
  missedFrames = 0;
5405
- ui2.updateInstruction("Centre your face in the oval");
5405
+ ui2.updateInstruction("Good — hold that position");
5406
5406
  if (stableFrames >= STABLE_REQUIRED) {
5407
5407
  resolve();
5408
5408
  return;
@@ -5411,7 +5411,7 @@ var SmartComplySDK = (() => {
5411
5411
  missedFrames++;
5412
5412
  if (missedFrames > MISS_TOLERANCE) {
5413
5413
  stableFrames = 0;
5414
- ui2.updateInstruction("Move your face into the oval");
5414
+ ui2.updateInstruction("Move closer so your whole face fits in the oval");
5415
5415
  }
5416
5416
  }
5417
5417
  }
@@ -5419,7 +5419,7 @@ var SmartComplySDK = (() => {
5419
5419
  };
5420
5420
  requestAnimationFrame(checkFace);
5421
5421
  });
5422
- ui2.updateInstruction("Hold still...");
5422
+ ui2.updateInstruction("Getting ready…");
5423
5423
  await new Promise((r2) => setTimeout(r2, 500));
5424
5424
  const challengeActions = actions || [...DEFAULT_CHALLENGE_ACTIONS];
5425
5425
  let entry;
@@ -5448,10 +5448,10 @@ var SmartComplySDK = (() => {
5448
5448
  tempVideo.remove();
5449
5449
  const uiChallenge = {
5450
5450
  time_limit_seconds: LIVENESS_TIME_LIMIT_SECONDS,
5451
- instruction: "Blink and look around naturally"
5451
+ instruction: "Blink naturally and look slowly left, right, up or down"
5452
5452
  };
5453
5453
  ui2.unmount();
5454
- const videoEl = ui2.mount(container, uiChallenge);
5454
+ const videoEl = ui2.mount(container, { ...uiChallenge, instruction: "Starting scan…" });
5455
5455
  videoEl.srcObject = stream;
5456
5456
  await videoEl.play();
5457
5457
  await new Promise((r2) => setTimeout(r2, 1e3));
@@ -5476,7 +5476,7 @@ var SmartComplySDK = (() => {
5476
5476
  ui2.updateScanProgress(scan.livenessConfirmed);
5477
5477
  ui2.setFaceTracking(detection.faceDetected);
5478
5478
  if (!detection.faceDetected) {
5479
- ui2.updateInstruction("Move your face into the oval");
5479
+ ui2.updateInstruction("Move closer so your whole face fits in the oval");
5480
5480
  } else {
5481
5481
  ui2.updateInstruction(uiChallenge.instruction);
5482
5482
  }
@@ -5845,126 +5845,6 @@ var SmartComplySDK = (() => {
5845
5845
  return `#${(r2 << 16 | g2 << 8 | b2).toString(16).padStart(6, "0")}`;
5846
5846
  }
5847
5847
 
5848
- // src/utils/ImageQuality.ts
5849
- function calculateSharpness(imageData) {
5850
- const { width, height, data } = imageData;
5851
- const gray = new Uint8Array(width * height);
5852
- for (let i2 = 0; i2 < width * height; i2++) {
5853
- const idx = i2 * 4;
5854
- gray[i2] = Math.round(0.299 * data[idx] + 0.587 * data[idx + 1] + 0.114 * data[idx + 2]);
5855
- }
5856
- const laplacian = new Float32Array(width * height);
5857
- let sum = 0;
5858
- let sumSq = 0;
5859
- let count = 0;
5860
- for (let y2 = 1; y2 < height - 1; y2++) {
5861
- for (let x2 = 1; x2 < width - 1; x2++) {
5862
- const idx = y2 * width + x2;
5863
- const val = -1 * gray[idx - width] + -1 * gray[idx - 1] + 4 * gray[idx] + -1 * gray[idx + 1] + -1 * gray[idx + width];
5864
- laplacian[idx] = val;
5865
- sum += val;
5866
- sumSq += val * val;
5867
- count++;
5868
- }
5869
- }
5870
- const mean = sum / count;
5871
- const variance = sumSq / count - mean * mean;
5872
- return Math.abs(variance);
5873
- }
5874
- function detectGlare(imageData, threshold = 250) {
5875
- const { data } = imageData;
5876
- let overexposed = 0;
5877
- for (let i2 = 0; i2 < data.length; i2 += 4) {
5878
- const brightness = (data[i2] + data[i2 + 1] + data[i2 + 2]) / 3;
5879
- if (brightness > threshold) {
5880
- overexposed++;
5881
- }
5882
- }
5883
- return overexposed / (data.length / 4);
5884
- }
5885
- function calculateContrast(imageData) {
5886
- const { data } = imageData;
5887
- let sum = 0;
5888
- let sumSq = 0;
5889
- const pixelCount = data.length / 4;
5890
- for (let i2 = 0; i2 < data.length; i2 += 4) {
5891
- const gray = 0.299 * data[i2] + 0.587 * data[i2 + 1] + 0.114 * data[i2 + 2];
5892
- sum += gray;
5893
- sumSq += gray * gray;
5894
- }
5895
- const mean = sum / pixelCount;
5896
- const variance = sumSq / pixelCount - mean * mean;
5897
- return Math.sqrt(variance);
5898
- }
5899
- function calculateBrightness(imageData) {
5900
- const { data } = imageData;
5901
- let sum = 0;
5902
- for (let i2 = 0; i2 < data.length; i2 += 4) {
5903
- sum += (data[i2] + data[i2 + 1] + data[i2 + 2]) / 3;
5904
- }
5905
- return sum / (data.length / 4);
5906
- }
5907
- function assessQuality(imageData) {
5908
- const sharpness = calculateSharpness(imageData);
5909
- const glare = detectGlare(imageData);
5910
- const contrast = calculateContrast(imageData);
5911
- const brightness = calculateBrightness(imageData);
5912
- const isBlurry = sharpness < 80;
5913
- const hasGlare = glare > 0.15;
5914
- const isTooDark = brightness < 50;
5915
- const isTooBright = brightness > 200;
5916
- const lowContrast = contrast < 30;
5917
- let score = 100;
5918
- if (isBlurry) score -= 40;
5919
- if (hasGlare) score -= 25;
5920
- if (isTooDark) score -= 20;
5921
- if (isTooBright) score -= 20;
5922
- if (lowContrast) score -= 15;
5923
- const metrics = {
5924
- sharpness: Math.round(sharpness * 10) / 10,
5925
- contrast: Math.round(contrast * 10) / 10,
5926
- brightness: Math.round(brightness * 10) / 10,
5927
- isBlurry,
5928
- hasGlare,
5929
- overallScore: Math.max(0, score)
5930
- };
5931
- const issues = [];
5932
- const recommendations = [];
5933
- if (isBlurry) {
5934
- issues.push("Too blurry");
5935
- recommendations.push("Hold camera steady");
5936
- recommendations.push("Ensure good lighting");
5937
- }
5938
- if (hasGlare) {
5939
- issues.push("Too much glare");
5940
- recommendations.push("Avoid direct light on document");
5941
- recommendations.push("Tilt document slightly");
5942
- }
5943
- if (isTooDark) {
5944
- issues.push("Too dark");
5945
- recommendations.push("Move to brighter area");
5946
- recommendations.push("Turn on more lights");
5947
- }
5948
- if (isTooBright) {
5949
- issues.push("Too bright");
5950
- recommendations.push("Reduce lighting");
5951
- recommendations.push("Move away from direct sun");
5952
- }
5953
- if (lowContrast) {
5954
- issues.push("Low contrast");
5955
- recommendations.push("Ensure even lighting");
5956
- }
5957
- const isValid = score >= 60;
5958
- const message = issues.length > 0 ? issues.join(", ") : "Good quality";
5959
- return {
5960
- isValid,
5961
- message,
5962
- recommendations: recommendations.slice(0, 3),
5963
- // Max 3 recommendations
5964
- quality: metrics
5965
- };
5966
- }
5967
-
5968
5848
  // src/utils/documentRules.ts
5969
5849
  function requiresBackSide(documentType) {
5970
5850
  const type = documentType.toLowerCase();
@@ -6250,8 +6130,7 @@ var SmartComplySDK = (() => {
6250
6130
  this.showError(err);
6251
6131
  return;
6252
6132
  }
6253
- this.showLoadingState("Processing image...", theme);
6254
- this.compressImage(file, 2e3, 1500, 0.92).then((c2) => this.handleFileUploadComplete(c2, container, theme, documentType)).catch(() => this.handleFileUploadComplete(file, container, theme, documentType));
6133
+ this.ensureUnderUploadLimit(file).then((c2) => this.handleFileUploadComplete(c2, container, theme, documentType));
6255
6134
  }
6256
6135
  });
6257
6136
  this.root.appendChild(fileInput);
@@ -6452,54 +6331,17 @@ var SmartComplySDK = (() => {
6452
6331
  if (!ctx) return;
6453
6332
  ctx.drawImage(this.videoEl, 0, 0, canvas.width, canvas.height);
6454
6333
  canvas.toBlob(
6455
- async (blob) => {
6334
+ (blob) => {
6456
6335
  if (blob) {
6457
6336
  this.stopCamera();
6458
- this.showLoadingState("Analyzing photo...", theme);
6459
- const quality = await this._assessImageQuality(canvas, ctx);
6460
- this.showPreview(container, theme, blob, documentType, quality, true);
6337
+ this.showPreview(container, theme, blob, documentType, true);
6461
6338
  }
6462
6339
  },
6463
6340
  "image/jpeg",
6464
6341
  0.92
6465
6342
  );
6466
6343
  }
6467
- /**
6468
- * Runs the local sharpness/glare/contrast/brightness check against a
6469
- * captured frame. Returns null (never blocks) if quality can't be computed
6470
- * — e.g. a tainted canvas from a cross-origin file, or an unsupported browser.
6471
- */
6472
- async _assessImageQuality(canvas, ctx) {
6473
- try {
6474
- const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
6475
- return assessQuality(imageData);
6476
- } catch {
6477
- return null;
6478
- }
6479
- }
6480
- /** Same as _assessImageQuality but starting from a Blob/File (upload path). */
6481
- async _assessBlobQuality(blob) {
6482
- try {
6483
- const url = URL.createObjectURL(blob);
6484
- const imgEl = document.createElement("img");
6485
- await new Promise((resolve, reject) => {
6486
- imgEl.onload = () => resolve();
6487
- imgEl.onerror = () => reject(new Error("Image load failed"));
6488
- imgEl.src = url;
6489
- });
6490
- const canvas = document.createElement("canvas");
6491
- canvas.width = imgEl.naturalWidth;
6492
- canvas.height = imgEl.naturalHeight;
6493
- const ctx = canvas.getContext("2d");
6494
- URL.revokeObjectURL(url);
6495
- if (!ctx) return null;
6496
- ctx.drawImage(imgEl, 0, 0);
6497
- return this._assessImageQuality(canvas, ctx);
6498
- } catch {
6499
- return null;
6500
- }
6501
- }
6502
- showPreview(container, theme, imageData, documentType, quality, viaCamera) {
6344
+ showPreview(container, theme, imageData, documentType, viaCamera) {
6503
6345
  if (this.root) this.root.innerHTML = "";
6504
6346
  else {
6505
6347
  this.root = document.createElement("div");
@@ -6532,29 +6374,9 @@ var SmartComplySDK = (() => {
6532
6374
  sublabel.style.cssText = `color:${theme.textMuted};font-size:${TYPE.xs}px;text-align:center;line-height:1.6;`;
6533
6375
  sublabel.textContent = "All text must be readable and all four corners must be visible.";
6534
6376
  this.root.appendChild(sublabel);
6535
- const qualityIsPoor = !!quality && !quality.isValid;
6536
- if (qualityIsPoor && quality) {
6537
- const warn = document.createElement("div");
6538
- warn.style.cssText = `
6539
- width:100%;box-sizing:border-box;padding:10px 14px;border-radius:10px;
6540
- background:#eab30815;border:1px solid #eab30840;
6541
- color:${theme.text};font-size:12px;line-height:1.5;
6542
- `;
6543
- const issueLine = document.createElement("div");
6544
- issueLine.style.cssText = "font-weight:700;color:#b8860b;";
6545
- issueLine.textContent = `⚠️ ${quality.message}`;
6546
- warn.appendChild(issueLine);
6547
- if (quality.recommendations.length > 0) {
6548
- const rec = document.createElement("div");
6549
- rec.style.cssText = "margin-top:4px;color:" + theme.textSecondary + ";";
6550
- rec.textContent = quality.recommendations.join(" · ");
6551
- warn.appendChild(rec);
6552
- }
6553
- this.root.appendChild(warn);
6554
- }
6555
6377
  const btnRow = document.createElement("div");
6556
6378
  btnRow.style.cssText = "display:flex;gap:12px;width:100%;";
6557
- const retakeBtn = this.createButton("Retake", theme, qualityIsPoor);
6379
+ const retakeBtn = this.createButton("Retake", theme, false);
6558
6380
  retakeBtn.style.flex = "1";
6559
6381
  retakeBtn.addEventListener("click", () => {
6560
6382
  if (img._blobUrl) {
@@ -6563,29 +6385,16 @@ var SmartComplySDK = (() => {
6563
6385
  this.root.innerHTML = "";
6564
6386
  this.renderChoiceScreen(container, theme, documentType);
6565
6387
  });
6566
- const proceedWithPhoto = () => {
6388
+ const useBtn = this.createButton("Use Photo ✓", theme, true);
6389
+ useBtn.style.flex = "1";
6390
+ useBtn.addEventListener("click", () => {
6567
6391
  if (img._blobUrl) {
6568
6392
  URL.revokeObjectURL(img._blobUrl);
6569
6393
  }
6570
6394
  useBtn.textContent = "Processing...";
6571
6395
  useBtn.disabled = true;
6572
- this.compressImage(imageData, 2e3, 1500, 0.92).then((compressed) => this.handleCaptureComplete(compressed, container, theme, documentType, viaCamera)).catch(() => this.handleCaptureComplete(imageData, container, theme, documentType, viaCamera));
6573
- };
6574
- const useBtn = this.createButton(qualityIsPoor ? "Use Anyway ⚠" : "Use Photo ✓", theme, !qualityIsPoor);
6575
- useBtn.style.flex = "1";
6576
- if (qualityIsPoor) {
6577
- let confirming = false;
6578
- useBtn.addEventListener("click", () => {
6579
- if (!confirming) {
6580
- confirming = true;
6581
- useBtn.textContent = "Tap again to confirm ⚠";
6582
- return;
6583
- }
6584
- proceedWithPhoto();
6585
- });
6586
- } else {
6587
- useBtn.addEventListener("click", proceedWithPhoto);
6588
- }
6396
+ this.ensureUnderUploadLimit(imageData).then((c2) => this.handleCaptureComplete(c2, container, theme, documentType, viaCamera));
6397
+ });
6589
6398
  btnRow.appendChild(retakeBtn);
6590
6399
  btnRow.appendChild(useBtn);
6591
6400
  this.root.appendChild(btnRow);
@@ -6616,24 +6425,39 @@ var SmartComplySDK = (() => {
6616
6425
  };
6617
6426
  this.resolveCapture?.(result);
6618
6427
  }
6619
- async handleFileUploadComplete(imageData, container, theme, documentType) {
6620
- const quality = await this._assessBlobQuality(imageData);
6621
- this.showPreview(container, theme, imageData, documentType, quality, false);
6428
+ handleFileUploadComplete(imageData, container, theme, documentType) {
6429
+ this.showPreview(container, theme, imageData, documentType, false);
6622
6430
  }
6623
6431
  validateFile(file) {
6624
6432
  const allowedTypes = ["image/jpeg", "image/jpg", "image/png"];
6625
6433
  if (!allowedTypes.includes(file.type)) {
6626
6434
  return "Invalid file type. Please upload a JPEG or PNG image.";
6627
6435
  }
6628
- const maxSize = 10 * 1024 * 1024;
6629
- if (file.size > maxSize) {
6630
- return "File is too large. Please upload an image smaller than 10MB.";
6436
+ if (file.size > MAX_DOCUMENT_BYTES) {
6437
+ return `File is too large. Please upload an image smaller than ${Math.round(MAX_DOCUMENT_BYTES / (1024 * 1024))}MB.`;
6631
6438
  }
6632
6439
  if (file.size < 1024) {
6633
6440
  return "File is too small. Please upload a valid image file.";
6634
6441
  }
6635
6442
  return null;
6636
6443
  }
6444
+ /**
6445
+ * Only shrinks/re-encodes when the file exceeds the backend's real upload
6446
+ * ceiling — otherwise returns the original bytes completely untouched.
6447
+ * Document photos become the face-comparison reference image later
6448
+ * (see backend _crop_and_save_face), so any avoidable re-encode is a
6449
+ * quality loss with no upside; compression here exists solely to keep
6450
+ * oversized captures under MAX_DOCUMENT_BYTES, not to "optimize" images
6451
+ * that already fit.
6452
+ */
6453
+ async ensureUnderUploadLimit(source) {
6454
+ if (source.size <= MAX_DOCUMENT_BYTES) return source;
6455
+ try {
6456
+ return await this.compressImage(source, DOC_CAMERA_IDEAL_WIDTH, DOC_CAMERA_IDEAL_HEIGHT, 0.92);
6457
+ } catch {
6458
+ return source;
6459
+ }
6460
+ }
6637
6461
  handleFileUpload(container, theme) {
6638
6462
  const input = this.root?.querySelector("#sc-doc-file-input");
6639
6463
  if (input) input.click();
@@ -8390,7 +8214,7 @@ var SmartComplySDK = (() => {
8390
8214
  container.appendChild(title);
8391
8215
  const desc = document.createElement("div");
8392
8216
  desc.style.cssText = `color:${this.theme.textSecondary};font-size:13px;margin-top:-8px;line-height:1.5;`;
8393
- desc.textContent = "Position your face in the oval and follow the on-screen instructions.";
8217
+ desc.textContent = "Fit your face inside the oval, then blink naturally while looking around for a few seconds.";
8394
8218
  container.appendChild(desc);
8395
8219
  const livenessContainer = document.createElement("div");
8396
8220
  livenessContainer.style.cssText = "margin:8px 0;width:100%;";