@neofaceid/web-sdk 1.11.1 → 1.13.0

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.
@@ -2808,12 +2808,14 @@ const modalReducer = (state, action) => {
2808
2808
  };
2809
2809
  }
2810
2810
  return state;
2811
+ case "CAMERA_LOADING":
2812
+ return { status: "camera-loading" };
2811
2813
  case "START_PROCESSING":
2812
2814
  return { status: "processing" };
2813
2815
  case "SUCCESS":
2814
2816
  return { status: "success" };
2815
2817
  case "ERROR":
2816
- return { status: "error", message: action.message };
2818
+ return { status: "error", message: action.message, retryable: action.retryable ?? true };
2817
2819
  case "RESET":
2818
2820
  return initialState;
2819
2821
  default:
@@ -2859,9 +2861,7 @@ function BiometricRegistrationModal({
2859
2861
  onClose,
2860
2862
  onSuccess,
2861
2863
  onPhotosCaptured,
2862
- onError,
2863
- useRealApi = true
2864
- // Sempre usa API real em produção
2864
+ onError
2865
2865
  }) {
2866
2866
  const videoRef = useRef(null);
2867
2867
  const canvasRef = useRef(null);
@@ -2870,10 +2870,12 @@ function BiometricRegistrationModal({
2870
2870
  const progressIntervalRef = useRef(null);
2871
2871
  const isCapturingRef = useRef(false);
2872
2872
  const currentStepRef = useRef("center");
2873
+ const watchdogTimerRef = useRef(null);
2874
+ const detectionStartedRef = useRef(false);
2873
2875
  const captureStateRef = useRef(null);
2874
2876
  const [state, dispatch] = useReducer(modalReducer, initialState);
2875
2877
  const [faceDetected, setFaceDetected] = useState(false);
2876
- const [faceDistance, setFaceDistance] = useState("ok");
2878
+ const [_faceDistance, setFaceDistance] = useState("ok");
2877
2879
  useEffect(() => {
2878
2880
  if (state.status === "liveness") {
2879
2881
  const newStep = state.step;
@@ -2989,20 +2991,66 @@ function BiometricRegistrationModal({
2989
2991
  const loadModels = async () => {
2990
2992
  try {
2991
2993
  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)
2994
+ const timeout = new Promise(
2995
+ (_, reject) => setTimeout(() => reject(new Error("Timeout ao carregar modelos")), 1e4)
2996
+ );
2997
+ await Promise.race([
2998
+ Promise.all([
2999
+ faceapi.nets.tinyFaceDetector.loadFromUri(MODEL_URL),
3000
+ faceapi.nets.faceLandmark68Net.loadFromUri(MODEL_URL)
3001
+ ]),
3002
+ timeout
2995
3003
  ]);
2996
3004
  } catch (error) {
2997
- dispatch({ type: "ERROR", message: "Erro ao carregar modelos de IA" });
3005
+ dispatch({
3006
+ type: "ERROR",
3007
+ message: "Não foi possível carregar os modelos de IA. Verifique sua conexão e tente novamente.",
3008
+ retryable: true
3009
+ });
2998
3010
  }
2999
3011
  };
3000
3012
  loadModels();
3001
3013
  }, []);
3014
+ useEffect(() => {
3015
+ return () => {
3016
+ if (streamRef.current) {
3017
+ streamRef.current.getTracks().forEach((track) => {
3018
+ track.stop();
3019
+ });
3020
+ streamRef.current = null;
3021
+ }
3022
+ if (videoRef.current) {
3023
+ videoRef.current.srcObject = null;
3024
+ }
3025
+ if (watchdogTimerRef.current) {
3026
+ clearTimeout(watchdogTimerRef.current);
3027
+ watchdogTimerRef.current = null;
3028
+ }
3029
+ if (progressIntervalRef.current) {
3030
+ clearInterval(progressIntervalRef.current);
3031
+ progressIntervalRef.current = null;
3032
+ }
3033
+ };
3034
+ }, []);
3002
3035
  useEffect(() => {
3003
3036
  let mounted = true;
3004
3037
  const initCamera = async () => {
3005
3038
  if (state.status !== "liveness" || streamRef.current) return;
3039
+ detectionStartedRef.current = false;
3040
+ if (watchdogTimerRef.current) {
3041
+ clearTimeout(watchdogTimerRef.current);
3042
+ watchdogTimerRef.current = null;
3043
+ }
3044
+ watchdogTimerRef.current = window.setTimeout(() => {
3045
+ if (!mounted) return;
3046
+ if (detectionStartedRef.current === false) {
3047
+ dispatch({
3048
+ type: "ERROR",
3049
+ message: "A câmera não respondeu a tempo. Verifique se ela está disponível e tente novamente.",
3050
+ retryable: true
3051
+ });
3052
+ }
3053
+ }, 2e4);
3006
3054
  try {
3007
3055
  const stream = await navigator.mediaDevices.getUserMedia({
3008
3056
  video: {
@@ -3032,13 +3080,30 @@ function BiometricRegistrationModal({
3032
3080
  await video.play();
3033
3081
  } catch (error) {
3034
3082
  if (!mounted) return;
3035
- const errorMessage = error instanceof Error ? error.message : "Erro ao acessar câmera";
3036
- dispatch({ type: "ERROR", message: errorMessage });
3083
+ if (watchdogTimerRef.current) {
3084
+ clearTimeout(watchdogTimerRef.current);
3085
+ watchdogTimerRef.current = null;
3086
+ }
3087
+ let errorMessage = error instanceof Error ? error.message : "Erro ao acessar câmera";
3088
+ if (error instanceof DOMException) {
3089
+ if (error.name === "NotAllowedError" || error.name === "PermissionDeniedError") {
3090
+ errorMessage = "Permissão de câmera negada. Permita o acesso à câmera nas configurações do navegador e tente novamente.";
3091
+ } else if (error.name === "NotFoundError" || error.name === "DevicesNotFoundError") {
3092
+ errorMessage = "Nenhuma câmera encontrada. Conecte uma câmera e tente novamente.";
3093
+ } else if (error.name === "NotReadableError") {
3094
+ errorMessage = "A câmera está sendo usada por outro programa. Feche-o e tente novamente.";
3095
+ }
3096
+ }
3097
+ dispatch({ type: "ERROR", message: errorMessage, retryable: true });
3037
3098
  }
3038
3099
  };
3039
3100
  initCamera();
3040
3101
  return () => {
3041
3102
  mounted = false;
3103
+ if (watchdogTimerRef.current) {
3104
+ clearTimeout(watchdogTimerRef.current);
3105
+ watchdogTimerRef.current = null;
3106
+ }
3042
3107
  if (streamRef.current) {
3043
3108
  streamRef.current.getTracks().forEach((track) => track.stop());
3044
3109
  streamRef.current = null;
@@ -3118,21 +3183,59 @@ function BiometricRegistrationModal({
3118
3183
  requestAnimationFrame(detectFace2);
3119
3184
  }
3120
3185
  };
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;
3186
+ const startDetection = () => {
3187
+ if (!isRunning || !videoRef.current) return;
3188
+ detectionStartedRef.current = true;
3189
+ if (watchdogTimerRef.current) {
3190
+ clearTimeout(watchdogTimerRef.current);
3191
+ watchdogTimerRef.current = null;
3192
+ }
3193
+ detectFace2();
3135
3194
  };
3195
+ const video = videoRef.current;
3196
+ if (video && video.readyState >= 2) {
3197
+ const startTimer = setTimeout(startDetection, 200);
3198
+ return () => {
3199
+ isRunning = false;
3200
+ clearTimeout(startTimer);
3201
+ if (progressIntervalRef.current) {
3202
+ clearInterval(progressIntervalRef.current);
3203
+ progressIntervalRef.current = null;
3204
+ }
3205
+ holdStartTimeRef.current = null;
3206
+ };
3207
+ } else {
3208
+ const onCanPlay = () => {
3209
+ if (isRunning) startDetection();
3210
+ };
3211
+ let pollCount = 0;
3212
+ const MAX_POLLS = 50;
3213
+ const pollTimer = window.setInterval(() => {
3214
+ pollCount++;
3215
+ if (!isRunning) {
3216
+ clearInterval(pollTimer);
3217
+ return;
3218
+ }
3219
+ if (videoRef.current && videoRef.current.readyState >= 2) {
3220
+ clearInterval(pollTimer);
3221
+ videoRef.current.removeEventListener("canplay", onCanPlay);
3222
+ startDetection();
3223
+ } else if (pollCount >= MAX_POLLS) {
3224
+ clearInterval(pollTimer);
3225
+ }
3226
+ }, 300);
3227
+ video == null ? void 0 : video.addEventListener("canplay", onCanPlay, { once: true });
3228
+ return () => {
3229
+ isRunning = false;
3230
+ clearInterval(pollTimer);
3231
+ video == null ? void 0 : video.removeEventListener("canplay", onCanPlay);
3232
+ if (progressIntervalRef.current) {
3233
+ clearInterval(progressIntervalRef.current);
3234
+ progressIntervalRef.current = null;
3235
+ }
3236
+ holdStartTimeRef.current = null;
3237
+ };
3238
+ }
3136
3239
  }, [state.status]);
3137
3240
  const capturePhoto = async () => {
3138
3241
  if (!videoRef.current || !canvasRef.current || isCapturingRef.current) {
@@ -3287,14 +3390,37 @@ function BiometricRegistrationModal({
3287
3390
  }
3288
3391
  )
3289
3392
  ] }) }),
3290
- /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { children: "Não foi possível concluir" }),
3393
+ /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { children: "Não foi possível processar" }),
3291
3394
  /* @__PURE__ */ jsxRuntimeExports.jsx("p", { children: state.message }),
3292
3395
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "button-group", children: [
3293
- /* @__PURE__ */ jsxRuntimeExports.jsx("button", { className: "secondary-button", onClick: () => dispatch({ type: "RESET" }), children: "Tentar Novamente" }),
3396
+ state.retryable && /* @__PURE__ */ jsxRuntimeExports.jsx("button", { className: "secondary-button", onClick: () => dispatch({ type: "RESET" }), children: "Tentar Novamente" }),
3294
3397
  /* @__PURE__ */ jsxRuntimeExports.jsx("button", { className: "ghost-button", onClick: onClose, children: "Cancelar" })
3295
3398
  ] })
3296
3399
  ] });
3297
3400
  };
3401
+ const renderCameraLoading = () => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "processing-container", children: [
3402
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "spinner-wrapper", children: [
3403
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "spinner" }),
3404
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "spinner-ring" })
3405
+ ] }),
3406
+ /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { children: "Inicializando câmera" }),
3407
+ /* @__PURE__ */ jsxRuntimeExports.jsx("p", { children: "Aguarde enquanto preparamos a câmera..." }),
3408
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(
3409
+ "div",
3410
+ {
3411
+ className: "branding",
3412
+ style: {
3413
+ marginTop: "40px",
3414
+ paddingTop: "24px",
3415
+ borderTop: "1px solid rgba(255, 255, 255, 0.1)"
3416
+ },
3417
+ children: [
3418
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "NeoFaceId by" }),
3419
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "brand-name", children: "OCTA" })
3420
+ ]
3421
+ }
3422
+ )
3423
+ ] });
3298
3424
  const renderSuccess = () => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "result-container success", children: [
3299
3425
  /* @__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
3426
  /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "30", cy: "30", r: "28", stroke: "currentColor", strokeWidth: "3" }),
@@ -3876,6 +4002,7 @@ function BiometricRegistrationModal({
3876
4002
  /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "modal-overlay", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "modal-content", children: [
3877
4003
  /* @__PURE__ */ jsxRuntimeExports.jsx("button", { className: "close-button", onClick: onClose, children: "×" }),
3878
4004
  state.status === "liveness" && renderLivenessDetection(),
4005
+ state.status === "camera-loading" && renderCameraLoading(),
3879
4006
  state.status === "processing" && renderProcessing(),
3880
4007
  state.status === "error" && renderError(),
3881
4008
  state.status === "success" && renderSuccess()
@@ -5844,8 +5971,8 @@ const biometricDetection = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.
5844
5971
  initializeBiometricDetection,
5845
5972
  isAdvancedDetectionAvailable
5846
5973
  }, Symbol.toStringTag, { value: "Module" }));
