smartcomply-web-sdk 1.0.68 → 1.0.70
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.
- package/dist/esm/flow/DocumentCapture.d.ts +20 -0
- package/dist/esm/flow/DocumentCapture.d.ts.map +1 -1
- package/dist/esm/flow/DocumentCapture.js +173 -19
- package/dist/esm/flow/DocumentCapture.js.map +1 -1
- package/dist/esm/flow/SmartComplyFlow.d.ts +12 -0
- package/dist/esm/flow/SmartComplyFlow.d.ts.map +1 -1
- package/dist/esm/flow/SmartComplyFlow.js +165 -22
- package/dist/esm/flow/SmartComplyFlow.js.map +1 -1
- package/dist/esm/flow/icons.d.ts +1 -1
- package/dist/esm/flow/icons.d.ts.map +1 -1
- package/dist/esm/flow/icons.js +1 -0
- package/dist/esm/flow/icons.js.map +1 -1
- package/dist/esm/modules/liveness/LivenessUI.d.ts.map +1 -1
- package/dist/esm/modules/liveness/LivenessUI.js +13 -3
- package/dist/esm/modules/liveness/LivenessUI.js.map +1 -1
- package/dist/esm/utils/ImageQuality.d.ts +12 -1
- package/dist/esm/utils/ImageQuality.d.ts.map +1 -1
- package/dist/esm/utils/ImageQuality.js +12 -1
- package/dist/esm/utils/ImageQuality.js.map +1 -1
- package/dist/flow/DocumentCapture.d.ts +20 -0
- package/dist/flow/DocumentCapture.d.ts.map +1 -1
- package/dist/flow/DocumentCapture.js +173 -19
- package/dist/flow/DocumentCapture.js.map +1 -1
- package/dist/flow/SmartComplyFlow.d.ts +12 -0
- package/dist/flow/SmartComplyFlow.d.ts.map +1 -1
- package/dist/flow/SmartComplyFlow.js +165 -22
- package/dist/flow/SmartComplyFlow.js.map +1 -1
- package/dist/flow/icons.d.ts +1 -1
- package/dist/flow/icons.d.ts.map +1 -1
- package/dist/flow/icons.js +1 -0
- package/dist/flow/icons.js.map +1 -1
- package/dist/modules/liveness/LivenessUI.d.ts.map +1 -1
- package/dist/modules/liveness/LivenessUI.js +13 -3
- package/dist/modules/liveness/LivenessUI.js.map +1 -1
- package/dist/smartcomply.browser.js +393 -44
- package/dist/smartcomply.browser.js.map +4 -4
- package/dist/utils/ImageQuality.d.ts +12 -1
- package/dist/utils/ImageQuality.d.ts.map +1 -1
- package/dist/utils/ImageQuality.js +12 -1
- package/dist/utils/ImageQuality.js.map +1 -1
- 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", "
|
|
4773
|
-
confOval.setAttribute("stroke-linecap", "
|
|
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"/>',
|
|
@@ -5875,7 +5999,8 @@ var SmartComplySDK = (() => {
|
|
|
5875
5999
|
passport: '<rect x="5" y="2" width="14" height="20" rx="2"/><circle cx="12" cy="9" r="2.5"/><path d="M9 15h6"/>',
|
|
5876
6000
|
car: '<path d="M4 16V11l2-5h12l2 5v5"/><path d="M4 16h16M7 16v2M17 16v2"/><circle cx="7.5" cy="14" r="1"/><circle cx="16.5" cy="14" r="1"/>',
|
|
5877
6001
|
ballot: '<rect x="4" y="8" width="16" height="12" rx="1"/><path d="M9 8V5a3 3 0 016 0v3"/><path d="M12 12v4"/>',
|
|
5878
|
-
refresh: '<path d="M4 4v6h6"/><path d="M20 20v-6h-6"/><path d="M20 10a8 8 0 00-14.6-4.6M4 14a8 8 0 0014.6 4.6"/>'
|
|
6002
|
+
refresh: '<path d="M4 4v6h6"/><path d="M20 20v-6h-6"/><path d="M20 10a8 8 0 00-14.6-4.6M4 14a8 8 0 0014.6 4.6"/>',
|
|
6003
|
+
rotate: '<rect x="7" y="7" width="12" height="12" rx="2"/><path d="M7 4h5a4 4 0 014 4"/><path d="M13 1.5L16 4l-3 2.5"/>'
|
|
5879
6004
|
};
|
|
5880
6005
|
function icon(name, size = 24) {
|
|
5881
6006
|
const d2 = PATHS[name];
|
|
@@ -6126,7 +6251,7 @@ var SmartComplySDK = (() => {
|
|
|
6126
6251
|
display:flex;align-items:center;gap:8px;
|
|
6127
6252
|
font-size:12px;color:${theme.textSecondary};line-height:1.5;
|
|
6128
6253
|
`;
|
|
6129
|
-
notice.innerHTML = backOptional ? `<span style="font-size:16px;flex-shrink:0;">ℹ️</span><span>After the <strong style="color:${theme.text}">front</strong>, you can optionally add the <strong style="color:${theme.text}">back</strong> if your
|
|
6254
|
+
notice.innerHTML = backOptional ? `<span style="font-size:16px;flex-shrink:0;">ℹ️</span><span>After the <strong style="color:${theme.text}">front</strong>, you can optionally add the <strong style="color:${theme.text}">back</strong> if your document has one.</span>` : `<span style="font-size:16px;flex-shrink:0;">ℹ️</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>`;
|
|
6130
6255
|
this.root.appendChild(notice);
|
|
6131
6256
|
}
|
|
6132
6257
|
if (requiresBack && !isCapturingFront && backOptional) {
|
|
@@ -6137,7 +6262,7 @@ var SmartComplySDK = (() => {
|
|
|
6137
6262
|
border:1.5px dashed ${theme.border};border-radius:10px;cursor:pointer;
|
|
6138
6263
|
font-size:12.5px;font-weight:600;font-family:inherit;
|
|
6139
6264
|
`;
|
|
6140
|
-
skipBtn.textContent = "Skip —
|
|
6265
|
+
skipBtn.textContent = "Skip — my document has no back side";
|
|
6141
6266
|
skipBtn.addEventListener("click", () => {
|
|
6142
6267
|
this.handleCaptureComplete(null, container, theme, documentType, false, true);
|
|
6143
6268
|
});
|
|
@@ -6384,14 +6509,15 @@ var SmartComplySDK = (() => {
|
|
|
6384
6509
|
stepBadge.textContent = "STEP 1 OF 2 · CHECK THE PHOTO";
|
|
6385
6510
|
this.root.appendChild(stepBadge);
|
|
6386
6511
|
const img = document.createElement("img");
|
|
6387
|
-
|
|
6388
|
-
img.src =
|
|
6512
|
+
let currentUrl = URL.createObjectURL(imageData);
|
|
6513
|
+
img.src = currentUrl;
|
|
6389
6514
|
img.style.cssText = `
|
|
6390
6515
|
width:100%;max-height:300px;object-fit:contain;border-radius:${RADIUS.md}px;
|
|
6391
6516
|
border:2px solid ${theme.border};
|
|
6392
6517
|
`;
|
|
6393
6518
|
this.root.appendChild(img);
|
|
6394
|
-
|
|
6519
|
+
let currentBlob = imageData;
|
|
6520
|
+
let isRotating = false;
|
|
6395
6521
|
const label = document.createElement("div");
|
|
6396
6522
|
label.style.cssText = `color:${theme.text};font-size:${TYPE.base}px;font-weight:700;text-align:center;`;
|
|
6397
6523
|
label.textContent = "Does the photo look clear?";
|
|
@@ -6400,31 +6526,157 @@ var SmartComplySDK = (() => {
|
|
|
6400
6526
|
sublabel.style.cssText = `color:${theme.textMuted};font-size:${TYPE.xs}px;text-align:center;line-height:1.6;`;
|
|
6401
6527
|
sublabel.textContent = "All text must be readable and all four corners must be visible.";
|
|
6402
6528
|
this.root.appendChild(sublabel);
|
|
6529
|
+
const rotateBtn = document.createElement("button");
|
|
6530
|
+
rotateBtn.type = "button";
|
|
6531
|
+
rotateBtn.setAttribute("aria-label", "Rotate photo 90 degrees");
|
|
6532
|
+
rotateBtn.style.cssText = `
|
|
6533
|
+
display:flex;align-items:center;justify-content:center;gap:6px;
|
|
6534
|
+
margin:0 auto;padding:8px 16px;background:transparent;
|
|
6535
|
+
color:${theme.textSecondary};border:1.5px solid ${theme.border};
|
|
6536
|
+
border-radius:${RADIUS.pill}px;cursor:pointer;
|
|
6537
|
+
font-size:12.5px;font-weight:600;font-family:inherit;
|
|
6538
|
+
`;
|
|
6539
|
+
rotateBtn.innerHTML = `<span style="display:inline-flex;">${icon("rotate", 15)}</span><span>Rotate</span>`;
|
|
6540
|
+
rotateBtn.addEventListener("click", async () => {
|
|
6541
|
+
if (isRotating) return;
|
|
6542
|
+
isRotating = true;
|
|
6543
|
+
rotateBtn.disabled = true;
|
|
6544
|
+
try {
|
|
6545
|
+
const rotated = await this._rotateImage(currentBlob, 90);
|
|
6546
|
+
currentBlob = rotated;
|
|
6547
|
+
URL.revokeObjectURL(currentUrl);
|
|
6548
|
+
currentUrl = URL.createObjectURL(rotated);
|
|
6549
|
+
img.src = currentUrl;
|
|
6550
|
+
this._checkPhotoQuality(currentBlob, qualityNotice, theme);
|
|
6551
|
+
} catch {
|
|
6552
|
+
} finally {
|
|
6553
|
+
isRotating = false;
|
|
6554
|
+
rotateBtn.disabled = false;
|
|
6555
|
+
}
|
|
6556
|
+
});
|
|
6557
|
+
this.root.appendChild(rotateBtn);
|
|
6558
|
+
const qualityNotice = document.createElement("div");
|
|
6559
|
+
qualityNotice.style.cssText = `
|
|
6560
|
+
display:none;width:100%;padding:10px 14px;border-radius:${RADIUS.md}px;
|
|
6561
|
+
background:${theme.warning}15;border:1px solid ${theme.warning}35;
|
|
6562
|
+
color:${theme.text};font-size:12px;text-align:left;line-height:1.5;
|
|
6563
|
+
`;
|
|
6564
|
+
qualityNotice.setAttribute("role", "status");
|
|
6565
|
+
this.root.appendChild(qualityNotice);
|
|
6566
|
+
this._checkPhotoQuality(currentBlob, qualityNotice, theme);
|
|
6403
6567
|
const btnRow = document.createElement("div");
|
|
6404
6568
|
btnRow.style.cssText = "display:flex;gap:12px;width:100%;";
|
|
6405
6569
|
const retakeBtn = this.createButton("Retake", theme, false);
|
|
6406
6570
|
retakeBtn.style.flex = "1";
|
|
6407
6571
|
retakeBtn.addEventListener("click", () => {
|
|
6408
|
-
|
|
6409
|
-
URL.revokeObjectURL(img._blobUrl);
|
|
6410
|
-
}
|
|
6572
|
+
URL.revokeObjectURL(currentUrl);
|
|
6411
6573
|
this.root.innerHTML = "";
|
|
6412
6574
|
this.renderChoiceScreen(container, theme, documentType);
|
|
6413
6575
|
});
|
|
6414
6576
|
const useBtn = this.createButton("Use Photo ✓", theme, true);
|
|
6415
6577
|
useBtn.style.flex = "1";
|
|
6416
6578
|
useBtn.addEventListener("click", () => {
|
|
6417
|
-
|
|
6418
|
-
URL.revokeObjectURL(img._blobUrl);
|
|
6419
|
-
}
|
|
6579
|
+
URL.revokeObjectURL(currentUrl);
|
|
6420
6580
|
useBtn.textContent = "Confirming photo…";
|
|
6421
6581
|
useBtn.disabled = true;
|
|
6422
|
-
|
|
6582
|
+
rotateBtn.disabled = true;
|
|
6583
|
+
this.ensureUnderUploadLimit(currentBlob).then((c2) => this.handleCaptureComplete(c2, container, theme, documentType, viaCamera));
|
|
6423
6584
|
});
|
|
6424
6585
|
btnRow.appendChild(retakeBtn);
|
|
6425
6586
|
btnRow.appendChild(useBtn);
|
|
6426
6587
|
this.root.appendChild(btnRow);
|
|
6427
6588
|
}
|
|
6589
|
+
/**
|
|
6590
|
+
* Runs the shared client-side quality heuristic (blur/glare/contrast/
|
|
6591
|
+
* brightness — see utils/ImageQuality.ts) against the just-captured photo
|
|
6592
|
+
* and, if it scores poorly, shows a plain-language nudge on the preview
|
|
6593
|
+
* screen. Fails silently on decode error — this is an advisory extra, not
|
|
6594
|
+
* a required step, so a browser that can't run it must not block capture.
|
|
6595
|
+
*/
|
|
6596
|
+
async _checkPhotoQuality(imageData, noticeEl, theme) {
|
|
6597
|
+
try {
|
|
6598
|
+
const canvas = await this._blobToCanvas(imageData, 640);
|
|
6599
|
+
const ctx = canvas.getContext("2d");
|
|
6600
|
+
if (!ctx) return;
|
|
6601
|
+
const pixels = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
6602
|
+
const result = assessQuality(pixels);
|
|
6603
|
+
if (noticeEl.isConnected && !result.isValid) {
|
|
6604
|
+
noticeEl.style.display = "block";
|
|
6605
|
+
noticeEl.textContent = "This photo might be hard to read — " + (result.recommendations[0] || "try retaking it in good lighting") + ".";
|
|
6606
|
+
}
|
|
6607
|
+
} catch {
|
|
6608
|
+
}
|
|
6609
|
+
}
|
|
6610
|
+
/** Decodes to a canvas, optionally downscaled to maxDim on the long edge (never upscales). */
|
|
6611
|
+
_blobToCanvas(source, maxDim) {
|
|
6612
|
+
return new Promise((resolve, reject) => {
|
|
6613
|
+
const url = URL.createObjectURL(source);
|
|
6614
|
+
const imgEl = document.createElement("img");
|
|
6615
|
+
imgEl.onload = () => {
|
|
6616
|
+
URL.revokeObjectURL(url);
|
|
6617
|
+
const { naturalWidth: w2, naturalHeight: h2 } = imgEl;
|
|
6618
|
+
const ratio = maxDim ? computeScaleRatio(w2, h2, maxDim, maxDim) : 1;
|
|
6619
|
+
const canvas = document.createElement("canvas");
|
|
6620
|
+
canvas.width = Math.round(w2 * ratio);
|
|
6621
|
+
canvas.height = Math.round(h2 * ratio);
|
|
6622
|
+
const ctx = canvas.getContext("2d");
|
|
6623
|
+
if (!ctx) {
|
|
6624
|
+
reject(new Error("Canvas unavailable"));
|
|
6625
|
+
return;
|
|
6626
|
+
}
|
|
6627
|
+
ctx.drawImage(imgEl, 0, 0, canvas.width, canvas.height);
|
|
6628
|
+
resolve(canvas);
|
|
6629
|
+
};
|
|
6630
|
+
imgEl.onerror = () => {
|
|
6631
|
+
URL.revokeObjectURL(url);
|
|
6632
|
+
reject(new Error("Image load failed"));
|
|
6633
|
+
};
|
|
6634
|
+
imgEl.src = url;
|
|
6635
|
+
});
|
|
6636
|
+
}
|
|
6637
|
+
/**
|
|
6638
|
+
* Rotate an image blob by a multiple of 90° and re-encode it — used by the
|
|
6639
|
+
* preview screen's rotate control for a document photographed or uploaded
|
|
6640
|
+
* sideways/upside-down. EXIF auto-rotation (see compressImage) only fixes
|
|
6641
|
+
* phone photos that embed correct orientation metadata; it can't help a
|
|
6642
|
+
* document whose actual pixel content is sideways (e.g. the phone itself
|
|
6643
|
+
* was held rotated relative to the document, or a pre-cropped/scanned
|
|
6644
|
+
* upload with no EXIF at all) — that needs the user to say so directly.
|
|
6645
|
+
*/
|
|
6646
|
+
_rotateImage(source, degrees) {
|
|
6647
|
+
return new Promise((resolve, reject) => {
|
|
6648
|
+
const url = URL.createObjectURL(source);
|
|
6649
|
+
const imgEl = document.createElement("img");
|
|
6650
|
+
imgEl.onload = () => {
|
|
6651
|
+
URL.revokeObjectURL(url);
|
|
6652
|
+
const { naturalWidth: w2, naturalHeight: h2 } = imgEl;
|
|
6653
|
+
const swapped = degrees === 90 || degrees === 270;
|
|
6654
|
+
const canvas = document.createElement("canvas");
|
|
6655
|
+
canvas.width = swapped ? h2 : w2;
|
|
6656
|
+
canvas.height = swapped ? w2 : h2;
|
|
6657
|
+
const ctx = canvas.getContext("2d");
|
|
6658
|
+
if (!ctx) {
|
|
6659
|
+
reject(new Error("Canvas unavailable"));
|
|
6660
|
+
return;
|
|
6661
|
+
}
|
|
6662
|
+
ctx.translate(canvas.width / 2, canvas.height / 2);
|
|
6663
|
+
ctx.rotate(degrees * Math.PI / 180);
|
|
6664
|
+
ctx.drawImage(imgEl, -w2 / 2, -h2 / 2);
|
|
6665
|
+
canvas.toBlob(
|
|
6666
|
+
(blob) => {
|
|
6667
|
+
blob ? resolve(blob) : reject(new Error("Rotation failed"));
|
|
6668
|
+
},
|
|
6669
|
+
"image/jpeg",
|
|
6670
|
+
0.95
|
|
6671
|
+
);
|
|
6672
|
+
};
|
|
6673
|
+
imgEl.onerror = () => {
|
|
6674
|
+
URL.revokeObjectURL(url);
|
|
6675
|
+
reject(new Error("Image load failed"));
|
|
6676
|
+
};
|
|
6677
|
+
imgEl.src = url;
|
|
6678
|
+
});
|
|
6679
|
+
}
|
|
6428
6680
|
handleCaptureComplete(imageData, container, theme, documentType, viaCamera, skipped = false) {
|
|
6429
6681
|
const requiresBack = this.shouldCaptureBackSide(documentType);
|
|
6430
6682
|
const wasCapturingFront = this.currentSide === "front";
|
|
@@ -6501,6 +6753,8 @@ var SmartComplySDK = (() => {
|
|
|
6501
6753
|
if (!this.root) return;
|
|
6502
6754
|
this.root.innerHTML = "";
|
|
6503
6755
|
this.root.style.cssText = "display:flex;flex-direction:column;align-items:center;justify-content:center;gap:20px;padding:40px 20px;min-height:300px;";
|
|
6756
|
+
this.root.setAttribute("aria-live", "polite");
|
|
6757
|
+
this.root.setAttribute("role", "status");
|
|
6504
6758
|
this.root.appendChild(renderSpinner(theme, { size: 80, icon: icon("document", 28) }));
|
|
6505
6759
|
const loadingText = document.createElement("div");
|
|
6506
6760
|
loadingText.style.cssText = `color:${theme.text};font-size:${TYPE.heading}px;font-weight:600;text-align:center;`;
|
|
@@ -6510,15 +6764,19 @@ var SmartComplySDK = (() => {
|
|
|
6510
6764
|
showError(message) {
|
|
6511
6765
|
if (!this.root) return;
|
|
6512
6766
|
const errorContainer = document.createElement("div");
|
|
6767
|
+
errorContainer.setAttribute("role", "alert");
|
|
6513
6768
|
errorContainer.style.cssText = `
|
|
6514
6769
|
display:flex;align-items:center;gap:12px;padding:16px;
|
|
6515
6770
|
background:#ff444415;border:1px solid #ff444430;border-radius:${RADIUS.md}px;
|
|
6516
6771
|
color:#ff4444;font-size:14px;margin-top:16px;animation:shake 0.5s ease-in-out;
|
|
6517
6772
|
`;
|
|
6518
|
-
|
|
6519
|
-
|
|
6520
|
-
|
|
6521
|
-
|
|
6773
|
+
const errIcon = document.createElement("span");
|
|
6774
|
+
errIcon.style.fontSize = "18px";
|
|
6775
|
+
errIcon.textContent = "⚠️";
|
|
6776
|
+
const errText = document.createElement("span");
|
|
6777
|
+
errText.textContent = message;
|
|
6778
|
+
errorContainer.appendChild(errIcon);
|
|
6779
|
+
errorContainer.appendChild(errText);
|
|
6522
6780
|
if (!document.head.querySelector("style[data-shake-animation]")) {
|
|
6523
6781
|
const style = document.createElement("style");
|
|
6524
6782
|
style.setAttribute("data-shake-animation", "true");
|
|
@@ -6693,13 +6951,32 @@ var SmartComplySDK = (() => {
|
|
|
6693
6951
|
if (t2.includes("passport")) return "passport";
|
|
6694
6952
|
if (t2.includes("driver") || t2.includes("licen")) return "driver's license";
|
|
6695
6953
|
if (t2.includes("voter") || t2.includes("pvc")) return "voter's card";
|
|
6696
|
-
if (t2.includes("
|
|
6697
|
-
if (t2.includes("
|
|
6954
|
+
if (t2.includes("nin")) return "NIN document";
|
|
6955
|
+
if (t2.includes("national")) return "national ID card";
|
|
6956
|
+
if (t2.includes("bvn")) return "BVN document";
|
|
6698
6957
|
return "identity document";
|
|
6699
6958
|
}
|
|
6700
6959
|
};
|
|
6701
6960
|
|
|
6702
6961
|
// src/flow/SmartComplyFlow.ts
|
|
6962
|
+
var _INFRASTRUCTURE_ERROR_PATTERNS = [
|
|
6963
|
+
"camera connection was lost",
|
|
6964
|
+
"network error",
|
|
6965
|
+
"failed after all retries",
|
|
6966
|
+
"camera access is not supported",
|
|
6967
|
+
"camera access failed",
|
|
6968
|
+
"recording failed unexpectedly"
|
|
6969
|
+
];
|
|
6970
|
+
function _isInfrastructureError(err) {
|
|
6971
|
+
const msg = String(err?.message || "").toLowerCase();
|
|
6972
|
+
return _INFRASTRUCTURE_ERROR_PATTERNS.some((p2) => msg.includes(p2));
|
|
6973
|
+
}
|
|
6974
|
+
function _userFacingLivenessError(err) {
|
|
6975
|
+
if (_isInfrastructureError(err)) {
|
|
6976
|
+
return "We lost connection during your scan — this wasn't anything you did. Please check your camera and internet connection, then try again.";
|
|
6977
|
+
}
|
|
6978
|
+
return err?.message || "We couldn't verify your face. Please try again.";
|
|
6979
|
+
}
|
|
6703
6980
|
var COUNTRY_LABELS = {
|
|
6704
6981
|
nigeria: "🇳🇬 Nigeria",
|
|
6705
6982
|
global: "🌍 Other Countries",
|
|
@@ -6722,7 +6999,7 @@ var SmartComplySDK = (() => {
|
|
|
6722
6999
|
if (u2.includes("NIN") || u2.includes("NATIONAL IDENTITY NUMBER")) {
|
|
6723
7000
|
return {
|
|
6724
7001
|
icon: "🆔",
|
|
6725
|
-
desc: isDocumentFlow ? "Capture or upload your NIN card
|
|
7002
|
+
desc: isDocumentFlow ? "Capture or upload your NIN (slip or card) — add the back if your document has one" : "Enter your 11-digit National Identification Number (NIN)",
|
|
6726
7003
|
requiresBack: isDocumentFlow ? "optional" : false
|
|
6727
7004
|
};
|
|
6728
7005
|
}
|
|
@@ -6907,9 +7184,14 @@ var SmartComplySDK = (() => {
|
|
|
6907
7184
|
this.isDestroyed = false;
|
|
6908
7185
|
this.headerBackBtn = null;
|
|
6909
7186
|
this._keydownHandler = null;
|
|
7187
|
+
this._visibilityHandler = null;
|
|
7188
|
+
this._hiddenAtMs = null;
|
|
7189
|
+
this._popstateHandler = null;
|
|
7190
|
+
this._historyGuardActive = false;
|
|
6910
7191
|
// Retry safety — max 3 confirmation rejections or liveness failures per session
|
|
6911
7192
|
this._confirmRetries = 0;
|
|
6912
7193
|
this._livenessFailures = 0;
|
|
7194
|
+
this._livenessAttemptId = 0;
|
|
6913
7195
|
// IVS result stored for confirm_identity screen
|
|
6914
7196
|
this._ivsName = "";
|
|
6915
7197
|
this._ivsGender = "";
|
|
@@ -6968,6 +7250,21 @@ var SmartComplySDK = (() => {
|
|
|
6968
7250
|
document.removeEventListener("keydown", this._keydownHandler);
|
|
6969
7251
|
this._keydownHandler = null;
|
|
6970
7252
|
}
|
|
7253
|
+
if (this._visibilityHandler) {
|
|
7254
|
+
document.removeEventListener("visibilitychange", this._visibilityHandler);
|
|
7255
|
+
this._visibilityHandler = null;
|
|
7256
|
+
}
|
|
7257
|
+
if (this._popstateHandler) {
|
|
7258
|
+
window.removeEventListener("popstate", this._popstateHandler);
|
|
7259
|
+
this._popstateHandler = null;
|
|
7260
|
+
}
|
|
7261
|
+
if (this._historyGuardActive) {
|
|
7262
|
+
this._historyGuardActive = false;
|
|
7263
|
+
try {
|
|
7264
|
+
if (history.state?.scFlowGuard) history.back();
|
|
7265
|
+
} catch {
|
|
7266
|
+
}
|
|
7267
|
+
}
|
|
6971
7268
|
this.docCapture?.destroy();
|
|
6972
7269
|
if (this._prewarmedStream) {
|
|
6973
7270
|
this._prewarmedStream.getTracks().forEach((t2) => t2.stop());
|
|
@@ -7084,6 +7381,43 @@ var SmartComplySDK = (() => {
|
|
|
7084
7381
|
}
|
|
7085
7382
|
};
|
|
7086
7383
|
document.addEventListener("keydown", this._keydownHandler);
|
|
7384
|
+
const REBACKGROUND_RESTART_MS = 3e3;
|
|
7385
|
+
this._visibilityHandler = () => {
|
|
7386
|
+
if (this.isDestroyed) return;
|
|
7387
|
+
if (document.hidden) {
|
|
7388
|
+
this._hiddenAtMs = Date.now();
|
|
7389
|
+
return;
|
|
7390
|
+
}
|
|
7391
|
+
const hiddenAt = this._hiddenAtMs;
|
|
7392
|
+
this._hiddenAtMs = null;
|
|
7393
|
+
if (hiddenAt === null) return;
|
|
7394
|
+
const awayMs = Date.now() - hiddenAt;
|
|
7395
|
+
if (awayMs >= REBACKGROUND_RESTART_MS && this.currentStep === "liveness") {
|
|
7396
|
+
this.showStep("liveness");
|
|
7397
|
+
}
|
|
7398
|
+
};
|
|
7399
|
+
document.addEventListener("visibilitychange", this._visibilityHandler);
|
|
7400
|
+
this._historyGuardActive = true;
|
|
7401
|
+
try {
|
|
7402
|
+
history.pushState({ scFlowGuard: true }, "");
|
|
7403
|
+
} catch {
|
|
7404
|
+
this._historyGuardActive = false;
|
|
7405
|
+
}
|
|
7406
|
+
this._popstateHandler = () => {
|
|
7407
|
+
if (this.isDestroyed || !this._historyGuardActive) return;
|
|
7408
|
+
const backAction = this._getBackAction(this.currentStep);
|
|
7409
|
+
if (backAction) {
|
|
7410
|
+
backAction();
|
|
7411
|
+
} else {
|
|
7412
|
+
this.close();
|
|
7413
|
+
return;
|
|
7414
|
+
}
|
|
7415
|
+
try {
|
|
7416
|
+
history.pushState({ scFlowGuard: true }, "");
|
|
7417
|
+
} catch {
|
|
7418
|
+
}
|
|
7419
|
+
};
|
|
7420
|
+
window.addEventListener("popstate", this._popstateHandler);
|
|
7087
7421
|
}
|
|
7088
7422
|
async initialize() {
|
|
7089
7423
|
this.showStep("loading");
|
|
@@ -7324,10 +7658,10 @@ var SmartComplySDK = (() => {
|
|
|
7324
7658
|
container.appendChild(btn);
|
|
7325
7659
|
const trust = document.createElement("div");
|
|
7326
7660
|
trust.style.cssText = `
|
|
7327
|
-
display:flex;align-items:
|
|
7328
|
-
color:${this.theme.textMuted};font-size:10.5px;
|
|
7661
|
+
display:flex;align-items:flex-start;gap:6px;text-align:left;
|
|
7662
|
+
color:${this.theme.textMuted};font-size:10.5px;line-height:1.5;
|
|
7329
7663
|
`;
|
|
7330
|
-
trust.
|
|
7664
|
+
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
7665
|
container.appendChild(trust);
|
|
7332
7666
|
}
|
|
7333
7667
|
// ── Country Select ──────────────────────────────────────────────
|
|
@@ -8270,6 +8604,7 @@ var SmartComplySDK = (() => {
|
|
|
8270
8604
|
this._renderRetryLimitReached(container);
|
|
8271
8605
|
return;
|
|
8272
8606
|
}
|
|
8607
|
+
const attemptId = ++this._livenessAttemptId;
|
|
8273
8608
|
const title = this.createStepTitle("Face verification");
|
|
8274
8609
|
container.appendChild(title);
|
|
8275
8610
|
const desc = document.createElement("div");
|
|
@@ -8311,9 +8646,10 @@ var SmartComplySDK = (() => {
|
|
|
8311
8646
|
warning: this.theme.warning
|
|
8312
8647
|
}
|
|
8313
8648
|
},
|
|
8314
|
-
[
|
|
8649
|
+
[...DEFAULT_CHALLENGE_ACTIONS],
|
|
8315
8650
|
this.livenessEntryId || void 0
|
|
8316
8651
|
).then((result) => {
|
|
8652
|
+
if (attemptId !== this._livenessAttemptId) return;
|
|
8317
8653
|
if (result.status === "failed") {
|
|
8318
8654
|
this._livenessFailures++;
|
|
8319
8655
|
}
|
|
@@ -8322,8 +8658,11 @@ var SmartComplySDK = (() => {
|
|
|
8322
8658
|
}
|
|
8323
8659
|
this.showStep("done");
|
|
8324
8660
|
}).catch((err) => {
|
|
8325
|
-
this.
|
|
8326
|
-
|
|
8661
|
+
if (attemptId !== this._livenessAttemptId) return;
|
|
8662
|
+
if (!_isInfrastructureError(err)) {
|
|
8663
|
+
this._livenessFailures++;
|
|
8664
|
+
}
|
|
8665
|
+
this.showErrorStep(_userFacingLivenessError(err));
|
|
8327
8666
|
});
|
|
8328
8667
|
}
|
|
8329
8668
|
// ── Error ───────────────────────────────────────────────────────
|
|
@@ -8605,6 +8944,30 @@ var SmartComplySDK = (() => {
|
|
|
8605
8944
|
const allFields = Object.values(channels).flat().flatMap((ch) => ch.fields || []);
|
|
8606
8945
|
return allFields.some((f2) => isUploadFieldType(f2.type));
|
|
8607
8946
|
}
|
|
8947
|
+
/**
|
|
8948
|
+
* The "go back one step" target for a given step, if that step is
|
|
8949
|
+
* navigable — shared by the header back button and the browser
|
|
8950
|
+
* back-button (popstate) handler in mount() so both agree on the same
|
|
8951
|
+
* step order.
|
|
8952
|
+
*/
|
|
8953
|
+
_getBackAction(step) {
|
|
8954
|
+
const countries = Object.keys(this.sdkConfig?.channels || {});
|
|
8955
|
+
const backTargets = {
|
|
8956
|
+
id_type: () => this.showStep(countries.length > 1 ? "country" : "welcome"),
|
|
8957
|
+
country: () => this.showStep("welcome"),
|
|
8958
|
+
id_input: () => this.showStep("id_type"),
|
|
8959
|
+
confirm_identity: () => this.showStep("id_input"),
|
|
8960
|
+
document_capture: () => {
|
|
8961
|
+
this.docCapture?.destroy();
|
|
8962
|
+
this.showStep("id_type");
|
|
8963
|
+
},
|
|
8964
|
+
document_confirm: () => {
|
|
8965
|
+
this.docCapture?.destroy();
|
|
8966
|
+
this.showStep("document_capture");
|
|
8967
|
+
}
|
|
8968
|
+
};
|
|
8969
|
+
return backTargets[step];
|
|
8970
|
+
}
|
|
8608
8971
|
updateProgress() {
|
|
8609
8972
|
const isDocFlow = this.isDocumentFlow();
|
|
8610
8973
|
const countries = Object.keys(this.sdkConfig?.channels || {});
|
|
@@ -8642,21 +9005,7 @@ var SmartComplySDK = (() => {
|
|
|
8642
9005
|
if (dots[1]) dots[1].style.background = stage2Active ? this.theme.primary : this.theme.border;
|
|
8643
9006
|
}
|
|
8644
9007
|
if (this.headerBackBtn) {
|
|
8645
|
-
const
|
|
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];
|
|
9008
|
+
const backAction = this._getBackAction(this.currentStep);
|
|
8660
9009
|
if (backAction) {
|
|
8661
9010
|
this.headerBackBtn.style.display = "flex";
|
|
8662
9011
|
const newBtn = this.headerBackBtn.cloneNode(true);
|