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.
Files changed (37) hide show
  1. package/dist/camera/ActionDetector.d.ts.map +1 -1
  2. package/dist/camera/ActionDetector.js +7 -5
  3. package/dist/camera/ActionDetector.js.map +1 -1
  4. package/dist/client/Smartcomply.d.ts +1 -1
  5. package/dist/client/Smartcomply.js +1 -1
  6. package/dist/esm/camera/ActionDetector.d.ts.map +1 -1
  7. package/dist/esm/camera/ActionDetector.js +7 -5
  8. package/dist/esm/camera/ActionDetector.js.map +1 -1
  9. package/dist/esm/client/Smartcomply.d.ts +1 -1
  10. package/dist/esm/client/Smartcomply.js +1 -1
  11. package/dist/esm/flow/DocumentCapture.d.ts +42 -5
  12. package/dist/esm/flow/DocumentCapture.d.ts.map +1 -1
  13. package/dist/esm/flow/DocumentCapture.js +176 -19
  14. package/dist/esm/flow/DocumentCapture.js.map +1 -1
  15. package/dist/esm/flow/SmartComplyFlow.d.ts +10 -0
  16. package/dist/esm/flow/SmartComplyFlow.d.ts.map +1 -1
  17. package/dist/esm/flow/SmartComplyFlow.js +53 -0
  18. package/dist/esm/flow/SmartComplyFlow.js.map +1 -1
  19. package/dist/esm/utils/ImageQuality.d.ts +38 -0
  20. package/dist/esm/utils/ImageQuality.d.ts.map +1 -1
  21. package/dist/esm/utils/ImageQuality.js +98 -0
  22. package/dist/esm/utils/ImageQuality.js.map +1 -1
  23. package/dist/flow/DocumentCapture.d.ts +42 -5
  24. package/dist/flow/DocumentCapture.d.ts.map +1 -1
  25. package/dist/flow/DocumentCapture.js +175 -18
  26. package/dist/flow/DocumentCapture.js.map +1 -1
  27. package/dist/flow/SmartComplyFlow.d.ts +10 -0
  28. package/dist/flow/SmartComplyFlow.d.ts.map +1 -1
  29. package/dist/flow/SmartComplyFlow.js +53 -0
  30. package/dist/flow/SmartComplyFlow.js.map +1 -1
  31. package/dist/smartcomply.browser.js +256 -11
  32. package/dist/smartcomply.browser.js.map +3 -3
  33. package/dist/utils/ImageQuality.d.ts +38 -0
  34. package/dist/utils/ImageQuality.d.ts.map +1 -1
  35. package/dist/utils/ImageQuality.js +99 -0
  36. package/dist/utils/ImageQuality.js.map +1 -1
  37. 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;AAsBrD,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"}
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: at least one genuine blink OR a natural head-yaw excursion
15
- * detected at any point during the window — whichever happens first, since a
16
- * real person glancing around naturally will trigger one or both without
17
- * being told to.
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 || this.motionDetected,
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;;;;;;;;;;;;;;GAcG;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"}
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.80";
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.80";
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;AAsBrD,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"}
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: at least one genuine blink OR a natural head-yaw excursion
12
- * detected at any point during the window — whichever happens first, since a
13
- * real person glancing around naturally will trigger one or both without
14
- * being told to.
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 || this.motionDetected,
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;;;;;;;;;;;;;;GAcG;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"}
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.80";
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.80";
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 heuristic (blur/glare/contrast/
66
- * brightness — see utils/ImageQuality.ts) against the just-captured photo
67
- * and, if it scores poorly, shows a plain-language nudge on the preview
68
- * screen. Fails silently on decode error this is an advisory extra, not
69
- * a required step, so a browser that can't run it must not block capture.
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;IAEnE;;;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;IAyHxB;;;;;;OAMG;IACH,OAAO,CAAC,YAAY;IAQpB;;;;;OAKG;IACH,OAAO,CAAC,iBAAiB;IAOzB,OAAO,CAAC,mBAAmB;YAQb,UAAU;IAsExB,OAAO,CAAC,YAAY;IA4BpB,OAAO,CAAC,WAAW;IA4JnB;;;;;;OAMG;YACW,kBAAkB;IA4BhC,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;IAUlB,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"}
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
- this.openCamera();
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
- // Advisory-only quality nudge catches an obviously blurry/dark/glare-
599
- // heavy photo right here instead of only finding out several seconds
600
- // later. Never blocks "Use Photo": this is a heuristic, not a hard
601
- // rule, so a photo that's actually fine but scores oddly must still be
602
- // usable it just nudges the user toward retaking with a plain-
603
- // language reason instead of silently letting a bad photo through.
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 heuristic (blur/glare/contrast/
641
- * brightness — see utils/ImageQuality.ts) against the just-captured photo
642
- * and, if it scores poorly, shows a plain-language nudge on the preview
643
- * screen. Fails silently on decode error this is an advisory extra, not
644
- * a required step, so a browser that can't run it must not block capture.
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
- // Advisory only — no notice shown if scoring fails for any reason.
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) {