@neofaceid/web-sdk 1.11.1 → 1.13.2

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.
@@ -2288,13 +2288,30 @@ const registerApplication = async (jwtToken, consumerId, applicationData) => {
2288
2288
  };
2289
2289
  const recordFaceVideo = async (options = {}) => {
2290
2290
  ensureSecureContext();
2291
- const { durationMs = 3e3, mimeType = "video/webm", onProgress } = options;
2291
+ const { durationMs = 3e3, onProgress } = options;
2292
+ const getSupportedMimeType = () => {
2293
+ const types = [
2294
+ "video/mp4;codecs=avc1",
2295
+ // iOS Safari (prioritário quando disponível)
2296
+ "video/mp4",
2297
+ // iOS Safari fallback
2298
+ "video/webm;codecs=vp9",
2299
+ // Chrome/Firefox
2300
+ "video/webm;codecs=vp8",
2301
+ // Chrome/Firefox fallback
2302
+ "video/webm"
2303
+ // Android Chrome
2304
+ ];
2305
+ for (const type of types) {
2306
+ if (MediaRecorder.isTypeSupported(type)) return type;
2307
+ }
2308
+ return "";
2309
+ };
2292
2310
  return new Promise(async (resolve, reject) => {
2293
- let stream = null;
2294
- let mediaRecorder = null;
2311
+ let cameraStream = null;
2295
2312
  const chunks = [];
2296
2313
  try {
2297
- stream = await navigator.mediaDevices.getUserMedia({
2314
+ cameraStream = await navigator.mediaDevices.getUserMedia({
2298
2315
  video: {
2299
2316
  facingMode: "user",
2300
2317
  width: { ideal: 640 },
@@ -2302,38 +2319,33 @@ const recordFaceVideo = async (options = {}) => {
2302
2319
  },
2303
2320
  audio: false
2304
2321
  });
2305
- let selectedMimeType = mimeType;
2306
- const mimeTypes = ["video/webm;codecs=vp9", "video/webm;codecs=vp8", "video/webm", "video/mp4"];
2307
- for (const type of mimeTypes) {
2308
- if (MediaRecorder.isTypeSupported(type)) {
2309
- selectedMimeType = type;
2310
- break;
2311
- }
2312
- }
2313
- mediaRecorder = new MediaRecorder(stream, {
2314
- mimeType: selectedMimeType,
2315
- videoBitsPerSecond: 1e6
2316
- // 1 Mbps
2317
- });
2318
- mediaRecorder.ondataavailable = (event) => {
2322
+ const selectedMimeType = getSupportedMimeType();
2323
+ let recorder;
2324
+ try {
2325
+ recorder = selectedMimeType ? new MediaRecorder(cameraStream, { mimeType: selectedMimeType, videoBitsPerSecond: 1e6 }) : new MediaRecorder(cameraStream, { videoBitsPerSecond: 1e6 });
2326
+ } catch {
2327
+ recorder = new MediaRecorder(cameraStream);
2328
+ }
2329
+ const finalMimeType = recorder.mimeType || selectedMimeType;
2330
+ recorder.ondataavailable = (event) => {
2319
2331
  if (event.data.size > 0) {
2320
2332
  chunks.push(event.data);
2321
2333
  }
2322
2334
  };
2323
- mediaRecorder.onstop = () => {
2324
- if (stream) {
2325
- stream.getTracks().forEach((track) => track.stop());
2335
+ recorder.onstop = () => {
2336
+ if (cameraStream) {
2337
+ cameraStream.getTracks().forEach((track) => track.stop());
2326
2338
  }
2327
- const videoBlob = new Blob(chunks, { type: selectedMimeType });
2339
+ const videoBlob = new Blob(chunks, { type: finalMimeType });
2328
2340
  resolve(videoBlob);
2329
2341
  };
2330
- mediaRecorder.onerror = (event) => {
2331
- if (stream) {
2332
- stream.getTracks().forEach((track) => track.stop());
2342
+ recorder.onerror = (event) => {
2343
+ if (cameraStream) {
2344
+ cameraStream.getTracks().forEach((track) => track.stop());
2333
2345
  }
2334
2346
  reject(new NeoFaceError(`Recording error: ${event}`, ErrorType.CAPTURE_ERROR));
2335
2347
  };
2336
- mediaRecorder.start(100);
2348
+ recorder.start(100);
2337
2349
  if (onProgress) {
2338
2350
  const progressInterval = 100;
2339
2351
  let elapsed = 0;
@@ -2347,13 +2359,13 @@ const recordFaceVideo = async (options = {}) => {
2347
2359
  }, progressInterval);
2348
2360
  }
2349
2361
  setTimeout(() => {
2350
- if (mediaRecorder && mediaRecorder.state === "recording") {
2351
- mediaRecorder.stop();
2362
+ if (recorder && recorder.state === "recording") {
2363
+ recorder.stop();
2352
2364
  }
2353
2365
  }, durationMs);
2354
2366
  } catch (error) {
2355
- if (stream) {
2356
- stream.getTracks().forEach((track) => track.stop());
2367
+ if (cameraStream) {
2368
+ cameraStream.getTracks().forEach((track) => track.stop());
2357
2369
  }
2358
2370
  if (error instanceof Error) {
2359
2371
  if (error.name === "NotAllowedError") {
@@ -2808,12 +2820,14 @@ const modalReducer = (state, action) => {
2808
2820
  };
2809
2821
  }
2810
2822
  return state;
2823
+ case "CAMERA_LOADING":
2824
+ return { status: "camera-loading" };
2811
2825
  case "START_PROCESSING":
2812
2826
  return { status: "processing" };
2813
2827
  case "SUCCESS":
2814
2828
  return { status: "success" };
2815
2829
  case "ERROR":
2816
- return { status: "error", message: action.message };
2830
+ return { status: "error", message: action.message, retryable: action.retryable ?? true };
2817
2831
  case "RESET":
2818
2832
  return initialState;
2819
2833
  default:
@@ -2859,9 +2873,7 @@ function BiometricRegistrationModal({
2859
2873
  onClose,
2860
2874
  onSuccess,
2861
2875
  onPhotosCaptured,
2862
- onError,
2863
- useRealApi = true
2864
- // Sempre usa API real em produção
2876
+ onError
2865
2877
  }) {
2866
2878
  const videoRef = useRef(null);
2867
2879
  const canvasRef = useRef(null);
@@ -2870,10 +2882,12 @@ function BiometricRegistrationModal({
2870
2882
  const progressIntervalRef = useRef(null);
2871
2883
  const isCapturingRef = useRef(false);
2872
2884
  const currentStepRef = useRef("center");
2885
+ const watchdogTimerRef = useRef(null);
2886
+ const detectionStartedRef = useRef(false);
2873
2887
  const captureStateRef = useRef(null);
2874
2888
  const [state, dispatch] = useReducer(modalReducer, initialState);
2875
2889
  const [faceDetected, setFaceDetected] = useState(false);
2876
- const [faceDistance, setFaceDistance] = useState("ok");
2890
+ const [_faceDistance, setFaceDistance] = useState("ok");
2877
2891
  useEffect(() => {
2878
2892
  if (state.status === "liveness") {
2879
2893
  const newStep = state.step;
@@ -2989,20 +3003,66 @@ function BiometricRegistrationModal({
2989
3003
  const loadModels = async () => {
2990
3004
  try {
2991
3005
  const MODEL_URL = "https://cdn.jsdelivr.net/npm/@vladmandic/face-api/model";
2992
- await Promise.all([
2993
- faceapi.nets.tinyFaceDetector.loadFromUri(MODEL_URL),
2994
- faceapi.nets.faceLandmark68Net.loadFromUri(MODEL_URL)
3006
+ const timeout = new Promise(
3007
+ (_, reject) => setTimeout(() => reject(new Error("Timeout ao carregar modelos")), 1e4)
3008
+ );
3009
+ await Promise.race([
3010
+ Promise.all([
3011
+ faceapi.nets.tinyFaceDetector.loadFromUri(MODEL_URL),
3012
+ faceapi.nets.faceLandmark68Net.loadFromUri(MODEL_URL)
3013
+ ]),
3014
+ timeout
2995
3015
  ]);
2996
3016
  } catch (error) {
2997
- dispatch({ type: "ERROR", message: "Erro ao carregar modelos de IA" });
3017
+ dispatch({
3018
+ type: "ERROR",
3019
+ message: "Não foi possível carregar os modelos de IA. Verifique sua conexão e tente novamente.",
3020
+ retryable: true
3021
+ });
2998
3022
  }
2999
3023
  };
3000
3024
  loadModels();
3001
3025
  }, []);
3026
+ useEffect(() => {
3027
+ return () => {
3028
+ if (streamRef.current) {
3029
+ streamRef.current.getTracks().forEach((track) => {
3030
+ track.stop();
3031
+ });
3032
+ streamRef.current = null;
3033
+ }
3034
+ if (videoRef.current) {
3035
+ videoRef.current.srcObject = null;
3036
+ }
3037
+ if (watchdogTimerRef.current) {
3038
+ clearTimeout(watchdogTimerRef.current);
3039
+ watchdogTimerRef.current = null;
3040
+ }
3041
+ if (progressIntervalRef.current) {
3042
+ clearInterval(progressIntervalRef.current);
3043
+ progressIntervalRef.current = null;
3044
+ }
3045
+ };
3046
+ }, []);
3002
3047
  useEffect(() => {
3003
3048
  let mounted = true;
3004
3049
  const initCamera = async () => {
3005
3050
  if (state.status !== "liveness" || streamRef.current) return;
3051
+ detectionStartedRef.current = false;
3052
+ if (watchdogTimerRef.current) {
3053
+ clearTimeout(watchdogTimerRef.current);
3054
+ watchdogTimerRef.current = null;
3055
+ }
3056
+ watchdogTimerRef.current = window.setTimeout(() => {
3057
+ if (!mounted) return;
3058
+ if (detectionStartedRef.current === false) {
3059
+ dispatch({
3060
+ type: "ERROR",
3061
+ message: "A câmera não respondeu a tempo. Verifique se ela está disponível e tente novamente.",
3062
+ retryable: true
3063
+ });
3064
+ }
3065
+ }, 2e4);
3006
3066
  try {
3007
3067
  const stream = await navigator.mediaDevices.getUserMedia({
3008
3068
  video: {
@@ -3032,13 +3092,30 @@ function BiometricRegistrationModal({
3032
3092
  await video.play();
3033
3093
  } catch (error) {
3034
3094
  if (!mounted) return;
3035
- const errorMessage = error instanceof Error ? error.message : "Erro ao acessar câmera";
3036
- dispatch({ type: "ERROR", message: errorMessage });
3095
+ if (watchdogTimerRef.current) {
3096
+ clearTimeout(watchdogTimerRef.current);
3097
+ watchdogTimerRef.current = null;
3098
+ }
3099
+ let errorMessage = error instanceof Error ? error.message : "Erro ao acessar câmera";
3100
+ if (error instanceof DOMException) {
3101
+ if (error.name === "NotAllowedError" || error.name === "PermissionDeniedError") {
3102
+ errorMessage = "Permissão de câmera negada. Permita o acesso à câmera nas configurações do navegador e tente novamente.";
3103
+ } else if (error.name === "NotFoundError" || error.name === "DevicesNotFoundError") {
3104
+ errorMessage = "Nenhuma câmera encontrada. Conecte uma câmera e tente novamente.";
3105
+ } else if (error.name === "NotReadableError") {
3106
+ errorMessage = "A câmera está sendo usada por outro programa. Feche-o e tente novamente.";
3107
+ }
3108
+ }
3109
+ dispatch({ type: "ERROR", message: errorMessage, retryable: true });
3037
3110
  }
3038
3111
  };
3039
3112
  initCamera();
3040
3113
  return () => {
3041
3114
  mounted = false;
3115
+ if (watchdogTimerRef.current) {
3116
+ clearTimeout(watchdogTimerRef.current);
3117
+ watchdogTimerRef.current = null;
3118
+ }
3042
3119
  if (streamRef.current) {
3043
3120
  streamRef.current.getTracks().forEach((track) => track.stop());
3044
3121
  streamRef.current = null;
@@ -3118,21 +3195,59 @@ function BiometricRegistrationModal({
3118
3195
  requestAnimationFrame(detectFace2);
3119
3196
  }
3120
3197
  };
3121
- const startTimer = setTimeout(() => {
3122
- var _a;
3123
- if (isRunning && ((_a = videoRef.current) == null ? void 0 : _a.readyState) === 4) {
3124
- detectFace2();
3125
- }
3126
- }, 1e3);
3127
- return () => {
3128
- isRunning = false;
3129
- clearTimeout(startTimer);
3130
- if (progressIntervalRef.current) {
3131
- clearInterval(progressIntervalRef.current);
3132
- progressIntervalRef.current = null;
3133
- }
3134
- holdStartTimeRef.current = null;
3198
+ const startDetection = () => {
3199
+ if (!isRunning || !videoRef.current) return;
3200
+ detectionStartedRef.current = true;
3201
+ if (watchdogTimerRef.current) {
3202
+ clearTimeout(watchdogTimerRef.current);
3203
+ watchdogTimerRef.current = null;
3204
+ }
3205
+ detectFace2();
3135
3206
  };
3207
+ const video = videoRef.current;
3208
+ if (video && video.readyState >= 2) {
3209
+ const startTimer = setTimeout(startDetection, 200);
3210
+ return () => {
3211
+ isRunning = false;
3212
+ clearTimeout(startTimer);
3213
+ if (progressIntervalRef.current) {
3214
+ clearInterval(progressIntervalRef.current);
3215
+ progressIntervalRef.current = null;
3216
+ }
3217
+ holdStartTimeRef.current = null;
3218
+ };
3219
+ } else {
3220
+ const onCanPlay = () => {
3221
+ if (isRunning) startDetection();
3222
+ };
3223
+ let pollCount = 0;
3224
+ const MAX_POLLS = 50;
3225
+ const pollTimer = window.setInterval(() => {
3226
+ pollCount++;
3227
+ if (!isRunning) {
3228
+ clearInterval(pollTimer);
3229
+ return;
3230
+ }
3231
+ if (videoRef.current && videoRef.current.readyState >= 2) {
3232
+ clearInterval(pollTimer);
3233
+ videoRef.current.removeEventListener("canplay", onCanPlay);
3234
+ startDetection();
3235
+ } else if (pollCount >= MAX_POLLS) {
3236
+ clearInterval(pollTimer);
3237
+ }
3238
+ }, 300);
3239
+ video == null ? void 0 : video.addEventListener("canplay", onCanPlay, { once: true });
3240
+ return () => {
3241
+ isRunning = false;
3242
+ clearInterval(pollTimer);
3243
+ video == null ? void 0 : video.removeEventListener("canplay", onCanPlay);
3244
+ if (progressIntervalRef.current) {
3245
+ clearInterval(progressIntervalRef.current);
3246
+ progressIntervalRef.current = null;
3247
+ }
3248
+ holdStartTimeRef.current = null;
3249
+ };
3250
+ }
3136
3251
  }, [state.status]);
3137
3252
  const capturePhoto = async () => {
3138
3253
  if (!videoRef.current || !canvasRef.current || isCapturingRef.current) {
@@ -3287,14 +3402,37 @@ function BiometricRegistrationModal({
3287
3402
  }
3288
3403
  )
3289
3404
  ] }) }),
3290
- /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { children: "Não foi possível concluir" }),
3405
+ /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { children: "Não foi possível processar" }),
3291
3406
  /* @__PURE__ */ jsxRuntimeExports.jsx("p", { children: state.message }),
3292
3407
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "button-group", children: [
3293
- /* @__PURE__ */ jsxRuntimeExports.jsx("button", { className: "secondary-button", onClick: () => dispatch({ type: "RESET" }), children: "Tentar Novamente" }),
3408
+ state.retryable && /* @__PURE__ */ jsxRuntimeExports.jsx("button", { className: "secondary-button", onClick: () => dispatch({ type: "RESET" }), children: "Tentar Novamente" }),
3294
3409
  /* @__PURE__ */ jsxRuntimeExports.jsx("button", { className: "ghost-button", onClick: onClose, children: "Cancelar" })
3295
3410
  ] })
3296
3411
  ] });
3297
3412
  };
3413
+ const renderCameraLoading = () => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "processing-container", children: [
3414
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "spinner-wrapper", children: [
3415
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "spinner" }),
3416
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "spinner-ring" })
3417
+ ] }),
3418
+ /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { children: "Inicializando câmera" }),
3419
+ /* @__PURE__ */ jsxRuntimeExports.jsx("p", { children: "Aguarde enquanto preparamos a câmera..." }),
3420
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(
3421
+ "div",
3422
+ {
3423
+ className: "branding",
3424
+ style: {
3425
+ marginTop: "40px",
3426
+ paddingTop: "24px",
3427
+ borderTop: "1px solid rgba(255, 255, 255, 0.1)"
3428
+ },
3429
+ children: [
3430
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "NeoFaceId by" }),
3431
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "brand-name", children: "OCTA" })
3432
+ ]
3433
+ }
3434
+ )
3435
+ ] });
3298
3436
  const renderSuccess = () => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "result-container success", children: [
3299
3437
  /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "result-icon success-icon", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("svg", { width: "60", height: "60", viewBox: "0 0 60 60", fill: "none", children: [
3300
3438
  /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "30", cy: "30", r: "28", stroke: "currentColor", strokeWidth: "3" }),
@@ -3876,6 +4014,7 @@ function BiometricRegistrationModal({
3876
4014
  /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "modal-overlay", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "modal-content", children: [
3877
4015
  /* @__PURE__ */ jsxRuntimeExports.jsx("button", { className: "close-button", onClick: onClose, children: "×" }),
3878
4016
  state.status === "liveness" && renderLivenessDetection(),
4017
+ state.status === "camera-loading" && renderCameraLoading(),
3879
4018
  state.status === "processing" && renderProcessing(),
3880
4019
  state.status === "error" && renderError(),
3881
4020
  state.status === "success" && renderSuccess()
@@ -5194,14 +5333,15 @@ async function captureFaceSilently() {
5194
5333
  video.style.width = "1px";
5195
5334
  video.style.height = "1px";
5196
5335
  video.style.opacity = "0";
5336
+ video.muted = true;
5337
+ video.playsInline = true;
5197
5338
  video.setAttribute("autoplay", "");
5198
- video.setAttribute("muted", "");
5199
5339
  video.setAttribute("playsinline", "");
5200
5340
  const canvas = document.createElement("canvas");
5201
5341
  canvas.style.display = "none";
5202
5342
  let stream = null;
5203
5343
  let faceDetectionAttempts = 0;
5204
- const MAX_FACE_DETECTION_ATTEMPTS = 10;
5344
+ const MAX_FACE_DETECTION_ATTEMPTS = 30;
5205
5345
  const cleanup = () => {
5206
5346
  if (stream) {
5207
5347
  stream.getTracks().forEach((track) => track.stop());
@@ -5215,22 +5355,27 @@ async function captureFaceSilently() {
5215
5355
  };
5216
5356
  document.body.appendChild(video);
5217
5357
  document.body.appendChild(canvas);
5218
- navigator.mediaDevices.getUserMedia({
5219
- video: {
5220
- width: { ideal: 640 },
5221
- height: { ideal: 480 },
5222
- facingMode: "user"
5223
- },
5224
- audio: false
5225
- }).then((mediaStream) => {
5358
+ const tryGetUserMedia = (withConstraints) => {
5359
+ if (withConstraints) {
5360
+ return navigator.mediaDevices.getUserMedia({
5361
+ video: {
5362
+ width: { ideal: 640 },
5363
+ height: { ideal: 480 },
5364
+ facingMode: "user"
5365
+ },
5366
+ audio: false
5367
+ });
5368
+ }
5369
+ return navigator.mediaDevices.getUserMedia({ video: { facingMode: "user" }, audio: false });
5370
+ };
5371
+ tryGetUserMedia(true).catch(() => tryGetUserMedia(false)).then((mediaStream) => {
5226
5372
  stream = mediaStream;
5227
5373
  video.srcObject = stream;
5228
- video.play();
5229
5374
  const tryCapture = () => {
5230
5375
  if (!video.videoWidth || !video.videoHeight) {
5231
5376
  if (faceDetectionAttempts < MAX_FACE_DETECTION_ATTEMPTS) {
5232
5377
  faceDetectionAttempts++;
5233
- setTimeout(tryCapture, 100);
5378
+ setTimeout(tryCapture, 150);
5234
5379
  return;
5235
5380
  }
5236
5381
  cleanup();
@@ -5260,7 +5405,7 @@ async function captureFaceSilently() {
5260
5405
  detectFace2().then((hasFace) => {
5261
5406
  if (!hasFace && faceDetectionAttempts < MAX_FACE_DETECTION_ATTEMPTS) {
5262
5407
  faceDetectionAttempts++;
5263
- setTimeout(tryCapture, 100);
5408
+ setTimeout(tryCapture, 150);
5264
5409
  return;
5265
5410
  }
5266
5411
  ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
@@ -5278,9 +5423,26 @@ async function captureFaceSilently() {
5278
5423
  );
5279
5424
  });
5280
5425
  };
5281
- video.onloadedmetadata = () => {
5282
- setTimeout(tryCapture, CAMERA_READY_DELAY);
5283
- };
5426
+ const playPromise = video.play();
5427
+ if (playPromise !== void 0) {
5428
+ playPromise.then(() => {
5429
+ if (video.videoWidth && video.videoHeight) {
5430
+ setTimeout(tryCapture, CAMERA_READY_DELAY);
5431
+ } else {
5432
+ video.onloadedmetadata = () => setTimeout(tryCapture, CAMERA_READY_DELAY);
5433
+ setTimeout(() => {
5434
+ if (video.videoWidth && video.videoHeight) {
5435
+ tryCapture();
5436
+ }
5437
+ }, 1500);
5438
+ }
5439
+ }).catch((err) => {
5440
+ cleanup();
5441
+ reject(new NeoFaceError("Erro ao reproduzir câmera: " + err.message, ErrorType.CAMERA_ERROR));
5442
+ });
5443
+ } else {
5444
+ video.onloadedmetadata = () => setTimeout(tryCapture, CAMERA_READY_DELAY);
5445
+ }
5284
5446
  video.onerror = () => {
5285
5447
  cleanup();
5286
5448
  reject(new NeoFaceError("Erro ao acessar câmera", ErrorType.CAMERA_ERROR));
@@ -5354,27 +5516,55 @@ async function attemptLogin(applicationToken, overlay, fastMode = false, isRetry
5354
5516
  video.style.width = "1px";
5355
5517
  video.style.height = "1px";
5356
5518
  video.style.opacity = "0";
5519
+ video.muted = true;
5520
+ video.playsInline = true;
5357
5521
  video.setAttribute("autoplay", "");
5358
- video.setAttribute("muted", "");
5359
5522
  video.setAttribute("playsinline", "");
5360
5523
  document.body.appendChild(video);
5361
5524
  let stream = null;
5362
5525
  try {
5363
- stream = await navigator.mediaDevices.getUserMedia({
5364
- video: {
5365
- width: { ideal: 640 },
5366
- height: { ideal: 480 },
5367
- facingMode: "user"
5368
- },
5369
- audio: false
5370
- });
5526
+ try {
5527
+ stream = await navigator.mediaDevices.getUserMedia({
5528
+ video: {
5529
+ width: { ideal: 640 },
5530
+ height: { ideal: 480 },
5531
+ facingMode: "user"
5532
+ },
5533
+ audio: false
5534
+ });
5535
+ } catch {
5536
+ stream = await navigator.mediaDevices.getUserMedia({
5537
+ video: { facingMode: "user" },
5538
+ audio: false
5539
+ });
5540
+ }
5371
5541
  video.srcObject = stream;
5542
+ await new Promise((resolve) => {
5543
+ if (video.readyState >= 1) {
5544
+ resolve();
5545
+ } else {
5546
+ const handler = () => {
5547
+ video.removeEventListener("loadedmetadata", handler);
5548
+ resolve();
5549
+ };
5550
+ video.addEventListener("loadedmetadata", handler);
5551
+ setTimeout(resolve, 2e3);
5552
+ }
5553
+ });
5372
5554
  await video.play();
5373
5555
  await new Promise((resolve) => {
5374
- if (video.readyState >= 2) {
5375
- resolve(void 0);
5556
+ if (video.videoWidth && video.videoHeight) {
5557
+ resolve();
5376
5558
  } else {
5377
- video.onloadedmetadata = () => resolve(void 0);
5559
+ const checkSize = () => {
5560
+ if (video.videoWidth && video.videoHeight) {
5561
+ resolve();
5562
+ } else {
5563
+ setTimeout(checkSize, 100);
5564
+ }
5565
+ };
5566
+ setTimeout(resolve, 3e3);
5567
+ checkSize();
5378
5568
  }
5379
5569
  });
5380
5570
  overlay.updateStatus("detecting");
@@ -5844,8 +6034,8 @@ const biometricDetection = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.
5844
6034
  initializeBiometricDetection,
5845
6035
  isAdvancedDetectionAvailable
5846
6036
  }, Symbol.toStringTag, { value: "Module" }));
5847
- const VERSION = "1.11.1";
5848
- const RELEASE_DATE = "2026-02-12";
6037
+ const VERSION = "1.13.2";
6038
+ const RELEASE_DATE = "2026-02-21";
5849
6039
  class OnboardingCaptureModal {
5850
6040
  constructor(options) {
5851
6041
  __publicField(this, "overlay", null);
@@ -5896,7 +6086,7 @@ class OnboardingCaptureModal {
5896
6086
  <div class="neofaceid-modal-body">
5897
6087
  <p class="neofaceid-subtitle">${this.subtitle ?? "Vamos capturar sua face e documento"}</p>
5898
6088
  <div class="neofaceid-camera-container">
5899
- <video class="neofaceid-video" autoplay playsinline></video>
6089
+ <video class="neofaceid-video" autoplay muted playsinline></video>
5900
6090
  <canvas class="neofaceid-canvas"></canvas>
5901
6091
  <div class="neofaceid-overlay">
5902
6092
  <div class="neofaceid-frame"></div>
@@ -5947,12 +6137,20 @@ class OnboardingCaptureModal {
5947
6137
  * Inicia a câmera do usuário com resolução adequada.
5948
6138
  */
5949
6139
  async startCamera() {
5950
- const constraints = {
5951
- video: { facingMode: "user", width: { ideal: 1280 }, height: { ideal: 720 } },
5952
- audio: false
5953
- };
5954
- this.stream = await navigator.mediaDevices.getUserMedia(constraints);
5955
6140
  if (!this.video) throw new Error("Elemento de vídeo não encontrado");
6141
+ this.video.muted = true;
6142
+ this.video.playsInline = true;
6143
+ try {
6144
+ this.stream = await navigator.mediaDevices.getUserMedia({
6145
+ video: { facingMode: "user", width: { ideal: 1280 }, height: { ideal: 720 } },
6146
+ audio: false
6147
+ });
6148
+ } catch {
6149
+ this.stream = await navigator.mediaDevices.getUserMedia({
6150
+ video: { facingMode: "user" },
6151
+ audio: false
6152
+ });
6153
+ }
5956
6154
  this.video.srcObject = this.stream;
5957
6155
  await this.video.play();
5958
6156
  }
@@ -6071,11 +6269,9 @@ class NeoFaceID {
6071
6269
  */
6072
6270
  constructor(config) {
6073
6271
  __publicField(this, "appToken");
6074
- __publicField(this, "baseUrl");
6075
6272
  __publicField(this, "signature");
6076
6273
  __publicField(this, "sessionData");
6077
6274
  this.appToken = config.appToken;
6078
- this.baseUrl = config.baseUrl || getBaseUrl();
6079
6275
  this.signature = config.signature;
6080
6276
  this.sessionData = config.sessionData;
6081
6277
  if (this.signature && !this.validateSignatureFormat(this.signature)) {
@@ -6164,7 +6360,7 @@ class NeoFaceID {
6164
6360
  * ```
6165
6361
  */
6166
6362
  async loginRecognition(options) {
6167
- const { biometricData, typeOfIdentification, purpose, confidenceThreshold = 0.8 } = options;
6363
+ const { biometricData, purpose, confidenceThreshold = 0.8 } = options;
6168
6364
  const imageBlobs = Array.isArray(biometricData) ? biometricData : [biometricData];
6169
6365
  if (imageBlobs.length === 0) {
6170
6366
  throw new NeoFaceError(
@@ -6634,7 +6830,7 @@ class DocumentCaptureModal {
6634
6830
 
6635
6831
  <div class="neoface-doc-body">
6636
6832
  <div class="neoface-doc-camera-container">
6637
- <video class="neoface-doc-video" autoplay playsinline></video>
6833
+ <video class="neoface-doc-video" autoplay muted playsinline></video>
6638
6834
  <canvas class="neoface-doc-canvas"></canvas>
6639
6835
 
6640
6836
  <div class="neoface-doc-guide">
@@ -6886,6 +7082,8 @@ class DocumentCaptureModal {
6886
7082
  this.video = (_a = this.overlay) == null ? void 0 : _a.querySelector(".neoface-doc-video");
6887
7083
  this.canvas = (_b = this.overlay) == null ? void 0 : _b.querySelector(".neoface-doc-canvas");
6888
7084
  if (this.video) {
7085
+ this.video.muted = true;
7086
+ this.video.playsInline = true;
6889
7087
  this.video.srcObject = this.stream;
6890
7088
  await this.video.play();
6891
7089
  }
@@ -7507,7 +7705,12 @@ function startBiometricRegistration(personData, applicationToken, callbacks, opt
7507
7705
  const container = document.createElement("div");
7508
7706
  container.id = "neoface-biometric-registration-container";
7509
7707
  document.body.appendChild(container);
7708
+ let rootRef = null;
7510
7709
  const cleanup = () => {
7710
+ if (rootRef) {
7711
+ rootRef.unmount();
7712
+ rootRef = null;
7713
+ }
7511
7714
  if (document.body.contains(container)) {
7512
7715
  document.body.removeChild(container);
7513
7716
  }
@@ -7517,7 +7720,6 @@ function startBiometricRegistration(personData, applicationToken, callbacks, opt
7517
7720
  personData,
7518
7721
  applicationToken,
7519
7722
  useRealApi: (options == null ? void 0 : options.useRealApi) ?? true,
7520
- // Sempre usa API real em produção
7521
7723
  onClose: cleanup,
7522
7724
  onSuccess: (result) => {
7523
7725
  cleanup();
@@ -7528,8 +7730,8 @@ function startBiometricRegistration(personData, applicationToken, callbacks, opt
7528
7730
  callbacks.onError("BIOMETRIC_REGISTRATION_ERROR", error);
7529
7731
  }
7530
7732
  });
7531
- const root = clientExports.createRoot(container);
7532
- root.render(modalElement);
7733
+ rootRef = clientExports.createRoot(container);
7734
+ rootRef.render(modalElement);
7533
7735
  }).catch((error) => {
7534
7736
  cleanup();
7535
7737
  callbacks.onError("TOKEN_VALIDATION_ERROR", error.message || "Failed to validate token");
@@ -7539,16 +7741,29 @@ function startLivenessCapture(applicationToken, callbacks) {
7539
7741
  const container = document.createElement("div");
7540
7742
  container.id = "neoface-liveness-capture-container";
7541
7743
  document.body.appendChild(container);
7744
+ let completed = false;
7745
+ let rootRef = null;
7542
7746
  const cleanup = () => {
7747
+ if (rootRef) {
7748
+ rootRef.unmount();
7749
+ rootRef = null;
7750
+ }
7543
7751
  if (document.body.contains(container)) {
7544
7752
  document.body.removeChild(container);
7545
7753
  }
7546
7754
  };
7755
+ const handleClose = () => {
7756
+ cleanup();
7757
+ if (!completed && callbacks.onCancel) {
7758
+ callbacks.onCancel();
7759
+ }
7760
+ };
7547
7761
  validateToken(applicationToken).then(() => {
7548
7762
  const modalElement = React.createElement(BiometricRegistrationModal, {
7549
7763
  applicationToken,
7550
- onClose: cleanup,
7764
+ onClose: handleClose,
7551
7765
  onPhotosCaptured: (photos) => {
7766
+ completed = true;
7552
7767
  callbacks.onSuccess(photos);
7553
7768
  },
7554
7769
  onError: (error) => {
@@ -7556,8 +7771,8 @@ function startLivenessCapture(applicationToken, callbacks) {
7556
7771
  callbacks.onError("LIVENESS_CAPTURE_ERROR", error);
7557
7772
  }
7558
7773
  });
7559
- const root = clientExports.createRoot(container);
7560
- root.render(modalElement);
7774
+ rootRef = clientExports.createRoot(container);
7775
+ rootRef.render(modalElement);
7561
7776
  }).catch((error) => {
7562
7777
  cleanup();
7563
7778
  callbacks.onError("TOKEN_VALIDATION_ERROR", error.message || "Failed to validate token");