@neofaceid/web-sdk 1.1.1 → 1.2.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.
@@ -1108,6 +1108,7 @@ const useCamera = () => {
1108
1108
  };
1109
1109
  var ErrorType = /* @__PURE__ */ ((ErrorType2) => {
1110
1110
  ErrorType2["NETWORK"] = "NetworkError";
1111
+ ErrorType2["NETWORK_ERROR"] = "NetworkError";
1111
1112
  ErrorType2["INVALID_TOKEN"] = "InvalidTokenError";
1112
1113
  ErrorType2["RECOGNITION_FAILED"] = "RecognitionFailedError";
1113
1114
  ErrorType2["LOGIN_FAILED"] = "LoginFailedError";
@@ -1115,12 +1116,18 @@ var ErrorType = /* @__PURE__ */ ((ErrorType2) => {
1115
1116
  ErrorType2["API_ERROR"] = "ApiError";
1116
1117
  ErrorType2["PERSON_NOT_FOUND"] = "PersonNotFoundError";
1117
1118
  ErrorType2["INITIALIZATION_ERROR"] = "InitializationError";
1118
- ErrorType2["NETWORK_ERROR"] = "NetworkError";
1119
1119
  ErrorType2["CAMERA_ERROR"] = "CameraError";
1120
1120
  ErrorType2["CAPTURE_ERROR"] = "CaptureError";
1121
+ ErrorType2["NOT_FOUND"] = "NotFoundError";
1122
+ ErrorType2["UNKNOWN"] = "UnknownError";
1121
1123
  return ErrorType2;
1122
1124
  })(ErrorType || {});
1123
1125
  class NeoFaceError extends Error {
1126
+ /**
1127
+ * Constrói um erro do SDK com mensagem e tipo categórico.
1128
+ * @param message Mensagem descritiva do erro
1129
+ * @param type Tipo categórico do erro
1130
+ */
1124
1131
  constructor(message, type) {
1125
1132
  super(message);
1126
1133
  __publicField(this, "type");
@@ -1197,6 +1204,92 @@ const validateToken = async (applicationToken) => {
1197
1204
  throw new NeoFaceError("Unknown error occurred", ErrorType.NETWORK_ERROR);
1198
1205
  }
1199
1206
  };
1207
+ const getOnboardingDetails = async (applicationToken, onboardingToken) => {
1208
+ ensureSecureContext();
1209
+ const controller = createTimeoutController();
1210
+ try {
1211
+ const response = await fetch(`${API_BASE_URL}/api/v1/onboardings/${encodeURIComponent(onboardingToken)}/`, {
1212
+ method: "GET",
1213
+ headers: {
1214
+ "X-App-Token": applicationToken
1215
+ },
1216
+ signal: controller.signal
1217
+ });
1218
+ if (!response.ok) {
1219
+ if (response.status === 401 || response.status === 403) {
1220
+ throw new NeoFaceError("Token de aplicação inválido ou expirado", ErrorType.INVALID_TOKEN);
1221
+ }
1222
+ if (response.status === 404) {
1223
+ throw new NeoFaceError("Link de onboarding não encontrado", ErrorType.NOT_FOUND);
1224
+ }
1225
+ throw new NeoFaceError(`Falha ao obter detalhes (${response.status})`, ErrorType.NETWORK);
1226
+ }
1227
+ const data = await response.json();
1228
+ return data;
1229
+ } catch (error) {
1230
+ if (error instanceof Error) {
1231
+ if (error.name === "AbortError") {
1232
+ throw new NeoFaceError("Tempo de requisição excedido", ErrorType.NETWORK);
1233
+ }
1234
+ if (error instanceof NeoFaceError) {
1235
+ throw error;
1236
+ }
1237
+ throw new NeoFaceError(error.message, ErrorType.NETWORK);
1238
+ }
1239
+ throw new NeoFaceError("Erro desconhecido", ErrorType.NETWORK_ERROR);
1240
+ }
1241
+ };
1242
+ const completeOnboarding = async (applicationToken, onboardingToken, faceImage, documentImage) => {
1243
+ ensureSecureContext();
1244
+ const controller = createTimeoutController();
1245
+ const compressedFace = await compressImage(faceImage);
1246
+ const compressedDocument = await compressImage(documentImage);
1247
+ try {
1248
+ const formData = new FormData();
1249
+ formData.append("face_image", compressedFace, "face.jpg");
1250
+ formData.append("document_image", compressedDocument, "document.jpg");
1251
+ const response = await fetch(
1252
+ `${API_BASE_URL}/api/v1/onboardings/${encodeURIComponent(onboardingToken)}/complete`,
1253
+ {
1254
+ method: "POST",
1255
+ headers: {
1256
+ "X-App-Token": applicationToken
1257
+ },
1258
+ body: formData,
1259
+ signal: controller.signal
1260
+ }
1261
+ );
1262
+ const data = await response.json();
1263
+ if (!response.ok) {
1264
+ if (response.status === 401 || response.status === 403) {
1265
+ throw new NeoFaceError("Token de aplicação inválido ou expirado", ErrorType.INVALID_TOKEN);
1266
+ }
1267
+ if (response.status === 404) {
1268
+ throw new NeoFaceError("Link de onboarding não encontrado", ErrorType.NOT_FOUND);
1269
+ }
1270
+ throw new NeoFaceError((data == null ? void 0 : data.error) || (data == null ? void 0 : data.message) || "Falha ao concluir onboarding", ErrorType.NETWORK);
1271
+ }
1272
+ return {
1273
+ success: !!data.success,
1274
+ message: data.message || "Onboarding concluído",
1275
+ person_id: data.person_id,
1276
+ identity_data_id: data.identity_data_id,
1277
+ confidence_score: data.confidence_score,
1278
+ processing_time: data.processing_time
1279
+ };
1280
+ } catch (error) {
1281
+ if (error instanceof Error) {
1282
+ if (error.name === "AbortError") {
1283
+ throw new NeoFaceError("Tempo de requisição excedido", ErrorType.NETWORK);
1284
+ }
1285
+ if (error instanceof NeoFaceError) {
1286
+ throw error;
1287
+ }
1288
+ throw new NeoFaceError(error.message, ErrorType.NETWORK);
1289
+ }
1290
+ throw new NeoFaceError("Erro desconhecido", ErrorType.NETWORK_ERROR);
1291
+ }
1292
+ };
1200
1293
  const recognize = async (image, applicationToken) => {
1201
1294
  ensureSecureContext();
1202
1295
  const controller = createTimeoutController();
@@ -1485,10 +1578,10 @@ const registerPersonWithoutFace = async (personData, applicationToken) => {
1485
1578
  const registerPersonWithBiometric = async (personData, facePhotos, applicationToken) => {
1486
1579
  ensureSecureContext();
1487
1580
  if (!facePhotos || facePhotos.length === 0) {
1488
- throw new NeoFaceError("At least one face photo is required for biometric registration", ErrorType.VALIDATION);
1581
+ throw new NeoFaceError("At least one face photo is required for biometric registration", ErrorType.VALIDATION_ERROR);
1489
1582
  }
1490
1583
  if (facePhotos.length > 5) {
1491
- throw new NeoFaceError("Maximum of 5 face photos allowed", ErrorType.VALIDATION);
1584
+ throw new NeoFaceError("Maximum of 5 face photos allowed", ErrorType.VALIDATION_ERROR);
1492
1585
  }
1493
1586
  const controller = createTimeoutController();
1494
1587
  try {
@@ -1530,13 +1623,13 @@ const registerPersonWithBiometric = async (personData, facePhotos, applicationTo
1530
1623
  try {
1531
1624
  const errorData = await response.json();
1532
1625
  if (errorData.error === "fraud_detected") {
1533
- throw new NeoFaceError("Fraud detected in biometric images. Registration blocked.", ErrorType.VALIDATION);
1626
+ throw new NeoFaceError("Fraud detected in biometric images. Registration blocked.", ErrorType.VALIDATION_ERROR);
1534
1627
  }
1535
1628
  if (errorData.error === "user_exists") {
1536
- throw new NeoFaceError("User with this email already exists", ErrorType.VALIDATION);
1629
+ throw new NeoFaceError("User with this email already exists", ErrorType.VALIDATION_ERROR);
1537
1630
  }
1538
1631
  if (errorData.error === "person_exists") {
1539
- throw new NeoFaceError("Person with this CPF already exists", ErrorType.VALIDATION);
1632
+ throw new NeoFaceError("Person with this CPF already exists", ErrorType.VALIDATION_ERROR);
1540
1633
  }
1541
1634
  throw new NeoFaceError(errorData.message || `Server returned status ${response.status}`, ErrorType.NETWORK);
1542
1635
  } catch (parseError) {
@@ -1566,6 +1659,52 @@ const registerPersonWithBiometric = async (personData, facePhotos, applicationTo
1566
1659
  throw new NeoFaceError("Unknown error occurred", ErrorType.NETWORK_ERROR);
1567
1660
  }
1568
1661
  };
1662
+ const loginWithEmail = async (email, password, applicationToken) => {
1663
+ ensureSecureContext();
1664
+ const controller = createTimeoutController();
1665
+ try {
1666
+ const response = await fetch(`${API_BASE_URL}/api/v1/auth/login/`, {
1667
+ method: "POST",
1668
+ headers: {
1669
+ "Content-Type": "application/json",
1670
+ "X-App-Token": applicationToken
1671
+ },
1672
+ body: JSON.stringify({ email, password }),
1673
+ signal: controller.signal
1674
+ });
1675
+ if (!response.ok) {
1676
+ if (response.status === 401 || response.status === 403) {
1677
+ throw new NeoFaceError("Invalid credentials or application token", ErrorType.INVALID_TOKEN);
1678
+ }
1679
+ throw new NeoFaceError(`Server returned status ${response.status}`, ErrorType.NETWORK);
1680
+ }
1681
+ const data = await response.json();
1682
+ return {
1683
+ success: true,
1684
+ // Compatível com diferentes formatos de resposta do backend
1685
+ accessToken: data.access_token || data.access,
1686
+ alias: data.user && data.user.alias || data.alias,
1687
+ user: {
1688
+ id: data.user && data.user.id || "",
1689
+ email: data.user && data.user.email || "",
1690
+ role: data.user && data.user.role || "",
1691
+ validated: data.user && data.user.validated || false,
1692
+ active: data.user && data.user.active || false
1693
+ }
1694
+ };
1695
+ } catch (error) {
1696
+ if (error instanceof Error) {
1697
+ if (error.name === "AbortError") {
1698
+ throw new NeoFaceError("Request timed out", ErrorType.NETWORK);
1699
+ }
1700
+ if (error instanceof NeoFaceError) {
1701
+ throw error;
1702
+ }
1703
+ throw new NeoFaceError(error.message, ErrorType.NETWORK);
1704
+ }
1705
+ throw new NeoFaceError("Unknown error occurred", ErrorType.NETWORK_ERROR);
1706
+ }
1707
+ };
1569
1708
  const modalReducer$1 = (state, action) => {
1570
1709
  switch (action.type) {
1571
1710
  case "PERMISSION_GRANTED":
@@ -3169,9 +3308,10 @@ class BiometricCaptureModal {
3169
3308
  context.drawImage(this.video, 0, 0, this.canvas.width, this.canvas.height);
3170
3309
  const imageData = this.canvas.toDataURL("image/jpeg", 0.8);
3171
3310
  const base64Data = imageData.split(",")[1];
3311
+ const dataUrl = `data:image/jpeg;base64,${base64Data}`;
3172
3312
  let detectedType = this.options.mode === "auto" ? await this.detectBiometricType(base64Data) : this.options.mode;
3173
3313
  this.close();
3174
- this.options.onSuccess(base64Data, detectedType);
3314
+ this.options.onSuccess(dataUrl, detectedType);
3175
3315
  } catch (error) {
3176
3316
  this.options.onError(
3177
3317
  new NeoFaceError(
@@ -3507,6 +3647,70 @@ class BiometricCaptureModal {
3507
3647
  document.head.appendChild(style);
3508
3648
  }
3509
3649
  }
3650
+ class EmailPasswordModal {
3651
+ constructor(options) {
3652
+ __publicField(this, "options");
3653
+ __publicField(this, "modalElement", null);
3654
+ this.options = options;
3655
+ }
3656
+ /**
3657
+ * Abre o modal de login com email e senha.
3658
+ */
3659
+ async open() {
3660
+ this.createModal();
3661
+ document.body.appendChild(this.modalElement);
3662
+ }
3663
+ /**
3664
+ * Fecha o modal.
3665
+ */
3666
+ close() {
3667
+ if (this.modalElement) {
3668
+ document.body.removeChild(this.modalElement);
3669
+ this.modalElement = null;
3670
+ }
3671
+ if (this.options.onCancel) {
3672
+ this.options.onCancel();
3673
+ }
3674
+ }
3675
+ createModal() {
3676
+ this.modalElement = document.createElement("div");
3677
+ this.modalElement.className = "neoface-modal";
3678
+ this.modalElement.innerHTML = `
3679
+ <div class="modal-content">
3680
+ <h2>${this.options.title || "Login com Email e Senha"}</h2>
3681
+ <p>${this.options.subtitle || "Informe seus dados para login."}</p>
3682
+ <form id="email-password-form">
3683
+ <input type="email" id="email" placeholder="Email" required />
3684
+ <input type="password" id="password" placeholder="Senha" required />
3685
+ <button type="submit">Entrar</button>
3686
+ </form>
3687
+ <button class="cancel-button">Cancelar</button>
3688
+ </div>
3689
+ `;
3690
+ const form = this.modalElement.querySelector("#email-password-form");
3691
+ form.addEventListener("submit", this.handleSubmit.bind(this));
3692
+ const cancelButton = this.modalElement.querySelector(".cancel-button");
3693
+ cancelButton.addEventListener("click", this.close.bind(this));
3694
+ }
3695
+ async handleSubmit(event) {
3696
+ event.preventDefault();
3697
+ const emailInput = this.modalElement.querySelector("#email");
3698
+ const passwordInput = this.modalElement.querySelector("#password");
3699
+ const email = emailInput.value.trim();
3700
+ const password = passwordInput.value.trim();
3701
+ if (!email || !password) {
3702
+ this.options.onError(new NeoFaceError("Email e senha são obrigatórios", ErrorType.VALIDATION_ERROR));
3703
+ return;
3704
+ }
3705
+ try {
3706
+ const result = await loginWithEmail(email, password, this.options.applicationToken);
3707
+ this.options.onSuccess(result);
3708
+ this.close();
3709
+ } catch (error) {
3710
+ this.options.onError(error);
3711
+ }
3712
+ }
3713
+ }
3510
3714
  async function startFaceLogin(options) {
3511
3715
  try {
3512
3716
  const isValidToken = await validateToken(options.applicationToken);
@@ -3542,6 +3746,134 @@ async function startFaceLogin(options) {
3542
3746
  );
3543
3747
  }
3544
3748
  }
3749
+ async function startHandLogin(options) {
3750
+ try {
3751
+ const isValidToken = await validateToken(options.applicationToken);
3752
+ if (!isValidToken) {
3753
+ options.onError(new NeoFaceError("Token de aplicação inválido", ErrorType.INVALID_TOKEN));
3754
+ return;
3755
+ }
3756
+ const captureOptions = {
3757
+ mode: "hand",
3758
+ onSuccess: async (imageData, _detectedType) => {
3759
+ try {
3760
+ const imageBlob = await fetch(imageData).then((res) => res.blob());
3761
+ const result = await loginWithBiometric(imageBlob, options.applicationToken);
3762
+ options.onSuccess(result);
3763
+ } catch (error) {
3764
+ options.onError(error);
3765
+ }
3766
+ },
3767
+ onError: options.onError,
3768
+ onCancel: options.onCancel,
3769
+ countdown: options.countdown,
3770
+ title: options.title || "Login por Mão",
3771
+ subtitle: options.subtitle || "Posicione sua mão dentro do quadro para fazer login."
3772
+ };
3773
+ const modal = new BiometricCaptureModal(captureOptions);
3774
+ await modal.open();
3775
+ } catch (error) {
3776
+ options.onError(
3777
+ new NeoFaceError(
3778
+ "Erro ao inicializar login por mão: " + error.message,
3779
+ ErrorType.INITIALIZATION_ERROR
3780
+ )
3781
+ );
3782
+ }
3783
+ }
3784
+ async function startAutoLogin(options) {
3785
+ try {
3786
+ const isValidToken = await validateToken(options.applicationToken);
3787
+ if (!isValidToken) {
3788
+ options.onError(new NeoFaceError("Token de aplicação inválido", ErrorType.INVALID_TOKEN));
3789
+ return;
3790
+ }
3791
+ const captureOptions = {
3792
+ mode: "auto",
3793
+ onSuccess: async (imageData, _detectedType) => {
3794
+ try {
3795
+ const imageBlob = await fetch(imageData).then((res) => res.blob());
3796
+ const result = await loginWithBiometric(imageBlob, options.applicationToken);
3797
+ options.onSuccess(result);
3798
+ } catch (error) {
3799
+ options.onError(error);
3800
+ }
3801
+ },
3802
+ onError: options.onError,
3803
+ onCancel: options.onCancel,
3804
+ countdown: options.countdown,
3805
+ title: options.title || "Login Biométrico",
3806
+ subtitle: options.subtitle || "Posicione seu rosto ou mão dentro do quadro para login automático."
3807
+ };
3808
+ const modal = new BiometricCaptureModal(captureOptions);
3809
+ await modal.open();
3810
+ } catch (error) {
3811
+ options.onError(
3812
+ new NeoFaceError(
3813
+ "Erro ao inicializar login automático: " + error.message,
3814
+ ErrorType.INITIALIZATION_ERROR
3815
+ )
3816
+ );
3817
+ }
3818
+ }
3819
+ function biometricLogin(applicationToken, mode = "auto") {
3820
+ return new Promise((resolve, reject) => {
3821
+ const options = {
3822
+ applicationToken,
3823
+ onSuccess: resolve,
3824
+ onError: reject
3825
+ };
3826
+ switch (mode) {
3827
+ case "face":
3828
+ startFaceLogin(options);
3829
+ break;
3830
+ case "hand":
3831
+ startHandLogin(options);
3832
+ break;
3833
+ case "auto":
3834
+ startAutoLogin(options);
3835
+ break;
3836
+ default:
3837
+ reject(new NeoFaceError("Modo de login inválido", ErrorType.VALIDATION_ERROR));
3838
+ }
3839
+ });
3840
+ }
3841
+ function biometricLoginWithFallback(applicationToken, mode = "auto") {
3842
+ return new Promise((resolve, reject) => {
3843
+ const options = {
3844
+ applicationToken,
3845
+ onSuccess: resolve,
3846
+ onError: (error) => {
3847
+ if (error.type === ErrorType.LOGIN_FAILED) {
3848
+ const fallbackOptions = {
3849
+ applicationToken,
3850
+ onSuccess: resolve,
3851
+ onError: reject,
3852
+ title: "Login Alternativo",
3853
+ subtitle: "Biometria não reconhecida. Por favor, informe email e senha."
3854
+ };
3855
+ const modal = new EmailPasswordModal(fallbackOptions);
3856
+ modal.open();
3857
+ } else {
3858
+ reject(error);
3859
+ }
3860
+ }
3861
+ };
3862
+ switch (mode) {
3863
+ case "face":
3864
+ startFaceLogin(options);
3865
+ break;
3866
+ case "hand":
3867
+ startHandLogin(options);
3868
+ break;
3869
+ case "auto":
3870
+ startAutoLogin(options);
3871
+ break;
3872
+ default:
3873
+ reject(new NeoFaceError("Modo de login inválido", ErrorType.VALIDATION_ERROR));
3874
+ }
3875
+ });
3876
+ }
3545
3877
  async function detectBiometricType(imageData) {
3546
3878
  try {
3547
3879
  const img = await loadImageFromBase64(imageData);
@@ -3578,7 +3910,8 @@ async function detectFace(img) {
3578
3910
  if (typeof window !== "undefined" && window.faceapi) {
3579
3911
  const faceapi2 = window.faceapi;
3580
3912
  await loadFaceApiModels();
3581
- const detections = await faceapi2.detectAllFaces(img).withFaceLandmarks().withFaceDescriptors();
3913
+ const tinyOptions = new faceapi2.TinyFaceDetectorOptions({ inputSize: 320, scoreThreshold: 0.5 });
3914
+ const detections = await faceapi2.detectAllFaces(img, tinyOptions).withFaceLandmarks();
3582
3915
  if (detections && detections.length > 0) {
3583
3916
  const bestDetection = detections.reduce(
3584
3917
  (best, current) => current.detection.score > best.detection.score ? current : best
@@ -3683,8 +4016,7 @@ async function loadFaceApiModels() {
3683
4016
  try {
3684
4017
  await Promise.all([
3685
4018
  faceapi2.nets.tinyFaceDetector.loadFromUri("./models"),
3686
- faceapi2.nets.faceLandmark68Net.loadFromUri("./models"),
3687
- faceapi2.nets.faceRecognitionNet.loadFromUri("./models")
4019
+ faceapi2.nets.faceLandmark68Net.loadFromUri("./models")
3688
4020
  ]);
3689
4021
  } catch (error) {
3690
4022
  console.warn("Não foi possível carregar modelos do face-api.js:", error);
@@ -3708,8 +4040,231 @@ const biometricDetection = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.
3708
4040
  initializeBiometricDetection,
3709
4041
  isAdvancedDetectionAvailable
3710
4042
  }, Symbol.toStringTag, { value: "Module" }));
3711
- const VERSION = "1.1.1";
3712
- const RELEASE_DATE = "2025-11-02";
4043
+ const VERSION = "1.2.0";
4044
+ const RELEASE_DATE = "2025-11-09";
4045
+ class OnboardingCaptureModal {
4046
+ constructor(options) {
4047
+ __publicField(this, "overlay", null);
4048
+ __publicField(this, "video", null);
4049
+ __publicField(this, "canvas", null);
4050
+ __publicField(this, "stream", null);
4051
+ __publicField(this, "step", "face");
4052
+ __publicField(this, "countdownSeconds");
4053
+ __publicField(this, "title");
4054
+ __publicField(this, "subtitle");
4055
+ __publicField(this, "faceBlob", null);
4056
+ __publicField(this, "documentBlob", null);
4057
+ this.countdownSeconds = (options == null ? void 0 : options.countdown) ?? 3;
4058
+ this.title = options == null ? void 0 : options.title;
4059
+ this.subtitle = options == null ? void 0 : options.subtitle;
4060
+ }
4061
+ /**
4062
+ * Abre o modal, inicia câmera e gerencia o fluxo de captura dos dois passos.
4063
+ * Retorna uma promessa resolvida com os Blobs de face e documento.
4064
+ */
4065
+ async open() {
4066
+ await this.createDom();
4067
+ await this.startCamera();
4068
+ await this.runFaceCaptureCountdown();
4069
+ this.faceBlob = await this.captureFrame();
4070
+ this.step = "document";
4071
+ this.updateTexts("Captura do Documento", 'Posicione seu documento visível e clique em "Capturar"');
4072
+ await this.waitForUserCaptureClick();
4073
+ this.documentBlob = await this.captureFrame();
4074
+ await this.close();
4075
+ if (!this.faceBlob || !this.documentBlob) {
4076
+ throw new Error("Falha na captura das imagens de onboarding");
4077
+ }
4078
+ return { face: this.faceBlob, document: this.documentBlob };
4079
+ }
4080
+ /**
4081
+ * Cria estrutura de DOM do modal e elementos de UI.
4082
+ */
4083
+ async createDom() {
4084
+ const overlay = document.createElement("div");
4085
+ overlay.className = "neofaceid-modal-overlay";
4086
+ overlay.innerHTML = `
4087
+ <div class="neofaceid-modal">
4088
+ <div class="neofaceid-modal-header">
4089
+ <h2 class="neofaceid-title">${this.title ?? "Onboarding NeoFaceID"}</h2>
4090
+ <button class="neofaceid-close-btn" aria-label="Fechar">×</button>
4091
+ </div>
4092
+ <div class="neofaceid-modal-body">
4093
+ <p class="neofaceid-subtitle">${this.subtitle ?? "Vamos capturar sua face e documento"}</p>
4094
+ <div class="neofaceid-camera-container">
4095
+ <video class="neofaceid-video" autoplay playsinline></video>
4096
+ <canvas class="neofaceid-canvas"></canvas>
4097
+ <div class="neofaceid-overlay">
4098
+ <div class="neofaceid-frame"></div>
4099
+ <div class="neofaceid-countdown" style="display:none"><span class="neofaceid-countdown-number">${this.countdownSeconds}</span></div>
4100
+ </div>
4101
+ </div>
4102
+ <div class="neofaceid-status"><p class="neofaceid-status-text">Posicione seu rosto no quadro</p></div>
4103
+ </div>
4104
+ <div class="neofaceid-modal-footer">
4105
+ <button class="neofaceid-cancel-btn">Cancelar</button>
4106
+ <button class="neofaceid-capture-btn" disabled>Capturar</button>
4107
+ </div>
4108
+ </div>
4109
+ `;
4110
+ const style = document.createElement("style");
4111
+ style.textContent = `
4112
+ .neofaceid-modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.6); display:flex; align-items:center; justify-content:center; z-index: 9999; }
4113
+ .neofaceid-modal { background:#fff; border-radius:12px; width: 92%; max-width: 640px; box-shadow: 0 20px 60px rgba(0,0,0,0.3); overflow:hidden; }
4114
+ .neofaceid-modal-header { padding: 16px 20px; border-bottom: 1px solid #eee; display:flex; align-items:center; justify-content:space-between; }
4115
+ .neofaceid-title { margin:0; font-size: 20px; color:#333; }
4116
+ .neofaceid-close-btn { background:none; border:none; font-size:24px; cursor:pointer; color:#666; }
4117
+ .neofaceid-modal-body { padding: 16px 20px; }
4118
+ .neofaceid-subtitle { margin: 0 0 12px 0; color:#666; }
4119
+ .neofaceid-camera-container { position: relative; background:#000; border-radius:8px; overflow:hidden; aspect-ratio: 4/3; margin-bottom: 12px; }
4120
+ .neofaceid-video { width:100%; height:100%; object-fit: cover; }
4121
+ .neofaceid-canvas { position:absolute; top:0; left:0; }
4122
+ .neofaceid-overlay { position:absolute; inset:0; display:flex; align-items:center; justify-content:center; }
4123
+ .neofaceid-frame { width: 200px; height: 200px; border: 3px solid #4CAF50; border-radius: 50%; box-shadow: 0 0 0 2px rgba(76,175,80,0.3); }
4124
+ .neofaceid-countdown { position:absolute; top:16px; right:16px; background: rgba(0,0,0,0.7); color:#fff; width: 48px; height:48px; display:flex; align-items:center; justify-content:center; border-radius:50%; }
4125
+ .neofaceid-modal-footer { padding: 16px 20px; border-top: 1px solid #eee; display:flex; gap: 8px; justify-content:flex-end; }
4126
+ .neofaceid-cancel-btn { background:#f5f5f5; color:#666; border:none; border-radius:6px; padding:10px 18px; cursor:pointer; }
4127
+ .neofaceid-capture-btn { background:#4CAF50; color:#fff; border:none; border-radius:6px; padding:10px 18px; cursor:pointer; }
4128
+ .neofaceid-capture-btn:disabled { background:#ccc; cursor:not-allowed; }
4129
+ `;
4130
+ document.head.appendChild(style);
4131
+ document.body.appendChild(overlay);
4132
+ this.overlay = overlay;
4133
+ this.video = this.overlay.querySelector(".neofaceid-video");
4134
+ this.canvas = this.overlay.querySelector(".neofaceid-canvas");
4135
+ const closeBtn = this.overlay.querySelector(".neofaceid-close-btn");
4136
+ const cancelBtn = this.overlay.querySelector(".neofaceid-cancel-btn");
4137
+ closeBtn.onclick = () => this.close();
4138
+ cancelBtn.onclick = () => this.close();
4139
+ const captureBtn = this.overlay.querySelector(".neofaceid-capture-btn");
4140
+ captureBtn.disabled = true;
4141
+ }
4142
+ /**
4143
+ * Inicia a câmera do usuário com resolução adequada.
4144
+ */
4145
+ async startCamera() {
4146
+ const constraints = {
4147
+ video: { facingMode: "user", width: { ideal: 1280 }, height: { ideal: 720 } },
4148
+ audio: false
4149
+ };
4150
+ this.stream = await navigator.mediaDevices.getUserMedia(constraints);
4151
+ if (!this.video)
4152
+ throw new Error("Elemento de vídeo não encontrado");
4153
+ this.video.srcObject = this.stream;
4154
+ await this.video.play();
4155
+ }
4156
+ /**
4157
+ * Executa a contagem regressiva e atualiza textos para captura de rosto.
4158
+ */
4159
+ async runFaceCaptureCountdown() {
4160
+ if (!this.overlay)
4161
+ return;
4162
+ const countdownEl = this.overlay.querySelector(".neofaceid-countdown");
4163
+ const countdownNumberEl = this.overlay.querySelector(".neofaceid-countdown-number");
4164
+ const statusText = this.overlay.querySelector(".neofaceid-status-text");
4165
+ countdownEl.style.display = "flex";
4166
+ statusText.textContent = "Prepare-se! Capturando seu rosto...";
4167
+ let remaining = this.countdownSeconds;
4168
+ countdownNumberEl.textContent = String(remaining);
4169
+ await new Promise((resolve) => {
4170
+ const interval = setInterval(() => {
4171
+ remaining -= 1;
4172
+ countdownNumberEl.textContent = String(remaining);
4173
+ if (remaining <= 0) {
4174
+ clearInterval(interval);
4175
+ countdownEl.style.display = "none";
4176
+ resolve();
4177
+ }
4178
+ }, 1e3);
4179
+ });
4180
+ }
4181
+ /**
4182
+ * Aguarda o clique do usuário no botão "Capturar" (para documento).
4183
+ */
4184
+ async waitForUserCaptureClick() {
4185
+ if (!this.overlay)
4186
+ return;
4187
+ const captureBtn = this.overlay.querySelector(".neofaceid-capture-btn");
4188
+ const statusText = this.overlay.querySelector(".neofaceid-status-text");
4189
+ captureBtn.disabled = false;
4190
+ statusText.textContent = 'Clique em "Capturar" quando o documento estiver visível';
4191
+ await new Promise((resolve) => {
4192
+ const handler = () => {
4193
+ captureBtn.removeEventListener("click", handler);
4194
+ resolve();
4195
+ };
4196
+ captureBtn.addEventListener("click", handler);
4197
+ });
4198
+ captureBtn.disabled = true;
4199
+ }
4200
+ /**
4201
+ * Captura o frame atual do vídeo e retorna como Blob JPEG.
4202
+ */
4203
+ async captureFrame() {
4204
+ if (!this.video || !this.canvas)
4205
+ throw new Error("Elementos de captura não encontrados");
4206
+ const ctx = this.canvas.getContext("2d");
4207
+ if (!ctx)
4208
+ throw new Error("Falha ao obter contexto do canvas");
4209
+ this.canvas.width = this.video.videoWidth;
4210
+ this.canvas.height = this.video.videoHeight;
4211
+ ctx.drawImage(this.video, 0, 0, this.canvas.width, this.canvas.height);
4212
+ const blob = await new Promise((resolve, reject) => {
4213
+ this.canvas.toBlob((b) => b ? resolve(b) : reject(new Error("Falha ao gerar imagem")), "image/jpeg", 0.92);
4214
+ });
4215
+ return blob;
4216
+ }
4217
+ /**
4218
+ * Atualiza título e subtítulo do modal conforme o passo.
4219
+ */
4220
+ updateTexts(title, subtitle) {
4221
+ if (!this.overlay)
4222
+ return;
4223
+ const titleEl = this.overlay.querySelector(".neofaceid-title");
4224
+ const subtitleEl = this.overlay.querySelector(".neofaceid-subtitle");
4225
+ const frameEl = this.overlay.querySelector(".neofaceid-frame");
4226
+ titleEl.textContent = title;
4227
+ subtitleEl.textContent = subtitle;
4228
+ if (this.step === "document") {
4229
+ frameEl.style.borderRadius = "8px";
4230
+ frameEl.style.width = "280px";
4231
+ frameEl.style.height = "180px";
4232
+ }
4233
+ }
4234
+ /**
4235
+ * Fecha o modal e libera a câmera.
4236
+ */
4237
+ async close() {
4238
+ if (this.stream) {
4239
+ this.stream.getTracks().forEach((t) => t.stop());
4240
+ this.stream = null;
4241
+ }
4242
+ if (this.overlay) {
4243
+ this.overlay.remove();
4244
+ this.overlay = null;
4245
+ }
4246
+ }
4247
+ }
4248
+ const startOnboarding = async (options) => {
4249
+ const { applicationToken, onboardingToken, countdown, title, subtitle, onSuccess, onError } = options;
4250
+ try {
4251
+ const details = await getOnboardingDetails(applicationToken, onboardingToken);
4252
+ if (!(details == null ? void 0 : details.is_valid)) {
4253
+ throw new NeoFaceError("Link de onboarding inválido ou expirado", ErrorType.VALIDATION_ERROR);
4254
+ }
4255
+ const modal = new OnboardingCaptureModal({ countdown, title, subtitle });
4256
+ const { face, document: document2 } = await modal.open();
4257
+ const result = await completeOnboarding(applicationToken, onboardingToken, face, document2);
4258
+ onSuccess({ ...result, details });
4259
+ } catch (err) {
4260
+ if (err instanceof NeoFaceError) {
4261
+ onError(err);
4262
+ return;
4263
+ }
4264
+ const message = (err == null ? void 0 : err.message) || "Erro desconhecido no onboarding";
4265
+ onError(new NeoFaceError(message, ErrorType.UNKNOWN));
4266
+ }
4267
+ };
3713
4268
  function start(applicationToken, callbacks) {
3714
4269
  const container = document.createElement("div");
3715
4270
  container.id = "neoface-modal-container";
@@ -3779,6 +4334,8 @@ export {
3779
4334
  NeoFaceError,
3780
4335
  RELEASE_DATE,
3781
4336
  VERSION,
4337
+ biometricLogin,
4338
+ biometricLoginWithFallback,
3782
4339
  detectBiometricType,
3783
4340
  initializeBiometricDetection,
3784
4341
  isAdvancedDetectionAvailable,
@@ -3790,8 +4347,11 @@ export {
3790
4347
  registerPersonWithoutFace,
3791
4348
  simpleIdentification,
3792
4349
  start,
4350
+ startAutoLogin,
3793
4351
  startBiometricRegistration,
3794
4352
  startFaceLogin,
4353
+ startHandLogin,
4354
+ startOnboarding,
3795
4355
  validateToken
3796
4356
  };
3797
4357
  //# sourceMappingURL=neoface-id-sdk.es.js.map