5847
- const VERSION = "1.11.1";
5848
- const RELEASE_DATE = "2026-02-12";
5974
+ const VERSION = "1.13.0";
5975
+ const RELEASE_DATE = "2026-02-20";
5849
5976
  class OnboardingCaptureModal {
5850
5977
  constructor(options) {
5851
5978
  __publicField(this, "overlay", null);
@@ -6071,11 +6198,9 @@ class NeoFaceID {
6071
6198
  */
6072
6199
  constructor(config) {
6073
6200
  __publicField(this, "appToken");
6074
- __publicField(this, "baseUrl");
6075
6201
  __publicField(this, "signature");
6076
6202
  __publicField(this, "sessionData");
6077
6203
  this.appToken = config.appToken;
6078
- this.baseUrl = config.baseUrl || getBaseUrl();
6079
6204
  this.signature = config.signature;
6080
6205
  this.sessionData = config.sessionData;
6081
6206
  if (this.signature && !this.validateSignatureFormat(this.signature)) {
@@ -6164,7 +6289,7 @@ class NeoFaceID {
6164
6289
  * ```
6165
6290
  */
6166
6291
  async loginRecognition(options) {
6167
- const { biometricData, typeOfIdentification, purpose, confidenceThreshold = 0.8 } = options;
6292
+ const { biometricData, purpose, confidenceThreshold = 0.8 } = options;
6168
6293
  const imageBlobs = Array.isArray(biometricData) ? biometricData : [biometricData];
6169
6294
  if (imageBlobs.length === 0) {
6170
6295
  throw new NeoFaceError(
@@ -7507,7 +7632,12 @@ function startBiometricRegistration(personData, applicationToken, callbacks, opt
7507
7632
  const container = document.createElement("div");
7508
7633
  container.id = "neoface-biometric-registration-container";
7509
7634
  document.body.appendChild(container);
7635
+ let rootRef = null;
7510
7636
  const cleanup = () => {
7637
+ if (rootRef) {
7638
+ rootRef.unmount();
7639
+ rootRef = null;
7640
+ }
7511
7641
  if (document.body.contains(container)) {
7512
7642
  document.body.removeChild(container);
7513
7643
  }
@@ -7517,7 +7647,6 @@ function startBiometricRegistration(personData, applicationToken, callbacks, opt
7517
7647
  personData,
7518
7648
  applicationToken,
7519
7649
  useRealApi: (options == null ? void 0 : options.useRealApi) ?? true,
7520
- // Sempre usa API real em produção
7521
7650
  onClose: cleanup,
7522
7651
  onSuccess: (result) => {
7523
7652
  cleanup();
@@ -7528,8 +7657,8 @@ function startBiometricRegistration(personData, applicationToken, callbacks, opt
7528
7657
  callbacks.onError("BIOMETRIC_REGISTRATION_ERROR", error);
7529
7658
  }
7530
7659
  });
7531
- const root = clientExports.createRoot(container);
7532
- root.render(modalElement);
7660
+ rootRef = clientExports.createRoot(container);
7661
+ rootRef.render(modalElement);
7533
7662
  }).catch((error) => {
7534
7663
  cleanup();
7535
7664
  callbacks.onError("TOKEN_VALIDATION_ERROR", error.message || "Failed to validate token");
@@ -7539,16 +7668,29 @@ function startLivenessCapture(applicationToken, callbacks) {
7539
7668
  const container = document.createElement("div");
7540
7669
  container.id = "neoface-liveness-capture-container";
7541
7670
  document.body.appendChild(container);
7671
+ let completed = false;
7672
+ let rootRef = null;
7542
7673
  const cleanup = () => {
7674
+ if (rootRef) {
7675
+ rootRef.unmount();
7676
+ rootRef = null;
7677
+ }
7543
7678
  if (document.body.contains(container)) {
7544
7679
  document.body.removeChild(container);
7545
7680
  }
7546
7681
  };
7682
+ const handleClose = () => {
7683
+ cleanup();
7684
+ if (!completed && callbacks.onCancel) {
7685
+ callbacks.onCancel();
7686
+ }
7687
+ };
7547
7688
  validateToken(applicationToken).then(() => {
7548
7689
  const modalElement = React.createElement(BiometricRegistrationModal, {
7549
7690
  applicationToken,
7550
- onClose: cleanup,
7691
+ onClose: handleClose,
7551
7692
  onPhotosCaptured: (photos) => {
7693
+ completed = true;
7552
7694
  callbacks.onSuccess(photos);
7553
7695
  },
7554
7696
  onError: (error) => {
@@ -7556,8 +7698,8 @@ function startLivenessCapture(applicationToken, callbacks) {
7556
7698
  callbacks.onError("LIVENESS_CAPTURE_ERROR", error);
7557
7699
  }
7558
7700
  });
7559
- const root = clientExports.createRoot(container);
7560
- root.render(modalElement);
7701
+ rootRef = clientExports.createRoot(container);
7702
+ rootRef.render(modalElement);
7561
7703
  }).catch((error) => {
7562
7704
  cleanup();
7563
7705
  callbacks.onError("TOKEN_VALIDATION_ERROR", error.message || "Failed to validate token");