react-native-biometric-verifier 0.0.39 → 0.0.41

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/package.json CHANGED
@@ -1,8 +1,11 @@
1
1
  {
2
2
  "name": "react-native-biometric-verifier",
3
- "version": "0.0.39",
3
+ "version": "0.0.41",
4
4
  "description": "A React Native module for biometric verification with face recognition and QR code scanning",
5
5
  "main": "src/index.js",
6
+ "private": false,
7
+ "license": "UNLICENSED",
8
+ "author": "PRAFUL DAS M M",
6
9
  "scripts": {
7
10
  "test": "echo \"Error: no test specified\" && exit 1"
8
11
  },
@@ -13,8 +16,18 @@
13
16
  "face-recognition",
14
17
  "qr-code"
15
18
  ],
16
- "author": "PRAFULDAS M M",
17
- "license": "JESCON TECHNOLOGIES PVT LTD",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "https://github.com/jescon-technologies/react-native-biometric-verifier"
22
+ },
23
+ "bugs": {
24
+ "url": "https://github.com/jescon-technologies/react-native-biometric-verifier/issues"
25
+ },
26
+ "homepage": "https://github.com/jescon-technologies/react-native-biometric-verifier#readme",
27
+ "files": [
28
+ "src",
29
+ "README.md"
30
+ ],
18
31
  "peerDependencies": {
19
32
  "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
20
33
  "react-native": ">=0.60.0",
@@ -24,7 +24,7 @@ const CaptureImageWithoutEdit = React.memo(
24
24
  showCodeScanner = false,
25
25
  isLoading = false,
26
26
  frameProcessorFps = 1,
27
- livenessLevel = 0,
27
+ livenessLevel = 0, // 0 = anti-spoof only, 1 = anti-spoof + blinking
28
28
  }) => {
29
29
  const cameraRef = useRef(null);
30
30
  const [cameraDevice, setCameraDevice] = useState(null);
@@ -42,7 +42,6 @@ const CaptureImageWithoutEdit = React.memo(
42
42
  const [antiSpoofConfidence, setAntiSpoofConfidence] = useState(0);
43
43
  const [isFaceCentered, setIsFaceCentered] = useState(false);
44
44
  const [hasSingleFace, setHasSingleFace] = useState(false);
45
- const [recognitionImageBase64, setRecognitionImageBase64] = useState(null);
46
45
 
47
46
  const captured = useRef(false);
48
47
  const isMounted = useRef(true);
@@ -61,7 +60,6 @@ const CaptureImageWithoutEdit = React.memo(
61
60
  setAntiSpoofConfidence(0);
62
61
  setIsFaceCentered(false);
63
62
  setHasSingleFace(false);
64
- setRecognitionImageBase64(null);
65
63
  }, []);
66
64
 
67
65
  const codeScanner = useCodeScanner({
@@ -78,13 +76,38 @@ const CaptureImageWithoutEdit = React.memo(
78
76
  });
79
77
 
80
78
  const onStableFaceDetected = useCallback(
81
- async (faceRect, Result) => {
79
+ async (faceRect) => {
82
80
  if (!isMounted.current) return;
83
81
  if (captured.current) return;
82
+
84
83
  captured.current = true;
85
84
  setFaces([faceRect]);
85
+
86
86
  try {
87
- onCapture(Result?.recognitionImageBase64);
87
+ if (!cameraRef.current) {
88
+ throw new Error('Camera ref not available');
89
+ }
90
+
91
+ const photo = await cameraRef.current.takePhoto({
92
+ flash: 'off',
93
+ qualityPrioritization: 'quality',
94
+ enableShutterSound: false,
95
+ skipMetadata: true,
96
+ });
97
+
98
+ if (!photo || !photo.path) {
99
+ throw new Error('Failed to capture photo - no path returned');
100
+ }
101
+
102
+ const photopath = `file://${photo.path}`;
103
+ const fileName = photopath.substr(photopath.lastIndexOf('/') + 1);
104
+ const photoData = {
105
+ uri: photopath,
106
+ filename: fileName,
107
+ filetype: 'image/jpeg',
108
+ };
109
+
110
+ onCapture(photoData, faceRect);
88
111
  } catch (e) {
89
112
  console.error('Capture error:', e);
90
113
  captured.current = false;
@@ -107,7 +130,6 @@ const CaptureImageWithoutEdit = React.memo(
107
130
  setAntiSpoofConfidence(antiSpoofState.confidence || 0);
108
131
  setIsFaceCentered(antiSpoofState.isFaceCentered || false);
109
132
  setHasSingleFace(antiSpoofState.hasSingleFace || false);
110
- setRecognitionImageBase64(antiSpoofState.recognitionImageBase64 || null);
111
133
  }
112
134
 
113
135
  if (count === 1) {
@@ -141,11 +163,6 @@ const CaptureImageWithoutEdit = React.memo(
141
163
  const onAntiSpoofUpdate = useCallback((result) => {
142
164
  if (!isMounted.current) return;
143
165
  try {
144
- // Update recognition image when available
145
- if (result?.recognitionImageBase64) {
146
- setRecognitionImageBase64(result.recognitionImageBase64);
147
- }
148
-
149
166
  // Animate live indicator when face becomes live
150
167
  if (result?.isLive && !isFaceLive) {
151
168
  Animated.spring(liveIndicatorAnim, {
@@ -109,7 +109,6 @@ export const useFaceDetectionFrameProcessor = ({
109
109
  lastResult: null,
110
110
  isLive: false,
111
111
  confidence: 0,
112
- recognitionImageBase64: null, // Added to store base64 image
113
112
  },
114
113
 
115
114
  // Face centering
@@ -148,7 +147,6 @@ export const useFaceDetectionFrameProcessor = ({
148
147
  state.antiSpoof.lastResult = null;
149
148
  state.antiSpoof.isLive = false;
150
149
  state.antiSpoof.confidence = 0;
151
- state.antiSpoof.recognitionImageBase64 = null; // Reset base64 image
152
150
  state.flags.hasSingleFace = false;
153
151
  state.centering.centeredFrames = 0;
154
152
  state.flags.isFaceCentered = false;
@@ -302,7 +300,6 @@ export const useFaceDetectionFrameProcessor = ({
302
300
  consecutiveLiveFrames: 0,
303
301
  isFaceCentered: false,
304
302
  hasSingleFace: false,
305
- recognitionImageBase64: null,
306
303
  });
307
304
  return;
308
305
  }
@@ -317,7 +314,6 @@ export const useFaceDetectionFrameProcessor = ({
317
314
  consecutiveLiveFrames: 0,
318
315
  isFaceCentered: false,
319
316
  hasSingleFace: false,
320
- recognitionImageBase64: null,
321
317
  });
322
318
  return;
323
319
  }
@@ -364,28 +360,19 @@ export const useFaceDetectionFrameProcessor = ({
364
360
  if (state.flags.isFaceCentered) {
365
361
  try {
366
362
  antiSpoofResult = faceAntiSpoofFrameProcessor?.(frame);
367
-
368
363
  if (antiSpoofResult != null) {
369
364
  state.antiSpoof.lastResult = antiSpoofResult;
370
365
 
371
366
  const isLive = antiSpoofResult.isLive === true;
372
367
  const confidence = antiSpoofResult.combinedScore || antiSpoofResult.neuralNetworkScore || 0;
373
- const recognitionImageBase64 = antiSpoofResult.recognitionImageBase64 || null;
374
368
 
375
- // Store the base64 image when face is live
376
369
  if (isLive && confidence > ANTI_SPOOF_CONFIDENCE_THRESHOLD) {
377
370
  state.antiSpoof.consecutiveLiveFrames = Math.min(
378
371
  REQUIRED_CONSECUTIVE_LIVE_FRAMES,
379
372
  state.antiSpoof.consecutiveLiveFrames + 1
380
373
  );
381
- // Store the base64 image when we have a live face
382
- if (recognitionImageBase64) {
383
- state.antiSpoof.recognitionImageBase64 = recognitionImageBase64;
384
- }
385
374
  } else {
386
375
  state.antiSpoof.consecutiveLiveFrames = Math.max(0, state.antiSpoof.consecutiveLiveFrames - 1);
387
- // Clear base64 image if face is not live
388
- state.antiSpoof.recognitionImageBase64 = null;
389
376
  }
390
377
  state.antiSpoof.isLive = state.antiSpoof.consecutiveLiveFrames >= REQUIRED_CONSECUTIVE_LIVE_FRAMES;
391
378
  state.antiSpoof.confidence = confidence;
@@ -398,19 +385,16 @@ export const useFaceDetectionFrameProcessor = ({
398
385
  rawResult: antiSpoofResult,
399
386
  consecutiveLiveFrames: state.antiSpoof.consecutiveLiveFrames,
400
387
  isFaceCentered: state.flags.isFaceCentered,
401
- recognitionImageBase64: state.antiSpoof.recognitionImageBase64,
402
388
  });
403
389
  }
404
390
  }
405
391
  } catch (antiSpoofError) {
406
392
  // Silent error handling
407
- console.log('Anti-spoof error:', antiSpoofError);
408
393
  }
409
394
  } else {
410
395
  // Reset anti-spoof if face not centered
411
396
  state.antiSpoof.consecutiveLiveFrames = 0;
412
397
  state.antiSpoof.isLive = false;
413
- state.antiSpoof.recognitionImageBase64 = null;
414
398
  }
415
399
 
416
400
  // Liveness logic - optimized
@@ -477,7 +461,6 @@ export const useFaceDetectionFrameProcessor = ({
477
461
  consecutiveLiveFrames: state.antiSpoof.consecutiveLiveFrames,
478
462
  isFaceCentered: state.flags.isFaceCentered,
479
463
  hasSingleFace: true,
480
- recognitionImageBase64: state.antiSpoof.recognitionImageBase64,
481
464
  });
482
465
  }
483
466
 
@@ -496,13 +479,9 @@ export const useFaceDetectionFrameProcessor = ({
496
479
 
497
480
  if (shouldCapture) {
498
481
  state.flags.captured = true;
499
- // Include the base64 image in the stable face detection callback
500
482
  runOnStable(
501
483
  { x, y, width, height },
502
- {
503
- ...state.antiSpoof.lastResult,
504
- recognitionImageBase64: state.antiSpoof.recognitionImageBase64
505
- }
484
+ state.antiSpoof.lastResult
506
485
  );
507
486
  }
508
487
  } else {
@@ -513,7 +492,6 @@ export const useFaceDetectionFrameProcessor = ({
513
492
  state.flags.hasSingleFace = false;
514
493
  state.centering.centeredFrames = 0;
515
494
  state.flags.isFaceCentered = false;
516
- state.antiSpoof.recognitionImageBase64 = null;
517
495
 
518
496
  runOnFaces(detected.length, 0, state.liveness.step, false, {
519
497
  isLive: false,
@@ -521,12 +499,10 @@ export const useFaceDetectionFrameProcessor = ({
521
499
  consecutiveLiveFrames: 0,
522
500
  isFaceCentered: false,
523
501
  hasSingleFace: false,
524
- recognitionImageBase64: null,
525
502
  });
526
503
  }
527
504
  } catch (err) {
528
505
  // Error boundary - ensure frame is released
529
- console.log('Frame processor error:', err);
530
506
  } finally {
531
507
  frame.release?.();
532
508
  }
@@ -551,7 +527,6 @@ export const useFaceDetectionFrameProcessor = ({
551
527
  state.antiSpoof.lastResult = null;
552
528
  state.antiSpoof.isLive = false;
553
529
  state.antiSpoof.confidence = 0;
554
- state.antiSpoof.recognitionImageBase64 = null; // Reset base64 image
555
530
  state.flags.hasSingleFace = false;
556
531
  state.centering.centeredFrames = 0;
557
532
  state.flags.isFaceCentered = false;
@@ -586,7 +561,6 @@ export const useFaceDetectionFrameProcessor = ({
586
561
  lastResult: null,
587
562
  isLive: false,
588
563
  confidence: 0,
589
- recognitionImageBase64: null, // Reset base64 image
590
564
  },
591
565
  centering: {
592
566
  centeredFrames: 0,
@@ -638,7 +612,6 @@ export const useFaceDetectionFrameProcessor = ({
638
612
  lastResult: sharedState.value.antiSpoof.lastResult,
639
613
  hasSingleFace: sharedState.value.flags.hasSingleFace,
640
614
  isFaceCentered: sharedState.value.flags.isFaceCentered,
641
- recognitionImageBase64: sharedState.value.antiSpoof.recognitionImageBase64,
642
615
  },
643
616
  };
644
617
  };
@@ -0,0 +1,64 @@
1
+ import { useCallback } from 'react';
2
+ import ImageResizer from 'react-native-image-resizer';
3
+ import RNFS from 'react-native-fs';
4
+ import { Global } from '../utils/Global';
5
+
6
+ /**
7
+ * Custom hook to process images: resize and convert to Base64.
8
+ *
9
+ * @returns {Object} { convertImageToBase64 }
10
+ */
11
+ export const useImageProcessing = () => {
12
+ /**
13
+ * Converts an image URI to a Base64 string after resizing.
14
+ *
15
+ * @param {string} uri - Image file URI.
16
+ * @param {boolean} includeMimeType - Whether to include MIME type in the result.
17
+ * @returns {Promise<string>} Base64 string of the image.
18
+ */
19
+ const convertImageToBase64 = useCallback(async (uri, includeMimeType = false) => {
20
+ try {
21
+ if (!uri || typeof uri !== 'string') {
22
+ throw new Error('Invalid image URI provided.');
23
+ }
24
+
25
+ // Optional: Check file info
26
+ try {
27
+ await RNFS.stat(uri);
28
+ } catch {
29
+ // Skip warnings; silently ignore
30
+ }
31
+
32
+ // Resize image
33
+ const resizedImage = await ImageResizer.createResizedImage(
34
+ uri,
35
+ Global.ImageResize.width,
36
+ Global.ImageResize.height,
37
+ Global.ImageResize.format, // 'JPEG' or 'PNG'
38
+ Global.ImageResize.quality, // e.g., 80
39
+ 0, // Rotation
40
+ undefined, // Output path
41
+ false // Keep EXIF metadata
42
+ );
43
+
44
+ if (!resizedImage?.uri) {
45
+ throw new Error('Image resizing returned an invalid result.');
46
+ }
47
+
48
+ // Convert resized image to Base64
49
+ let base64Data = await RNFS.readFile(resizedImage.uri, 'base64');
50
+
51
+ if (includeMimeType) {
52
+ const mimeType = Global.ImageResize.format.toLowerCase() === 'png' ? 'image/png' : 'image/jpeg';
53
+ base64Data = `data:${mimeType};base64,${base64Data}`;
54
+ }
55
+
56
+ return base64Data;
57
+ } catch (error) {
58
+ console.error('Error in convertImageToBase64:', error.message || error);
59
+ throw error; // Rethrow for caller handling
60
+ }
61
+ }, []);
62
+
63
+ return { convertImageToBase64 };
64
+ };
package/src/index.js CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  TouchableOpacity,
11
11
  Text,
12
12
  Modal,
13
+ InteractionManager,
13
14
  StyleSheet,
14
15
  Platform,
15
16
  Animated,
@@ -20,6 +21,7 @@ import { useNavigation } from "@react-navigation/native";
20
21
  // Custom hooks
21
22
  import { useCountdown } from "./hooks/useCountdown";
22
23
  import { useGeolocation } from "./hooks/useGeolocation";
24
+ import { useImageProcessing } from "./hooks/useImageProcessing";
23
25
  import { useNotifyMessage } from "./hooks/useNotifyMessage";
24
26
  import { useSafeCallback } from "./hooks/useSafeCallback";
25
27
 
@@ -44,6 +46,7 @@ const BiometricModal = React.memo(
44
46
  // Custom hooks
45
47
  const { countdown, startCountdown, resetCountdown, pauseCountdown, resumeCountdown } = useCountdown();
46
48
  const { requestLocationPermission, getCurrentLocation } = useGeolocation();
49
+ const { convertImageToBase64 } = useImageProcessing();
47
50
  const { notification, fadeAnim, slideAnim, notifyMessage, clearNotification } = useNotifyMessage();
48
51
  const safeCallback = useSafeCallback(callback, notifyMessage);
49
52
 
@@ -213,7 +216,7 @@ const BiometricModal = React.memo(
213
216
 
214
217
  // Face scan upload
215
218
  const uploadFaceScan = useCallback(
216
- async (base64) => {
219
+ async (selfie) => {
217
220
  if (!validateApiUrl()) return;
218
221
  const currentData = dataRef.current;
219
222
 
@@ -228,68 +231,84 @@ const BiometricModal = React.memo(
228
231
  animationState: Global.AnimationStates.processing,
229
232
  });
230
233
 
231
- if (!base64) {
232
- handleProcessError("Failed to process image.");
233
- return;
234
- }
234
+ InteractionManager.runAfterInteractions(async () => {
235
+ let base64;
235
236
 
236
- try {
237
- const body = { image: base64 };
238
- const header = { faceid: currentData };
239
- const buttonapi = `${apiurl}python/recognize`;
237
+ try {
238
+ updateState({
239
+ loadingType: Global.LoadingTypes.imageProcessing,
240
+ });
240
241
 
241
- updateState({
242
- loadingType: Global.LoadingTypes.networkRequest,
243
- });
244
- console.log("API URL:", buttonapi);
245
- console.log("Sending request to API...", JSON.stringify(body));
246
- const response = await networkServiceCall(
247
- "POST",
248
- buttonapi,
249
- header,
250
- body
251
- );
252
- console.log("API Response:", JSON.stringify(response));
253
- if (response?.httpstatus === 200 && response?.data?.data) {
254
- responseRef.current = response;
242
+ base64 = await convertImageToBase64(selfie?.uri);
243
+ } catch (err) {
244
+ console.error("Image conversion failed:", err);
245
+ handleProcessError("Image conversion failed.", err);
246
+ return;
247
+ }
248
+
249
+ if (!base64) {
250
+ handleProcessError("Failed to process image.");
251
+ return;
252
+ }
253
+
254
+ try {
255
+ const body = { image: base64 };
256
+ const header = { faceid: currentData };
257
+ const buttonapi = `${apiurl}python/recognize`;
255
258
 
256
259
  updateState({
257
- employeeData: response.data?.data || null,
258
- animationState: Global.AnimationStates.success,
259
- isLoading: false,
260
- loadingType: Global.LoadingTypes.none,
260
+ loadingType: Global.LoadingTypes.networkRequest,
261
261
  });
262
262
 
263
- notifyMessage("Identity verified successfully!", "success");
263
+ const response = await networkServiceCall(
264
+ "POST",
265
+ buttonapi,
266
+ header,
267
+ body
268
+ );
264
269
 
265
- if (qrscan) {
266
- setTimeout(() => startQRCodeScan(), 1200);
267
- } else {
268
- safeCallback(responseRef.current);
270
+ if (response?.httpstatus === 200) {
271
+ responseRef.current = response;
269
272
 
270
- if (resetTimeoutRef.current) {
271
- clearTimeout(resetTimeoutRef.current);
272
- }
273
+ updateState({
274
+ employeeData: response.data?.data || null,
275
+ animationState: Global.AnimationStates.success,
276
+ isLoading: false,
277
+ loadingType: Global.LoadingTypes.none,
278
+ });
273
279
 
274
- resetTimeoutRef.current = setTimeout(() => {
275
- resetState();
276
- }, 1200);
280
+ notifyMessage("Identity verified successfully!", "success");
281
+
282
+ if (qrscan) {
283
+ setTimeout(() => startQRCodeScan(), 1200);
284
+ } else {
285
+ safeCallback(responseRef.current);
286
+
287
+ if (resetTimeoutRef.current) {
288
+ clearTimeout(resetTimeoutRef.current);
289
+ }
290
+
291
+ resetTimeoutRef.current = setTimeout(() => {
292
+ resetState();
293
+ }, 1200);
294
+ }
295
+ } else {
296
+ handleProcessError(
297
+ response?.data?.message ||
298
+ "Face not recognized. Please try again."
299
+ );
277
300
  }
278
- } else {
301
+ } catch (error) {
302
+ console.error("Network request failed:", error);
279
303
  handleProcessError(
280
- response?.data?.message ||
281
- "Face not recognized. Please try again."
304
+ "Connection error. Please check your network.",
305
+ error
282
306
  );
283
307
  }
284
- } catch (error) {
285
- console.error("Network request failed:", error);
286
- handleProcessError(
287
- "Connection error. Please check your network.",
288
- error
289
- );
290
- }
308
+ });
291
309
  },
292
310
  [
311
+ convertImageToBase64,
293
312
  notifyMessage,
294
313
  qrscan,
295
314
  resetState,
@@ -357,7 +376,28 @@ const BiometricModal = React.memo(
357
376
  );
358
377
 
359
378
  if (distance <= Global.MaxDistanceMeters) {
379
+ const locationDetails = {
380
+ qrLocation: {
381
+ latitude: lat,
382
+ longitude: lng,
383
+ },
384
+ deviceLocation: {
385
+ latitude: location.latitude,
386
+ longitude: location.longitude,
387
+ accuracy: location.accuracy,
388
+ },
389
+ distanceMeters: distance,
390
+ verified: true,
391
+ verifiedAt: new Date().toISOString(),
392
+ };
393
+
394
+ responseRef.current = {
395
+ ...(responseRef.current || {}), // existing faceData
396
+ location: locationDetails,
397
+ };
398
+
360
399
  safeCallback(responseRef.current);
400
+
361
401
  notifyMessage("Location verified successfully!", "success");
362
402
 
363
403
  updateState({
@@ -373,7 +413,8 @@ const BiometricModal = React.memo(
373
413
  resetTimeoutRef.current = setTimeout(() => {
374
414
  resetState();
375
415
  }, 1200);
376
- } else {
416
+ }
417
+ else {
377
418
  handleProcessError(
378
419
  `Location mismatch (${distance.toFixed(0)}m away).`
379
420
  );