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.
- package/dist/camera/CameraManager.d.ts +16 -0
- package/dist/camera/CameraManager.d.ts.map +1 -1
- package/dist/camera/CameraManager.js +44 -1
- package/dist/camera/CameraManager.js.map +1 -1
- package/dist/camera/VideoRecorder.d.ts +3 -0
- package/dist/camera/VideoRecorder.d.ts.map +1 -1
- package/dist/camera/VideoRecorder.js +10 -1
- package/dist/camera/VideoRecorder.js.map +1 -1
- package/dist/esm/camera/CameraManager.d.ts +16 -0
- package/dist/esm/camera/CameraManager.d.ts.map +1 -1
- package/dist/esm/camera/CameraManager.js +44 -1
- package/dist/esm/camera/CameraManager.js.map +1 -1
- package/dist/esm/camera/VideoRecorder.d.ts +3 -0
- package/dist/esm/camera/VideoRecorder.d.ts.map +1 -1
- package/dist/esm/camera/VideoRecorder.js +10 -1
- package/dist/esm/camera/VideoRecorder.js.map +1 -1
- package/dist/esm/flow/SmartComplyFlow.d.ts +4 -1
- package/dist/esm/flow/SmartComplyFlow.d.ts.map +1 -1
- package/dist/esm/flow/SmartComplyFlow.js +78 -22
- package/dist/esm/flow/SmartComplyFlow.js.map +1 -1
- package/dist/esm/modules/liveness/LivenessUI.d.ts.map +1 -1
- package/dist/esm/modules/liveness/LivenessUI.js +10 -1
- package/dist/esm/modules/liveness/LivenessUI.js.map +1 -1
- package/dist/esm/modules/liveness/liveness.d.ts +3 -1
- package/dist/esm/modules/liveness/liveness.d.ts.map +1 -1
- package/dist/esm/modules/liveness/liveness.js +55 -23
- package/dist/esm/modules/liveness/liveness.js.map +1 -1
- package/dist/esm/utils/constants.d.ts +6 -2
- package/dist/esm/utils/constants.d.ts.map +1 -1
- package/dist/esm/utils/constants.js +6 -2
- package/dist/esm/utils/constants.js.map +1 -1
- package/dist/flow/SmartComplyFlow.d.ts +4 -1
- package/dist/flow/SmartComplyFlow.d.ts.map +1 -1
- package/dist/flow/SmartComplyFlow.js +78 -22
- package/dist/flow/SmartComplyFlow.js.map +1 -1
- package/dist/modules/liveness/LivenessUI.d.ts.map +1 -1
- package/dist/modules/liveness/LivenessUI.js +10 -1
- package/dist/modules/liveness/LivenessUI.js.map +1 -1
- package/dist/modules/liveness/liveness.d.ts +3 -1
- package/dist/modules/liveness/liveness.d.ts.map +1 -1
- package/dist/modules/liveness/liveness.js +55 -23
- package/dist/modules/liveness/liveness.js.map +1 -1
- package/dist/smartcomply.browser.js +392 -255
- package/dist/smartcomply.browser.js.map +4 -4
- package/dist/utils/constants.d.ts +6 -2
- package/dist/utils/constants.d.ts.map +1 -1
- package/dist/utils/constants.js +6 -2
- package/dist/utils/constants.js.map +1 -1
- 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
|
-
|
|
317
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
5435
|
+
let scanAnimFrameId;
|
|
5436
|
+
let scanTimeoutId;
|
|
5437
|
+
const scanPromise = new Promise(
|
|
5161
5438
|
(resolve) => {
|
|
5162
|
-
let animFrameId;
|
|
5163
5439
|
const scanStart = performance.now();
|
|
5164
|
-
|
|
5165
|
-
cancelAnimationFrame(
|
|
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(
|
|
5182
|
-
cancelAnimationFrame(
|
|
5457
|
+
clearTimeout(scanTimeoutId);
|
|
5458
|
+
if (scanAnimFrameId !== void 0) cancelAnimationFrame(scanAnimFrameId);
|
|
5183
5459
|
resolve("completed");
|
|
5184
5460
|
return;
|
|
5185
5461
|
}
|
|
5186
5462
|
}
|
|
5187
|
-
|
|
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
|
-
|
|
6821
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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"
|
|
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"
|
|
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"
|
|
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);
|