smartcomply-web-sdk 1.0.57 → 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 +55 -23
  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 +55 -23
  42. package/dist/modules/liveness/liveness.js.map +1 -1
  43. package/dist/smartcomply.browser.js +392 -255
  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
@@ -298,12 +298,88 @@ var SmartComplySDK = (() => {
298
298
  var MAX_SNAPSHOT_BYTES = 2 * 1024 * 1024;
299
299
  var DEFAULT_CHALLENGE_ACTIONS = ["BLINK"];
300
300
  var LIVENESS_TIME_LIMIT_SECONDS = 12;
301
+ var RECORDING_BITS_PER_SECOND = 8e5;
302
+
303
+ // src/camera/Permission.ts
304
+ async function queryCameraPermission() {
305
+ if (typeof navigator === "undefined" || !navigator.mediaDevices) {
306
+ return { status: "unsupported", reason: "getUserMedia is not supported in this browser." };
307
+ }
308
+ if (!navigator.permissions?.query) {
309
+ return { status: "unknown" };
310
+ }
311
+ try {
312
+ const result = await navigator.permissions.query({ name: "camera" });
313
+ return { status: result.state };
314
+ } catch {
315
+ return { status: "unknown" };
316
+ }
317
+ }
318
+ async function requestCameraAccess(constraints) {
319
+ const defaults = {
320
+ video: { facingMode: "user", width: { ideal: 640 }, height: { ideal: 480 } },
321
+ audio: false
322
+ };
323
+ try {
324
+ const stream = await navigator.mediaDevices.getUserMedia(constraints ?? defaults);
325
+ return { stream, permission: { status: "granted" } };
326
+ } catch (err) {
327
+ const name = err?.name ?? "";
328
+ if (name === "NotAllowedError" || name === "PermissionDeniedError") {
329
+ return {
330
+ stream: null,
331
+ permission: {
332
+ status: "denied",
333
+ reason: "Camera access was denied. Please allow camera permissions in your browser settings and try again."
334
+ }
335
+ };
336
+ }
337
+ if (name === "NotFoundError" || name === "DevicesNotFoundError") {
338
+ return {
339
+ stream: null,
340
+ permission: {
341
+ status: "unsupported",
342
+ reason: "No camera was found. Please connect a camera and try again."
343
+ }
344
+ };
345
+ }
346
+ if (name === "NotReadableError" || name === "TrackStartError") {
347
+ return {
348
+ stream: null,
349
+ permission: {
350
+ status: "unknown",
351
+ reason: "Camera is already in use by another application. Please close it and try again."
352
+ }
353
+ };
354
+ }
355
+ return {
356
+ stream: null,
357
+ permission: {
358
+ status: "unknown",
359
+ reason: err?.message ?? "Unable to access the camera."
360
+ }
361
+ };
362
+ }
363
+ }
364
+ function releaseCameraStream(stream) {
365
+ if (!stream) return;
366
+ stream.getTracks().forEach((t2) => t2.stop());
367
+ }
368
+ function isCameraSupported() {
369
+ return typeof navigator !== "undefined" && !!navigator.mediaDevices?.getUserMedia && typeof MediaRecorder !== "undefined";
370
+ }
301
371
 
302
372
  // src/camera/CameraManager.ts
303
373
  var CameraManager = class {
304
374
  constructor() {
305
375
  this.stream = null;
376
+ this.onInterrupted = null;
377
+ this.trackEndedHandlers = [];
306
378
  }
379
+ /**
380
+ * Opens the camera via requestCameraAccess(), which maps browser errors
381
+ * (denied, no device, in-use-by-another-app) to a clean message thrown here.
382
+ */
307
383
  async open(constraints) {
308
384
  const defaults = {
309
385
  video: {
@@ -313,9 +389,12 @@ var SmartComplySDK = (() => {
313
389
  },
314
390
  audio: false
315
391
  };
316
- this.stream = await navigator.mediaDevices.getUserMedia(
317
- constraints || defaults
318
- );
392
+ const { stream, permission } = await requestCameraAccess(constraints || defaults);
393
+ if (!stream) {
394
+ throw new Error(permission.reason || "Unable to access the camera.");
395
+ }
396
+ this.stream = stream;
397
+ this._watchTracks();
319
398
  return this.stream;
320
399
  }
321
400
  getStream() {
@@ -329,12 +408,39 @@ var SmartComplySDK = (() => {
329
408
  */
330
409
  setStream(stream) {
331
410
  this.stream = stream;
411
+ this._watchTracks();
412
+ }
413
+ /**
414
+ * Register a callback fired the moment any track in the current stream
415
+ * ends unexpectedly (OS revokes camera permission mid-session, another
416
+ * app steals the camera, device sleeps and the driver drops the feed).
417
+ * Without this, a dead stream is silently invisible — the video element
418
+ * just freezes on its last frame and face detection quietly fails
419
+ * forever with no indication anything went wrong.
420
+ */
421
+ onStreamInterrupted(callback) {
422
+ this.onInterrupted = callback;
423
+ }
424
+ _watchTracks() {
425
+ this.trackEndedHandlers = [];
426
+ if (!this.stream) return;
427
+ this.stream.getTracks().forEach((track) => {
428
+ const handler = () => {
429
+ if (this.stream) {
430
+ this.onInterrupted?.();
431
+ }
432
+ };
433
+ track.addEventListener("ended", handler);
434
+ this.trackEndedHandlers.push(() => track.removeEventListener("ended", handler));
435
+ });
332
436
  }
333
437
  stop() {
334
438
  if (this.stream) {
335
439
  this.stream.getTracks().forEach((track) => track.stop());
336
440
  this.stream = null;
337
441
  }
442
+ this.trackEndedHandlers.forEach((remove) => remove());
443
+ this.trackEndedHandlers = [];
338
444
  }
339
445
  };
340
446
 
@@ -344,20 +450,27 @@ var SmartComplySDK = (() => {
344
450
  this.recorder = null;
345
451
  this.chunks = [];
346
452
  this.startedAt = 0;
453
+ this.onRecordingError = null;
454
+ }
455
+ /** Fires immediately on a mid-recording error, unlike stop()'s onerror which only fires when stop() is later called. */
456
+ onError(callback) {
457
+ this.onRecordingError = callback;
347
458
  }
348
459
  start(stream) {
349
460
  this.chunks = [];
350
461
  const mimeType = MediaRecorder.isTypeSupported("video/webm;codecs=vp9") ? "video/webm;codecs=vp9" : "video/webm";
351
462
  this.recorder = new MediaRecorder(stream, {
352
463
  mimeType,
353
- videoBitsPerSecond: 8e5
354
- // raised from 250k — higher bitrate = sharper frames for SmartComply face extractor
464
+ videoBitsPerSecond: RECORDING_BITS_PER_SECOND
355
465
  });
356
466
  this.recorder.ondataavailable = (e2) => {
357
467
  if (e2.data.size > 0) {
358
468
  this.chunks.push(e2.data);
359
469
  }
360
470
  };
471
+ this.recorder.addEventListener("error", () => {
472
+ this.onRecordingError?.();
473
+ });
361
474
  this.startedAt = Date.now();
362
475
  this.recorder.start();
363
476
  }
@@ -4798,6 +4911,10 @@ var SmartComplySDK = (() => {
4798
4911
  const btn = this.overlayEl.querySelector("#sc-retry-btn");
4799
4912
  if (btn) {
4800
4913
  btn.addEventListener("click", () => {
4914
+ if (btn.disabled) return;
4915
+ btn.disabled = true;
4916
+ btn.style.opacity = "0.6";
4917
+ btn.style.cursor = "default";
4801
4918
  onRetry();
4802
4919
  resolve();
4803
4920
  });
@@ -4905,6 +5022,148 @@ var SmartComplySDK = (() => {
4905
5022
  }
4906
5023
  };
4907
5024
 
5025
+ // src/utils/logger.ts
5026
+ var PREFIX = "[SmartComply SDK]";
5027
+ function isDebugEnabled() {
5028
+ try {
5029
+ return !!window.__SC_DEBUG__;
5030
+ } catch {
5031
+ return false;
5032
+ }
5033
+ }
5034
+ var SDKLogger = {
5035
+ /** Emit an informational message (debug mode only). */
5036
+ info(message, ...args) {
5037
+ if (!isDebugEnabled()) return;
5038
+ console.info(`${PREFIX} ℹ ${message}`, ...args);
5039
+ },
5040
+ /** Emit a warning (always visible). */
5041
+ warn(message, ...args) {
5042
+ console.warn(`${PREFIX} ⚠ ${message}`, ...args);
5043
+ },
5044
+ /** Emit an error (always visible). */
5045
+ error(message, ...args) {
5046
+ console.error(`${PREFIX} ✖ ${message}`, ...args);
5047
+ },
5048
+ /** Emit a debug trace (debug mode only — lowest priority). */
5049
+ debug(message, ...args) {
5050
+ if (!isDebugEnabled()) return;
5051
+ console.debug(`${PREFIX} ⬡ ${message}`, ...args);
5052
+ },
5053
+ /** Log a timed group of related messages (debug mode only). */
5054
+ group(label, fn2) {
5055
+ if (!isDebugEnabled()) {
5056
+ fn2();
5057
+ return;
5058
+ }
5059
+ console.group(`${PREFIX} ${label}`);
5060
+ fn2();
5061
+ console.groupEnd();
5062
+ },
5063
+ /**
5064
+ * Enable or disable verbose debug output at runtime.
5065
+ *
5066
+ * Example:
5067
+ * import { SDKLogger } from "smartcomply-web-sdk";
5068
+ * SDKLogger.setDebug(true);
5069
+ */
5070
+ setDebug(enabled) {
5071
+ try {
5072
+ window.__SC_DEBUG__ = enabled;
5073
+ } catch {
5074
+ }
5075
+ }
5076
+ };
5077
+
5078
+ // src/modules/liveness/uploader.ts
5079
+ var LivenessUploader = class {
5080
+ constructor(http) {
5081
+ this.http = http;
5082
+ }
5083
+ /**
5084
+ * Upload a FormData payload to `path` with optional progress and
5085
+ * cancellation support.
5086
+ *
5087
+ * Falls back to the standard `uploadWithSession()` on browsers that do not
5088
+ * support XMLHttpRequest upload progress (very rare).
5089
+ */
5090
+ async upload(path, formData, options = {}) {
5091
+ const { onProgress, signal, retries = MAX_RETRIES } = options;
5092
+ if (!onProgress) {
5093
+ return this.http.uploadWithSession(path, formData);
5094
+ }
5095
+ return this.uploadWithProgress(path, formData, onProgress, signal, retries);
5096
+ }
5097
+ // ── Private ────────────────────────────────────────────────────────────────
5098
+ async uploadWithProgress(path, formData, onProgress, signal, retriesLeft) {
5099
+ const sessionToken = this.http.sessionToken;
5100
+ if (!sessionToken) {
5101
+ throw new Error("SDK: No session token. Call sdk.createSession() first.");
5102
+ }
5103
+ const baseUrl = this.http.baseUrl;
5104
+ return new Promise((resolve, reject) => {
5105
+ const xhr = new XMLHttpRequest();
5106
+ if (signal) {
5107
+ if (signal.aborted) {
5108
+ reject(new DOMException("Upload cancelled", "AbortError"));
5109
+ return;
5110
+ }
5111
+ signal.addEventListener("abort", () => xhr.abort(), { once: true });
5112
+ }
5113
+ xhr.open("POST", `${baseUrl}${path}`, true);
5114
+ xhr.setRequestHeader("x-access-token", sessionToken);
5115
+ xhr.upload.onprogress = (evt) => {
5116
+ if (evt.lengthComputable) {
5117
+ onProgress(evt.loaded / evt.total);
5118
+ }
5119
+ };
5120
+ xhr.onload = () => {
5121
+ onProgress(1);
5122
+ let envelope;
5123
+ try {
5124
+ envelope = JSON.parse(xhr.responseText);
5125
+ } catch {
5126
+ reject(new Error(`SDK: Non-JSON response from ${path} (${xhr.status})`));
5127
+ return;
5128
+ }
5129
+ if (xhr.status >= 200 && xhr.status < 300 && envelope.status !== "error") {
5130
+ SDKLogger.info(`Upload to ${path} succeeded`, { status: xhr.status });
5131
+ resolve(envelope.data);
5132
+ } else {
5133
+ const err = new Error(envelope.message || `Upload to ${path} failed (${xhr.status})`);
5134
+ err.statusCode = xhr.status;
5135
+ err.code = envelope.code;
5136
+ reject(err);
5137
+ }
5138
+ };
5139
+ xhr.onerror = async () => {
5140
+ SDKLogger.warn(`Upload to ${path} failed — network error`, { retriesLeft });
5141
+ if (retriesLeft > 0) {
5142
+ await new Promise((r2) => setTimeout(r2, RETRY_DELAY_MS));
5143
+ try {
5144
+ const result = await this.uploadWithProgress(
5145
+ path,
5146
+ formData,
5147
+ onProgress,
5148
+ signal,
5149
+ retriesLeft - 1
5150
+ );
5151
+ resolve(result);
5152
+ } catch (retryErr) {
5153
+ reject(retryErr);
5154
+ }
5155
+ } else {
5156
+ reject(new Error(`SDK: Upload to ${path} failed after all retries.`));
5157
+ }
5158
+ };
5159
+ xhr.onabort = () => {
5160
+ reject(new DOMException("Upload cancelled by user", "AbortError"));
5161
+ };
5162
+ xhr.send(formData);
5163
+ });
5164
+ }
5165
+ };
5166
+
4908
5167
  // src/modules/liveness/liveness.ts
4909
5168
  var LivenessModule = class {
4910
5169
  constructor(http) {
@@ -4978,8 +5237,10 @@ var SmartComplySDK = (() => {
4978
5237
  *
4979
5238
  * Note: After submission, the session is revoked (single-use).
4980
5239
  * The backend returns status: "processing" — results come via webhook.
5240
+ *
5241
+ * Pass onProgress for 0–1 upload-fraction callbacks (via LivenessUploader).
4981
5242
  */
4982
- async submit(entryId, videoBlob, snapshotBlob, autoshotBlob, videoDurationSeconds, videoSizeBytes) {
5243
+ async submit(entryId, videoBlob, snapshotBlob, autoshotBlob, videoDurationSeconds, videoSizeBytes, onProgress) {
4983
5244
  const formData = new FormData();
4984
5245
  formData.append("entry", String(entryId));
4985
5246
  formData.append("video", videoBlob, "liveness.webm");
@@ -4993,6 +5254,14 @@ var SmartComplySDK = (() => {
4993
5254
  if (videoSizeBytes !== void 0) {
4994
5255
  formData.append("video_size_bytes", String(videoSizeBytes));
4995
5256
  }
5257
+ if (onProgress) {
5258
+ const uploader = new LivenessUploader(this.http);
5259
+ return uploader.upload(
5260
+ "/v1/sdk/liveness/submit/",
5261
+ formData,
5262
+ { onProgress }
5263
+ );
5264
+ }
4996
5265
  return this.http.uploadWithSession(
4997
5266
  "/v1/sdk/liveness/submit/",
4998
5267
  formData
@@ -5042,25 +5311,31 @@ var SmartComplySDK = (() => {
5042
5311
  camera.stop();
5043
5312
  ui2.unmount();
5044
5313
  };
5314
+ let interruptedReject = null;
5315
+ const interrupted = new Promise((_2, reject) => {
5316
+ interruptedReject = reject;
5317
+ });
5318
+ camera.onStreamInterrupted(() => {
5319
+ interruptedReject?.(
5320
+ new Error("Camera connection was lost. Please check your camera and try again.")
5321
+ );
5322
+ });
5323
+ recorder.onError(() => {
5324
+ interruptedReject?.(
5325
+ new Error("Recording failed unexpectedly. Please try again.")
5326
+ );
5327
+ });
5328
+ interrupted.catch(() => {
5329
+ });
5045
5330
  try {
5046
5331
  let stream;
5047
5332
  if (params.prewarmedStream && params.prewarmedStream.active) {
5048
5333
  stream = params.prewarmedStream;
5049
5334
  camera.setStream(stream);
5050
5335
  } else {
5051
- try {
5052
- stream = await camera.open();
5053
- } catch (camErr) {
5054
- if (camErr.name === "NotAllowedError" || camErr.name === "PermissionDeniedError") {
5055
- throw new Error("Camera access denied. Please allow camera permission in your browser settings and try again.");
5056
- }
5057
- if (camErr.name === "NotFoundError" || camErr.name === "DevicesNotFoundError") {
5058
- throw new Error("No camera found. Please connect a camera and try again.");
5059
- }
5060
- throw new Error(`Camera error: ${camErr.message || "Unable to access camera"}`);
5061
- }
5336
+ stream = await camera.open();
5062
5337
  }
5063
- let capturedDocBlob = params.id_file;
5338
+ let capturedDocBlob = params.id_file || params.document || params.document_front;
5064
5339
  if (!capturedDocBlob && params.verification_type === "document_verification") {
5065
5340
  capturedDocBlob = await ui2.mountDocumentCapture(container, stream);
5066
5341
  }
@@ -5157,12 +5432,13 @@ var SmartComplySDK = (() => {
5157
5432
  ui2.startScanClock();
5158
5433
  const MIN_SCAN_MS = 2e3;
5159
5434
  const actionDetector = new ActionDetector();
5160
- const detectionResult = await new Promise(
5435
+ let scanAnimFrameId;
5436
+ let scanTimeoutId;
5437
+ const scanPromise = new Promise(
5161
5438
  (resolve) => {
5162
- let animFrameId;
5163
5439
  const scanStart = performance.now();
5164
- const timeout = setTimeout(() => {
5165
- cancelAnimationFrame(animFrameId);
5440
+ scanTimeoutId = setTimeout(() => {
5441
+ if (scanAnimFrameId !== void 0) cancelAnimationFrame(scanAnimFrameId);
5166
5442
  resolve("timeout");
5167
5443
  }, uiChallenge.time_limit_seconds * 1e3);
5168
5444
  const detectLoop = () => {
@@ -5178,17 +5454,27 @@ var SmartComplySDK = (() => {
5178
5454
  }
5179
5455
  const elapsed = performance.now() - scanStart;
5180
5456
  if (detection.faceDetected && scan.livenessConfirmed && elapsed >= MIN_SCAN_MS) {
5181
- clearTimeout(timeout);
5182
- cancelAnimationFrame(animFrameId);
5457
+ clearTimeout(scanTimeoutId);
5458
+ if (scanAnimFrameId !== void 0) cancelAnimationFrame(scanAnimFrameId);
5183
5459
  resolve("completed");
5184
5460
  return;
5185
5461
  }
5186
5462
  }
5187
- animFrameId = requestAnimationFrame(detectLoop);
5463
+ scanAnimFrameId = requestAnimationFrame(detectLoop);
5188
5464
  };
5189
5465
  detectLoop();
5190
5466
  }
5191
5467
  );
5468
+ let detectionResult;
5469
+ try {
5470
+ detectionResult = await Promise.race([scanPromise, interrupted]);
5471
+ } catch (interruptErr) {
5472
+ if (scanAnimFrameId !== void 0) cancelAnimationFrame(scanAnimFrameId);
5473
+ if (scanTimeoutId !== void 0) clearTimeout(scanTimeoutId);
5474
+ recorder.stop().catch(() => {
5475
+ });
5476
+ throw interruptErr;
5477
+ }
5192
5478
  if (detectionResult === "timeout") {
5193
5479
  cleanedUp = true;
5194
5480
  return new Promise((resolveRetry, rejectRetry) => {
@@ -5224,7 +5510,11 @@ var SmartComplySDK = (() => {
5224
5510
  snapshotBlob,
5225
5511
  autoshotBlob,
5226
5512
  durationSeconds,
5227
- videoBlob.size
5513
+ videoBlob.size,
5514
+ (fraction) => {
5515
+ const pct = Math.round(fraction * 100);
5516
+ ui2.updateInstruction(pct < 100 ? `Uploading... ${pct}%` : "Verifying...");
5517
+ }
5228
5518
  );
5229
5519
  await ui2.showResult(
5230
5520
  submitResult.status === "failed" ? "failed" : "processing",
@@ -6657,6 +6947,7 @@ var SmartComplySDK = (() => {
6657
6947
  this.livenessEntryId = null;
6658
6948
  this.isDestroyed = false;
6659
6949
  this.headerBackBtn = null;
6950
+ this._keydownHandler = null;
6660
6951
  // Retry safety — max 3 confirmation rejections or liveness failures per session
6661
6952
  this._confirmRetries = 0;
6662
6953
  this._livenessFailures = 0;
@@ -6714,6 +7005,10 @@ var SmartComplySDK = (() => {
6714
7005
  */
6715
7006
  close() {
6716
7007
  this.isDestroyed = true;
7008
+ if (this._keydownHandler) {
7009
+ document.removeEventListener("keydown", this._keydownHandler);
7010
+ this._keydownHandler = null;
7011
+ }
6717
7012
  this.docCapture?.destroy();
6718
7013
  if (this._prewarmedStream) {
6719
7014
  this._prewarmedStream.getTracks().forEach((t2) => t2.stop());
@@ -6807,6 +7102,29 @@ var SmartComplySDK = (() => {
6807
7102
  if (this.overlay) this.overlay.style.opacity = "1";
6808
7103
  if (this.modal) this.modal.style.transform = "translateY(0) scale(1)";
6809
7104
  });
7105
+ this._keydownHandler = (e2) => {
7106
+ if (e2.key === "Escape") {
7107
+ e2.preventDefault();
7108
+ this.close();
7109
+ return;
7110
+ }
7111
+ if (e2.key === "Tab" && this.modal) {
7112
+ const focusable = this.modal.querySelectorAll(
7113
+ 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
7114
+ );
7115
+ if (focusable.length === 0) return;
7116
+ const first = focusable[0];
7117
+ const last = focusable[focusable.length - 1];
7118
+ if (e2.shiftKey && document.activeElement === first) {
7119
+ e2.preventDefault();
7120
+ last.focus();
7121
+ } else if (!e2.shiftKey && document.activeElement === last) {
7122
+ e2.preventDefault();
7123
+ first.focus();
7124
+ }
7125
+ }
7126
+ };
7127
+ document.addEventListener("keydown", this._keydownHandler);
6810
7128
  }
6811
7129
  async initialize() {
6812
7130
  this.showStep("loading");
@@ -6816,11 +7134,10 @@ var SmartComplySDK = (() => {
6816
7134
  if (expiresAt) {
6817
7135
  const expiresMs = new Date(expiresAt).getTime() - Date.now();
6818
7136
  const warnMs = Math.max(expiresMs - 12e4, 0);
6819
- this.sessionExpiryTimer = setTimeout(() => {
6820
- if (!this.isDestroyed && this.currentStep !== "result") {
6821
- this.showErrorStep("Your session is about to expire. Please complete verification quickly or restart.");
6822
- }
6823
- }, warnMs);
7137
+ this.sessionExpiryTimer = setTimeout(
7138
+ () => this._showExpiryWarningWhenSafe(),
7139
+ warnMs
7140
+ );
6824
7141
  }
6825
7142
  this.sdkConfig = await this.sdk.initializeConfig();
6826
7143
  if (this.sdkConfig.theme) {
@@ -6836,6 +7153,18 @@ var SmartComplySDK = (() => {
6836
7153
  this.showErrorStep(err.message || "Failed to initialize. Please try again.");
6837
7154
  }
6838
7155
  }
7156
+ // Defers the expiry warning while the user is on a camera-active step
7157
+ // (document_capture/liveness) — showing it there would blow away a live
7158
+ // video element and in-progress MediaRecorder mid-scan.
7159
+ _showExpiryWarningWhenSafe() {
7160
+ if (this.isDestroyed) return;
7161
+ const cameraActiveSteps = ["document_capture", "liveness"];
7162
+ if (cameraActiveSteps.includes(this.currentStep)) {
7163
+ this.sessionExpiryTimer = setTimeout(() => this._showExpiryWarningWhenSafe(), 5e3);
7164
+ return;
7165
+ }
7166
+ this.showErrorStep("Your session is about to expire. Please complete verification quickly or restart.");
7167
+ }
6839
7168
  // ─────────────────────────────────────────────────────────────────
6840
7169
  // Step rendering
6841
7170
  // ─────────────────────────────────────────────────────────────────
@@ -6894,16 +7223,12 @@ var SmartComplySDK = (() => {
6894
7223
  case "document_confirm":
6895
7224
  this.renderDocumentConfirm(wrapper);
6896
7225
  break;
6897
- case "processing":
6898
- break;
6899
7226
  case "liveness":
6900
7227
  this.renderLiveness(wrapper);
6901
7228
  break;
6902
7229
  case "done":
6903
7230
  this.renderDone(wrapper);
6904
7231
  break;
6905
- case "result":
6906
- break;
6907
7232
  case "error":
6908
7233
  break;
6909
7234
  }
@@ -6912,6 +7237,21 @@ var SmartComplySDK = (() => {
6912
7237
  wrapper.style.opacity = "1";
6913
7238
  wrapper.style.transform = "translateX(0)";
6914
7239
  });
7240
+ const skipAutoFocusSteps = ["document_capture", "liveness", "id_input"];
7241
+ if (!skipAutoFocusSteps.includes(step)) {
7242
+ requestAnimationFrame(() => {
7243
+ const heading = wrapper.querySelector("h1, h2, h3");
7244
+ const target = heading || wrapper.querySelector(
7245
+ 'button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
7246
+ );
7247
+ if (target) {
7248
+ if (heading && !heading.hasAttribute("tabindex")) {
7249
+ heading.setAttribute("tabindex", "-1");
7250
+ }
7251
+ target.focus({ preventScroll: true });
7252
+ }
7253
+ });
7254
+ }
6915
7255
  }
6916
7256
  // ── Loading ─────────────────────────────────────────────────────
6917
7257
  renderLoading(container) {
@@ -7498,13 +7838,13 @@ var SmartComplySDK = (() => {
7498
7838
  container.appendChild(sub);
7499
7839
  }
7500
7840
  const yesBtn = this.createPrimaryButton("Yes, that's me — continue");
7501
- yesBtn.addEventListener("click", () => {
7841
+ this._onceClick(yesBtn, () => {
7502
7842
  this._confirmRetries = 0;
7503
7843
  this.showStep("liveness");
7504
7844
  });
7505
7845
  container.appendChild(yesBtn);
7506
7846
  const noBtn = this.createSecondaryButton("No, use a different ID");
7507
- noBtn.addEventListener("click", () => {
7847
+ this._onceClick(noBtn, () => {
7508
7848
  this._confirmRetries++;
7509
7849
  this._ivsName = "";
7510
7850
  this._ivsGender = "";
@@ -7580,13 +7920,13 @@ var SmartComplySDK = (() => {
7580
7920
  }
7581
7921
  container.appendChild(card);
7582
7922
  const yesBtn = this.createPrimaryButton("Yes, continue");
7583
- yesBtn.addEventListener("click", () => {
7923
+ this._onceClick(yesBtn, () => {
7584
7924
  this._confirmRetries = 0;
7585
7925
  this.showStep("liveness");
7586
7926
  });
7587
7927
  container.appendChild(yesBtn);
7588
7928
  const noBtn = this.createSecondaryButton("No, retake document");
7589
- noBtn.addEventListener("click", () => {
7929
+ this._onceClick(noBtn, () => {
7590
7930
  this._confirmRetries++;
7591
7931
  this._ocrName = "";
7592
7932
  this._ocrDocNumber = "";
@@ -8013,8 +8353,6 @@ var SmartComplySDK = (() => {
8013
8353
  this.showErrorStep(err.message || "Liveness verification failed. Please try again.");
8014
8354
  });
8015
8355
  }
8016
- renderResult(_container) {
8017
- }
8018
8356
  // ── Error ───────────────────────────────────────────────────────
8019
8357
  showErrorStep(message) {
8020
8358
  this.currentStep = "error";
@@ -8263,6 +8601,16 @@ var SmartComplySDK = (() => {
8263
8601
  });
8264
8602
  return btn;
8265
8603
  }
8604
+ /** Click handler that fires once and disables the button, preventing a double-tap double-dispatch. */
8605
+ _onceClick(btn, handler) {
8606
+ btn.addEventListener("click", () => {
8607
+ if (btn.disabled) return;
8608
+ btn.disabled = true;
8609
+ btn.style.opacity = "0.6";
8610
+ btn.style.cursor = "default";
8611
+ handler();
8612
+ });
8613
+ }
8266
8614
  isDocumentFlow() {
8267
8615
  const vType = this.sdkConfig?.verification_type;
8268
8616
  if (vType) {
@@ -8283,14 +8631,14 @@ var SmartComplySDK = (() => {
8283
8631
  const allActionSteps = [...stage1Steps, ...stage2Steps];
8284
8632
  const totalSteps = allActionSteps.length;
8285
8633
  const actionIndex = allActionSteps.indexOf(this.currentStep);
8286
- const pct = this.currentStep === "done" || this.currentStep === "result" ? 100 : actionIndex >= 0 ? (actionIndex + 1) / totalSteps * 100 : 0;
8634
+ const pct = this.currentStep === "done" ? 100 : actionIndex >= 0 ? (actionIndex + 1) / totalSteps * 100 : 0;
8287
8635
  if (this.progressBar) {
8288
8636
  this.progressBar.style.width = `${pct}%`;
8289
8637
  }
8290
8638
  if (this.stepLabel) {
8291
8639
  if (this.currentStep === "loading" || this.currentStep === "welcome") {
8292
8640
  this.stepLabel.textContent = "";
8293
- } else if (this.currentStep === "done" || this.currentStep === "result") {
8641
+ } else if (this.currentStep === "done") {
8294
8642
  this.stepLabel.textContent = "Complete ✓";
8295
8643
  } else if (stage2Steps.includes(this.currentStep)) {
8296
8644
  this.stepLabel.textContent = "Stage 2 of 2: Face Check";
@@ -8304,7 +8652,7 @@ var SmartComplySDK = (() => {
8304
8652
  const dots = Array.from(this.stageDots.children);
8305
8653
  const inStage1 = stage1Steps.includes(this.currentStep);
8306
8654
  const inStage2 = stage2Steps.includes(this.currentStep);
8307
- const isComplete = this.currentStep === "done" || this.currentStep === "result";
8655
+ const isComplete = this.currentStep === "done";
8308
8656
  const stage1Active = inStage1 || inStage2 || isComplete;
8309
8657
  const stage2Active = inStage2 || isComplete;
8310
8658
  if (dots[0]) dots[0].style.background = stage1Active ? this.theme.primary : this.theme.border;
@@ -8392,217 +8740,6 @@ var SmartComplySDK = (() => {
8392
8740
  _SmartComplyFlow.MAX_RETRIES = 3;
8393
8741
  var SmartComplyFlow = _SmartComplyFlow;
8394
8742
 
8395
- // src/utils/logger.ts
8396
- var PREFIX = "[SmartComply SDK]";
8397
- function isDebugEnabled() {
8398
- try {
8399
- return !!window.__SC_DEBUG__;
8400
- } catch {
8401
- return false;
8402
- }
8403
- }
8404
- var SDKLogger = {
8405
- /** Emit an informational message (debug mode only). */
8406
- info(message, ...args) {
8407
- if (!isDebugEnabled()) return;
8408
- console.info(`${PREFIX} ℹ ${message}`, ...args);
8409
- },
8410
- /** Emit a warning (always visible). */
8411
- warn(message, ...args) {
8412
- console.warn(`${PREFIX} ⚠ ${message}`, ...args);
8413
- },
8414
- /** Emit an error (always visible). */
8415
- error(message, ...args) {
8416
- console.error(`${PREFIX} ✖ ${message}`, ...args);
8417
- },
8418
- /** Emit a debug trace (debug mode only — lowest priority). */
8419
- debug(message, ...args) {
8420
- if (!isDebugEnabled()) return;
8421
- console.debug(`${PREFIX} ⬡ ${message}`, ...args);
8422
- },
8423
- /** Log a timed group of related messages (debug mode only). */
8424
- group(label, fn2) {
8425
- if (!isDebugEnabled()) {
8426
- fn2();
8427
- return;
8428
- }
8429
- console.group(`${PREFIX} ${label}`);
8430
- fn2();
8431
- console.groupEnd();
8432
- },
8433
- /**
8434
- * Enable or disable verbose debug output at runtime.
8435
- *
8436
- * Example:
8437
- * import { SDKLogger } from "smartcomply-web-sdk";
8438
- * SDKLogger.setDebug(true);
8439
- */
8440
- setDebug(enabled) {
8441
- try {
8442
- window.__SC_DEBUG__ = enabled;
8443
- } catch {
8444
- }
8445
- }
8446
- };
8447
-
8448
- // src/camera/Permission.ts
8449
- async function queryCameraPermission() {
8450
- if (typeof navigator === "undefined" || !navigator.mediaDevices) {
8451
- return { status: "unsupported", reason: "getUserMedia is not supported in this browser." };
8452
- }
8453
- if (!navigator.permissions?.query) {
8454
- return { status: "unknown" };
8455
- }
8456
- try {
8457
- const result = await navigator.permissions.query({ name: "camera" });
8458
- return { status: result.state };
8459
- } catch {
8460
- return { status: "unknown" };
8461
- }
8462
- }
8463
- async function requestCameraAccess(constraints) {
8464
- const defaults = {
8465
- video: { facingMode: "user", width: { ideal: 640 }, height: { ideal: 480 } },
8466
- audio: false
8467
- };
8468
- try {
8469
- const stream = await navigator.mediaDevices.getUserMedia(constraints ?? defaults);
8470
- return { stream, permission: { status: "granted" } };
8471
- } catch (err) {
8472
- const name = err?.name ?? "";
8473
- if (name === "NotAllowedError" || name === "PermissionDeniedError") {
8474
- return {
8475
- stream: null,
8476
- permission: {
8477
- status: "denied",
8478
- reason: "Camera access was denied. Please allow camera permissions in your browser settings and try again."
8479
- }
8480
- };
8481
- }
8482
- if (name === "NotFoundError" || name === "DevicesNotFoundError") {
8483
- return {
8484
- stream: null,
8485
- permission: {
8486
- status: "unsupported",
8487
- reason: "No camera was found. Please connect a camera and try again."
8488
- }
8489
- };
8490
- }
8491
- if (name === "NotReadableError" || name === "TrackStartError") {
8492
- return {
8493
- stream: null,
8494
- permission: {
8495
- status: "unknown",
8496
- reason: "Camera is already in use by another application. Please close it and try again."
8497
- }
8498
- };
8499
- }
8500
- return {
8501
- stream: null,
8502
- permission: {
8503
- status: "unknown",
8504
- reason: err?.message ?? "Unable to access the camera."
8505
- }
8506
- };
8507
- }
8508
- }
8509
- function releaseCameraStream(stream) {
8510
- if (!stream) return;
8511
- stream.getTracks().forEach((t2) => t2.stop());
8512
- }
8513
- function isCameraSupported() {
8514
- return typeof navigator !== "undefined" && !!navigator.mediaDevices?.getUserMedia && typeof MediaRecorder !== "undefined";
8515
- }
8516
-
8517
- // src/modules/liveness/uploader.ts
8518
- var LivenessUploader = class {
8519
- constructor(http) {
8520
- this.http = http;
8521
- }
8522
- /**
8523
- * Upload a FormData payload to `path` with optional progress and
8524
- * cancellation support.
8525
- *
8526
- * Falls back to the standard `uploadWithSession()` on browsers that do not
8527
- * support XMLHttpRequest upload progress (very rare).
8528
- */
8529
- async upload(path, formData, options = {}) {
8530
- const { onProgress, signal, retries = MAX_RETRIES } = options;
8531
- if (!onProgress) {
8532
- return this.http.uploadWithSession(path, formData);
8533
- }
8534
- return this.uploadWithProgress(path, formData, onProgress, signal, retries);
8535
- }
8536
- // ── Private ────────────────────────────────────────────────────────────────
8537
- async uploadWithProgress(path, formData, onProgress, signal, retriesLeft) {
8538
- const sessionToken = this.http.sessionToken;
8539
- if (!sessionToken) {
8540
- throw new Error("SDK: No session token. Call sdk.createSession() first.");
8541
- }
8542
- const baseUrl = this.http.baseUrl;
8543
- return new Promise((resolve, reject) => {
8544
- const xhr = new XMLHttpRequest();
8545
- if (signal) {
8546
- if (signal.aborted) {
8547
- reject(new DOMException("Upload cancelled", "AbortError"));
8548
- return;
8549
- }
8550
- signal.addEventListener("abort", () => xhr.abort(), { once: true });
8551
- }
8552
- xhr.open("POST", `${baseUrl}${path}`, true);
8553
- xhr.setRequestHeader("x-access-token", sessionToken);
8554
- xhr.upload.onprogress = (evt) => {
8555
- if (evt.lengthComputable) {
8556
- onProgress(evt.loaded / evt.total);
8557
- }
8558
- };
8559
- xhr.onload = () => {
8560
- onProgress(1);
8561
- let envelope;
8562
- try {
8563
- envelope = JSON.parse(xhr.responseText);
8564
- } catch {
8565
- reject(new Error(`SDK: Non-JSON response from ${path} (${xhr.status})`));
8566
- return;
8567
- }
8568
- if (xhr.status >= 200 && xhr.status < 300 && envelope.status !== "error") {
8569
- SDKLogger.info(`Upload to ${path} succeeded`, { status: xhr.status });
8570
- resolve(envelope.data);
8571
- } else {
8572
- const err = new Error(envelope.message || `Upload to ${path} failed (${xhr.status})`);
8573
- err.statusCode = xhr.status;
8574
- err.code = envelope.code;
8575
- reject(err);
8576
- }
8577
- };
8578
- xhr.onerror = async () => {
8579
- SDKLogger.warn(`Upload to ${path} failed — network error`, { retriesLeft });
8580
- if (retriesLeft > 0) {
8581
- await new Promise((r2) => setTimeout(r2, RETRY_DELAY_MS));
8582
- try {
8583
- const result = await this.uploadWithProgress(
8584
- path,
8585
- formData,
8586
- onProgress,
8587
- signal,
8588
- retriesLeft - 1
8589
- );
8590
- resolve(result);
8591
- } catch (retryErr) {
8592
- reject(retryErr);
8593
- }
8594
- } else {
8595
- reject(new Error(`SDK: Upload to ${path} failed after all retries.`));
8596
- }
8597
- };
8598
- xhr.onabort = () => {
8599
- reject(new DOMException("Upload cancelled by user", "AbortError"));
8600
- };
8601
- xhr.send(formData);
8602
- });
8603
- }
8604
- };
8605
-
8606
8743
  // src/index.ts
8607
8744
  var index_default = SmartComply;
8608
8745
  return __toCommonJS(index_exports);