@neofaceid/web-sdk 1.35.1 → 1.36.1

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,16 +6553,15 @@ 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);
6365
6561
  const streamRef = useRef(null);
6366
6562
  const faceDetectionTimeoutRef = useRef(null);
6367
- const livenessCheckRef = useRef(null);
6368
6563
  const [state, dispatch] = useReducer(modalReducer$1, { status: "requestingPermission" });
6369
6564
  const [faceDetected, setFaceDetected] = useState(false);
6370
- const [livenessAttempts, setLivenessAttempts] = useState(0);
6371
6565
  const { startCamera, stopCamera } = useCamera();
6372
6566
  const isFaceCentered = (detection) => {
6373
6567
  if (!videoRef.current) return false;
@@ -6379,14 +6573,6 @@ function FaceCaptureModal({
6379
6573
  const centerHeight = videoHeight * 0.4;
6380
6574
  return box.x + box.width / 2 > centerX - centerWidth / 2 && box.x + box.width / 2 < centerX + centerWidth / 2 && box.y + box.height / 2 > centerY - centerHeight / 2 && box.y + box.height / 2 < centerY + centerHeight / 2;
6381
6575
  };
6382
- const startLivenessCheck = () => {
6383
- livenessCheckRef.current = window.setTimeout(() => {
6384
- if (state.status === "ready") {
6385
- setLivenessAttempts((prev) => prev + 1);
6386
- dispatch({ type: "START_CAPTURE" });
6387
- }
6388
- }, 3e3);
6389
- };
6390
6576
  const startFaceDetection = () => {
6391
6577
  if (!videoRef.current) return;
6392
6578
  const detectFaces = async () => {
@@ -6399,9 +6585,6 @@ function FaceCaptureModal({
6399
6585
  const isOneFace = detections.length === 1;
6400
6586
  const isCentered = isOneFace && isFaceCentered(detections[0]);
6401
6587
  setFaceDetected(isOneFace && isCentered);
6402
- if (isOneFace && isCentered && livenessAttempts === 0) {
6403
- startLivenessCheck();
6404
- }
6405
6588
  } catch (error) {
6406
6589
  console.error("Face detection error:", error);
6407
6590
  }
@@ -6412,6 +6595,8 @@ function FaceCaptureModal({
6412
6595
  useEffect(() => {
6413
6596
  injectThemeStyles();
6414
6597
  }, []);
6598
+ const flashEnabled = autoLighting && (state.status === "ready" || state.status === "capturing");
6599
+ const { flashOn } = useScreenFlash(videoRef, { enabled: flashEnabled });
6415
6600
  useEffect(() => {
6416
6601
  const loadModels = async () => {
6417
6602
  try {
@@ -6428,9 +6613,6 @@ function FaceCaptureModal({
6428
6613
  if (faceDetectionTimeoutRef.current) {
6429
6614
  window.clearTimeout(faceDetectionTimeoutRef.current);
6430
6615
  }
6431
- if (livenessCheckRef.current) {
6432
- window.clearInterval(livenessCheckRef.current);
6433
- }
6434
6616
  };
6435
6617
  }, []);
6436
6618
  useEffect(() => {
@@ -6497,9 +6679,9 @@ function FaceCaptureModal({
6497
6679
  };
6498
6680
  const handleRetry = () => {
6499
6681
  dispatch({ type: "RESET" });
6500
- setLivenessAttempts(0);
6501
6682
  };
6502
6683
  return /* @__PURE__ */ jsxs("div", { className: "neofaceid-root", style: styles.overlay, children: [
6684
+ /* @__PURE__ */ jsx(ScreenFlashOverlay, { active: flashOn }),
6503
6685
  /* @__PURE__ */ jsx("style", { children: keyframes }),
6504
6686
  /* @__PURE__ */ jsx("style", { children: mediaStyles }),
6505
6687
  /* @__PURE__ */ jsxs("div", { style: styles.modal, className: "modal", children: [
@@ -6985,7 +7167,8 @@ function BiometricRegistrationModal({
6985
7167
  onSuccess,
6986
7168
  onPhotosCaptured,
6987
7169
  onError,
6988
- onCannotGesture
7170
+ onCannotGesture,
7171
+ autoLighting = true
6989
7172
  }) {
6990
7173
  const videoRef = useRef(null);
6991
7174
  const canvasRef = useRef(null);
@@ -7007,6 +7190,8 @@ function BiometricRegistrationModal({
7007
7190
  useEffect(() => {
7008
7191
  injectThemeStyles();
7009
7192
  }, []);
7193
+ const flashEnabled = autoLighting && (state.status === "liveness" || state.status === "camera-loading");
7194
+ const { flashOn } = useScreenFlash(videoRef, { enabled: flashEnabled });
7010
7195
  useEffect(() => {
7011
7196
  if (state.status === "liveness") {
7012
7197
  const newStep = state.step;
@@ -8384,6 +8569,7 @@ function BiometricRegistrationModal({
8384
8569
  }
8385
8570
  }
8386
8571
  ` }),
8572
+ /* @__PURE__ */ jsx(ScreenFlashOverlay, { active: flashOn }),
8387
8573
  /* @__PURE__ */ jsx("div", { className: "neofaceid-root modal-overlay", children: /* @__PURE__ */ jsxs("div", { className: "modal-content", children: [
8388
8574
  /* @__PURE__ */ jsx("button", { type: "button", className: "close-button", onClick: onClose, children: "×" }),
8389
8575
  state.status === "liveness" && renderLivenessDetection(),
@@ -8770,6 +8956,122 @@ const DEFAULT_CONSENT_INFO = {
8770
8956
  privacyPolicyUrl: DEFAULT_PRIVACY_URL,
8771
8957
  retentionDays: DEFAULT_RETENTION_DAYS
8772
8958
  };
8959
+ const DARK_THRESHOLD = 0.235;
8960
+ const IMPROVEMENT_TARGET = 0.12;
8961
+ const SAMPLE_INTERVAL_MS = 600;
8962
+ const EVALUATION_DELAY_MS = 600;
8963
+ const MIN_TOGGLE_MS = 3e3;
8964
+ const OVERLAY_ID_PREFIX = "nfid-flash-";
8965
+ function createScreenFlashController(getVideo, options = {}) {
8966
+ const darkThreshold = options.darkThreshold ?? DARK_THRESHOLD;
8967
+ const improvementTarget = options.improvementTarget ?? IMPROVEMENT_TARGET;
8968
+ const sampleIntervalMs = options.sampleIntervalMs ?? SAMPLE_INTERVAL_MS;
8969
+ const minToggleMs = options.minToggleMs ?? MIN_TOGGLE_MS;
8970
+ const zIndex = options.zIndex ?? 10001;
8971
+ const overlayId = `${OVERLAY_ID_PREFIX}${Math.random().toString(36).slice(2, 9)}`;
8972
+ let interval = null;
8973
+ let evalTimer = null;
8974
+ let flashOn = false;
8975
+ let lastBrightness = 0;
8976
+ let darkStreak = 0;
8977
+ let lastToggleAt = 0;
8978
+ let preFlashBrightness = null;
8979
+ let overlayEl = null;
8980
+ let scratchCanvas = null;
8981
+ const showOverlay = () => {
8982
+ if (overlayEl || typeof document === "undefined") return;
8983
+ overlayEl = document.createElement("div");
8984
+ overlayEl.id = overlayId;
8985
+ overlayEl.setAttribute("aria-hidden", "true");
8986
+ overlayEl.style.cssText = `
8987
+ position: fixed; inset: 0; background: #FFFFFF;
8988
+ z-index: ${zIndex}; pointer-events: none;
8989
+ opacity: 0; transition: opacity 200ms ease-out;
8990
+ `;
8991
+ document.body.appendChild(overlayEl);
8992
+ requestAnimationFrame(() => {
8993
+ if (overlayEl) overlayEl.style.opacity = "1";
8994
+ });
8995
+ };
8996
+ const hideOverlay = () => {
8997
+ if (!overlayEl) return;
8998
+ const el = overlayEl;
8999
+ overlayEl = null;
9000
+ el.style.opacity = "0";
9001
+ setTimeout(() => {
9002
+ if (el.parentNode) el.parentNode.removeChild(el);
9003
+ }, 220);
9004
+ };
9005
+ const setFlashOn = (next) => {
9006
+ if (flashOn === next) return;
9007
+ flashOn = next;
9008
+ if (next) showOverlay();
9009
+ else hideOverlay();
9010
+ if (options.onChange) options.onChange(next, lastBrightness);
9011
+ };
9012
+ const tick = () => {
9013
+ if (typeof document !== "undefined" && !scratchCanvas) {
9014
+ scratchCanvas = document.createElement("canvas");
9015
+ }
9016
+ const b = measureFrameBrightness(getVideo(), {
9017
+ scratch: scratchCanvas ?? void 0
9018
+ });
9019
+ lastBrightness = b;
9020
+ const now = Date.now();
9021
+ if (!flashOn) {
9022
+ if (b > 0 && b < darkThreshold) darkStreak += 1;
9023
+ else darkStreak = 0;
9024
+ if (darkStreak >= 2 && now - lastToggleAt >= minToggleMs) {
9025
+ preFlashBrightness = b;
9026
+ lastToggleAt = now;
9027
+ darkStreak = 0;
9028
+ setFlashOn(true);
9029
+ evalTimer = setTimeout(() => {
9030
+ const after = measureFrameBrightness(getVideo(), {
9031
+ scratch: scratchCanvas ?? void 0
9032
+ });
9033
+ const gained = after - (preFlashBrightness ?? 0);
9034
+ if (gained < improvementTarget) {
9035
+ lastToggleAt = Date.now();
9036
+ setFlashOn(false);
9037
+ }
9038
+ preFlashBrightness = null;
9039
+ }, EVALUATION_DELAY_MS);
9040
+ }
9041
+ }
9042
+ };
9043
+ return {
9044
+ start() {
9045
+ if (options.enabled === false) return;
9046
+ if (interval) return;
9047
+ tick();
9048
+ interval = setInterval(tick, sampleIntervalMs);
9049
+ },
9050
+ stop() {
9051
+ if (interval) {
9052
+ clearInterval(interval);
9053
+ interval = null;
9054
+ }
9055
+ if (evalTimer) {
9056
+ clearTimeout(evalTimer);
9057
+ evalTimer = null;
9058
+ }
9059
+ setFlashOn(false);
9060
+ darkStreak = 0;
9061
+ preFlashBrightness = null;
9062
+ },
9063
+ destroy() {
9064
+ this.stop();
9065
+ scratchCanvas = null;
9066
+ },
9067
+ get flashOn() {
9068
+ return flashOn;
9069
+ },
9070
+ get lastBrightness() {
9071
+ return lastBrightness;
9072
+ }
9073
+ };
9074
+ }
8773
9075
  class BiometricCaptureModal {
8774
9076
  constructor(options) {
8775
9077
  __publicField(this, "modal", null);
@@ -8779,6 +9081,7 @@ class BiometricCaptureModal {
8779
9081
  __publicField(this, "options");
8780
9082
  __publicField(this, "countdownInterval", null);
8781
9083
  __publicField(this, "isCapturing", false);
9084
+ __publicField(this, "flashController", null);
8782
9085
  this.options = options;
8783
9086
  }
8784
9087
  /**
@@ -8789,6 +9092,10 @@ class BiometricCaptureModal {
8789
9092
  await this.createModal();
8790
9093
  await this.initializeCamera();
8791
9094
  this.enableCaptureButton();
9095
+ if (this.options.autoLighting !== false) {
9096
+ this.flashController = createScreenFlashController(() => this.video);
9097
+ this.flashController.start();
9098
+ }
8792
9099
  } catch (error) {
8793
9100
  this.options.onError(
8794
9101
  new NeoFaceError(
@@ -8974,6 +9281,10 @@ class BiometricCaptureModal {
8974
9281
  * Limpa recursos (câmera, intervalos, etc.)
8975
9282
  */
8976
9283
  cleanup() {
9284
+ if (this.flashController) {
9285
+ this.flashController.destroy();
9286
+ this.flashController = null;
9287
+ }
8977
9288
  if (this.stream) {
8978
9289
  this.stream.getTracks().forEach((track) => track.stop());
8979
9290
  this.stream = null;
@@ -10866,8 +11177,8 @@ const biometricDetection = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.
10866
11177
  initializeBiometricDetection,
10867
11178
  isAdvancedDetectionAvailable
10868
11179
  }, Symbol.toStringTag, { value: "Module" }));
10869
- const VERSION = "1.35.1";
10870
- const RELEASE_DATE = "2026-09-02";
11180
+ const VERSION = "1.36.1";
11181
+ const RELEASE_DATE = "2026-09-03";
10871
11182
  let cachedSession = null;
10872
11183
  function getCachedCaptureSession() {
10873
11184
  return cachedSession;
@@ -11132,9 +11443,12 @@ class OnboardingCaptureModal {
11132
11443
  __publicField(this, "subtitle");
11133
11444
  __publicField(this, "faceBlob", null);
11134
11445
  __publicField(this, "documentBlob", null);
11446
+ __publicField(this, "autoLighting");
11447
+ __publicField(this, "flashController", null);
11135
11448
  this.countdownSeconds = (options == null ? void 0 : options.countdown) ?? 3;
11136
11449
  this.title = options == null ? void 0 : options.title;
11137
11450
  this.subtitle = options == null ? void 0 : options.subtitle;
11451
+ this.autoLighting = (options == null ? void 0 : options.autoLighting) !== false;
11138
11452
  }
11139
11453
  /**
11140
11454
  * Abre o modal, inicia câmera e gerencia o fluxo de captura dos dois passos.
@@ -11248,6 +11562,10 @@ class OnboardingCaptureModal {
11248
11562
  }
11249
11563
  this.video.srcObject = this.stream;
11250
11564
  await this.video.play();
11565
+ if (this.autoLighting && !this.flashController) {
11566
+ this.flashController = createScreenFlashController(() => this.video);
11567
+ this.flashController.start();
11568
+ }
11251
11569
  }
11252
11570
  /**
11253
11571
  * Executa a contagem regressiva e atualiza textos para captura de rosto.
@@ -11332,6 +11650,10 @@ class OnboardingCaptureModal {
11332
11650
  * Fecha o modal e libera a câmera.
11333
11651
  */
11334
11652
  async close() {
11653
+ if (this.flashController) {
11654
+ this.flashController.destroy();
11655
+ this.flashController = null;
11656
+ }
11335
11657
  if (this.stream) {
11336
11658
  this.stream.getTracks().forEach((t) => t.stop());
11337
11659
  this.stream = null;
@@ -12465,201 +12787,6 @@ function AuthorizePermissionDenied({
12465
12787
  ] })
12466
12788
  ] });
12467
12789
  }
12468
- function measureFrameBrightness(video, options = {}) {
12469
- if (!video || video.videoWidth === 0 || video.videoHeight === 0) return 0;
12470
- const sample = options.sample ?? 64;
12471
- const vw = video.videoWidth;
12472
- const vh = video.videoHeight;
12473
- const roi = options.roi ?? centerRoi(vw, vh, 0.4);
12474
- const canvas = options.scratch ?? document.createElement("canvas");
12475
- canvas.width = sample;
12476
- canvas.height = sample;
12477
- const ctx = canvas.getContext("2d", { willReadFrequently: true });
12478
- if (!ctx) return 0;
12479
- try {
12480
- ctx.drawImage(
12481
- video,
12482
- roi.x,
12483
- roi.y,
12484
- roi.width,
12485
- roi.height,
12486
- 0,
12487
- 0,
12488
- sample,
12489
- sample
12490
- );
12491
- const data = ctx.getImageData(0, 0, sample, sample).data;
12492
- return computeLumaMean(data);
12493
- } catch {
12494
- return 0;
12495
- }
12496
- }
12497
- function centerRoi(vw, vh, fraction) {
12498
- const w = Math.round(vw * fraction);
12499
- const h = Math.round(vh * fraction);
12500
- return {
12501
- x: Math.round((vw - w) / 2),
12502
- y: Math.round((vh - h) / 2),
12503
- width: w,
12504
- height: h
12505
- };
12506
- }
12507
- function computeLumaMean(rgba) {
12508
- const len = rgba.length;
12509
- if (len === 0) return 0;
12510
- let sum = 0;
12511
- let n = 0;
12512
- for (let i = 0; i < len; i += 4) {
12513
- const r = rgba[i];
12514
- const g = rgba[i + 1];
12515
- const b = rgba[i + 2];
12516
- sum += 0.2126 * r + 0.7152 * g + 0.0722 * b;
12517
- n += 1;
12518
- }
12519
- if (n === 0) return 0;
12520
- return sum / (n * 255);
12521
- }
12522
- const DARK_THRESHOLD = 0.235;
12523
- const IMPROVEMENT_TARGET = 0.12;
12524
- const SAMPLE_INTERVAL_MS = 600;
12525
- const EVALUATION_DELAY_MS = 600;
12526
- const MIN_TOGGLE_MS = 3e3;
12527
- function useScreenFlash(videoRef, options = {}) {
12528
- const enabled = options.enabled ?? true;
12529
- const darkThreshold = options.darkThreshold ?? DARK_THRESHOLD;
12530
- const improvementTarget = options.improvementTarget ?? IMPROVEMENT_TARGET;
12531
- const sampleIntervalMs = options.sampleIntervalMs ?? SAMPLE_INTERVAL_MS;
12532
- const minToggleMs = options.minToggleMs ?? MIN_TOGGLE_MS;
12533
- const [flashOn, setFlashOn] = useState(false);
12534
- const [lastBrightness, setLastBrightness] = useState(0);
12535
- const darkStreakRef = useRef(0);
12536
- const lastToggleAtRef = useRef(0);
12537
- const preFlashBrightnessRef = useRef(null);
12538
- const flashOnRef = useRef(false);
12539
- const scratchRef = useRef(null);
12540
- useEffect(() => {
12541
- flashOnRef.current = flashOn;
12542
- }, [flashOn]);
12543
- useEffect(() => {
12544
- if (!enabled) {
12545
- setFlashOn(false);
12546
- return () => {
12547
- };
12548
- }
12549
- if (typeof document !== "undefined" && !scratchRef.current) {
12550
- scratchRef.current = document.createElement("canvas");
12551
- }
12552
- let timer = null;
12553
- let evalTimer = null;
12554
- let disposed = false;
12555
- const tick = () => {
12556
- if (disposed) return;
12557
- const b = measureFrameBrightness(videoRef.current, {
12558
- scratch: scratchRef.current ?? void 0
12559
- });
12560
- setLastBrightness(b);
12561
- const now = Date.now();
12562
- if (!flashOnRef.current) {
12563
- if (b > 0 && b < darkThreshold) {
12564
- darkStreakRef.current += 1;
12565
- } else {
12566
- darkStreakRef.current = 0;
12567
- }
12568
- if (darkStreakRef.current >= 2 && now - lastToggleAtRef.current >= minToggleMs) {
12569
- preFlashBrightnessRef.current = b;
12570
- lastToggleAtRef.current = now;
12571
- flashOnRef.current = true;
12572
- setFlashOn(true);
12573
- darkStreakRef.current = 0;
12574
- evalTimer = setTimeout(() => {
12575
- if (disposed) return;
12576
- const after = measureFrameBrightness(videoRef.current, {
12577
- scratch: scratchRef.current ?? void 0
12578
- });
12579
- const gained = after - (preFlashBrightnessRef.current ?? 0);
12580
- if (gained < improvementTarget) {
12581
- lastToggleAtRef.current = Date.now();
12582
- flashOnRef.current = false;
12583
- setFlashOn(false);
12584
- }
12585
- preFlashBrightnessRef.current = null;
12586
- }, EVALUATION_DELAY_MS);
12587
- }
12588
- }
12589
- };
12590
- timer = setInterval(tick, sampleIntervalMs);
12591
- tick();
12592
- return () => {
12593
- disposed = true;
12594
- if (timer) clearInterval(timer);
12595
- if (evalTimer) clearTimeout(evalTimer);
12596
- };
12597
- }, [enabled, darkThreshold, improvementTarget, sampleIntervalMs, minToggleMs, videoRef]);
12598
- return { flashOn, lastBrightness };
12599
- }
12600
- function ScreenFlashOverlay({
12601
- active,
12602
- zIndex = 10001,
12603
- onDismiss
12604
- }) {
12605
- if (!active) return null;
12606
- const overlayStyle = {
12607
- position: "fixed",
12608
- inset: 0,
12609
- background: "#FFFFFF",
12610
- zIndex,
12611
- pointerEvents: "none",
12612
- animation: "nfid-flash-in 200ms ease-out"
12613
- };
12614
- const hintStyle = {
12615
- position: "fixed",
12616
- top: 12,
12617
- left: "50%",
12618
- transform: "translateX(-50%)",
12619
- zIndex: zIndex + 1,
12620
- background: "rgba(10,19,32,.72)",
12621
- color: "#FFFFFF",
12622
- fontSize: 12,
12623
- fontWeight: 600,
12624
- letterSpacing: 0.2,
12625
- padding: "6px 12px",
12626
- borderRadius: 999,
12627
- pointerEvents: onDismiss ? "auto" : "none",
12628
- display: "flex",
12629
- alignItems: "center",
12630
- gap: 8
12631
- };
12632
- return /* @__PURE__ */ jsxs(Fragment, { children: [
12633
- /* @__PURE__ */ jsx("style", { children: `
12634
- @keyframes nfid-flash-in { from { opacity: 0 } to { opacity: 1 } }
12635
- @media (prefers-reduced-motion: reduce) {
12636
- .nfid-screen-flash { animation: none !important; }
12637
- }
12638
- ` }),
12639
- /* @__PURE__ */ jsx("div", { className: "nfid-screen-flash", style: overlayStyle, "aria-hidden": "true" }),
12640
- /* @__PURE__ */ jsxs("div", { className: "nfid-screen-flash-hint", style: hintStyle, role: "status", children: [
12641
- "Aumentando a luz da tela",
12642
- onDismiss && /* @__PURE__ */ jsx(
12643
- "button",
12644
- {
12645
- type: "button",
12646
- onClick: onDismiss,
12647
- "aria-label": "Desligar iluminação assistiva",
12648
- style: {
12649
- background: "transparent",
12650
- border: "none",
12651
- color: "#FFFFFF",
12652
- cursor: "pointer",
12653
- fontSize: 14,
12654
- lineHeight: 1,
12655
- padding: "0 4px"
12656
- },
12657
- children: "×"
12658
- }
12659
- )
12660
- ] })
12661
- ] });
12662
- }
12663
12790
  const STYLE_ID = "neofaceid-authorize-styles";
12664
12791
  const GESTURE_TO_GLYPH = {
12665
12792
  center: "center",
@@ -12993,6 +13120,7 @@ class DocumentCaptureModal {
12993
13120
  __publicField(this, "options");
12994
13121
  __publicField(this, "selectedDocument", null);
12995
13122
  __publicField(this, "currentStep", "select");
13123
+ __publicField(this, "flashController", null);
12996
13124
  __publicField(this, "frontBlob", null);
12997
13125
  __publicField(this, "backBlob", null);
12998
13126
  this.options = {
@@ -13384,6 +13512,7 @@ class DocumentCaptureModal {
13384
13512
  this.video.playsInline = true;
13385
13513
  this.video.srcObject = this.stream;
13386
13514
  await this.video.play();
13515
+ this.startScreenFlash();
13387
13516
  }
13388
13517
  } catch (error) {
13389
13518
  console.error("[DocumentCapture] Camera error:", error);
@@ -13396,6 +13525,7 @@ class DocumentCaptureModal {
13396
13525
  if (this.video) {
13397
13526
  this.video.srcObject = this.stream;
13398
13527
  await this.video.play();
13528
+ this.startScreenFlash();
13399
13529
  }
13400
13530
  } catch (fallbackError) {
13401
13531
  (_c = this.resolvePromise) == null ? void 0 : _c.call(this, {
@@ -13410,11 +13540,22 @@ class DocumentCaptureModal {
13410
13540
  }
13411
13541
  }
13412
13542
  stopCamera() {
13543
+ if (this.flashController) {
13544
+ this.flashController.destroy();
13545
+ this.flashController = null;
13546
+ }
13413
13547
  if (this.stream) {
13414
13548
  this.stream.getTracks().forEach((track) => track.stop());
13415
13549
  this.stream = null;
13416
13550
  }
13417
13551
  }
13552
+ // NEO-425 · US14.11.a — screen flash assistivo. Chamado após câmera acordar.
13553
+ startScreenFlash() {
13554
+ if (this.options.autoLighting === false) return;
13555
+ if (this.flashController) return;
13556
+ this.flashController = createScreenFlashController(() => this.video);
13557
+ this.flashController.start();
13558
+ }
13418
13559
  async handleCapture() {
13419
13560
  var _a2;
13420
13561
  if (!this.video || !this.canvas) return;