smartcomply-web-sdk 1.0.58 → 1.0.59

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 (49) hide show
  1. package/dist/camera/CameraManager.d.ts +16 -0
  2. package/dist/camera/CameraManager.d.ts.map +1 -1
  3. package/dist/camera/CameraManager.js +44 -1
  4. package/dist/camera/CameraManager.js.map +1 -1
  5. package/dist/camera/VideoRecorder.d.ts +3 -0
  6. package/dist/camera/VideoRecorder.d.ts.map +1 -1
  7. package/dist/camera/VideoRecorder.js +10 -1
  8. package/dist/camera/VideoRecorder.js.map +1 -1
  9. package/dist/esm/camera/CameraManager.d.ts +16 -0
  10. package/dist/esm/camera/CameraManager.d.ts.map +1 -1
  11. package/dist/esm/camera/CameraManager.js +44 -1
  12. package/dist/esm/camera/CameraManager.js.map +1 -1
  13. package/dist/esm/camera/VideoRecorder.d.ts +3 -0
  14. package/dist/esm/camera/VideoRecorder.d.ts.map +1 -1
  15. package/dist/esm/camera/VideoRecorder.js +10 -1
  16. package/dist/esm/camera/VideoRecorder.js.map +1 -1
  17. package/dist/esm/flow/SmartComplyFlow.d.ts +4 -1
  18. package/dist/esm/flow/SmartComplyFlow.d.ts.map +1 -1
  19. package/dist/esm/flow/SmartComplyFlow.js +78 -22
  20. package/dist/esm/flow/SmartComplyFlow.js.map +1 -1
  21. package/dist/esm/modules/liveness/LivenessUI.d.ts.map +1 -1
  22. package/dist/esm/modules/liveness/LivenessUI.js +10 -1
  23. package/dist/esm/modules/liveness/LivenessUI.js.map +1 -1
  24. package/dist/esm/modules/liveness/liveness.d.ts +3 -1
  25. package/dist/esm/modules/liveness/liveness.d.ts.map +1 -1
  26. package/dist/esm/modules/liveness/liveness.js +54 -25
  27. package/dist/esm/modules/liveness/liveness.js.map +1 -1
  28. package/dist/esm/utils/constants.d.ts +6 -2
  29. package/dist/esm/utils/constants.d.ts.map +1 -1
  30. package/dist/esm/utils/constants.js +6 -2
  31. package/dist/esm/utils/constants.js.map +1 -1
  32. package/dist/flow/SmartComplyFlow.d.ts +4 -1
  33. package/dist/flow/SmartComplyFlow.d.ts.map +1 -1
  34. package/dist/flow/SmartComplyFlow.js +78 -22
  35. package/dist/flow/SmartComplyFlow.js.map +1 -1
  36. package/dist/modules/liveness/LivenessUI.d.ts.map +1 -1
  37. package/dist/modules/liveness/LivenessUI.js +10 -1
  38. package/dist/modules/liveness/LivenessUI.js.map +1 -1
  39. package/dist/modules/liveness/liveness.d.ts +3 -1
  40. package/dist/modules/liveness/liveness.d.ts.map +1 -1
  41. package/dist/modules/liveness/liveness.js +54 -25
  42. package/dist/modules/liveness/liveness.js.map +1 -1
  43. package/dist/smartcomply.browser.js +391 -254
  44. package/dist/smartcomply.browser.js.map +4 -4
  45. package/dist/utils/constants.d.ts +6 -2
  46. package/dist/utils/constants.d.ts.map +1 -1
  47. package/dist/utils/constants.js +6 -2
  48. package/dist/utils/constants.js.map +1 -1
  49. package/package.json +1 -1
@@ -1,5 +1,11 @@
1
1
  export declare class CameraManager {
2
2
  private stream;
3
+ private onInterrupted;
4
+ private trackEndedHandlers;
5
+ /**
6
+ * Opens the camera via requestCameraAccess(), which maps browser errors
7
+ * (denied, no device, in-use-by-another-app) to a clean message thrown here.
8
+ */
3
9
  open(constraints?: MediaStreamConstraints): Promise<MediaStream>;
4
10
  getStream(): MediaStream | null;
5
11
  /**
@@ -9,6 +15,16 @@ export declare class CameraManager {
9
15
  * stop() becomes a no-op for it, leaking the camera.
10
16
  */
11
17
  setStream(stream: MediaStream): void;
18
+ /**
19
+ * Register a callback fired the moment any track in the current stream
20
+ * ends unexpectedly (OS revokes camera permission mid-session, another
21
+ * app steals the camera, device sleeps and the driver drops the feed).
22
+ * Without this, a dead stream is silently invisible — the video element
23
+ * just freezes on its last frame and face detection quietly fails
24
+ * forever with no indication anything went wrong.
25
+ */
26
+ onStreamInterrupted(callback: () => void): void;
27
+ private _watchTracks;
12
28
  stop(): void;
13
29
  }
14
30
  //# sourceMappingURL=CameraManager.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"CameraManager.d.ts","sourceRoot":"","sources":["../../src/camera/CameraManager.ts"],"names":[],"mappings":"AAAA,qBAAa,aAAa;IACxB,OAAO,CAAC,MAAM,CAA4B;IAEpC,IAAI,CAAC,WAAW,CAAC,EAAE,sBAAsB,GAAG,OAAO,CAAC,WAAW,CAAC;IAiBtE,SAAS,IAAI,WAAW,GAAG,IAAI;IAI/B;;;;;OAKG;IACH,SAAS,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI;IAIpC,IAAI,IAAI,IAAI;CAMb"}
