@neofaceid/web-sdk 1.1.1 → 1.2.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.
|
@@ -1108,6 +1108,7 @@ const useCamera = () => {
|
|
|
1108
1108
|
};
|
|
1109
1109
|
var ErrorType = /* @__PURE__ */ ((ErrorType2) => {
|
|
1110
1110
|
ErrorType2["NETWORK"] = "NetworkError";
|
|
1111
|
+
ErrorType2["NETWORK_ERROR"] = "NetworkError";
|
|
1111
1112
|
ErrorType2["INVALID_TOKEN"] = "InvalidTokenError";
|
|
1112
1113
|
ErrorType2["RECOGNITION_FAILED"] = "RecognitionFailedError";
|
|
1113
1114
|
ErrorType2["LOGIN_FAILED"] = "LoginFailedError";
|
|
@@ -1115,12 +1116,18 @@ var ErrorType = /* @__PURE__ */ ((ErrorType2) => {
|
|
|
1115
1116
|
ErrorType2["API_ERROR"] = "ApiError";
|
|
1116
1117
|
ErrorType2["PERSON_NOT_FOUND"] = "PersonNotFoundError";
|
|
1117
1118
|
ErrorType2["INITIALIZATION_ERROR"] = "InitializationError";
|
|
1118
|
-
ErrorType2["NETWORK_ERROR"] = "NetworkError";
|
|
1119
1119
|
ErrorType2["CAMERA_ERROR"] = "CameraError";
|
|
1120
1120
|
ErrorType2["CAPTURE_ERROR"] = "CaptureError";
|
|
1121
|
+
ErrorType2["NOT_FOUND"] = "NotFoundError";
|
|
1122
|
+
ErrorType2["UNKNOWN"] = "UnknownError";
|
|
1121
1123
|
return ErrorType2;
|
|
1122
1124
|
})(ErrorType || {});
|
|
1123
1125
|
class NeoFaceError extends Error {
|
|
1126
|
+
/**
|
|
1127
|
+
* Constrói um erro do SDK com mensagem e tipo categórico.
|
|
1128
|
+
* @param message Mensagem descritiva do erro
|
|
1129
|
+
* @param type Tipo categórico do erro
|
|
1130
|
+
*/
|
|
1124
1131
|
constructor(message, type) {
|
|
1125
1132
|
super(message);
|
|
1126
1133
|
__publicField(this, "type");
|
|
@@ -1197,6 +1204,92 @@ const validateToken = async (applicationToken) => {
|
|
|
1197
1204
|
throw new NeoFaceError("Unknown error occurred", ErrorType.NETWORK_ERROR);
|
|
1198
1205
|
}
|
|
1199
1206
|
};
|
|
1207
|
+
const getOnboardingDetails = async (applicationToken, onboardingToken) => {
|
|
1208
|
+
ensureSecureContext();
|
|
1209
|
+
const controller = createTimeoutController();
|
|
1210
|
+
try {
|
|
1211
|
+
const response = await fetch(`${API_BASE_URL}/api/v1/onboardings/${encodeURIComponent(onboardingToken)}/`, {
|
|
1212
|
+
method: "GET",
|
|
1213
|
+
headers: {
|
|
1214
|
+
"X-App-Token": applicationToken
|
|
1215
|
+
},
|
|
1216
|
+
signal: controller.signal
|
|
1217
|
+
});
|
|
1218
|
+
if (!response.ok) {
|
|
1219
|
+
if (response.status === 401 || response.status === 403) {
|
|
1220
|
+
throw new NeoFaceError("Token de aplicação inválido ou expirado", ErrorType.INVALID_TOKEN);
|
|
1221
|
+
}
|
|
1222
|
+
if (response.status === 404) {
|
|
1223
|
+
throw new NeoFaceError("Link de onboarding não encontrado", ErrorType.NOT_FOUND);
|
|
1224
|
+
}
|
|
1225
|
+
throw new NeoFaceError(`Falha ao obter detalhes (${response.status})`, ErrorType.NETWORK);
|
|
1226
|
+
}
|
|
1227
|
+
const data = await response.json();
|
|
1228
|
+
return data;
|
|
1229
|
+
} catch (error) {
|
|
1230
|
+
if (error instanceof Error) {
|
|
1231
|
+
if (error.name === "AbortError") {
|
|
1232
|
+
throw new NeoFaceError("Tempo de requisição excedido", ErrorType.NETWORK);
|
|
1233
|
+
}
|
|
1234
|
+
if (error instanceof NeoFaceError) {
|
|
1235
|
+
throw error;
|
|
1236
|
+
}
|
|
1237
|
+
throw new NeoFaceError(error.message, ErrorType.NETWORK);
|
|
1238
|
+
}
|
|
1239
|
+
throw new NeoFaceError("Erro desconhecido", ErrorType.NETWORK_ERROR);
|
|
1240
|
+
}
|
|
1241
|
+
};
|
|
1242
|
+
const completeOnboarding = async (applicationToken, onboardingToken, faceImage, documentImage) => {
|
|
1243
|
+
ensureSecureContext();
|
|
1244
|
+
const controller = createTimeoutController();
|
|
1245
|
+
const compressedFace = await compressImage(faceImage);
|
|
1246
|
+
const compressedDocument = await compressImage(documentImage);
|
|
1247
|
+
try {
|
|
1248
|
+
const formData = new FormData();
|
|
1249
|
+
formData.append("face_image", compressedFace, "face.jpg");
|
|
1250
|
+
formData.append("document_image", compressedDocument, "document.jpg");
|
|
1251
|
+
const response = await fetch(
|
|
1252
|
+
`${API_BASE_URL}/api/v1/onboardings/${encodeURIComponent(onboardingToken)}/complete`,
|
|
1253
|
+
{
|
|
1254
|
+
method: "POST",
|
|
1255
|
+
headers: {
|
|
1256
|
+
"X-App-Token": applicationToken
|
|
1257
|
+
},
|
|
1258
|
+
body: formData,
|
|
1259
|
+
signal: controller.signal
|
|
1260
|
+
}
|
|
1261
|
+
);
|
|
1262
|
+
const data = await response.json();
|
|
1263
|
+
if (!response.ok) {
|
|
1264
|
+
if (response.status === 401 || response.status === 403) {
|
|
1265
|
+
throw new NeoFaceError("Token de aplicação inválido ou expirado", ErrorType.INVALID_TOKEN);
|
|
1266
|
+
}
|
|
1267
|
+
if (response.status === 404) {
|
|
1268
|
+
throw new NeoFaceError("Link de onboarding não encontrado", ErrorType.NOT_FOUND);
|
|
1269
|
+
}
|
|
1270
|
+
throw new NeoFaceError((data == null ? void 0 : data.error) || (data == null ? void 0 : data.message) || "Falha ao concluir onboarding", ErrorType.NETWORK);
|
|
1271
|
+
}
|
|
1272
|
+
return {
|
|
1273
|
+
success: !!data.success,
|
|
1274
|
+
message: data.message || "Onboarding concluído",
|
|
1275
|
+
person_id: data.person_id,
|
|
1276
|
+
identity_data_id: data.identity_data_id,
|
|
1277
|
+
confidence_score: data.confidence_score,
|
|
1278
|
+
processing_time: data.processing_time
|
|
1279
|
+
};
|
|
1280
|
+
} catch (error) {
|
|
1281
|
+
if (error instanceof Error) {
|
|
1282
|
+
if (error.name === "AbortError") {
|
|
1283
|
+
throw new NeoFaceError("Tempo de requisição excedido", ErrorType.NETWORK);
|
|
1284
|
+
}
|
|
1285
|
+
if (error instanceof NeoFaceError) {
|
|
1286
|
+
throw error;
|
|
1287
|
+
}
|
|
1288
|
+
throw new NeoFaceError(error.message, ErrorType.NETWORK);
|
|
1289
|
+
}
|
|
1290
|
+
throw new NeoFaceError("Erro desconhecido", ErrorType.NETWORK_ERROR);
|
|
1291
|
+
}
|
|
1292
|
+
};
|
|
1200
1293
|
const recognize = async (image, applicationToken) => {
|
|
1201
1294
|
ensureSecureContext();
|
|
1202
1295
|
const controller = createTimeoutController();
|
|
@@ -1485,10 +1578,10 @@ const registerPersonWithoutFace = async (personData, applicationToken) => {
|
|
|
1485
1578
|
const registerPersonWithBiometric = async (personData, facePhotos, applicationToken) => {
|
|
1486
1579
|
ensureSecureContext();
|
|
1487
1580
|
if (!facePhotos || facePhotos.length === 0) {
|
|
1488
|
-
throw new NeoFaceError("At least one face photo is required for biometric registration", ErrorType.
|
|
1581
|
+
throw new NeoFaceError("At least one face photo is required for biometric registration", ErrorType.VALIDATION_ERROR);
|
|
1489
1582
|
}
|
|
1490
1583
|
if (facePhotos.length > 5) {
|
|
1491
|
-
throw new NeoFaceError("Maximum of 5 face photos allowed", ErrorType.
|
|
1584
|
+
throw new NeoFaceError("Maximum of 5 face photos allowed", ErrorType.VALIDATION_ERROR);
|
|
1492
1585
|
}
|
|
1493
1586
|
const controller = createTimeoutController();
|
|
1494
1587
|
try {
|
|
@@ -1530,13 +1623,13 @@ const registerPersonWithBiometric = async (personData, facePhotos, applicationTo
|
|
|
1530
1623
|
try {
|
|
1531
1624
|
const errorData = await response.json();
|
|
1532
1625
|
if (errorData.error === "fraud_detected") {
|
|
1533
|
-
throw new NeoFaceError("Fraud detected in biometric images. Registration blocked.", ErrorType.
|
|
1626
|
+
throw new NeoFaceError("Fraud detected in biometric images. Registration blocked.", ErrorType.VALIDATION_ERROR);
|
|
1534
1627
|
}
|
|
1535
1628
|
if (errorData.error === "user_exists") {
|
|
1536
|
-
throw new NeoFaceError("User with this email already exists", ErrorType.
|
|
1629
|
+
throw new NeoFaceError("User with this email already exists", ErrorType.VALIDATION_ERROR);
|
|
1537
1630
|
}
|
|
1538
1631
|
if (errorData.error === "person_exists") {
|
|
1539
|
-
throw new NeoFaceError("Person with this CPF already exists", ErrorType.
|
|
1632
|
+
throw new NeoFaceError("Person with this CPF already exists", ErrorType.VALIDATION_ERROR);
|
|
1540
1633
|
}
|
|
1541
1634
|
throw new NeoFaceError(errorData.message || `Server returned status ${response.status}`, ErrorType.NETWORK);
|
|
1542
1635
|
} catch (parseError) {
|
|
@@ -1566,6 +1659,52 @@ const registerPersonWithBiometric = async (personData, facePhotos, applicationTo
|
|
|
1566
1659
|
throw new NeoFaceError("Unknown error occurred", ErrorType.NETWORK_ERROR);
|
|
1567
1660
|
}
|
|
1568
1661
|
};
|
|
1662
|
+
const loginWithEmail = async (email, password, applicationToken) => {
|
|
1663
|
+
ensureSecureContext();
|
|
1664
|
+
const controller = createTimeoutController();
|
|
1665
|
+
try {
|
|
1666
|
+
const response = await fetch(`${API_BASE_URL}/api/v1/auth/login/`, {
|
|
1667
|
+
method: "POST",
|
|
1668
|
+
headers: {
|
|
1669
|
+
"Content-Type": "application/json",
|
|
1670
|
+
"X-App-Token": applicationToken
|
|
1671
|
+
},
|
|
1672
|
+
body: JSON.stringify({ email, password }),
|
|
1673
|
+
signal: controller.signal
|
|
1674
|
+
});
|
|
1675
|
+
if (!response.ok) {
|
|
1676
|
+
if (response.status === 401 || response.status === 403) {
|
|
1677
|
+
throw new NeoFaceError("Invalid credentials or application token", ErrorType.INVALID_TOKEN);
|
|
1678
|
+
}
|
|
1679
|
+
throw new NeoFaceError(`Server returned status ${response.status}`, ErrorType.NETWORK);
|
|
1680
|
+
}
|
|
1681
|
+
const data = await response.json();
|
|
1682
|
+
return {
|
|
1683
|
+
success: true,
|
|
1684
|
+
// Compatível com diferentes formatos de resposta do backend
|
|
1685
|
+
accessToken: data.access_token || data.access,
|
|
1686
|
+
alias: data.user && data.user.alias || data.alias,
|
|
1687
|
+
user: {
|
|
1688
|
+
id: data.user && data.user.id || "",
|
|
1689
|
+
email: data.user && data.user.email || "",
|
|
1690
|
+
role: data.user && data.user.role || "",
|
|
1691
|
+
validated: data.user && data.user.validated || false,
|
|
1692
|
+
active: data.user && data.user.active || false
|
|
1693
|
+
}
|
|
1694
|
+
};
|
|
1695
|
+
} catch (error) {
|
|
1696
|
+
if (error instanceof Error) {
|
|
1697
|
+
if (error.name === "AbortError") {
|
|
1698
|
+
throw new NeoFaceError("Request timed out", ErrorType.NETWORK);
|
|
1699
|
+
}
|
|
1700
|
+
if (error instanceof NeoFaceError) {
|
|
1701
|
+
throw error;
|
|
1702
|
+
}
|
|
1703
|
+
throw new NeoFaceError(error.message, ErrorType.NETWORK);
|
|
1704
|
+
}
|
|
1705
|
+
throw new NeoFaceError("Unknown error occurred", ErrorType.NETWORK_ERROR);
|
|
1706
|
+
}
|
|
1707
|
+
};
|
|
1569
1708
|
const modalReducer$1 = (state, action) => {
|
|
1570
1709
|
switch (action.type) {
|
|
1571
1710
|
case "PERMISSION_GRANTED":
|
|
@@ -3169,9 +3308,10 @@ class BiometricCaptureModal {
|
|
|
3169
3308
|
context.drawImage(this.video, 0, 0, this.canvas.width, this.canvas.height);
|
|
3170
3309
|
const imageData = this.canvas.toDataURL("image/jpeg", 0.8);
|
|
3171
3310
|
const base64Data = imageData.split(",")[1];
|
|
3311
|
+
const dataUrl = `data:image/jpeg;base64,${base64Data}`;
|
|
3172
3312
|
let detectedType = this.options.mode === "auto" ? await this.detectBiometricType(base64Data) : this.options.mode;
|
|
3173
3313
|
this.close();
|
|
3174
|
-
this.options.onSuccess(
|
|
3314
|
+
this.options.onSuccess(dataUrl, detectedType);
|
|
3175
3315
|
} catch (error) {
|
|
3176
3316
|
this.options.onError(
|
|
3177
3317
|
new NeoFaceError(
|
|
@@ -3507,6 +3647,70 @@ class BiometricCaptureModal {
|
|
|
3507
3647
|
document.head.appendChild(style);
|
|
3508
3648
|
}
|
|
3509
3649
|
}
|
|
3650
|
+
class EmailPasswordModal {
|
|
3651
|
+
constructor(options) {
|
|
3652
|
+
__publicField(this, "options");
|
|
3653
|
+
__publicField(this, "modalElement", null);
|
|
3654
|
+
this.options = options;
|
|
3655
|
+
}
|
|
3656
|
+
/**
|
|
3657
|
+
* Abre o modal de login com email e senha.
|
|
3658
|
+
*/
|
|
3659
|
+
async open() {
|
|
3660
|
+
this.createModal();
|
|
3661
|
+
document.body.appendChild(this.modalElement);
|
|
3662
|
+
}
|
|
3663
|
+
/**
|
|
3664
|
+
* Fecha o modal.
|
|
3665
|
+
*/
|
|
3666
|
+
close() {
|
|
3667
|
+
if (this.modalElement) {
|
|
3668
|
+
document.body.removeChild(this.modalElement);
|
|
3669
|
+
this.modalElement = null;
|
|
3670
|
+
}
|
|
3671
|
+
if (this.options.onCancel) {
|
|
3672
|
+
this.options.onCancel();
|
|
3673
|
+
}
|
|
3674
|
+
}
|
|
3675
|
+
createModal() {
|
|
3676
|
+
this.modalElement = document.createElement("div");
|
|
3677
|
+
this.modalElement.className = "neoface-modal";
|
|
3678
|
+
this.modalElement.innerHTML = `
|
|
3679
|
+
<div class="modal-content">
|
|
3680
|
+
<h2>${this.options.title || "Login com Email e Senha"}</h2>
|
|
3681
|
+
<p>${this.options.subtitle || "Informe seus dados para login."}</p>
|
|
3682
|
+
<form id="email-password-form">
|
|
3683
|
+
<input type="email" id="email" placeholder="Email" required />
|
|
3684
|
+
<input type="password" id="password" placeholder="Senha" required />
|
|
3685
|
+
<button type="submit">Entrar</button>
|
|
3686
|
+
</form>
|
|
3687
|
+
<button class="cancel-button">Cancelar</button>
|
|
3688
|
+
</div>
|
|
3689
|
+
`;
|
|
3690
|
+
const form = this.modalElement.querySelector("#email-password-form");
|
|
3691
|
+
form.addEventListener("submit", this.handleSubmit.bind(this));
|
|
3692
|
+
const cancelButton = this.modalElement.querySelector(".cancel-button");
|
|
3693
|
+
cancelButton.addEventListener("click", this.close.bind(this));
|
|
3694
|
+
}
|
|
3695
|
+
async handleSubmit(event) {
|
|
3696
|
+
event.preventDefault();
|
|
3697
|
+
const emailInput = this.modalElement.querySelector("#email");
|
|
3698
|
+
const passwordInput = this.modalElement.querySelector("#password");
|
|
3699
|
+
const email = emailInput.value.trim();
|
|
3700
|
+
const password = passwordInput.value.trim();
|
|
3701
|
+
if (!email || !password) {
|
|
3702
|
+
this.options.onError(new NeoFaceError("Email e senha são obrigatórios", ErrorType.VALIDATION_ERROR));
|
|
3703
|
+
return;
|
|
3704
|
+
}
|
|
3705
|
+
try {
|
|
3706
|
+
const result = await loginWithEmail(email, password, this.options.applicationToken);
|
|
3707
|
+
this.options.onSuccess(result);
|
|
3708
|
+
this.close();
|
|
3709
|
+
} catch (error) {
|
|
3710
|
+
this.options.onError(error);
|
|
3711
|
+
}
|
|
3712
|
+
}
|
|
3713
|
+
}
|
|
3510
3714
|
async function startFaceLogin(options) {
|
|
3511
3715
|
try {
|
|
3512
3716
|
const isValidToken = await validateToken(options.applicationToken);
|
|
@@ -3542,6 +3746,134 @@ async function startFaceLogin(options) {
|
|
|
3542
3746
|
);
|
|
3543
3747
|
}
|
|
3544
3748
|
}
|
|
3749
|
+
async function startHandLogin(options) {
|
|
3750
|
+
try {
|
|
3751
|
+
const isValidToken = await validateToken(options.applicationToken);
|
|
3752
|
+
if (!isValidToken) {
|
|
3753
|
+
options.onError(new NeoFaceError("Token de aplicação inválido", ErrorType.INVALID_TOKEN));
|
|
3754
|
+
return;
|
|
3755
|
+
}
|
|
3756
|
+
const captureOptions = {
|
|
3757
|
+
mode: "hand",
|
|
3758
|
+
onSuccess: async (imageData, _detectedType) => {
|
|
3759
|
+
try {
|
|
3760
|
+
const imageBlob = await fetch(imageData).then((res) => res.blob());
|
|
3761
|
+
const result = await loginWithBiometric(imageBlob, options.applicationToken);
|
|
3762
|
+
options.onSuccess(result);
|
|
3763
|
+
} catch (error) {
|
|
3764
|
+
options.onError(error);
|
|
3765
|
+
}
|
|
3766
|
+
},
|
|
3767
|
+
onError: options.onError,
|
|
3768
|
+
onCancel: options.onCancel,
|
|
3769
|
+
countdown: options.countdown,
|
|
3770
|
+
title: options.title || "Login por Mão",
|
|
3771
|
+
subtitle: options.subtitle || "Posicione sua mão dentro do quadro para fazer login."
|
|
3772
|
+
};
|
|
3773
|
+
const modal = new BiometricCaptureModal(captureOptions);
|
|
3774
|
+
await modal.open();
|
|
3775
|
+
} catch (error) {
|
|
3776
|
+
options.onError(
|
|
3777
|
+
new NeoFaceError(
|
|
3778
|
+
"Erro ao inicializar login por mão: " + error.message,
|
|
3779
|
+
ErrorType.INITIALIZATION_ERROR
|
|
3780
|
+
)
|
|
3781
|
+
);
|
|
3782
|
+
}
|
|
3783
|
+
}
|
|
3784
|
+
async function startAutoLogin(options) {
|
|
3785
|
+
try {
|
|
3786
|
+
const isValidToken = await validateToken(options.applicationToken);
|
|
3787
|
+
if (!isValidToken) {
|
|
3788
|
+
options.onError(new NeoFaceError("Token de aplicação inválido", ErrorType.INVALID_TOKEN));
|
|
3789
|
+
return;
|
|
3790
|
+
}
|
|
3791
|
+
const captureOptions = {
|
|
3792
|
+
mode: "auto",
|
|
3793
|
+
onSuccess: async (imageData, _detectedType) => {
|
|
3794
|
+
try {
|
|
3795
|
+
const imageBlob = await fetch(imageData).then((res) => res.blob());
|
|
3796
|
+
const result = await loginWithBiometric(imageBlob, options.applicationToken);
|
|
3797
|
+
options.onSuccess(result);
|
|
3798
|
+
} catch (error) {
|
|
3799
|
+
options.onError(error);
|
|
3800
|
+
}
|
|
3801
|
+
},
|
|
3802
|
+
onError: options.onError,
|
|
3803
|
+
onCancel: options.onCancel,
|
|
3804
|
+
countdown: options.countdown,
|
|
3805
|
+
title: options.title || "Login Biométrico",
|
|
3806
|
+
subtitle: options.subtitle || "Posicione seu rosto ou mão dentro do quadro para login automático."
|
|
3807
|
+
};
|
|
3808
|
+
const modal = new BiometricCaptureModal(captureOptions);
|
|
3809
|
+
await modal.open();
|
|
3810
|
+
} catch (error) {
|
|
3811
|
+
options.onError(
|
|
3812
|
+
new NeoFaceError(
|
|
3813
|
+
"Erro ao inicializar login automático: " + error.message,
|
|
3814
|
+
ErrorType.INITIALIZATION_ERROR
|
|
3815
|
+
)
|
|
3816
|
+
);
|
|
3817
|
+
}
|
|
3818
|
+
}
|
|
3819
|
+
function biometricLogin(applicationToken, mode = "auto") {
|
|
3820
|
+
return new Promise((resolve, reject) => {
|
|
3821
|
+
const options = {
|
|
3822
|
+
applicationToken,
|
|
3823
|
+
onSuccess: resolve,
|
|
3824
|
+
onError: reject
|
|
3825
|
+
};
|
|
3826
|
+
switch (mode) {
|
|
3827
|
+
case "face":
|
|
3828
|
+
startFaceLogin(options);
|
|
3829
|
+
break;
|
|
3830
|
+
case "hand":
|
|
3831
|
+
startHandLogin(options);
|
|
3832
|
+
break;
|
|
3833
|
+
case "auto":
|
|
3834
|
+
startAutoLogin(options);
|
|
3835
|
+
break;
|
|
3836
|
+
default:
|
|
3837
|
+
reject(new NeoFaceError("Modo de login inválido", ErrorType.VALIDATION_ERROR));
|
|
3838
|
+
}
|
|
3839
|
+
});
|
|
3840
|
+
}
|
|
3841
|
+
function biometricLoginWithFallback(applicationToken, mode = "auto") {
|
|
3842
|
+
return new Promise((resolve, reject) => {
|
|
3843
|
+
const options = {
|
|
3844
|
+
applicationToken,
|
|
3845
|
+
onSuccess: resolve,
|
|
3846
|
+
onError: (error) => {
|
|
3847
|
+
if (error.type === ErrorType.LOGIN_FAILED) {
|
|
3848
|
+
const fallbackOptions = {
|
|
3849
|
+
applicationToken,
|
|
3850
|
+
onSuccess: resolve,
|
|
3851
|
+
onError: reject,
|
|
3852
|
+
title: "Login Alternativo",
|
|
3853
|
+
subtitle: "Biometria não reconhecida. Por favor, informe email e senha."
|
|
3854
|
+
};
|
|
3855
|
+
const modal = new EmailPasswordModal(fallbackOptions);
|
|
3856
|
+
modal.open();
|
|
3857
|
+
} else {
|
|
3858
|
+
reject(error);
|
|
3859
|
+
}
|
|
3860
|
+
}
|
|
3861
|
+
};
|
|
3862
|
+
switch (mode) {
|
|
3863
|
+
case "face":
|
|
3864
|
+
startFaceLogin(options);
|
|
3865
|
+
break;
|
|
3866
|
+
case "hand":
|
|
3867
|
+
startHandLogin(options);
|
|
3868
|
+
break;
|
|
3869
|
+
case "auto":
|
|
3870
|
+
startAutoLogin(options);
|
|
3871
|
+
break;
|
|
3872
|
+
default:
|
|
3873
|
+
reject(new NeoFaceError("Modo de login inválido", ErrorType.VALIDATION_ERROR));
|
|
3874
|
+
}
|
|
3875
|
+
});
|
|
3876
|
+
}
|
|
3545
3877
|
async function detectBiometricType(imageData) {
|
|
3546
3878
|
try {
|
|
3547
3879
|
const img = await loadImageFromBase64(imageData);
|
|
@@ -3578,7 +3910,8 @@ async function detectFace(img) {
|
|
|
3578
3910
|
if (typeof window !== "undefined" && window.faceapi) {
|
|
3579
3911
|
const faceapi2 = window.faceapi;
|
|
3580
3912
|
await loadFaceApiModels();
|
|
3581
|
-
const
|
|
3913
|
+
const tinyOptions = new faceapi2.TinyFaceDetectorOptions({ inputSize: 320, scoreThreshold: 0.5 });
|
|
3914
|
+
const detections = await faceapi2.detectAllFaces(img, tinyOptions).withFaceLandmarks();
|
|
3582
3915
|
if (detections && detections.length > 0) {
|
|
3583
3916
|
const bestDetection = detections.reduce(
|
|
3584
3917
|
(best, current) => current.detection.score > best.detection.score ? current : best
|
|
@@ -3683,8 +4016,7 @@ async function loadFaceApiModels() {
|
|
|
3683
4016
|
try {
|
|
3684
4017
|
await Promise.all([
|
|
3685
4018
|
faceapi2.nets.tinyFaceDetector.loadFromUri("./models"),
|
|
3686
|
-
faceapi2.nets.faceLandmark68Net.loadFromUri("./models")
|
|
3687
|
-
faceapi2.nets.faceRecognitionNet.loadFromUri("./models")
|
|
4019
|
+
faceapi2.nets.faceLandmark68Net.loadFromUri("./models")
|
|
3688
4020
|
]);
|
|
3689
4021
|
} catch (error) {
|
|
3690
4022
|
console.warn("Não foi possível carregar modelos do face-api.js:", error);
|
|
@@ -3708,8 +4040,231 @@ const biometricDetection = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.
|
|
|
3708
4040
|
initializeBiometricDetection,
|
|
3709
4041
|
isAdvancedDetectionAvailable
|
|
3710
4042
|
}, Symbol.toStringTag, { value: "Module" }));
|
|
3711
|
-
const VERSION = "1.
|
|
3712
|
-
const RELEASE_DATE = "2025-11-
|
|
4043
|
+
const VERSION = "1.2.0";
|
|
4044
|
+
const RELEASE_DATE = "2025-11-09";
|
|
4045
|
+
class OnboardingCaptureModal {
|
|
4046
|
+
constructor(options) {
|
|
4047
|
+
__publicField(this, "overlay", null);
|
|
4048
|
+
__publicField(this, "video", null);
|
|
4049
|
+
__publicField(this, "canvas", null);
|
|
4050
|
+
__publicField(this, "stream", null);
|
|
4051
|
+
__publicField(this, "step", "face");
|
|
4052
|
+
__publicField(this, "countdownSeconds");
|
|
4053
|
+
__publicField(this, "title");
|
|
4054
|
+
__publicField(this, "subtitle");
|
|
4055
|
+
__publicField(this, "faceBlob", null);
|
|
4056
|
+
__publicField(this, "documentBlob", null);
|
|
4057
|
+
this.countdownSeconds = (options == null ? void 0 : options.countdown) ?? 3;
|
|
4058
|
+
this.title = options == null ? void 0 : options.title;
|
|
4059
|
+
this.subtitle = options == null ? void 0 : options.subtitle;
|
|
4060
|
+
}
|
|
4061
|
+
/**
|
|
4062
|
+
* Abre o modal, inicia câmera e gerencia o fluxo de captura dos dois passos.
|
|
4063
|
+
* Retorna uma promessa resolvida com os Blobs de face e documento.
|
|
4064
|
+
*/
|
|
4065
|
+
async open() {
|
|
4066
|
+
await this.createDom();
|
|
4067
|
+
await this.startCamera();
|
|
4068
|
+
await this.runFaceCaptureCountdown();
|
|
4069
|
+
this.faceBlob = await this.captureFrame();
|
|
4070
|
+
this.step = "document";
|
|
4071
|
+
this.updateTexts("Captura do Documento", 'Posicione seu documento visível e clique em "Capturar"');
|
|
4072
|
+
await this.waitForUserCaptureClick();
|
|
4073
|
+
this.documentBlob = await this.captureFrame();
|
|
4074
|
+
await this.close();
|
|
4075
|
+
if (!this.faceBlob || !this.documentBlob) {
|
|
4076
|
+
throw new Error("Falha na captura das imagens de onboarding");
|
|
4077
|
+
}
|
|
4078
|
+
return { face: this.faceBlob, document: this.documentBlob };
|
|
4079
|
+
}
|
|
4080
|
+
/**
|
|
4081
|
+
* Cria estrutura de DOM do modal e elementos de UI.
|
|
4082
|
+
*/
|
|
4083
|
+
async createDom() {
|
|
4084
|
+
const overlay = document.createElement("div");
|
|
4085
|
+
overlay.className = "neofaceid-modal-overlay";
|
|
4086
|
+
overlay.innerHTML = `
|
|
4087
|
+
<div class="neofaceid-modal">
|
|
4088
|
+
<div class="neofaceid-modal-header">
|
|
4089
|
+
<h2 class="neofaceid-title">${this.title ?? "Onboarding NeoFaceID"}</h2>
|
|
4090
|
+
<button class="neofaceid-close-btn" aria-label="Fechar">×</button>
|
|
4091
|
+
</div>
|
|
4092
|
+
<div class="neofaceid-modal-body">
|
|
4093
|
+
<p class="neofaceid-subtitle">${this.subtitle ?? "Vamos capturar sua face e documento"}</p>
|
|
4094
|
+
<div class="neofaceid-camera-container">
|
|
4095
|
+
<video class="neofaceid-video" autoplay playsinline></video>
|
|
4096
|
+
<canvas class="neofaceid-canvas"></canvas>
|
|
4097
|
+
<div class="neofaceid-overlay">
|
|
4098
|
+
<div class="neofaceid-frame"></div>
|
|
4099
|
+
<div class="neofaceid-countdown" style="display:none"><span class="neofaceid-countdown-number">${this.countdownSeconds}</span></div>
|
|
4100
|
+
</div>
|
|
4101
|
+
</div>
|
|
4102
|
+
<div class="neofaceid-status"><p class="neofaceid-status-text">Posicione seu rosto no quadro</p></div>
|
|
4103
|
+
</div>
|
|
4104
|
+
<div class="neofaceid-modal-footer">
|
|
4105
|
+
<button class="neofaceid-cancel-btn">Cancelar</button>
|
|
4106
|
+
<button class="neofaceid-capture-btn" disabled>Capturar</button>
|
|
4107
|
+
</div>
|
|
4108
|
+
</div>
|
|
4109
|
+
`;
|
|
4110
|
+
const style = document.createElement("style");
|
|
4111
|
+
style.textContent = `
|
|
4112
|
+
.neofaceid-modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.6); display:flex; align-items:center; justify-content:center; z-index: 9999; }
|
|
4113
|
+
.neofaceid-modal { background:#fff; border-radius:12px; width: 92%; max-width: 640px; box-shadow: 0 20px 60px rgba(0,0,0,0.3); overflow:hidden; }
|
|
4114
|
+
.neofaceid-modal-header { padding: 16px 20px; border-bottom: 1px solid #eee; display:flex; align-items:center; justify-content:space-between; }
|
|
4115
|
+
.neofaceid-title { margin:0; font-size: 20px; color:#333; }
|
|
4116
|
+
.neofaceid-close-btn { background:none; border:none; font-size:24px; cursor:pointer; color:#666; }
|
|
4117
|
+
.neofaceid-modal-body { padding: 16px 20px; }
|
|
4118
|
+
.neofaceid-subtitle { margin: 0 0 12px 0; color:#666; }
|
|
4119
|
+
.neofaceid-camera-container { position: relative; background:#000; border-radius:8px; overflow:hidden; aspect-ratio: 4/3; margin-bottom: 12px; }
|
|
4120
|
+
.neofaceid-video { width:100%; height:100%; object-fit: cover; }
|
|
4121
|
+
.neofaceid-canvas { position:absolute; top:0; left:0; }
|
|
4122
|
+
.neofaceid-overlay { position:absolute; inset:0; display:flex; align-items:center; justify-content:center; }
|
|
4123
|
+
.neofaceid-frame { width: 200px; height: 200px; border: 3px solid #4CAF50; border-radius: 50%; box-shadow: 0 0 0 2px rgba(76,175,80,0.3); }
|
|
4124
|
+
.neofaceid-countdown { position:absolute; top:16px; right:16px; background: rgba(0,0,0,0.7); color:#fff; width: 48px; height:48px; display:flex; align-items:center; justify-content:center; border-radius:50%; }
|
|
4125
|
+
.neofaceid-modal-footer { padding: 16px 20px; border-top: 1px solid #eee; display:flex; gap: 8px; justify-content:flex-end; }
|
|
4126
|
+
.neofaceid-cancel-btn { background:#f5f5f5; color:#666; border:none; border-radius:6px; padding:10px 18px; cursor:pointer; }
|
|
4127
|
+
.neofaceid-capture-btn { background:#4CAF50; color:#fff; border:none; border-radius:6px; padding:10px 18px; cursor:pointer; }
|
|
4128
|
+
.neofaceid-capture-btn:disabled { background:#ccc; cursor:not-allowed; }
|
|
4129
|
+
`;
|
|
4130
|
+
document.head.appendChild(style);
|
|
4131
|
+
document.body.appendChild(overlay);
|
|
4132
|
+
this.overlay = overlay;
|
|
4133
|
+
this.video = this.overlay.querySelector(".neofaceid-video");
|
|
4134
|
+
this.canvas = this.overlay.querySelector(".neofaceid-canvas");
|
|
4135
|
+
const closeBtn = this.overlay.querySelector(".neofaceid-close-btn");
|
|
4136
|
+
const cancelBtn = this.overlay.querySelector(".neofaceid-cancel-btn");
|
|
4137
|
+
closeBtn.onclick = () => this.close();
|
|
4138
|
+
cancelBtn.onclick = () => this.close();
|
|
4139
|
+
const captureBtn = this.overlay.querySelector(".neofaceid-capture-btn");
|
|
4140
|
+
captureBtn.disabled = true;
|
|
4141
|
+
}
|
|
4142
|
+
/**
|
|
4143
|
+
* Inicia a câmera do usuário com resolução adequada.
|
|
4144
|
+
*/
|
|
4145
|
+
async startCamera() {
|
|
4146
|
+
const constraints = {
|
|
4147
|
+
video: { facingMode: "user", width: { ideal: 1280 }, height: { ideal: 720 } },
|
|
4148
|
+
audio: false
|
|
4149
|
+
};
|
|
4150
|
+
this.stream = await navigator.mediaDevices.getUserMedia(constraints);
|
|
4151
|
+
if (!this.video)
|
|
4152
|
+
throw new Error("Elemento de vídeo não encontrado");
|
|
4153
|
+
this.video.srcObject = this.stream;
|
|
4154
|
+
await this.video.play();
|
|
4155
|
+
}
|
|
4156
|
+
/**
|
|
4157
|
+
* Executa a contagem regressiva e atualiza textos para captura de rosto.
|
|
4158
|
+
*/
|
|
4159
|
+
async runFaceCaptureCountdown() {
|
|
4160
|
+
if (!this.overlay)
|
|
4161
|
+
return;
|
|
4162
|
+
const countdownEl = this.overlay.querySelector(".neofaceid-countdown");
|
|
4163
|
+
const countdownNumberEl = this.overlay.querySelector(".neofaceid-countdown-number");
|
|
4164
|
+
const statusText = this.overlay.querySelector(".neofaceid-status-text");
|
|
4165
|
+
countdownEl.style.display = "flex";
|
|
4166
|
+
statusText.textContent = "Prepare-se! Capturando seu rosto...";
|
|
4167
|
+
let remaining = this.countdownSeconds;
|
|
4168
|
+
countdownNumberEl.textContent = String(remaining);
|
|
4169
|
+
await new Promise((resolve) => {
|
|
4170
|
+
const interval = setInterval(() => {
|
|
4171
|
+
remaining -= 1;
|
|
4172
|
+
countdownNumberEl.textContent = String(remaining);
|
|
4173
|
+
if (remaining <= 0) {
|
|
4174
|
+
clearInterval(interval);
|
|
4175
|
+
countdownEl.style.display = "none";
|
|
4176
|
+
resolve();
|
|
4177
|
+
}
|
|
4178
|
+
}, 1e3);
|
|
4179
|
+
});
|
|
4180
|
+
}
|
|
4181
|
+
/**
|
|
4182
|
+
* Aguarda o clique do usuário no botão "Capturar" (para documento).
|
|
4183
|
+
*/
|
|
4184
|
+
async waitForUserCaptureClick() {
|
|
4185
|
+
if (!this.overlay)
|
|
4186
|
+
return;
|
|
4187
|
+
const captureBtn = this.overlay.querySelector(".neofaceid-capture-btn");
|
|
4188
|
+
const statusText = this.overlay.querySelector(".neofaceid-status-text");
|
|
4189
|
+
captureBtn.disabled = false;
|
|
4190
|
+
statusText.textContent = 'Clique em "Capturar" quando o documento estiver visível';
|
|
4191
|
+
await new Promise((resolve) => {
|
|
4192
|
+
const handler = () => {
|
|
4193
|
+
captureBtn.removeEventListener("click", handler);
|
|
4194
|
+
resolve();
|
|
4195
|
+
};
|
|
4196
|
+
captureBtn.addEventListener("click", handler);
|
|
4197
|
+
});
|
|
4198
|
+
captureBtn.disabled = true;
|
|
4199
|
+
}
|
|
4200
|
+
/**
|
|
4201
|
+
* Captura o frame atual do vídeo e retorna como Blob JPEG.
|
|
4202
|
+
*/
|
|
4203
|
+
async captureFrame() {
|
|
4204
|
+
if (!this.video || !this.canvas)
|
|
4205
|
+
throw new Error("Elementos de captura não encontrados");
|
|
4206
|
+
const ctx = this.canvas.getContext("2d");
|
|
4207
|
+
if (!ctx)
|
|
4208
|
+
throw new Error("Falha ao obter contexto do canvas");
|
|
4209
|
+
this.canvas.width = this.video.videoWidth;
|
|
4210
|
+
this.canvas.height = this.video.videoHeight;
|
|
4211
|
+
ctx.drawImage(this.video, 0, 0, this.canvas.width, this.canvas.height);
|
|
4212
|
+
const blob = await new Promise((resolve, reject) => {
|
|
4213
|
+
this.canvas.toBlob((b) => b ? resolve(b) : reject(new Error("Falha ao gerar imagem")), "image/jpeg", 0.92);
|
|
4214
|
+
});
|
|
4215
|
+
return blob;
|
|
4216
|
+
}
|
|
4217
|
+
/**
|
|
4218
|
+
* Atualiza título e subtítulo do modal conforme o passo.
|
|
4219
|
+
*/
|
|
4220
|
+
updateTexts(title, subtitle) {
|
|
4221
|
+
if (!this.overlay)
|
|
4222
|
+
return;
|
|
4223
|
+
const titleEl = this.overlay.querySelector(".neofaceid-title");
|
|
4224
|
+
const subtitleEl = this.overlay.querySelector(".neofaceid-subtitle");
|
|
4225
|
+
const frameEl = this.overlay.querySelector(".neofaceid-frame");
|
|
4226
|
+
titleEl.textContent = title;
|
|
4227
|
+
subtitleEl.textContent = subtitle;
|
|
4228
|
+
if (this.step === "document") {
|
|
4229
|
+
frameEl.style.borderRadius = "8px";
|
|
4230
|
+
frameEl.style.width = "280px";
|
|
4231
|
+
frameEl.style.height = "180px";
|
|
4232
|
+
}
|
|
4233
|
+
}
|
|
4234
|
+
/**
|
|
4235
|
+
* Fecha o modal e libera a câmera.
|
|
4236
|
+
*/
|
|
4237
|
+
async close() {
|
|
4238
|
+
if (this.stream) {
|
|
4239
|
+
this.stream.getTracks().forEach((t) => t.stop());
|
|
4240
|
+
this.stream = null;
|
|
4241
|
+
}
|
|
4242
|
+
if (this.overlay) {
|
|
4243
|
+
this.overlay.remove();
|
|
4244
|
+
this.overlay = null;
|
|
4245
|
+
}
|
|
4246
|
+
}
|
|
4247
|
+
}
|
|
4248
|
+
const startOnboarding = async (options) => {
|
|
4249
|
+
const { applicationToken, onboardingToken, countdown, title, subtitle, onSuccess, onError } = options;
|
|
4250
|
+
try {
|
|
4251
|
+
const details = await getOnboardingDetails(applicationToken, onboardingToken);
|
|
4252
|
+
if (!(details == null ? void 0 : details.is_valid)) {
|
|
4253
|
+
throw new NeoFaceError("Link de onboarding inválido ou expirado", ErrorType.VALIDATION_ERROR);
|
|
4254
|
+
}
|
|
4255
|
+
const modal = new OnboardingCaptureModal({ countdown, title, subtitle });
|
|
4256
|
+
const { face, document: document2 } = await modal.open();
|
|
4257
|
+
const result = await completeOnboarding(applicationToken, onboardingToken, face, document2);
|
|
4258
|
+
onSuccess({ ...result, details });
|
|
4259
|
+
} catch (err) {
|
|
4260
|
+
if (err instanceof NeoFaceError) {
|
|
4261
|
+
onError(err);
|
|
4262
|
+
return;
|
|
4263
|
+
}
|
|
4264
|
+
const message = (err == null ? void 0 : err.message) || "Erro desconhecido no onboarding";
|
|
4265
|
+
onError(new NeoFaceError(message, ErrorType.UNKNOWN));
|
|
4266
|
+
}
|
|
4267
|
+
};
|
|
3713
4268
|
function start(applicationToken, callbacks) {
|
|
3714
4269
|
const container = document.createElement("div");
|
|
3715
4270
|
container.id = "neoface-modal-container";
|
|
@@ -3779,6 +4334,8 @@ export {
|
|
|
3779
4334
|
NeoFaceError,
|
|
3780
4335
|
RELEASE_DATE,
|
|
3781
4336
|
VERSION,
|
|
4337
|
+
biometricLogin,
|
|
4338
|
+
biometricLoginWithFallback,
|
|
3782
4339
|
detectBiometricType,
|
|
3783
4340
|
initializeBiometricDetection,
|
|
3784
4341
|
isAdvancedDetectionAvailable,
|
|
@@ -3790,8 +4347,11 @@ export {
|
|
|
3790
4347
|
registerPersonWithoutFace,
|
|
3791
4348
|
simpleIdentification,
|
|
3792
4349
|
start,
|
|
4350
|
+
startAutoLogin,
|
|
3793
4351
|
startBiometricRegistration,
|
|
3794
4352
|
startFaceLogin,
|
|
4353
|
+
startHandLogin,
|
|
4354
|
+
startOnboarding,
|
|
3795
4355
|
validateToken
|
|
3796
4356
|
};
|
|
3797
4357
|
//# sourceMappingURL=neoface-id-sdk.es.js.map
|