@neofaceid/web-sdk 1.41.1 → 1.42.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.
@@ -4311,9 +4311,16 @@ function refine(fn, _params = {}) {
4311
4311
  function superRefine(fn) {
4312
4312
  return /* @__PURE__ */ _superRefine(fn);
4313
4313
  }
4314
+ const AuthMethodSchema = _enum(["face", "password"]);
4314
4315
  object({
4315
4316
  success: boolean(),
4316
4317
  accessToken: string().optional(),
4318
+ /**
4319
+ * Só chega quando `application.can_issue_user_tokens` está ligado no core.
4320
+ * Sessões longas (o console) dependem dele; fluxos curtos podem ignorá-lo.
4321
+ */
4322
+ refreshToken: string().optional(),
4323
+ method: AuthMethodSchema.optional(),
4317
4324
  alias: string().optional(),
4318
4325
  user: object({
4319
4326
  id: string(),
@@ -4547,6 +4554,10 @@ const ENVIRONMENT_URLS = {
4547
4554
  sandbox: "https://sandbox-core.neofaceid.com",
4548
4555
  production: "https://core.neofaceid.com.br"
4549
4556
  };
4557
+ const DEFAULT_AUTO_FALLBACK = {
4558
+ toPhone: false,
4559
+ toPassword: true
4560
+ };
4550
4561
  const DEFAULT_LOCALE = "pt-BR";
4551
4562
  const DEFAULT_RADIUS = 16;
4552
4563
  const DEFAULT_THEME = "light";
@@ -4563,13 +4574,31 @@ function makeDefaultConfig() {
4563
4574
  theme: DEFAULT_THEME,
4564
4575
  locale: DEFAULT_LOCALE,
4565
4576
  resolvedTheme: null,
4566
- consent: { ...DEFAULT_CONSENT }
4577
+ consent: { ...DEFAULT_CONSENT },
4578
+ autoFallback: { ...DEFAULT_AUTO_FALLBACK }
4567
4579
  };
4568
4580
  }
4569
4581
  let globalConfig = makeDefaultConfig();
4570
4582
  let consentWarningShown = false;
4571
4583
  function init(options = {}) {
4572
- const { environment, baseUrl, applicationToken, appName, accent, radius, theme, locale, consent } = options;
4584
+ const {
4585
+ environment,
4586
+ baseUrl,
4587
+ applicationToken,
4588
+ appName,
4589
+ accent,
4590
+ radius,
4591
+ theme,
4592
+ locale,
4593
+ consent,
4594
+ autoFallback
4595
+ } = options;
4596
+ if (autoFallback) {
4597
+ globalConfig.autoFallback = {
4598
+ toPhone: autoFallback.toPhone ?? DEFAULT_AUTO_FALLBACK.toPhone,
4599
+ toPassword: autoFallback.toPassword ?? DEFAULT_AUTO_FALLBACK.toPassword
4600
+ };
4601
+ }
4573
4602
  if (consent) {
4574
4603
  globalConfig.consent = consent;
4575
4604
  } else {
@@ -4667,6 +4696,9 @@ function getAppName() {
4667
4696
  function getAccent() {
4668
4697
  return globalConfig.accent;
4669
4698
  }
4699
+ function getAutoFallback() {
4700
+ return { ...globalConfig.autoFallback };
4701
+ }
4670
4702
  function getRadius() {
4671
4703
  return globalConfig.radius;
4672
4704
  }
@@ -5132,6 +5164,9 @@ const loginWithBiometric = async (image, applicationToken) => {
5132
5164
  return {
5133
5165
  success: true,
5134
5166
  accessToken: data.accessToken || data.access_token || "",
5167
+ // Só vem quando `can_issue_user_tokens` está ligado na aplicação.
5168
+ refreshToken: data.refreshToken || data.refresh_token || void 0,
5169
+ method: "face",
5135
5170
  alias: data.alias,
5136
5171
  user: {
5137
5172
  id: ((_a2 = data.user) == null ? void 0 : _a2.id) || "",
@@ -5618,6 +5653,8 @@ const loginWithEmail = async (email2, password, applicationToken) => {
5618
5653
  success: true,
5619
5654
  // Compatível com diferentes formatos de resposta do backend
5620
5655
  accessToken: data.access_token || data.access,
5656
+ refreshToken: data.refresh_token || data.refresh || void 0,
5657
+ method: "password",
5621
5658
  alias: data.user && data.user.alias || data.alias,
5622
5659
  user: {
5623
5660
  id: data.user && data.user.id || "",
@@ -5640,6 +5677,98 @@ const loginWithEmail = async (email2, password, applicationToken) => {
5640
5677
  throw new NeoFaceError("Unknown error occurred", ErrorType.NETWORK);
5641
5678
  }
5642
5679
  };
5680
+ const requestDeviceEnrollmentToken = async (userAccessToken, applicationToken) => {
5681
+ var _a2, _b, _c;
5682
+ if (!userAccessToken) {
5683
+ throw new NeoFaceError(
5684
+ "Inscrição de aparelho exige um usuário autenticado (access token ausente).",
5685
+ ErrorType.INVALID_TOKEN
5686
+ );
5687
+ }
5688
+ const controller = createTimeoutController();
5689
+ try {
5690
+ const response = await fetch(`${getApiBaseUrl()}/api/v1/devices/enrollment-token/`, {
5691
+ method: "POST",
5692
+ headers: {
5693
+ "Content-Type": "application/json",
5694
+ "X-App-Token": applicationToken,
5695
+ Authorization: `Bearer ${userAccessToken}`
5696
+ },
5697
+ signal: controller.signal
5698
+ });
5699
+ if (!response.ok) {
5700
+ if (response.status === 401 || response.status === 403) {
5701
+ throw new NeoFaceError(
5702
+ "Não foi possível inscrever o aparelho: sessão expirada ou conta sem permissão. Faça login novamente.",
5703
+ ErrorType.INVALID_TOKEN
5704
+ );
5705
+ }
5706
+ throw new NeoFaceError(`API Error ${response.status}`, ErrorType.API_ERROR);
5707
+ }
5708
+ 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);
5712
+ }
5713
+ return {
5714
+ 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"
5717
+ };
5718
+ } catch (error) {
5719
+ if (error instanceof NeoFaceError) throw error;
5720
+ if (error instanceof Error) {
5721
+ if (error.name === "AbortError") {
5722
+ throw new NeoFaceError("Request timed out", ErrorType.NETWORK);
5723
+ }
5724
+ throw new NeoFaceError(error.message, ErrorType.NETWORK);
5725
+ }
5726
+ throw new NeoFaceError("Unknown error occurred", ErrorType.NETWORK);
5727
+ }
5728
+ };
5729
+ const refreshSession = async (refreshToken, applicationToken) => {
5730
+ const controller = createTimeoutController();
5731
+ try {
5732
+ const response = await fetch(`${getApiBaseUrl()}/api/v1/token/refresh/`, {
5733
+ method: "POST",
5734
+ headers: {
5735
+ "Content-Type": "application/json",
5736
+ "X-App-Token": applicationToken
5737
+ },
5738
+ body: JSON.stringify({ refresh: refreshToken }),
5739
+ signal: controller.signal
5740
+ });
5741
+ if (!response.ok) {
5742
+ if (response.status === 401 || response.status === 403) {
5743
+ throw new NeoFaceError(
5744
+ "Sessão expirada — faça login novamente",
5745
+ ErrorType.INVALID_TOKEN
5746
+ );
5747
+ }
5748
+ throw new NeoFaceError(`API Error ${response.status}`, ErrorType.API_ERROR);
5749
+ }
5750
+ const data = await response.json();
5751
+ const access = data.access ?? data.access_token;
5752
+ if (!access) {
5753
+ throw new NeoFaceError("Resposta de refresh sem access token", ErrorType.API_ERROR);
5754
+ }
5755
+ return {
5756
+ accessToken: access,
5757
+ // Com rotação ligada o core devolve refresh novo. Se algum ambiente
5758
+ // estiver sem rotação, o refresh atual segue valendo.
5759
+ refreshToken: data.refresh ?? data.refresh_token ?? refreshToken
5760
+ };
5761
+ } catch (error) {
5762
+ if (error instanceof NeoFaceError) throw error;
5763
+ if (error instanceof Error) {
5764
+ if (error.name === "AbortError") {
5765
+ throw new NeoFaceError("Request timed out", ErrorType.NETWORK);
5766
+ }
5767
+ throw new NeoFaceError(error.message, ErrorType.NETWORK);
5768
+ }
5769
+ throw new NeoFaceError("Unknown error occurred", ErrorType.NETWORK);
5770
+ }
5771
+ };
5643
5772
  const identifyPerson = async (image, applicationToken) => {
5644
5773
  var _a2, _b, _c, _d;
5645
5774
  ensureSecureContext();
@@ -5952,7 +6081,7 @@ const recordFaceVideo = async (options = {}) => {
5952
6081
  });
5953
6082
  };
5954
6083
  const pollTaskStatus = async (taskId, applicationToken, options = {}) => {
5955
- var _a2, _b, _c, _d, _e, _f;
6084
+ var _a2, _b, _c, _d, _e, _f, _g, _h, _i, _j;
5956
6085
  const { maxAttempts = 60, intervalMs = 1e3, onStatusChange } = options;
5957
6086
  let attempts = 0;
5958
6087
  while (attempts < maxAttempts) {
@@ -5984,6 +6113,10 @@ const pollTaskStatus = async (taskId, applicationToken, options = {}) => {
5984
6113
  if (status === "revoked") {
5985
6114
  throw new NeoFaceError("Task was cancelled", ErrorType.RECOGNITION_FAILED);
5986
6115
  }
6116
+ if (data.success === false) {
6117
+ const reason = ((_h = (_g = data.data) == null ? void 0 : _g.result) == null ? void 0 : _h.message) || ((_i = data.data) == null ? void 0 : _i.error) || ((_j = data.data) == null ? void 0 : _j.message) || data.message || "Task failed";
6118
+ throw new NeoFaceError(reason, ErrorType.RECOGNITION_FAILED);
6119
+ }
5987
6120
  await new Promise((resolve) => setTimeout(resolve, intervalMs));
5988
6121
  } catch (error) {
5989
6122
  if (error instanceof NeoFaceError) {
@@ -6242,11 +6375,13 @@ const api = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty(
6242
6375
  recognizeBiometric,
6243
6376
  recognizeByPurpose,
6244
6377
  recordFaceVideo,
6378
+ refreshSession,
6245
6379
  registerApplication,
6246
6380
  registerBiometric,
6247
6381
  registerDocumentByImage,
6248
6382
  registerPersonWithBiometric,
6249
6383
  registerPersonWithoutFace,
6384
+ requestDeviceEnrollmentToken,
6250
6385
  requestPasswordReset,
6251
6386
  simpleIdentification,
6252
6387
  startProofOfLife,
@@ -8732,7 +8867,7 @@ function BiometricRegistrationModal({
8732
8867
  /* @__PURE__ */ jsx("canvas", { ref: canvasRef })
8733
8868
  ] });
8734
8869
  }
8735
- const VERSION = "1.41.1";
8870
+ const VERSION = "1.42.0";
8736
8871
  const RELEASE_DATE = "2026-09-10";
8737
8872
  const CONSENT_TRAIL_STORAGE_KEY = "neoface_consent_trail_v1";
8738
8873
  const CONSENT_TRAIL_MAX_LIMIT = 100;
@@ -9178,118 +9313,814 @@ const DEFAULT_CONSENT_INFO = {
9178
9313
  privacyPolicyUrl: DEFAULT_PRIVACY_URL,
9179
9314
  retentionDays: DEFAULT_RETENTION_DAYS
9180
9315
  };
9181
- function getHaloStyles(state) {
9182
- switch (state) {
9183
- case "centered":
9184
- return {
9185
- borderColor: "#0E9F6E",
9186
- boxShadow: "0 0 0 4px rgba(14, 159, 110, 0.8), 0 0 45px rgba(14, 159, 110, 0.45)",
9187
- svgStroke: "#0E9F6E",
9188
- badgeBg: "rgba(14, 159, 110, 0.95)",
9189
- badgeColor: "#FFFFFF"
9190
- };
9191
- case "too-far":
9192
- case "too-close":
9193
- case "off-center":
9194
- return {
9195
- borderColor: "#F59E0B",
9196
- boxShadow: "0 0 0 4px rgba(245, 158, 11, 0.7), 0 0 35px rgba(245, 158, 11, 0.35)",
9197
- svgStroke: "#F59E0B",
9198
- badgeBg: "rgba(245, 158, 11, 0.95)",
9199
- badgeColor: "#1F2937"
9200
- };
9201
- case "searching":
9202
- default:
9203
- return {
9204
- borderColor: "rgba(156, 163, 175, 0.5)",
9205
- boxShadow: "0 0 0 4px rgba(156, 163, 175, 0.3), 0 0 20px rgba(156, 163, 175, 0.15)",
9206
- svgStroke: "#9CA3AF",
9207
- badgeBg: "rgba(55, 65, 81, 0.9)",
9208
- badgeColor: "#FFFFFF"
9209
- };
9210
- }
9211
- }
9212
- function CameraOvalGuide({
9213
- stream,
9214
- faceState,
9215
- instruction,
9216
- className = "",
9217
- style = {}
9316
+ function Sheet({
9317
+ open,
9318
+ onDismiss,
9319
+ dismissOnOverlay = true,
9320
+ ariaLabel,
9321
+ ariaLabelledBy,
9322
+ children,
9323
+ className,
9324
+ style
9218
9325
  }) {
9219
- const videoRef = useRef(null);
9220
9326
  useEffect(() => {
9221
- const videoEl = videoRef.current;
9222
- if (!videoEl) return;
9223
- if (stream) {
9224
- videoEl.srcObject = stream;
9225
- videoEl.play().catch(() => {
9226
- });
9227
- } else {
9228
- videoEl.srcObject = null;
9229
- }
9230
- return () => {
9231
- if (stream) {
9232
- stream.getTracks().forEach((track) => track.stop());
9233
- }
9234
- if (videoEl) {
9235
- videoEl.srcObject = null;
9236
- }
9237
- };
9238
- }, [stream]);
9239
- const halo = getHaloStyles(faceState);
9240
- return /* @__PURE__ */ jsxs(
9327
+ injectThemeStyles();
9328
+ }, []);
9329
+ if (!open) return null;
9330
+ const handleOverlay = () => {
9331
+ if (dismissOnOverlay && onDismiss) onDismiss();
9332
+ };
9333
+ return /* @__PURE__ */ jsx(
9241
9334
  "div",
9242
9335
  {
9243
- className: `neofaceid-camera-oval-guide-container ${className}`,
9336
+ className: "neofaceid-root",
9337
+ role: "presentation",
9244
9338
  style: {
9339
+ position: "fixed",
9340
+ inset: 0,
9341
+ zIndex: 10001,
9245
9342
  display: "flex",
9246
- flexDirection: "column",
9247
9343
  alignItems: "center",
9248
9344
  justifyContent: "center",
9249
- position: "relative",
9250
- width: "100%",
9251
- maxHeight: "100%",
9252
- padding: "16px",
9253
- boxSizing: "border-box",
9254
- ...style
9345
+ padding: 20,
9346
+ background: `var(${CSS_VAR.overlay})`,
9347
+ backdropFilter: "blur(3px)",
9348
+ WebkitBackdropFilter: "blur(3px)"
9255
9349
  },
9256
- children: [
9257
- /* @__PURE__ */ jsx("style", { children: `
9258
- .neofaceid-oval-frame {
9259
- width: 320px;
9260
- height: 400px;
9261
- border-radius: 50%;
9262
- overflow: hidden;
9263
- position: relative;
9264
- background: #000000;
9265
- transition: box-shadow 0.3s ease, border-color 0.3s ease;
9350
+ onClick: handleOverlay,
9351
+ children: /* @__PURE__ */ jsx(
9352
+ "div",
9353
+ {
9354
+ role: "dialog",
9355
+ "aria-modal": "true",
9356
+ "aria-label": ariaLabel,
9357
+ "aria-labelledby": ariaLabelledBy,
9358
+ className,
9359
+ onClick: (e) => e.stopPropagation(),
9360
+ style: {
9361
+ background: `var(${CSS_VAR.surface})`,
9362
+ color: `var(${CSS_VAR.text})`,
9363
+ borderRadius: `var(${CSS_VAR.radius}, 16px)`,
9364
+ padding: 28,
9365
+ maxWidth: 440,
9366
+ width: "100%",
9367
+ boxShadow: "0 1px 3px rgba(10,19,32,.08)",
9368
+ boxSizing: "border-box",
9369
+ ...style
9370
+ },
9371
+ children
9266
9372
  }
9267
-
9268
- .neofaceid-oval-video {
9269
- width: 100%;
9270
- height: 100%;
9271
- object-fit: cover;
9272
- transform: scaleX(-1);
9273
- display: block;
9373
+ )
9374
+ }
9375
+ );
9376
+ }
9377
+ const NON_TERMINAL_STATUSES = /* @__PURE__ */ new Set(["connected", "heartbeat", "processing"]);
9378
+ const DEFAULT_TIMEOUT_MS = 31e4;
9379
+ const DEFAULT_MAX_RECONNECTS = 5;
9380
+ const RECONNECT_BASE_DELAY_MS = 500;
9381
+ const RECONNECT_MAX_DELAY_MS = 8e3;
9382
+ function delay(ms, signal) {
9383
+ return new Promise((resolve) => {
9384
+ const id = setTimeout(resolve, ms);
9385
+ signal == null ? void 0 : signal.addEventListener(
9386
+ "abort",
9387
+ () => {
9388
+ clearTimeout(id);
9389
+ resolve();
9390
+ },
9391
+ { once: true }
9392
+ );
9393
+ });
9394
+ }
9395
+ async function consumeSseStream(options) {
9396
+ const {
9397
+ requestTicket,
9398
+ buildUrl,
9399
+ classify,
9400
+ timeoutMs = DEFAULT_TIMEOUT_MS,
9401
+ maxReconnects = DEFAULT_MAX_RECONNECTS,
9402
+ signal,
9403
+ eventSourceFactory,
9404
+ onProgress
9405
+ } = options;
9406
+ const makeEventSource = eventSourceFactory ?? ((url) => {
9407
+ const Ctor = globalThis.EventSource;
9408
+ if (!Ctor) {
9409
+ throw new NeoFaceError(
9410
+ "EventSource indisponível neste ambiente",
9411
+ ErrorType.INITIALIZATION_ERROR
9412
+ );
9413
+ }
9414
+ return new Ctor(url);
9415
+ });
9416
+ const startedAt = Date.now();
9417
+ let reconnects = 0;
9418
+ for (; ; ) {
9419
+ if (signal == null ? void 0 : signal.aborted) {
9420
+ throw new NeoFaceError("Fluxo cancelado", ErrorType.UNKNOWN);
9421
+ }
9422
+ const remaining = timeoutMs - (Date.now() - startedAt);
9423
+ if (remaining <= 0) {
9424
+ throw new NeoFaceError("Tempo esgotado aguardando resposta", ErrorType.NETWORK);
9425
+ }
9426
+ const ticket = await requestTicket();
9427
+ const outcome = await new Promise((resolve, reject) => {
9428
+ let source;
9429
+ try {
9430
+ source = makeEventSource(buildUrl(ticket));
9431
+ } catch (err) {
9432
+ reject(err);
9433
+ return;
9434
+ }
9435
+ let settled = false;
9436
+ let timerId;
9437
+ let onAbort;
9438
+ const cleanup = () => {
9439
+ if (timerId !== void 0) clearTimeout(timerId);
9440
+ if (onAbort) signal == null ? void 0 : signal.removeEventListener("abort", onAbort);
9441
+ source.close();
9442
+ };
9443
+ timerId = setTimeout(() => {
9444
+ if (settled) return;
9445
+ settled = true;
9446
+ cleanup();
9447
+ reject(new NeoFaceError("Tempo esgotado aguardando resposta", ErrorType.NETWORK));
9448
+ }, remaining);
9449
+ onAbort = () => {
9450
+ if (settled) return;
9451
+ settled = true;
9452
+ cleanup();
9453
+ reject(new NeoFaceError("Fluxo cancelado", ErrorType.UNKNOWN));
9454
+ };
9455
+ signal == null ? void 0 : signal.addEventListener("abort", onAbort, { once: true });
9456
+ source.onmessage = (event) => {
9457
+ if (settled) return;
9458
+ let frame;
9459
+ try {
9460
+ frame = JSON.parse(event.data);
9461
+ } catch {
9462
+ return;
9274
9463
  }
9275
-
9276
- .neofaceid-oval-svg-overlay {
9277
- position: absolute;
9278
- inset: 0;
9279
- width: 100%;
9280
- height: 100%;
9281
- pointer-events: none;
9282
- z-index: 2;
9464
+ if (frame.status && NON_TERMINAL_STATUSES.has(frame.status)) {
9465
+ if (onProgress) onProgress(frame);
9466
+ return;
9283
9467
  }
9284
-
9285
- .neofaceid-oval-instruction-badge {
9286
- margin-top: 20px;
9287
- padding: 10px 20px;
9288
- border-radius: 20px;
9289
- font-size: 15px;
9290
- font-weight: 600;
9291
- text-align: center;
9292
- max-width: 320px;
9468
+ const verdict = classify(frame);
9469
+ if (verdict.kind === "pending") {
9470
+ if (onProgress) onProgress(frame);
9471
+ return;
9472
+ }
9473
+ settled = true;
9474
+ cleanup();
9475
+ resolve({ type: "settled", verdict });
9476
+ };
9477
+ source.onerror = () => {
9478
+ if (settled) return;
9479
+ settled = true;
9480
+ cleanup();
9481
+ resolve({ type: "transport" });
9482
+ };
9483
+ });
9484
+ if (outcome.type === "settled") {
9485
+ const { verdict } = outcome;
9486
+ if (verdict.kind === "resolve") return verdict.value;
9487
+ throw verdict.error;
9488
+ }
9489
+ reconnects += 1;
9490
+ if (reconnects > maxReconnects) {
9491
+ throw new NeoFaceError(
9492
+ `Conexão perdida após ${maxReconnects} tentativas de reconexão`,
9493
+ ErrorType.NETWORK
9494
+ );
9495
+ }
9496
+ const backoff = Math.min(
9497
+ RECONNECT_BASE_DELAY_MS * 2 ** (reconnects - 1),
9498
+ RECONNECT_MAX_DELAY_MS
9499
+ );
9500
+ await delay(backoff, signal);
9501
+ }
9502
+ }
9503
+ const BASE_STYLE = {
9504
+ minHeight: 44,
9505
+ minWidth: 44,
9506
+ padding: "12px 20px",
9507
+ borderRadius: 16,
9508
+ border: "none",
9509
+ fontSize: 15,
9510
+ fontWeight: 600,
9511
+ fontFamily: "inherit",
9512
+ cursor: "pointer",
9513
+ transition: "background 120ms ease-out",
9514
+ display: "inline-flex",
9515
+ alignItems: "center",
9516
+ justifyContent: "center",
9517
+ gap: 8
9518
+ };
9519
+ const Button = forwardRef(
9520
+ ({ variant = "primary", style, type = "button", ...rest }, ref) => {
9521
+ const variantStyle = variant === "primary" ? {
9522
+ background: `var(${CSS_VAR.action})`,
9523
+ color: `var(${CSS_VAR.actionText})`
9524
+ } : variant === "secondary" ? {
9525
+ background: `var(${CSS_VAR.surfaceMuted})`,
9526
+ color: `var(${CSS_VAR.text})`
9527
+ } : {
9528
+ background: "transparent",
9529
+ color: `var(${CSS_VAR.textMuted})`
9530
+ };
9531
+ return (
9532
+ // eslint-disable-next-line react/button-has-type
9533
+ /* @__PURE__ */ jsx(
9534
+ "button",
9535
+ {
9536
+ ref,
9537
+ type,
9538
+ ...rest,
9539
+ style: { ...BASE_STYLE, ...variantStyle, ...style }
9540
+ }
9541
+ )
9542
+ );
9543
+ }
9544
+ );
9545
+ Button.displayName = "Button";
9546
+ function StatusPill({ tone, children, icon, className }) {
9547
+ const palette = SEMANTIC[tone];
9548
+ return /* @__PURE__ */ jsxs(
9549
+ "span",
9550
+ {
9551
+ className,
9552
+ style: {
9553
+ display: "inline-flex",
9554
+ alignItems: "center",
9555
+ gap: 6,
9556
+ padding: "4px 10px",
9557
+ borderRadius: 9999,
9558
+ background: palette.bg,
9559
+ color: palette.fg,
9560
+ fontSize: 13,
9561
+ fontWeight: 600,
9562
+ fontFamily: "inherit",
9563
+ lineHeight: 1.3
9564
+ },
9565
+ children: [
9566
+ icon,
9567
+ children
9568
+ ]
9569
+ }
9570
+ );
9571
+ }
9572
+ const STYLE_ID$2 = "neofaceid-awaiting-push-styles";
9573
+ const CSS$1 = `
9574
+ .neofaceid-root .nfid-push {
9575
+ display: flex; flex-direction: column; align-items: center; text-align: center; gap: 10px;
9576
+ padding: 8px 8px 4px;
9577
+ }
9578
+ .neofaceid-root .nfid-push h2 {
9579
+ margin: 6px 0 0; font-size: 20px; font-weight: 700; color: var(--nfid-text); line-height: 1.25;
9580
+ letter-spacing: -0.015em;
9581
+ }
9582
+ .neofaceid-root .nfid-push p {
9583
+ margin: 0; font-size: 14px; color: var(--nfid-text-muted); line-height: 1.5;
9584
+ }
9585
+ .neofaceid-root .nfid-push-phone-icon {
9586
+ width: 56px; height: 56px; color: var(--nfid-action);
9587
+ }
9588
+ .neofaceid-root .nfid-push-code {
9589
+ display: flex; flex-direction: column; align-items: center; gap: 6px;
9590
+ margin-top: 14px; padding: 16px 28px;
9591
+ background: var(--nfid-surface-muted); border-radius: 16px; width: 100%;
9592
+ }
9593
+ .neofaceid-root .nfid-push-code-label {
9594
+ font-size: 11px; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase;
9595
+ color: var(--nfid-text-subtle);
9596
+ }
9597
+ .neofaceid-root .nfid-push-code-value {
9598
+ font-size: 52px; font-weight: 800; line-height: 1; letter-spacing: 0.08em;
9599
+ color: var(--nfid-text); font-variant-numeric: tabular-nums;
9600
+ }
9601
+ .neofaceid-root .nfid-push-countdown {
9602
+ font-size: 13px; color: var(--nfid-text-muted); font-variant-numeric: tabular-nums;
9603
+ margin-top: 12px;
9604
+ }
9605
+ .neofaceid-root .nfid-push-actions {
9606
+ display: flex; flex-direction: column; gap: 10px; width: 100%; margin-top: 16px;
9607
+ }
9608
+ .neofaceid-root .nfid-push-footer {
9609
+ display: flex; justify-content: center; margin-top: 16px; padding-top: 12px;
9610
+ border-top: 1px solid var(--nfid-line);
9611
+ width: 100%;
9612
+ }
9613
+ .neofaceid-root .nfid-push-pulse {
9614
+ animation: nfidPushPulse 1.8s ease-in-out infinite;
9615
+ }
9616
+ @keyframes nfidPushPulse {
9617
+ 0%, 100% { opacity: 1; }
9618
+ 50% { opacity: 0.45; }
9619
+ }
9620
+ @media (prefers-reduced-motion: reduce) {
9621
+ .neofaceid-root .nfid-push-pulse { animation: none; }
9622
+ }
9623
+ `;
9624
+ function formatRemaining(seconds) {
9625
+ const safe = Math.max(0, seconds);
9626
+ const mm = Math.floor(safe / 60);
9627
+ const ss = safe % 60;
9628
+ return `${mm}:${String(ss).padStart(2, "0")}`;
9629
+ }
9630
+ function AwaitingPushView({
9631
+ appName,
9632
+ numericCode,
9633
+ remainingSeconds,
9634
+ onCancel
9635
+ }) {
9636
+ useEffect(() => {
9637
+ injectThemeStyles();
9638
+ injectScopedStyles(STYLE_ID$2, CSS$1);
9639
+ }, []);
9640
+ return /* @__PURE__ */ jsxs("div", { className: "nfid-push", role: "status", "aria-live": "polite", children: [
9641
+ /* @__PURE__ */ jsx(StatusPill, { tone: "attention", children: "Aguardando confirmação" }),
9642
+ /* @__PURE__ */ jsxs(
9643
+ "svg",
9644
+ {
9645
+ className: "nfid-push-phone-icon nfid-push-pulse",
9646
+ viewBox: "0 0 24 24",
9647
+ fill: "none",
9648
+ stroke: "currentColor",
9649
+ strokeWidth: "1.75",
9650
+ strokeLinecap: "round",
9651
+ strokeLinejoin: "round",
9652
+ "aria-hidden": "true",
9653
+ children: [
9654
+ /* @__PURE__ */ jsx("rect", { x: "6", y: "2", width: "12", height: "20", rx: "3" }),
9655
+ /* @__PURE__ */ jsx("path", { d: "M11 18h2" })
9656
+ ]
9657
+ }
9658
+ ),
9659
+ /* @__PURE__ */ jsx("h2", { children: "Confirme no seu celular" }),
9660
+ /* @__PURE__ */ jsxs("p", { children: [
9661
+ "Enviamos um pedido de confirmação para o seu aparelho. Abra o NeoFaceID Push e confirme com o seu rosto para continuar no ",
9662
+ appName,
9663
+ "."
9664
+ ] }),
9665
+ numericCode && /* @__PURE__ */ jsxs("div", { className: "nfid-push-code", children: [
9666
+ /* @__PURE__ */ jsx("span", { className: "nfid-push-code-label", children: "Digite este número no celular" }),
9667
+ /* @__PURE__ */ jsx(
9668
+ "span",
9669
+ {
9670
+ className: "nfid-push-code-value",
9671
+ "aria-label": `Código de confirmação: ${numericCode.split("").join(" ")}`,
9672
+ children: numericCode
9673
+ }
9674
+ )
9675
+ ] }),
9676
+ /* @__PURE__ */ jsxs("p", { className: "nfid-push-countdown", children: [
9677
+ "Expira em ",
9678
+ formatRemaining(remainingSeconds)
9679
+ ] }),
9680
+ /* @__PURE__ */ jsx("div", { className: "nfid-push-actions", children: /* @__PURE__ */ jsx(Button, { variant: "ghost", onClick: onCancel, children: "Cancelar" }) }),
9681
+ /* @__PURE__ */ jsx("div", { className: "nfid-push-footer", children: /* @__PURE__ */ jsx(BrandFooter, {}) })
9682
+ ] });
9683
+ }
9684
+ function PushExpiredView({ onFallback, onCancel }) {
9685
+ useEffect(() => {
9686
+ injectThemeStyles();
9687
+ injectScopedStyles(STYLE_ID$2, CSS$1);
9688
+ }, []);
9689
+ return /* @__PURE__ */ jsxs("div", { className: "nfid-push", role: "alert", "aria-live": "assertive", children: [
9690
+ /* @__PURE__ */ jsxs(
9691
+ "svg",
9692
+ {
9693
+ className: "nfid-push-phone-icon",
9694
+ viewBox: "0 0 24 24",
9695
+ fill: "none",
9696
+ stroke: "currentColor",
9697
+ strokeWidth: "1.75",
9698
+ strokeLinecap: "round",
9699
+ strokeLinejoin: "round",
9700
+ "aria-hidden": "true",
9701
+ style: { color: "var(--nfid-text-muted)" },
9702
+ children: [
9703
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "9" }),
9704
+ /* @__PURE__ */ jsx("path", { d: "M12 7v5l3 2" })
9705
+ ]
9706
+ }
9707
+ ),
9708
+ /* @__PURE__ */ jsx("h2", { children: "O tempo esgotou" }),
9709
+ /* @__PURE__ */ jsx("p", { children: "Não recebemos a confirmação do seu celular a tempo." }),
9710
+ /* @__PURE__ */ jsx("div", { className: "nfid-push-actions", children: onFallback ? /* @__PURE__ */ jsxs(Fragment, { children: [
9711
+ /* @__PURE__ */ jsx(Button, { variant: "primary", onClick: onFallback, children: "Entrar de outro jeito" }),
9712
+ /* @__PURE__ */ jsx(Button, { variant: "ghost", onClick: onCancel, children: "Agora não" })
9713
+ ] }) : /* @__PURE__ */ jsx(Button, { variant: "primary", onClick: onCancel, children: "Entendi" }) }),
9714
+ /* @__PURE__ */ jsx("div", { className: "nfid-push-footer", children: /* @__PURE__ */ jsx(BrandFooter, {}) })
9715
+ ] });
9716
+ }
9717
+ function PushDeniedView({ appName, onCancel }) {
9718
+ useEffect(() => {
9719
+ injectThemeStyles();
9720
+ injectScopedStyles(STYLE_ID$2, CSS$1);
9721
+ }, []);
9722
+ return /* @__PURE__ */ jsxs("div", { className: "nfid-push", role: "alert", "aria-live": "assertive", children: [
9723
+ /* @__PURE__ */ jsxs(
9724
+ "svg",
9725
+ {
9726
+ className: "nfid-push-phone-icon",
9727
+ viewBox: "0 0 24 24",
9728
+ fill: "none",
9729
+ stroke: "currentColor",
9730
+ strokeWidth: "1.75",
9731
+ strokeLinecap: "round",
9732
+ strokeLinejoin: "round",
9733
+ "aria-hidden": "true",
9734
+ style: { color: "#D64545" },
9735
+ children: [
9736
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "9" }),
9737
+ /* @__PURE__ */ jsx("path", { d: "M15 9l-6 6M9 9l6 6" })
9738
+ ]
9739
+ }
9740
+ ),
9741
+ /* @__PURE__ */ jsx("h2", { children: "Confirmação recusada" }),
9742
+ /* @__PURE__ */ jsxs("p", { children: [
9743
+ "O pedido foi recusado no celular. Se não foi você, procure o suporte do ",
9744
+ appName,
9745
+ "."
9746
+ ] }),
9747
+ /* @__PURE__ */ jsx("div", { className: "nfid-push-actions", children: /* @__PURE__ */ jsx(Button, { variant: "primary", onClick: onCancel, children: "Entendi" }) }),
9748
+ /* @__PURE__ */ jsx("div", { className: "nfid-push-footer", children: /* @__PURE__ */ jsx(BrandFooter, {}) })
9749
+ ] });
9750
+ }
9751
+ const CONTAINER_ID = "neofaceid-push-approval-container";
9752
+ function classifyAuthorizationFrame(frame) {
9753
+ var _a2, _b;
9754
+ switch (frame.status) {
9755
+ case "approved":
9756
+ case "success":
9757
+ return { kind: "resolve", value: { status: "approved", data: frame.data ?? {} } };
9758
+ case "denied":
9759
+ return {
9760
+ kind: "resolve",
9761
+ value: { status: "denied", reason: ((_a2 = frame.data) == null ? void 0 : _a2.reason) ?? frame.message }
9762
+ };
9763
+ case "expired":
9764
+ return { kind: "resolve", value: { status: "expired" } };
9765
+ case "failed":
9766
+ case "error":
9767
+ return {
9768
+ kind: "reject",
9769
+ error: new NeoFaceError(
9770
+ frame.message ?? ((_b = frame.data) == null ? void 0 : _b.message) ?? "Falha na confirmação",
9771
+ ErrorType.API_ERROR
9772
+ )
9773
+ };
9774
+ default:
9775
+ return { kind: "pending" };
9776
+ }
9777
+ }
9778
+ function PushApprovalModal({
9779
+ requestId,
9780
+ applicationToken,
9781
+ expiresIn,
9782
+ numericCode,
9783
+ appName,
9784
+ onFallback,
9785
+ onSettle
9786
+ }) {
9787
+ const headerAppName = appName ?? getAppName() ?? "sua aplicação";
9788
+ const [remaining, setRemaining] = useState(expiresIn);
9789
+ const [phase, setPhase] = useState({ kind: "waiting" });
9790
+ useEffect(() => {
9791
+ if (phase.kind !== "waiting") return void 0;
9792
+ const id = setInterval(() => {
9793
+ setRemaining((r) => r > 0 ? r - 1 : 0);
9794
+ }, 1e3);
9795
+ return () => clearInterval(id);
9796
+ }, [phase.kind]);
9797
+ useEffect(() => {
9798
+ const controller = new AbortController();
9799
+ let done = false;
9800
+ const base = getBaseUrl();
9801
+ const ticketUrl = `${base}/api/v1/authorization/events/${encodeURIComponent(requestId)}/ticket/`;
9802
+ consumeSseStream({
9803
+ signal: controller.signal,
9804
+ timeoutMs: (expiresIn + 30) * 1e3,
9805
+ requestTicket: async () => {
9806
+ var _a2;
9807
+ const res = await fetch(ticketUrl, {
9808
+ method: "POST",
9809
+ headers: { "Content-Type": "application/json", "X-App-Token": applicationToken }
9810
+ });
9811
+ if (!res.ok) {
9812
+ throw new NeoFaceError(
9813
+ `Falha ao abrir canal de confirmação: ${res.status}`,
9814
+ res.status === 401 || res.status === 403 ? ErrorType.INVALID_TOKEN : ErrorType.NETWORK
9815
+ );
9816
+ }
9817
+ const body = await res.json();
9818
+ const ticket = ((_a2 = body == null ? void 0 : body.data) == null ? void 0 : _a2.ticket) ?? (body == null ? void 0 : body.ticket);
9819
+ if (!ticket) {
9820
+ throw new NeoFaceError("Canal de confirmação sem ticket", ErrorType.API_ERROR);
9821
+ }
9822
+ return ticket;
9823
+ },
9824
+ buildUrl: (ticket) => `${base}/api/v1/authorization/events/${encodeURIComponent(requestId)}/?ticket=${encodeURIComponent(ticket)}`,
9825
+ classify: classifyAuthorizationFrame
9826
+ }).then((outcome) => {
9827
+ if (done) return;
9828
+ done = true;
9829
+ if (outcome.status === "denied") {
9830
+ setPhase({ kind: "denied" });
9831
+ return;
9832
+ }
9833
+ if (outcome.status === "expired") {
9834
+ setPhase({ kind: "expired" });
9835
+ return;
9836
+ }
9837
+ onSettle(outcome);
9838
+ }).catch(() => {
9839
+ if (done || controller.signal.aborted) return;
9840
+ done = true;
9841
+ setPhase({ kind: "expired" });
9842
+ });
9843
+ return () => {
9844
+ done = true;
9845
+ controller.abort();
9846
+ };
9847
+ }, [requestId]);
9848
+ if (phase.kind === "denied") {
9849
+ return /* @__PURE__ */ jsx(Sheet, { open: true, dismissOnOverlay: false, ariaLabel: "Confirmação recusada", children: /* @__PURE__ */ jsx(PushDeniedView, { appName: headerAppName, onCancel: () => onSettle({ status: "denied" }) }) });
9850
+ }
9851
+ if (phase.kind === "expired") {
9852
+ return /* @__PURE__ */ jsx(Sheet, { open: true, dismissOnOverlay: false, ariaLabel: "Tempo esgotado", children: /* @__PURE__ */ jsx(
9853
+ PushExpiredView,
9854
+ {
9855
+ onFallback: onFallback ? () => {
9856
+ onFallback();
9857
+ onSettle({ status: "expired" });
9858
+ } : void 0,
9859
+ onCancel: () => onSettle({ status: "expired" })
9860
+ }
9861
+ ) });
9862
+ }
9863
+ return /* @__PURE__ */ jsx(Sheet, { open: true, dismissOnOverlay: false, ariaLabel: "Aguardando confirmação no celular", children: /* @__PURE__ */ jsx(
9864
+ AwaitingPushView,
9865
+ {
9866
+ appName: headerAppName,
9867
+ numericCode,
9868
+ remainingSeconds: remaining,
9869
+ onCancel: () => onSettle({ status: "cancelled" })
9870
+ }
9871
+ ) });
9872
+ }
9873
+ function awaitPushApproval(options) {
9874
+ return new Promise((resolve) => {
9875
+ const container = document.createElement("div");
9876
+ container.id = CONTAINER_ID;
9877
+ document.body.appendChild(container);
9878
+ const root = createRoot(container);
9879
+ const settle = (outcome) => {
9880
+ root.unmount();
9881
+ if (document.body.contains(container)) document.body.removeChild(container);
9882
+ resolve(outcome);
9883
+ };
9884
+ root.render(/* @__PURE__ */ jsx(PushApprovalModal, { ...options, onSettle: settle }));
9885
+ });
9886
+ }
9887
+ function evaluateFacePosition(box, videoWidth, videoHeight) {
9888
+ if (!box || !videoWidth || !videoHeight) {
9889
+ return {
9890
+ faceState: "searching",
9891
+ instruction: "Centralize o rosto"
9892
+ };
9893
+ }
9894
+ const faceCenterX = box.x + box.width / 2;
9895
+ const faceCenterY = box.y + box.height / 2;
9896
+ const frameCenterX = videoWidth / 2;
9897
+ const frameCenterY = videoHeight / 2;
9898
+ const offsetX = Math.abs(faceCenterX - frameCenterX) / videoWidth;
9899
+ const offsetY = Math.abs(faceCenterY - frameCenterY) / videoHeight;
9900
+ const faceArea = box.width * box.height;
9901
+ const frameArea = videoWidth * videoHeight;
9902
+ const faceAreaRatio = faceArea / frameArea;
9903
+ if (faceAreaRatio < 0.08) {
9904
+ return {
9905
+ faceState: "too-far",
9906
+ instruction: "Aproxime-se"
9907
+ };
9908
+ }
9909
+ if (faceAreaRatio > 0.45) {
9910
+ return {
9911
+ faceState: "too-close",
9912
+ instruction: "Afaste-se"
9913
+ };
9914
+ }
9915
+ if (offsetX > 0.18 || offsetY > 0.18) {
9916
+ return {
9917
+ faceState: "off-center",
9918
+ instruction: "Centralize o rosto"
9919
+ };
9920
+ }
9921
+ return {
9922
+ faceState: "centered",
9923
+ instruction: "Perfeito, mantenha"
9924
+ };
9925
+ }
9926
+ function showCameraOvalGuideModal(initialOptions = {}) {
9927
+ const overlayDiv = document.createElement("div");
9928
+ overlayDiv.className = "neofaceid-camera-oval-guide-overlay-root";
9929
+ Object.assign(overlayDiv.style, {
9930
+ position: "fixed",
9931
+ top: "0",
9932
+ left: "0",
9933
+ width: "100vw",
9934
+ height: "100vh",
9935
+ backgroundColor: "rgba(0, 0, 0, 0.85)",
9936
+ backdropFilter: "blur(8px)",
9937
+ zIndex: "99999",
9938
+ display: "flex",
9939
+ flexDirection: "column",
9940
+ alignItems: "center",
9941
+ justifyContent: "center"
9942
+ });
9943
+ document.body.appendChild(overlayDiv);
9944
+ let root = createRoot(overlayDiv);
9945
+ let currentStream = initialOptions.stream || null;
9946
+ let currentFaceState = initialOptions.faceState || "searching";
9947
+ let currentInstruction = initialOptions.instruction || "Centralize o rosto";
9948
+ const renderModal = (stream, faceState, instruction) => {
9949
+ if (!root) return;
9950
+ root.render(
9951
+ /* @__PURE__ */ jsxs("div", { style: { position: "relative", width: "100%", height: "100%", display: "flex", alignItems: "center", justifyContent: "center" }, children: [
9952
+ initialOptions.onCancel && /* @__PURE__ */ jsx(
9953
+ "button",
9954
+ {
9955
+ onClick: () => {
9956
+ var _a2;
9957
+ handle.close();
9958
+ (_a2 = initialOptions.onCancel) == null ? void 0 : _a2.call(initialOptions);
9959
+ },
9960
+ style: {
9961
+ position: "absolute",
9962
+ top: "20px",
9963
+ right: "20px",
9964
+ background: "rgba(255, 255, 255, 0.2)",
9965
+ border: "none",
9966
+ color: "#fff",
9967
+ fontSize: "24px",
9968
+ width: "40px",
9969
+ height: "40px",
9970
+ borderRadius: "50%",
9971
+ cursor: "pointer",
9972
+ zIndex: 10,
9973
+ display: "flex",
9974
+ alignItems: "center",
9975
+ justifyContent: "center"
9976
+ },
9977
+ "aria-label": "Cancelar",
9978
+ children: "✕"
9979
+ }
9980
+ ),
9981
+ /* @__PURE__ */ jsx(
9982
+ CameraOvalGuide,
9983
+ {
9984
+ stream,
9985
+ faceState,
9986
+ instruction
9987
+ }
9988
+ )
9989
+ ] })
9990
+ );
9991
+ };
9992
+ renderModal(currentStream, currentFaceState, currentInstruction);
9993
+ const handle = {
9994
+ update: (stream, faceState, instruction) => {
9995
+ currentStream = stream;
9996
+ currentFaceState = faceState;
9997
+ currentInstruction = instruction;
9998
+ renderModal(stream, faceState, instruction);
9999
+ },
10000
+ close: () => {
10001
+ if (root) {
10002
+ root.unmount();
10003
+ root = null;
10004
+ }
10005
+ if (overlayDiv.parentElement) {
10006
+ overlayDiv.parentElement.removeChild(overlayDiv);
10007
+ }
10008
+ }
10009
+ };
10010
+ return handle;
10011
+ }
10012
+ function getHaloStyles(state) {
10013
+ switch (state) {
10014
+ case "centered":
10015
+ return {
10016
+ borderColor: "#0E9F6E",
10017
+ boxShadow: "0 0 0 4px rgba(14, 159, 110, 0.8), 0 0 45px rgba(14, 159, 110, 0.45)",
10018
+ svgStroke: "#0E9F6E",
10019
+ badgeBg: "rgba(14, 159, 110, 0.95)",
10020
+ badgeColor: "#FFFFFF"
10021
+ };
10022
+ case "too-far":
10023
+ case "too-close":
10024
+ case "off-center":
10025
+ return {
10026
+ borderColor: "#F59E0B",
10027
+ boxShadow: "0 0 0 4px rgba(245, 158, 11, 0.7), 0 0 35px rgba(245, 158, 11, 0.35)",
10028
+ svgStroke: "#F59E0B",
10029
+ badgeBg: "rgba(245, 158, 11, 0.95)",
10030
+ badgeColor: "#1F2937"
10031
+ };
10032
+ case "searching":
10033
+ default:
10034
+ return {
10035
+ borderColor: "rgba(156, 163, 175, 0.5)",
10036
+ boxShadow: "0 0 0 4px rgba(156, 163, 175, 0.3), 0 0 20px rgba(156, 163, 175, 0.15)",
10037
+ svgStroke: "#9CA3AF",
10038
+ badgeBg: "rgba(55, 65, 81, 0.9)",
10039
+ badgeColor: "#FFFFFF"
10040
+ };
10041
+ }
10042
+ }
10043
+ function CameraOvalGuide({
10044
+ stream,
10045
+ faceState,
10046
+ instruction,
10047
+ className = "",
10048
+ style = {}
10049
+ }) {
10050
+ const videoRef = useRef(null);
10051
+ useEffect(() => {
10052
+ const videoEl = videoRef.current;
10053
+ if (!videoEl) return;
10054
+ if (stream) {
10055
+ videoEl.srcObject = stream;
10056
+ videoEl.play().catch(() => {
10057
+ });
10058
+ } else {
10059
+ videoEl.srcObject = null;
10060
+ }
10061
+ return () => {
10062
+ if (stream) {
10063
+ stream.getTracks().forEach((track) => track.stop());
10064
+ }
10065
+ if (videoEl) {
10066
+ videoEl.srcObject = null;
10067
+ }
10068
+ };
10069
+ }, [stream]);
10070
+ const halo = getHaloStyles(faceState);
10071
+ return /* @__PURE__ */ jsxs(
10072
+ "div",
10073
+ {
10074
+ className: `neofaceid-camera-oval-guide-container ${className}`,
10075
+ style: {
10076
+ display: "flex",
10077
+ flexDirection: "column",
10078
+ alignItems: "center",
10079
+ justifyContent: "center",
10080
+ position: "relative",
10081
+ width: "100%",
10082
+ maxHeight: "100%",
10083
+ padding: "16px",
10084
+ boxSizing: "border-box",
10085
+ ...style
10086
+ },
10087
+ children: [
10088
+ /* @__PURE__ */ jsx("style", { children: `
10089
+ .neofaceid-oval-frame {
10090
+ width: 320px;
10091
+ height: 400px;
10092
+ border-radius: 50%;
10093
+ overflow: hidden;
10094
+ position: relative;
10095
+ background: #000000;
10096
+ transition: box-shadow 0.3s ease, border-color 0.3s ease;
10097
+ }
10098
+
10099
+ .neofaceid-oval-video {
10100
+ width: 100%;
10101
+ height: 100%;
10102
+ object-fit: cover;
10103
+ transform: scaleX(-1);
10104
+ display: block;
10105
+ }
10106
+
10107
+ .neofaceid-oval-svg-overlay {
10108
+ position: absolute;
10109
+ inset: 0;
10110
+ width: 100%;
10111
+ height: 100%;
10112
+ pointer-events: none;
10113
+ z-index: 2;
10114
+ }
10115
+
10116
+ .neofaceid-oval-instruction-badge {
10117
+ margin-top: 20px;
10118
+ padding: 10px 20px;
10119
+ border-radius: 20px;
10120
+ font-size: 15px;
10121
+ font-weight: 600;
10122
+ text-align: center;
10123
+ max-width: 320px;
9293
10124
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.25);
9294
10125
  backdrop-filter: blur(8px);
9295
10126
  transition: background-color 0.3s ease, color 0.3s ease;
@@ -10789,7 +11620,6 @@ const CAMERA_READY_DELAY = 200;
10789
11620
  const CAMERA_STABILITY_DELAY = 100;
10790
11621
  const FACE_DETECTION_TIME = 5e3;
10791
11622
  const DETECTION_INTERVAL = 100;
10792
- const SUCCESS_DELAY = 200;
10793
11623
  let modelsLoaded = false;
10794
11624
  let modelsLoading = null;
10795
11625
  async function preloadFaceDetectionModels() {
@@ -10946,14 +11776,13 @@ async function captureFaceSilently() {
10946
11776
  });
10947
11777
  });
10948
11778
  }
10949
- async function detectFaceQuickly(video, maxTime = FACE_DETECTION_TIME, interval = DETECTION_INTERVAL, overlay) {
11779
+ async function detectFaceWithPosition(video, stream, guideModal, maxTime = FACE_DETECTION_TIME, interval = DETECTION_INTERVAL) {
10950
11780
  return new Promise((resolve) => {
10951
11781
  let attempts = 0;
10952
11782
  const maxAttempts = Math.ceil(maxTime / interval);
10953
11783
  let timeoutId = null;
10954
11784
  let isResolved = false;
10955
- let overlayShowingWaiting = false;
10956
- const ATTEMPTS_BEFORE_WAITING = Math.ceil(3e3 / interval);
11785
+ let centeredDurationMs = 0;
10957
11786
  const finish = (success) => {
10958
11787
  if (isResolved) return;
10959
11788
  isResolved = true;
@@ -10964,11 +11793,7 @@ async function detectFaceQuickly(video, maxTime = FACE_DETECTION_TIME, interval
10964
11793
  if (isResolved) return;
10965
11794
  attempts++;
10966
11795
  if (!modelsLoaded) {
10967
- if (overlay && !overlayShowingWaiting && attempts >= ATTEMPTS_BEFORE_WAITING) {
10968
- overlay.updateStatus("waiting-for-face");
10969
- overlayShowingWaiting = true;
10970
- console.log("⏳ Models not loaded - waiting for face...");
10971
- }
11796
+ guideModal.update(stream, "searching", "Carregando modelos de detecção...");
10972
11797
  if (attempts >= maxAttempts) {
10973
11798
  finish(false);
10974
11799
  return;
@@ -10981,33 +11806,29 @@ async function detectFaceQuickly(video, maxTime = FACE_DETECTION_TIME, interval
10981
11806
  video,
10982
11807
  new faceapi.TinyFaceDetectorOptions({ inputSize: 160, scoreThreshold: 0.3 })
10983
11808
  );
10984
- if (detection) {
10985
- if (overlay && overlayShowingWaiting) {
10986
- overlay.updateStatus("detecting");
10987
- overlayShowingWaiting = false;
10988
- console.log("👤 Face appeared");
11809
+ const { faceState, instruction } = evaluateFacePosition(
11810
+ detection ? detection.box : null,
11811
+ video.videoWidth,
11812
+ video.videoHeight
11813
+ );
11814
+ guideModal.update(stream, faceState, instruction);
11815
+ if (faceState === "centered") {
11816
+ centeredDurationMs += interval;
11817
+ if (centeredDurationMs >= 1e3) {
11818
+ console.log("✅ Rosto mantido centralizado por 1s consecutivo");
11819
+ finish(true);
11820
+ return;
10989
11821
  }
10990
- console.log("✅ Face detected!");
10991
- finish(true);
10992
- return;
10993
- }
10994
- if (overlay && !overlayShowingWaiting && attempts >= ATTEMPTS_BEFORE_WAITING) {
10995
- overlay.updateStatus("waiting-for-face");
10996
- overlayShowingWaiting = true;
10997
- console.log("⏳ Waiting for face...");
11822
+ } else {
11823
+ centeredDurationMs = 0;
10998
11824
  }
10999
11825
  if (attempts >= maxAttempts) {
11000
- console.warn("⏱️ Face detection timeout - no face found");
11001
11826
  finish(false);
11002
11827
  return;
11003
11828
  }
11004
11829
  timeoutId = window.setTimeout(checkFace, interval);
11005
11830
  } catch (error) {
11006
- console.error("❌ Face detection error:", error);
11007
- if (overlay && !overlayShowingWaiting && attempts >= ATTEMPTS_BEFORE_WAITING) {
11008
- overlay.updateStatus("waiting-for-face");
11009
- overlayShowingWaiting = true;
11010
- }
11831
+ console.error("❌ Erro na detecção facial:", error);
11011
11832
  if (attempts >= maxAttempts) {
11012
11833
  finish(false);
11013
11834
  return;
@@ -11017,19 +11838,19 @@ async function detectFaceQuickly(video, maxTime = FACE_DETECTION_TIME, interval
11017
11838
  };
11018
11839
  window.setTimeout(() => {
11019
11840
  if (!isResolved) {
11020
- console.warn("⏱️ Face detection safety timeout reached");
11021
11841
  finish(false);
11022
11842
  }
11023
11843
  }, maxTime + 500);
11024
11844
  checkFace();
11025
11845
  });
11026
11846
  }
11027
- async function attemptLogin(applicationToken, overlay, fastMode = false, isRetry = false) {
11028
- if (!isRetry) {
11029
- overlay.updateStatus("preparing");
11030
- } else {
11031
- overlay.updateStatus("detecting");
11032
- }
11847
+ async function attemptLogin(applicationToken, fastMode = false, isRetry = false, onCancel) {
11848
+ const guideModal = showCameraOvalGuideModal({
11849
+ stream: null,
11850
+ faceState: "searching",
11851
+ instruction: "Iniciando câmera...",
11852
+ onCancel
11853
+ });
11033
11854
  const video = document.createElement("video");
11034
11855
  video.style.position = "fixed";
11035
11856
  video.style.top = "-9999px";
@@ -11060,6 +11881,7 @@ async function attemptLogin(applicationToken, overlay, fastMode = false, isRetry
11060
11881
  });
11061
11882
  }
11062
11883
  video.srcObject = stream;
11884
+ guideModal.update(stream, "searching", "Centralize o rosto");
11063
11885
  await new Promise((resolve) => {
11064
11886
  if (video.readyState >= 1) {
11065
11887
  resolve();
@@ -11088,18 +11910,19 @@ async function attemptLogin(applicationToken, overlay, fastMode = false, isRetry
11088
11910
  checkSize();
11089
11911
  }
11090
11912
  });
11091
- overlay.updateStatus("detecting");
11092
11913
  await new Promise((resolve) => setTimeout(resolve, CAMERA_STABILITY_DELAY));
11093
11914
  let faceDetected = true;
11094
11915
  if (!fastMode) {
11095
- faceDetected = await detectFaceQuickly(
11916
+ faceDetected = await detectFaceWithPosition(
11096
11917
  video,
11918
+ stream,
11919
+ guideModal,
11097
11920
  FACE_DETECTION_TIME,
11098
- DETECTION_INTERVAL,
11099
- overlay
11921
+ DETECTION_INTERVAL
11100
11922
  );
11101
11923
  }
11102
11924
  if (!faceDetected) {
11925
+ guideModal.close();
11103
11926
  if (stream) {
11104
11927
  stream.getTracks().forEach((track) => track.stop());
11105
11928
  }
@@ -11107,16 +11930,17 @@ async function attemptLogin(applicationToken, overlay, fastMode = false, isRetry
11107
11930
  video.parentElement.removeChild(video);
11108
11931
  }
11109
11932
  throw new NeoFaceError(
11110
- "Nenhum rosto detectado. Por favor, posicione seu rosto na frente da câmera e tente novamente.",
11933
+ "Nenhum rosto centralizado detectado. Por favor, tente novamente.",
11111
11934
  ErrorType.VALIDATION_ERROR
11112
11935
  );
11113
11936
  }
11114
- overlay.updateStatus("capturing");
11937
+ guideModal.update(stream, "centered", "Capturando imagem...");
11115
11938
  const canvas = document.createElement("canvas");
11116
11939
  canvas.width = video.videoWidth;
11117
11940
  canvas.height = video.videoHeight;
11118
11941
  const ctx = canvas.getContext("2d");
11119
11942
  if (!ctx) {
11943
+ guideModal.close();
11120
11944
  if (stream) {
11121
11945
  stream.getTracks().forEach((track) => track.stop());
11122
11946
  }
@@ -11126,12 +11950,6 @@ async function attemptLogin(applicationToken, overlay, fastMode = false, isRetry
11126
11950
  throw new NeoFaceError("Erro ao capturar imagem", ErrorType.CAPTURE_ERROR);
11127
11951
  }
11128
11952
  ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
11129
- if (stream) {
11130
- stream.getTracks().forEach((track) => track.stop());
11131
- }
11132
- if (video.parentElement) {
11133
- video.parentElement.removeChild(video);
11134
- }
11135
11953
  const imageBlob = await new Promise((resolve, reject) => {
11136
11954
  canvas.toBlob(
11137
11955
  (blob) => {
@@ -11146,13 +11964,21 @@ async function attemptLogin(applicationToken, overlay, fastMode = false, isRetry
11146
11964
  0.85
11147
11965
  );
11148
11966
  });
11149
- overlay.updateStatus("verifying");
11967
+ guideModal.update(stream, "centered", "Verificando com o servidor...");
11150
11968
  const result = await loginWithBiometric(imageBlob, applicationToken);
11969
+ guideModal.close();
11970
+ if (stream) {
11971
+ stream.getTracks().forEach((track) => track.stop());
11972
+ }
11973
+ if (video.parentElement) {
11974
+ video.parentElement.removeChild(video);
11975
+ }
11151
11976
  if (!result.success) {
11152
11977
  throw new NeoFaceError("Login falhou", ErrorType.LOGIN_FAILED);
11153
11978
  }
11154
11979
  return result;
11155
11980
  } catch (error) {
11981
+ guideModal.close();
11156
11982
  if (stream) {
11157
11983
  stream.getTracks().forEach((track) => track.stop());
11158
11984
  }
@@ -11177,7 +12003,6 @@ async function executeBiometricLoginFlow(options) {
11177
12003
  onCancel,
11178
12004
  fastMode = false
11179
12005
  } = options;
11180
- const overlay = new BiometricStatusOverlay();
11181
12006
  const fallbackPrompt = new FallbackPrompt();
11182
12007
  let attempts = 0;
11183
12008
  let lastError;
@@ -11185,34 +12010,26 @@ async function executeBiometricLoginFlow(options) {
11185
12010
  preloadFaceDetectionModels().catch(() => {
11186
12011
  });
11187
12012
  }
11188
- overlay.show("preparing");
11189
12013
  const tryLogin = async () => {
11190
12014
  try {
11191
12015
  attempts++;
11192
- const result = await attemptLogin(applicationToken, overlay, fastMode, attempts > 1);
11193
- overlay.updateStatus("success");
11194
- setTimeout(() => {
11195
- overlay.close();
11196
- onSuccess(result);
11197
- }, SUCCESS_DELAY);
12016
+ const result = await attemptLogin(applicationToken, fastMode, attempts > 1, onCancel);
12017
+ onSuccess(result);
11198
12018
  } catch (error) {
11199
12019
  lastError = error instanceof NeoFaceError ? error : new NeoFaceError(
11200
12020
  error instanceof Error ? error.message : "Erro desconhecido",
11201
12021
  ErrorType.UNKNOWN
11202
12022
  );
11203
- overlay.updateStatus("error");
11204
12023
  if (attempts < MAX_ATTEMPTS$1) {
11205
12024
  setTimeout(() => {
11206
12025
  tryLogin();
11207
12026
  }, RETRY_DELAY);
11208
12027
  } else {
11209
12028
  setTimeout(() => {
11210
- overlay.close();
11211
12029
  fallbackPrompt.show(
11212
12030
  lastError,
11213
12031
  () => {
11214
12032
  attempts = 0;
11215
- overlay.show("preparing");
11216
12033
  setTimeout(() => tryLogin(), RETRY_DELAY);
11217
12034
  },
11218
12035
  () => {
@@ -11234,9 +12051,7 @@ async function executeBiometricLoginFlow(options) {
11234
12051
  }
11235
12052
  }
11236
12053
  };
11237
- {
11238
- tryLogin();
11239
- }
12054
+ tryLogin();
11240
12055
  }
11241
12056
  const biometricLoginFlow = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
11242
12057
  __proto__: null,
@@ -11245,6 +12060,25 @@ const biometricLoginFlow = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.
11245
12060
  preloadFaceDetectionModels
11246
12061
  }, Symbol.toStringTag, { value: "Module" }));
11247
12062
  const CONSENT_DENIED_MESSAGE$1 = "Consentimento recusado pelo usuário.";
12063
+ function openPasswordFallback(options) {
12064
+ if (!getAutoFallback().toPassword) {
12065
+ options.onError(
12066
+ new NeoFaceError(
12067
+ "Biometria não reconhecida e a queda automática para senha está desligada.",
12068
+ ErrorType.LOGIN_FAILED
12069
+ )
12070
+ );
12071
+ return;
12072
+ }
12073
+ const fallbackOptions = {
12074
+ applicationToken: options.applicationToken,
12075
+ onSuccess: options.onSuccess,
12076
+ onError: options.onError,
12077
+ title: "Login Alternativo",
12078
+ subtitle: "Biometria não reconhecida. Por favor, informe email e senha."
12079
+ };
12080
+ new EmailPasswordModal(fallbackOptions).open();
12081
+ }
11248
12082
  async function startFaceLogin(options) {
11249
12083
  const consentAccepted = await requestConsent({
11250
12084
  appName: options.appName,
@@ -11259,17 +12093,7 @@ async function startFaceLogin(options) {
11259
12093
  applicationToken: options.applicationToken,
11260
12094
  onSuccess: options.onSuccess,
11261
12095
  onError: options.onError,
11262
- onFallbackRequest: options.onFallbackRequest || (() => {
11263
- const fallbackOptions = {
11264
- applicationToken: options.applicationToken,
11265
- onSuccess: options.onSuccess,
11266
- onError: options.onError,
11267
- title: "Login Alternativo",
11268
- subtitle: "Biometria não reconhecida. Por favor, informe email e senha."
11269
- };
11270
- const modal = new EmailPasswordModal(fallbackOptions);
11271
- modal.open();
11272
- }),
12096
+ onFallbackRequest: options.onFallbackRequest || (() => openPasswordFallback(options)),
11273
12097
  onCancel: options.onCancel
11274
12098
  });
11275
12099
  } catch (error) {
@@ -12438,15 +13262,23 @@ const authorizeOperation = async (applicationToken, cpf, options) => {
12438
13262
  resolve();
12439
13263
  };
12440
13264
  });
13265
+ const guideModal = showCameraOvalGuideModal({
13266
+ stream,
13267
+ faceState: "centered",
13268
+ instruction: PHOTO_INSTRUCTIONS[0]
13269
+ });
12441
13270
  const canvas = document.createElement("canvas");
12442
13271
  canvas.width = video.videoWidth;
12443
13272
  canvas.height = video.videoHeight;
12444
13273
  const ctx = canvas.getContext("2d");
12445
13274
  if (!ctx) {
13275
+ guideModal.close();
13276
+ stream.getTracks().forEach((track) => track.stop());
12446
13277
  throw new NeoFaceError("Failed to get canvas context", ErrorType.CAPTURE_ERROR);
12447
13278
  }
12448
13279
  for (let i = 0; i < TOTAL_PHOTOS; i++) {
12449
13280
  const instruction = PHOTO_INSTRUCTIONS[i];
13281
+ guideModal.update(stream, "centered", instruction);
12450
13282
  if (options == null ? void 0 : options.onProgress) {
12451
13283
  options.onProgress(i + 1, TOTAL_PHOTOS, instruction);
12452
13284
  }
@@ -12472,6 +13304,7 @@ const authorizeOperation = async (applicationToken, cpf, options) => {
12472
13304
  options.onPhotoTaken(i + 1, blob);
12473
13305
  }
12474
13306
  }
13307
+ guideModal.close();
12475
13308
  stream.getTracks().forEach((track) => track.stop());
12476
13309
  const lastPhoto = capturedPhotos[TOTAL_PHOTOS - 1];
12477
13310
  const sessionData = {
@@ -12519,110 +13352,6 @@ const authorizeOperation = async (applicationToken, cpf, options) => {
12519
13352
  throw new NeoFaceError("Unknown error during authorization", ErrorType.UNKNOWN);
12520
13353
  }
12521
13354
  };
12522
- function Sheet({
12523
- open,
12524
- onDismiss,
12525
- dismissOnOverlay = true,
12526
- ariaLabel,
12527
- ariaLabelledBy,
12528
- children,
12529
- className,
12530
- style
12531
- }) {
12532
- useEffect(() => {
12533
- injectThemeStyles();
12534
- }, []);
12535
- if (!open) return null;
12536
- const handleOverlay = () => {
12537
- if (dismissOnOverlay && onDismiss) onDismiss();
12538
- };
12539
- return /* @__PURE__ */ jsx(
12540
- "div",
12541
- {
12542
- className: "neofaceid-root",
12543
- role: "presentation",
12544
- style: {
12545
- position: "fixed",
12546
- inset: 0,
12547
- zIndex: 10001,
12548
- display: "flex",
12549
- alignItems: "center",
12550
- justifyContent: "center",
12551
- padding: 20,
12552
- background: `var(${CSS_VAR.overlay})`,
12553
- backdropFilter: "blur(3px)",
12554
- WebkitBackdropFilter: "blur(3px)"
12555
- },
12556
- onClick: handleOverlay,
12557
- children: /* @__PURE__ */ jsx(
12558
- "div",
12559
- {
12560
- role: "dialog",
12561
- "aria-modal": "true",
12562
- "aria-label": ariaLabel,
12563
- "aria-labelledby": ariaLabelledBy,
12564
- className,
12565
- onClick: (e) => e.stopPropagation(),
12566
- style: {
12567
- background: `var(${CSS_VAR.surface})`,
12568
- color: `var(${CSS_VAR.text})`,
12569
- borderRadius: `var(${CSS_VAR.radius}, 16px)`,
12570
- padding: 28,
12571
- maxWidth: 440,
12572
- width: "100%",
12573
- boxShadow: "0 1px 3px rgba(10,19,32,.08)",
12574
- boxSizing: "border-box",
12575
- ...style
12576
- },
12577
- children
12578
- }
12579
- )
12580
- }
12581
- );
12582
- }
12583
- const BASE_STYLE = {
12584
- minHeight: 44,
12585
- minWidth: 44,
12586
- padding: "12px 20px",
12587
- borderRadius: 16,
12588
- border: "none",
12589
- fontSize: 15,
12590
- fontWeight: 600,
12591
- fontFamily: "inherit",
12592
- cursor: "pointer",
12593
- transition: "background 120ms ease-out",
12594
- display: "inline-flex",
12595
- alignItems: "center",
12596
- justifyContent: "center",
12597
- gap: 8
12598
- };
12599
- const Button = forwardRef(
12600
- ({ variant = "primary", style, type = "button", ...rest }, ref) => {
12601
- const variantStyle = variant === "primary" ? {
12602
- background: `var(${CSS_VAR.action})`,
12603
- color: `var(${CSS_VAR.actionText})`
12604
- } : variant === "secondary" ? {
12605
- background: `var(${CSS_VAR.surfaceMuted})`,
12606
- color: `var(${CSS_VAR.text})`
12607
- } : {
12608
- background: "transparent",
12609
- color: `var(${CSS_VAR.textMuted})`
12610
- };
12611
- return (
12612
- // eslint-disable-next-line react/button-has-type
12613
- /* @__PURE__ */ jsx(
12614
- "button",
12615
- {
12616
- ref,
12617
- type,
12618
- ...rest,
12619
- style: { ...BASE_STYLE, ...variantStyle, ...style }
12620
- }
12621
- )
12622
- );
12623
- }
12624
- );
12625
- Button.displayName = "Button";
12626
13355
  function FocusFrame({
12627
13356
  size = 96,
12628
13357
  color,
@@ -12850,32 +13579,6 @@ function pointsFor(g) {
12850
13579
  return ["24 24", "24 24", "24 24"];
12851
13580
  }
12852
13581
  }
12853
- function StatusPill({ tone, children, icon, className }) {
12854
- const palette = SEMANTIC[tone];
12855
- return /* @__PURE__ */ jsxs(
12856
- "span",
12857
- {
12858
- className,
12859
- style: {
12860
- display: "inline-flex",
12861
- alignItems: "center",
12862
- gap: 6,
12863
- padding: "4px 10px",
12864
- borderRadius: 9999,
12865
- background: palette.bg,
12866
- color: palette.fg,
12867
- fontSize: 13,
12868
- fontWeight: 600,
12869
- fontFamily: "inherit",
12870
- lineHeight: 1.3
12871
- },
12872
- children: [
12873
- icon,
12874
- children
12875
- ]
12876
- }
12877
- );
12878
- }
12879
13582
  const DEFAULT = {
12880
13583
  title: "Não deu certo desta vez",
12881
13584
  hint: "Tente se posicionar em um ambiente mais estável.",
@@ -14646,15 +15349,18 @@ export {
14646
15349
  VERSION,
14647
15350
  authorize,
14648
15351
  authorizeOperation,
15352
+ awaitPushApproval,
14649
15353
  checkUserExistence,
14650
15354
  clearConsentTrail,
14651
15355
  completeOnboarding,
14652
15356
  completeOnboardingWithData,
14653
15357
  confirmPasswordReset,
15358
+ consumeSseStream,
14654
15359
  detectBiometricType,
14655
15360
  getAccent,
14656
15361
  getAppName,
14657
15362
  getApplicationToken,
15363
+ getAutoFallback,
14658
15364
  getBaseUrl,
14659
15365
  getCachedCaptureSession,
14660
15366
  getConfig,
@@ -14679,10 +15385,12 @@ export {
14679
15385
  recognizeBiometric,
14680
15386
  recognizeByPurpose,
14681
15387
  recordConsent,
15388
+ refreshSession,
14682
15389
  registerBiometric,
14683
15390
  registerPersonWithBiometric,
14684
15391
  registerPersonWithoutFace,
14685
15392
  requestConsent,
15393
+ requestDeviceEnrollmentToken,
14686
15394
  requestLivenessChallenge,
14687
15395
  requestLivenessChallengeWithSession,
14688
15396
  requestPasswordReset,