react-native-biometric-verifier 0.0.13 → 0.0.14

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,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-biometric-verifier",
3
- "version": "0.0.13",
3
+ "version": "0.0.14",
4
4
  "description": "A React Native module for biometric verification with face recognition and QR code scanning",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -1,3 +1,4 @@
1
+ // CaptureImageWithoutEdit.js
1
2
  import React, { useRef, useEffect, useState, useCallback } from 'react';
2
3
  import {
3
4
  View,
@@ -6,6 +7,8 @@ import {
6
7
  StyleSheet,
7
8
  ActivityIndicator,
8
9
  Platform,
10
+ AppState,
11
+ BackHandler,
9
12
  } from 'react-native';
10
13
  import Icon from 'react-native-vector-icons/MaterialIcons';
11
14
  import {
@@ -21,10 +24,11 @@ import { useFaceDetector } from 'react-native-vision-camera-face-detector';
21
24
  import { COLORS } from "../utils/constants";
22
25
 
23
26
  // Constants for configuration
24
- const FACE_STABILITY_THRESHOLD = 5; // Number of stable frames required
25
- const FACE_MOVEMENT_THRESHOLD = 15; // Pixel movement threshold for stability
26
- const FRAME_PROCESSOR_FPS = 3; // Frames per second for face detection
27
- const MIN_FACE_SIZE = 0.2; // Minimum face size for detection
27
+ const FACE_STABILITY_THRESHOLD = 4; // how many stable samples required
28
+ const FACE_MOVEMENT_THRESHOLD = 18; // pixel threshold (adjust as needed)
29
+ const FRAME_PROCESSOR_MIN_INTERVAL_MS = 700; // throttle; process at most every ~700ms
30
+ const FRAME_PROCESSOR_FPS = 1;
31
+ const MIN_FACE_SIZE = 0.2;
28
32
 
29
33
  const CaptureImageWithoutEdit = React.memo(
30
34
  ({
@@ -40,139 +44,193 @@ const CaptureImageWithoutEdit = React.memo(
40
44
  const [cameraPermission, setCameraPermission] = useState('not-determined');
41
45
  const [showCamera, setShowCamera] = useState(false);
42
46
  const [cameraInitialized, setCameraInitialized] = useState(false);
43
-
44
- // Face detection states
45
- const [faces, setFaces] = useState([]);
47
+
48
+ // Face detection states (JS side holds minimal info only)
49
+ const [faces, setFaces] = useState([]); // we store minimal face rectangles here
46
50
  const [singleFaceDetected, setSingleFaceDetected] = useState(false);
47
51
  const [cameraError, setCameraError] = useState(null);
48
-
49
- // Stability tracking refs
50
- const stableCounter = useRef(0);
51
- const lastBounds = useRef(null);
52
+
53
+ // refs & lifecycle
52
54
  const captured = useRef(false);
53
- const faceDetectionTimeout = useRef(null);
54
- const processingFrame = useRef(false); // To prevent concurrent frame processing
55
+ const appState = useRef(AppState.currentState);
56
+ const isMounted = useRef(true);
55
57
 
58
+ // Face detector options
56
59
  const faceDetectionOptions = {
57
- performanceMode: 'accurate',
58
- landmarkMode: 'all',
59
- contourMode: 'all',
60
- classificationMode: 'all',
60
+ performanceMode: 'fast',
61
+ landmarkMode: 'none',
62
+ contourMode: 'none',
63
+ classificationMode: 'none',
61
64
  minFaceSize: MIN_FACE_SIZE,
62
65
  };
63
-
64
66
  const { detectFaces } = useFaceDetector(faceDetectionOptions);
65
67
 
68
+ // Code scanner (unchanged)
66
69
  const codeScanner = useCodeScanner({
67
- codeTypes: ['qr', 'ean-13', 'code-128', 'code-39', 'code-93'],
70
+ codeTypes: ['qr', 'ean-13'],
68
71
  onCodeScanned: (codes) => {
69
72
  if (showCodeScanner && codes && codes[0]?.value && !isLoading) {
70
- console.log('🔍 QR Code scanned:', codes[0].value);
73
+ console.log('QR Code scanned:', codes[0].value);
71
74
  onCapture(codes[0].value);
72
75
  }
73
76
  },
74
77
  });
75
78
 
76
- // Safe callback to JS for face detection results
77
- const onFacesDetected = Worklets.createRunOnJS((detectedFaces) => {
78
- // Only process if we have exactly one face
79
- if (detectedFaces.length === 1 && !showCodeScanner && !captured.current) {
80
- setFaces(detectedFaces);
81
- setSingleFaceDetected(true);
82
-
83
- const face = detectedFaces[0];
84
- const { bounds } = face;
79
+ //
80
+ // --- Worklet -> JS callbacks (small payloads only) ---
81
+ //
82
+ // Called when the worklet believes a face is stable enough to capture.
83
+ const onStableFaceDetected = useCallback((faceRect) => {
84
+ // faceRect is a tiny object: { x, y, width, height }
85
+ if (!isMounted.current) return;
86
+ if (captured.current) return;
85
87
 
86
- if (lastBounds.current) {
87
- const dx = Math.abs(bounds.x - lastBounds.current.x);
88
- const dy = Math.abs(bounds.y - lastBounds.current.y);
88
+ // Lock capture to avoid duplicate triggers
89
+ captured.current = true;
90
+ setSingleFaceDetected(true);
91
+ setFaces([faceRect]); // minimal UI update
89
92
 
90
- // small movement = stable
91
- if (dx < FACE_MOVEMENT_THRESHOLD && dy < FACE_MOVEMENT_THRESHOLD) {
92
- stableCounter.current += 1;
93
- } else {
94
- stableCounter.current = 0;
93
+ // Kick off actual photo capture on JS thread
94
+ (async () => {
95
+ try {
96
+ if (!cameraRef.current) {
97
+ throw new Error('Camera ref not available');
95
98
  }
96
- }
97
99
 
98
- lastBounds.current = bounds;
99
-
100
- // if face stable for required frames → capture
101
- if (stableCounter.current >= FACE_STABILITY_THRESHOLD && cameraRef.current) {
102
- captured.current = true; // lock to avoid multiple shots
103
- setSingleFaceDetected(false); // Reset UI state
104
-
105
- (async () => {
106
- try {
107
- const photo = await cameraRef.current.takePhoto({
108
- flash: 'off',
109
- qualityPrioritization: 'balanced',
110
- enableShutterSound: false,
111
- });
112
-
113
- console.log('Photo captured:', photo.path);
114
- const photopath = `file://${photo.path}`;
115
- const fileName = photopath.substr(photopath.lastIndexOf('/') + 1);
116
- const photoData = {
117
- uri: photopath,
118
- filename: fileName,
119
- filetype: 'image/jpeg',
120
- };
121
-
122
- onCapture(photoData);
123
-
124
- } catch (e) {
125
- console.error('Capture error:', e);
126
- captured.current = false; // Reset capture lock on error
127
- setCameraError('Failed to capture image. Please try again.');
128
- }
129
- })();
100
+ const photo = await cameraRef.current.takePhoto({
101
+ flash: 'off',
102
+ qualityPrioritization: 'balanced',
103
+ enableShutterSound: false,
104
+ skipMetadata: true,
105
+ });
106
+
107
+ const photopath = `file://${photo.path}`;
108
+ const fileName = photopath.substr(photopath.lastIndexOf('/') + 1);
109
+ const photoData = {
110
+ uri: photopath,
111
+ filename: fileName,
112
+ filetype: 'image/jpeg',
113
+ };
114
+
115
+ // send captured photo upstream
116
+ onCapture(photoData);
117
+ } catch (e) {
118
+ console.error('Capture error:', e);
119
+ captured.current = false; // allow retries
120
+ setCameraError('Failed to capture image. Please try again.');
130
121
  }
122
+ })();
123
+ }, [onCapture]);
124
+
125
+ // Called to update face count / progress minimally for UI
126
+ const onFacesUpdate = useCallback((payload) => {
127
+ // payload: { count, progress? }
128
+ if (!isMounted.current) return;
129
+ const { count, progress } = payload;
130
+ if (count === 1) {
131
+ setSingleFaceDetected(true);
132
+ setFaces(prev => {
133
+ // keep existing rectangle if present; otherwise set a placeholder
134
+ if (prev.length === 1) return prev;
135
+ return [{ x: 0, y: 0, width: 0, height: 0 }];
136
+ });
131
137
  } else {
132
- // Reset if no face or multiple faces detected
133
- setFaces(detectedFaces);
134
- setSingleFaceDetected(detectedFaces.length === 1);
135
- stableCounter.current = 0;
136
- lastBounds.current = null;
137
-
138
- // Clear any pending timeout
139
- if (faceDetectionTimeout.current) {
140
- clearTimeout(faceDetectionTimeout.current);
141
- }
138
+ setSingleFaceDetected(false);
139
+ setFaces([]);
142
140
  }
143
-
144
- processingFrame.current = false;
145
- });
141
+ // progress not necessary to store but could be used to show UI progress if desired
142
+ }, []);
143
+
144
+ // Create runOnJS wrappers (these are the functions that we'll call from the worklet)
145
+ // Note: Worklets.createRunOnJS returns a callable function which is available inside the worklet.
146
+ const runOnStable = Worklets.createRunOnJS(onStableFaceDetected);
147
+ const runOnFaces = Worklets.createRunOnJS(onFacesUpdate);
146
148
 
147
- // Frame processor runs on UI thread
149
+ //
150
+ // --- Frame processor (worklet) ---
151
+ //
152
+ // The heavy detection is done here, with stability tracking inside the worklet.
153
+ // The worklet calls runOnStable(faceRect) only when it's time to capture.
148
154
  const frameProcessor = useFrameProcessor((frame) => {
149
155
  'worklet';
150
- if (showCodeScanner || captured.current || processingFrame.current) return;
151
-
152
- processingFrame.current = true;
156
+ // prevent processing when scanning QR or when we already captured
157
+ if (global.__showCodeScanner || global.__captured) return;
158
+
159
+ // throttle processing to avoid overloading UI thread / CPU
160
+ const now = performance.now();
161
+ const last = global.__lastProcessedTime || 0;
162
+ if (now - last < FRAME_PROCESSOR_MIN_INTERVAL_MS) return;
163
+ global.__lastProcessedTime = now;
164
+
153
165
  try {
154
- const detected = detectFaces(frame); // run detection
155
-
156
- // Only proceed if exactly one face is detected
157
- if (detected.length === 1) {
158
- onFacesDetected(detected); // send results back to JS
166
+ // run face detection (this returns native objects)
167
+ const detected = detectFaces(frame); // array
168
+
169
+ const len = detected.length;
170
+
171
+ if (len === 1 && !global.__captured) {
172
+ // single face logic and stability tracking inside the worklet
173
+ const f = detected[0];
174
+ const bounds = f.bounds; // { x, y, width, height }
175
+
176
+ // initialize worklet-side state if necessary
177
+ if (!global.__lastBounds) {
178
+ global.__lastBounds = bounds;
179
+ global.__stableCount = 1;
180
+ } else {
181
+ const dx = Math.abs(bounds.x - global.__lastBounds.x);
182
+ const dy = Math.abs(bounds.y - global.__lastBounds.y);
183
+
184
+ if (dx < FACE_MOVEMENT_THRESHOLD && dy < FACE_MOVEMENT_THRESHOLD) {
185
+ global.__stableCount = (global.__stableCount || 0) + 1;
186
+ } else {
187
+ global.__stableCount = 0;
188
+ }
189
+ global.__lastBounds = bounds;
190
+ }
191
+
192
+ // If stable long enough -> trigger JS capture (only send very small payload)
193
+ if ((global.__stableCount || 0) >= FACE_STABILITY_THRESHOLD) {
194
+ // mark captured on worklet side to stop further processing immediately
195
+ global.__captured = true;
196
+
197
+ // send only minimal rectangle data to JS to avoid expensive bridging
198
+ const faceRect = {
199
+ x: Math.round(global.__lastBounds.x),
200
+ y: Math.round(global.__lastBounds.y),
201
+ width: Math.round(global.__lastBounds.width),
202
+ height: Math.round(global.__lastBounds.height),
203
+ };
204
+
205
+ // runOnStable is the JS callback created above
206
+ runOnStable(faceRect);
207
+ } else {
208
+ // optionally send lightweight updates for UI (count & progress)
209
+ runOnFaces({ count: 1, progress: global.__stableCount || 0 });
210
+ }
159
211
  } else {
160
- onFacesDetected(detected); // send results (could be empty or multiple)
212
+ // no face or multiple faces -> reset worklet-side stability trackers
213
+ global.__lastBounds = null;
214
+ global.__stableCount = 0;
215
+ // notify JS about the number of faces (minimal payload)
216
+ runOnFaces({ count: len });
161
217
  }
162
- } catch (error) {
163
- console.error('Frame processor error:', error);
164
- processingFrame.current = false;
218
+ } catch (e) {
219
+ // Avoid console.log heavy operations in worklet; just swallow or set a simple global flag
220
+ // If you need debugging, temporarily put a runOnFaces({ debug: String(e) }) but avoid in production.
165
221
  }
166
222
  }, [detectFaces, showCodeScanner]);
167
223
 
168
- // Initialize camera
224
+ //
225
+ // --- Effect: app lifecycle & camera initialization ---
226
+ //
169
227
  useEffect(() => {
170
- let isMounted = true;
171
-
228
+ isMounted.current = true;
229
+
172
230
  const initializeCamera = async () => {
173
231
  try {
174
- if (!isMounted) return;
175
-
232
+ if (!isMounted.current) return;
233
+
176
234
  const permission = await Camera.requestCameraPermission();
177
235
  setCameraPermission(permission);
178
236
 
@@ -182,17 +240,18 @@ const CaptureImageWithoutEdit = React.memo(
182
240
 
183
241
  if (device) {
184
242
  setCameraDevice(device);
185
- setShowCamera(true);
243
+ // short delay so camera UI transitions cleanly
244
+ setTimeout(() => setShowCamera(true), 100);
186
245
  } else {
187
- console.error('No camera device found for type:', cameraType);
246
+ console.error('No camera device found for type:', cameraType);
188
247
  setCameraError('Camera not available on this device');
189
248
  }
190
249
  } else {
191
250
  setCameraError('Camera permission denied');
192
251
  }
193
252
  } catch (error) {
194
- console.error('Camera initialization failed:', error);
195
- if (isMounted) {
253
+ console.error('Camera init failed:', error);
254
+ if (isMounted.current) {
196
255
  setCameraError('Failed to initialize camera');
197
256
  }
198
257
  }
@@ -201,85 +260,145 @@ const CaptureImageWithoutEdit = React.memo(
201
260
  initializeCamera();
202
261
 
203
262
  return () => {
204
- isMounted = false;
263
+ isMounted.current = false;
205
264
  setShowCamera(false);
206
- // Clean up timeout on unmount
207
- if (faceDetectionTimeout.current) {
208
- clearTimeout(faceDetectionTimeout.current);
209
- }
210
265
  };
211
266
  }, [cameraType]);
212
-
213
- const format = useCameraFormat(cameraDevice, [
214
- { photoResolution: { width: 640, height: 640 } },
215
- ]);
216
267
 
268
+ // app state change
269
+ useEffect(() => {
270
+ const subscription = AppState.addEventListener('change', nextAppState => {
271
+ if (appState.current.match(/inactive|background/) && nextAppState === 'active') {
272
+ // App came to the foreground
273
+ if (cameraPermission === 'granted') {
274
+ setShowCamera(true);
275
+ }
276
+ } else if (nextAppState.match(/inactive|background/)) {
277
+ setShowCamera(false);
278
+ }
279
+ appState.current = nextAppState;
280
+ });
281
+
282
+ return () => subscription.remove();
283
+ }, [cameraPermission]);
284
+
285
+ // android back handler (same as before)
286
+ useEffect(() => {
287
+ const backHandler = BackHandler.addEventListener('hardwareBackPress', () => false);
288
+ return () => backHandler.remove();
289
+ }, []);
290
+
291
+ // handle camera errors
217
292
  const handleCameraError = useCallback((error) => {
218
293
  console.error('Camera error:', error);
219
294
  let errorMessage = 'Camera error occurred';
220
-
295
+
221
296
  if (error instanceof CameraRuntimeError) {
222
297
  switch (error.code) {
223
298
  case 'session/configuration-failed':
224
- errorMessage = 'Camera configuration failed';
299
+ errorMessage = 'Camera configuration failed. Please try again.';
225
300
  break;
226
301
  case 'device/not-available':
227
302
  errorMessage = 'Camera not available';
228
303
  break;
229
304
  case 'permission/microphone-not-granted':
230
- errorMessage = 'Microphone permission required';
305
+ errorMessage = 'Camera permission required';
231
306
  break;
232
307
  default:
233
308
  errorMessage = `Camera error: ${error.message}`;
234
309
  }
235
310
  }
236
-
311
+
237
312
  setCameraError(errorMessage);
313
+ setShowCamera(false);
238
314
  }, []);
239
315
 
240
316
  const handleCameraInitialized = useCallback(() => {
241
- console.log('Camera initialized successfully');
242
317
  setCameraInitialized(true);
243
318
  setCameraError(null);
244
319
  }, []);
245
320
 
321
+ // format selection (unchanged)
322
+ const format = useCameraFormat(cameraDevice, [
323
+ { videoResolution: { width: 640, height: 640 } },
324
+ { fps: 30 },
325
+ ]);
326
+
327
+ // camera placeholder / UI (kept the same)
246
328
  const renderCameraPlaceholder = () => (
247
329
  <View style={styles.cameraContainer}>
248
330
  {cameraError ? (
249
331
  <View style={styles.errorContainer}>
250
332
  <Icon name="error-outline" size={40} color={COLORS.error} />
251
333
  <Text style={styles.errorText}>{cameraError}</Text>
252
- <TouchableOpacity
334
+ <TouchableOpacity
253
335
  style={styles.retryButton}
254
336
  onPress={() => {
255
337
  setCameraError(null);
256
338
  setShowCamera(false);
257
- setTimeout(() => setShowCamera(true), 100);
339
+ setTimeout(() => {
340
+ if (isMounted.current) {
341
+ setShowCamera(true);
342
+ }
343
+ }, 500);
258
344
  }}
259
345
  >
260
346
  <Text style={styles.retryButtonText}>Retry</Text>
261
347
  </TouchableOpacity>
262
348
  </View>
263
349
  ) : cameraPermission === 'denied' ? (
264
- <Text style={styles.placeholderText}>
265
- Camera permission required. Please enable in settings.
266
- </Text>
350
+ <View style={styles.placeholderContainer}>
351
+ <Icon name="camera-off" size={40} color={COLORS.light} />
352
+ <Text style={styles.placeholderText}>
353
+ Camera permission required. Please enable in settings.
354
+ </Text>
355
+ </View>
267
356
  ) : cameraPermission === 'not-determined' ? (
268
- <Text style={styles.placeholderText}>
269
- Requesting camera access...
270
- </Text>
357
+ <View style={styles.placeholderContainer}>
358
+ <ActivityIndicator size="large" color={COLORS.primary} />
359
+ <Text style={styles.placeholderText}>
360
+ Requesting camera access...
361
+ </Text>
362
+ </View>
271
363
  ) : !cameraDevice ? (
272
- <Text style={styles.placeholderText}>Camera not available</Text>
364
+ <View style={styles.placeholderContainer}>
365
+ <Icon name="camera-alt" size={40} color={COLORS.light} />
366
+ <Text style={styles.placeholderText}>Camera not available</Text>
367
+ </View>
273
368
  ) : (
274
- <ActivityIndicator size="large" color={COLORS.primary} />
369
+ <View style={styles.placeholderContainer}>
370
+ <ActivityIndicator size="large" color={COLORS.primary} />
371
+ <Text style={styles.placeholderText}>Initializing camera...</Text>
372
+ </View>
275
373
  )}
276
374
  </View>
277
375
  );
278
376
 
279
- const shouldRenderCamera = showCamera &&
280
- cameraPermission === 'granted' &&
281
- cameraDevice &&
282
- !cameraError;
377
+ const shouldRenderCamera = showCamera &&
378
+ cameraPermission === 'granted' &&
379
+ cameraDevice &&
380
+ !cameraError;
381
+
382
+ // keep a global-ish small flag for the worklet to know about codeScanner state
383
+ useEffect(() => {
384
+ // make accessible inside the worklet
385
+ global.__showCodeScanner = !!showCodeScanner;
386
+ return () => {
387
+ global.__showCodeScanner = false;
388
+ };
389
+ }, [showCodeScanner]);
390
+
391
+ // When component unmounts, clear any worklet globals to avoid stale state
392
+ useEffect(() => {
393
+ return () => {
394
+ // Clear worklet globals
395
+ global.__lastProcessedTime = 0;
396
+ global.__lastBounds = null;
397
+ global.__stableCount = 0;
398
+ global.__captured = false;
399
+ global.__showCodeScanner = false;
400
+ };
401
+ }, []);
283
402
 
284
403
  return (
285
404
  <View style={styles.cameraContainer}>
@@ -288,18 +407,20 @@ const CaptureImageWithoutEdit = React.memo(
288
407
  ref={cameraRef}
289
408
  style={styles.camera}
290
409
  device={cameraDevice}
291
- isActive={showCamera && !isLoading}
410
+ isActive={showCamera && !isLoading && appState.current === 'active'}
292
411
  photo={true}
293
412
  format={format}
294
413
  codeScanner={showCodeScanner ? codeScanner : undefined}
414
+ // only attach frameProcessor when not scanning codes and camera is initialized
295
415
  frameProcessor={!showCodeScanner && cameraInitialized ? frameProcessor : undefined}
296
416
  frameProcessorFps={FRAME_PROCESSOR_FPS}
297
417
  onInitialized={handleCameraInitialized}
298
418
  onError={handleCameraError}
299
- enableZoomGesture={true}
300
- exposure={!showCodeScanner ? 0.5 : 0}
419
+ enableZoomGesture={false}
420
+ exposure={0}
301
421
  pixelFormat="yuv"
302
- preset="high"
422
+ preset="medium"
423
+ orientation="portrait"
303
424
  />
304
425
  ) : (
305
426
  renderCameraPlaceholder()
@@ -346,13 +467,12 @@ const CaptureImageWithoutEdit = React.memo(
346
467
  <Icon name="flip-camera-ios" size={28} color={COLORS.light} />
347
468
  </TouchableOpacity>
348
469
  )}
349
- </View>
470
+ </View>
350
471
  </View>
351
472
  );
352
473
  }
353
474
  );
354
475
 
355
- // Add display name for better debugging
356
476
  CaptureImageWithoutEdit.displayName = 'CaptureImageWithoutEdit';
357
477
 
358
478
  const styles = StyleSheet.create({
@@ -368,6 +488,12 @@ const styles = StyleSheet.create({
368
488
  flex: 1,
369
489
  width: '100%',
370
490
  },
491
+ placeholderContainer: {
492
+ flex: 1,
493
+ justifyContent: 'center',
494
+ alignItems: 'center',
495
+ padding: 20,
496
+ },
371
497
  errorContainer: {
372
498
  flex: 1,
373
499
  justifyContent: 'center',
@@ -394,8 +520,7 @@ const styles = StyleSheet.create({
394
520
  color: COLORS.light,
395
521
  fontSize: 16,
396
522
  textAlign: 'center',
397
- opacity: 0.8,
398
- padding: 20,
523
+ marginTop: 16,
399
524
  },
400
525
  cameraControls: {
401
526
  position: 'absolute',
@@ -454,4 +579,4 @@ const styles = StyleSheet.create({
454
579
  },
455
580
  });
456
581
 
457
- export default CaptureImageWithoutEdit;
582
+ export default CaptureImageWithoutEdit;