smartcomply-web-sdk 1.0.66 → 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.
- package/dist/esm/flow/DocumentCapture.d.ts +10 -0
- package/dist/esm/flow/DocumentCapture.d.ts.map +1 -1
- package/dist/esm/flow/DocumentCapture.js +83 -5
- package/dist/esm/flow/DocumentCapture.js.map +1 -1
- package/dist/esm/flow/SmartComplyFlow.d.ts +19 -0
- package/dist/esm/flow/SmartComplyFlow.d.ts.map +1 -1
- package/dist/esm/flow/SmartComplyFlow.js +216 -32
- package/dist/esm/flow/SmartComplyFlow.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/modules/liveness/liveness.js +1 -1
- package/dist/esm/modules/liveness/liveness.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/esm/utils/constants.d.ts +9 -6
- package/dist/esm/utils/constants.d.ts.map +1 -1
- package/dist/esm/utils/constants.js +9 -6
- package/dist/esm/utils/constants.js.map +1 -1
- package/dist/flow/DocumentCapture.d.ts +10 -0
- package/dist/flow/DocumentCapture.d.ts.map +1 -1
- package/dist/flow/DocumentCapture.js +83 -5
- package/dist/flow/DocumentCapture.js.map +1 -1
- package/dist/flow/SmartComplyFlow.d.ts +19 -0
- package/dist/flow/SmartComplyFlow.d.ts.map +1 -1
- package/dist/flow/SmartComplyFlow.js +216 -32
- package/dist/flow/SmartComplyFlow.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/modules/liveness/liveness.js +1 -1
- package/dist/modules/liveness/liveness.js.map +1 -1
- package/dist/smartcomply.browser.js +360 -43
- 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/dist/utils/constants.d.ts +9 -6
- package/dist/utils/constants.d.ts.map +1 -1
- package/dist/utils/constants.js +9 -6
- package/dist/utils/constants.js.map +1 -1
- package/package.json +1 -1
|
@@ -296,7 +296,7 @@ var SmartComplySDK = (() => {
|
|
|
296
296
|
var MAX_DOCUMENT_BYTES = 5 * 1024 * 1024;
|
|
297
297
|
var MAX_VIDEO_BYTES = 50 * 1024 * 1024;
|
|
298
298
|
var MAX_SNAPSHOT_BYTES = 2 * 1024 * 1024;
|
|
299
|
-
var DEFAULT_CHALLENGE_ACTIONS = ["BLINK"];
|
|
299
|
+
var DEFAULT_CHALLENGE_ACTIONS = ["BLINK", "TURN_HEAD"];
|
|
300
300
|
var LIVENESS_TIME_LIMIT_SECONDS = 12;
|
|
301
301
|
var DOC_CAMERA_IDEAL_WIDTH = 2e3;
|
|
302
302
|
var DOC_CAMERA_IDEAL_HEIGHT = 1500;
|
|
@@ -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
|
`;
|
|
@@ -5448,7 +5452,7 @@ var SmartComplySDK = (() => {
|
|
|
5448
5452
|
tempVideo.remove();
|
|
5449
5453
|
const uiChallenge = {
|
|
5450
5454
|
time_limit_seconds: LIVENESS_TIME_LIMIT_SECONDS,
|
|
5451
|
-
instruction: "Blink naturally and
|
|
5455
|
+
instruction: "Blink naturally and slowly turn your head from side to side"
|
|
5452
5456
|
};
|
|
5453
5457
|
ui2.unmount();
|
|
5454
5458
|
const videoEl = ui2.mount(container, { ...uiChallenge, instruction: "Starting scan…" });
|
|
@@ -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);
|
|
@@ -6417,7 +6550,7 @@ var SmartComplySDK = (() => {
|
|
|
6417
6550
|
if (img._blobUrl) {
|
|
6418
6551
|
URL.revokeObjectURL(img._blobUrl);
|
|
6419
6552
|
}
|
|
6420
|
-
useBtn.textContent = "
|
|
6553
|
+
useBtn.textContent = "Confirming photo…";
|
|
6421
6554
|
useBtn.disabled = true;
|
|
6422
6555
|
this.ensureUnderUploadLimit(imageData).then((c2) => this.handleCaptureComplete(c2, container, theme, documentType, viaCamera));
|
|
6423
6556
|
});
|
|
@@ -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
|
-
|
|
6519
|
-
|
|
6520
|
-
|
|
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");
|
|
@@ -7254,7 +7516,7 @@ var SmartComplySDK = (() => {
|
|
|
7254
7516
|
topRow.appendChild(badge);
|
|
7255
7517
|
container.appendChild(topRow);
|
|
7256
7518
|
const titleWrap = document.createElement("div");
|
|
7257
|
-
titleWrap.style.cssText = "display:flex;flex-direction:column;gap:5px;";
|
|
7519
|
+
titleWrap.style.cssText = "display:flex;flex-direction:column;align-items:center;gap:5px;width:100%;";
|
|
7258
7520
|
const title = document.createElement("h2");
|
|
7259
7521
|
title.style.cssText = `
|
|
7260
7522
|
color:${this.theme.text};font-size:${TYPE.heading}px;font-weight:700;
|
|
@@ -7266,7 +7528,7 @@ var SmartComplySDK = (() => {
|
|
|
7266
7528
|
const desc = document.createElement("p");
|
|
7267
7529
|
desc.style.cssText = `
|
|
7268
7530
|
color:${this.theme.textSecondary};font-size:13px;line-height:1.5;
|
|
7269
|
-
margin:0;max-width:300px;
|
|
7531
|
+
margin:0 auto;max-width:300px;text-align:center;
|
|
7270
7532
|
`;
|
|
7271
7533
|
desc.textContent = isDocFlow ? "You'll need your document and camera access. This takes about 2 minutes." : "You'll need your ID number and camera access. This takes about 2 minutes.";
|
|
7272
7534
|
titleWrap.appendChild(desc);
|
|
@@ -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:
|
|
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.
|
|
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 ──────────────────────────────────────────────
|
|
@@ -7802,6 +8064,10 @@ var SmartComplySDK = (() => {
|
|
|
7802
8064
|
this._renderRetryLimitReached(container);
|
|
7803
8065
|
return;
|
|
7804
8066
|
}
|
|
8067
|
+
if (!this._ocrName && !this._ocrDocNumber && !this._ocrExpiry) {
|
|
8068
|
+
this._renderOcrTimeout(container);
|
|
8069
|
+
return;
|
|
8070
|
+
}
|
|
7805
8071
|
if (this._documentViaCamera) this._prewarmCamera();
|
|
7806
8072
|
const stepBadge = document.createElement("div");
|
|
7807
8073
|
stepBadge.style.cssText = `
|
|
@@ -7952,11 +8218,8 @@ var SmartComplySDK = (() => {
|
|
|
7952
8218
|
if (polls < MAX_POLLS && !this.isDestroyed) {
|
|
7953
8219
|
this._ocrPollTimer = setTimeout(poll, 2e3);
|
|
7954
8220
|
} else if (!this.isDestroyed) {
|
|
7955
|
-
|
|
7956
|
-
|
|
7957
|
-
this._ocrPollTimer = setTimeout(() => {
|
|
7958
|
-
if (!this.isDestroyed) this.showStep("document_confirm");
|
|
7959
|
-
}, 2e3);
|
|
8221
|
+
container.innerHTML = "";
|
|
8222
|
+
this._renderOcrTimeout(container);
|
|
7960
8223
|
}
|
|
7961
8224
|
} catch {
|
|
7962
8225
|
if (polls < MAX_POLLS && !this.isDestroyed) {
|
|
@@ -8021,6 +8284,35 @@ var SmartComplySDK = (() => {
|
|
|
8021
8284
|
});
|
|
8022
8285
|
container.appendChild(retryBtn);
|
|
8023
8286
|
}
|
|
8287
|
+
/**
|
|
8288
|
+
* OCR ran to completion but never produced a single usable field within
|
|
8289
|
+
* the poll window — do not let the flow reach liveness with no verified
|
|
8290
|
+
* identity data. Distinct from _renderOcrQualityFail (a bad photo) since
|
|
8291
|
+
* this covers a genuinely stuck/slow OCR pass instead of a rejected image.
|
|
8292
|
+
*/
|
|
8293
|
+
_renderOcrTimeout(container) {
|
|
8294
|
+
container.style.cssText += "display:flex;flex-direction:column;align-items:center;text-align:center;gap:14px;padding:20px 0;";
|
|
8295
|
+
const icon2 = document.createElement("div");
|
|
8296
|
+
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;`;
|
|
8297
|
+
icon2.textContent = "⏱️";
|
|
8298
|
+
container.appendChild(icon2);
|
|
8299
|
+
const t2 = document.createElement("h2");
|
|
8300
|
+
t2.style.cssText = `color:${this.theme.text};font-size:${TYPE.heading}px;font-weight:700;margin:0;`;
|
|
8301
|
+
t2.textContent = "Couldn't Read Your Document";
|
|
8302
|
+
container.appendChild(t2);
|
|
8303
|
+
const d2 = document.createElement("p");
|
|
8304
|
+
d2.style.cssText = `color:${this.theme.textSecondary};font-size:13px;margin:0;max-width:280px;line-height:1.6;`;
|
|
8305
|
+
d2.textContent = "We weren't able to read any details from your document. Please retake the photo in good lighting, with all four corners visible.";
|
|
8306
|
+
container.appendChild(d2);
|
|
8307
|
+
const retryBtn = this.createPrimaryButton("Retake photo");
|
|
8308
|
+
retryBtn.addEventListener("click", () => {
|
|
8309
|
+
this.livenessEntryId = null;
|
|
8310
|
+
this.documentBlob = null;
|
|
8311
|
+
this.documentBackBlob = null;
|
|
8312
|
+
this.showStep("document_capture");
|
|
8313
|
+
});
|
|
8314
|
+
container.appendChild(retryBtn);
|
|
8315
|
+
}
|
|
8024
8316
|
// ── W11: You're all done (end-user final screen) ─────────────────
|
|
8025
8317
|
renderDone(container) {
|
|
8026
8318
|
container.style.cssText += "display:flex;flex-direction:column;align-items:center;text-align:center;gap:16px;padding:24px 0;";
|
|
@@ -8240,6 +8532,7 @@ var SmartComplySDK = (() => {
|
|
|
8240
8532
|
this._renderRetryLimitReached(container);
|
|
8241
8533
|
return;
|
|
8242
8534
|
}
|
|
8535
|
+
const attemptId = ++this._livenessAttemptId;
|
|
8243
8536
|
const title = this.createStepTitle("Face verification");
|
|
8244
8537
|
container.appendChild(title);
|
|
8245
8538
|
const desc = document.createElement("div");
|
|
@@ -8281,9 +8574,10 @@ var SmartComplySDK = (() => {
|
|
|
8281
8574
|
warning: this.theme.warning
|
|
8282
8575
|
}
|
|
8283
8576
|
},
|
|
8284
|
-
[
|
|
8577
|
+
[...DEFAULT_CHALLENGE_ACTIONS],
|
|
8285
8578
|
this.livenessEntryId || void 0
|
|
8286
8579
|
).then((result) => {
|
|
8580
|
+
if (attemptId !== this._livenessAttemptId) return;
|
|
8287
8581
|
if (result.status === "failed") {
|
|
8288
8582
|
this._livenessFailures++;
|
|
8289
8583
|
}
|
|
@@ -8292,8 +8586,11 @@ var SmartComplySDK = (() => {
|
|
|
8292
8586
|
}
|
|
8293
8587
|
this.showStep("done");
|
|
8294
8588
|
}).catch((err) => {
|
|
8295
|
-
this.
|
|
8296
|
-
|
|
8589
|
+
if (attemptId !== this._livenessAttemptId) return;
|
|
8590
|
+
if (!_isInfrastructureError(err)) {
|
|
8591
|
+
this._livenessFailures++;
|
|
8592
|
+
}
|
|
8593
|
+
this.showErrorStep(_userFacingLivenessError(err));
|
|
8297
8594
|
});
|
|
8298
8595
|
}
|
|
8299
8596
|
// ── Error ───────────────────────────────────────────────────────
|
|
@@ -8449,9 +8746,18 @@ var SmartComplySDK = (() => {
|
|
|
8449
8746
|
const powered = document.createElement("span");
|
|
8450
8747
|
powered.style.cssText = `font-size:11px;color:${this.theme.textMuted};`;
|
|
8451
8748
|
powered.textContent = "Powered by ";
|
|
8452
|
-
const brand = document.createElement("
|
|
8453
|
-
brand.
|
|
8749
|
+
const brand = document.createElement("a");
|
|
8750
|
+
brand.href = "https://adhere.smartcomply.com/";
|
|
8751
|
+
brand.target = "_blank";
|
|
8752
|
+
brand.rel = "noopener noreferrer";
|
|
8753
|
+
brand.style.cssText = `font-size:11px;font-weight:700;color:${this.theme.primary};text-decoration:none;`;
|
|
8454
8754
|
brand.textContent = "Adhere";
|
|
8755
|
+
brand.addEventListener("mouseenter", () => {
|
|
8756
|
+
brand.style.textDecoration = "underline";
|
|
8757
|
+
});
|
|
8758
|
+
brand.addEventListener("mouseleave", () => {
|
|
8759
|
+
brand.style.textDecoration = "none";
|
|
8760
|
+
});
|
|
8455
8761
|
footer.appendChild(powered);
|
|
8456
8762
|
footer.appendChild(brand);
|
|
8457
8763
|
return footer;
|
|
@@ -8488,18 +8794,19 @@ var SmartComplySDK = (() => {
|
|
|
8488
8794
|
});
|
|
8489
8795
|
card.addEventListener("click", onClick);
|
|
8490
8796
|
const textWrap = document.createElement("div");
|
|
8797
|
+
textWrap.style.cssText = "flex:1 1 auto;min-width:0;";
|
|
8491
8798
|
const titleEl = document.createElement("div");
|
|
8492
|
-
titleEl.style.cssText = `color:${this.theme.text};font-size:14px;font-weight:600;`;
|
|
8799
|
+
titleEl.style.cssText = `color:${this.theme.text};font-size:14px;font-weight:600;line-height:1.35;`;
|
|
8493
8800
|
titleEl.textContent = title;
|
|
8494
8801
|
textWrap.appendChild(titleEl);
|
|
8495
8802
|
if (subtitle) {
|
|
8496
8803
|
const subEl = document.createElement("div");
|
|
8497
|
-
subEl.style.cssText = `color:${this.theme.textMuted};font-size:12px;margin-top:2px;`;
|
|
8804
|
+
subEl.style.cssText = `color:${this.theme.textMuted};font-size:12px;line-height:1.4;margin-top:2px;`;
|
|
8498
8805
|
subEl.textContent = subtitle;
|
|
8499
8806
|
textWrap.appendChild(subEl);
|
|
8500
8807
|
}
|
|
8501
8808
|
const arrow = document.createElement("span");
|
|
8502
|
-
arrow.style.cssText = `color:${this.theme.textMuted};font-size:18px;flex-shrink:0;`;
|
|
8809
|
+
arrow.style.cssText = `color:${this.theme.textMuted};font-size:18px;flex-shrink:0;margin-left:12px;line-height:1;`;
|
|
8503
8810
|
arrow.textContent = "→";
|
|
8504
8811
|
card.appendChild(textWrap);
|
|
8505
8812
|
card.appendChild(arrow);
|
|
@@ -8565,6 +8872,30 @@ var SmartComplySDK = (() => {
|
|
|
8565
8872
|
const allFields = Object.values(channels).flat().flatMap((ch) => ch.fields || []);
|
|
8566
8873
|
return allFields.some((f2) => isUploadFieldType(f2.type));
|
|
8567
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
|
+
}
|
|
8568
8899
|
updateProgress() {
|
|
8569
8900
|
const isDocFlow = this.isDocumentFlow();
|
|
8570
8901
|
const countries = Object.keys(this.sdkConfig?.channels || {});
|
|
@@ -8602,21 +8933,7 @@ var SmartComplySDK = (() => {
|
|
|
8602
8933
|
if (dots[1]) dots[1].style.background = stage2Active ? this.theme.primary : this.theme.border;
|
|
8603
8934
|
}
|
|
8604
8935
|
if (this.headerBackBtn) {
|
|
8605
|
-
const
|
|
8606
|
-
id_type: () => this.showStep(countries.length > 1 ? "country" : "welcome"),
|
|
8607
|
-
country: () => this.showStep("welcome"),
|
|
8608
|
-
id_input: () => this.showStep("id_type"),
|
|
8609
|
-
confirm_identity: () => this.showStep("id_input"),
|
|
8610
|
-
document_capture: () => {
|
|
8611
|
-
this.docCapture?.destroy();
|
|
8612
|
-
this.showStep("id_type");
|
|
8613
|
-
},
|
|
8614
|
-
document_confirm: () => {
|
|
8615
|
-
this.docCapture?.destroy();
|
|
8616
|
-
this.showStep("document_capture");
|
|
8617
|
-
}
|
|
8618
|
-
};
|
|
8619
|
-
const backAction = backTargets[this.currentStep];
|
|
8936
|
+
const backAction = this._getBackAction(this.currentStep);
|
|
8620
8937
|
if (backAction) {
|
|
8621
8938
|
this.headerBackBtn.style.display = "flex";
|
|
8622
8939
|
const newBtn = this.headerBackBtn.cloneNode(true);
|