@neofaceid/web-sdk 1.43.0 → 2.0.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.
@@ -5678,7 +5678,6 @@ const loginWithEmail = async (email2, password, applicationToken) => {
5678
5678
  }
5679
5679
  };
5680
5680
  const requestDeviceEnrollmentToken = async (userAccessToken, applicationToken) => {
5681
- var _a2, _b, _c;
5682
5681
  if (!userAccessToken) {
5683
5682
  throw new NeoFaceError(
5684
5683
  "Inscrição de aparelho exige um usuário autenticado (access token ausente).",
@@ -5697,23 +5696,36 @@ const requestDeviceEnrollmentToken = async (userAccessToken, applicationToken) =
5697
5696
  signal: controller.signal
5698
5697
  });
5699
5698
  if (!response.ok) {
5700
- if (response.status === 401 || response.status === 403) {
5699
+ if (response.status === 401) {
5700
+ throw new NeoFaceError(
5701
+ "Sua sessão expirou. Entre novamente para inscrever o aparelho.",
5702
+ ErrorType.INVALID_TOKEN
5703
+ );
5704
+ }
5705
+ if (response.status === 403) {
5701
5706
  throw new NeoFaceError(
5702
- "Não foi possível inscrever o aparelho: sessão expirada ou conta sem permissão. Faça login novamente.",
5707
+ "Esta conta não pode inscrever aparelhos. Procure o suporte.",
5703
5708
  ErrorType.INVALID_TOKEN
5704
5709
  );
5705
5710
  }
5706
5711
  throw new NeoFaceError(`API Error ${response.status}`, ErrorType.API_ERROR);
5707
5712
  }
5708
5713
  const data = await response.json();
5709
- const token = data.token ?? ((_a2 = data.data) == null ? void 0 : _a2.token);
5710
- if (!token) {
5711
- throw new NeoFaceError("Resposta sem token de inscrição", ErrorType.API_ERROR);
5714
+ const body = data.data ?? data;
5715
+ const token = body.token;
5716
+ const handle = body.handle;
5717
+ if (!token || !handle) {
5718
+ throw new NeoFaceError(
5719
+ "Resposta de inscrição incompleta (token ou handle ausente)",
5720
+ ErrorType.API_ERROR
5721
+ );
5712
5722
  }
5713
5723
  return {
5724
+ handle,
5725
+ jti: body.jti ?? "",
5714
5726
  token,
5715
- expiresIn: data.expires_in ?? ((_b = data.data) == null ? void 0 : _b.expires_in) ?? 300,
5716
- purpose: data.purpose ?? ((_c = data.data) == null ? void 0 : _c.purpose) ?? "device-enrollment"
5727
+ expiresIn: body.expires_in ?? 300,
5728
+ purpose: body.purpose ?? "device-enrollment"
5717
5729
  };
5718
5730
  } catch (error) {
5719
5731
  if (error instanceof NeoFaceError) throw error;
@@ -6224,7 +6236,7 @@ const blobToBase64 = (blob) => new Promise((resolve, reject) => {
6224
6236
  reader.onerror = () => reject(new NeoFaceError("Failed to convert image to base64", ErrorType.CAPTURE_ERROR));
6225
6237
  reader.readAsDataURL(blob);
6226
6238
  });
6227
- const registerDocumentByImage = async (personId, jwtToken, images) => {
6239
+ const registerDocumentByImage$1 = async (personId, jwtToken, images) => {
6228
6240
  var _a2, _b;
6229
6241
  ensureSecureContext();
6230
6242
  if (!personId) {
@@ -6397,7 +6409,7 @@ const api = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty(
6397
6409
  refreshSession,
6398
6410
  registerApplication,
6399
6411
  registerBiometric,
6400
- registerDocumentByImage,
6412
+ registerDocumentByImage: registerDocumentByImage$1,
6401
6413
  registerPersonWithBiometric,
6402
6414
  registerPersonWithoutFace,
6403
6415
  requestDeviceEnrollmentToken,
@@ -7803,17 +7815,17 @@ function BiometricRegistrationModal({
7803
7815
  }
7804
7816
  let isRunning = true;
7805
7817
  let lastDetectionTime = 0;
7806
- const detectFace2 = async () => {
7818
+ const detectFace = async () => {
7807
7819
  if (!isRunning || state.status !== "liveness") {
7808
7820
  return;
7809
7821
  }
7810
7822
  if (isCapturingRef.current || currentStepRef.current === "complete") {
7811
- requestAnimationFrame(detectFace2);
7823
+ requestAnimationFrame(detectFace);
7812
7824
  return;
7813
7825
  }
7814
7826
  const now = Date.now();
7815
7827
  if (now - lastDetectionTime < CALIBRATION.DETECTION_INTERVAL) {
7816
- requestAnimationFrame(detectFace2);
7828
+ requestAnimationFrame(detectFace);
7817
7829
  return;
7818
7830
  }
7819
7831
  lastDetectionTime = now;
@@ -7837,7 +7849,7 @@ function BiometricRegistrationModal({
7837
7849
  }
7838
7850
  if (positionStatus !== "ok") {
7839
7851
  manageHoldAndCapture(false);
7840
- requestAnimationFrame(detectFace2);
7852
+ requestAnimationFrame(detectFace);
7841
7853
  return;
7842
7854
  }
7843
7855
  }
@@ -7862,7 +7874,7 @@ function BiometricRegistrationModal({
7862
7874
  } catch (error) {
7863
7875
  }
7864
7876
  if (isRunning && state.status === "liveness") {
7865
- requestAnimationFrame(detectFace2);
7877
+ requestAnimationFrame(detectFace);
7866
7878
  }
7867
7879
  };
7868
7880
  const startDetection = () => {
@@ -7872,7 +7884,7 @@ function BiometricRegistrationModal({
7872
7884
  clearTimeout(watchdogTimerRef.current);
7873
7885
  watchdogTimerRef.current = null;
7874
7886
  }
7875
- detectFace2();
7887
+ detectFace();
7876
7888
  };
7877
7889
  const video = videoRef.current;
7878
7890
  if (video && video.readyState >= 2) {
@@ -8886,8 +8898,8 @@ function BiometricRegistrationModal({
8886
8898
  /* @__PURE__ */ jsx("canvas", { ref: canvasRef })
8887
8899
  ] });
8888
8900
  }
8889
- const VERSION = "1.43.0";
8890
- const RELEASE_DATE = "2026-09-15";
8901
+ const VERSION = "2.0.0";
8902
+ const RELEASE_DATE = "2026-09-19";
8891
8903
  const CONSENT_TRAIL_STORAGE_KEY = "neoface_consent_trail_v1";
8892
8904
  const CONSENT_TRAIL_MAX_LIMIT = 100;
8893
8905
  async function hashString(inputStr) {
@@ -9599,8 +9611,8 @@ function StatusPill({ tone, children, icon, className }) {
9599
9611
  }
9600
9612
  );
9601
9613
  }
9602
- const STYLE_ID$2 = "neofaceid-awaiting-push-styles";
9603
- const CSS$1 = `
9614
+ const STYLE_ID$3 = "neofaceid-awaiting-push-styles";
9615
+ const CSS$2 = `
9604
9616
  .neofaceid-root .nfid-push {
9605
9617
  display: flex; flex-direction: column; align-items: center; text-align: center; gap: 10px;
9606
9618
  padding: 8px 8px 4px;
@@ -9651,7 +9663,7 @@ const CSS$1 = `
9651
9663
  .neofaceid-root .nfid-push-pulse { animation: none; }
9652
9664
  }
9653
9665
  `;
9654
- function formatRemaining(seconds) {
9666
+ function formatRemaining$1(seconds) {
9655
9667
  const safe = Math.max(0, seconds);
9656
9668
  const mm = Math.floor(safe / 60);
9657
9669
  const ss = safe % 60;
@@ -9665,7 +9677,7 @@ function AwaitingPushView({
9665
9677
  }) {
9666
9678
  useEffect(() => {
9667
9679
  injectThemeStyles();
9668
- injectScopedStyles(STYLE_ID$2, CSS$1);
9680
+ injectScopedStyles(STYLE_ID$3, CSS$2);
9669
9681
  }, []);
9670
9682
  return /* @__PURE__ */ jsxs("div", { className: "nfid-push", role: "status", "aria-live": "polite", children: [
9671
9683
  /* @__PURE__ */ jsx(StatusPill, { tone: "attention", children: "Aguardando confirmação" }),
@@ -9705,7 +9717,7 @@ function AwaitingPushView({
9705
9717
  ] }),
9706
9718
  /* @__PURE__ */ jsxs("p", { className: "nfid-push-countdown", children: [
9707
9719
  "Expira em ",
9708
- formatRemaining(remainingSeconds)
9720
+ formatRemaining$1(remainingSeconds)
9709
9721
  ] }),
9710
9722
  /* @__PURE__ */ jsx("div", { className: "nfid-push-actions", children: /* @__PURE__ */ jsx(Button, { variant: "ghost", onClick: onCancel, children: "Cancelar" }) }),
9711
9723
  /* @__PURE__ */ jsx("div", { className: "nfid-push-footer", children: /* @__PURE__ */ jsx(BrandFooter, {}) })
@@ -9714,7 +9726,7 @@ function AwaitingPushView({
9714
9726
  function PushExpiredView({ onFallback, onCancel }) {
9715
9727
  useEffect(() => {
9716
9728
  injectThemeStyles();
9717
- injectScopedStyles(STYLE_ID$2, CSS$1);
9729
+ injectScopedStyles(STYLE_ID$3, CSS$2);
9718
9730
  }, []);
9719
9731
  return /* @__PURE__ */ jsxs("div", { className: "nfid-push", role: "alert", "aria-live": "assertive", children: [
9720
9732
  /* @__PURE__ */ jsxs(
@@ -9751,7 +9763,7 @@ function PushCancelledView({
9751
9763
  }) {
9752
9764
  useEffect(() => {
9753
9765
  injectThemeStyles();
9754
- injectScopedStyles(STYLE_ID$2, CSS$1);
9766
+ injectScopedStyles(STYLE_ID$3, CSS$2);
9755
9767
  }, []);
9756
9768
  return /* @__PURE__ */ jsxs("div", { className: "nfid-push", role: "alert", "aria-live": "assertive", children: [
9757
9769
  /* @__PURE__ */ jsxs(
@@ -9789,7 +9801,7 @@ function PushCancelledView({
9789
9801
  function PushDeniedView({ appName, onCancel }) {
9790
9802
  useEffect(() => {
9791
9803
  injectThemeStyles();
9792
- injectScopedStyles(STYLE_ID$2, CSS$1);
9804
+ injectScopedStyles(STYLE_ID$3, CSS$2);
9793
9805
  }, []);
9794
9806
  return /* @__PURE__ */ jsxs("div", { className: "nfid-push", role: "alert", "aria-live": "assertive", children: [
9795
9807
  /* @__PURE__ */ jsxs(
@@ -9820,7 +9832,7 @@ function PushDeniedView({ appName, onCancel }) {
9820
9832
  /* @__PURE__ */ jsx("div", { className: "nfid-push-footer", children: /* @__PURE__ */ jsx(BrandFooter, {}) })
9821
9833
  ] });
9822
9834
  }
9823
- const CONTAINER_ID = "neofaceid-push-approval-container";
9835
+ const CONTAINER_ID$1 = "neofaceid-push-approval-container";
9824
9836
  function classifyAuthorizationFrame(frame) {
9825
9837
  var _a2, _b, _c;
9826
9838
  switch (frame.status) {
@@ -9976,7 +9988,7 @@ function PushApprovalModal({
9976
9988
  function awaitPushApproval(options) {
9977
9989
  return new Promise((resolve) => {
9978
9990
  const container = document.createElement("div");
9979
- container.id = CONTAINER_ID;
9991
+ container.id = CONTAINER_ID$1;
9980
9992
  document.body.appendChild(container);
9981
9993
  const root = createRoot(container);
9982
9994
  const settle = (outcome) => {
@@ -9987,6 +9999,392 @@ function awaitPushApproval(options) {
9987
9999
  root.render(/* @__PURE__ */ jsx(PushApprovalModal, { ...options, onSettle: settle }));
9988
10000
  });
9989
10001
  }
10002
+ const STYLE_ID$2 = "neofaceid-device-enrollment-styles";
10003
+ const CSS$1 = `
10004
+ .neofaceid-root .nfid-enroll {
10005
+ display: flex; flex-direction: column; align-items: center; text-align: center; gap: 10px;
10006
+ padding: 8px 8px 4px;
10007
+ }
10008
+ .neofaceid-root .nfid-enroll h2 {
10009
+ margin: 6px 0 0; font-size: 20px; font-weight: 700; color: var(--nfid-text); line-height: 1.25;
10010
+ letter-spacing: -0.015em;
10011
+ }
10012
+ .neofaceid-root .nfid-enroll p {
10013
+ margin: 0; font-size: 14px; color: var(--nfid-text-muted); line-height: 1.5;
10014
+ }
10015
+ /* O QR é sempre escuro sobre branco, independente do tema: leitor de código
10016
+ falha com contraste invertido, e a moldura clara faz a zona de silêncio. */
10017
+ .neofaceid-root .nfid-enroll-qr {
10018
+ background: #FFFFFF; padding: 14px; border-radius: 16px; margin-top: 12px;
10019
+ line-height: 0; box-shadow: 0 0 0 1px var(--nfid-line);
10020
+ }
10021
+ .neofaceid-root .nfid-enroll-qr svg { display: block; width: 184px; height: 184px; }
10022
+ .neofaceid-root .nfid-enroll-handle {
10023
+ display: flex; flex-direction: column; align-items: center; gap: 4px;
10024
+ margin-top: 14px; padding: 12px 24px;
10025
+ background: var(--nfid-surface-muted); border-radius: 12px; width: 100%;
10026
+ }
10027
+ .neofaceid-root .nfid-enroll-handle-label {
10028
+ font-size: 11px; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase;
10029
+ color: var(--nfid-text-subtle);
10030
+ }
10031
+ .neofaceid-root .nfid-enroll-handle-value {
10032
+ font-size: 26px; font-weight: 800; line-height: 1.1; letter-spacing: 0.12em;
10033
+ color: var(--nfid-text); font-variant-numeric: tabular-nums;
10034
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
10035
+ }
10036
+ .neofaceid-root .nfid-enroll-countdown {
10037
+ font-size: 13px; color: var(--nfid-text-muted); font-variant-numeric: tabular-nums;
10038
+ margin-top: 10px;
10039
+ }
10040
+ .neofaceid-root .nfid-enroll-actions {
10041
+ display: flex; flex-direction: column; gap: 10px; width: 100%; margin-top: 16px;
10042
+ }
10043
+ .neofaceid-root .nfid-enroll-note {
10044
+ font-size: 12px; line-height: 1.45; color: var(--nfid-text-muted);
10045
+ background: var(--nfid-surface-muted); border-radius: 10px;
10046
+ padding: 10px 14px; margin-top: 12px; text-align: left; width: 100%;
10047
+ }
10048
+ .neofaceid-root .nfid-enroll-icon { width: 56px; height: 56px; }
10049
+ .neofaceid-root .nfid-enroll-footer {
10050
+ display: flex; justify-content: center; margin-top: 16px; padding-top: 12px;
10051
+ border-top: 1px solid var(--nfid-line); width: 100%;
10052
+ }
10053
+ `;
10054
+ function formatRemaining(seconds) {
10055
+ const safe = Math.max(0, seconds);
10056
+ const mm = Math.floor(safe / 60);
10057
+ const ss = safe % 60;
10058
+ return `${mm}:${String(ss).padStart(2, "0")}`;
10059
+ }
10060
+ function QrCode({ value }) {
10061
+ const [path, setPath] = useState(null);
10062
+ useEffect(() => {
10063
+ let cancelled = false;
10064
+ import("./qrcode-DYfGiWu6.js").then((mod) => {
10065
+ if (cancelled) return;
10066
+ const qrcode = mod.default ?? mod;
10067
+ const qr = qrcode(0, "M");
10068
+ qr.addData(value);
10069
+ qr.make();
10070
+ const count = qr.getModuleCount();
10071
+ const parts = [];
10072
+ for (let r = 0; r < count; r += 1) {
10073
+ for (let c = 0; c < count; c += 1) {
10074
+ if (qr.isDark(r, c)) parts.push(`M${c} ${r}h1v1h-1z`);
10075
+ }
10076
+ }
10077
+ setPath({ d: parts.join(""), count });
10078
+ }).catch(() => {
10079
+ if (!cancelled) setPath(null);
10080
+ });
10081
+ return () => {
10082
+ cancelled = true;
10083
+ };
10084
+ }, [value]);
10085
+ if (!path) {
10086
+ return /* @__PURE__ */ jsx("svg", { viewBox: "0 0 21 21", "aria-hidden": "true" });
10087
+ }
10088
+ return /* @__PURE__ */ jsx(
10089
+ "svg",
10090
+ {
10091
+ viewBox: `0 0 ${path.count} ${path.count}`,
10092
+ shapeRendering: "crispEdges",
10093
+ role: "img",
10094
+ "aria-label": `Código para inscrição do aparelho: ${value.split("").join(" ")}`,
10095
+ children: /* @__PURE__ */ jsx("path", { d: path.d, fill: "#000000" })
10096
+ }
10097
+ );
10098
+ }
10099
+ function DeviceEnrollmentView({
10100
+ appName,
10101
+ handle,
10102
+ remainingSeconds,
10103
+ onCancel
10104
+ }) {
10105
+ useEffect(() => {
10106
+ injectThemeStyles();
10107
+ injectScopedStyles(STYLE_ID$2, CSS$1);
10108
+ }, []);
10109
+ return /* @__PURE__ */ jsxs("div", { className: "nfid-enroll", role: "status", "aria-live": "polite", children: [
10110
+ /* @__PURE__ */ jsx(StatusPill, { tone: "attention", children: "Aguardando o aparelho" }),
10111
+ /* @__PURE__ */ jsx("h2", { children: "Vincule seu celular" }),
10112
+ /* @__PURE__ */ jsxs("p", { children: [
10113
+ "Abra o NeoFaceID Push no celular e aponte para este código para vinculá-lo à sua conta no",
10114
+ " ",
10115
+ appName,
10116
+ "."
10117
+ ] }),
10118
+ /* @__PURE__ */ jsx("div", { className: "nfid-enroll-qr", children: /* @__PURE__ */ jsx(QrCode, { value: handle }) }),
10119
+ /* @__PURE__ */ jsxs("div", { className: "nfid-enroll-handle", children: [
10120
+ /* @__PURE__ */ jsx("span", { className: "nfid-enroll-handle-label", children: "Ou digite este código" }),
10121
+ /* @__PURE__ */ jsx("span", { className: "nfid-enroll-handle-value", children: handle })
10122
+ ] }),
10123
+ /* @__PURE__ */ jsxs("p", { className: "nfid-enroll-countdown", children: [
10124
+ "O código expira em ",
10125
+ formatRemaining(remainingSeconds)
10126
+ ] }),
10127
+ /* @__PURE__ */ jsx("div", { className: "nfid-enroll-actions", children: /* @__PURE__ */ jsx(Button, { variant: "ghost", onClick: onCancel, children: "Cancelar" }) }),
10128
+ /* @__PURE__ */ jsx("div", { className: "nfid-enroll-footer", children: /* @__PURE__ */ jsx(BrandFooter, {}) })
10129
+ ] });
10130
+ }
10131
+ function DeviceEnrolledView({ deviceLabel, onDone }) {
10132
+ useEffect(() => {
10133
+ injectThemeStyles();
10134
+ injectScopedStyles(STYLE_ID$2, CSS$1);
10135
+ }, []);
10136
+ return /* @__PURE__ */ jsxs("div", { className: "nfid-enroll", role: "alert", "aria-live": "assertive", children: [
10137
+ /* @__PURE__ */ jsxs(
10138
+ "svg",
10139
+ {
10140
+ className: "nfid-enroll-icon",
10141
+ viewBox: "0 0 24 24",
10142
+ fill: "none",
10143
+ stroke: "currentColor",
10144
+ strokeWidth: "1.75",
10145
+ strokeLinecap: "round",
10146
+ strokeLinejoin: "round",
10147
+ "aria-hidden": "true",
10148
+ style: { color: "#1E9E6A" },
10149
+ children: [
10150
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "9" }),
10151
+ /* @__PURE__ */ jsx("path", { d: "M8.5 12.5l2.5 2.5 4.5-5" })
10152
+ ]
10153
+ }
10154
+ ),
10155
+ /* @__PURE__ */ jsx("h2", { children: "Aparelho vinculado" }),
10156
+ deviceLabel ? /* @__PURE__ */ jsxs("p", { children: [
10157
+ /* @__PURE__ */ jsx("strong", { children: deviceLabel }),
10158
+ " agora pode confirmar suas operações."
10159
+ ] }) : /* @__PURE__ */ jsx("p", { children: "Um aparelho acabou de ser vinculado à sua conta." }),
10160
+ /* @__PURE__ */ jsx("div", { className: "nfid-enroll-note", children: deviceLabel ? /* @__PURE__ */ jsx(Fragment, { children: "Se este não é o seu aparelho, procure o suporte agora — alguém pode ter vinculado um celular à sua conta." }) : /* @__PURE__ */ jsxs(Fragment, { children: [
10161
+ /* @__PURE__ */ jsx("strong", { children: "Não conseguimos identificar qual aparelho foi vinculado." }),
10162
+ " Se não foi você quem acabou de fazer isso, procure o suporte agora — alguém pode ter vinculado um celular à sua conta."
10163
+ ] }) }),
10164
+ /* @__PURE__ */ jsx("div", { className: "nfid-enroll-actions", children: /* @__PURE__ */ jsx(Button, { variant: "primary", onClick: onDone, children: "Entendi" }) }),
10165
+ /* @__PURE__ */ jsx("div", { className: "nfid-enroll-footer", children: /* @__PURE__ */ jsx(BrandFooter, {}) })
10166
+ ] });
10167
+ }
10168
+ function DeviceEnrollmentExpiredView({
10169
+ onRetry,
10170
+ onCancel
10171
+ }) {
10172
+ useEffect(() => {
10173
+ injectThemeStyles();
10174
+ injectScopedStyles(STYLE_ID$2, CSS$1);
10175
+ }, []);
10176
+ return /* @__PURE__ */ jsxs("div", { className: "nfid-enroll", role: "alert", "aria-live": "assertive", children: [
10177
+ /* @__PURE__ */ jsxs(
10178
+ "svg",
10179
+ {
10180
+ className: "nfid-enroll-icon",
10181
+ viewBox: "0 0 24 24",
10182
+ fill: "none",
10183
+ stroke: "currentColor",
10184
+ strokeWidth: "1.75",
10185
+ strokeLinecap: "round",
10186
+ strokeLinejoin: "round",
10187
+ "aria-hidden": "true",
10188
+ style: { color: "var(--nfid-text-muted)" },
10189
+ children: [
10190
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "9" }),
10191
+ /* @__PURE__ */ jsx("path", { d: "M12 7v5l3 2" })
10192
+ ]
10193
+ }
10194
+ ),
10195
+ /* @__PURE__ */ jsx("h2", { children: "O código expirou" }),
10196
+ /* @__PURE__ */ jsx("p", { children: "Códigos de vinculação valem poucos minutos, por segurança. Gere um novo para tentar." }),
10197
+ /* @__PURE__ */ jsxs("div", { className: "nfid-enroll-actions", children: [
10198
+ /* @__PURE__ */ jsx(Button, { variant: "primary", onClick: onRetry, children: "Gerar novo código" }),
10199
+ /* @__PURE__ */ jsx(Button, { variant: "ghost", onClick: onCancel, children: "Agora não" })
10200
+ ] }),
10201
+ /* @__PURE__ */ jsx("div", { className: "nfid-enroll-footer", children: /* @__PURE__ */ jsx(BrandFooter, {}) })
10202
+ ] });
10203
+ }
10204
+ const CONTAINER_ID = "neofaceid-device-enrollment-container";
10205
+ function classifyEnrollmentFrame(frame) {
10206
+ var _a2, _b, _c;
10207
+ switch (frame.status) {
10208
+ case "enrolled":
10209
+ return {
10210
+ kind: "resolve",
10211
+ value: {
10212
+ status: "enrolled",
10213
+ // Vem `null` enquanto a PUSH-133 não sai. Propagamos o nulo em vez de
10214
+ // inventar um rótulo: a tela precisa poder dizer que não identificou.
10215
+ deviceLabel: ((_a2 = frame.data) == null ? void 0 : _a2.device_label) ?? null,
10216
+ enrolledAt: (_b = frame.data) == null ? void 0 : _b.enrolled_at
10217
+ }
10218
+ };
10219
+ case "expired":
10220
+ return { kind: "resolve", value: { status: "expired" } };
10221
+ case "error":
10222
+ case "failed":
10223
+ return {
10224
+ kind: "reject",
10225
+ error: new NeoFaceError(
10226
+ frame.message ?? ((_c = frame.data) == null ? void 0 : _c.message) ?? "Falha ao inscrever o aparelho",
10227
+ ErrorType.API_ERROR
10228
+ )
10229
+ };
10230
+ default:
10231
+ return { kind: "pending" };
10232
+ }
10233
+ }
10234
+ function DeviceEnrollmentModal({
10235
+ userAccessToken,
10236
+ applicationToken,
10237
+ appName,
10238
+ attempt,
10239
+ onSettle,
10240
+ onRetry,
10241
+ onFatal
10242
+ }) {
10243
+ const headerAppName = appName ?? getAppName() ?? "sua aplicação";
10244
+ const [phase, setPhase] = useState({ kind: "loading" });
10245
+ const [remaining, setRemaining] = useState(0);
10246
+ useEffect(() => {
10247
+ const controller = new AbortController();
10248
+ let done = false;
10249
+ (async () => {
10250
+ let issued;
10251
+ try {
10252
+ issued = await requestDeviceEnrollmentToken(userAccessToken, applicationToken);
10253
+ } catch (err) {
10254
+ if (!done) onFatal(err);
10255
+ return;
10256
+ }
10257
+ if (done) return;
10258
+ setPhase({
10259
+ kind: "waiting",
10260
+ handle: issued.handle,
10261
+ jti: issued.jti,
10262
+ expiresIn: issued.expiresIn
10263
+ });
10264
+ setRemaining(issued.expiresIn);
10265
+ const base = getBaseUrl();
10266
+ const channel = `${base}/api/v1/devices/enrollment/${encodeURIComponent(issued.jti)}/events/`;
10267
+ try {
10268
+ const outcome = await consumeSseStream({
10269
+ signal: controller.signal,
10270
+ timeoutMs: (issued.expiresIn + 30) * 1e3,
10271
+ requestTicket: async () => {
10272
+ var _a2;
10273
+ const res = await fetch(`${channel}ticket/`, {
10274
+ method: "POST",
10275
+ headers: { "Content-Type": "application/json", "X-App-Token": applicationToken }
10276
+ });
10277
+ if (!res.ok) {
10278
+ throw new NeoFaceError(
10279
+ `Falha ao abrir o canal de inscrição: ${res.status}`,
10280
+ res.status === 401 || res.status === 403 ? ErrorType.INVALID_TOKEN : ErrorType.NETWORK
10281
+ );
10282
+ }
10283
+ const body = await res.json();
10284
+ const ticket = ((_a2 = body == null ? void 0 : body.data) == null ? void 0 : _a2.ticket) ?? (body == null ? void 0 : body.ticket);
10285
+ if (!ticket) {
10286
+ throw new NeoFaceError("Canal de inscrição sem ticket", ErrorType.API_ERROR);
10287
+ }
10288
+ return ticket;
10289
+ },
10290
+ buildUrl: (ticket) => `${channel}?ticket=${encodeURIComponent(ticket)}`,
10291
+ classify: classifyEnrollmentFrame
10292
+ });
10293
+ if (done) return;
10294
+ if (outcome.status === "enrolled") {
10295
+ setPhase({
10296
+ kind: "enrolled",
10297
+ deviceLabel: outcome.deviceLabel,
10298
+ enrolledAt: outcome.enrolledAt
10299
+ });
10300
+ return;
10301
+ }
10302
+ setPhase({ kind: "expired" });
10303
+ } catch {
10304
+ if (done || controller.signal.aborted) return;
10305
+ setPhase({ kind: "expired" });
10306
+ }
10307
+ })();
10308
+ return () => {
10309
+ done = true;
10310
+ controller.abort();
10311
+ };
10312
+ }, [attempt]);
10313
+ useEffect(() => {
10314
+ if (phase.kind !== "waiting") return void 0;
10315
+ const id = setInterval(() => setRemaining((r) => r > 0 ? r - 1 : 0), 1e3);
10316
+ return () => clearInterval(id);
10317
+ }, [phase.kind]);
10318
+ if (phase.kind === "loading") return null;
10319
+ if (phase.kind === "enrolled") {
10320
+ return /* @__PURE__ */ jsx(Sheet, { open: true, dismissOnOverlay: false, ariaLabel: "Aparelho vinculado", children: /* @__PURE__ */ jsx(
10321
+ DeviceEnrolledView,
10322
+ {
10323
+ deviceLabel: phase.deviceLabel,
10324
+ onDone: () => onSettle({
10325
+ status: "enrolled",
10326
+ deviceLabel: phase.deviceLabel,
10327
+ enrolledAt: phase.enrolledAt
10328
+ })
10329
+ }
10330
+ ) });
10331
+ }
10332
+ if (phase.kind === "expired") {
10333
+ return /* @__PURE__ */ jsx(Sheet, { open: true, dismissOnOverlay: false, ariaLabel: "Código expirado", children: /* @__PURE__ */ jsx(
10334
+ DeviceEnrollmentExpiredView,
10335
+ {
10336
+ onRetry,
10337
+ onCancel: () => onSettle({ status: "expired" })
10338
+ }
10339
+ ) });
10340
+ }
10341
+ return /* @__PURE__ */ jsx(Sheet, { open: true, dismissOnOverlay: false, ariaLabel: "Vincular aparelho", children: /* @__PURE__ */ jsx(
10342
+ DeviceEnrollmentView,
10343
+ {
10344
+ appName: headerAppName,
10345
+ handle: phase.handle,
10346
+ remainingSeconds: remaining,
10347
+ onCancel: () => onSettle({ status: "cancelled" })
10348
+ }
10349
+ ) });
10350
+ }
10351
+ function startDeviceEnrollment(options) {
10352
+ return new Promise((resolve, reject) => {
10353
+ const container = document.createElement("div");
10354
+ container.id = CONTAINER_ID;
10355
+ document.body.appendChild(container);
10356
+ const root = createRoot(container);
10357
+ let attempt = 0;
10358
+ const cleanup = () => {
10359
+ root.unmount();
10360
+ if (document.body.contains(container)) document.body.removeChild(container);
10361
+ };
10362
+ const render = () => {
10363
+ root.render(
10364
+ /* @__PURE__ */ jsx(
10365
+ DeviceEnrollmentModal,
10366
+ {
10367
+ ...options,
10368
+ attempt,
10369
+ onSettle: (outcome) => {
10370
+ cleanup();
10371
+ resolve(outcome);
10372
+ },
10373
+ onRetry: () => {
10374
+ attempt += 1;
10375
+ render();
10376
+ },
10377
+ onFatal: (error) => {
10378
+ cleanup();
10379
+ reject(error);
10380
+ }
10381
+ }
10382
+ )
10383
+ );
10384
+ };
10385
+ render();
10386
+ });
10387
+ }
9990
10388
  function evaluateFacePosition(box, videoWidth, videoHeight) {
9991
10389
  if (!box || !videoWidth || !videoHeight) {
9992
10390
  return {
@@ -10299,604 +10697,6 @@ function CameraOvalGuide({
10299
10697
  }
10300
10698
  );
10301
10699
  }
10302
- const DARK_THRESHOLD = 0.235;
10303
- const IMPROVEMENT_TARGET = 0.12;
10304
- const SAMPLE_INTERVAL_MS = 600;
10305
- const EVALUATION_DELAY_MS = 600;
10306
- const MIN_TOGGLE_MS = 3e3;
10307
- const OVERLAY_ID_PREFIX = "nfid-flash-";
10308
- function createScreenFlashController(getVideo, options = {}) {
10309
- const darkThreshold = options.darkThreshold ?? DARK_THRESHOLD;
10310
- const improvementTarget = options.improvementTarget ?? IMPROVEMENT_TARGET;
10311
- const sampleIntervalMs = options.sampleIntervalMs ?? SAMPLE_INTERVAL_MS;
10312
- const minToggleMs = options.minToggleMs ?? MIN_TOGGLE_MS;
10313
- const zIndex = options.zIndex ?? 10001;
10314
- const overlayId = `${OVERLAY_ID_PREFIX}${Math.random().toString(36).slice(2, 9)}`;
10315
- let interval = null;
10316
- let evalTimer = null;
10317
- let flashOn = false;
10318
- let lastBrightness = 0;
10319
- let darkStreak = 0;
10320
- let lastToggleAt = 0;
10321
- let preFlashBrightness = null;
10322
- let overlayEl = null;
10323
- let scratchCanvas = null;
10324
- const showOverlay = () => {
10325
- if (overlayEl || typeof document === "undefined") return;
10326
- overlayEl = document.createElement("div");
10327
- overlayEl.id = overlayId;
10328
- overlayEl.setAttribute("aria-hidden", "true");
10329
- overlayEl.style.cssText = `
10330
- position: fixed; inset: 0; background: #FFFFFF;
10331
- z-index: ${zIndex}; pointer-events: none;
10332
- opacity: 0; transition: opacity 200ms ease-out;
10333
- `;
10334
- document.body.appendChild(overlayEl);
10335
- requestAnimationFrame(() => {
10336
- if (overlayEl) overlayEl.style.opacity = "1";
10337
- });
10338
- };
10339
- const hideOverlay = () => {
10340
- if (!overlayEl) return;
10341
- const el = overlayEl;
10342
- overlayEl = null;
10343
- el.style.opacity = "0";
10344
- setTimeout(() => {
10345
- if (el.parentNode) el.parentNode.removeChild(el);
10346
- }, 220);
10347
- };
10348
- const setFlashOn = (next) => {
10349
- if (flashOn === next) return;
10350
- flashOn = next;
10351
- if (next) showOverlay();
10352
- else hideOverlay();
10353
- if (options.onChange) options.onChange(next, lastBrightness);
10354
- };
10355
- const tick = () => {
10356
- if (typeof document !== "undefined" && !scratchCanvas) {
10357
- scratchCanvas = document.createElement("canvas");
10358
- }
10359
- const b = measureFrameBrightness(getVideo(), {
10360
- scratch: scratchCanvas ?? void 0
10361
- });
10362
- lastBrightness = b;
10363
- const now = Date.now();
10364
- if (!flashOn) {
10365
- if (b > 0 && b < darkThreshold) darkStreak += 1;
10366
- else darkStreak = 0;
10367
- if (darkStreak >= 2 && now - lastToggleAt >= minToggleMs) {
10368
- preFlashBrightness = b;
10369
- lastToggleAt = now;
10370
- darkStreak = 0;
10371
- setFlashOn(true);
10372
- evalTimer = setTimeout(() => {
10373
- const after = measureFrameBrightness(getVideo(), {
10374
- scratch: scratchCanvas ?? void 0
10375
- });
10376
- const gained = after - (preFlashBrightness ?? 0);
10377
- if (gained < improvementTarget) {
10378
- lastToggleAt = Date.now();
10379
- setFlashOn(false);
10380
- }
10381
- preFlashBrightness = null;
10382
- }, EVALUATION_DELAY_MS);
10383
- }
10384
- }
10385
- };
10386
- return {
10387
- start() {
10388
- if (options.enabled === false) return;
10389
- if (interval) return;
10390
- tick();
10391
- interval = setInterval(tick, sampleIntervalMs);
10392
- },
10393
- stop() {
10394
- if (interval) {
10395
- clearInterval(interval);
10396
- interval = null;
10397
- }
10398
- if (evalTimer) {
10399
- clearTimeout(evalTimer);
10400
- evalTimer = null;
10401
- }
10402
- setFlashOn(false);
10403
- darkStreak = 0;
10404
- preFlashBrightness = null;
10405
- },
10406
- destroy() {
10407
- this.stop();
10408
- scratchCanvas = null;
10409
- },
10410
- get flashOn() {
10411
- return flashOn;
10412
- },
10413
- get lastBrightness() {
10414
- return lastBrightness;
10415
- }
10416
- };
10417
- }
10418
- class BiometricCaptureModal {
10419
- constructor(options) {
10420
- __publicField(this, "modal", null);
10421
- __publicField(this, "video", null);
10422
- __publicField(this, "canvas", null);
10423
- __publicField(this, "stream", null);
10424
- __publicField(this, "options");
10425
- __publicField(this, "countdownInterval", null);
10426
- __publicField(this, "isCapturing", false);
10427
- __publicField(this, "flashController", null);
10428
- this.options = options;
10429
- }
10430
- /**
10431
- * Abre o modal de captura biométrica
10432
- */
10433
- async open() {
10434
- try {
10435
- await this.createModal();
10436
- await this.initializeCamera();
10437
- this.enableCaptureButton();
10438
- if (this.options.autoLighting !== false) {
10439
- this.flashController = createScreenFlashController(() => this.video);
10440
- this.flashController.start();
10441
- }
10442
- } catch (error) {
10443
- this.options.onError(
10444
- new NeoFaceError(
10445
- `Erro ao inicializar câmera: ${error.message}`,
10446
- ErrorType.CAMERA_ERROR
10447
- )
10448
- );
10449
- }
10450
- }
10451
- /**
10452
- * Fecha o modal e limpa recursos
10453
- */
10454
- close() {
10455
- this.cleanup();
10456
- if (this.modal) {
10457
- document.body.removeChild(this.modal);
10458
- this.modal = null;
10459
- }
10460
- }
10461
- /**
10462
- * Cria a estrutura HTML do modal
10463
- */
10464
- async createModal() {
10465
- this.modal = document.createElement("div");
10466
- this.modal.className = "neofaceid-root neofaceid-biometric-modal";
10467
- const title = this.options.title || this.getDefaultTitle();
10468
- const subtitle = this.options.subtitle || this.getDefaultSubtitle();
10469
- this.modal.innerHTML = `
10470
- <div class="neofaceid-modal-overlay">
10471
- <div class="neofaceid-modal-content">
10472
- <div class="neofaceid-modal-header">
10473
- <h2>${title}</h2>
10474
- <button class="neofaceid-close-btn" type="button" aria-label="Fechar">&times;</button>
10475
- </div>
10476
- <div class="neofaceid-modal-body">
10477
- <p class="neofaceid-subtitle">${subtitle}</p>
10478
- <div class="neofaceid-camera-container">
10479
- <video class="neofaceid-video" autoplay muted playsinline></video>
10480
- <canvas class="neofaceid-canvas" style="display: none;"></canvas>
10481
- <div class="neofaceid-overlay">
10482
- <div class="neofaceid-frame"></div>
10483
- </div>
10484
- </div>
10485
- <div class="neofaceid-status">
10486
- <p class="neofaceid-status-text">Posicione-se na frente da câmera</p>
10487
- </div>
10488
- </div>
10489
- <div class="neofaceid-modal-footer">
10490
- <button class="neofaceid-cancel-btn" type="button">Cancelar</button>
10491
- <button class="neofaceid-capture-btn" type="button" disabled>Capturar</button>
10492
- </div>
10493
- <div class="neofaceid-seal-row">
10494
- ${renderBrandFooter()}
10495
- </div>
10496
- </div>
10497
- </div>
10498
- `;
10499
- injectThemeStyles();
10500
- this.addStyles();
10501
- this.addEventListeners();
10502
- document.body.appendChild(this.modal);
10503
- }
10504
- /**
10505
- * Inicializa a câmera
10506
- */
10507
- async initializeCamera() {
10508
- if (!this.modal) throw new Error("Modal não inicializado");
10509
- this.video = this.modal.querySelector(".neofaceid-video");
10510
- this.canvas = this.modal.querySelector(".neofaceid-canvas");
10511
- if (!this.video || !this.canvas) {
10512
- throw new Error("Elementos de vídeo ou canvas não encontrados");
10513
- }
10514
- try {
10515
- this.stream = await navigator.mediaDevices.getUserMedia({
10516
- video: {
10517
- width: { ideal: 640 },
10518
- height: { ideal: 480 },
10519
- facingMode: "user"
10520
- },
10521
- audio: false
10522
- });
10523
- this.video.srcObject = this.stream;
10524
- await this.video.play();
10525
- this.canvas.width = this.video.videoWidth;
10526
- this.canvas.height = this.video.videoHeight;
10527
- } catch (error) {
10528
- throw new Error(`Não foi possível acessar a câmera: ${error.message}`);
10529
- }
10530
- }
10531
- /**
10532
- * NEO-413 · US14.6 (item 18) — a contagem "3, 2, 1" foi removida.
10533
- * Agora habilita direto o botão "Capturar" assim que a câmera fica pronta.
10534
- * Vivacidade fica com o desafio `runLivenessChallenge` (NEO-408), servidor decide.
10535
- */
10536
- enableCaptureButton() {
10537
- if (!this.modal) return;
10538
- const captureBtn = this.modal.querySelector(".neofaceid-capture-btn");
10539
- const statusText = this.modal.querySelector(".neofaceid-status-text");
10540
- if (captureBtn) {
10541
- captureBtn.disabled = false;
10542
- captureBtn.textContent = "Capturar";
10543
- }
10544
- if (statusText) {
10545
- statusText.textContent = "Pronto para capturar";
10546
- }
10547
- if (this.options.mode === "auto") {
10548
- setTimeout(() => this.captureImage(), 300);
10549
- }
10550
- }
10551
- /**
10552
- * Captura a imagem da câmera
10553
- */
10554
- async captureImage() {
10555
- if (this.isCapturing || !this.video || !this.canvas) return;
10556
- this.isCapturing = true;
10557
- try {
10558
- const context = this.canvas.getContext("2d");
10559
- if (!context) throw new Error("Não foi possível obter contexto do canvas");
10560
- context.drawImage(this.video, 0, 0, this.canvas.width, this.canvas.height);
10561
- const imageData = this.canvas.toDataURL("image/jpeg", 0.8);
10562
- const base64Data = imageData.split(",")[1];
10563
- const dataUrl = `data:image/jpeg;base64,${base64Data}`;
10564
- const detectedType = this.options.mode === "auto" ? await this.detectBiometricType(base64Data) : this.options.mode;
10565
- this.close();
10566
- this.options.onSuccess(dataUrl, detectedType);
10567
- } catch (error) {
10568
- this.options.onError(
10569
- new NeoFaceError(
10570
- `Erro ao capturar imagem: ${error.message}`,
10571
- ErrorType.CAPTURE_ERROR
10572
- )
10573
- );
10574
- } finally {
10575
- this.isCapturing = false;
10576
- }
10577
- }
10578
- /**
10579
- * Detecta o tipo biométrico na imagem (face ou mão)
10580
- */
10581
- async detectBiometricType(imageData) {
10582
- try {
10583
- const { detectBiometricType: detectBiometricType2 } = await Promise.resolve().then(() => biometricDetection);
10584
- const result = await detectBiometricType2(imageData);
10585
- if (result.type !== "unknown" && result.confidence > 0.6) {
10586
- return result.type;
10587
- }
10588
- return "face";
10589
- } catch (error) {
10590
- console.warn("Erro na detecção automática, usando face como padrão:", error);
10591
- return "face";
10592
- }
10593
- }
10594
- /**
10595
- * Adiciona event listeners aos elementos do modal
10596
- */
10597
- addEventListeners() {
10598
- if (!this.modal) return;
10599
- const closeBtn = this.modal.querySelector(".neofaceid-close-btn");
10600
- const cancelBtn = this.modal.querySelector(".neofaceid-cancel-btn");
10601
- const captureBtn = this.modal.querySelector(".neofaceid-capture-btn");
10602
- closeBtn == null ? void 0 : closeBtn.addEventListener("click", () => {
10603
- var _a2, _b;
10604
- this.close();
10605
- (_b = (_a2 = this.options).onCancel) == null ? void 0 : _b.call(_a2);
10606
- });
10607
- cancelBtn == null ? void 0 : cancelBtn.addEventListener("click", () => {
10608
- var _a2, _b;
10609
- this.close();
10610
- (_b = (_a2 = this.options).onCancel) == null ? void 0 : _b.call(_a2);
10611
- });
10612
- captureBtn == null ? void 0 : captureBtn.addEventListener("click", () => {
10613
- this.captureImage();
10614
- });
10615
- this.modal.addEventListener("click", (e) => {
10616
- var _a2, _b, _c;
10617
- if (e.target === ((_a2 = this.modal) == null ? void 0 : _a2.querySelector(".neofaceid-modal-overlay"))) {
10618
- this.close();
10619
- (_c = (_b = this.options).onCancel) == null ? void 0 : _c.call(_b);
10620
- }
10621
- });
10622
- }
10623
- /**
10624
- * Limpa recursos (câmera, intervalos, etc.)
10625
- */
10626
- cleanup() {
10627
- if (this.flashController) {
10628
- this.flashController.destroy();
10629
- this.flashController = null;
10630
- }
10631
- if (this.stream) {
10632
- this.stream.getTracks().forEach((track) => track.stop());
10633
- this.stream = null;
10634
- }
10635
- if (this.countdownInterval) {
10636
- clearInterval(this.countdownInterval);
10637
- this.countdownInterval = null;
10638
- }
10639
- this.video = null;
10640
- this.canvas = null;
10641
- this.isCapturing = false;
10642
- }
10643
- /**
10644
- * Retorna o título padrão baseado no modo
10645
- */
10646
- getDefaultTitle() {
10647
- switch (this.options.mode) {
10648
- case "face":
10649
- return "Autenticação Facial";
10650
- case "hand":
10651
- return "Autenticação por Mão";
10652
- case "auto":
10653
- return "Autenticação Biométrica";
10654
- default:
10655
- return "Captura Biométrica";
10656
- }
10657
- }
10658
- /**
10659
- * Retorna o subtítulo padrão baseado no modo
10660
- */
10661
- getDefaultSubtitle() {
10662
- switch (this.options.mode) {
10663
- case "face":
10664
- return "Posicione seu rosto dentro do quadro e aguarde a captura automática.";
10665
- case "hand":
10666
- return "Posicione sua mão dentro do quadro e aguarde a captura automática.";
10667
- case "auto":
10668
- return "Posicione seu rosto ou mão dentro do quadro para autenticação automática.";
10669
- default:
10670
- return "Posicione-se dentro do quadro para captura.";
10671
- }
10672
- }
10673
- /**
10674
- * Adiciona estilos CSS ao modal
10675
- */
10676
- addStyles() {
10677
- const styleId = "neofaceid-biometric-modal-styles";
10678
- const existingStyles = document.getElementById(styleId);
10679
- if (existingStyles) {
10680
- existingStyles.remove();
10681
- }
10682
- injectScopedStyles(styleId, `
10683
- .neofaceid-biometric-modal {
10684
- position: fixed;
10685
- top: 0;
10686
- left: 0;
10687
- width: 100%;
10688
- height: 100%;
10689
- z-index: 10000;
10690
- font-family: inherit;
10691
- }
10692
-
10693
- .neofaceid-modal-overlay {
10694
- position: absolute;
10695
- top: 0;
10696
- left: 0;
10697
- width: 100%;
10698
- height: 100%;
10699
- background: rgba(0, 0, 0, 0.8);
10700
- display: flex;
10701
- align-items: center;
10702
- justify-content: center;
10703
- padding: 20px;
10704
- box-sizing: border-box;
10705
- }
10706
-
10707
- .neofaceid-modal-content {
10708
- background: white;
10709
- border-radius: 12px;
10710
- max-width: 600px;
10711
- width: 100%;
10712
- max-height: 90vh;
10713
- overflow: hidden;
10714
- box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
10715
- }
10716
-
10717
- .neofaceid-modal-header {
10718
- padding: 20px;
10719
- border-bottom: 1px solid #e0e0e0;
10720
- display: flex;
10721
- justify-content: space-between;
10722
- align-items: center;
10723
- }
10724
-
10725
- .neofaceid-modal-header h2 {
10726
- margin: 0;
10727
- font-size: 24px;
10728
- font-weight: 600;
10729
- color: #333;
10730
- }
10731
-
10732
- .neofaceid-close-btn {
10733
- background: none;
10734
- border: none;
10735
- font-size: 22px;
10736
- cursor: pointer;
10737
- color: #666;
10738
- padding: 0;
10739
- min-width: 44px;
10740
- min-height: 44px;
10741
- display: flex;
10742
- align-items: center;
10743
- justify-content: center;
10744
- border-radius: 22px;
10745
- transition: background-color 0.2s;
10746
- }
10747
- .neofaceid-close-btn:focus-visible {
10748
- outline: 2px solid var(--nfid-focus-ring);
10749
- outline-offset: 2px;
10750
- }
10751
-
10752
- .neofaceid-close-btn:hover {
10753
- background-color: #f0f0f0;
10754
- }
10755
-
10756
- .neofaceid-modal-body {
10757
- padding: 20px;
10758
- }
10759
-
10760
- .neofaceid-subtitle {
10761
- margin: 0 0 20px 0;
10762
- color: #666;
10763
- font-size: 16px;
10764
- line-height: 1.4;
10765
- }
10766
-
10767
- .neofaceid-camera-container {
10768
- position: relative;
10769
- background: #000;
10770
- border-radius: 8px;
10771
- overflow: hidden;
10772
- aspect-ratio: 4/3;
10773
- margin-bottom: 20px;
10774
- }
10775
-
10776
- .neofaceid-video {
10777
- width: 100%;
10778
- height: 100%;
10779
- object-fit: cover;
10780
- }
10781
-
10782
- .neofaceid-canvas {
10783
- position: absolute;
10784
- top: 0;
10785
- left: 0;
10786
- }
10787
-
10788
- .neofaceid-overlay {
10789
- position: absolute;
10790
- top: 0;
10791
- left: 0;
10792
- width: 100%;
10793
- height: 100%;
10794
- display: flex;
10795
- align-items: center;
10796
- justify-content: center;
10797
- }
10798
-
10799
- .neofaceid-frame {
10800
- width: 200px;
10801
- height: 200px;
10802
- border: 3px solid #0E9F6E;
10803
- border-radius: 50%;
10804
- box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.3);
10805
- animation: pulse 2s infinite;
10806
- }
10807
-
10808
- .neofaceid-seal-row {
10809
- display: flex;
10810
- justify-content: center;
10811
- padding: 12px 20px 16px;
10812
- border-top: 1px solid var(--nfid-line, #E6ECF4);
10813
- }
10814
- .neofaceid-seal {
10815
- font-size: 10px;
10816
- font-weight: 500;
10817
- letter-spacing: 0.06em;
10818
- text-transform: uppercase;
10819
- color: var(--nfid-text-subtle, #617489);
10820
- }
10821
-
10822
- .neofaceid-status {
10823
- text-align: center;
10824
- margin-bottom: 20px;
10825
- }
10826
-
10827
- .neofaceid-status-text {
10828
- margin: 0;
10829
- color: #666;
10830
- font-size: 16px;
10831
- }
10832
-
10833
- .neofaceid-modal-footer {
10834
- padding: 20px;
10835
- border-top: 1px solid #e0e0e0;
10836
- display: flex;
10837
- gap: 12px;
10838
- justify-content: flex-end;
10839
- }
10840
-
10841
- .neofaceid-cancel-btn,
10842
- .neofaceid-capture-btn {
10843
- padding: 12px 24px;
10844
- border: none;
10845
- border-radius: 6px;
10846
- font-size: 16px;
10847
- font-weight: 500;
10848
- cursor: pointer;
10849
- transition: all 0.2s;
10850
- }
10851
-
10852
- .neofaceid-cancel-btn {
10853
- background: #f5f5f5;
10854
- color: #666;
10855
- }
10856
-
10857
- .neofaceid-cancel-btn:hover {
10858
- background: #e0e0e0;
10859
- }
10860
-
10861
- .neofaceid-capture-btn {
10862
- background: #0E9F6E;
10863
- color: white;
10864
- }
10865
-
10866
- .neofaceid-capture-btn:hover:not(:disabled) {
10867
- background: #45a049;
10868
- }
10869
-
10870
- .neofaceid-capture-btn:disabled {
10871
- background: #ccc;
10872
- cursor: not-allowed;
10873
- }
10874
-
10875
- @keyframes pulse {
10876
- 0% { box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.3); }
10877
- 50% { box-shadow: 0 0 0 10px rgba(76, 175, 80, 0.1); }
10878
- 100% { box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.3); }
10879
- }
10880
-
10881
- @media (max-width: 640px) {
10882
- .neofaceid-modal-overlay {
10883
- padding: 10px;
10884
- }
10885
-
10886
- .neofaceid-modal-header,
10887
- .neofaceid-modal-body,
10888
- .neofaceid-modal-footer {
10889
- padding: 15px;
10890
- }
10891
-
10892
- .neofaceid-frame {
10893
- width: 150px;
10894
- height: 150px;
10895
- }
10896
- }
10897
- `);
10898
- }
10899
- }
10900
10700
  class BiometricStatusOverlay {
10901
10701
  constructor() {
10902
10702
  __publicField(this, "container", null);
@@ -11482,686 +11282,1284 @@ class EmailPasswordModal {
11482
11282
  if (existingStyles) {
11483
11283
  return;
11484
11284
  }
11485
- injectScopedStyles(styleId, `
11486
- .neoface-email-password-modal {
11487
- position: fixed;
11488
- top: 0;
11489
- left: 0;
11490
- width: 100%;
11491
- height: 100%;
11492
- z-index: 10002;
11493
- font-family: inherit;
11285
+ injectScopedStyles(styleId, `
11286
+ .neoface-email-password-modal {
11287
+ position: fixed;
11288
+ top: 0;
11289
+ left: 0;
11290
+ width: 100%;
11291
+ height: 100%;
11292
+ z-index: 10002;
11293
+ font-family: inherit;
11294
+ }
11295
+
11296
+ .neoface-email-modal-overlay {
11297
+ position: absolute;
11298
+ inset: 0;
11299
+ background: rgba(0, 0, 0, 0.6);
11300
+ backdrop-filter: blur(8px);
11301
+ display: flex;
11302
+ align-items: center;
11303
+ justify-content: center;
11304
+ padding: 20px;
11305
+ animation: fadeIn 0.3s ease-out;
11306
+ }
11307
+
11308
+ @keyframes fadeIn {
11309
+ from { opacity: 0; }
11310
+ to { opacity: 1; }
11311
+ }
11312
+
11313
+ @keyframes slideUp {
11314
+ from {
11315
+ opacity: 0;
11316
+ transform: translateY(20px);
11317
+ }
11318
+ to {
11319
+ opacity: 1;
11320
+ transform: translateY(0);
11321
+ }
11322
+ }
11323
+
11324
+ .neoface-email-modal-content {
11325
+ background: linear-gradient(135deg, #0A1320 0%, #111C2B 100%);
11326
+ border-radius: 20px;
11327
+ padding: 32px;
11328
+ max-width: 400px;
11329
+ width: 100%;
11330
+ box-shadow: 0 24px 48px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.1);
11331
+ animation: slideUp 0.4s ease-out;
11332
+ }
11333
+
11334
+ .neoface-email-modal-content h2 {
11335
+ font-size: 24px;
11336
+ font-weight: 700;
11337
+ color: white;
11338
+ margin: 0 0 12px 0;
11339
+ text-align: center;
11340
+ }
11341
+
11342
+ .neoface-email-modal-content p {
11343
+ font-size: 15px;
11344
+ color: rgba(255, 255, 255, 0.7);
11345
+ margin: 0 0 24px 0;
11346
+ text-align: center;
11347
+ line-height: 1.5;
11348
+ }
11349
+
11350
+ .neoface-email-modal-content form {
11351
+ display: flex;
11352
+ flex-direction: column;
11353
+ gap: 16px;
11354
+ margin-bottom: 16px;
11355
+ }
11356
+
11357
+ .neoface-email-modal-content input {
11358
+ width: 100%;
11359
+ padding: 14px 16px;
11360
+ border: 2px solid rgba(255, 255, 255, 0.2);
11361
+ border-radius: 12px;
11362
+ font-size: 15px;
11363
+ background: rgba(255, 255, 255, 0.1);
11364
+ color: white;
11365
+ font-family: inherit;
11366
+ transition: all 0.2s;
11367
+ box-sizing: border-box;
11368
+ }
11369
+
11370
+ .neoface-email-modal-content input::placeholder {
11371
+ color: rgba(255, 255, 255, 0.5);
11372
+ }
11373
+
11374
+ .neoface-email-modal-content input:focus {
11375
+ outline: none;
11376
+ border-color: #0059C4;
11377
+ box-shadow: 0 0 0 3px rgba(0, 89, 196, 0.2);
11378
+ background: rgba(255, 255, 255, 0.15);
11379
+ }
11380
+
11381
+ .neoface-email-submit-btn {
11382
+ width: 100%;
11383
+ padding: 14px 24px;
11384
+ border-radius: 12px;
11385
+ border: none;
11386
+ background: linear-gradient(135deg, #0059C4 0%, #00449A 100%);
11387
+ color: white;
11388
+ font-size: 16px;
11389
+ font-weight: 600;
11390
+ cursor: pointer;
11391
+ transition: all 0.2s;
11392
+ box-shadow: 0 4px 16px rgba(0, 89, 196, 0.3);
11393
+ font-family: inherit;
11394
+ }
11395
+
11396
+ .neoface-email-submit-btn:hover {
11397
+ transform: translateY(-2px);
11398
+ box-shadow: 0 8px 24px rgba(0, 89, 196, 0.4);
11399
+ }
11400
+
11401
+ .neoface-email-submit-btn:active {
11402
+ transform: translateY(0);
11403
+ }
11404
+
11405
+ .neoface-email-submit-btn:disabled {
11406
+ opacity: 0.6;
11407
+ cursor: not-allowed;
11408
+ transform: none;
11409
+ }
11410
+
11411
+ .neoface-email-cancel-btn {
11412
+ width: 100%;
11413
+ padding: 12px 24px;
11414
+ border-radius: 12px;
11415
+ border: 1px solid rgba(255, 255, 255, 0.2);
11416
+ background: transparent;
11417
+ color: rgba(255, 255, 255, 0.7);
11418
+ font-size: 15px;
11419
+ font-weight: 600;
11420
+ cursor: pointer;
11421
+ transition: all 0.2s;
11422
+ font-family: inherit;
11423
+ }
11424
+
11425
+ .neoface-email-cancel-btn:hover {
11426
+ background: rgba(255, 255, 255, 0.1);
11427
+ color: white;
11428
+ border-color: rgba(255, 255, 255, 0.3);
11429
+ }
11430
+
11431
+ .neoface-email-branding {
11432
+ display: flex;
11433
+ align-items: center;
11434
+ justify-content: center;
11435
+ gap: 8px;
11436
+ margin-top: 32px;
11437
+ padding-top: 24px;
11438
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
11439
+ font-size: 13px;
11440
+ color: rgba(255, 255, 255, 0.5);
11441
+ }
11442
+
11443
+ .neoface-email-brand-logo {
11444
+ height: 20px;
11445
+ width: auto;
11446
+ }
11447
+
11448
+ .neoface-email-brand-name {
11449
+ font-weight: 600;
11450
+ font-family: inherit;
11451
+ color: #ffffff;
11452
+ letter-spacing: 0.3px;
11453
+ }
11454
+
11455
+ .neoface-email-error {
11456
+ background: rgba(239, 68, 68, 0.15);
11457
+ border: 1px solid rgba(239, 68, 68, 0.4);
11458
+ border-radius: 10px;
11459
+ color: #fca5a5;
11460
+ font-size: 14px;
11461
+ padding: 12px 16px;
11462
+ margin-bottom: 16px;
11463
+ text-align: center;
11464
+ animation: fadeIn 0.2s ease-out;
11465
+ }
11466
+
11467
+ @media (max-width: 640px) {
11468
+ .neoface-email-modal-content {
11469
+ padding: 24px;
11470
+ border-radius: 16px;
11471
+ }
11472
+
11473
+ .neoface-email-modal-content h2 {
11474
+ font-size: 20px;
11475
+ }
11476
+ }
11477
+ `);
11478
+ }
11479
+ async handleSubmit(event) {
11480
+ event.preventDefault();
11481
+ const emailInput = this.modalElement.querySelector("#email");
11482
+ const passwordInput = this.modalElement.querySelector("#password");
11483
+ const submitBtn = this.modalElement.querySelector(
11484
+ ".neoface-email-submit-btn"
11485
+ );
11486
+ const errorDiv = this.modalElement.querySelector("#login-error");
11487
+ const showError = (msg) => {
11488
+ errorDiv.textContent = msg;
11489
+ errorDiv.style.display = "block";
11490
+ };
11491
+ errorDiv.style.display = "none";
11492
+ errorDiv.textContent = "";
11493
+ const email2 = emailInput.value.trim();
11494
+ const password = passwordInput.value.trim();
11495
+ if (!email2 || !password) {
11496
+ showError("Preencha o email e a senha para continuar.");
11497
+ return;
11498
+ }
11499
+ submitBtn.disabled = true;
11500
+ submitBtn.textContent = "Entrando...";
11501
+ try {
11502
+ const result = await loginWithEmail(email2, password, this.options.applicationToken);
11503
+ this.options.onSuccess(result);
11504
+ this.close();
11505
+ } catch (error) {
11506
+ submitBtn.disabled = false;
11507
+ submitBtn.textContent = "Entrar";
11508
+ const isCredentialError = error instanceof NeoFaceError && (error.type === ErrorType.INVALID_TOKEN || error.type === ErrorType.API_ERROR && error.message.includes("400"));
11509
+ if (isCredentialError) {
11510
+ showError("Email ou senha incorretos. Verifique seus dados e tente novamente.");
11511
+ passwordInput.value = "";
11512
+ passwordInput.focus();
11513
+ } else {
11514
+ showError("Ocorreu um erro inesperado. Tente novamente mais tarde.");
11515
+ this.options.onError(error);
11516
+ }
11517
+ }
11518
+ }
11519
+ }
11520
+ const MAX_ATTEMPTS$1 = 2;
11521
+ const RETRY_DELAY = 500;
11522
+ const CAMERA_READY_DELAY = 200;
11523
+ const CAMERA_STABILITY_DELAY = 100;
11524
+ const FACE_DETECTION_TIME = 5e3;
11525
+ const DETECTION_INTERVAL = 100;
11526
+ let modelsLoaded = false;
11527
+ let modelsLoading = null;
11528
+ async function preloadFaceDetectionModels() {
11529
+ if (modelsLoaded) return;
11530
+ if (modelsLoading) return modelsLoading;
11531
+ modelsLoading = (async () => {
11532
+ try {
11533
+ const MODEL_URL = "/models";
11534
+ await faceapi.nets.tinyFaceDetector.loadFromUri(MODEL_URL);
11535
+ modelsLoaded = true;
11536
+ console.log("✅ Face detection models preloaded");
11537
+ } catch (error) {
11538
+ console.warn("⚠️ Face-api.js models not loaded:", error);
11539
+ }
11540
+ })();
11541
+ return modelsLoading;
11542
+ }
11543
+ async function captureFaceSilently() {
11544
+ return new Promise((resolve, reject) => {
11545
+ const video = document.createElement("video");
11546
+ video.style.position = "fixed";
11547
+ video.style.top = "-9999px";
11548
+ video.style.left = "-9999px";
11549
+ video.style.width = "1px";
11550
+ video.style.height = "1px";
11551
+ video.style.opacity = "0";
11552
+ video.muted = true;
11553
+ video.playsInline = true;
11554
+ video.setAttribute("autoplay", "");
11555
+ video.setAttribute("playsinline", "");
11556
+ const canvas = document.createElement("canvas");
11557
+ canvas.style.display = "none";
11558
+ let stream = null;
11559
+ let faceDetectionAttempts = 0;
11560
+ const MAX_FACE_DETECTION_ATTEMPTS = 30;
11561
+ const cleanup = () => {
11562
+ if (stream) {
11563
+ stream.getTracks().forEach((track) => track.stop());
11494
11564
  }
11495
-
11496
- .neoface-email-modal-overlay {
11497
- position: absolute;
11498
- inset: 0;
11499
- background: rgba(0, 0, 0, 0.6);
11500
- backdrop-filter: blur(8px);
11501
- display: flex;
11502
- align-items: center;
11503
- justify-content: center;
11504
- padding: 20px;
11505
- animation: fadeIn 0.3s ease-out;
11565
+ if (video.parentElement) {
11566
+ video.parentElement.removeChild(video);
11506
11567
  }
11507
-
11508
- @keyframes fadeIn {
11509
- from { opacity: 0; }
11510
- to { opacity: 1; }
11568
+ if (canvas.parentElement) {
11569
+ canvas.parentElement.removeChild(canvas);
11511
11570
  }
11512
-
11513
- @keyframes slideUp {
11514
- from {
11515
- opacity: 0;
11516
- transform: translateY(20px);
11571
+ };
11572
+ document.body.appendChild(video);
11573
+ document.body.appendChild(canvas);
11574
+ const tryGetUserMedia = (withConstraints) => {
11575
+ if (withConstraints) {
11576
+ return navigator.mediaDevices.getUserMedia({
11577
+ video: {
11578
+ width: { ideal: 640 },
11579
+ height: { ideal: 480 },
11580
+ facingMode: "user"
11581
+ },
11582
+ audio: false
11583
+ });
11584
+ }
11585
+ return navigator.mediaDevices.getUserMedia({ video: { facingMode: "user" }, audio: false });
11586
+ };
11587
+ tryGetUserMedia(true).catch(() => tryGetUserMedia(false)).then((mediaStream) => {
11588
+ stream = mediaStream;
11589
+ video.srcObject = stream;
11590
+ const tryCapture = () => {
11591
+ if (!video.videoWidth || !video.videoHeight) {
11592
+ if (faceDetectionAttempts < MAX_FACE_DETECTION_ATTEMPTS) {
11593
+ faceDetectionAttempts++;
11594
+ setTimeout(tryCapture, 150);
11595
+ return;
11596
+ }
11597
+ cleanup();
11598
+ reject(new NeoFaceError("Câmera não está pronta", ErrorType.CAMERA_ERROR));
11599
+ return;
11517
11600
  }
11518
- to {
11519
- opacity: 1;
11520
- transform: translateY(0);
11601
+ canvas.width = video.videoWidth;
11602
+ canvas.height = video.videoHeight;
11603
+ const ctx = canvas.getContext("2d");
11604
+ if (!ctx) {
11605
+ cleanup();
11606
+ reject(new Error("Failed to get canvas context"));
11607
+ return;
11521
11608
  }
11609
+ const detectFace = async () => {
11610
+ if (!modelsLoaded) return true;
11611
+ try {
11612
+ const detection = await faceapi.detectSingleFace(
11613
+ video,
11614
+ new faceapi.TinyFaceDetectorOptions({ inputSize: 160, scoreThreshold: 0.3 })
11615
+ );
11616
+ return !!detection;
11617
+ } catch {
11618
+ return true;
11619
+ }
11620
+ };
11621
+ detectFace().then((hasFace) => {
11622
+ if (!hasFace && faceDetectionAttempts < MAX_FACE_DETECTION_ATTEMPTS) {
11623
+ faceDetectionAttempts++;
11624
+ setTimeout(tryCapture, 150);
11625
+ return;
11626
+ }
11627
+ ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
11628
+ canvas.toBlob(
11629
+ (blob) => {
11630
+ cleanup();
11631
+ if (blob) {
11632
+ resolve(blob);
11633
+ } else {
11634
+ reject(new Error("Failed to capture image"));
11635
+ }
11636
+ },
11637
+ "image/jpeg",
11638
+ 0.85
11639
+ );
11640
+ });
11641
+ };
11642
+ const playPromise = video.play();
11643
+ if (playPromise !== void 0) {
11644
+ playPromise.then(() => {
11645
+ if (video.videoWidth && video.videoHeight) {
11646
+ setTimeout(tryCapture, CAMERA_READY_DELAY);
11647
+ } else {
11648
+ video.onloadedmetadata = () => setTimeout(tryCapture, CAMERA_READY_DELAY);
11649
+ setTimeout(() => {
11650
+ if (video.videoWidth && video.videoHeight) {
11651
+ tryCapture();
11652
+ }
11653
+ }, 1500);
11654
+ }
11655
+ }).catch((err) => {
11656
+ cleanup();
11657
+ reject(
11658
+ new NeoFaceError(
11659
+ `Erro ao reproduzir câmera: ${err.message}`,
11660
+ ErrorType.CAMERA_ERROR
11661
+ )
11662
+ );
11663
+ });
11664
+ } else {
11665
+ video.onloadedmetadata = () => setTimeout(tryCapture, CAMERA_READY_DELAY);
11522
11666
  }
11523
-
11524
- .neoface-email-modal-content {
11525
- background: linear-gradient(135deg, #0A1320 0%, #111C2B 100%);
11526
- border-radius: 20px;
11527
- padding: 32px;
11528
- max-width: 400px;
11529
- width: 100%;
11530
- box-shadow: 0 24px 48px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.1);
11531
- animation: slideUp 0.4s ease-out;
11532
- }
11533
-
11534
- .neoface-email-modal-content h2 {
11535
- font-size: 24px;
11536
- font-weight: 700;
11537
- color: white;
11538
- margin: 0 0 12px 0;
11539
- text-align: center;
11540
- }
11541
-
11542
- .neoface-email-modal-content p {
11543
- font-size: 15px;
11544
- color: rgba(255, 255, 255, 0.7);
11545
- margin: 0 0 24px 0;
11546
- text-align: center;
11547
- line-height: 1.5;
11548
- }
11549
-
11550
- .neoface-email-modal-content form {
11551
- display: flex;
11552
- flex-direction: column;
11553
- gap: 16px;
11554
- margin-bottom: 16px;
11555
- }
11556
-
11557
- .neoface-email-modal-content input {
11558
- width: 100%;
11559
- padding: 14px 16px;
11560
- border: 2px solid rgba(255, 255, 255, 0.2);
11561
- border-radius: 12px;
11562
- font-size: 15px;
11563
- background: rgba(255, 255, 255, 0.1);
11564
- color: white;
11565
- font-family: inherit;
11566
- transition: all 0.2s;
11567
- box-sizing: border-box;
11667
+ video.onerror = () => {
11668
+ cleanup();
11669
+ reject(new NeoFaceError("Erro ao acessar câmera", ErrorType.CAMERA_ERROR));
11670
+ };
11671
+ }).catch((error) => {
11672
+ cleanup();
11673
+ reject(
11674
+ new NeoFaceError(
11675
+ `Não foi possível acessar a câmera: ${error.message}`,
11676
+ ErrorType.CAMERA_ERROR
11677
+ )
11678
+ );
11679
+ });
11680
+ });
11681
+ }
11682
+ async function detectFaceWithPosition(video, stream, guideModal, maxTime = FACE_DETECTION_TIME, interval = DETECTION_INTERVAL) {
11683
+ return new Promise((resolve) => {
11684
+ let attempts = 0;
11685
+ const maxAttempts = Math.ceil(maxTime / interval);
11686
+ let timeoutId = null;
11687
+ let isResolved = false;
11688
+ let centeredDurationMs = 0;
11689
+ const finish = (success) => {
11690
+ if (isResolved) return;
11691
+ isResolved = true;
11692
+ if (timeoutId) clearTimeout(timeoutId);
11693
+ resolve(success);
11694
+ };
11695
+ const checkFace = async () => {
11696
+ if (isResolved) return;
11697
+ attempts++;
11698
+ if (!modelsLoaded) {
11699
+ guideModal.update(stream, "searching", "Carregando modelos de detecção...");
11700
+ if (attempts >= maxAttempts) {
11701
+ finish(false);
11702
+ return;
11703
+ }
11704
+ timeoutId = window.setTimeout(checkFace, interval);
11705
+ return;
11568
11706
  }
11569
-
11570
- .neoface-email-modal-content input::placeholder {
11571
- color: rgba(255, 255, 255, 0.5);
11707
+ try {
11708
+ const detection = await faceapi.detectSingleFace(
11709
+ video,
11710
+ new faceapi.TinyFaceDetectorOptions({ inputSize: 160, scoreThreshold: 0.3 })
11711
+ );
11712
+ const { faceState, instruction } = evaluateFacePosition(
11713
+ detection ? detection.box : null,
11714
+ video.videoWidth,
11715
+ video.videoHeight
11716
+ );
11717
+ guideModal.update(stream, faceState, instruction);
11718
+ if (faceState === "centered") {
11719
+ centeredDurationMs += interval;
11720
+ if (centeredDurationMs >= 1e3) {
11721
+ console.log("✅ Rosto mantido centralizado por 1s consecutivo");
11722
+ finish(true);
11723
+ return;
11724
+ }
11725
+ } else {
11726
+ centeredDurationMs = 0;
11727
+ }
11728
+ if (attempts >= maxAttempts) {
11729
+ finish(false);
11730
+ return;
11731
+ }
11732
+ timeoutId = window.setTimeout(checkFace, interval);
11733
+ } catch (error) {
11734
+ console.error("❌ Erro na detecção facial:", error);
11735
+ if (attempts >= maxAttempts) {
11736
+ finish(false);
11737
+ return;
11738
+ }
11739
+ timeoutId = window.setTimeout(checkFace, interval);
11572
11740
  }
11573
-
11574
- .neoface-email-modal-content input:focus {
11575
- outline: none;
11576
- border-color: #0059C4;
11577
- box-shadow: 0 0 0 3px rgba(0, 89, 196, 0.2);
11578
- background: rgba(255, 255, 255, 0.15);
11741
+ };
11742
+ window.setTimeout(() => {
11743
+ if (!isResolved) {
11744
+ finish(false);
11579
11745
  }
11580
-
11581
- .neoface-email-submit-btn {
11582
- width: 100%;
11583
- padding: 14px 24px;
11584
- border-radius: 12px;
11585
- border: none;
11586
- background: linear-gradient(135deg, #0059C4 0%, #00449A 100%);
11587
- color: white;
11588
- font-size: 16px;
11589
- font-weight: 600;
11590
- cursor: pointer;
11591
- transition: all 0.2s;
11592
- box-shadow: 0 4px 16px rgba(0, 89, 196, 0.3);
11593
- font-family: inherit;
11746
+ }, maxTime + 500);
11747
+ checkFace();
11748
+ });
11749
+ }
11750
+ async function attemptLogin(applicationToken, fastMode = false, onCancel) {
11751
+ const guideModal = showCameraOvalGuideModal({
11752
+ stream: null,
11753
+ faceState: "searching",
11754
+ instruction: "Iniciando câmera...",
11755
+ onCancel
11756
+ });
11757
+ const video = document.createElement("video");
11758
+ video.style.position = "fixed";
11759
+ video.style.top = "-9999px";
11760
+ video.style.left = "-9999px";
11761
+ video.style.width = "1px";
11762
+ video.style.height = "1px";
11763
+ video.style.opacity = "0";
11764
+ video.muted = true;
11765
+ video.playsInline = true;
11766
+ video.setAttribute("autoplay", "");
11767
+ video.setAttribute("playsinline", "");
11768
+ document.body.appendChild(video);
11769
+ let stream = null;
11770
+ try {
11771
+ try {
11772
+ stream = await navigator.mediaDevices.getUserMedia({
11773
+ video: {
11774
+ width: { ideal: 640 },
11775
+ height: { ideal: 480 },
11776
+ facingMode: "user"
11777
+ },
11778
+ audio: false
11779
+ });
11780
+ } catch {
11781
+ stream = await navigator.mediaDevices.getUserMedia({
11782
+ video: { facingMode: "user" },
11783
+ audio: false
11784
+ });
11785
+ }
11786
+ video.srcObject = stream;
11787
+ guideModal.update(stream, "searching", "Centralize o rosto");
11788
+ await new Promise((resolve) => {
11789
+ if (video.readyState >= 1) {
11790
+ resolve();
11791
+ } else {
11792
+ const handler = () => {
11793
+ video.removeEventListener("loadedmetadata", handler);
11794
+ resolve();
11795
+ };
11796
+ video.addEventListener("loadedmetadata", handler);
11797
+ setTimeout(resolve, 2e3);
11594
11798
  }
11595
-
11596
- .neoface-email-submit-btn:hover {
11597
- transform: translateY(-2px);
11598
- box-shadow: 0 8px 24px rgba(0, 89, 196, 0.4);
11799
+ });
11800
+ await video.play();
11801
+ await new Promise((resolve) => {
11802
+ if (video.videoWidth && video.videoHeight) {
11803
+ resolve();
11804
+ } else {
11805
+ const checkSize = () => {
11806
+ if (video.videoWidth && video.videoHeight) {
11807
+ resolve();
11808
+ } else {
11809
+ setTimeout(checkSize, 100);
11810
+ }
11811
+ };
11812
+ setTimeout(resolve, 3e3);
11813
+ checkSize();
11599
11814
  }
11600
-
11601
- .neoface-email-submit-btn:active {
11602
- transform: translateY(0);
11815
+ });
11816
+ await new Promise((resolve) => setTimeout(resolve, CAMERA_STABILITY_DELAY));
11817
+ let faceDetected = true;
11818
+ if (!fastMode) {
11819
+ faceDetected = await detectFaceWithPosition(
11820
+ video,
11821
+ stream,
11822
+ guideModal,
11823
+ FACE_DETECTION_TIME,
11824
+ DETECTION_INTERVAL
11825
+ );
11826
+ }
11827
+ if (!faceDetected) {
11828
+ guideModal.close();
11829
+ if (stream) {
11830
+ stream.getTracks().forEach((track) => track.stop());
11603
11831
  }
11604
-
11605
- .neoface-email-submit-btn:disabled {
11606
- opacity: 0.6;
11607
- cursor: not-allowed;
11608
- transform: none;
11832
+ if (video.parentElement) {
11833
+ video.parentElement.removeChild(video);
11609
11834
  }
11610
-
11611
- .neoface-email-cancel-btn {
11612
- width: 100%;
11613
- padding: 12px 24px;
11614
- border-radius: 12px;
11615
- border: 1px solid rgba(255, 255, 255, 0.2);
11616
- background: transparent;
11617
- color: rgba(255, 255, 255, 0.7);
11618
- font-size: 15px;
11619
- font-weight: 600;
11620
- cursor: pointer;
11621
- transition: all 0.2s;
11622
- font-family: inherit;
11835
+ throw new NeoFaceError(
11836
+ "Nenhum rosto centralizado detectado. Por favor, tente novamente.",
11837
+ ErrorType.VALIDATION_ERROR
11838
+ );
11839
+ }
11840
+ guideModal.update(stream, "centered", "Capturando imagem...");
11841
+ const canvas = document.createElement("canvas");
11842
+ canvas.width = video.videoWidth;
11843
+ canvas.height = video.videoHeight;
11844
+ const ctx = canvas.getContext("2d");
11845
+ if (!ctx) {
11846
+ guideModal.close();
11847
+ if (stream) {
11848
+ stream.getTracks().forEach((track) => track.stop());
11623
11849
  }
11624
-
11625
- .neoface-email-cancel-btn:hover {
11626
- background: rgba(255, 255, 255, 0.1);
11627
- color: white;
11628
- border-color: rgba(255, 255, 255, 0.3);
11850
+ if (video.parentElement) {
11851
+ video.parentElement.removeChild(video);
11629
11852
  }
11630
-
11631
- .neoface-email-branding {
11632
- display: flex;
11633
- align-items: center;
11634
- justify-content: center;
11635
- gap: 8px;
11636
- margin-top: 32px;
11637
- padding-top: 24px;
11638
- border-top: 1px solid rgba(255, 255, 255, 0.1);
11639
- font-size: 13px;
11640
- color: rgba(255, 255, 255, 0.5);
11853
+ throw new NeoFaceError("Erro ao capturar imagem", ErrorType.CAPTURE_ERROR);
11854
+ }
11855
+ ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
11856
+ const imageBlob = await new Promise((resolve, reject) => {
11857
+ canvas.toBlob(
11858
+ (blob) => {
11859
+ canvas.remove();
11860
+ if (blob) {
11861
+ resolve(blob);
11862
+ } else {
11863
+ reject(new Error("Failed to capture image"));
11864
+ }
11865
+ },
11866
+ "image/jpeg",
11867
+ 0.85
11868
+ );
11869
+ });
11870
+ guideModal.update(stream, "centered", "Verificando com o servidor...");
11871
+ const result = await loginWithBiometric(imageBlob, applicationToken);
11872
+ guideModal.close();
11873
+ if (stream) {
11874
+ stream.getTracks().forEach((track) => track.stop());
11875
+ }
11876
+ if (video.parentElement) {
11877
+ video.parentElement.removeChild(video);
11878
+ }
11879
+ if (!result.success) {
11880
+ throw new NeoFaceError("Login falhou", ErrorType.LOGIN_FAILED);
11881
+ }
11882
+ return result;
11883
+ } catch (error) {
11884
+ guideModal.close();
11885
+ if (stream) {
11886
+ stream.getTracks().forEach((track) => track.stop());
11887
+ }
11888
+ if (video.parentElement) {
11889
+ video.parentElement.removeChild(video);
11890
+ }
11891
+ if (error instanceof NeoFaceError) {
11892
+ throw error;
11893
+ }
11894
+ throw new NeoFaceError(
11895
+ error instanceof Error ? error.message : "Erro desconhecido",
11896
+ ErrorType.UNKNOWN
11897
+ );
11898
+ }
11899
+ }
11900
+ async function executeBiometricLoginFlow(options) {
11901
+ const {
11902
+ applicationToken,
11903
+ onSuccess,
11904
+ onError,
11905
+ onFallbackRequest,
11906
+ onCancel,
11907
+ fastMode = false
11908
+ } = options;
11909
+ const fallbackPrompt = new FallbackPrompt();
11910
+ let attempts = 0;
11911
+ let lastError;
11912
+ if (!modelsLoaded && !fastMode) {
11913
+ preloadFaceDetectionModels().catch(() => {
11914
+ });
11915
+ }
11916
+ const tryLogin = async () => {
11917
+ try {
11918
+ attempts++;
11919
+ const result = await attemptLogin(applicationToken, fastMode, onCancel);
11920
+ onSuccess(result);
11921
+ } catch (error) {
11922
+ lastError = error instanceof NeoFaceError ? error : new NeoFaceError(
11923
+ error instanceof Error ? error.message : "Erro desconhecido",
11924
+ ErrorType.UNKNOWN
11925
+ );
11926
+ if (attempts < MAX_ATTEMPTS$1) {
11927
+ setTimeout(() => {
11928
+ tryLogin();
11929
+ }, RETRY_DELAY);
11930
+ } else {
11931
+ setTimeout(() => {
11932
+ fallbackPrompt.show(
11933
+ lastError,
11934
+ () => {
11935
+ attempts = 0;
11936
+ setTimeout(() => tryLogin(), RETRY_DELAY);
11937
+ },
11938
+ () => {
11939
+ if (onFallbackRequest) {
11940
+ onFallbackRequest();
11941
+ } else {
11942
+ onError(
11943
+ new NeoFaceError(
11944
+ "Por favor, use email e senha para fazer login",
11945
+ ErrorType.LOGIN_FAILED
11946
+ )
11947
+ );
11948
+ }
11949
+ },
11950
+ onCancel,
11951
+ applicationToken
11952
+ );
11953
+ }, 500);
11641
11954
  }
11642
-
11643
- .neoface-email-brand-logo {
11644
- height: 20px;
11645
- width: auto;
11955
+ }
11956
+ };
11957
+ tryLogin();
11958
+ }
11959
+ const biometricLoginFlow = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
11960
+ __proto__: null,
11961
+ captureFaceSilently,
11962
+ executeBiometricLoginFlow,
11963
+ preloadFaceDetectionModels
11964
+ }, Symbol.toStringTag, { value: "Module" }));
11965
+ const DARK_THRESHOLD = 0.235;
11966
+ const IMPROVEMENT_TARGET = 0.12;
11967
+ const SAMPLE_INTERVAL_MS = 600;
11968
+ const EVALUATION_DELAY_MS = 600;
11969
+ const MIN_TOGGLE_MS = 3e3;
11970
+ const OVERLAY_ID_PREFIX = "nfid-flash-";
11971
+ function createScreenFlashController(getVideo, options = {}) {
11972
+ const darkThreshold = options.darkThreshold ?? DARK_THRESHOLD;
11973
+ const improvementTarget = options.improvementTarget ?? IMPROVEMENT_TARGET;
11974
+ const sampleIntervalMs = options.sampleIntervalMs ?? SAMPLE_INTERVAL_MS;
11975
+ const minToggleMs = options.minToggleMs ?? MIN_TOGGLE_MS;
11976
+ const zIndex = options.zIndex ?? 10001;
11977
+ const overlayId = `${OVERLAY_ID_PREFIX}${Math.random().toString(36).slice(2, 9)}`;
11978
+ let interval = null;
11979
+ let evalTimer = null;
11980
+ let flashOn = false;
11981
+ let lastBrightness = 0;
11982
+ let darkStreak = 0;
11983
+ let lastToggleAt = 0;
11984
+ let preFlashBrightness = null;
11985
+ let overlayEl = null;
11986
+ let scratchCanvas = null;
11987
+ const showOverlay = () => {
11988
+ if (overlayEl || typeof document === "undefined") return;
11989
+ overlayEl = document.createElement("div");
11990
+ overlayEl.id = overlayId;
11991
+ overlayEl.setAttribute("aria-hidden", "true");
11992
+ overlayEl.style.cssText = `
11993
+ position: fixed; inset: 0; background: #FFFFFF;
11994
+ z-index: ${zIndex}; pointer-events: none;
11995
+ opacity: 0; transition: opacity 200ms ease-out;
11996
+ `;
11997
+ document.body.appendChild(overlayEl);
11998
+ requestAnimationFrame(() => {
11999
+ if (overlayEl) overlayEl.style.opacity = "1";
12000
+ });
12001
+ };
12002
+ const hideOverlay = () => {
12003
+ if (!overlayEl) return;
12004
+ const el = overlayEl;
12005
+ overlayEl = null;
12006
+ el.style.opacity = "0";
12007
+ setTimeout(() => {
12008
+ if (el.parentNode) el.parentNode.removeChild(el);
12009
+ }, 220);
12010
+ };
12011
+ const setFlashOn = (next) => {
12012
+ if (flashOn === next) return;
12013
+ flashOn = next;
12014
+ if (next) showOverlay();
12015
+ else hideOverlay();
12016
+ if (options.onChange) options.onChange(next, lastBrightness);
12017
+ };
12018
+ const tick = () => {
12019
+ if (typeof document !== "undefined" && !scratchCanvas) {
12020
+ scratchCanvas = document.createElement("canvas");
12021
+ }
12022
+ const b = measureFrameBrightness(getVideo(), {
12023
+ scratch: scratchCanvas ?? void 0
12024
+ });
12025
+ lastBrightness = b;
12026
+ const now = Date.now();
12027
+ if (!flashOn) {
12028
+ if (b > 0 && b < darkThreshold) darkStreak += 1;
12029
+ else darkStreak = 0;
12030
+ if (darkStreak >= 2 && now - lastToggleAt >= minToggleMs) {
12031
+ preFlashBrightness = b;
12032
+ lastToggleAt = now;
12033
+ darkStreak = 0;
12034
+ setFlashOn(true);
12035
+ evalTimer = setTimeout(() => {
12036
+ const after = measureFrameBrightness(getVideo(), {
12037
+ scratch: scratchCanvas ?? void 0
12038
+ });
12039
+ const gained = after - (preFlashBrightness ?? 0);
12040
+ if (gained < improvementTarget) {
12041
+ lastToggleAt = Date.now();
12042
+ setFlashOn(false);
12043
+ }
12044
+ preFlashBrightness = null;
12045
+ }, EVALUATION_DELAY_MS);
11646
12046
  }
11647
-
11648
- .neoface-email-brand-name {
11649
- font-weight: 600;
11650
- font-family: inherit;
11651
- color: #ffffff;
11652
- letter-spacing: 0.3px;
12047
+ }
12048
+ };
12049
+ return {
12050
+ start() {
12051
+ if (options.enabled === false) return;
12052
+ if (interval) return;
12053
+ tick();
12054
+ interval = setInterval(tick, sampleIntervalMs);
12055
+ },
12056
+ stop() {
12057
+ if (interval) {
12058
+ clearInterval(interval);
12059
+ interval = null;
11653
12060
  }
11654
-
11655
- .neoface-email-error {
11656
- background: rgba(239, 68, 68, 0.15);
11657
- border: 1px solid rgba(239, 68, 68, 0.4);
11658
- border-radius: 10px;
11659
- color: #fca5a5;
11660
- font-size: 14px;
11661
- padding: 12px 16px;
11662
- margin-bottom: 16px;
11663
- text-align: center;
11664
- animation: fadeIn 0.2s ease-out;
12061
+ if (evalTimer) {
12062
+ clearTimeout(evalTimer);
12063
+ evalTimer = null;
11665
12064
  }
11666
-
11667
- @media (max-width: 640px) {
11668
- .neoface-email-modal-content {
11669
- padding: 24px;
11670
- border-radius: 16px;
11671
- }
11672
-
11673
- .neoface-email-modal-content h2 {
11674
- font-size: 20px;
11675
- }
12065
+ setFlashOn(false);
12066
+ darkStreak = 0;
12067
+ preFlashBrightness = null;
12068
+ },
12069
+ destroy() {
12070
+ this.stop();
12071
+ scratchCanvas = null;
12072
+ },
12073
+ get flashOn() {
12074
+ return flashOn;
12075
+ },
12076
+ get lastBrightness() {
12077
+ return lastBrightness;
12078
+ }
12079
+ };
12080
+ }
12081
+ class BiometricCaptureModal {
12082
+ constructor(options) {
12083
+ __publicField(this, "modal", null);
12084
+ __publicField(this, "video", null);
12085
+ __publicField(this, "canvas", null);
12086
+ __publicField(this, "stream", null);
12087
+ __publicField(this, "options");
12088
+ __publicField(this, "countdownInterval", null);
12089
+ __publicField(this, "isCapturing", false);
12090
+ __publicField(this, "flashController", null);
12091
+ this.options = options;
12092
+ }
12093
+ /**
12094
+ * Abre o modal de captura biométrica
12095
+ */
12096
+ async open() {
12097
+ try {
12098
+ await this.createModal();
12099
+ await this.initializeCamera();
12100
+ this.enableCaptureButton();
12101
+ if (this.options.autoLighting !== false) {
12102
+ this.flashController = createScreenFlashController(() => this.video);
12103
+ this.flashController.start();
11676
12104
  }
11677
- `);
12105
+ } catch (error) {
12106
+ this.options.onError(
12107
+ new NeoFaceError(
12108
+ `Erro ao inicializar câmera: ${error.message}`,
12109
+ ErrorType.CAMERA_ERROR
12110
+ )
12111
+ );
12112
+ }
12113
+ }
12114
+ /**
12115
+ * Fecha o modal e limpa recursos
12116
+ */
12117
+ close() {
12118
+ this.cleanup();
12119
+ if (this.modal) {
12120
+ document.body.removeChild(this.modal);
12121
+ this.modal = null;
12122
+ }
12123
+ }
12124
+ /**
12125
+ * Cria a estrutura HTML do modal
12126
+ */
12127
+ async createModal() {
12128
+ this.modal = document.createElement("div");
12129
+ this.modal.className = "neofaceid-root neofaceid-biometric-modal";
12130
+ const title = this.options.title || this.getDefaultTitle();
12131
+ const subtitle = this.options.subtitle || this.getDefaultSubtitle();
12132
+ this.modal.innerHTML = `
12133
+ <div class="neofaceid-modal-overlay">
12134
+ <div class="neofaceid-modal-content">
12135
+ <div class="neofaceid-modal-header">
12136
+ <h2>${title}</h2>
12137
+ <button class="neofaceid-close-btn" type="button" aria-label="Fechar">&times;</button>
12138
+ </div>
12139
+ <div class="neofaceid-modal-body">
12140
+ <p class="neofaceid-subtitle">${subtitle}</p>
12141
+ <div class="neofaceid-camera-container">
12142
+ <video class="neofaceid-video" autoplay muted playsinline></video>
12143
+ <canvas class="neofaceid-canvas" style="display: none;"></canvas>
12144
+ <div class="neofaceid-overlay">
12145
+ <div class="neofaceid-frame"></div>
12146
+ </div>
12147
+ </div>
12148
+ <div class="neofaceid-status">
12149
+ <p class="neofaceid-status-text">Posicione-se na frente da câmera</p>
12150
+ </div>
12151
+ </div>
12152
+ <div class="neofaceid-modal-footer">
12153
+ <button class="neofaceid-cancel-btn" type="button">Cancelar</button>
12154
+ <button class="neofaceid-capture-btn" type="button" disabled>Capturar</button>
12155
+ </div>
12156
+ <div class="neofaceid-seal-row">
12157
+ ${renderBrandFooter()}
12158
+ </div>
12159
+ </div>
12160
+ </div>
12161
+ `;
12162
+ injectThemeStyles();
12163
+ this.addStyles();
12164
+ this.addEventListeners();
12165
+ document.body.appendChild(this.modal);
11678
12166
  }
11679
- async handleSubmit(event) {
11680
- event.preventDefault();
11681
- const emailInput = this.modalElement.querySelector("#email");
11682
- const passwordInput = this.modalElement.querySelector("#password");
11683
- const submitBtn = this.modalElement.querySelector(
11684
- ".neoface-email-submit-btn"
11685
- );
11686
- const errorDiv = this.modalElement.querySelector("#login-error");
11687
- const showError = (msg) => {
11688
- errorDiv.textContent = msg;
11689
- errorDiv.style.display = "block";
11690
- };
11691
- errorDiv.style.display = "none";
11692
- errorDiv.textContent = "";
11693
- const email2 = emailInput.value.trim();
11694
- const password = passwordInput.value.trim();
11695
- if (!email2 || !password) {
11696
- showError("Preencha o email e a senha para continuar.");
11697
- return;
12167
+ /**
12168
+ * Inicializa a câmera
12169
+ */
12170
+ async initializeCamera() {
12171
+ if (!this.modal) throw new Error("Modal não inicializado");
12172
+ this.video = this.modal.querySelector(".neofaceid-video");
12173
+ this.canvas = this.modal.querySelector(".neofaceid-canvas");
12174
+ if (!this.video || !this.canvas) {
12175
+ throw new Error("Elementos de vídeo ou canvas não encontrados");
11698
12176
  }
11699
- submitBtn.disabled = true;
11700
- submitBtn.textContent = "Entrando...";
11701
12177
  try {
11702
- const result = await loginWithEmail(email2, password, this.options.applicationToken);
11703
- this.options.onSuccess(result);
12178
+ this.stream = await navigator.mediaDevices.getUserMedia({
12179
+ video: {
12180
+ width: { ideal: 640 },
12181
+ height: { ideal: 480 },
12182
+ facingMode: "user"
12183
+ },
12184
+ audio: false
12185
+ });
12186
+ this.video.srcObject = this.stream;
12187
+ await this.video.play();
12188
+ this.canvas.width = this.video.videoWidth;
12189
+ this.canvas.height = this.video.videoHeight;
12190
+ } catch (error) {
12191
+ throw new Error(`Não foi possível acessar a câmera: ${error.message}`);
12192
+ }
12193
+ }
12194
+ /**
12195
+ * NEO-413 · US14.6 (item 18) — a contagem "3, 2, 1" foi removida.
12196
+ * Agora habilita direto o botão "Capturar" assim que a câmera fica pronta.
12197
+ * Vivacidade fica com o desafio `runLivenessChallenge` (NEO-408), servidor decide.
12198
+ */
12199
+ enableCaptureButton() {
12200
+ if (!this.modal) return;
12201
+ const captureBtn = this.modal.querySelector(".neofaceid-capture-btn");
12202
+ const statusText = this.modal.querySelector(".neofaceid-status-text");
12203
+ if (captureBtn) {
12204
+ captureBtn.disabled = false;
12205
+ captureBtn.textContent = "Capturar";
12206
+ }
12207
+ if (statusText) {
12208
+ statusText.textContent = "Pronto para capturar";
12209
+ }
12210
+ if (this.options.mode === "auto") {
12211
+ setTimeout(() => this.captureImage(), 300);
12212
+ }
12213
+ }
12214
+ /**
12215
+ * Captura a imagem da câmera
12216
+ */
12217
+ async captureImage() {
12218
+ if (this.isCapturing || !this.video || !this.canvas) return;
12219
+ this.isCapturing = true;
12220
+ try {
12221
+ const context = this.canvas.getContext("2d");
12222
+ if (!context) throw new Error("Não foi possível obter contexto do canvas");
12223
+ context.drawImage(this.video, 0, 0, this.canvas.width, this.canvas.height);
12224
+ const imageData = this.canvas.toDataURL("image/jpeg", 0.8);
12225
+ const base64Data = imageData.split(",")[1];
12226
+ const dataUrl = `data:image/jpeg;base64,${base64Data}`;
12227
+ const detectedType = this.options.mode === "auto" ? await this.detectBiometricType(base64Data) : this.options.mode;
11704
12228
  this.close();
12229
+ this.options.onSuccess(dataUrl, detectedType);
11705
12230
  } catch (error) {
11706
- submitBtn.disabled = false;
11707
- submitBtn.textContent = "Entrar";
11708
- const isCredentialError = error instanceof NeoFaceError && (error.type === ErrorType.INVALID_TOKEN || error.type === ErrorType.API_ERROR && error.message.includes("400"));
11709
- if (isCredentialError) {
11710
- showError("Email ou senha incorretos. Verifique seus dados e tente novamente.");
11711
- passwordInput.value = "";
11712
- passwordInput.focus();
11713
- } else {
11714
- showError("Ocorreu um erro inesperado. Tente novamente mais tarde.");
11715
- this.options.onError(error);
11716
- }
12231
+ this.options.onError(
12232
+ new NeoFaceError(
12233
+ `Erro ao capturar imagem: ${error.message}`,
12234
+ ErrorType.CAPTURE_ERROR
12235
+ )
12236
+ );
12237
+ } finally {
12238
+ this.isCapturing = false;
11717
12239
  }
11718
12240
  }
11719
- }
11720
- const MAX_ATTEMPTS$1 = 2;
11721
- const RETRY_DELAY = 500;
11722
- const CAMERA_READY_DELAY = 200;
11723
- const CAMERA_STABILITY_DELAY = 100;
11724
- const FACE_DETECTION_TIME = 5e3;
11725
- const DETECTION_INTERVAL = 100;
11726
- let modelsLoaded = false;
11727
- let modelsLoading = null;
11728
- async function preloadFaceDetectionModels() {
11729
- if (modelsLoaded) return;
11730
- if (modelsLoading) return modelsLoading;
11731
- modelsLoading = (async () => {
12241
+ /**
12242
+ * Detecta o tipo biométrico na imagem (face ou mão)
12243
+ */
12244
+ async detectBiometricType(imageData) {
11732
12245
  try {
11733
- const MODEL_URL = "/models";
11734
- await faceapi.nets.tinyFaceDetector.loadFromUri(MODEL_URL);
11735
- modelsLoaded = true;
11736
- console.log("✅ Face detection models preloaded");
12246
+ const { detectBiometricType } = await import("./biometricDetection-8utmtR25.js");
12247
+ const result = await detectBiometricType(imageData);
12248
+ if (result.type !== "unknown" && result.confidence > 0.6) {
12249
+ return result.type;
12250
+ }
12251
+ return "face";
11737
12252
  } catch (error) {
11738
- console.warn("⚠️ Face-api.js models not loaded:", error);
12253
+ console.warn("Erro na detecção automática, usando face como padrão:", error);
12254
+ return "face";
11739
12255
  }
11740
- })();
11741
- return modelsLoading;
11742
- }
11743
- async function captureFaceSilently() {
11744
- return new Promise((resolve, reject) => {
11745
- const video = document.createElement("video");
11746
- video.style.position = "fixed";
11747
- video.style.top = "-9999px";
11748
- video.style.left = "-9999px";
11749
- video.style.width = "1px";
11750
- video.style.height = "1px";
11751
- video.style.opacity = "0";
11752
- video.muted = true;
11753
- video.playsInline = true;
11754
- video.setAttribute("autoplay", "");
11755
- video.setAttribute("playsinline", "");
11756
- const canvas = document.createElement("canvas");
11757
- canvas.style.display = "none";
11758
- let stream = null;
11759
- let faceDetectionAttempts = 0;
11760
- const MAX_FACE_DETECTION_ATTEMPTS = 30;
11761
- const cleanup = () => {
11762
- if (stream) {
11763
- stream.getTracks().forEach((track) => track.stop());
12256
+ }
12257
+ /**
12258
+ * Adiciona event listeners aos elementos do modal
12259
+ */
12260
+ addEventListeners() {
12261
+ if (!this.modal) return;
12262
+ const closeBtn = this.modal.querySelector(".neofaceid-close-btn");
12263
+ const cancelBtn = this.modal.querySelector(".neofaceid-cancel-btn");
12264
+ const captureBtn = this.modal.querySelector(".neofaceid-capture-btn");
12265
+ closeBtn == null ? void 0 : closeBtn.addEventListener("click", () => {
12266
+ var _a2, _b;
12267
+ this.close();
12268
+ (_b = (_a2 = this.options).onCancel) == null ? void 0 : _b.call(_a2);
12269
+ });
12270
+ cancelBtn == null ? void 0 : cancelBtn.addEventListener("click", () => {
12271
+ var _a2, _b;
12272
+ this.close();
12273
+ (_b = (_a2 = this.options).onCancel) == null ? void 0 : _b.call(_a2);
12274
+ });
12275
+ captureBtn == null ? void 0 : captureBtn.addEventListener("click", () => {
12276
+ this.captureImage();
12277
+ });
12278
+ this.modal.addEventListener("click", (e) => {
12279
+ var _a2, _b, _c;
12280
+ if (e.target === ((_a2 = this.modal) == null ? void 0 : _a2.querySelector(".neofaceid-modal-overlay"))) {
12281
+ this.close();
12282
+ (_c = (_b = this.options).onCancel) == null ? void 0 : _c.call(_b);
12283
+ }
12284
+ });
12285
+ }
12286
+ /**
12287
+ * Limpa recursos (câmera, intervalos, etc.)
12288
+ */
12289
+ cleanup() {
12290
+ if (this.flashController) {
12291
+ this.flashController.destroy();
12292
+ this.flashController = null;
12293
+ }
12294
+ if (this.stream) {
12295
+ this.stream.getTracks().forEach((track) => track.stop());
12296
+ this.stream = null;
12297
+ }
12298
+ if (this.countdownInterval) {
12299
+ clearInterval(this.countdownInterval);
12300
+ this.countdownInterval = null;
12301
+ }
12302
+ this.video = null;
12303
+ this.canvas = null;
12304
+ this.isCapturing = false;
12305
+ }
12306
+ /**
12307
+ * Retorna o título padrão baseado no modo
12308
+ */
12309
+ getDefaultTitle() {
12310
+ switch (this.options.mode) {
12311
+ case "face":
12312
+ return "Autenticação Facial";
12313
+ case "hand":
12314
+ return "Autenticação por Mão";
12315
+ case "auto":
12316
+ return "Autenticação Biométrica";
12317
+ default:
12318
+ return "Captura Biométrica";
12319
+ }
12320
+ }
12321
+ /**
12322
+ * Retorna o subtítulo padrão baseado no modo
12323
+ */
12324
+ getDefaultSubtitle() {
12325
+ switch (this.options.mode) {
12326
+ case "face":
12327
+ return "Posicione seu rosto dentro do quadro e aguarde a captura automática.";
12328
+ case "hand":
12329
+ return "Posicione sua mão dentro do quadro e aguarde a captura automática.";
12330
+ case "auto":
12331
+ return "Posicione seu rosto ou mão dentro do quadro para autenticação automática.";
12332
+ default:
12333
+ return "Posicione-se dentro do quadro para captura.";
12334
+ }
12335
+ }
12336
+ /**
12337
+ * Adiciona estilos CSS ao modal
12338
+ */
12339
+ addStyles() {
12340
+ const styleId = "neofaceid-biometric-modal-styles";
12341
+ const existingStyles = document.getElementById(styleId);
12342
+ if (existingStyles) {
12343
+ existingStyles.remove();
12344
+ }
12345
+ injectScopedStyles(styleId, `
12346
+ .neofaceid-biometric-modal {
12347
+ position: fixed;
12348
+ top: 0;
12349
+ left: 0;
12350
+ width: 100%;
12351
+ height: 100%;
12352
+ z-index: 10000;
12353
+ font-family: inherit;
11764
12354
  }
11765
- if (video.parentElement) {
11766
- video.parentElement.removeChild(video);
12355
+
12356
+ .neofaceid-modal-overlay {
12357
+ position: absolute;
12358
+ top: 0;
12359
+ left: 0;
12360
+ width: 100%;
12361
+ height: 100%;
12362
+ background: rgba(0, 0, 0, 0.8);
12363
+ display: flex;
12364
+ align-items: center;
12365
+ justify-content: center;
12366
+ padding: 20px;
12367
+ box-sizing: border-box;
11767
12368
  }
11768
- if (canvas.parentElement) {
11769
- canvas.parentElement.removeChild(canvas);
12369
+
12370
+ .neofaceid-modal-content {
12371
+ background: white;
12372
+ border-radius: 12px;
12373
+ max-width: 600px;
12374
+ width: 100%;
12375
+ max-height: 90vh;
12376
+ overflow: hidden;
12377
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
11770
12378
  }
11771
- };
11772
- document.body.appendChild(video);
11773
- document.body.appendChild(canvas);
11774
- const tryGetUserMedia = (withConstraints) => {
11775
- if (withConstraints) {
11776
- return navigator.mediaDevices.getUserMedia({
11777
- video: {
11778
- width: { ideal: 640 },
11779
- height: { ideal: 480 },
11780
- facingMode: "user"
11781
- },
11782
- audio: false
11783
- });
12379
+
12380
+ .neofaceid-modal-header {
12381
+ padding: 20px;
12382
+ border-bottom: 1px solid #e0e0e0;
12383
+ display: flex;
12384
+ justify-content: space-between;
12385
+ align-items: center;
11784
12386
  }
11785
- return navigator.mediaDevices.getUserMedia({ video: { facingMode: "user" }, audio: false });
11786
- };
11787
- tryGetUserMedia(true).catch(() => tryGetUserMedia(false)).then((mediaStream) => {
11788
- stream = mediaStream;
11789
- video.srcObject = stream;
11790
- const tryCapture = () => {
11791
- if (!video.videoWidth || !video.videoHeight) {
11792
- if (faceDetectionAttempts < MAX_FACE_DETECTION_ATTEMPTS) {
11793
- faceDetectionAttempts++;
11794
- setTimeout(tryCapture, 150);
11795
- return;
11796
- }
11797
- cleanup();
11798
- reject(new NeoFaceError("Câmera não está pronta", ErrorType.CAMERA_ERROR));
11799
- return;
11800
- }
11801
- canvas.width = video.videoWidth;
11802
- canvas.height = video.videoHeight;
11803
- const ctx = canvas.getContext("2d");
11804
- if (!ctx) {
11805
- cleanup();
11806
- reject(new Error("Failed to get canvas context"));
11807
- return;
11808
- }
11809
- const detectFace2 = async () => {
11810
- if (!modelsLoaded) return true;
11811
- try {
11812
- const detection = await faceapi.detectSingleFace(
11813
- video,
11814
- new faceapi.TinyFaceDetectorOptions({ inputSize: 160, scoreThreshold: 0.3 })
11815
- );
11816
- return !!detection;
11817
- } catch {
11818
- return true;
11819
- }
11820
- };
11821
- detectFace2().then((hasFace) => {
11822
- if (!hasFace && faceDetectionAttempts < MAX_FACE_DETECTION_ATTEMPTS) {
11823
- faceDetectionAttempts++;
11824
- setTimeout(tryCapture, 150);
11825
- return;
11826
- }
11827
- ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
11828
- canvas.toBlob(
11829
- (blob) => {
11830
- cleanup();
11831
- if (blob) {
11832
- resolve(blob);
11833
- } else {
11834
- reject(new Error("Failed to capture image"));
11835
- }
11836
- },
11837
- "image/jpeg",
11838
- 0.85
11839
- );
11840
- });
11841
- };
11842
- const playPromise = video.play();
11843
- if (playPromise !== void 0) {
11844
- playPromise.then(() => {
11845
- if (video.videoWidth && video.videoHeight) {
11846
- setTimeout(tryCapture, CAMERA_READY_DELAY);
11847
- } else {
11848
- video.onloadedmetadata = () => setTimeout(tryCapture, CAMERA_READY_DELAY);
11849
- setTimeout(() => {
11850
- if (video.videoWidth && video.videoHeight) {
11851
- tryCapture();
11852
- }
11853
- }, 1500);
11854
- }
11855
- }).catch((err) => {
11856
- cleanup();
11857
- reject(
11858
- new NeoFaceError(
11859
- `Erro ao reproduzir câmera: ${err.message}`,
11860
- ErrorType.CAMERA_ERROR
11861
- )
11862
- );
11863
- });
11864
- } else {
11865
- video.onloadedmetadata = () => setTimeout(tryCapture, CAMERA_READY_DELAY);
12387
+
12388
+ .neofaceid-modal-header h2 {
12389
+ margin: 0;
12390
+ font-size: 24px;
12391
+ font-weight: 600;
12392
+ color: #333;
11866
12393
  }
11867
- video.onerror = () => {
11868
- cleanup();
11869
- reject(new NeoFaceError("Erro ao acessar câmera", ErrorType.CAMERA_ERROR));
11870
- };
11871
- }).catch((error) => {
11872
- cleanup();
11873
- reject(
11874
- new NeoFaceError(
11875
- `Não foi possível acessar a câmera: ${error.message}`,
11876
- ErrorType.CAMERA_ERROR
11877
- )
11878
- );
11879
- });
11880
- });
11881
- }
11882
- async function detectFaceWithPosition(video, stream, guideModal, maxTime = FACE_DETECTION_TIME, interval = DETECTION_INTERVAL) {
11883
- return new Promise((resolve) => {
11884
- let attempts = 0;
11885
- const maxAttempts = Math.ceil(maxTime / interval);
11886
- let timeoutId = null;
11887
- let isResolved = false;
11888
- let centeredDurationMs = 0;
11889
- const finish = (success) => {
11890
- if (isResolved) return;
11891
- isResolved = true;
11892
- if (timeoutId) clearTimeout(timeoutId);
11893
- resolve(success);
11894
- };
11895
- const checkFace = async () => {
11896
- if (isResolved) return;
11897
- attempts++;
11898
- if (!modelsLoaded) {
11899
- guideModal.update(stream, "searching", "Carregando modelos de detecção...");
11900
- if (attempts >= maxAttempts) {
11901
- finish(false);
11902
- return;
11903
- }
11904
- timeoutId = window.setTimeout(checkFace, interval);
11905
- return;
12394
+
12395
+ .neofaceid-close-btn {
12396
+ background: none;
12397
+ border: none;
12398
+ font-size: 22px;
12399
+ cursor: pointer;
12400
+ color: #666;
12401
+ padding: 0;
12402
+ min-width: 44px;
12403
+ min-height: 44px;
12404
+ display: flex;
12405
+ align-items: center;
12406
+ justify-content: center;
12407
+ border-radius: 22px;
12408
+ transition: background-color 0.2s;
11906
12409
  }
11907
- try {
11908
- const detection = await faceapi.detectSingleFace(
11909
- video,
11910
- new faceapi.TinyFaceDetectorOptions({ inputSize: 160, scoreThreshold: 0.3 })
11911
- );
11912
- const { faceState, instruction } = evaluateFacePosition(
11913
- detection ? detection.box : null,
11914
- video.videoWidth,
11915
- video.videoHeight
11916
- );
11917
- guideModal.update(stream, faceState, instruction);
11918
- if (faceState === "centered") {
11919
- centeredDurationMs += interval;
11920
- if (centeredDurationMs >= 1e3) {
11921
- console.log("✅ Rosto mantido centralizado por 1s consecutivo");
11922
- finish(true);
11923
- return;
11924
- }
11925
- } else {
11926
- centeredDurationMs = 0;
11927
- }
11928
- if (attempts >= maxAttempts) {
11929
- finish(false);
11930
- return;
11931
- }
11932
- timeoutId = window.setTimeout(checkFace, interval);
11933
- } catch (error) {
11934
- console.error("❌ Erro na detecção facial:", error);
11935
- if (attempts >= maxAttempts) {
11936
- finish(false);
11937
- return;
11938
- }
11939
- timeoutId = window.setTimeout(checkFace, interval);
12410
+ .neofaceid-close-btn:focus-visible {
12411
+ outline: 2px solid var(--nfid-focus-ring);
12412
+ outline-offset: 2px;
12413
+ }
12414
+
12415
+ .neofaceid-close-btn:hover {
12416
+ background-color: #f0f0f0;
12417
+ }
12418
+
12419
+ .neofaceid-modal-body {
12420
+ padding: 20px;
12421
+ }
12422
+
12423
+ .neofaceid-subtitle {
12424
+ margin: 0 0 20px 0;
12425
+ color: #666;
12426
+ font-size: 16px;
12427
+ line-height: 1.4;
12428
+ }
12429
+
12430
+ .neofaceid-camera-container {
12431
+ position: relative;
12432
+ background: #000;
12433
+ border-radius: 8px;
12434
+ overflow: hidden;
12435
+ aspect-ratio: 4/3;
12436
+ margin-bottom: 20px;
11940
12437
  }
11941
- };
11942
- window.setTimeout(() => {
11943
- if (!isResolved) {
11944
- finish(false);
12438
+
12439
+ .neofaceid-video {
12440
+ width: 100%;
12441
+ height: 100%;
12442
+ object-fit: cover;
11945
12443
  }
11946
- }, maxTime + 500);
11947
- checkFace();
11948
- });
11949
- }
11950
- async function attemptLogin(applicationToken, fastMode = false, onCancel) {
11951
- const guideModal = showCameraOvalGuideModal({
11952
- stream: null,
11953
- faceState: "searching",
11954
- instruction: "Iniciando câmera...",
11955
- onCancel
11956
- });
11957
- const video = document.createElement("video");
11958
- video.style.position = "fixed";
11959
- video.style.top = "-9999px";
11960
- video.style.left = "-9999px";
11961
- video.style.width = "1px";
11962
- video.style.height = "1px";
11963
- video.style.opacity = "0";
11964
- video.muted = true;
11965
- video.playsInline = true;
11966
- video.setAttribute("autoplay", "");
11967
- video.setAttribute("playsinline", "");
11968
- document.body.appendChild(video);
11969
- let stream = null;
11970
- try {
11971
- try {
11972
- stream = await navigator.mediaDevices.getUserMedia({
11973
- video: {
11974
- width: { ideal: 640 },
11975
- height: { ideal: 480 },
11976
- facingMode: "user"
11977
- },
11978
- audio: false
11979
- });
11980
- } catch {
11981
- stream = await navigator.mediaDevices.getUserMedia({
11982
- video: { facingMode: "user" },
11983
- audio: false
11984
- });
11985
- }
11986
- video.srcObject = stream;
11987
- guideModal.update(stream, "searching", "Centralize o rosto");
11988
- await new Promise((resolve) => {
11989
- if (video.readyState >= 1) {
11990
- resolve();
11991
- } else {
11992
- const handler = () => {
11993
- video.removeEventListener("loadedmetadata", handler);
11994
- resolve();
11995
- };
11996
- video.addEventListener("loadedmetadata", handler);
11997
- setTimeout(resolve, 2e3);
12444
+
12445
+ .neofaceid-canvas {
12446
+ position: absolute;
12447
+ top: 0;
12448
+ left: 0;
11998
12449
  }
11999
- });
12000
- await video.play();
12001
- await new Promise((resolve) => {
12002
- if (video.videoWidth && video.videoHeight) {
12003
- resolve();
12004
- } else {
12005
- const checkSize = () => {
12006
- if (video.videoWidth && video.videoHeight) {
12007
- resolve();
12008
- } else {
12009
- setTimeout(checkSize, 100);
12010
- }
12011
- };
12012
- setTimeout(resolve, 3e3);
12013
- checkSize();
12450
+
12451
+ .neofaceid-overlay {
12452
+ position: absolute;
12453
+ top: 0;
12454
+ left: 0;
12455
+ width: 100%;
12456
+ height: 100%;
12457
+ display: flex;
12458
+ align-items: center;
12459
+ justify-content: center;
12014
12460
  }
12015
- });
12016
- await new Promise((resolve) => setTimeout(resolve, CAMERA_STABILITY_DELAY));
12017
- let faceDetected = true;
12018
- if (!fastMode) {
12019
- faceDetected = await detectFaceWithPosition(
12020
- video,
12021
- stream,
12022
- guideModal,
12023
- FACE_DETECTION_TIME,
12024
- DETECTION_INTERVAL
12025
- );
12026
- }
12027
- if (!faceDetected) {
12028
- guideModal.close();
12029
- if (stream) {
12030
- stream.getTracks().forEach((track) => track.stop());
12461
+
12462
+ .neofaceid-frame {
12463
+ width: 200px;
12464
+ height: 200px;
12465
+ border: 3px solid #0E9F6E;
12466
+ border-radius: 50%;
12467
+ box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.3);
12468
+ animation: pulse 2s infinite;
12031
12469
  }
12032
- if (video.parentElement) {
12033
- video.parentElement.removeChild(video);
12470
+
12471
+ .neofaceid-seal-row {
12472
+ display: flex;
12473
+ justify-content: center;
12474
+ padding: 12px 20px 16px;
12475
+ border-top: 1px solid var(--nfid-line, #E6ECF4);
12034
12476
  }
12035
- throw new NeoFaceError(
12036
- "Nenhum rosto centralizado detectado. Por favor, tente novamente.",
12037
- ErrorType.VALIDATION_ERROR
12038
- );
12039
- }
12040
- guideModal.update(stream, "centered", "Capturando imagem...");
12041
- const canvas = document.createElement("canvas");
12042
- canvas.width = video.videoWidth;
12043
- canvas.height = video.videoHeight;
12044
- const ctx = canvas.getContext("2d");
12045
- if (!ctx) {
12046
- guideModal.close();
12047
- if (stream) {
12048
- stream.getTracks().forEach((track) => track.stop());
12477
+ .neofaceid-seal {
12478
+ font-size: 10px;
12479
+ font-weight: 500;
12480
+ letter-spacing: 0.06em;
12481
+ text-transform: uppercase;
12482
+ color: var(--nfid-text-subtle, #617489);
12049
12483
  }
12050
- if (video.parentElement) {
12051
- video.parentElement.removeChild(video);
12484
+
12485
+ .neofaceid-status {
12486
+ text-align: center;
12487
+ margin-bottom: 20px;
12052
12488
  }
12053
- throw new NeoFaceError("Erro ao capturar imagem", ErrorType.CAPTURE_ERROR);
12054
- }
12055
- ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
12056
- const imageBlob = await new Promise((resolve, reject) => {
12057
- canvas.toBlob(
12058
- (blob) => {
12059
- canvas.remove();
12060
- if (blob) {
12061
- resolve(blob);
12062
- } else {
12063
- reject(new Error("Failed to capture image"));
12064
- }
12065
- },
12066
- "image/jpeg",
12067
- 0.85
12068
- );
12069
- });
12070
- guideModal.update(stream, "centered", "Verificando com o servidor...");
12071
- const result = await loginWithBiometric(imageBlob, applicationToken);
12072
- guideModal.close();
12073
- if (stream) {
12074
- stream.getTracks().forEach((track) => track.stop());
12075
- }
12076
- if (video.parentElement) {
12077
- video.parentElement.removeChild(video);
12078
- }
12079
- if (!result.success) {
12080
- throw new NeoFaceError("Login falhou", ErrorType.LOGIN_FAILED);
12081
- }
12082
- return result;
12083
- } catch (error) {
12084
- guideModal.close();
12085
- if (stream) {
12086
- stream.getTracks().forEach((track) => track.stop());
12087
- }
12088
- if (video.parentElement) {
12089
- video.parentElement.removeChild(video);
12090
- }
12091
- if (error instanceof NeoFaceError) {
12092
- throw error;
12093
- }
12094
- throw new NeoFaceError(
12095
- error instanceof Error ? error.message : "Erro desconhecido",
12096
- ErrorType.UNKNOWN
12097
- );
12098
- }
12099
- }
12100
- async function executeBiometricLoginFlow(options) {
12101
- const {
12102
- applicationToken,
12103
- onSuccess,
12104
- onError,
12105
- onFallbackRequest,
12106
- onCancel,
12107
- fastMode = false
12108
- } = options;
12109
- const fallbackPrompt = new FallbackPrompt();
12110
- let attempts = 0;
12111
- let lastError;
12112
- if (!modelsLoaded && !fastMode) {
12113
- preloadFaceDetectionModels().catch(() => {
12114
- });
12115
- }
12116
- const tryLogin = async () => {
12117
- try {
12118
- attempts++;
12119
- const result = await attemptLogin(applicationToken, fastMode, onCancel);
12120
- onSuccess(result);
12121
- } catch (error) {
12122
- lastError = error instanceof NeoFaceError ? error : new NeoFaceError(
12123
- error instanceof Error ? error.message : "Erro desconhecido",
12124
- ErrorType.UNKNOWN
12125
- );
12126
- if (attempts < MAX_ATTEMPTS$1) {
12127
- setTimeout(() => {
12128
- tryLogin();
12129
- }, RETRY_DELAY);
12130
- } else {
12131
- setTimeout(() => {
12132
- fallbackPrompt.show(
12133
- lastError,
12134
- () => {
12135
- attempts = 0;
12136
- setTimeout(() => tryLogin(), RETRY_DELAY);
12137
- },
12138
- () => {
12139
- if (onFallbackRequest) {
12140
- onFallbackRequest();
12141
- } else {
12142
- onError(
12143
- new NeoFaceError(
12144
- "Por favor, use email e senha para fazer login",
12145
- ErrorType.LOGIN_FAILED
12146
- )
12147
- );
12148
- }
12149
- },
12150
- onCancel,
12151
- applicationToken
12152
- );
12153
- }, 500);
12489
+
12490
+ .neofaceid-status-text {
12491
+ margin: 0;
12492
+ color: #666;
12493
+ font-size: 16px;
12494
+ }
12495
+
12496
+ .neofaceid-modal-footer {
12497
+ padding: 20px;
12498
+ border-top: 1px solid #e0e0e0;
12499
+ display: flex;
12500
+ gap: 12px;
12501
+ justify-content: flex-end;
12154
12502
  }
12155
- }
12156
- };
12157
- tryLogin();
12503
+
12504
+ .neofaceid-cancel-btn,
12505
+ .neofaceid-capture-btn {
12506
+ padding: 12px 24px;
12507
+ border: none;
12508
+ border-radius: 6px;
12509
+ font-size: 16px;
12510
+ font-weight: 500;
12511
+ cursor: pointer;
12512
+ transition: all 0.2s;
12513
+ }
12514
+
12515
+ .neofaceid-cancel-btn {
12516
+ background: #f5f5f5;
12517
+ color: #666;
12518
+ }
12519
+
12520
+ .neofaceid-cancel-btn:hover {
12521
+ background: #e0e0e0;
12522
+ }
12523
+
12524
+ .neofaceid-capture-btn {
12525
+ background: #0E9F6E;
12526
+ color: white;
12527
+ }
12528
+
12529
+ .neofaceid-capture-btn:hover:not(:disabled) {
12530
+ background: #45a049;
12531
+ }
12532
+
12533
+ .neofaceid-capture-btn:disabled {
12534
+ background: #ccc;
12535
+ cursor: not-allowed;
12536
+ }
12537
+
12538
+ @keyframes pulse {
12539
+ 0% { box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.3); }
12540
+ 50% { box-shadow: 0 0 0 10px rgba(76, 175, 80, 0.1); }
12541
+ 100% { box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.3); }
12542
+ }
12543
+
12544
+ @media (max-width: 640px) {
12545
+ .neofaceid-modal-overlay {
12546
+ padding: 10px;
12547
+ }
12548
+
12549
+ .neofaceid-modal-header,
12550
+ .neofaceid-modal-body,
12551
+ .neofaceid-modal-footer {
12552
+ padding: 15px;
12553
+ }
12554
+
12555
+ .neofaceid-frame {
12556
+ width: 150px;
12557
+ height: 150px;
12558
+ }
12559
+ }
12560
+ `);
12561
+ }
12158
12562
  }
12159
- const biometricLoginFlow = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
12160
- __proto__: null,
12161
- captureFaceSilently,
12162
- executeBiometricLoginFlow,
12163
- preloadFaceDetectionModels
12164
- }, Symbol.toStringTag, { value: "Module" }));
12165
12563
  const CONSENT_DENIED_MESSAGE$1 = "Consentimento recusado pelo usuário.";
12166
12564
  function openPasswordFallback(options) {
12167
12565
  if (!getAutoFallback().toPassword) {
@@ -12284,175 +12682,6 @@ async function startAutoLogin(options) {
12284
12682
  );
12285
12683
  }
12286
12684
  }
12287
- async function detectBiometricType(imageData) {
12288
- try {
12289
- const img = await loadImageFromBase64(imageData);
12290
- const faceResult = await detectFace(img);
12291
- if (faceResult.confidence > 0.7) {
12292
- return {
12293
- type: "face",
12294
- confidence: faceResult.confidence,
12295
- details: faceResult.details
12296
- };
12297
- }
12298
- const handResult = await detectHand(img);
12299
- if (handResult.confidence > 0.6) {
12300
- return {
12301
- type: "hand",
12302
- confidence: handResult.confidence,
12303
- details: handResult.details
12304
- };
12305
- }
12306
- return {
12307
- type: "unknown",
12308
- confidence: Math.max(faceResult.confidence, handResult.confidence)
12309
- };
12310
- } catch (error) {
12311
- console.warn("Erro na detecção biométrica:", error);
12312
- return {
12313
- type: "face",
12314
- confidence: 0.5
12315
- };
12316
- }
12317
- }
12318
- async function detectFace(img) {
12319
- try {
12320
- if (typeof window !== "undefined" && window.faceapi) {
12321
- const { faceapi: faceapi2 } = window;
12322
- await loadFaceApiModels();
12323
- const tinyOptions = new faceapi2.TinyFaceDetectorOptions({
12324
- inputSize: 320,
12325
- scoreThreshold: 0.5
12326
- });
12327
- const detections = await faceapi2.detectAllFaces(img, tinyOptions).withFaceLandmarks();
12328
- if (detections && detections.length > 0) {
12329
- const bestDetection = detections.reduce(
12330
- (best, current) => current.detection.score > best.detection.score ? current : best
12331
- );
12332
- return {
12333
- confidence: bestDetection.detection.score,
12334
- details: {
12335
- faces: detections.length,
12336
- landmarks: bestDetection.landmarks,
12337
- box: bestDetection.detection.box
12338
- }
12339
- };
12340
- }
12341
- }
12342
- return { confidence: 0 };
12343
- } catch (error) {
12344
- console.warn("Erro na detecção facial:", error);
12345
- return { confidence: 0 };
12346
- }
12347
- }
12348
- async function detectHand(img) {
12349
- try {
12350
- const canvas = document.createElement("canvas");
12351
- const ctx = canvas.getContext("2d");
12352
- if (!ctx) {
12353
- return { confidence: 0 };
12354
- }
12355
- canvas.width = img.width;
12356
- canvas.height = img.height;
12357
- ctx.drawImage(img, 0, 0);
12358
- const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
12359
- const handFeatures = analyzeHandFeatures(imageData);
12360
- return {
12361
- confidence: handFeatures.confidence,
12362
- details: handFeatures
12363
- };
12364
- } catch (error) {
12365
- console.warn("Erro na detecção de mão:", error);
12366
- return { confidence: 0 };
12367
- }
12368
- }
12369
- function analyzeHandFeatures(imageData) {
12370
- const { data, width, height } = imageData;
12371
- let skinPixels = 0;
12372
- let totalPixels = 0;
12373
- for (let i = 0; i < data.length; i += 4) {
12374
- const r = data[i];
12375
- const g = data[i + 1];
12376
- const b = data[i + 2];
12377
- if (isSkinColor(r, g, b)) {
12378
- skinPixels++;
12379
- }
12380
- totalPixels++;
12381
- }
12382
- const skinRatio = skinPixels / totalPixels;
12383
- const aspectRatio = width / height;
12384
- const isHandAspectRatio = aspectRatio > 0.6 && aspectRatio < 1.8;
12385
- let confidence = 0;
12386
- if (skinRatio > 0.15 && skinRatio < 0.7) {
12387
- confidence += 0.4 * (skinRatio / 0.7);
12388
- }
12389
- if (isHandAspectRatio) {
12390
- confidence += 0.3;
12391
- }
12392
- const imageSize = width * height;
12393
- if (imageSize > 1e4 && imageSize < 5e5) {
12394
- confidence += 0.3;
12395
- }
12396
- return {
12397
- confidence: Math.min(confidence, 0.8),
12398
- // Máximo 80% para detecção básica
12399
- skinRatio,
12400
- aspectRatio,
12401
- imageSize,
12402
- skinPixels,
12403
- totalPixels
12404
- };
12405
- }
12406
- function isSkinColor(r, g, b) {
12407
- const y = 0.299 * r + 0.587 * g + 0.114 * b;
12408
- const cb = -0.169 * r - 0.331 * g + 0.5 * b + 128;
12409
- const cr = 0.5 * r - 0.419 * g - 0.081 * b + 128;
12410
- return y > 80 && y < 255 && cb > 85 && cb < 135 && cr > 135 && cr < 180;
12411
- }
12412
- function loadImageFromBase64(base64Data) {
12413
- return new Promise((resolve, reject) => {
12414
- const img = new Image();
12415
- img.onload = () => resolve(img);
12416
- img.onerror = () => reject(new Error("Erro ao carregar imagem"));
12417
- const dataUrl = base64Data.startsWith("data:") ? base64Data : `data:image/jpeg;base64,${base64Data}`;
12418
- img.src = dataUrl;
12419
- });
12420
- }
12421
- async function loadFaceApiModels() {
12422
- if (typeof window === "undefined" || !window.faceapi) {
12423
- return;
12424
- }
12425
- const { faceapi: faceapi2 } = window;
12426
- if (faceapi2.nets.tinyFaceDetector.isLoaded) {
12427
- return;
12428
- }
12429
- try {
12430
- await Promise.all([
12431
- faceapi2.nets.tinyFaceDetector.loadFromUri("/models"),
12432
- faceapi2.nets.faceLandmark68Net.loadFromUri("/models")
12433
- ]);
12434
- } catch (error) {
12435
- console.warn("Não foi possível carregar modelos do face-api.js:", error);
12436
- }
12437
- }
12438
- async function initializeBiometricDetection() {
12439
- try {
12440
- if (typeof window !== "undefined") {
12441
- await loadFaceApiModels();
12442
- }
12443
- } catch (error) {
12444
- console.warn("Inicialização da detecção biométrica com limitações:", error);
12445
- }
12446
- }
12447
- function isAdvancedDetectionAvailable() {
12448
- return typeof window !== "undefined" && window.faceapi && window.faceapi.nets.tinyFaceDetector.isLoaded;
12449
- }
12450
- const biometricDetection = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
12451
- __proto__: null,
12452
- detectBiometricType,
12453
- initializeBiometricDetection,
12454
- isAdvancedDetectionAvailable
12455
- }, Symbol.toStringTag, { value: "Module" }));
12456
12685
  let cachedSession = null;
12457
12686
  function getCachedCaptureSession() {
12458
12687
  return cachedSession;
@@ -12980,342 +13209,103 @@ const startOnboarding = async (options) => {
12980
13209
  onError(new NeoFaceError(message, ErrorType.UNKNOWN));
12981
13210
  }
12982
13211
  };
12983
- class NeoFaceID {
12984
- /**
12985
- * Creates a new NeoFaceID instance
12986
- * @param config Configuration object
12987
- * @throws NeoFaceError if signature format is invalid
12988
- */
12989
- constructor(config2) {
12990
- __publicField(this, "appToken");
12991
- __publicField(this, "signature");
12992
- __publicField(this, "sessionData");
12993
- this.appToken = config2.appToken;
12994
- this.signature = config2.signature;
12995
- this.sessionData = config2.sessionData;
12996
- if (this.signature && !this.validateSignatureFormat(this.signature)) {
12997
- throw new NeoFaceError(
12998
- "Invalid signature format. Expected HMAC-SHA256 hex string (minimum 64 characters)",
12999
- ErrorType.VALIDATION_ERROR
13000
- );
13001
- }
13002
- if (this.sessionData) {
13003
- if (!this.sessionData.email || !this.sessionData.cpf || !this.sessionData.sessionId) {
13004
- throw new NeoFaceError(
13005
- "Session data must include email, cpf, and sessionId",
13006
- ErrorType.VALIDATION_ERROR
13007
- );
13008
- }
13009
- }
13010
- }
13011
- /**
13012
- * Validates the format of a signature
13013
- * HMAC-SHA256 produces a 64-character hexadecimal string
13014
- * @param signature The signature to validate
13015
- * @returns true if valid, false otherwise
13016
- */
13017
- validateSignatureFormat(signature) {
13018
- return /^[a-f0-9]{64,}$/i.test(signature);
13019
- }
13020
- /**
13021
- * Captures multiple face frames for biometric recognition
13022
- * @param options Capture options
13023
- * @returns Promise that resolves to an array of image blobs
13024
- * @throws NeoFaceError if capture fails
13025
- */
13026
- async captureFaceFrames(options) {
13027
- const { numFrames, livenessCheck } = options;
13028
- if (numFrames < 1 || numFrames > 10) {
13029
- throw new NeoFaceError(
13030
- "Number of frames must be between 1 and 10",
13031
- ErrorType.VALIDATION_ERROR
13032
- );
13033
- }
13034
- const frames = [];
13035
- try {
13036
- const { captureFaceSilently: captureFaceSilently2 } = await Promise.resolve().then(() => biometricLoginFlow);
13037
- for (let i = 0; i < numFrames; i++) {
13038
- const frame = await captureFaceSilently2();
13039
- frames.push(frame);
13040
- if (livenessCheck && i < numFrames - 1) {
13041
- await new Promise((resolve) => setTimeout(resolve, 300));
13042
- }
13043
- }
13044
- return frames;
13045
- } catch (error) {
13046
- if (error instanceof NeoFaceError) {
13047
- throw error;
13048
- }
13049
- throw new NeoFaceError(
13050
- `Failed to capture face frames: ${error instanceof Error ? error.message : "Unknown error"}`,
13051
- ErrorType.CAPTURE_ERROR
13052
- );
13053
- }
13212
+ async function proofOfLife(options = {}) {
13213
+ const {
13214
+ videoDurationMs = 3e3,
13215
+ maxPollingAttempts = 60,
13216
+ pollingIntervalMs = 1e3,
13217
+ onRecordingProgress,
13218
+ onTaskStatusChange
13219
+ } = options;
13220
+ const applicationToken = options.applicationToken ?? getApplicationToken();
13221
+ if (!applicationToken) {
13222
+ throw new NeoFaceError(
13223
+ "proofOfLife exige applicationToken — passe nas opções ou em init({ applicationToken }).",
13224
+ ErrorType.INVALID_TOKEN
13225
+ );
13054
13226
  }
13055
- /**
13056
- * Performs login recognition using biometric data
13057
- *
13058
- * This method is designed for external integrations that require
13059
- * signature validation and session management.
13060
- *
13061
- * @param options Recognition options
13062
- * @returns Promise that resolves to recognition result
13063
- * @throws NeoFaceError if recognition fails
13064
- *
13065
- * @example
13066
- * ```typescript
13067
- * const result = await sdk.loginRecognition({
13068
- * biometricData: await sdk.captureFaceFrames({
13069
- * numFrames: 5,
13070
- * livenessCheck: true
13071
- * }),
13072
- * typeOfIdentification: 'FACE',
13073
- * purpose: 'LOGIN',
13074
- * confidenceThreshold: 0.8
13075
- * });
13076
- *
13077
- * // Result includes signature and sessionId for callback validation
13078
- * console.log(result.signature, result.sessionId);
13079
- * ```
13080
- */
13081
- async loginRecognition(options) {
13082
- const { biometricData, purpose, confidenceThreshold = 0.8 } = options;
13083
- const imageBlobs = Array.isArray(biometricData) ? biometricData : [biometricData];
13084
- if (imageBlobs.length === 0) {
13085
- throw new NeoFaceError(
13086
- "At least one biometric data frame is required",
13087
- ErrorType.VALIDATION_ERROR
13088
- );
13089
- }
13090
- const primaryImage = imageBlobs[0];
13091
- try {
13092
- const result = await recognizeByPurpose(
13093
- primaryImage,
13094
- this.appToken,
13095
- purpose,
13096
- confidenceThreshold,
13097
- this.signature,
13098
- this.sessionData
13099
- );
13100
- if (!result.success) {
13101
- throw new NeoFaceError("Recognition failed", ErrorType.RECOGNITION_FAILED);
13102
- }
13103
- if (!result.personName || !result.email || !result.cpf) {
13104
- throw new NeoFaceError(
13105
- "Incomplete recognition result: missing personName, email, or cpf",
13106
- ErrorType.RECOGNITION_FAILED
13107
- );
13108
- }
13227
+ try {
13228
+ const videoBlob = await recordFaceVideo({
13229
+ durationMs: videoDurationMs,
13230
+ onProgress: onRecordingProgress
13231
+ });
13232
+ const videoBase64 = await videoToBase64(videoBlob);
13233
+ const started = await startProofOfLife(videoBase64, applicationToken);
13234
+ if (onTaskStatusChange) onTaskStatusChange("processing", 0);
13235
+ const taskResult = await pollTaskStatus(started.taskId, applicationToken, {
13236
+ maxAttempts: maxPollingAttempts,
13237
+ intervalMs: pollingIntervalMs,
13238
+ onStatusChange: onTaskStatusChange
13239
+ });
13240
+ const base = {
13241
+ isLive: (taskResult == null ? void 0 : taskResult.is_live) || false,
13242
+ livenessScore: (taskResult == null ? void 0 : taskResult.liveness_score) || 0,
13243
+ faceRecognitionScore: (taskResult == null ? void 0 : taskResult.face_recognition_score) || 0,
13244
+ combinedConfidence: (taskResult == null ? void 0 : taskResult.combined_confidence) || 0,
13245
+ personId: taskResult == null ? void 0 : taskResult.person_id,
13246
+ personalData: (taskResult == null ? void 0 : taskResult.personal_data) || [],
13247
+ processingTime: (taskResult == null ? void 0 : taskResult.processing_time) || 0,
13248
+ historyId: started.historyId,
13249
+ taskId: started.taskId
13250
+ };
13251
+ if (!taskResult || !taskResult.success) {
13109
13252
  return {
13110
- success: true,
13111
- personName: result.personName,
13112
- email: result.email,
13113
- cpf: result.cpf,
13114
- signature: result.signature,
13115
- sessionId: result.sessionId,
13116
- confidenceScore: result.confidenceScore,
13117
- accessToken: result.accessToken
13253
+ ...base,
13254
+ success: false,
13255
+ error: (taskResult == null ? void 0 : taskResult.error) || "Verification failed",
13256
+ message: (taskResult == null ? void 0 : taskResult.message) || "Proof of life verification failed"
13118
13257
  };
13119
- } catch (error) {
13120
- if (error instanceof NeoFaceError) {
13121
- throw error;
13122
- }
13123
- throw new NeoFaceError(
13124
- `Login recognition failed: ${error instanceof Error ? error.message : "Unknown error"}`,
13125
- ErrorType.RECOGNITION_FAILED
13126
- );
13127
- }
13128
- }
13129
- /**
13130
- * Register a new application for a consumer
13131
- *
13132
- * This method allows authenticated users to register new applications
13133
- * that will receive their own app_token for API access.
13134
- *
13135
- * @param jwtToken JWT authentication token from logged user
13136
- * @param consumerId Consumer ID (UUID) who will own the application
13137
- * @param applicationData Application registration data
13138
- * @returns Promise with registration result including app_token
13139
- * @throws NeoFaceError if registration fails
13140
- *
13141
- * @example
13142
- * ```typescript
13143
- * const sdk = new NeoFaceID({
13144
- * appToken: 'your-application-token'
13145
- * });
13146
- *
13147
- * const result = await sdk.registerApplication(
13148
- * userJwtToken,
13149
- * consumerUuid,
13150
- * {
13151
- * applicationName: 'My New App',
13152
- * domain: 'example.com',
13153
- * acceptOnlyEmailWithSameDomain: true
13154
- * }
13155
- * );
13156
- *
13157
- * // Use the generated app_token for the new application
13158
- * console.log('New App Token:', result.application.app_token);
13159
- * ```
13160
- */
13161
- async registerApplication(jwtToken, consumerId, applicationData) {
13162
- try {
13163
- const result = await registerApplication(jwtToken, consumerId, applicationData);
13164
- return result;
13165
- } catch (error) {
13166
- if (error instanceof NeoFaceError) {
13167
- throw error;
13168
- }
13169
- throw new NeoFaceError(
13170
- `Application registration failed: ${error instanceof Error ? error.message : "Unknown error"}`,
13171
- ErrorType.VALIDATION_ERROR
13172
- );
13173
13258
  }
13259
+ return { ...base, success: true, message: "Proof of life verification successful" };
13260
+ } catch (error) {
13261
+ if (error instanceof NeoFaceError) throw error;
13262
+ throw new NeoFaceError(
13263
+ `Proof of life failed: ${error instanceof Error ? error.message : "Unknown error"}`,
13264
+ ErrorType.RECOGNITION_FAILED
13265
+ );
13174
13266
  }
13175
- /**
13176
- * Performs Proof of Life verification
13177
- *
13178
- * This method records a video from the camera, sends it to the backend
13179
- * for liveness detection and face recognition, and returns the result
13180
- * with personal data filtered by purpose.
13181
- *
13182
- * The process is asynchronous:
13183
- * 1. Records video from the camera (default 3 seconds)
13184
- * 2. Converts video to base64 and sends to backend
13185
- * 3. Backend processes liveness detection and face recognition
13186
- * 4. Polls for task completion
13187
- * 5. Returns personal data of the identified person
13188
- *
13189
- * @param options Configuration options for the proof of life process
13190
- * @returns Promise that resolves to ProofOfLifeResult
13191
- * @throws NeoFaceError if verification fails
13192
- *
13193
- * @example
13194
- * ```typescript
13195
- * const sdk = new NeoFaceID({
13196
- * appToken: 'your-application-token'
13197
- * });
13198
- *
13199
- * const result = await sdk.proofOfLife({
13200
- * videoDurationMs: 3000, // 3 seconds
13201
- * onRecordingProgress: (progress) => {
13202
- * console.log(`Recording: ${progress}%`);
13203
- * },
13204
- * onTaskStatusChange: (status, progress) => {
13205
- * console.log(`Status: ${status}, Progress: ${progress}%`);
13206
- * }
13207
- * });
13208
- *
13209
- * if (result.success && result.isLive) {
13210
- * console.log('Person verified:', result.personalData);
13211
- * console.log('Liveness score:', result.livenessScore);
13212
- * console.log('Face recognition score:', result.faceRecognitionScore);
13213
- * }
13214
- * ```
13215
- */
13216
- /**
13217
- * Registers document images for an already-registered donor person.
13218
- *
13219
- * Opens the document capture UI (front + optional back), then submits
13220
- * the images to the backend for extraction via DocExt.
13221
- * The backend processes this asynchronously — use the returned `taskId`
13222
- * to poll status if needed.
13223
- *
13224
- * @param personId UUID of the donor's person record
13225
- * @param jwtToken JWT Bearer token of the authenticated donor
13226
- * @param options Optional capture configuration
13227
- * @returns Promise with task_id and processing status
13228
- * @throws NeoFaceError if capture is cancelled, validation fails, or API call fails
13229
- *
13230
- * @example
13231
- * ```typescript
13232
- * const sdk = new NeoFaceID({ appToken: 'your-token' });
13233
- *
13234
- * const result = await sdk.registerDocumentByImage(personId, userJwtToken);
13235
- * console.log('Task ID:', result.taskId); // poll for completion
13236
- * ```
13237
- */
13238
- async registerDocumentByImage(personId, jwtToken, options = {}) {
13239
- const { DocumentCaptureModal: DocumentCaptureModal2 } = await Promise.resolve().then(() => DocumentCaptureModal$1);
13240
- const modal = new DocumentCaptureModal2({
13241
- useBackCamera: options.useBackCamera ?? true,
13242
- preSelectedDocument: options.preSelectedDocument
13243
- });
13244
- const captureResult = await modal.open();
13245
- if (!captureResult.success || !captureResult.frontImage) {
13246
- throw new NeoFaceError(
13247
- captureResult.error === "cancelled" ? "Document capture was cancelled by the user" : `Document capture failed: ${captureResult.error ?? "unknown error"}`,
13248
- ErrorType.CAPTURE_ERROR
13249
- );
13250
- }
13251
- const images = [captureResult.frontImage];
13252
- if (captureResult.backImage) {
13253
- images.push(captureResult.backImage);
13254
- }
13255
- return registerDocumentByImage(personId, jwtToken, images);
13267
+ }
13268
+ async function captureFaceFrames(options) {
13269
+ const { numFrames, livenessCheck } = options;
13270
+ if (numFrames < 1 || numFrames > 10) {
13271
+ throw new NeoFaceError("Number of frames must be between 1 and 10", ErrorType.VALIDATION_ERROR);
13256
13272
  }
13257
- async proofOfLife(options = {}) {
13258
- const {
13259
- videoDurationMs = 3e3,
13260
- maxPollingAttempts = 60,
13261
- pollingIntervalMs = 1e3,
13262
- onRecordingProgress,
13263
- onTaskStatusChange
13264
- } = options;
13265
- try {
13266
- const videoBlob = await recordFaceVideo({
13267
- durationMs: videoDurationMs,
13268
- onProgress: onRecordingProgress
13269
- });
13270
- const videoBase64 = await videoToBase64(videoBlob);
13271
- const startResult = await startProofOfLife(videoBase64, this.appToken);
13272
- if (onTaskStatusChange) {
13273
- onTaskStatusChange("processing", 0);
13274
- }
13275
- const taskResult = await pollTaskStatus(startResult.taskId, this.appToken, {
13276
- maxAttempts: maxPollingAttempts,
13277
- intervalMs: pollingIntervalMs,
13278
- onStatusChange: onTaskStatusChange
13279
- });
13280
- if (!taskResult || !taskResult.success) {
13281
- return {
13282
- success: false,
13283
- isLive: (taskResult == null ? void 0 : taskResult.is_live) || false,
13284
- livenessScore: (taskResult == null ? void 0 : taskResult.liveness_score) || 0,
13285
- faceRecognitionScore: (taskResult == null ? void 0 : taskResult.face_recognition_score) || 0,
13286
- combinedConfidence: (taskResult == null ? void 0 : taskResult.combined_confidence) || 0,
13287
- personId: taskResult == null ? void 0 : taskResult.person_id,
13288
- personalData: (taskResult == null ? void 0 : taskResult.personal_data) || [],
13289
- processingTime: (taskResult == null ? void 0 : taskResult.processing_time) || 0,
13290
- historyId: startResult.historyId,
13291
- taskId: startResult.taskId,
13292
- error: (taskResult == null ? void 0 : taskResult.error) || "Verification failed",
13293
- message: (taskResult == null ? void 0 : taskResult.message) || "Proof of life verification failed"
13294
- };
13295
- }
13296
- return {
13297
- success: true,
13298
- isLive: taskResult.is_live || false,
13299
- livenessScore: taskResult.liveness_score || 0,
13300
- faceRecognitionScore: taskResult.face_recognition_score || 0,
13301
- combinedConfidence: taskResult.combined_confidence || 0,
13302
- personId: taskResult.person_id,
13303
- personalData: taskResult.personal_data || [],
13304
- processingTime: taskResult.processing_time || 0,
13305
- historyId: startResult.historyId,
13306
- taskId: startResult.taskId,
13307
- message: "Proof of life verification successful"
13308
- };
13309
- } catch (error) {
13310
- if (error instanceof NeoFaceError) {
13311
- throw error;
13273
+ try {
13274
+ const { captureFaceSilently: captureFaceSilently2 } = await Promise.resolve().then(() => biometricLoginFlow);
13275
+ const frames = [];
13276
+ for (let i = 0; i < numFrames; i += 1) {
13277
+ frames.push(await captureFaceSilently2());
13278
+ if (livenessCheck && i < numFrames - 1) {
13279
+ await new Promise((resolve) => {
13280
+ setTimeout(resolve, 300);
13281
+ });
13312
13282
  }
13313
- throw new NeoFaceError(
13314
- `Proof of life failed: ${error instanceof Error ? error.message : "Unknown error"}`,
13315
- ErrorType.RECOGNITION_FAILED
13316
- );
13317
13283
  }
13284
+ return frames;
13285
+ } catch (error) {
13286
+ if (error instanceof NeoFaceError) throw error;
13287
+ throw new NeoFaceError(
13288
+ `Failed to capture face frames: ${error instanceof Error ? error.message : "Unknown error"}`,
13289
+ ErrorType.CAPTURE_ERROR
13290
+ );
13291
+ }
13292
+ }
13293
+ async function registerDocumentByImage(personId, jwtToken, options = {}) {
13294
+ const { DocumentCaptureModal: DocumentCaptureModal2 } = await Promise.resolve().then(() => DocumentCaptureModal$1);
13295
+ const modal = new DocumentCaptureModal2({
13296
+ useBackCamera: options.useBackCamera ?? true,
13297
+ preSelectedDocument: options.preSelectedDocument
13298
+ });
13299
+ const captured = await modal.open();
13300
+ if (!captured.success || !captured.frontImage) {
13301
+ throw new NeoFaceError(
13302
+ captured.error === "cancelled" ? "Document capture was cancelled by the user" : `Document capture failed: ${captured.error ?? "unknown error"}`,
13303
+ ErrorType.CAPTURE_ERROR
13304
+ );
13318
13305
  }
13306
+ const images = [captured.frontImage];
13307
+ if (captured.backImage) images.push(captured.backImage);
13308
+ return registerDocumentByImage$1(personId, jwtToken, images);
13319
13309
  }
13320
13310
  const PHOTO_INSTRUCTIONS = [
13321
13311
  "Olhe diretamente para a câmera",
@@ -15433,7 +15423,6 @@ function startLivenessCapture(applicationToken, callbacks, options) {
15433
15423
  });
15434
15424
  }
15435
15425
  export {
15436
- BiometricCaptureModal,
15437
15426
  BiometricRegistrationModal,
15438
15427
  BiometricStatusOverlay,
15439
15428
  CameraOvalGuide,
@@ -15447,19 +15436,18 @@ export {
15447
15436
  FallbackPrompt,
15448
15437
  ForgotPasswordModal,
15449
15438
  NeoFaceError,
15450
- NeoFaceID,
15451
15439
  RELEASE_DATE,
15452
15440
  VERSION,
15453
15441
  authorize,
15454
15442
  authorizeOperation,
15455
15443
  awaitPushApproval,
15444
+ captureFaceFrames,
15456
15445
  checkUserExistence,
15457
15446
  clearConsentTrail,
15458
15447
  completeOnboarding,
15459
15448
  completeOnboardingWithData,
15460
15449
  confirmPasswordReset,
15461
15450
  consumeSseStream,
15462
- detectBiometricType,
15463
15451
  getAccent,
15464
15452
  getAppName,
15465
15453
  getApplicationToken,
@@ -15477,19 +15465,20 @@ export {
15477
15465
  identifyPerson,
15478
15466
  identifyPersonAsync,
15479
15467
  init,
15480
- initializeBiometricDetection,
15481
- isAdvancedDetectionAvailable,
15482
15468
  isInitialized,
15483
15469
  loginWithBiometric,
15484
15470
  loginWithEmail,
15485
15471
  openCaptureSession,
15486
15472
  preloadFaceDetectionModels,
15473
+ proofOfLife,
15487
15474
  recognize,
15488
15475
  recognizeBiometric,
15489
15476
  recognizeByPurpose,
15490
15477
  recordConsent,
15491
15478
  refreshSession,
15479
+ registerApplication,
15492
15480
  registerBiometric,
15481
+ registerDocumentByImage,
15493
15482
  registerPersonWithBiometric,
15494
15483
  registerPersonWithoutFace,
15495
15484
  requestConsent,
@@ -15503,6 +15492,7 @@ export {
15503
15492
  start,
15504
15493
  startAutoLogin,
15505
15494
  startBiometricRegistration,
15495
+ startDeviceEnrollment,
15506
15496
  startDocumentCapture,
15507
15497
  startFaceLogin,
15508
15498
  startHandLogin,