1
+ {"version":3,"file":"CameraManager.d.ts","sourceRoot":"","sources":["../../src/camera/CameraManager.ts"],"names":[],"mappings":"AAEA,qBAAa,aAAa;IACxB,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,aAAa,CAA6B;IAClD,OAAO,CAAC,kBAAkB,CAAyB;IAEnD;;;OAGG;IACG,IAAI,CAAC,WAAW,CAAC,EAAE,sBAAsB,GAAG,OAAO,CAAC,WAAW,CAAC;IAqBtE,SAAS,IAAI,WAAW,GAAG,IAAI;IAI/B;;;;;OAKG;IACH,SAAS,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI;IAKpC;;;;;;;OAOG;IACH,mBAAmB,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,IAAI;IAI/C,OAAO,CAAC,YAAY;IAiBpB,IAAI,IAAI,IAAI;CAQb"}
@@ -1,10 +1,17 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.CameraManager = void 0;
4
+ const Permission_1 = require("./Permission");
4
5
  class CameraManager {
5
6
  constructor() {
6
7
  this.stream = null;
8
+ this.onInterrupted = null;
9
+ this.trackEndedHandlers = [];
7
10
  }
11
+ /**
12
+ * Opens the camera via requestCameraAccess(), which maps browser errors
13
+ * (denied, no device, in-use-by-another-app) to a clean message thrown here.
14
+ */
8
15
  async open(constraints) {
9
16
  const defaults = {
10
17
  video: {
@@ -14,7 +21,12 @@ class CameraManager {
14
21
  },
15
22
  audio: false,
16
23
  };
17
- this.stream = await navigator.mediaDevices.getUserMedia(constraints || defaults);
24
+ const { stream, permission } = await (0, Permission_1.requestCameraAccess)(constraints || defaults);
25
+ if (!stream) {
26
+ throw new Error(permission.reason || "Unable to access the camera.");
27
+ }
28
+ this.stream = stream;
29
+ this._watchTracks();
18
30
  return this.stream;
19
31
  }
20
32
  getStream() {
@@ -28,12 +40,43 @@ class CameraManager {
28
40
  */
29
41
  setStream(stream) {
30
42
  this.stream = stream;
43
+ this._watchTracks();
44
+ }
45
+ /**
46
+ * Register a callback fired the moment any track in the current stream
47
+ * ends unexpectedly (OS revokes camera permission mid-session, another
48
+ * app steals the camera, device sleeps and the driver drops the feed).
49
+ * Without this, a dead stream is silently invisible — the video element
50
+ * just freezes on its last frame and face detection quietly fails
51
+ * forever with no indication anything went wrong.
52
+ */
53
+ onStreamInterrupted(callback) {
54
+ this.onInterrupted = callback;
55
+ }
56
+ _watchTracks() {
57
+ this.trackEndedHandlers = [];
58
+ if (!this.stream)
59
+ return;
60
+ this.stream.getTracks().forEach((track) => {
61
+ const handler = () => {
62
+ // A track we intentionally stopped (via stop()) also fires "ended" —
63
+ // only treat this as an interruption if we still think we have a
64
+ // live stream (stop() nulls this.stream before that would happen).
65
+ if (this.stream) {
66
+ this.onInterrupted?.();
67
+ }
68
+ };
69
+ track.addEventListener("ended", handler);
70
+ this.trackEndedHandlers.push(() => track.removeEventListener("ended", handler));
71
+ });
31
72
  }
32
73
  stop() {
33
74
  if (this.stream) {
34
75
  this.stream.getTracks().forEach((track) => track.stop());
35
76
  this.stream = null;
36
77
  }
78
+ this.trackEndedHandlers.forEach((remove) => remove());
79
+ this.trackEndedHandlers = [];
37
80
  }
38
81
  }
39
82
  exports.CameraManager = CameraManager;
@@ -1 +1 @@
1
- {"version":3,"file":"CameraManager.js","sourceRoot":"","sources":["../../src/camera/CameraManager.ts"],"names":[],"mappings":";;;AAAA,MAAa,aAAa;IAA1B;QACU,WAAM,GAAuB,IAAI,CAAC;IAuC5C,CAAC;IArCC,KAAK,CAAC,IAAI,CAAC,WAAoC;QAC7C,MAAM,QAAQ,GAA2B;YACvC,KAAK,EAAE;gBACL,UAAU,EAAE,MAAM;gBAClB,KAAK,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE;gBACtB,MAAM,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE;aACvB;YACD,KAAK,EAAE,KAAK;SACb,CAAC;QAEF,IAAI,CAAC,MAAM,GAAG,MAAM,SAAS,CAAC,YAAY,CAAC,YAAY,CACrD,WAAW,IAAI,QAAQ,CACxB,CAAC;QAEF,OAAO,IAAI,CAAC,MAAM,CAAC;IACrB,CAAC;IAED,SAAS;QACP,OAAO,IAAI,CAAC,MAAM,CAAC;IACrB,CAAC;IAED;;;;;OAKG;IACH,SAAS,CAAC,MAAmB;QAC3B,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;IAED,IAAI;QACF,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAChB,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;YACzD,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACrB,CAAC;IACH,CAAC;CACF;AAxCD,sCAwCC"}
1
+ {"version":3,"file":"CameraManager.js","sourceRoot":"","sources":["../../src/camera/CameraManager.ts"],"names":[],"mappings":";;;AAAA,6CAAmD;AAEnD,MAAa,aAAa;IAA1B;QACU,WAAM,GAAuB,IAAI,CAAC;QAClC,kBAAa,GAAwB,IAAI,CAAC;QAC1C,uBAAkB,GAAsB,EAAE,CAAC;IA+ErD,CAAC;IA7EC;;;OAGG;IACH,KAAK,CAAC,IAAI,CAAC,WAAoC;QAC7C,MAAM,QAAQ,GAA2B;YACvC,KAAK,EAAE;gBACL,UAAU,EAAE,MAAM;gBAClB,KAAK,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE;gBACtB,MAAM,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE;aACvB;YACD,KAAK,EAAE,KAAK;SACb,CAAC;QAEF,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG,MAAM,IAAA,gCAAmB,EAAC,WAAW,IAAI,QAAQ,CAAC,CAAC;QAClF,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,MAAM,IAAI,KAAK,CAAC,UAAU,CAAC,MAAM,IAAI,8BAA8B,CAAC,CAAC;QACvE,CAAC;QAED,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,YAAY,EAAE,CAAC;QAEpB,OAAO,IAAI,CAAC,MAAM,CAAC;IACrB,CAAC;IAED,SAAS;QACP,OAAO,IAAI,CAAC,MAAM,CAAC;IACrB,CAAC;IAED;;;;;OAKG;IACH,SAAS,CAAC,MAAmB;QAC3B,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,YAAY,EAAE,CAAC;IACtB,CAAC;IAED;;;;;;;OAOG;IACH,mBAAmB,CAAC,QAAoB;QACtC,IAAI,CAAC,aAAa,GAAG,QAAQ,CAAC;IAChC,CAAC;IAEO,YAAY;QAClB,IAAI,CAAC,kBAAkB,GAAG,EAAE,CAAC;QAC7B,IAAI,CAAC,IAAI,CAAC,MAAM;YAAE,OAAO;QACzB,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,EAAE;YACxC,MAAM,OAAO,GAAG,GAAG,EAAE;gBACnB,qEAAqE;gBACrE,iEAAiE;gBACjE,mEAAmE;gBACnE,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;oBAChB,IAAI,CAAC,aAAa,EAAE,EAAE,CAAC;gBACzB,CAAC;YACH,CAAC,CAAC;YACF,KAAK,CAAC,gBAAgB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;YACzC,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;QAClF,CAAC,CAAC,CAAC;IACL,CAAC;IAED,IAAI;QACF,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAChB,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;YACzD,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACrB,CAAC;QACD,IAAI,CAAC,kBAAkB,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC,CAAC;QACtD,IAAI,CAAC,kBAAkB,GAAG,EAAE,CAAC;IAC/B,CAAC;CACF;AAlFD,sCAkFC"}
@@ -2,6 +2,9 @@ export declare class VideoRecorder {
2
2
  private recorder;
3
3
  private chunks;
4
4
  private startedAt;
5
+ private onRecordingError;
6
+ /** Fires immediately on a mid-recording error, unlike stop()'s onerror which only fires when stop() is later called. */
7
+ onError(callback: () => void): void;
5
8
  start(stream: MediaStream): void;
6
9
  stop(): Promise<{
7
10
  blob: Blob;
@@ -1 +1 @@
1
- {"version":3,"file":"VideoRecorder.d.ts","sourceRoot":"","sources":["../../src/camera/VideoRecorder.ts"],"names":[],"mappings":"AAAA,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAA8B;IAC9C,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,SAAS,CAAa;IAE9B,KAAK,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI;IAsBhC,IAAI,IAAI,OAAO,CAAC;QAAE,IAAI,EAAE,IAAI,CAAC;QAAC,eAAe,EAAE,MAAM,CAAA;KAAE,CAAC;IA0BxD,WAAW,IAAI,OAAO;CAGvB"}
1
+ {"version":3,"file":"VideoRecorder.d.ts","sourceRoot":"","sources":["../../src/camera/VideoRecorder.ts"],"names":[],"mappings":"AAEA,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAA8B;IAC9C,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,SAAS,CAAa;IAC9B,OAAO,CAAC,gBAAgB,CAA6B;IAErD,wHAAwH;IACxH,OAAO,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,IAAI;IAInC,KAAK,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI;IA0BhC,IAAI,IAAI,OAAO,CAAC;QAAE,IAAI,EAAE,IAAI,CAAC;QAAC,eAAe,EAAE,MAAM,CAAA;KAAE,CAAC;IA0BxD,WAAW,IAAI,OAAO;CAGvB"}
@@ -1,11 +1,17 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.VideoRecorder = void 0;
4
+ const constants_1 = require("../utils/constants");
4
5
  class VideoRecorder {
5
6
  constructor() {
6
7
  this.recorder = null;
7
8
  this.chunks = [];
8
9
  this.startedAt = 0;
10
+ this.onRecordingError = null;
11
+ }
12
+ /** Fires immediately on a mid-recording error, unlike stop()'s onerror which only fires when stop() is later called. */
13
+ onError(callback) {
14
+ this.onRecordingError = callback;
9
15
  }
10
16
  start(stream) {
11
17
  this.chunks = [];
@@ -14,13 +20,16 @@ class VideoRecorder {
14
20
  : "video/webm";
15
21
  this.recorder = new MediaRecorder(stream, {
16
22
  mimeType,
17
- videoBitsPerSecond: 800000, // raised from 250k — higher bitrate = sharper frames for SmartComply face extractor
23
+ videoBitsPerSecond: constants_1.RECORDING_BITS_PER_SECOND,
18
24
  });
19
25
  this.recorder.ondataavailable = (e) => {
20
26
  if (e.data.size > 0) {
21
27
  this.chunks.push(e.data);
22
28
  }
23
29
  };
30
+ this.recorder.addEventListener("error", () => {
31
+ this.onRecordingError?.();
32
+ });
24
33
  this.startedAt = Date.now();
25
34
  this.recorder.start();
26
35
  }
@@ -1 +1 @@
1
- {"version":3,"file":"VideoRecorder.js","sourceRoot":"","sources":["../../src/camera/VideoRecorder.ts"],"names":[],"mappings":";;;AAAA,MAAa,aAAa;IAA1B;QACU,aAAQ,GAAyB,IAAI,CAAC;QACtC,WAAM,GAAW,EAAE,CAAC;QACpB,cAAS,GAAW,CAAC,CAAC;IAqDhC,CAAC;IAnDC,KAAK,CAAC,MAAmB;QACvB,IAAI,CAAC,MAAM,GAAG,EAAE,CAAC;QAEjB,MAAM,QAAQ,GAAG,aAAa,CAAC,eAAe,CAAC,uBAAuB,CAAC;YACrE,CAAC,CAAC,uBAAuB;YACzB,CAAC,CAAC,YAAY,CAAC;QAEjB,IAAI,CAAC,QAAQ,GAAG,IAAI,aAAa,CAAC,MAAM,EAAE;YACxC,QAAQ;YACR,kBAAkB,EAAE,MAAO,EAAE,oFAAoF;SAClH,CAAC,CAAC;QAEH,IAAI,CAAC,QAAQ,CAAC,eAAe,GAAG,CAAC,CAAC,EAAE,EAAE;YACpC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC;gBACpB,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;YAC3B,CAAC;QACH,CAAC,CAAC;QAEF,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAC5B,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;IACxB,CAAC;IAED,IAAI;QACF,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACrC,IAAI,CAAC,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,QAAQ,CAAC,KAAK,KAAK,UAAU,EAAE,CAAC;gBACzD,MAAM,CAAC,IAAI,KAAK,CAAC,wBAAwB,CAAC,CAAC,CAAC;gBAC5C,OAAO;YACT,CAAC;YAED,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;YAE7B,IAAI,CAAC,QAAQ,CAAC,MAAM,GAAG,GAAG,EAAE;gBAC1B,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC,CAAC;gBAC3D,MAAM,eAAe,GAAG,UAAU,CAChC,CAAC,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CACjD,CAAC;gBACF,IAAI,CAAC,MAAM,GAAG,EAAE,CAAC;gBACjB,OAAO,CAAC,EAAE,IAAI,EAAE,eAAe,EAAE,CAAC,CAAC;YACrC,CAAC,CAAC;YAEF,IAAI,CAAC,QAAQ,CAAC,OAAO,GAAG,GAAG,EAAE;gBAC3B,MAAM,CAAC,IAAI,KAAK,CAAC,kBAAkB,CAAC,CAAC,CAAC;YACxC,CAAC,CAAC;YAEF,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;QACvB,CAAC,CAAC,CAAC;IACL,CAAC;IAED,WAAW;QACT,OAAO,IAAI,CAAC,QAAQ,EAAE,KAAK,KAAK,WAAW,CAAC;IAC9C,CAAC;CACF;AAxDD,sCAwDC"}
1
+ {"version":3,"file":"VideoRecorder.js","sourceRoot":"","sources":["../../src/camera/VideoRecorder.ts"],"names":[],"mappings":";;;AAAA,kDAA+D;AAE/D,MAAa,aAAa;IAA1B;QACU,aAAQ,GAAyB,IAAI,CAAC;QACtC,WAAM,GAAW,EAAE,CAAC;QACpB,cAAS,GAAW,CAAC,CAAC;QACtB,qBAAgB,GAAwB,IAAI,CAAC;IA8DvD,CAAC;IA5DC,wHAAwH;IACxH,OAAO,CAAC,QAAoB;QAC1B,IAAI,CAAC,gBAAgB,GAAG,QAAQ,CAAC;IACnC,CAAC;IAED,KAAK,CAAC,MAAmB;QACvB,IAAI,CAAC,MAAM,GAAG,EAAE,CAAC;QAEjB,MAAM,QAAQ,GAAG,aAAa,CAAC,eAAe,CAAC,uBAAuB,CAAC;YACrE,CAAC,CAAC,uBAAuB;YACzB,CAAC,CAAC,YAAY,CAAC;QAEjB,IAAI,CAAC,QAAQ,GAAG,IAAI,aAAa,CAAC,MAAM,EAAE;YACxC,QAAQ;YACR,kBAAkB,EAAE,qCAAyB;SAC9C,CAAC,CAAC;QAEH,IAAI,CAAC,QAAQ,CAAC,eAAe,GAAG,CAAC,CAAC,EAAE,EAAE;YACpC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC;gBACpB,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;YAC3B,CAAC;QACH,CAAC,CAAC;QAEF,IAAI,CAAC,QAAQ,CAAC,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE;YAC3C,IAAI,CAAC,gBAAgB,EAAE,EAAE,CAAC;QAC5B,CAAC,CAAC,CAAC;QAEH,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAC5B,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;IACxB,CAAC;IAED,IAAI;QACF,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACrC,IAAI,CAAC,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,QAAQ,CAAC,KAAK,KAAK,UAAU,EAAE,CAAC;gBACzD,MAAM,CAAC,IAAI,KAAK,CAAC,wBAAwB,CAAC,CAAC,CAAC;gBAC5C,OAAO;YACT,CAAC;YAED,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;YAE7B,IAAI,CAAC,QAAQ,CAAC,MAAM,GAAG,GAAG,EAAE;gBAC1B,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC,CAAC;gBAC3D,MAAM,eAAe,GAAG,UAAU,CAChC,CAAC,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CACjD,CAAC;gBACF,IAAI,CAAC,MAAM,GAAG,EAAE,CAAC;gBACjB,OAAO,CAAC,EAAE,IAAI,EAAE,eAAe,EAAE,CAAC,CAAC;YACrC,CAAC,CAAC;YAEF,IAAI,CAAC,QAAQ,CAAC,OAAO,GAAG,GAAG,EAAE;gBAC3B,MAAM,CAAC,IAAI,KAAK,CAAC,kBAAkB,CAAC,CAAC,CAAC;YACxC,CAAC,CAAC;YAEF,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;QACvB,CAAC,CAAC,CAAC;IACL,CAAC;IAED,WAAW;QACT,OAAO,IAAI,CAAC,QAAQ,EAAE,KAAK,KAAK,WAAW,CAAC;IAC9C,CAAC;CACF;AAlED,sCAkEC"}
@@ -1,5 +1,11 @@
1
1
  export declare class CameraManager {
2
2
  private stream;
3
+ private onInterrupted;
4
+ private trackEndedHandlers;
5
+ /**
6
+ * Opens the camera via requestCameraAccess(), which maps browser errors
7
+ * (denied, no device, in-use-by-another-app) to a clean message thrown here.
8
+ */
3
9
  open(constraints?: MediaStreamConstraints): Promise<MediaStream>;
4
10
  getStream(): MediaStream | null;
5
11
  /**
@@ -9,6 +15,16 @@ export declare class CameraManager {
9
15
  * stop() becomes a no-op for it, leaking the camera.
10
16
  */
11
17
  setStream(stream: MediaStream): void;
18
+ /**
19
+ * Register a callback fired the moment any track in the current stream
20
+ * ends unexpectedly (OS revokes camera permission mid-session, another
21
+ * app steals the camera, device sleeps and the driver drops the feed).
22
+ * Without this, a dead stream is silently invisible — the video element
23
+ * just freezes on its last frame and face detection quietly fails
24
+ * forever with no indication anything went wrong.
25
+ */
26
+ onStreamInterrupted(callback: () => void): void;
27
+ private _watchTracks;
12
28
  stop(): void;
13
29
  }
14
30
  //# sourceMappingURL=CameraManager.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"CameraManager.d.ts","sourceRoot":"","sources":["../../../src/camera/CameraManager.ts"],"names":[],"mappings":"AAAA,qBAAa,aAAa;IACxB,OAAO,CAAC,MAAM,CAA4B;IAEpC,IAAI,CAAC,WAAW,CAAC,EAAE,sBAAsB,GAAG,OAAO,CAAC,WAAW,CAAC;IAiBtE,SAAS,IAAI,WAAW,GAAG,IAAI;IAI/B;;;;;OAKG;IACH,SAAS,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI;IAIpC,IAAI,IAAI,IAAI;CAMb"}
1
+ {"version":3,"file":"CameraManager.d.ts","sourceRoot":"","sources":["../../../src/camera/CameraManager.ts"],"names":[],"mappings":"AAEA,qBAAa,aAAa;IACxB,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,aAAa,CAA6B;IAClD,OAAO,CAAC,kBAAkB,CAAyB;IAEnD;;;OAGG;IACG,IAAI,CAAC,WAAW,CAAC,EAAE,sBAAsB,GAAG,OAAO,CAAC,WAAW,CAAC;IAqBtE,SAAS,IAAI,WAAW,GAAG,IAAI;IAI/B;;;;;OAKG;IACH,SAAS,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI;IAKpC;;;;;;;OAOG;IACH,mBAAmB,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,IAAI;IAI/C,OAAO,CAAC,YAAY;IAiBpB,IAAI,IAAI,IAAI;CAQb"}
@@ -1,7 +1,14 @@
1
+ import { requestCameraAccess } from "./Permission";
1
2
  export class CameraManager {
2
3
  constructor() {
3
4
  this.stream = null;
5
+ this.onInterrupted = null;
6
+ this.trackEndedHandlers = [];
4
7
  }
8
+ /**
9
+ * Opens the camera via requestCameraAccess(), which maps browser errors
10
+ * (denied, no device, in-use-by-another-app) to a clean message thrown here.
11
+ */
5
12
  async open(constraints) {
6
13
  const defaults = {
7
14
  video: {
@@ -11,7 +18,12 @@ export class CameraManager {
11
18
  },
12
19
  audio: false,
13
20
  };
14
- this.stream = await navigator.mediaDevices.getUserMedia(constraints || defaults);
21
+ const { stream, permission } = await requestCameraAccess(constraints || defaults);
22
+ if (!stream) {
23
+ throw new Error(permission.reason || "Unable to access the camera.");
24
+ }
25
+ this.stream = stream;
26
+ this._watchTracks();
15
27
  return this.stream;
16
28
  }
17
29
  getStream() {
@@ -25,12 +37,43 @@ export class CameraManager {
25
37
  */
26
38
  setStream(stream) {
27
39
  this.stream = stream;
40
+ this._watchTracks();
41
+ }
42
+ /**
43
+ * Register a callback fired the moment any track in the current stream
44
+ * ends unexpectedly (OS revokes camera permission mid-session, another
45
+ * app steals the camera, device sleeps and the driver drops the feed).
46
+ * Without this, a dead stream is silently invisible — the video element
47
+ * just freezes on its last frame and face detection quietly fails
48
+ * forever with no indication anything went wrong.
49
+ */
50
+ onStreamInterrupted(callback) {
51
+ this.onInterrupted = callback;
52
+ }
53
+ _watchTracks() {
54
+ this.trackEndedHandlers = [];
55
+ if (!this.stream)
56
+ return;
57
+ this.stream.getTracks().forEach((track) => {
58
+ const handler = () => {
59
+ // A track we intentionally stopped (via stop()) also fires "ended" —
60
+ // only treat this as an interruption if we still think we have a
61
+ // live stream (stop() nulls this.stream before that would happen).
62
+ if (this.stream) {
63
+ this.onInterrupted?.();
64
+ }
65
+ };
66
+ track.addEventListener("ended", handler);
67
+ this.trackEndedHandlers.push(() => track.removeEventListener("ended", handler));
68
+ });
28
69
  }
29
70
  stop() {
30
71
  if (this.stream) {
31
72
  this.stream.getTracks().forEach((track) => track.stop());
32
73
  this.stream = null;
33
74
  }
75
+ this.trackEndedHandlers.forEach((remove) => remove());
76
+ this.trackEndedHandlers = [];
34
77
  }
35
78
  }
36
79
  //# sourceMappingURL=CameraManager.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"CameraManager.js","sourceRoot":"","sources":["../../../src/camera/CameraManager.ts"],"names":[],"mappings":"AAAA,MAAM,OAAO,aAAa;IAA1B;QACU,WAAM,GAAuB,IAAI,CAAC;IAuC5C,CAAC;IArCC,KAAK,CAAC,IAAI,CAAC,WAAoC;QAC7C,MAAM,QAAQ,GAA2B;YACvC,KAAK,EAAE;gBACL,UAAU,EAAE,MAAM;gBAClB,KAAK,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE;gBACtB,MAAM,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE;aACvB;YACD,KAAK,EAAE,KAAK;SACb,CAAC;QAEF,IAAI,CAAC,MAAM,GAAG,MAAM,SAAS,CAAC,YAAY,CAAC,YAAY,CACrD,WAAW,IAAI,QAAQ,CACxB,CAAC;QAEF,OAAO,IAAI,CAAC,MAAM,CAAC;IACrB,CAAC;IAED,SAAS;QACP,OAAO,IAAI,CAAC,MAAM,CAAC;IACrB,CAAC;IAED;;;;;OAKG;IACH,SAAS,CAAC,MAAmB;QAC3B,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;IAED,IAAI;QACF,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAChB,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;YACzD,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACrB,CAAC;IACH,CAAC;CACF"}
1
+ {"version":3,"file":"CameraManager.js","sourceRoot":"","sources":["../../../src/camera/CameraManager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAEnD,MAAM,OAAO,aAAa;IAA1B;QACU,WAAM,GAAuB,IAAI,CAAC;QAClC,kBAAa,GAAwB,IAAI,CAAC;QAC1C,uBAAkB,GAAsB,EAAE,CAAC;IA+ErD,CAAC;IA7EC;;;OAGG;IACH,KAAK,CAAC,IAAI,CAAC,WAAoC;QAC7C,MAAM,QAAQ,GAA2B;YACvC,KAAK,EAAE;gBACL,UAAU,EAAE,MAAM;gBAClB,KAAK,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE;gBACtB,MAAM,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE;aACvB;YACD,KAAK,EAAE,KAAK;SACb,CAAC;QAEF,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG,MAAM,mBAAmB,CAAC,WAAW,IAAI,QAAQ,CAAC,CAAC;QAClF,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,MAAM,IAAI,KAAK,CAAC,UAAU,CAAC,MAAM,IAAI,8BAA8B,CAAC,CAAC;QACvE,CAAC;QAED,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,YAAY,EAAE,CAAC;QAEpB,OAAO,IAAI,CAAC,MAAM,CAAC;IACrB,CAAC;IAED,SAAS;QACP,OAAO,IAAI,CAAC,MAAM,CAAC;IACrB,CAAC;IAED;;;;;OAKG;IACH,SAAS,CAAC,MAAmB;QAC3B,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,YAAY,EAAE,CAAC;IACtB,CAAC;IAED;;;;;;;OAOG;IACH,mBAAmB,CAAC,QAAoB;QACtC,IAAI,CAAC,aAAa,GAAG,QAAQ,CAAC;IAChC,CAAC;IAEO,YAAY;QAClB,IAAI,CAAC,kBAAkB,GAAG,EAAE,CAAC;QAC7B,IAAI,CAAC,IAAI,CAAC,MAAM;YAAE,OAAO;QACzB,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,EAAE;YACxC,MAAM,OAAO,GAAG,GAAG,EAAE;gBACnB,qEAAqE;gBACrE,iEAAiE;gBACjE,mEAAmE;gBACnE,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;oBAChB,IAAI,CAAC,aAAa,EAAE,EAAE,CAAC;gBACzB,CAAC;YACH,CAAC,CAAC;YACF,KAAK,CAAC,gBAAgB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;YACzC,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;QAClF,CAAC,CAAC,CAAC;IACL,CAAC;IAED,IAAI;QACF,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAChB,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;YACzD,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACrB,CAAC;QACD,IAAI,CAAC,kBAAkB,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC,CAAC;QACtD,IAAI,CAAC,kBAAkB,GAAG,EAAE,CAAC;IAC/B,CAAC;CACF"}
@@ -2,6 +2,9 @@ export declare class VideoRecorder {
2
2
  private recorder;
3
3
  private chunks;
4
4
  private startedAt;
5
+ private onRecordingError;
6
+ /** Fires immediately on a mid-recording error, unlike stop()'s onerror which only fires when stop() is later called. */
7
+ onError(callback: () => void): void;
5
8
  start(stream: MediaStream): void;
6
9
  stop(): Promise<{
7
10
  blob: Blob;
@@ -1 +1 @@
1
- {"version":3,"file":"VideoRecorder.d.ts","sourceRoot":"","sources":["../../../src/camera/VideoRecorder.ts"],"names":[],"mappings":"AAAA,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAA8B;IAC9C,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,SAAS,CAAa;IAE9B,KAAK,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI;IAsBhC,IAAI,IAAI,OAAO,CAAC;QAAE,IAAI,EAAE,IAAI,CAAC;QAAC,eAAe,EAAE,MAAM,CAAA;KAAE,CAAC;IA0BxD,WAAW,IAAI,OAAO;CAGvB"}
1
+ {"version":3,"file":"VideoRecorder.d.ts","sourceRoot":"","sources":["../../../src/camera/VideoRecorder.ts"],"names":[],"mappings":"AAEA,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAA8B;IAC9C,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,SAAS,CAAa;IAC9B,OAAO,CAAC,gBAAgB,CAA6B;IAErD,wHAAwH;IACxH,OAAO,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,IAAI;IAInC,KAAK,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI;IA0BhC,IAAI,IAAI,OAAO,CAAC;QAAE,IAAI,EAAE,IAAI,CAAC;QAAC,eAAe,EAAE,MAAM,CAAA;KAAE,CAAC;IA0BxD,WAAW,IAAI,OAAO;CAGvB"}
@@ -1,8 +1,14 @@
1
+ import { RECORDING_BITS_PER_SECOND } from "../utils/constants";
1
2
  export class VideoRecorder {
2
3
  constructor() {
3
4
  this.recorder = null;
4
5
  this.chunks = [];
5
6
  this.startedAt = 0;
7
+ this.onRecordingError = null;
8
+ }
9
+ /** Fires immediately on a mid-recording error, unlike stop()'s onerror which only fires when stop() is later called. */
10
+ onError(callback) {
11
+ this.onRecordingError = callback;
6
12
  }
7
13
  start(stream) {
8
14
  this.chunks = [];
@@ -11,13 +17,16 @@ export class VideoRecorder {
11
17
  : "video/webm";
12
18
  this.recorder = new MediaRecorder(stream, {
13
19
  mimeType,
14
- videoBitsPerSecond: 800000, // raised from 250k — higher bitrate = sharper frames for SmartComply face extractor
20
+ videoBitsPerSecond: RECORDING_BITS_PER_SECOND,
15
21
  });
16
22
  this.recorder.ondataavailable = (e) => {
17
23
  if (e.data.size > 0) {
18
24
  this.chunks.push(e.data);
19
25
  }
20
26
  };
27
+ this.recorder.addEventListener("error", () => {
28
+ this.onRecordingError?.();
29
+ });
21
30
  this.startedAt = Date.now();
22
31
  this.recorder.start();
23
32
  }
@@ -1 +1 @@
1
- {"version":3,"file":"VideoRecorder.js","sourceRoot":"","sources":["../../../src/camera/VideoRecorder.ts"],"names":[],"mappings":"AAAA,MAAM,OAAO,aAAa;IAA1B;QACU,aAAQ,GAAyB,IAAI,CAAC;QACtC,WAAM,GAAW,EAAE,CAAC;QACpB,cAAS,GAAW,CAAC,CAAC;IAqDhC,CAAC;IAnDC,KAAK,CAAC,MAAmB;QACvB,IAAI,CAAC,MAAM,GAAG,EAAE,CAAC;QAEjB,MAAM,QAAQ,GAAG,aAAa,CAAC,eAAe,CAAC,uBAAuB,CAAC;YACrE,CAAC,CAAC,uBAAuB;YACzB,CAAC,CAAC,YAAY,CAAC;QAEjB,IAAI,CAAC,QAAQ,GAAG,IAAI,aAAa,CAAC,MAAM,EAAE;YACxC,QAAQ;YACR,kBAAkB,EAAE,MAAO,EAAE,oFAAoF;SAClH,CAAC,CAAC;QAEH,IAAI,CAAC,QAAQ,CAAC,eAAe,GAAG,CAAC,CAAC,EAAE,EAAE;YACpC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC;gBACpB,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;YAC3B,CAAC;QACH,CAAC,CAAC;QAEF,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAC5B,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;IACxB,CAAC;IAED,IAAI;QACF,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACrC,IAAI,CAAC,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,QAAQ,CAAC,KAAK,KAAK,UAAU,EAAE,CAAC;gBACzD,MAAM,CAAC,IAAI,KAAK,CAAC,wBAAwB,CAAC,CAAC,CAAC;gBAC5C,OAAO;YACT,CAAC;YAED,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;YAE7B,IAAI,CAAC,QAAQ,CAAC,MAAM,GAAG,GAAG,EAAE;gBAC1B,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC,CAAC;gBAC3D,MAAM,eAAe,GAAG,UAAU,CAChC,CAAC,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CACjD,CAAC;gBACF,IAAI,CAAC,MAAM,GAAG,EAAE,CAAC;gBACjB,OAAO,CAAC,EAAE,IAAI,EAAE,eAAe,EAAE,CAAC,CAAC;YACrC,CAAC,CAAC;YAEF,IAAI,CAAC,QAAQ,CAAC,OAAO,GAAG,GAAG,EAAE;gBAC3B,MAAM,CAAC,IAAI,KAAK,CAAC,kBAAkB,CAAC,CAAC,CAAC;YACxC,CAAC,CAAC;YAEF,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;QACvB,CAAC,CAAC,CAAC;IACL,CAAC;IAED,WAAW;QACT,OAAO,IAAI,CAAC,QAAQ,EAAE,KAAK,KAAK,WAAW,CAAC;IAC9C,CAAC;CACF"}
1
+ {"version":3,"file":"VideoRecorder.js","sourceRoot":"","sources":["../../../src/camera/VideoRecorder.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,yBAAyB,EAAE,MAAM,oBAAoB,CAAC;AAE/D,MAAM,OAAO,aAAa;IAA1B;QACU,aAAQ,GAAyB,IAAI,CAAC;QACtC,WAAM,GAAW,EAAE,CAAC;QACpB,cAAS,GAAW,CAAC,CAAC;QACtB,qBAAgB,GAAwB,IAAI,CAAC;IA8DvD,CAAC;IA5DC,wHAAwH;IACxH,OAAO,CAAC,QAAoB;QAC1B,IAAI,CAAC,gBAAgB,GAAG,QAAQ,CAAC;IACnC,CAAC;IAED,KAAK,CAAC,MAAmB;QACvB,IAAI,CAAC,MAAM,GAAG,EAAE,CAAC;QAEjB,MAAM,QAAQ,GAAG,aAAa,CAAC,eAAe,CAAC,uBAAuB,CAAC;YACrE,CAAC,CAAC,uBAAuB;YACzB,CAAC,CAAC,YAAY,CAAC;QAEjB,IAAI,CAAC,QAAQ,GAAG,IAAI,aAAa,CAAC,MAAM,EAAE;YACxC,QAAQ;YACR,kBAAkB,EAAE,yBAAyB;SAC9C,CAAC,CAAC;QAEH,IAAI,CAAC,QAAQ,CAAC,eAAe,GAAG,CAAC,CAAC,EAAE,EAAE;YACpC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC;gBACpB,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;YAC3B,CAAC;QACH,CAAC,CAAC;QAEF,IAAI,CAAC,QAAQ,CAAC,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE;YAC3C,IAAI,CAAC,gBAAgB,EAAE,EAAE,CAAC;QAC5B,CAAC,CAAC,CAAC;QAEH,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAC5B,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;IACxB,CAAC;IAED,IAAI;QACF,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACrC,IAAI,CAAC,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,QAAQ,CAAC,KAAK,KAAK,UAAU,EAAE,CAAC;gBACzD,MAAM,CAAC,IAAI,KAAK,CAAC,wBAAwB,CAAC,CAAC,CAAC;gBAC5C,OAAO;YACT,CAAC;YAED,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;YAE7B,IAAI,CAAC,QAAQ,CAAC,MAAM,GAAG,GAAG,EAAE;gBAC1B,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC,CAAC;gBAC3D,MAAM,eAAe,GAAG,UAAU,CAChC,CAAC,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CACjD,CAAC;gBACF,IAAI,CAAC,MAAM,GAAG,EAAE,CAAC;gBACjB,OAAO,CAAC,EAAE,IAAI,EAAE,eAAe,EAAE,CAAC,CAAC;YACrC,CAAC,CAAC;YAEF,IAAI,CAAC,QAAQ,CAAC,OAAO,GAAG,GAAG,EAAE;gBAC3B,MAAM,CAAC,IAAI,KAAK,CAAC,kBAAkB,CAAC,CAAC,CAAC;YACxC,CAAC,CAAC;YAEF,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;QACvB,CAAC,CAAC,CAAC;IACL,CAAC;IAED,WAAW;QACT,OAAO,IAAI,CAAC,QAAQ,EAAE,KAAK,KAAK,WAAW,CAAC;IAC9C,CAAC;CACF"}
@@ -44,6 +44,7 @@ export declare class SmartComplyFlow {
44
44
  private livenessEntryId;
45
45
  private isDestroyed;
46
46
  private headerBackBtn;
47
+ private _keydownHandler;
47
48
  private _confirmRetries;
48
49
  private _livenessFailures;
49
50
  private static readonly MAX_RETRIES;
@@ -82,6 +83,7 @@ export declare class SmartComplyFlow {
82
83
  close(): void;
83
84
  private mount;
84
85
  private initialize;
86
+ private _showExpiryWarningWhenSafe;
85
87
  private showStep;
86
88
  private renderStep;
87
89
  private renderLoading;
@@ -108,7 +110,6 @@ export declare class SmartComplyFlow {
108
110
  private processDocumentBlob;
109
111
  private handleDocumentProcessingError;
110
112
  private renderLiveness;
111
- private renderResult;
112
113
  private showErrorStep;
113
114
  private createHeader;
114
115
  private createFooter;
@@ -116,6 +117,8 @@ export declare class SmartComplyFlow {
116
117
  private createOptionCard;
117
118
  private createPrimaryButton;
118
119
  private createSecondaryButton;
120
+ /** Click handler that fires once and disables the button, preventing a double-tap double-dispatch. */
121
+ private _onceClick;
119
122
  private isDocumentFlow;
120
123
  private updateProgress;
121
124
  private applyTheme;
@@ -1 +1 @@
1
- {"version":3,"file":"SmartComplyFlow.d.ts","sourceRoot":"","sources":["../../../src/flow/SmartComplyFlow.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"SmartComplyFlow.d.ts","sourceRoot":"","sources":["../../../src/flow/SmartComplyFlow.ts"],"names":[],"mappings":"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"}
@@ -14,7 +14,6 @@ const STEP_ORDER = [
14
14
  "document_confirm",
15
15
  "liveness",
16
16
  "done",
17
- "result",
18
17
  ];
19
18
  const COUNTRY_LABELS = {
20
19
  nigeria: "🇳🇬 Nigeria",
@@ -279,6 +278,7 @@ export class SmartComplyFlow {
279
278
  this.livenessEntryId = null;
280
279
  this.isDestroyed = false;
281
280
  this.headerBackBtn = null;
281
+ this._keydownHandler = null;
282
282
  // Retry safety — max 3 confirmation rejections or liveness failures per session
283
283
  this._confirmRetries = 0;
284
284
  this._livenessFailures = 0;
@@ -336,6 +336,10 @@ export class SmartComplyFlow {
336
336
  */
337
337
  close() {
338
338
  this.isDestroyed = true;
339
+ if (this._keydownHandler) {
340
+ document.removeEventListener("keydown", this._keydownHandler);
341
+ this._keydownHandler = null;
342
+ }
339
343
  this.docCapture?.destroy();
340
344
  // Release any pre-warmed camera stream
341
345
  if (this._prewarmedStream) {
@@ -442,6 +446,30 @@ export class SmartComplyFlow {
442
446
  if (this.modal)
443
447
  this.modal.style.transform = "translateY(0) scale(1)";
444
448
  });
449
+ // Escape closes the dialog; Tab is trapped inside it (standard WCAG dialog pattern).
450
+ this._keydownHandler = (e) => {
451
+ if (e.key === "Escape") {
452
+ e.preventDefault();
453
+ this.close();
454
+ return;
455
+ }
456
+ if (e.key === "Tab" && this.modal) {
457
+ const focusable = this.modal.querySelectorAll('button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])');
458
+ if (focusable.length === 0)
459
+ return;
460
+ const first = focusable[0];
461
+ const last = focusable[focusable.length - 1];
462
+ if (e.shiftKey && document.activeElement === first) {
463
+ e.preventDefault();
464
+ last.focus();
465
+ }
466
+ else if (!e.shiftKey && document.activeElement === last) {
467
+ e.preventDefault();
468
+ first.focus();
469
+ }
470
+ }
471
+ };
472
+ document.addEventListener("keydown", this._keydownHandler);
445
473
  }
446
474
  async initialize() {
447
475
  this.showStep("loading");
@@ -453,11 +481,7 @@ export class SmartComplyFlow {
453
481
  if (expiresAt) {
454
482
  const expiresMs = new Date(expiresAt).getTime() - Date.now();
455
483
  const warnMs = Math.max(expiresMs - 120000, 0); // warn 2 min before
456
- this.sessionExpiryTimer = setTimeout(() => {
457
- if (!this.isDestroyed && this.currentStep !== "result") {
458
- this.showErrorStep("Your session is about to expire. Please complete verification quickly or restart.");
459
- }
460
- }, warnMs);
484
+ this.sessionExpiryTimer = setTimeout(() => this._showExpiryWarningWhenSafe(), warnMs);
461
485
  }
462
486
  // Load config
463
487
  this.sdkConfig = await this.sdk.initializeConfig();
@@ -478,6 +502,19 @@ export class SmartComplyFlow {
478
502
  this.showErrorStep(err.message || "Failed to initialize. Please try again.");
479
503
  }
480
504
  }
505
+ // Defers the expiry warning while the user is on a camera-active step
506
+ // (document_capture/liveness) — showing it there would blow away a live
507
+ // video element and in-progress MediaRecorder mid-scan.
508
+ _showExpiryWarningWhenSafe() {
509
+ if (this.isDestroyed)
510
+ return;
511
+ const cameraActiveSteps = ["document_capture", "liveness"];
512
+ if (cameraActiveSteps.includes(this.currentStep)) {
513
+ this.sessionExpiryTimer = setTimeout(() => this._showExpiryWarningWhenSafe(), 5000);
514
+ return;
515
+ }
516
+ this.showErrorStep("Your session is about to expire. Please complete verification quickly or restart.");
517
+ }
481
518
  // ─────────────────────────────────────────────────────────────────
482
519
  // Step rendering
483
520
  // ─────────────────────────────────────────────────────────────────
@@ -547,18 +584,12 @@ export class SmartComplyFlow {
547
584
  case "document_confirm":
548
585
  this.renderDocumentConfirm(wrapper);
549
586
  break;
550
- case "processing":
551
- // C1: processing step is dead code — flow now goes to 'done' after submit
552
- break;
553
587
  case "liveness":
554
588
  this.renderLiveness(wrapper);
555
589
  break;
556
590
  case "done":
557
591
  this.renderDone(wrapper);
558
592
  break;
559
- case "result":
560
- // C2: result step is dead code — showResult/renderResult removed
561
- break;
562
593
  case "error":
563
594
  break;
564
595
  }
@@ -568,6 +599,23 @@ export class SmartComplyFlow {
568
599
  wrapper.style.opacity = "1";
569
600
  wrapper.style.transform = "translateX(0)";
570
601
  });
602
+ // Move focus to the new step's heading so keyboard/screen-reader users
603
+ // aren't left on a now-detached button. Skipped for camera-active steps
604
+ // and id_input, which already focuses its first field itself.
605
+ const skipAutoFocusSteps = ["document_capture", "liveness", "id_input"];
606
+ if (!skipAutoFocusSteps.includes(step)) {
607
+ requestAnimationFrame(() => {
608
+ const heading = wrapper.querySelector("h1, h2, h3");
609
+ const target = heading ||
610
+ wrapper.querySelector('button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])');
611
+ if (target) {
612
+ if (heading && !heading.hasAttribute("tabindex")) {
613
+ heading.setAttribute("tabindex", "-1");
614
+ }
615
+ target.focus({ preventScroll: true });
616
+ }
617
+ });
618
+ }
571
619
  }
572
620
  // ── Loading ─────────────────────────────────────────────────────
573
621
  renderLoading(container) {
@@ -1239,14 +1287,14 @@ export class SmartComplyFlow {
1239
1287
  container.appendChild(sub);
1240
1288
  }
1241
1289
  const yesBtn = this.createPrimaryButton("Yes, that's me — continue");
1242
- yesBtn.addEventListener("click", () => {
1290
+ this._onceClick(yesBtn, () => {
1243
1291
  // B5: Reset retry counter on successful confirmation
1244
1292
  this._confirmRetries = 0;
1245
1293
  this.showStep("liveness");
1246
1294
  });
1247
1295
  container.appendChild(yesBtn);
1248
1296
  const noBtn = this.createSecondaryButton("No, use a different ID");
1249
- noBtn.addEventListener("click", () => {
1297
+ this._onceClick(noBtn, () => {
1250
1298
  this._confirmRetries++;
1251
1299
  this._ivsName = "";
1252
1300
  this._ivsGender = "";
@@ -1332,14 +1380,14 @@ export class SmartComplyFlow {
1332
1380
  }
1333
1381
  container.appendChild(card);
1334
1382
  const yesBtn = this.createPrimaryButton("Yes, continue");
1335
- yesBtn.addEventListener("click", () => {
1383
+ this._onceClick(yesBtn, () => {
1336
1384
  // B5: Reset retry counter on successful confirmation
1337
1385
  this._confirmRetries = 0;
1338
1386
  this.showStep("liveness");
1339
1387
  });
1340
1388
  container.appendChild(yesBtn);
1341
1389
  const noBtn = this.createSecondaryButton("No, retake document");
1342
- noBtn.addEventListener("click", () => {
1390
+ this._onceClick(noBtn, () => {
1343
1391
  this._confirmRetries++;
1344
1392
  this._ocrName = "";
1345
1393
  this._ocrDocNumber = "";
@@ -1840,9 +1888,6 @@ export class SmartComplyFlow {
1840
1888
  this.showErrorStep(err.message || "Liveness verification failed. Please try again.");
1841
1889
  });
1842
1890
  }
1843
- renderResult(_container) {
1844
- // C2: Dead code — flow routes to 'done' step, not 'result'. Kept as stub.
1845
- }
1846
1891
  // ── Error ───────────────────────────────────────────────────────
1847
1892
  showErrorStep(message) {
1848
1893
  this.currentStep = "error";
@@ -2128,6 +2173,17 @@ export class SmartComplyFlow {
2128
2173
  });
2129
2174
  return btn;
2130
2175
  }
2176
+ /** Click handler that fires once and disables the button, preventing a double-tap double-dispatch. */
2177
+ _onceClick(btn, handler) {
2178
+ btn.addEventListener("click", () => {
2179
+ if (btn.disabled)
2180
+ return;
2181
+ btn.disabled = true;
2182
+ btn.style.opacity = "0.6";
2183
+ btn.style.cursor = "default";
2184
+ handler();
2185
+ });
2186
+ }
2131
2187
  isDocumentFlow() {
2132
2188
  // 1. Check verification_type — handle both string and array from backend
2133
2189
  const vType = this.sdkConfig?.verification_type;
@@ -2157,7 +2213,7 @@ export class SmartComplyFlow {
2157
2213
  const allActionSteps = [...stage1Steps, ...stage2Steps];
2158
2214
  const totalSteps = allActionSteps.length;
2159
2215
  const actionIndex = allActionSteps.indexOf(this.currentStep);
2160
- const pct = this.currentStep === "done" || this.currentStep === "result" ? 100
2216
+ const pct = this.currentStep === "done" ? 100
2161
2217
  : actionIndex >= 0 ? ((actionIndex + 1) / totalSteps) * 100
2162
2218
  : 0;
2163
2219
  if (this.progressBar) {
@@ -2168,7 +2224,7 @@ export class SmartComplyFlow {
2168
2224
  if (this.currentStep === "loading" || this.currentStep === "welcome") {
2169
2225
  this.stepLabel.textContent = "";
2170
2226
  }
2171
- else if (this.currentStep === "done" || this.currentStep === "result") {
2227
+ else if (this.currentStep === "done") {
2172
2228
  this.stepLabel.textContent = "Complete ✓";
2173
2229
  }
2174
2230
  else if (stage2Steps.includes(this.currentStep)) {
@@ -2186,7 +2242,7 @@ export class SmartComplyFlow {
2186
2242
  const dots = Array.from(this.stageDots.children);
2187
2243
  const inStage1 = stage1Steps.includes(this.currentStep);
2188
2244
  const inStage2 = stage2Steps.includes(this.currentStep);
2189
- const isComplete = this.currentStep === "done" || this.currentStep === "result";
2245
+ const isComplete = this.currentStep === "done";
2190
2246
  const stage1Active = inStage1 || inStage2 || isComplete;
2191
2247
  const stage2Active = inStage2 || isComplete;
2192
2248
  if (dots[0])