smartcomply-web-sdk 1.0.80 → 1.0.82
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 +42 -5
- package/dist/esm/flow/DocumentCapture.d.ts.map +1 -1
- package/dist/esm/flow/DocumentCapture.js +176 -19
- 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 +42 -5
- package/dist/flow/DocumentCapture.d.ts.map +1 -1
- package/dist/flow/DocumentCapture.js +175 -18
- 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 +256 -11
- 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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ActionDetector.d.ts","sourceRoot":"","sources":["../../src/camera/ActionDetector.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"ActionDetector.d.ts","sourceRoot":"","sources":["../../src/camera/ActionDetector.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAwBrD,MAAM,WAAW,YAAY;IAC3B,YAAY,EAAE,OAAO,CAAC;IACtB,aAAa,EAAE,OAAO,CAAC;IACvB,cAAc,EAAE,OAAO,CAAC;IACxB,iBAAiB,EAAE,OAAO,CAAC;CAC5B;AAED,qBAAa,cAAc;IACzB,OAAO,CAAC,cAAc,CAAK;IAC3B,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,WAAW,CAAuB;IAE1C,KAAK,CAAC,MAAM,EAAE,mBAAmB,GAAG,YAAY;IA+BhD,OAAO,CAAC,aAAa;IASrB,OAAO,CAAC,UAAU;CAInB"}
|
|
@@ -11,10 +11,12 @@ exports.ActionDetector = void 0;
|
|
|
11
11
|
* commanding specific actions in order. This mirrors a passive liveness scan
|
|
12
12
|
* rather than a step-by-step obstacle course.
|
|
13
13
|
*
|
|
14
|
-
* Pass criteria:
|
|
15
|
-
* detected
|
|
16
|
-
*
|
|
17
|
-
*
|
|
14
|
+
* Pass criteria: BOTH a genuine blink AND a natural head-yaw excursion must
|
|
15
|
+
* be detected during the window, in either order — one signal alone is too
|
|
16
|
+
* easy for a held-up photo or a short video replay to trigger by accident
|
|
17
|
+
* (e.g. incidental camera/device motion registering as "head-yaw" with no
|
|
18
|
+
* live person present). Requiring both raises the bar without going back to
|
|
19
|
+
* commanded, sequential actions.
|
|
18
20
|
*/
|
|
19
21
|
const BLINK_THRESHOLD = 0.25;
|
|
20
22
|
const BLINK_HOLD_FRAMES = 2;
|
|
@@ -59,7 +61,7 @@ class ActionDetector {
|
|
|
59
61
|
faceDetected,
|
|
60
62
|
blinkDetected: this.blinkDetected,
|
|
61
63
|
motionDetected: this.motionDetected,
|
|
62
|
-
livenessConfirmed: this.blinkDetected
|
|
64
|
+
livenessConfirmed: this.blinkDetected && this.motionDetected,
|
|
63
65
|
};
|
|
64
66
|
}
|
|
65
67
|
getHeadYaw(matrix) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ActionDetector.js","sourceRoot":"","sources":["../../src/camera/ActionDetector.ts"],"names":[],"mappings":";;;AAEA
|
|
1
|
+
{"version":3,"file":"ActionDetector.js","sourceRoot":"","sources":["../../src/camera/ActionDetector.ts"],"names":[],"mappings":";;;AAEA;;;;;;;;;;;;;;;;GAgBG;AAEH,MAAM,eAAe,GAAG,IAAI,CAAC;AAC7B,MAAM,iBAAiB,GAAG,CAAC,CAAC;AAC5B,MAAM,wBAAwB,GAAG,CAAC,CAAC,CAAC,oDAAoD;AASxF,MAAa,cAAc;IAA3B;QACU,mBAAc,GAAG,CAAC,CAAC;QACnB,kBAAa,GAAG,KAAK,CAAC;QACtB,mBAAc,GAAG,KAAK,CAAC;QACvB,gBAAW,GAAkB,IAAI,CAAC;IA8C5C,CAAC;IA5CC,KAAK,CAAC,MAA2B;QAC/B,IAAI,CAAC,MAAM,CAAC,YAAY,EAAE,CAAC;YACzB,OAAO,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;QACnC,CAAC;QAED,MAAM,EAAE,GAAG,MAAM,CAAC,WAAW,CAAC;QAC9B,MAAM,SAAS,GAAG,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;QAEvF,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,CAAC;YACxB,IAAI,SAAS,IAAI,eAAe,EAAE,CAAC;gBACjC,IAAI,CAAC,cAAc,EAAE,CAAC;gBACtB,IAAI,IAAI,CAAC,cAAc,IAAI,iBAAiB,EAAE,CAAC;oBAC7C,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC;gBAC5B,CAAC;YACH,CAAC;iBAAM,CAAC;gBACN,IAAI,CAAC,cAAc,GAAG,CAAC,CAAC;YAC1B,CAAC;QACH,CAAC;QAED,IAAI,CAAC,IAAI,CAAC,cAAc,EAAE,CAAC;YACzB,MAAM,GAAG,GAAG,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC;YACpD,IAAI,IAAI,CAAC,WAAW,KAAK,IAAI,EAAE,CAAC;gBAC9B,IAAI,CAAC,WAAW,GAAG,GAAG,CAAC;YACzB,CAAC;iBAAM,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,GAAG,IAAI,CAAC,WAAW,CAAC,IAAI,wBAAwB,EAAE,CAAC;gBACxE,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC;YAC7B,CAAC;QACH,CAAC;QAED,OAAO,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC;IAClC,CAAC;IAEO,aAAa,CAAC,YAAqB;QACzC,OAAO;YACL,YAAY;YACZ,aAAa,EAAE,IAAI,CAAC,aAAa;YACjC,cAAc,EAAE,IAAI,CAAC,cAAc;YACnC,iBAAiB,EAAE,IAAI,CAAC,aAAa,IAAI,IAAI,CAAC,cAAc;SAC7D,CAAC;IACJ,CAAC;IAEO,UAAU,CAAC,MAAuB;QACxC,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,MAAM,GAAG,EAAE;YAAE,OAAO,CAAC,CAAC;QAC5C,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,GAAG,IAAI,CAAC,EAAE,CAAC,CAAC;IAC7D,CAAC;CACF;AAlDD,wCAkDC"}
|
|
@@ -21,7 +21,7 @@ export declare class SmartComply {
|
|
|
21
21
|
* CDN tag they installed with (e.g. @1, which floats to the latest
|
|
22
22
|
* matching release) actually resolved to the version they expect.
|
|
23
23
|
*/
|
|
24
|
-
static readonly VERSION = "1.0.
|
|
24
|
+
static readonly VERSION = "1.0.82";
|
|
25
25
|
private http;
|
|
26
26
|
private clientId;
|
|
27
27
|
private _sessionToken;
|
|
@@ -85,5 +85,5 @@ exports.SmartComply = SmartComply;
|
|
|
85
85
|
* CDN tag they installed with (e.g. @1, which floats to the latest
|
|
86
86
|
* matching release) actually resolved to the version they expect.
|
|
87
87
|
*/
|
|
88
|
-
SmartComply.VERSION = "1.0.
|
|
88
|
+
SmartComply.VERSION = "1.0.82";
|
|
89
89
|
//# sourceMappingURL=Smartcomply.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ActionDetector.d.ts","sourceRoot":"","sources":["../../../src/camera/ActionDetector.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"ActionDetector.d.ts","sourceRoot":"","sources":["../../../src/camera/ActionDetector.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAwBrD,MAAM,WAAW,YAAY;IAC3B,YAAY,EAAE,OAAO,CAAC;IACtB,aAAa,EAAE,OAAO,CAAC;IACvB,cAAc,EAAE,OAAO,CAAC;IACxB,iBAAiB,EAAE,OAAO,CAAC;CAC5B;AAED,qBAAa,cAAc;IACzB,OAAO,CAAC,cAAc,CAAK;IAC3B,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,WAAW,CAAuB;IAE1C,KAAK,CAAC,MAAM,EAAE,mBAAmB,GAAG,YAAY;IA+BhD,OAAO,CAAC,aAAa;IASrB,OAAO,CAAC,UAAU;CAInB"}
|
|
@@ -8,10 +8,12 @@
|
|
|
8
8
|
* commanding specific actions in order. This mirrors a passive liveness scan
|
|
9
9
|
* rather than a step-by-step obstacle course.
|
|
10
10
|
*
|
|
11
|
-
* Pass criteria:
|
|
12
|
-
* detected
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* Pass criteria: BOTH a genuine blink AND a natural head-yaw excursion must
|
|
12
|
+
* be detected during the window, in either order — one signal alone is too
|
|
13
|
+
* easy for a held-up photo or a short video replay to trigger by accident
|
|
14
|
+
* (e.g. incidental camera/device motion registering as "head-yaw" with no
|
|
15
|
+
* live person present). Requiring both raises the bar without going back to
|
|
16
|
+
* commanded, sequential actions.
|
|
15
17
|
*/
|
|
16
18
|
const BLINK_THRESHOLD = 0.25;
|
|
17
19
|
const BLINK_HOLD_FRAMES = 2;
|
|
@@ -56,7 +58,7 @@ export class ActionDetector {
|
|
|
56
58
|
faceDetected,
|
|
57
59
|
blinkDetected: this.blinkDetected,
|
|
58
60
|
motionDetected: this.motionDetected,
|
|
59
|
-
livenessConfirmed: this.blinkDetected
|
|
61
|
+
livenessConfirmed: this.blinkDetected && this.motionDetected,
|
|
60
62
|
};
|
|
61
63
|
}
|
|
62
64
|
getHeadYaw(matrix) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ActionDetector.js","sourceRoot":"","sources":["../../../src/camera/ActionDetector.ts"],"names":[],"mappings":"AAEA
|
|
1
|
+
{"version":3,"file":"ActionDetector.js","sourceRoot":"","sources":["../../../src/camera/ActionDetector.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;GAgBG;AAEH,MAAM,eAAe,GAAG,IAAI,CAAC;AAC7B,MAAM,iBAAiB,GAAG,CAAC,CAAC;AAC5B,MAAM,wBAAwB,GAAG,CAAC,CAAC,CAAC,oDAAoD;AASxF,MAAM,OAAO,cAAc;IAA3B;QACU,mBAAc,GAAG,CAAC,CAAC;QACnB,kBAAa,GAAG,KAAK,CAAC;QACtB,mBAAc,GAAG,KAAK,CAAC;QACvB,gBAAW,GAAkB,IAAI,CAAC;IA8C5C,CAAC;IA5CC,KAAK,CAAC,MAA2B;QAC/B,IAAI,CAAC,MAAM,CAAC,YAAY,EAAE,CAAC;YACzB,OAAO,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;QACnC,CAAC;QAED,MAAM,EAAE,GAAG,MAAM,CAAC,WAAW,CAAC;QAC9B,MAAM,SAAS,GAAG,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;QAEvF,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,CAAC;YACxB,IAAI,SAAS,IAAI,eAAe,EAAE,CAAC;gBACjC,IAAI,CAAC,cAAc,EAAE,CAAC;gBACtB,IAAI,IAAI,CAAC,cAAc,IAAI,iBAAiB,EAAE,CAAC;oBAC7C,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC;gBAC5B,CAAC;YACH,CAAC;iBAAM,CAAC;gBACN,IAAI,CAAC,cAAc,GAAG,CAAC,CAAC;YAC1B,CAAC;QACH,CAAC;QAED,IAAI,CAAC,IAAI,CAAC,cAAc,EAAE,CAAC;YACzB,MAAM,GAAG,GAAG,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC;YACpD,IAAI,IAAI,CAAC,WAAW,KAAK,IAAI,EAAE,CAAC;gBAC9B,IAAI,CAAC,WAAW,GAAG,GAAG,CAAC;YACzB,CAAC;iBAAM,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,GAAG,IAAI,CAAC,WAAW,CAAC,IAAI,wBAAwB,EAAE,CAAC;gBACxE,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC;YAC7B,CAAC;QACH,CAAC;QAED,OAAO,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC;IAClC,CAAC;IAEO,aAAa,CAAC,YAAqB;QACzC,OAAO;YACL,YAAY;YACZ,aAAa,EAAE,IAAI,CAAC,aAAa;YACjC,cAAc,EAAE,IAAI,CAAC,cAAc;YACnC,iBAAiB,EAAE,IAAI,CAAC,aAAa,IAAI,IAAI,CAAC,cAAc;SAC7D,CAAC;IACJ,CAAC;IAEO,UAAU,CAAC,MAAuB;QACxC,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,MAAM,GAAG,EAAE;YAAE,OAAO,CAAC,CAAC;QAC5C,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,GAAG,IAAI,CAAC,EAAE,CAAC,CAAC;IAC7D,CAAC;CACF"}
|
|
@@ -21,7 +21,7 @@ export declare class SmartComply {
|
|
|
21
21
|
* CDN tag they installed with (e.g. @1, which floats to the latest
|
|
22
22
|
* matching release) actually resolved to the version they expect.
|
|
23
23
|
*/
|
|
24
|
-
static readonly VERSION = "1.0.
|
|
24
|
+
static readonly VERSION = "1.0.82";
|
|
25
25
|
private http;
|
|
26
26
|
private clientId;
|
|
27
27
|
private _sessionToken;
|
|
@@ -81,5 +81,5 @@ export class SmartComply {
|
|
|
81
81
|
* CDN tag they installed with (e.g. @1, which floats to the latest
|
|
82
82
|
* matching release) actually resolved to the version they expect.
|
|
83
83
|
*/
|
|
84
|
-
SmartComply.VERSION = "1.0.
|
|
84
|
+
SmartComply.VERSION = "1.0.82";
|
|
85
85
|
//# sourceMappingURL=Smartcomply.js.map
|
|
@@ -27,6 +27,9 @@ export declare class DocumentCapture {
|
|
|
27
27
|
private backBlob;
|
|
28
28
|
private currentSide;
|
|
29
29
|
private requiresBackOverride;
|
|
30
|
+
private framingTimer;
|
|
31
|
+
private framingStableCount;
|
|
32
|
+
private framingAutoTriggered;
|
|
30
33
|
/**
|
|
31
34
|
* Mount the document capture UI and return a promise that resolves
|
|
32
35
|
* with the captured document image(s).
|
|
@@ -58,15 +61,49 @@ export declare class DocumentCapture {
|
|
|
58
61
|
*/
|
|
59
62
|
private _watchOrientation;
|
|
60
63
|
private _unwatchOrientation;
|
|
64
|
+
/**
|
|
65
|
+
* Auto-capture-on-stability — the same pattern the liveness module already
|
|
66
|
+
* uses for face-lock (see liveness.ts's checkFace), applied to documents:
|
|
67
|
+
* poll the raw video frame on an interval, score it against the guide box
|
|
68
|
+
* with assessFraming() (see utils/ImageQuality.ts), and fire the shutter
|
|
69
|
+
* automatically once several consecutive samples score as "framed". This
|
|
70
|
+
* is industry-standard for document capture (Persona/Onfido/Jumio-style
|
|
71
|
+
* SDKs all auto-capture rather than trust a manual tap against a static
|
|
72
|
+
* guide overlay) — the guide box alone never blocked a badly cropped tap.
|
|
73
|
+
*
|
|
74
|
+
* The manual capture button still works throughout and always wins
|
|
75
|
+
* (framingAutoTriggered guards against a race between the two).
|
|
76
|
+
*/
|
|
77
|
+
private _startFramingLoop;
|
|
78
|
+
private _stopFramingLoop;
|
|
61
79
|
private openCamera;
|
|
62
80
|
private captureFrame;
|
|
63
81
|
private showPreview;
|
|
64
82
|
/**
|
|
65
|
-
* Runs the shared client-side quality
|
|
66
|
-
* brightness
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
83
|
+
* Runs the shared client-side quality heuristics — blur/glare/contrast/
|
|
84
|
+
* brightness (assessQuality) and document framing/completeness
|
|
85
|
+
* (assessFraming) — against the just-captured or just-uploaded photo, and
|
|
86
|
+
* shows a plain-language nudge on the preview screen if either scores
|
|
87
|
+
* poorly. This is the one chokepoint both the camera and manual-upload
|
|
88
|
+
* paths funnel through (see showPreview), so it's the only place that
|
|
89
|
+
* reliably catches a badly cropped document regardless of how the photo
|
|
90
|
+
* arrived — the camera's own auto-capture framing loop only guards live
|
|
91
|
+
* capture, and does nothing for an uploaded file or a manual shutter tap
|
|
92
|
+
* fired before the loop ever samples.
|
|
93
|
+
*
|
|
94
|
+
* Framing here checks the WHOLE image (guideBoxPct = full frame), not a
|
|
95
|
+
* guide-box region — there's no guide overlay for an arbitrary uploaded
|
|
96
|
+
* photo, so "does the document fill most of the frame" is the correct
|
|
97
|
+
* question, matching the same border-edge + interior-variance heuristic
|
|
98
|
+
* the camera loop already uses.
|
|
99
|
+
*
|
|
100
|
+
* A framing failure disables useBtn — unlike the blur/glare nudge, this
|
|
101
|
+
* one blocks: a cropped ID reads as a confident, wrong identity downstream
|
|
102
|
+
* (partial OCR fields, a registry check against a truncated ID number)
|
|
103
|
+
* with no further chance to catch it, so the user must retake/re-upload
|
|
104
|
+
* rather than being trusted to notice an advisory nudge and act on it
|
|
105
|
+
* voluntarily. Fails open on decode error — this heuristic not running at
|
|
106
|
+
* all must not lock the user out of a photo it never actually scored.
|
|
70
107
|
*/
|
|
71
108
|
private _checkPhotoQuality;
|
|
72
109
|
/** Decodes to a canvas, optionally downscaled to maxDim on the long edge (never upscales). */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DocumentCapture.d.ts","sourceRoot":"","sources":["../../../src/flow/DocumentCapture.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAgB,MAAM,SAAS,CAAC;AACpD,OAAO,EAAuC,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAQ3F;;;;;;;;GAQG;AACH,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,IAAI,CAAC;IACZ,IAAI,CAAC,EAAE,IAAI,CAAC;IAKZ,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,IAAI,CAA+B;IAC3C,OAAO,CAAC,OAAO,CAAiC;IAChD,OAAO,CAAC,WAAW,CAA+B;IAClD,OAAO,CAAC,aAAa,CAAsC;IAC3D,OAAO,CAAC,kBAAkB,CAA6B;IACvD,OAAO,CAAC,cAAc,CAA0D;IAChF,OAAO,CAAC,aAAa,CAAuC;IAC5D,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,SAAS,CAAqB;IACtC,OAAO,CAAC,QAAQ,CAAqB;IACrC,OAAO,CAAC,WAAW,CAA6B;IAChD,OAAO,CAAC,oBAAoB,CAAuC;
|
|
1
|
+
{"version":3,"file":"DocumentCapture.d.ts","sourceRoot":"","sources":["../../../src/flow/DocumentCapture.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAgB,MAAM,SAAS,CAAC;AACpD,OAAO,EAAuC,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAQ3F;;;;;;;;GAQG;AACH,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,IAAI,CAAC;IACZ,IAAI,CAAC,EAAE,IAAI,CAAC;IAKZ,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,IAAI,CAA+B;IAC3C,OAAO,CAAC,OAAO,CAAiC;IAChD,OAAO,CAAC,WAAW,CAA+B;IAClD,OAAO,CAAC,aAAa,CAAsC;IAC3D,OAAO,CAAC,kBAAkB,CAA6B;IACvD,OAAO,CAAC,cAAc,CAA0D;IAChF,OAAO,CAAC,aAAa,CAAuC;IAC5D,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,SAAS,CAAqB;IACtC,OAAO,CAAC,QAAQ,CAAqB;IACrC,OAAO,CAAC,WAAW,CAA6B;IAChD,OAAO,CAAC,oBAAoB,CAAuC;IACnE,OAAO,CAAC,YAAY,CAA8C;IAClE,OAAO,CAAC,kBAAkB,CAAK;IAC/B,OAAO,CAAC,oBAAoB,CAAS;IAErC;;;OAGG;IACH,OAAO,CACL,SAAS,EAAE,WAAW,EACtB,KAAK,EAAE,WAAW,EAClB,YAAY,EAAE,MAAM,EACpB,YAAY,CAAC,EAAE,YAAY,GAC1B,OAAO,CAAC,qBAAqB,CAAC;IAiCjC,OAAO,IAAI,IAAI;IAuBf,OAAO,CAAC,eAAe;IAOvB,8EAA8E;IAC9E,OAAO,CAAC,qBAAqB;IAI7B,0EAA0E;IAC1E,OAAO,CAAC,kBAAkB;IAI1B,OAAO,CAAC,kBAAkB;IA4L1B,wDAAwD;IACxD,OAAO,CAAC,QAAQ;IAQhB,OAAO,CAAC,gBAAgB;IA0IxB;;;;;;OAMG;IACH,OAAO,CAAC,YAAY;IAQpB;;;;;OAKG;IACH,OAAO,CAAC,iBAAiB;IAOzB,OAAO,CAAC,mBAAmB;IAQ3B;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,iBAAiB;IA2FzB,OAAO,CAAC,gBAAgB;YAOV,UAAU;IAsExB,OAAO,CAAC,YAAY;IA4BpB,OAAO,CAAC,WAAW;IA+JnB;;;;;;;;;;;;;;;;;;;;;;;;;OAyBG;YACW,kBAAkB;IAsDhC,8FAA8F;IAC9F,OAAO,CAAC,aAAa;IAqBrB;;;;;;;;OAQG;IACH,OAAO,CAAC,YAAY;IA2BpB,OAAO,CAAC,qBAAqB;IAyC7B,OAAO,CAAC,wBAAwB;IAShC,OAAO,CAAC,YAAY;IAsBpB;;;;;;;;OAQG;YACW,sBAAsB;IASpC,OAAO,CAAC,gBAAgB;IAKxB,OAAO,CAAC,UAAU;IAWlB,OAAO,CAAC,gBAAgB;IAgBxB,OAAO,CAAC,SAAS;IAoCjB,OAAO,CAAC,YAAY;IA4BpB,OAAO,CAAC,aAAa;IA4ErB;;;OAGG;IACH,OAAO,CAAC,oBAAoB;IAuC5B,OAAO,CAAC,gBAAgB;CAazB"}
|
|
@@ -3,7 +3,7 @@ import { requiresBackSide, computeScaleRatio } from "../utils/documentRules";
|
|
|
3
3
|
import { CameraManager, CameraAccessError } from "../camera/CameraManager";
|
|
4
4
|
import { isRetryableCameraError } from "../camera/Permission";
|
|
5
5
|
import { DOC_CAMERA_IDEAL_WIDTH, DOC_CAMERA_IDEAL_HEIGHT, MAX_DOCUMENT_BYTES } from "../utils/constants";
|
|
6
|
-
import { assessQuality } from "../utils/ImageQuality";
|
|
6
|
+
import { assessQuality, assessFraming } from "../utils/ImageQuality";
|
|
7
7
|
import { icon } from "./icons";
|
|
8
8
|
import { renderSpinner } from "./spinner";
|
|
9
9
|
export class DocumentCapture {
|
|
@@ -20,6 +20,9 @@ export class DocumentCapture {
|
|
|
20
20
|
this.backBlob = null;
|
|
21
21
|
this.currentSide = 'front';
|
|
22
22
|
this.requiresBackOverride = undefined;
|
|
23
|
+
this.framingTimer = null;
|
|
24
|
+
this.framingStableCount = 0;
|
|
25
|
+
this.framingAutoTriggered = false;
|
|
23
26
|
}
|
|
24
27
|
/**
|
|
25
28
|
* Mount the document capture UI and return a promise that resolves
|
|
@@ -329,7 +332,7 @@ export class DocumentCapture {
|
|
|
329
332
|
svg.appendChild(bracketGroup);
|
|
330
333
|
guideWrap.appendChild(svg);
|
|
331
334
|
wrapper.appendChild(guideWrap);
|
|
332
|
-
// Header — gradient scrim + document label
|
|
335
|
+
// Header — gradient scrim + document label + live framing status
|
|
333
336
|
const header = document.createElement("div");
|
|
334
337
|
header.style.cssText = `
|
|
335
338
|
position:absolute;top:0;left:0;right:0;
|
|
@@ -337,6 +340,13 @@ export class DocumentCapture {
|
|
|
337
340
|
padding:14px 16px 32px;text-align:center;
|
|
338
341
|
`;
|
|
339
342
|
header.innerHTML = `<div style="color:#fff;font-size:${TYPE.sm}px;font-weight:600;">Align your ${this.getDocumentLabel(documentType)} within the frame</div>`;
|
|
343
|
+
const framingStatus = document.createElement("div");
|
|
344
|
+
framingStatus.style.cssText = `
|
|
345
|
+
margin-top:6px;font-size:${TYPE.xs}px;font-weight:600;
|
|
346
|
+
color:rgba(255,255,255,0.7);transition:color 0.2s ease;
|
|
347
|
+
`;
|
|
348
|
+
framingStatus.textContent = "Fit the document inside the frame";
|
|
349
|
+
header.appendChild(framingStatus);
|
|
340
350
|
wrapper.appendChild(header);
|
|
341
351
|
// Footer — gradient scrim + tips
|
|
342
352
|
const footer = document.createElement("div");
|
|
@@ -371,11 +381,17 @@ export class DocumentCapture {
|
|
|
371
381
|
inner.style.transform = "translate(-50%,-50%) scale(1)";
|
|
372
382
|
});
|
|
373
383
|
captureBtn.addEventListener("click", () => {
|
|
384
|
+
this.framingAutoTriggered = true; // manual tap always wins, skip any pending auto-trigger
|
|
374
385
|
this.captureFrame(container, theme, documentType);
|
|
375
386
|
});
|
|
376
387
|
container.appendChild(captureBtn);
|
|
377
|
-
// Open camera
|
|
378
|
-
|
|
388
|
+
// Open camera, then start the auto-capture framing loop once the stream
|
|
389
|
+
// has real dimensions to sample against.
|
|
390
|
+
this.openCamera().then(() => {
|
|
391
|
+
if (this.videoEl) {
|
|
392
|
+
this.videoEl.addEventListener("loadedmetadata", () => this._startFramingLoop(container, theme, documentType, framingStatus), { once: true });
|
|
393
|
+
}
|
|
394
|
+
});
|
|
379
395
|
}
|
|
380
396
|
/**
|
|
381
397
|
* Matches the video box's aspect-ratio CSS to the stream's actual
|
|
@@ -412,6 +428,103 @@ export class DocumentCapture {
|
|
|
412
428
|
this.orientationHandler = null;
|
|
413
429
|
}
|
|
414
430
|
}
|
|
431
|
+
/**
|
|
432
|
+
* Auto-capture-on-stability — the same pattern the liveness module already
|
|
433
|
+
* uses for face-lock (see liveness.ts's checkFace), applied to documents:
|
|
434
|
+
* poll the raw video frame on an interval, score it against the guide box
|
|
435
|
+
* with assessFraming() (see utils/ImageQuality.ts), and fire the shutter
|
|
436
|
+
* automatically once several consecutive samples score as "framed". This
|
|
437
|
+
* is industry-standard for document capture (Persona/Onfido/Jumio-style
|
|
438
|
+
* SDKs all auto-capture rather than trust a manual tap against a static
|
|
439
|
+
* guide overlay) — the guide box alone never blocked a badly cropped tap.
|
|
440
|
+
*
|
|
441
|
+
* The manual capture button still works throughout and always wins
|
|
442
|
+
* (framingAutoTriggered guards against a race between the two).
|
|
443
|
+
*/
|
|
444
|
+
_startFramingLoop(container, theme, documentType, statusEl) {
|
|
445
|
+
if (!this.videoEl)
|
|
446
|
+
return;
|
|
447
|
+
this.framingStableCount = 0;
|
|
448
|
+
this.framingAutoTriggered = false;
|
|
449
|
+
const STABLE_REQUIRED = 3; // consecutive good samples before firing
|
|
450
|
+
const SAMPLE_INTERVAL_MS = 350; // cheap enough to run this often; not every animation frame
|
|
451
|
+
const MAX_WAIT_MS = 12000; // give up auto-capture, leave the manual button as the only path
|
|
452
|
+
const startTime = performance.now();
|
|
453
|
+
// Guide box in fractional coordinates, matching the on-screen CSS guide
|
|
454
|
+
// (width:84%, aspect-ratio:16/10, centered) — computed directly against
|
|
455
|
+
// the video's own pixel dimensions so no CSS/object-fit mapping is needed.
|
|
456
|
+
const boxWidthPct = 0.84;
|
|
457
|
+
const boxAspect = 16 / 10;
|
|
458
|
+
const tick = () => {
|
|
459
|
+
if (!this.videoEl || this.framingAutoTriggered)
|
|
460
|
+
return;
|
|
461
|
+
if (performance.now() - startTime >= MAX_WAIT_MS) {
|
|
462
|
+
statusEl.textContent = "Fit the document inside the frame";
|
|
463
|
+
return; // stop auto-detecting; manual capture button remains available
|
|
464
|
+
}
|
|
465
|
+
const w = this.videoEl.videoWidth;
|
|
466
|
+
const h = this.videoEl.videoHeight;
|
|
467
|
+
if (!w || !h || this.videoEl.readyState < 2) {
|
|
468
|
+
this.framingTimer = setTimeout(tick, SAMPLE_INTERVAL_MS);
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
try {
|
|
472
|
+
const boxWidthPx = w * boxWidthPct;
|
|
473
|
+
const boxHeightPx = boxWidthPx / boxAspect;
|
|
474
|
+
const boxXPct = (1 - boxWidthPct) / 2;
|
|
475
|
+
const boxHeightPct = boxHeightPx / h;
|
|
476
|
+
const boxYPct = (1 - boxHeightPct) / 2;
|
|
477
|
+
const canvas = document.createElement("canvas");
|
|
478
|
+
// Downscaled sampling copy only — matches _checkPhotoQuality's own
|
|
479
|
+
// reasoning for why this must not run against the full-resolution
|
|
480
|
+
// frame on every tick.
|
|
481
|
+
const scale = Math.min(1, 480 / w);
|
|
482
|
+
canvas.width = Math.round(w * scale);
|
|
483
|
+
canvas.height = Math.round(h * scale);
|
|
484
|
+
const ctx = canvas.getContext("2d", { willReadFrequently: true });
|
|
485
|
+
if (!ctx) {
|
|
486
|
+
this.framingTimer = setTimeout(tick, SAMPLE_INTERVAL_MS);
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
ctx.drawImage(this.videoEl, 0, 0, canvas.width, canvas.height);
|
|
490
|
+
const pixels = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
491
|
+
const result = assessFraming(pixels, {
|
|
492
|
+
x: boxXPct, y: boxYPct, width: boxWidthPct, height: boxHeightPct,
|
|
493
|
+
});
|
|
494
|
+
if (result.isFramed) {
|
|
495
|
+
this.framingStableCount++;
|
|
496
|
+
statusEl.style.color = theme.primary;
|
|
497
|
+
statusEl.textContent = this.framingStableCount >= STABLE_REQUIRED
|
|
498
|
+
? "Capturing…"
|
|
499
|
+
: "Hold steady…";
|
|
500
|
+
}
|
|
501
|
+
else {
|
|
502
|
+
this.framingStableCount = 0;
|
|
503
|
+
statusEl.style.color = "rgba(255,255,255,0.7)";
|
|
504
|
+
statusEl.textContent = result.interiorVariance < 0.25
|
|
505
|
+
? "Move closer — fill the frame with your document"
|
|
506
|
+
: "Align all 4 corners within the guide";
|
|
507
|
+
}
|
|
508
|
+
if (this.framingStableCount >= STABLE_REQUIRED) {
|
|
509
|
+
this.framingAutoTriggered = true;
|
|
510
|
+
this.captureFrame(container, theme, documentType);
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
catch {
|
|
515
|
+
// Advisory/auto-capture only — a scoring failure just falls back to
|
|
516
|
+
// the manual capture button, never blocks the flow.
|
|
517
|
+
}
|
|
518
|
+
this.framingTimer = setTimeout(tick, SAMPLE_INTERVAL_MS);
|
|
519
|
+
};
|
|
520
|
+
this.framingTimer = setTimeout(tick, SAMPLE_INTERVAL_MS);
|
|
521
|
+
}
|
|
522
|
+
_stopFramingLoop() {
|
|
523
|
+
if (this.framingTimer !== null) {
|
|
524
|
+
clearTimeout(this.framingTimer);
|
|
525
|
+
this.framingTimer = null;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
415
528
|
async openCamera() {
|
|
416
529
|
// Check browser compatibility
|
|
417
530
|
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
|
@@ -573,7 +686,7 @@ export class DocumentCapture {
|
|
|
573
686
|
URL.revokeObjectURL(currentUrl);
|
|
574
687
|
currentUrl = URL.createObjectURL(rotated);
|
|
575
688
|
img.src = currentUrl;
|
|
576
|
-
this._checkPhotoQuality(currentBlob, qualityNotice, theme);
|
|
689
|
+
this._checkPhotoQuality(currentBlob, qualityNotice, theme, useBtn);
|
|
577
690
|
}
|
|
578
691
|
catch {
|
|
579
692
|
// Leave the current photo untouched — rotation is a convenience,
|
|
@@ -595,12 +708,14 @@ export class DocumentCapture {
|
|
|
595
708
|
sublabel.style.cssText = `color:${theme.textMuted};font-size:${TYPE.xs}px;text-align:center;line-height:1.6;`;
|
|
596
709
|
sublabel.textContent = "All text must be readable and all four corners must be visible. Sideways or upside-down? Use Rotate on the photo above.";
|
|
597
710
|
this.root.appendChild(sublabel);
|
|
598
|
-
//
|
|
599
|
-
//
|
|
600
|
-
//
|
|
601
|
-
//
|
|
602
|
-
//
|
|
603
|
-
//
|
|
711
|
+
// Quality/framing notice. Blur/glare/contrast issues (assessQuality) stay
|
|
712
|
+
// advisory-only — a heuristic that's off shouldn't block a photo that's
|
|
713
|
+
// actually fine. A framing failure (document not fully in frame —
|
|
714
|
+
// assessFraming) DOES block "Use Photo": a cropped ID reads as a
|
|
715
|
+
// confident, wrong identity downstream (partial OCR fields, a registry
|
|
716
|
+
// check against a truncated ID number) with no further chance to catch
|
|
717
|
+
// it, so this is the one signal worth hard-blocking on rather than
|
|
718
|
+
// trusting the user to notice and retake voluntarily.
|
|
604
719
|
const qualityNotice = document.createElement("div");
|
|
605
720
|
qualityNotice.style.cssText = `
|
|
606
721
|
display:none;width:100%;padding:10px 14px;border-radius:${RADIUS.md}px;
|
|
@@ -609,7 +724,6 @@ export class DocumentCapture {
|
|
|
609
724
|
`;
|
|
610
725
|
qualityNotice.setAttribute("role", "status");
|
|
611
726
|
this.root.appendChild(qualityNotice);
|
|
612
|
-
this._checkPhotoQuality(currentBlob, qualityNotice, theme);
|
|
613
727
|
// Buttons
|
|
614
728
|
const btnRow = document.createElement("div");
|
|
615
729
|
btnRow.style.cssText = "display:flex;gap:12px;width:100%;";
|
|
@@ -635,15 +749,35 @@ export class DocumentCapture {
|
|
|
635
749
|
btnRow.appendChild(retakeBtn);
|
|
636
750
|
btnRow.appendChild(useBtn);
|
|
637
751
|
this.root.appendChild(btnRow);
|
|
752
|
+
this._checkPhotoQuality(currentBlob, qualityNotice, theme, useBtn);
|
|
638
753
|
}
|
|
639
754
|
/**
|
|
640
|
-
* Runs the shared client-side quality
|
|
641
|
-
* brightness
|
|
642
|
-
*
|
|
643
|
-
*
|
|
644
|
-
*
|
|
755
|
+
* Runs the shared client-side quality heuristics — blur/glare/contrast/
|
|
756
|
+
* brightness (assessQuality) and document framing/completeness
|
|
757
|
+
* (assessFraming) — against the just-captured or just-uploaded photo, and
|
|
758
|
+
* shows a plain-language nudge on the preview screen if either scores
|
|
759
|
+
* poorly. This is the one chokepoint both the camera and manual-upload
|
|
760
|
+
* paths funnel through (see showPreview), so it's the only place that
|
|
761
|
+
* reliably catches a badly cropped document regardless of how the photo
|
|
762
|
+
* arrived — the camera's own auto-capture framing loop only guards live
|
|
763
|
+
* capture, and does nothing for an uploaded file or a manual shutter tap
|
|
764
|
+
* fired before the loop ever samples.
|
|
765
|
+
*
|
|
766
|
+
* Framing here checks the WHOLE image (guideBoxPct = full frame), not a
|
|
767
|
+
* guide-box region — there's no guide overlay for an arbitrary uploaded
|
|
768
|
+
* photo, so "does the document fill most of the frame" is the correct
|
|
769
|
+
* question, matching the same border-edge + interior-variance heuristic
|
|
770
|
+
* the camera loop already uses.
|
|
771
|
+
*
|
|
772
|
+
* A framing failure disables useBtn — unlike the blur/glare nudge, this
|
|
773
|
+
* one blocks: a cropped ID reads as a confident, wrong identity downstream
|
|
774
|
+
* (partial OCR fields, a registry check against a truncated ID number)
|
|
775
|
+
* with no further chance to catch it, so the user must retake/re-upload
|
|
776
|
+
* rather than being trusted to notice an advisory nudge and act on it
|
|
777
|
+
* voluntarily. Fails open on decode error — this heuristic not running at
|
|
778
|
+
* all must not lock the user out of a photo it never actually scored.
|
|
645
779
|
*/
|
|
646
|
-
async _checkPhotoQuality(imageData, noticeEl, theme) {
|
|
780
|
+
async _checkPhotoQuality(imageData, noticeEl, theme, useBtn) {
|
|
647
781
|
try {
|
|
648
782
|
// Document photos are captured at up to DOC_CAMERA_IDEAL_WIDTH/HEIGHT
|
|
649
783
|
// (2000x1500) — scoring every pixel across four full-image passes in
|
|
@@ -656,6 +790,28 @@ export class DocumentCapture {
|
|
|
656
790
|
if (!ctx)
|
|
657
791
|
return;
|
|
658
792
|
const pixels = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
793
|
+
const framing = assessFraming(pixels, { x: 0, y: 0, width: 1, height: 1 });
|
|
794
|
+
if (!framing.isFramed) {
|
|
795
|
+
if (noticeEl.isConnected) {
|
|
796
|
+
noticeEl.style.display = "block";
|
|
797
|
+
noticeEl.textContent = framing.interiorVariance < 0.25
|
|
798
|
+
? "We couldn't detect a document in this photo — make sure the ID is clearly visible and try again."
|
|
799
|
+
: "Part of the document looks cut off — make sure all four corners are visible and try again.";
|
|
800
|
+
}
|
|
801
|
+
if (useBtn.isConnected) {
|
|
802
|
+
useBtn.disabled = true;
|
|
803
|
+
useBtn.style.opacity = "0.5";
|
|
804
|
+
useBtn.style.cursor = "not-allowed";
|
|
805
|
+
}
|
|
806
|
+
return; // framing issue is the more actionable/specific one — don't also show the quality nudge
|
|
807
|
+
}
|
|
808
|
+
// Framing passed — make sure a previous failed attempt (before Retake/
|
|
809
|
+
// Rotate) doesn't leave the button stuck disabled.
|
|
810
|
+
if (useBtn.isConnected) {
|
|
811
|
+
useBtn.disabled = false;
|
|
812
|
+
useBtn.style.opacity = "1";
|
|
813
|
+
useBtn.style.cursor = "pointer";
|
|
814
|
+
}
|
|
659
815
|
const result = assessQuality(pixels);
|
|
660
816
|
if (noticeEl.isConnected && !result.isValid) {
|
|
661
817
|
noticeEl.style.display = "block";
|
|
@@ -665,7 +821,7 @@ export class DocumentCapture {
|
|
|
665
821
|
}
|
|
666
822
|
}
|
|
667
823
|
catch {
|
|
668
|
-
//
|
|
824
|
+
// Fails open — no notice, button stays enabled, if scoring fails for any reason.
|
|
669
825
|
}
|
|
670
826
|
}
|
|
671
827
|
/** Decodes to a canvas, optionally downscaled to maxDim on the long edge (never upscales). */
|
|
@@ -802,6 +958,7 @@ export class DocumentCapture {
|
|
|
802
958
|
input.click();
|
|
803
959
|
}
|
|
804
960
|
stopCamera() {
|
|
961
|
+
this._stopFramingLoop();
|
|
805
962
|
this._unwatchOrientation();
|
|
806
963
|
this.cameraManager.stop();
|
|
807
964
|
if (this.videoEl) {
|