@neofaceid/web-sdk 1.35.0 → 1.36.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.
@@ -1,9 +1,9 @@
1
1
  var __defProp = Object.defineProperty;
2
2
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
3
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
- import React, { useRef, useState, useCallback, useReducer, useEffect, forwardRef } from "react";
4
+ import React, { useRef, useState, useCallback, useEffect, useReducer, forwardRef } from "react";
5
5
  import { createRoot } from "react-dom/client";
6
- import { jsxs, jsx, Fragment } from "react/jsx-runtime";
6
+ import { jsxs, Fragment, jsx } from "react/jsx-runtime";
7
7
  import * as faceapi from "face-api.js";
8
8
  class CameraPermissionDenied extends Error {
9
9
  constructor(message) {
@@ -6219,6 +6219,201 @@ const api = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty(
6219
6219
  validateToken,
6220
6220
  videoToBase64
6221
6221
  }, Symbol.toStringTag, { value: "Module" }));
6222
+ function measureFrameBrightness(video, options = {}) {
6223
+ if (!video || video.videoWidth === 0 || video.videoHeight === 0) return 0;
6224
+ const sample = options.sample ?? 64;
6225
+ const vw = video.videoWidth;
6226
+ const vh = video.videoHeight;
6227
+ const roi = options.roi ?? centerRoi(vw, vh, 0.4);
6228
+ const canvas = options.scratch ?? document.createElement("canvas");
6229
+ canvas.width = sample;
6230
+ canvas.height = sample;
6231
+ const ctx = canvas.getContext("2d", { willReadFrequently: true });
6232
+ if (!ctx) return 0;
6233
+ try {
6234
+ ctx.drawImage(
6235
+ video,
6236
+ roi.x,
6237
+ roi.y,
6238
+ roi.width,
6239
+ roi.height,
6240
+ 0,
6241
+ 0,
6242
+ sample,
6243
+ sample
6244
+ );
6245
+ const data = ctx.getImageData(0, 0, sample, sample).data;
6246
+ return computeLumaMean(data);
6247
+ } catch {
6248
+ return 0;
6249
+ }
6250
+ }
6251
+ function centerRoi(vw, vh, fraction) {
6252
+ const w = Math.round(vw * fraction);
6253
+ const h = Math.round(vh * fraction);
6254
+ return {
6255
+ x: Math.round((vw - w) / 2),
6256
+ y: Math.round((vh - h) / 2),
6257
+ width: w,
6258
+ height: h
6259
+ };
6260
+ }
6261
+ function computeLumaMean(rgba) {
6262
+ const len = rgba.length;
6263
+ if (len === 0) return 0;
6264
+ let sum = 0;
6265
+ let n = 0;
6266
+ for (let i = 0; i < len; i += 4) {
6267
+ const r = rgba[i];
6268
+ const g = rgba[i + 1];
6269
+ const b = rgba[i + 2];
6270
+ sum += 0.2126 * r + 0.7152 * g + 0.0722 * b;
6271
+ n += 1;
6272
+ }
6273
+ if (n === 0) return 0;
6274
+ return sum / (n * 255);
6275
+ }
6276
+ const DARK_THRESHOLD$1 = 0.235;
6277
+ const IMPROVEMENT_TARGET$1 = 0.12;
6278
+ const SAMPLE_INTERVAL_MS$1 = 600;
6279
+ const EVALUATION_DELAY_MS$1 = 600;
6280
+ const MIN_TOGGLE_MS$1 = 3e3;
6281
+ function useScreenFlash(videoRef, options = {}) {
6282
+ const enabled = options.enabled ?? true;
6283
+ const darkThreshold = options.darkThreshold ?? DARK_THRESHOLD$1;
6284
+ const improvementTarget = options.improvementTarget ?? IMPROVEMENT_TARGET$1;
6285
+ const sampleIntervalMs = options.sampleIntervalMs ?? SAMPLE_INTERVAL_MS$1;
6286
+ const minToggleMs = options.minToggleMs ?? MIN_TOGGLE_MS$1;
6287
+ const [flashOn, setFlashOn] = useState(false);
6288
+ const [lastBrightness, setLastBrightness] = useState(0);
6289
+ const darkStreakRef = useRef(0);
6290
+ const lastToggleAtRef = useRef(0);
6291
+ const preFlashBrightnessRef = useRef(null);
6292
+ const flashOnRef = useRef(false);
6293
+ const scratchRef = useRef(null);
6294
+ useEffect(() => {
6295
+ flashOnRef.current = flashOn;
6296
+ }, [flashOn]);
6297
+ useEffect(() => {
6298
+ if (!enabled) {
6299
+ setFlashOn(false);
6300
+ return () => {
6301
+ };
6302
+ }
6303
+ if (typeof document !== "undefined" && !scratchRef.current) {
6304
+ scratchRef.current = document.createElement("canvas");
6305
+ }
6306
+ let timer = null;
6307
+ let evalTimer = null;
6308
+ let disposed = false;
6309
+ const tick = () => {
6310
+ if (disposed) return;
6311
+ const b = measureFrameBrightness(videoRef.current, {
6312
+ scratch: scratchRef.current ?? void 0
6313
+ });
6314
+ setLastBrightness(b);
6315
+ const now = Date.now();
6316
+ if (!flashOnRef.current) {
6317
+ if (b > 0 && b < darkThreshold) {
6318
+ darkStreakRef.current += 1;
6319
+ } else {
6320
+ darkStreakRef.current = 0;
6321
+ }
6322
+ if (darkStreakRef.current >= 2 && now - lastToggleAtRef.current >= minToggleMs) {
6323
+ preFlashBrightnessRef.current = b;
6324
+ lastToggleAtRef.current = now;
6325
+ flashOnRef.current = true;
6326
+ setFlashOn(true);
6327
+ darkStreakRef.current = 0;
6328
+ evalTimer = setTimeout(() => {
6329
+ if (disposed) return;
6330
+ const after = measureFrameBrightness(videoRef.current, {
6331
+ scratch: scratchRef.current ?? void 0
6332
+ });
6333
+ const gained = after - (preFlashBrightnessRef.current ?? 0);
6334
+ if (gained < improvementTarget) {
6335
+ lastToggleAtRef.current = Date.now();
6336
+ flashOnRef.current = false;
6337
+ setFlashOn(false);
6338
+ }
6339
+ preFlashBrightnessRef.current = null;
6340
+ }, EVALUATION_DELAY_MS$1);
6341
+ }
6342
+ }
6343
+ };
6344
+ timer = setInterval(tick, sampleIntervalMs);
6345
+ tick();
6346
+ return () => {
6347
+ disposed = true;
6348
+ if (timer) clearInterval(timer);
6349
+ if (evalTimer) clearTimeout(evalTimer);
6350
+ };
6351
+ }, [enabled, darkThreshold, improvementTarget, sampleIntervalMs, minToggleMs, videoRef]);
6352
+ return { flashOn, lastBrightness };
6353
+ }
6354
+ function ScreenFlashOverlay({
6355
+ active,
6356
+ zIndex = 10001,
6357
+ onDismiss
6358
+ }) {
6359
+ if (!active) return null;
6360
+ const overlayStyle = {
6361
+ position: "fixed",
6362
+ inset: 0,
6363
+ background: "#FFFFFF",
6364
+ zIndex,
6365
+ pointerEvents: "none",
6366
+ animation: "nfid-flash-in 200ms ease-out"
6367
+ };
6368
+ const hintStyle = {
6369
+ position: "fixed",
6370
+ top: 12,
6371
+ left: "50%",
6372
+ transform: "translateX(-50%)",
6373
+ zIndex: zIndex + 1,
6374
+ background: "rgba(10,19,32,.72)",
6375
+ color: "#FFFFFF",
6376
+ fontSize: 12,
6377
+ fontWeight: 600,
6378
+ letterSpacing: 0.2,
6379
+ padding: "6px 12px",
6380
+ borderRadius: 999,
6381
+ pointerEvents: onDismiss ? "auto" : "none",
6382
+ display: "flex",
6383
+ alignItems: "center",
6384
+ gap: 8
6385
+ };
6386
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
6387
+ /* @__PURE__ */ jsx("style", { children: `
6388
+ @keyframes nfid-flash-in { from { opacity: 0 } to { opacity: 1 } }
6389
+ @media (prefers-reduced-motion: reduce) {
6390
+ .nfid-screen-flash { animation: none !important; }
6391
+ }
6392
+ ` }),
6393
+ /* @__PURE__ */ jsx("div", { className: "nfid-screen-flash", style: overlayStyle, "aria-hidden": "true" }),
6394
+ /* @__PURE__ */ jsxs("div", { className: "nfid-screen-flash-hint", style: hintStyle, role: "status", children: [
6395
+ "Aumentando a luz da tela",
6396
+ onDismiss && /* @__PURE__ */ jsx(
6397
+ "button",
6398
+ {
6399
+ type: "button",
6400
+ onClick: onDismiss,
6401
+ "aria-label": "Desligar iluminação assistiva",
6402
+ style: {
6403
+ background: "transparent",
6404
+ border: "none",
6405
+ color: "#FFFFFF",
6406
+ cursor: "pointer",
6407
+ fontSize: 14,
6408
+ lineHeight: 1,
6409
+ padding: "0 4px"
6410
+ },
6411
+ children: "×"
6412
+ }
6413
+ )
6414
+ ] })
6415
+ ] });
6416
+ }
6222
6417
  const modalReducer$1 = (state, action) => {
6223
6418
  switch (action.type) {
6224
6419
  case "PERMISSION_GRANTED":
@@ -6358,7 +6553,8 @@ function FaceCaptureModal({
6358
6553
  accessToken,
6359
6554
  onClose,
6360
6555
  onSuccess,
6361
- onCannotGesture
6556
+ onCannotGesture,
6557
+ autoLighting = true
6362
6558
  }) {
6363
6559
  const videoRef = useRef(null);
6364
6560
  const canvasRef = useRef(null);
@@ -6412,6 +6608,8 @@ function FaceCaptureModal({
6412
6608
  useEffect(() => {
6413
6609
  injectThemeStyles();
6414
6610
  }, []);
6611
+ const flashEnabled = autoLighting && (state.status === "ready" || state.status === "capturing");
6612
+ const { flashOn } = useScreenFlash(videoRef, { enabled: flashEnabled });
6415
6613
  useEffect(() => {
6416
6614
  const loadModels = async () => {
6417
6615
  try {
@@ -6500,6 +6698,7 @@ function FaceCaptureModal({
6500
6698
  setLivenessAttempts(0);
6501
6699
  };
6502
6700
  return /* @__PURE__ */ jsxs("div", { className: "neofaceid-root", style: styles.overlay, children: [
6701
+ /* @__PURE__ */ jsx(ScreenFlashOverlay, { active: flashOn }),
6503
6702
  /* @__PURE__ */ jsx("style", { children: keyframes }),
6504
6703
  /* @__PURE__ */ jsx("style", { children: mediaStyles }),
6505
6704
  /* @__PURE__ */ jsxs("div", { style: styles.modal, className: "modal", children: [
@@ -6985,7 +7184,8 @@ function BiometricRegistrationModal({
6985
7184
  onSuccess,
6986
7185
  onPhotosCaptured,
6987
7186
  onError,
6988
- onCannotGesture
7187
+ onCannotGesture,
7188
+ autoLighting = true
6989
7189
  }) {
6990
7190
  const videoRef = useRef(null);
6991
7191
  const canvasRef = useRef(null);
@@ -7007,6 +7207,8 @@ function BiometricRegistrationModal({
7007
7207
  useEffect(() => {
7008
7208
  injectThemeStyles();
7009
7209
  }, []);
7210
+ const flashEnabled = autoLighting && (state.status === "liveness" || state.status === "camera-loading");
7211
+ const { flashOn } = useScreenFlash(videoRef, { enabled: flashEnabled });
7010
7212
  useEffect(() => {
7011
7213
  if (state.status === "liveness") {
7012
7214
  const newStep = state.step;
@@ -8384,6 +8586,7 @@ function BiometricRegistrationModal({
8384
8586
  }
8385
8587
  }
8386
8588
  ` }),
8589
+ /* @__PURE__ */ jsx(ScreenFlashOverlay, { active: flashOn }),
8387
8590
  /* @__PURE__ */ jsx("div", { className: "neofaceid-root modal-overlay", children: /* @__PURE__ */ jsxs("div", { className: "modal-content", children: [
8388
8591
  /* @__PURE__ */ jsx("button", { type: "button", className: "close-button", onClick: onClose, children: "×" }),
8389
8592
  state.status === "liveness" && renderLivenessDetection(),
@@ -8770,6 +8973,122 @@ const DEFAULT_CONSENT_INFO = {
8770
8973
  privacyPolicyUrl: DEFAULT_PRIVACY_URL,
8771
8974
  retentionDays: DEFAULT_RETENTION_DAYS
8772
8975
  };
8976
+ const DARK_THRESHOLD = 0.235;
8977
+ const IMPROVEMENT_TARGET = 0.12;
8978
+ const SAMPLE_INTERVAL_MS = 600;
8979
+ const EVALUATION_DELAY_MS = 600;
8980
+ const MIN_TOGGLE_MS = 3e3;
8981
+ const OVERLAY_ID_PREFIX = "nfid-flash-";
8982
+ function createScreenFlashController(getVideo, options = {}) {
8983
+ const darkThreshold = options.darkThreshold ?? DARK_THRESHOLD;
8984
+ const improvementTarget = options.improvementTarget ?? IMPROVEMENT_TARGET;
8985
+ const sampleIntervalMs = options.sampleIntervalMs ?? SAMPLE_INTERVAL_MS;
8986
+ const minToggleMs = options.minToggleMs ?? MIN_TOGGLE_MS;
8987
+ const zIndex = options.zIndex ?? 10001;
8988
+ const overlayId = `${OVERLAY_ID_PREFIX}${Math.random().toString(36).slice(2, 9)}`;
8989
+ let interval = null;
8990
+ let evalTimer = null;
8991
+ let flashOn = false;
8992
+ let lastBrightness = 0;
8993
+ let darkStreak = 0;
8994
+ let lastToggleAt = 0;
8995
+ let preFlashBrightness = null;
8996
+ let overlayEl = null;
8997
+ let scratchCanvas = null;
8998
+ const showOverlay = () => {
8999
+ if (overlayEl || typeof document === "undefined") return;
9000
+ overlayEl = document.createElement("div");
9001
+ overlayEl.id = overlayId;
9002
+ overlayEl.setAttribute("aria-hidden", "true");
9003
+ overlayEl.style.cssText = `
9004
+ position: fixed; inset: 0; background: #FFFFFF;
9005
+ z-index: ${zIndex}; pointer-events: none;
9006
+ opacity: 0; transition: opacity 200ms ease-out;
9007
+ `;
9008
+ document.body.appendChild(overlayEl);
9009
+ requestAnimationFrame(() => {
9010
+ if (overlayEl) overlayEl.style.opacity = "1";
9011
+ });
9012
+ };
9013
+ const hideOverlay = () => {
9014
+ if (!overlayEl) return;
9015
+ const el = overlayEl;
9016
+ overlayEl = null;
9017
+ el.style.opacity = "0";
9018
+ setTimeout(() => {
9019
+ if (el.parentNode) el.parentNode.removeChild(el);
9020
+ }, 220);
9021
+ };
9022
+ const setFlashOn = (next) => {
9023
+ if (flashOn === next) return;
9024
+ flashOn = next;
9025
+ if (next) showOverlay();
9026
+ else hideOverlay();
9027
+ if (options.onChange) options.onChange(next, lastBrightness);
9028
+ };
9029
+ const tick = () => {
9030
+ if (typeof document !== "undefined" && !scratchCanvas) {
9031
+ scratchCanvas = document.createElement("canvas");
9032
+ }
9033
+ const b = measureFrameBrightness(getVideo(), {
9034
+ scratch: scratchCanvas ?? void 0
9035
+ });
9036
+ lastBrightness = b;
9037
+ const now = Date.now();
9038
+ if (!flashOn) {
9039
+ if (b > 0 && b < darkThreshold) darkStreak += 1;
9040
+ else darkStreak = 0;
9041
+ if (darkStreak >= 2 && now - lastToggleAt >= minToggleMs) {
9042
+ preFlashBrightness = b;
9043
+ lastToggleAt = now;
9044
+ darkStreak = 0;
9045
+ setFlashOn(true);
9046
+ evalTimer = setTimeout(() => {
9047
+ const after = measureFrameBrightness(getVideo(), {
9048
+ scratch: scratchCanvas ?? void 0
9049
+ });
9050
+ const gained = after - (preFlashBrightness ?? 0);
9051
+ if (gained < improvementTarget) {
9052
+ lastToggleAt = Date.now();
9053
+ setFlashOn(false);
9054
+ }
9055
+ preFlashBrightness = null;
9056
+ }, EVALUATION_DELAY_MS);
9057
+ }
9058
+ }
9059
+ };
9060
+ return {
9061
+ start() {
9062
+ if (options.enabled === false) return;
9063
+ if (interval) return;
9064
+ tick();
9065
+ interval = setInterval(tick, sampleIntervalMs);
9066
+ },
9067
+ stop() {
9068
+ if (interval) {
9069
+ clearInterval(interval);
9070
+ interval = null;
9071
+ }
9072
+ if (evalTimer) {
9073
+ clearTimeout(evalTimer);
9074
+ evalTimer = null;
9075
+ }
9076
+ setFlashOn(false);
9077
+ darkStreak = 0;
9078
+ preFlashBrightness = null;
9079
+ },
9080
+ destroy() {
9081
+ this.stop();
9082
+ scratchCanvas = null;
9083
+ },
9084
+ get flashOn() {
9085
+ return flashOn;
9086
+ },
9087
+ get lastBrightness() {
9088
+ return lastBrightness;
9089
+ }
9090
+ };
9091
+ }
8773
9092
  class BiometricCaptureModal {
8774
9093
  constructor(options) {
8775
9094
  __publicField(this, "modal", null);
@@ -8779,6 +9098,7 @@ class BiometricCaptureModal {
8779
9098
  __publicField(this, "options");
8780
9099
  __publicField(this, "countdownInterval", null);
8781
9100
  __publicField(this, "isCapturing", false);
9101
+ __publicField(this, "flashController", null);
8782
9102
  this.options = options;
8783
9103
  }
8784
9104
  /**
@@ -8789,6 +9109,10 @@ class BiometricCaptureModal {
8789
9109
  await this.createModal();
8790
9110
  await this.initializeCamera();
8791
9111
  this.enableCaptureButton();
9112
+ if (this.options.autoLighting !== false) {
9113
+ this.flashController = createScreenFlashController(() => this.video);
9114
+ this.flashController.start();
9115
+ }
8792
9116
  } catch (error) {
8793
9117
  this.options.onError(
8794
9118
  new NeoFaceError(
@@ -8974,6 +9298,10 @@ class BiometricCaptureModal {
8974
9298
  * Limpa recursos (câmera, intervalos, etc.)
8975
9299
  */
8976
9300
  cleanup() {
9301
+ if (this.flashController) {
9302
+ this.flashController.destroy();
9303
+ this.flashController = null;
9304
+ }
8977
9305
  if (this.stream) {
8978
9306
  this.stream.getTracks().forEach((track) => track.stop());
8979
9307
  this.stream = null;
@@ -10866,7 +11194,7 @@ const biometricDetection = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.
10866
11194
  initializeBiometricDetection,
10867
11195
  isAdvancedDetectionAvailable
10868
11196
  }, Symbol.toStringTag, { value: "Module" }));
10869
- const VERSION = "1.35.0";
11197
+ const VERSION = "1.36.0";
10870
11198
  const RELEASE_DATE = "2026-09-02";
10871
11199
  let cachedSession = null;
10872
11200
  function getCachedCaptureSession() {
@@ -10894,7 +11222,9 @@ async function openCaptureSession({
10894
11222
  "Content-Type": "application/json",
10895
11223
  "X-App-Token": applicationToken
10896
11224
  },
10897
- body: JSON.stringify({ purpose })
11225
+ // NEO-424 · backend real aceita apenas valores em UPPERCASE
11226
+ // (`AUTHORIZATION`, `LOGIN`, `ONBOARDING`, …). API pública mantém lowercase.
11227
+ body: JSON.stringify({ purpose: String(purpose).toUpperCase() })
10898
11228
  });
10899
11229
  } catch (err) {
10900
11230
  const message = err instanceof Error ? err.message : "Erro de rede";
@@ -10912,16 +11242,20 @@ async function openCaptureSession({
10912
11242
  const raw = await response.json().catch(() => null);
10913
11243
  const payload = (raw == null ? void 0 : raw.data) ?? raw;
10914
11244
  const sessionId = payload == null ? void 0 : payload.session_id;
10915
- const expiresAt = payload == null ? void 0 : payload.expires_at;
10916
- if (!sessionId || !expiresAt) {
11245
+ if (!sessionId) {
10917
11246
  throw new NeoFaceError(
10918
- "Resposta inválida ao abrir sessão de captura (campos ausentes)",
11247
+ "Resposta inválida ao abrir sessão de captura (session_id ausente)",
10919
11248
  ErrorType.API_ERROR
10920
11249
  );
10921
11250
  }
11251
+ const expiresAt = typeof (payload == null ? void 0 : payload.expires_at) === "string" && payload.expires_at.length > 0 ? payload.expires_at : deriveExpiresAt(payload == null ? void 0 : payload.expires_in);
10922
11252
  cachedSession = { session_id: sessionId, expires_at: expiresAt };
10923
11253
  return cachedSession;
10924
11254
  }
11255
+ function deriveExpiresAt(expiresIn) {
11256
+ const seconds = typeof expiresIn === "number" && Number.isFinite(expiresIn) ? expiresIn : 0;
11257
+ return new Date(Date.now() + seconds * 1e3).toISOString();
11258
+ }
10925
11259
  async function withCaptureSession(params, fn) {
10926
11260
  const session = cachedSession ?? await openCaptureSession(params);
10927
11261
  try {
@@ -11126,9 +11460,12 @@ class OnboardingCaptureModal {
11126
11460
  __publicField(this, "subtitle");
11127
11461
  __publicField(this, "faceBlob", null);
11128
11462
  __publicField(this, "documentBlob", null);
11463
+ __publicField(this, "autoLighting");
11464
+ __publicField(this, "flashController", null);
11129
11465
  this.countdownSeconds = (options == null ? void 0 : options.countdown) ?? 3;
11130
11466
  this.title = options == null ? void 0 : options.title;
11131
11467
  this.subtitle = options == null ? void 0 : options.subtitle;
11468
+ this.autoLighting = (options == null ? void 0 : options.autoLighting) !== false;
11132
11469
  }
11133
11470
  /**
11134
11471
  * Abre o modal, inicia câmera e gerencia o fluxo de captura dos dois passos.
@@ -11242,6 +11579,10 @@ class OnboardingCaptureModal {
11242
11579
  }
11243
11580
  this.video.srcObject = this.stream;
11244
11581
  await this.video.play();
11582
+ if (this.autoLighting && !this.flashController) {
11583
+ this.flashController = createScreenFlashController(() => this.video);
11584
+ this.flashController.start();
11585
+ }
11245
11586
  }
11246
11587
  /**
11247
11588
  * Executa a contagem regressiva e atualiza textos para captura de rosto.
@@ -11326,6 +11667,10 @@ class OnboardingCaptureModal {
11326
11667
  * Fecha o modal e libera a câmera.
11327
11668
  */
11328
11669
  async close() {
11670
+ if (this.flashController) {
11671
+ this.flashController.destroy();
11672
+ this.flashController = null;
11673
+ }
11329
11674
  if (this.stream) {
11330
11675
  this.stream.getTracks().forEach((t) => t.stop());
11331
11676
  this.stream = null;
@@ -12459,201 +12804,6 @@ function AuthorizePermissionDenied({
12459
12804
  ] })
12460
12805
  ] });
12461
12806
  }
12462
- function measureFrameBrightness(video, options = {}) {
12463
- if (!video || video.videoWidth === 0 || video.videoHeight === 0) return 0;
12464
- const sample = options.sample ?? 64;
12465
- const vw = video.videoWidth;
12466
- const vh = video.videoHeight;
12467
- const roi = options.roi ?? centerRoi(vw, vh, 0.4);
12468
- const canvas = options.scratch ?? document.createElement("canvas");
12469
- canvas.width = sample;
12470
- canvas.height = sample;
12471
- const ctx = canvas.getContext("2d", { willReadFrequently: true });
12472
- if (!ctx) return 0;
12473
- try {
12474
- ctx.drawImage(
12475
- video,
12476
- roi.x,
12477
- roi.y,
12478
- roi.width,
12479
- roi.height,
12480
- 0,
12481
- 0,
12482
- sample,
12483
- sample
12484
- );
12485
- const data = ctx.getImageData(0, 0, sample, sample).data;
12486
- return computeLumaMean(data);
12487
- } catch {
12488
- return 0;
12489
- }
12490
- }
12491
- function centerRoi(vw, vh, fraction) {
12492
- const w = Math.round(vw * fraction);
12493
- const h = Math.round(vh * fraction);
12494
- return {
12495
- x: Math.round((vw - w) / 2),
12496
- y: Math.round((vh - h) / 2),
12497
- width: w,
12498
- height: h
12499
- };
12500
- }
12501
- function computeLumaMean(rgba) {
12502
- const len = rgba.length;
12503
- if (len === 0) return 0;
12504
- let sum = 0;
12505
- let n = 0;
12506
- for (let i = 0; i < len; i += 4) {
12507
- const r = rgba[i];
12508
- const g = rgba[i + 1];
12509
- const b = rgba[i + 2];
12510
- sum += 0.2126 * r + 0.7152 * g + 0.0722 * b;
12511
- n += 1;
12512
- }
12513
- if (n === 0) return 0;
12514
- return sum / (n * 255);
12515
- }
12516
- const DARK_THRESHOLD = 0.235;
12517
- const IMPROVEMENT_TARGET = 0.12;
12518
- const SAMPLE_INTERVAL_MS = 600;
12519
- const EVALUATION_DELAY_MS = 600;
12520
- const MIN_TOGGLE_MS = 3e3;
12521
- function useScreenFlash(videoRef, options = {}) {
12522
- const enabled = options.enabled ?? true;
12523
- const darkThreshold = options.darkThreshold ?? DARK_THRESHOLD;
12524
- const improvementTarget = options.improvementTarget ?? IMPROVEMENT_TARGET;
12525
- const sampleIntervalMs = options.sampleIntervalMs ?? SAMPLE_INTERVAL_MS;
12526
- const minToggleMs = options.minToggleMs ?? MIN_TOGGLE_MS;
12527
- const [flashOn, setFlashOn] = useState(false);
12528
- const [lastBrightness, setLastBrightness] = useState(0);
12529
- const darkStreakRef = useRef(0);
12530
- const lastToggleAtRef = useRef(0);
12531
- const preFlashBrightnessRef = useRef(null);
12532
- const flashOnRef = useRef(false);
12533
- const scratchRef = useRef(null);
12534
- useEffect(() => {
12535
- flashOnRef.current = flashOn;
12536
- }, [flashOn]);
12537
- useEffect(() => {
12538
- if (!enabled) {
12539
- setFlashOn(false);
12540
- return () => {
12541
- };
12542
- }
12543
- if (typeof document !== "undefined" && !scratchRef.current) {
12544
- scratchRef.current = document.createElement("canvas");
12545
- }
12546
- let timer = null;
12547
- let evalTimer = null;
12548
- let disposed = false;
12549
- const tick = () => {
12550
- if (disposed) return;
12551
- const b = measureFrameBrightness(videoRef.current, {
12552
- scratch: scratchRef.current ?? void 0
12553
- });
12554
- setLastBrightness(b);
12555
- const now = Date.now();
12556
- if (!flashOnRef.current) {
12557
- if (b > 0 && b < darkThreshold) {
12558
- darkStreakRef.current += 1;
12559
- } else {
12560
- darkStreakRef.current = 0;
12561
- }
12562
- if (darkStreakRef.current >= 2 && now - lastToggleAtRef.current >= minToggleMs) {
12563
- preFlashBrightnessRef.current = b;
12564
- lastToggleAtRef.current = now;
12565
- flashOnRef.current = true;
12566
- setFlashOn(true);
12567
- darkStreakRef.current = 0;
12568
- evalTimer = setTimeout(() => {
12569
- if (disposed) return;
12570
- const after = measureFrameBrightness(videoRef.current, {
12571
- scratch: scratchRef.current ?? void 0
12572
- });
12573
- const gained = after - (preFlashBrightnessRef.current ?? 0);
12574
- if (gained < improvementTarget) {
12575
- lastToggleAtRef.current = Date.now();
12576
- flashOnRef.current = false;
12577
- setFlashOn(false);
12578
- }
12579
- preFlashBrightnessRef.current = null;
12580
- }, EVALUATION_DELAY_MS);
12581
- }
12582
- }
12583
- };
12584
- timer = setInterval(tick, sampleIntervalMs);
12585
- tick();
12586
- return () => {
12587
- disposed = true;
12588
- if (timer) clearInterval(timer);
12589
- if (evalTimer) clearTimeout(evalTimer);
12590
- };
12591
- }, [enabled, darkThreshold, improvementTarget, sampleIntervalMs, minToggleMs, videoRef]);
12592
- return { flashOn, lastBrightness };
12593
- }
12594
- function ScreenFlashOverlay({
12595
- active,
12596
- zIndex = 10001,
12597
- onDismiss
12598
- }) {
12599
- if (!active) return null;
12600
- const overlayStyle = {
12601
- position: "fixed",
12602
- inset: 0,
12603
- background: "#FFFFFF",
12604
- zIndex,
12605
- pointerEvents: "none",
12606
- animation: "nfid-flash-in 200ms ease-out"
12607
- };
12608
- const hintStyle = {
12609
- position: "fixed",
12610
- top: 12,
12611
- left: "50%",
12612
- transform: "translateX(-50%)",
12613
- zIndex: zIndex + 1,
12614
- background: "rgba(10,19,32,.72)",
12615
- color: "#FFFFFF",
12616
- fontSize: 12,
12617
- fontWeight: 600,
12618
- letterSpacing: 0.2,
12619
- padding: "6px 12px",
12620
- borderRadius: 999,
12621
- pointerEvents: onDismiss ? "auto" : "none",
12622
- display: "flex",
12623
- alignItems: "center",
12624
- gap: 8
12625
- };
12626
- return /* @__PURE__ */ jsxs(Fragment, { children: [
12627
- /* @__PURE__ */ jsx("style", { children: `
12628
- @keyframes nfid-flash-in { from { opacity: 0 } to { opacity: 1 } }
12629
- @media (prefers-reduced-motion: reduce) {
12630
- .nfid-screen-flash { animation: none !important; }
12631
- }
12632
- ` }),
12633
- /* @__PURE__ */ jsx("div", { className: "nfid-screen-flash", style: overlayStyle, "aria-hidden": "true" }),
12634
- /* @__PURE__ */ jsxs("div", { className: "nfid-screen-flash-hint", style: hintStyle, role: "status", children: [
12635
- "Aumentando a luz da tela",
12636
- onDismiss && /* @__PURE__ */ jsx(
12637
- "button",
12638
- {
12639
- type: "button",
12640
- onClick: onDismiss,
12641
- "aria-label": "Desligar iluminação assistiva",
12642
- style: {
12643
- background: "transparent",
12644
- border: "none",
12645
- color: "#FFFFFF",
12646
- cursor: "pointer",
12647
- fontSize: 14,
12648
- lineHeight: 1,
12649
- padding: "0 4px"
12650
- },
12651
- children: "×"
12652
- }
12653
- )
12654
- ] })
12655
- ] });
12656
- }
12657
12807
  const STYLE_ID = "neofaceid-authorize-styles";
12658
12808
  const GESTURE_TO_GLYPH = {
12659
12809
  center: "center",
@@ -12987,6 +13137,7 @@ class DocumentCaptureModal {
12987
13137
  __publicField(this, "options");
12988
13138
  __publicField(this, "selectedDocument", null);
12989
13139
  __publicField(this, "currentStep", "select");
13140
+ __publicField(this, "flashController", null);
12990
13141
  __publicField(this, "frontBlob", null);
12991
13142
  __publicField(this, "backBlob", null);
12992
13143
  this.options = {
@@ -13378,6 +13529,7 @@ class DocumentCaptureModal {
13378
13529
  this.video.playsInline = true;
13379
13530
  this.video.srcObject = this.stream;
13380
13531
  await this.video.play();
13532
+ this.startScreenFlash();
13381
13533
  }
13382
13534
  } catch (error) {
13383
13535
  console.error("[DocumentCapture] Camera error:", error);
@@ -13390,6 +13542,7 @@ class DocumentCaptureModal {
13390
13542
  if (this.video) {
13391
13543
  this.video.srcObject = this.stream;
13392
13544
  await this.video.play();
13545
+ this.startScreenFlash();
13393
13546
  }
13394
13547
  } catch (fallbackError) {
13395
13548
  (_c = this.resolvePromise) == null ? void 0 : _c.call(this, {
@@ -13404,11 +13557,22 @@ class DocumentCaptureModal {
13404
13557
  }
13405
13558
  }
13406
13559
  stopCamera() {
13560
+ if (this.flashController) {
13561
+ this.flashController.destroy();
13562
+ this.flashController = null;
13563
+ }
13407
13564
  if (this.stream) {
13408
13565
  this.stream.getTracks().forEach((track) => track.stop());
13409
13566
  this.stream = null;
13410
13567
  }
13411
13568
  }
13569
+ // NEO-425 · US14.11.a — screen flash assistivo. Chamado após câmera acordar.
13570
+ startScreenFlash() {
13571
+ if (this.options.autoLighting === false) return;
13572
+ if (this.flashController) return;
13573
+ this.flashController = createScreenFlashController(() => this.video);
13574
+ this.flashController.start();
13575
+ }
13412
13576
  async handleCapture() {
13413
13577
  var _a2;
13414
13578
  if (!this.video || !this.canvas) return;