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