@neofaceid/web-sdk 1.13.0 → 1.14.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.
@@ -1841,11 +1841,13 @@ const registerPersonWithoutFace = async (personData, applicationToken, options)
1841
1841
  password_confirm: personData.password
1842
1842
  };
1843
1843
  if (documentFrontBase64) {
1844
- body.document_images = [{
1845
- front: documentFrontBase64,
1846
- back: documentBackBase64 || void 0,
1847
- type: (options == null ? void 0 : options.documentType) || "RG"
1848
- }];
1844
+ body.document_images = [
1845
+ {
1846
+ front: documentFrontBase64,
1847
+ back: documentBackBase64 || void 0,
1848
+ type: (options == null ? void 0 : options.documentType) || "RG"
1849
+ }
1850
+ ];
1849
1851
  }
1850
1852
  const response = await fetch(`${getApiBaseUrl()}/api/v1/signup/donor/`, {
1851
1853
  method: "POST",
@@ -1935,11 +1937,13 @@ const registerPersonWithBiometric = async (personData, facePhotos, applicationTo
1935
1937
  face_photos: facePhotosBase64
1936
1938
  };
1937
1939
  if (documentFrontBase64) {
1938
- body.document_images = [{
1939
- front: documentFrontBase64,
1940
- back: documentBackBase64 || void 0,
1941
- type: (options == null ? void 0 : options.documentType) || "RG"
1942
- }];
1940
+ body.document_images = [
1941
+ {
1942
+ front: documentFrontBase64,
1943
+ back: documentBackBase64 || void 0,
1944
+ type: (options == null ? void 0 : options.documentType) || "RG"
1945
+ }
1946
+ ];
1943
1947
  }
1944
1948
  const response = await fetch(`${getApiBaseUrl()}/api/v1/signup/donor/`, {
1945
1949
  method: "POST",
@@ -2288,13 +2292,30 @@ const registerApplication = async (jwtToken, consumerId, applicationData) => {
2288
2292
  };
2289
2293
  const recordFaceVideo = async (options = {}) => {
2290
2294
  ensureSecureContext();
2291
- const { durationMs = 3e3, mimeType = "video/webm", onProgress } = options;
2295
+ const { durationMs = 3e3, onProgress } = options;
2296
+ const getSupportedMimeType = () => {
2297
+ const types = [
2298
+ "video/mp4;codecs=avc1",
2299
+ // iOS Safari (prioritário quando disponível)
2300
+ "video/mp4",
2301
+ // iOS Safari fallback
2302
+ "video/webm;codecs=vp9",
2303
+ // Chrome/Firefox
2304
+ "video/webm;codecs=vp8",
2305
+ // Chrome/Firefox fallback
2306
+ "video/webm"
2307
+ // Android Chrome
2308
+ ];
2309
+ for (const type of types) {
2310
+ if (MediaRecorder.isTypeSupported(type)) return type;
2311
+ }
2312
+ return "";
2313
+ };
2292
2314
  return new Promise(async (resolve, reject) => {
2293
- let stream = null;
2294
- let mediaRecorder = null;
2315
+ let cameraStream = null;
2295
2316
  const chunks = [];
2296
2317
  try {
2297
- stream = await navigator.mediaDevices.getUserMedia({
2318
+ cameraStream = await navigator.mediaDevices.getUserMedia({
2298
2319
  video: {
2299
2320
  facingMode: "user",
2300
2321
  width: { ideal: 640 },
@@ -2302,38 +2323,36 @@ const recordFaceVideo = async (options = {}) => {
2302
2323
  },
2303
2324
  audio: false
2304
2325
  });
2305
- let selectedMimeType = mimeType;
2306
- const mimeTypes = ["video/webm;codecs=vp9", "video/webm;codecs=vp8", "video/webm", "video/mp4"];
2307
- for (const type of mimeTypes) {
2308
- if (MediaRecorder.isTypeSupported(type)) {
2309
- selectedMimeType = type;
2310
- break;
2311
- }
2312
- }
2313
- mediaRecorder = new MediaRecorder(stream, {
2314
- mimeType: selectedMimeType,
2315
- videoBitsPerSecond: 1e6
2316
- // 1 Mbps
2317
- });
2318
- mediaRecorder.ondataavailable = (event) => {
2326
+ const selectedMimeType = getSupportedMimeType();
2327
+ let recorder;
2328
+ try {
2329
+ recorder = selectedMimeType ? new MediaRecorder(cameraStream, {
2330
+ mimeType: selectedMimeType,
2331
+ videoBitsPerSecond: 1e6
2332
+ }) : new MediaRecorder(cameraStream, { videoBitsPerSecond: 1e6 });
2333
+ } catch {
2334
+ recorder = new MediaRecorder(cameraStream);
2335
+ }
2336
+ const finalMimeType = recorder.mimeType || selectedMimeType;
2337
+ recorder.ondataavailable = (event) => {
2319
2338
  if (event.data.size > 0) {
2320
2339
  chunks.push(event.data);
2321
2340
  }
2322
2341
  };
2323
- mediaRecorder.onstop = () => {
2324
- if (stream) {
2325
- stream.getTracks().forEach((track) => track.stop());
2342
+ recorder.onstop = () => {
2343
+ if (cameraStream) {
2344
+ cameraStream.getTracks().forEach((track) => track.stop());
2326
2345
  }
2327
- const videoBlob = new Blob(chunks, { type: selectedMimeType });
2346
+ const videoBlob = new Blob(chunks, { type: finalMimeType });
2328
2347
  resolve(videoBlob);
2329
2348
  };
2330
- mediaRecorder.onerror = (event) => {
2331
- if (stream) {
2332
- stream.getTracks().forEach((track) => track.stop());
2349
+ recorder.onerror = (event) => {
2350
+ if (cameraStream) {
2351
+ cameraStream.getTracks().forEach((track) => track.stop());
2333
2352
  }
2334
2353
  reject(new NeoFaceError(`Recording error: ${event}`, ErrorType.CAPTURE_ERROR));
2335
2354
  };
2336
- mediaRecorder.start(100);
2355
+ recorder.start(100);
2337
2356
  if (onProgress) {
2338
2357
  const progressInterval = 100;
2339
2358
  let elapsed = 0;
@@ -2347,13 +2366,13 @@ const recordFaceVideo = async (options = {}) => {
2347
2366
  }, progressInterval);
2348
2367
  }
2349
2368
  setTimeout(() => {
2350
- if (mediaRecorder && mediaRecorder.state === "recording") {
2351
- mediaRecorder.stop();
2369
+ if (recorder && recorder.state === "recording") {
2370
+ recorder.stop();
2352
2371
  }
2353
2372
  }, durationMs);
2354
2373
  } catch (error) {
2355
- if (stream) {
2356
- stream.getTracks().forEach((track) => track.stop());
2374
+ if (cameraStream) {
2375
+ cameraStream.getTracks().forEach((track) => track.stop());
2357
2376
  }
2358
2377
  if (error instanceof Error) {
2359
2378
  if (error.name === "NotAllowedError") {
@@ -2483,6 +2502,68 @@ const videoToBase64 = async (videoBlob) => {
2483
2502
  reader.readAsDataURL(videoBlob);
2484
2503
  });
2485
2504
  };
2505
+ const blobToBase64 = (blob) => new Promise((resolve, reject) => {
2506
+ const reader = new FileReader();
2507
+ reader.onload = () => {
2508
+ const result = reader.result;
2509
+ resolve(result.includes(",") ? result.split(",")[1] : result);
2510
+ };
2511
+ reader.onerror = () => reject(new NeoFaceError("Failed to convert image to base64", ErrorType.CAPTURE_ERROR));
2512
+ reader.readAsDataURL(blob);
2513
+ });
2514
+ const registerDocumentByImage = async (personId, jwtToken, images) => {
2515
+ var _a, _b;
2516
+ ensureSecureContext();
2517
+ if (!personId) {
2518
+ throw new NeoFaceError("personId is required", ErrorType.VALIDATION_ERROR);
2519
+ }
2520
+ if (!jwtToken) {
2521
+ throw new NeoFaceError("jwtToken is required", ErrorType.VALIDATION_ERROR);
2522
+ }
2523
+ if (!images || images.length === 0) {
2524
+ throw new NeoFaceError("At least one document image is required", ErrorType.VALIDATION_ERROR);
2525
+ }
2526
+ const base64Images = await Promise.all(
2527
+ images.map(async (img) => {
2528
+ const compressed = await compressDocumentImage(img);
2529
+ return blobToBase64(compressed);
2530
+ })
2531
+ );
2532
+ const controller = createTimeoutController();
2533
+ try {
2534
+ const response = await fetch(`${getApiBaseUrl()}/api/v1/donors/${personId}/documents/`, {
2535
+ method: "POST",
2536
+ headers: {
2537
+ "Content-Type": "application/json",
2538
+ Authorization: `Bearer ${jwtToken}`
2539
+ },
2540
+ body: JSON.stringify({ document_images: base64Images }),
2541
+ signal: controller.signal
2542
+ });
2543
+ const data = await response.json();
2544
+ if (!response.ok) {
2545
+ throw new NeoFaceError(
2546
+ (data == null ? void 0 : data.message) || `API Error ${response.status}`,
2547
+ ErrorType.NETWORK_ERROR
2548
+ );
2549
+ }
2550
+ return {
2551
+ success: true,
2552
+ taskId: ((_a = data.data) == null ? void 0 : _a.task_id) ?? "",
2553
+ status: ((_b = data.data) == null ? void 0 : _b.status) ?? "processing",
2554
+ message: data.message ?? "Documents submitted for processing"
2555
+ };
2556
+ } catch (error) {
2557
+ if (error instanceof NeoFaceError) throw error;
2558
+ if (error instanceof Error) {
2559
+ if (error.name === "AbortError") {
2560
+ throw new NeoFaceError("Request timeout", ErrorType.NETWORK_ERROR);
2561
+ }
2562
+ throw new NeoFaceError(error.message, ErrorType.NETWORK_ERROR);
2563
+ }
2564
+ throw new NeoFaceError("Unknown error occurred", ErrorType.NETWORK_ERROR);
2565
+ }
2566
+ };
2486
2567
  const modalReducer$1 = (state, action) => {
2487
2568
  switch (action.type) {
2488
2569
  case "PERMISSION_GRANTED":
@@ -5321,14 +5402,15 @@ async function captureFaceSilently() {
5321
5402
  video.style.width = "1px";
5322
5403
  video.style.height = "1px";
5323
5404
  video.style.opacity = "0";
5405
+ video.muted = true;
5406
+ video.playsInline = true;
5324
5407
  video.setAttribute("autoplay", "");
5325
- video.setAttribute("muted", "");
5326
5408
  video.setAttribute("playsinline", "");
5327
5409
  const canvas = document.createElement("canvas");
5328
5410
  canvas.style.display = "none";
5329
5411
  let stream = null;
5330
5412
  let faceDetectionAttempts = 0;
5331
- const MAX_FACE_DETECTION_ATTEMPTS = 10;
5413
+ const MAX_FACE_DETECTION_ATTEMPTS = 30;
5332
5414
  const cleanup = () => {
5333
5415
  if (stream) {
5334
5416
  stream.getTracks().forEach((track) => track.stop());
@@ -5342,22 +5424,27 @@ async function captureFaceSilently() {
5342
5424
  };
5343
5425
  document.body.appendChild(video);
5344
5426
  document.body.appendChild(canvas);
5345
- navigator.mediaDevices.getUserMedia({
5346
- video: {
5347
- width: { ideal: 640 },
5348
- height: { ideal: 480 },
5349
- facingMode: "user"
5350
- },
5351
- audio: false
5352
- }).then((mediaStream) => {
5427
+ const tryGetUserMedia = (withConstraints) => {
5428
+ if (withConstraints) {
5429
+ return navigator.mediaDevices.getUserMedia({
5430
+ video: {
5431
+ width: { ideal: 640 },
5432
+ height: { ideal: 480 },
5433
+ facingMode: "user"
5434
+ },
5435
+ audio: false
5436
+ });
5437
+ }
5438
+ return navigator.mediaDevices.getUserMedia({ video: { facingMode: "user" }, audio: false });
5439
+ };
5440
+ tryGetUserMedia(true).catch(() => tryGetUserMedia(false)).then((mediaStream) => {
5353
5441
  stream = mediaStream;
5354
5442
  video.srcObject = stream;
5355
- video.play();
5356
5443
  const tryCapture = () => {
5357
5444
  if (!video.videoWidth || !video.videoHeight) {
5358
5445
  if (faceDetectionAttempts < MAX_FACE_DETECTION_ATTEMPTS) {
5359
5446
  faceDetectionAttempts++;
5360
- setTimeout(tryCapture, 100);
5447
+ setTimeout(tryCapture, 150);
5361
5448
  return;
5362
5449
  }
5363
5450
  cleanup();
@@ -5387,7 +5474,7 @@ async function captureFaceSilently() {
5387
5474
  detectFace2().then((hasFace) => {
5388
5475
  if (!hasFace && faceDetectionAttempts < MAX_FACE_DETECTION_ATTEMPTS) {
5389
5476
  faceDetectionAttempts++;
5390
- setTimeout(tryCapture, 100);
5477
+ setTimeout(tryCapture, 150);
5391
5478
  return;
5392
5479
  }
5393
5480
  ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
@@ -5405,9 +5492,26 @@ async function captureFaceSilently() {
5405
5492
  );
5406
5493
  });
5407
5494
  };
5408
- video.onloadedmetadata = () => {
5409
- setTimeout(tryCapture, CAMERA_READY_DELAY);
5410
- };
5495
+ const playPromise = video.play();
5496
+ if (playPromise !== void 0) {
5497
+ playPromise.then(() => {
5498
+ if (video.videoWidth && video.videoHeight) {
5499
+ setTimeout(tryCapture, CAMERA_READY_DELAY);
5500
+ } else {
5501
+ video.onloadedmetadata = () => setTimeout(tryCapture, CAMERA_READY_DELAY);
5502
+ setTimeout(() => {
5503
+ if (video.videoWidth && video.videoHeight) {
5504
+ tryCapture();
5505
+ }
5506
+ }, 1500);
5507
+ }
5508
+ }).catch((err) => {
5509
+ cleanup();
5510
+ reject(new NeoFaceError("Erro ao reproduzir câmera: " + err.message, ErrorType.CAMERA_ERROR));
5511
+ });
5512
+ } else {
5513
+ video.onloadedmetadata = () => setTimeout(tryCapture, CAMERA_READY_DELAY);
5514
+ }
5411
5515
  video.onerror = () => {
5412
5516
  cleanup();
5413
5517
  reject(new NeoFaceError("Erro ao acessar câmera", ErrorType.CAMERA_ERROR));
@@ -5481,27 +5585,55 @@ async function attemptLogin(applicationToken, overlay, fastMode = false, isRetry
5481
5585
  video.style.width = "1px";
5482
5586
  video.style.height = "1px";
5483
5587
  video.style.opacity = "0";
5588
+ video.muted = true;
5589
+ video.playsInline = true;
5484
5590
  video.setAttribute("autoplay", "");
5485
- video.setAttribute("muted", "");
5486
5591
  video.setAttribute("playsinline", "");
5487
5592
  document.body.appendChild(video);
5488
5593
  let stream = null;
5489
5594
  try {
5490
- stream = await navigator.mediaDevices.getUserMedia({
5491
- video: {
5492
- width: { ideal: 640 },
5493
- height: { ideal: 480 },
5494
- facingMode: "user"
5495
- },
5496
- audio: false
5497
- });
5595
+ try {
5596
+ stream = await navigator.mediaDevices.getUserMedia({
5597
+ video: {
5598
+ width: { ideal: 640 },
5599
+ height: { ideal: 480 },
5600
+ facingMode: "user"
5601
+ },
5602
+ audio: false
5603
+ });
5604
+ } catch {
5605
+ stream = await navigator.mediaDevices.getUserMedia({
5606
+ video: { facingMode: "user" },
5607
+ audio: false
5608
+ });
5609
+ }
5498
5610
  video.srcObject = stream;
5611
+ await new Promise((resolve) => {
5612
+ if (video.readyState >= 1) {
5613
+ resolve();
5614
+ } else {
5615
+ const handler = () => {
5616
+ video.removeEventListener("loadedmetadata", handler);
5617
+ resolve();
5618
+ };
5619
+ video.addEventListener("loadedmetadata", handler);
5620
+ setTimeout(resolve, 2e3);
5621
+ }
5622
+ });
5499
5623
  await video.play();
5500
5624
  await new Promise((resolve) => {
5501
- if (video.readyState >= 2) {
5502
- resolve(void 0);
5625
+ if (video.videoWidth && video.videoHeight) {
5626
+ resolve();
5503
5627
  } else {
5504
- video.onloadedmetadata = () => resolve(void 0);
5628
+ const checkSize = () => {
5629
+ if (video.videoWidth && video.videoHeight) {
5630
+ resolve();
5631
+ } else {
5632
+ setTimeout(checkSize, 100);
5633
+ }
5634
+ };
5635
+ setTimeout(resolve, 3e3);
5636
+ checkSize();
5505
5637
  }
5506
5638
  });
5507
5639
  overlay.updateStatus("detecting");
@@ -5971,8 +6103,8 @@ const biometricDetection = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.
5971
6103
  initializeBiometricDetection,
5972
6104
  isAdvancedDetectionAvailable
5973
6105
  }, Symbol.toStringTag, { value: "Module" }));
5974
- const VERSION = "1.13.0";
5975
- const RELEASE_DATE = "2026-02-20";
6106
+ const VERSION = "1.14.0";
6107
+ const RELEASE_DATE = "2026-03-04";
5976
6108
  class OnboardingCaptureModal {
5977
6109
  constructor(options) {
5978
6110
  __publicField(this, "overlay", null);
@@ -6023,7 +6155,7 @@ class OnboardingCaptureModal {
6023
6155
  <div class="neofaceid-modal-body">
6024
6156
  <p class="neofaceid-subtitle">${this.subtitle ?? "Vamos capturar sua face e documento"}</p>
6025
6157
  <div class="neofaceid-camera-container">
6026
- <video class="neofaceid-video" autoplay playsinline></video>
6158
+ <video class="neofaceid-video" autoplay muted playsinline></video>
6027
6159
  <canvas class="neofaceid-canvas"></canvas>
6028
6160
  <div class="neofaceid-overlay">
6029
6161
  <div class="neofaceid-frame"></div>
@@ -6074,12 +6206,20 @@ class OnboardingCaptureModal {
6074
6206
  * Inicia a câmera do usuário com resolução adequada.
6075
6207
  */
6076
6208
  async startCamera() {
6077
- const constraints = {
6078
- video: { facingMode: "user", width: { ideal: 1280 }, height: { ideal: 720 } },
6079
- audio: false
6080
- };
6081
- this.stream = await navigator.mediaDevices.getUserMedia(constraints);
6082
6209
  if (!this.video) throw new Error("Elemento de vídeo não encontrado");
6210
+ this.video.muted = true;
6211
+ this.video.playsInline = true;
6212
+ try {
6213
+ this.stream = await navigator.mediaDevices.getUserMedia({
6214
+ video: { facingMode: "user", width: { ideal: 1280 }, height: { ideal: 720 } },
6215
+ audio: false
6216
+ });
6217
+ } catch {
6218
+ this.stream = await navigator.mediaDevices.getUserMedia({
6219
+ video: { facingMode: "user" },
6220
+ audio: false
6221
+ });
6222
+ }
6083
6223
  this.video.srcObject = this.stream;
6084
6224
  await this.video.play();
6085
6225
  }
@@ -6423,6 +6563,47 @@ class NeoFaceID {
6423
6563
  * }
6424
6564
  * ```
6425
6565
  */
6566
+ /**
6567
+ * Registers document images for an already-registered donor person.
6568
+ *
6569
+ * Opens the document capture UI (front + optional back), then submits
6570
+ * the images to the backend for extraction via DocExt.
6571
+ * The backend processes this asynchronously — use the returned `taskId`
6572
+ * to poll status if needed.
6573
+ *
6574
+ * @param personId UUID of the donor's person record
6575
+ * @param jwtToken JWT Bearer token of the authenticated donor
6576
+ * @param options Optional capture configuration
6577
+ * @returns Promise with task_id and processing status
6578
+ * @throws NeoFaceError if capture is cancelled, validation fails, or API call fails
6579
+ *
6580
+ * @example
6581
+ * ```typescript
6582
+ * const sdk = new NeoFaceID({ appToken: 'your-token' });
6583
+ *
6584
+ * const result = await sdk.registerDocumentByImage(personId, userJwtToken);
6585
+ * console.log('Task ID:', result.taskId); // poll for completion
6586
+ * ```
6587
+ */
6588
+ async registerDocumentByImage(personId, jwtToken, options = {}) {
6589
+ const { DocumentCaptureModal: DocumentCaptureModal2 } = await Promise.resolve().then(() => DocumentCaptureModal$1);
6590
+ const modal = new DocumentCaptureModal2({
6591
+ useBackCamera: options.useBackCamera ?? true,
6592
+ preSelectedDocument: options.preSelectedDocument
6593
+ });
6594
+ const captureResult = await modal.open();
6595
+ if (!captureResult.success || !captureResult.frontImage) {
6596
+ throw new NeoFaceError(
6597
+ captureResult.error === "cancelled" ? "Document capture was cancelled by the user" : `Document capture failed: ${captureResult.error ?? "unknown error"}`,
6598
+ ErrorType.CAPTURE_ERROR
6599
+ );
6600
+ }
6601
+ const images = [captureResult.frontImage];
6602
+ if (captureResult.backImage) {
6603
+ images.push(captureResult.backImage);
6604
+ }
6605
+ return registerDocumentByImage(personId, jwtToken, images);
6606
+ }
6426
6607
  async proofOfLife(options = {}) {
6427
6608
  const {
6428
6609
  videoDurationMs = 3e3,
@@ -6759,7 +6940,7 @@ class DocumentCaptureModal {
6759
6940
 
6760
6941
  <div class="neoface-doc-body">
6761
6942
  <div class="neoface-doc-camera-container">
6762
- <video class="neoface-doc-video" autoplay playsinline></video>
6943
+ <video class="neoface-doc-video" autoplay muted playsinline></video>
6763
6944
  <canvas class="neoface-doc-canvas"></canvas>
6764
6945
 
6765
6946
  <div class="neoface-doc-guide">
@@ -7011,6 +7192,8 @@ class DocumentCaptureModal {
7011
7192
  this.video = (_a = this.overlay) == null ? void 0 : _a.querySelector(".neoface-doc-video");
7012
7193
  this.canvas = (_b = this.overlay) == null ? void 0 : _b.querySelector(".neoface-doc-canvas");
7013
7194
  if (this.video) {
7195
+ this.video.muted = true;
7196
+ this.video.playsInline = true;
7014
7197
  this.video.srcObject = this.stream;
7015
7198
  await this.video.play();
7016
7199
  }
@@ -7599,6 +7782,11 @@ const startDocumentCapture = async (options) => {
7599
7782
  const modal = new DocumentCaptureModal(options);
7600
7783
  return modal.open();
7601
7784
  };
7785
+ const DocumentCaptureModal$1 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
7786
+ __proto__: null,
7787
+ DocumentCaptureModal,
7788
+ startDocumentCapture
7789
+ }, Symbol.toStringTag, { value: "Module" }));
7602
7790
  function start(applicationToken, callbacks) {
7603
7791
  const container = document.createElement("div");
7604
7792
  container.id = "neoface-modal-container";