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