react-native-biometric-verifier 0.0.12 → 0.0.13

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.12",
3
+ "version": "0.0.13",
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": {
@@ -0,0 +1,136 @@
1
+ import React, { useState } from 'react';
2
+ import { View, Text, Image, StyleSheet, Platform } from 'react-native';
3
+ import Icon from 'react-native-vector-icons/MaterialIcons';
4
+ import PropTypes from 'prop-types';
5
+ import { COLORS } from '../utils/constants';
6
+
7
+ export const CCard = ({ employeeData, apiurl }) => {
8
+ const [imageError, setImageError] = useState(false);
9
+
10
+ if (!employeeData || typeof employeeData !== 'object') {
11
+ console.warn('CCard: Invalid or missing employeeData');
12
+ return null;
13
+ }
14
+
15
+ const { facename, faceid, imageurl } = employeeData;
16
+
17
+ const employeeName = facename || 'Unknown Employee';
18
+ const employeeId = faceid || 'N/A';
19
+ const imageSource =
20
+ !imageError && imageurl
21
+ ? { uri: `${apiurl}file/filedownload/photo/${imageurl}` }
22
+ : { uri: `${apiurl}file/getCommonFile/image/camera.png` };
23
+
24
+ return (
25
+ <View style={styles.card}>
26
+ {/* Employee Image */}
27
+ <View style={styles.imageWrapper}>
28
+ <Image
29
+ source={imageSource}
30
+ style={styles.image}
31
+ resizeMode="cover"
32
+ onError={() => {
33
+ console.error(`Error loading image for employee: ${employeeName}`);
34
+ setImageError(true);
35
+ }}
36
+ />
37
+ <View style={styles.imageOverlay} />
38
+ </View>
39
+
40
+ {/* Employee Info */}
41
+ <Text style={styles.name}>{employeeName}</Text>
42
+ <Text style={styles.id}>ID: {employeeId}</Text>
43
+
44
+ {/* Verified Badge */}
45
+ <View style={styles.badgeWrapper}>
46
+ <View style={styles.badge}>
47
+ <Icon name="verified-user" size={16} color={COLORS.success} />
48
+ <Text style={styles.badgeText}>Identity Verified</Text>
49
+ </View>
50
+ </View>
51
+ </View>
52
+ );
53
+ };
54
+
55
+ CCard.propTypes = {
56
+ employeeData: PropTypes.shape({
57
+ facename: PropTypes.string,
58
+ faceid: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
59
+ imageurl: PropTypes.string,
60
+ }),
61
+ };
62
+
63
+ const styles = StyleSheet.create({
64
+ card: {
65
+ alignItems: 'center',
66
+ marginVertical: 20,
67
+ width: '100%',
68
+ padding: 20,
69
+ backgroundColor: 'rgba(255, 255, 255, 0.12)',
70
+ borderRadius: 20,
71
+ shadowColor: '#000',
72
+ shadowOffset: { width: 0, height: 6 },
73
+ shadowOpacity: 0.15,
74
+ shadowRadius: 10,
75
+ elevation: 4,
76
+ borderWidth: 1,
77
+ borderColor: 'rgba(255,255,255,0.2)',
78
+ backdropFilter: 'blur(10px)', // works only in web, safe to keep
79
+ },
80
+ imageWrapper: {
81
+ width: 110,
82
+ height: 110,
83
+ borderRadius: 55,
84
+ borderWidth: 3,
85
+ borderColor: COLORS.primary,
86
+ overflow: 'hidden',
87
+ justifyContent: 'center',
88
+ alignItems: 'center',
89
+ backgroundColor: 'rgba(255,255,255,0.05)',
90
+ },
91
+ image: {
92
+ width: '100%',
93
+ height: '100%',
94
+ },
95
+ imageOverlay: {
96
+ ...StyleSheet.absoluteFillObject,
97
+ backgroundColor: 'rgba(108, 99, 255, 0.12)',
98
+ },
99
+ name: {
100
+ fontSize: 20,
101
+ fontWeight: '600',
102
+ marginTop: 15,
103
+ color: COLORS.light,
104
+ fontFamily: Platform.OS === 'ios' ? 'Helvetica Neue' : 'sans-serif-medium',
105
+ },
106
+ id: {
107
+ fontSize: 15,
108
+ color: COLORS.gray,
109
+ marginTop: 5,
110
+ fontFamily: Platform.OS === 'ios' ? 'Helvetica Neue' : 'sans-serif',
111
+ },
112
+ badgeWrapper: {
113
+ flexDirection: 'row',
114
+ marginTop: 15,
115
+ justifyContent: 'center',
116
+ flexWrap: 'wrap',
117
+ },
118
+ badge: {
119
+ flexDirection: 'row',
120
+ alignItems: 'center',
121
+ paddingHorizontal: 12,
122
+ paddingVertical: 6,
123
+ borderRadius: 20,
124
+ backgroundColor: 'rgba(0,0,0,0.25)',
125
+ borderWidth: 1,
126
+ borderColor: 'rgba(255,255,255,0.2)',
127
+ },
128
+ badgeText: {
129
+ fontSize: 13,
130
+ color: COLORS.light,
131
+ marginLeft: 6,
132
+ fontFamily: Platform.OS === 'ios' ? 'Helvetica Neue' : 'sans-serif',
133
+ },
134
+ });
135
+
136
+ export default CCard;
@@ -0,0 +1,457 @@
1
+ import React, { useRef, useEffect, useState, useCallback } from 'react';
2
+ import {
3
+ View,
4
+ TouchableOpacity,
5
+ Text,
6
+ StyleSheet,
7
+ ActivityIndicator,
8
+ Platform,
9
+ } from 'react-native';
10
+ import Icon from 'react-native-vector-icons/MaterialIcons';
11
+ import {
12
+ Camera,
13
+ getCameraDevice,
14
+ useCodeScanner,
15
+ useCameraFormat,
16
+ useFrameProcessor,
17
+ CameraRuntimeError,
18
+ } from 'react-native-vision-camera';
19
+ import { Worklets } from 'react-native-worklets-core';
20
+ import { useFaceDetector } from 'react-native-vision-camera-face-detector';
21
+ import { COLORS } from "../utils/constants";
22
+
23
+ // 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
28
+
29
+ const CaptureImageWithoutEdit = React.memo(
30
+ ({
31
+ cameraType = 'front',
32
+ onCapture,
33
+ onToggleCamera,
34
+ showCodeScanner = false,
35
+ isLoading = false,
36
+ currentStep = '',
37
+ }) => {
38
+ const cameraRef = useRef(null);
39
+ const [cameraDevice, setCameraDevice] = useState(null);
40
+ const [cameraPermission, setCameraPermission] = useState('not-determined');
41
+ const [showCamera, setShowCamera] = useState(false);
42
+ const [cameraInitialized, setCameraInitialized] = useState(false);
43
+
44
+ // Face detection states
45
+ const [faces, setFaces] = useState([]);
46
+ const [singleFaceDetected, setSingleFaceDetected] = useState(false);
47
+ const [cameraError, setCameraError] = useState(null);
48
+
49
+ // Stability tracking refs
50
+ const stableCounter = useRef(0);
51
+ const lastBounds = useRef(null);
52
+ const captured = useRef(false);
53
+ const faceDetectionTimeout = useRef(null);
54
+ const processingFrame = useRef(false); // To prevent concurrent frame processing
55
+
56
+ const faceDetectionOptions = {
57
+ performanceMode: 'accurate',
58
+ landmarkMode: 'all',
59
+ contourMode: 'all',
60
+ classificationMode: 'all',
61
+ minFaceSize: MIN_FACE_SIZE,
62
+ };
63
+
64
+ const { detectFaces } = useFaceDetector(faceDetectionOptions);
65
+
66
+ const codeScanner = useCodeScanner({
67
+ codeTypes: ['qr', 'ean-13', 'code-128', 'code-39', 'code-93'],
68
+ onCodeScanned: (codes) => {
69
+ if (showCodeScanner && codes && codes[0]?.value && !isLoading) {
70
+ console.log('🔍 QR Code scanned:', codes[0].value);
71
+ onCapture(codes[0].value);
72
+ }
73
+ },
74
+ });
75
+
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;
85
+
86
+ if (lastBounds.current) {
87
+ const dx = Math.abs(bounds.x - lastBounds.current.x);
88
+ const dy = Math.abs(bounds.y - lastBounds.current.y);
89
+
90
+ // small movement = stable
91
+ if (dx < FACE_MOVEMENT_THRESHOLD && dy < FACE_MOVEMENT_THRESHOLD) {
92
+ stableCounter.current += 1;
93
+ } else {
94
+ stableCounter.current = 0;
95
+ }
96
+ }
97
+
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
+ })();
130
+ }
131
+ } 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
+ }
142
+ }
143
+
144
+ processingFrame.current = false;
145
+ });
146
+
147
+ // Frame processor runs on UI thread
148
+ const frameProcessor = useFrameProcessor((frame) => {
149
+ 'worklet';
150
+ if (showCodeScanner || captured.current || processingFrame.current) return;
151
+
152
+ processingFrame.current = true;
153
+ 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
159
+ } else {
160
+ onFacesDetected(detected); // send results (could be empty or multiple)
161
+ }
162
+ } catch (error) {
163
+ console.error('Frame processor error:', error);
164
+ processingFrame.current = false;
165
+ }
166
+ }, [detectFaces, showCodeScanner]);
167
+
168
+ // Initialize camera
169
+ useEffect(() => {
170
+ let isMounted = true;
171
+
172
+ const initializeCamera = async () => {
173
+ try {
174
+ if (!isMounted) return;
175
+
176
+ const permission = await Camera.requestCameraPermission();
177
+ setCameraPermission(permission);
178
+
179
+ if (permission === 'granted') {
180
+ const devices = await Camera.getAvailableCameraDevices();
181
+ const device = getCameraDevice(devices, cameraType);
182
+
183
+ if (device) {
184
+ setCameraDevice(device);
185
+ setShowCamera(true);
186
+ } else {
187
+ console.error('❌ No camera device found for type:', cameraType);
188
+ setCameraError('Camera not available on this device');
189
+ }
190
+ } else {
191
+ setCameraError('Camera permission denied');
192
+ }
193
+ } catch (error) {
194
+ console.error('❌ Camera initialization failed:', error);
195
+ if (isMounted) {
196
+ setCameraError('Failed to initialize camera');
197
+ }
198
+ }
199
+ };
200
+
201
+ initializeCamera();
202
+
203
+ return () => {
204
+ isMounted = false;
205
+ setShowCamera(false);
206
+ // Clean up timeout on unmount
207
+ if (faceDetectionTimeout.current) {
208
+ clearTimeout(faceDetectionTimeout.current);
209
+ }
210
+ };
211
+ }, [cameraType]);
212
+
213
+ const format = useCameraFormat(cameraDevice, [
214
+ { photoResolution: { width: 640, height: 640 } },
215
+ ]);
216
+
217
+ const handleCameraError = useCallback((error) => {
218
+ console.error('Camera error:', error);
219
+ let errorMessage = 'Camera error occurred';
220
+
221
+ if (error instanceof CameraRuntimeError) {
222
+ switch (error.code) {
223
+ case 'session/configuration-failed':
224
+ errorMessage = 'Camera configuration failed';
225
+ break;
226
+ case 'device/not-available':
227
+ errorMessage = 'Camera not available';
228
+ break;
229
+ case 'permission/microphone-not-granted':
230
+ errorMessage = 'Microphone permission required';
231
+ break;
232
+ default:
233
+ errorMessage = `Camera error: ${error.message}`;
234
+ }
235
+ }
236
+
237
+ setCameraError(errorMessage);
238
+ }, []);
239
+
240
+ const handleCameraInitialized = useCallback(() => {
241
+ console.log('Camera initialized successfully');
242
+ setCameraInitialized(true);
243
+ setCameraError(null);
244
+ }, []);
245
+
246
+ const renderCameraPlaceholder = () => (
247
+ <View style={styles.cameraContainer}>
248
+ {cameraError ? (
249
+ <View style={styles.errorContainer}>
250
+ <Icon name="error-outline" size={40} color={COLORS.error} />
251
+ <Text style={styles.errorText}>{cameraError}</Text>
252
+ <TouchableOpacity
253
+ style={styles.retryButton}
254
+ onPress={() => {
255
+ setCameraError(null);
256
+ setShowCamera(false);
257
+ setTimeout(() => setShowCamera(true), 100);
258
+ }}
259
+ >
260
+ <Text style={styles.retryButtonText}>Retry</Text>
261
+ </TouchableOpacity>
262
+ </View>
263
+ ) : cameraPermission === 'denied' ? (
264
+ <Text style={styles.placeholderText}>
265
+ Camera permission required. Please enable in settings.
266
+ </Text>
267
+ ) : cameraPermission === 'not-determined' ? (
268
+ <Text style={styles.placeholderText}>
269
+ Requesting camera access...
270
+ </Text>
271
+ ) : !cameraDevice ? (
272
+ <Text style={styles.placeholderText}>Camera not available</Text>
273
+ ) : (
274
+ <ActivityIndicator size="large" color={COLORS.primary} />
275
+ )}
276
+ </View>
277
+ );
278
+
279
+ const shouldRenderCamera = showCamera &&
280
+ cameraPermission === 'granted' &&
281
+ cameraDevice &&
282
+ !cameraError;
283
+
284
+ return (
285
+ <View style={styles.cameraContainer}>
286
+ {shouldRenderCamera ? (
287
+ <Camera
288
+ ref={cameraRef}
289
+ style={styles.camera}
290
+ device={cameraDevice}
291
+ isActive={showCamera && !isLoading}
292
+ photo={true}
293
+ format={format}
294
+ codeScanner={showCodeScanner ? codeScanner : undefined}
295
+ frameProcessor={!showCodeScanner && cameraInitialized ? frameProcessor : undefined}
296
+ frameProcessorFps={FRAME_PROCESSOR_FPS}
297
+ onInitialized={handleCameraInitialized}
298
+ onError={handleCameraError}
299
+ enableZoomGesture={true}
300
+ exposure={!showCodeScanner ? 0.5 : 0}
301
+ pixelFormat="yuv"
302
+ preset="high"
303
+ />
304
+ ) : (
305
+ renderCameraPlaceholder()
306
+ )}
307
+
308
+ {/* Face detection status text */}
309
+ {!showCodeScanner && shouldRenderCamera && (
310
+ <View style={styles.faceDetectionStatus}>
311
+ {faces.length === 0 && (
312
+ <Text style={styles.faceDetectionText}>
313
+ Position your face in the frame
314
+ </Text>
315
+ )}
316
+ {faces.length > 1 && (
317
+ <Text style={[styles.faceDetectionText, styles.multipleFacesText]}>
318
+ Multiple faces detected. Please ensure only one person is in the frame.
319
+ </Text>
320
+ )}
321
+ {singleFaceDetected && !captured.current && (
322
+ <Text style={[styles.faceDetectionText, styles.singleFaceText]}>
323
+ Face detected. Hold still...
324
+ </Text>
325
+ )}
326
+ {captured.current && (
327
+ <Text style={[styles.faceDetectionText, styles.capturingText]}>
328
+ Capturing...
329
+ </Text>
330
+ )}
331
+ </View>
332
+ )}
333
+
334
+ {/* Controls */}
335
+ <View style={styles.cameraControls}>
336
+ {onToggleCamera && currentStep === 'Identity Verification' && shouldRenderCamera && (
337
+ <TouchableOpacity
338
+ style={[
339
+ styles.flipButton,
340
+ (isLoading || captured.current) && styles.flipButtonDisabled,
341
+ ]}
342
+ onPress={onToggleCamera}
343
+ disabled={isLoading || captured.current}
344
+ accessibilityLabel="Flip camera"
345
+ >
346
+ <Icon name="flip-camera-ios" size={28} color={COLORS.light} />
347
+ </TouchableOpacity>
348
+ )}
349
+ </View>
350
+ </View>
351
+ );
352
+ }
353
+ );
354
+
355
+ // Add display name for better debugging
356
+ CaptureImageWithoutEdit.displayName = 'CaptureImageWithoutEdit';
357
+
358
+ const styles = StyleSheet.create({
359
+ cameraContainer: {
360
+ flex: 1,
361
+ width: '100%',
362
+ borderRadius: 12,
363
+ overflow: 'hidden',
364
+ backgroundColor: COLORS.dark,
365
+ minHeight: 300,
366
+ },
367
+ camera: {
368
+ flex: 1,
369
+ width: '100%',
370
+ },
371
+ errorContainer: {
372
+ flex: 1,
373
+ justifyContent: 'center',
374
+ alignItems: 'center',
375
+ padding: 20,
376
+ },
377
+ errorText: {
378
+ color: COLORS.error,
379
+ fontSize: 16,
380
+ textAlign: 'center',
381
+ marginVertical: 16,
382
+ },
383
+ retryButton: {
384
+ backgroundColor: COLORS.primary,
385
+ paddingHorizontal: 20,
386
+ paddingVertical: 10,
387
+ borderRadius: 8,
388
+ },
389
+ retryButtonText: {
390
+ color: COLORS.light,
391
+ fontWeight: 'bold',
392
+ },
393
+ placeholderText: {
394
+ color: COLORS.light,
395
+ fontSize: 16,
396
+ textAlign: 'center',
397
+ opacity: 0.8,
398
+ padding: 20,
399
+ },
400
+ cameraControls: {
401
+ position: 'absolute',
402
+ bottom: 30,
403
+ left: 0,
404
+ right: 0,
405
+ flexDirection: 'row',
406
+ justifyContent: 'center',
407
+ alignItems: 'center',
408
+ },
409
+ flipButton: {
410
+ position: 'absolute',
411
+ right: 30,
412
+ bottom: 40,
413
+ width: 55,
414
+ height: 55,
415
+ borderRadius: 27.5,
416
+ backgroundColor: 'rgba(0,0,0,0.5)',
417
+ justifyContent: 'center',
418
+ alignItems: 'center',
419
+ shadowColor: '#000',
420
+ shadowOpacity: 0.3,
421
+ shadowOffset: { width: 0, height: 2 },
422
+ shadowRadius: 5,
423
+ elevation: 6,
424
+ },
425
+ flipButtonDisabled: {
426
+ opacity: 0.5,
427
+ },
428
+ faceDetectionStatus: {
429
+ position: 'absolute',
430
+ top: 40,
431
+ left: 0,
432
+ right: 0,
433
+ alignItems: 'center',
434
+ },
435
+ faceDetectionText: {
436
+ color: 'white',
437
+ fontSize: 16,
438
+ fontWeight: 'bold',
439
+ backgroundColor: 'rgba(0,0,0,0.7)',
440
+ paddingHorizontal: 16,
441
+ paddingVertical: 8,
442
+ borderRadius: 8,
443
+ textAlign: 'center',
444
+ marginHorizontal: 20,
445
+ },
446
+ multipleFacesText: {
447
+ backgroundColor: 'rgba(255,0,0,0.7)',
448
+ },
449
+ singleFaceText: {
450
+ backgroundColor: 'rgba(0,255,0,0.7)',
451
+ },
452
+ capturingText: {
453
+ backgroundColor: 'rgba(0,100,255,0.7)',
454
+ },
455
+ });
456
+
457
+ export default CaptureImageWithoutEdit;
@@ -1,7 +1,6 @@
1
1
  import React from 'react';
2
2
  import { View, Text, Animated, StyleSheet } from 'react-native';
3
3
  import { COLORS } from '../utils/constants';
4
- import { styles } from './styles';
5
4
 
6
5
  export const CountdownTimer = ({ duration, currentTime }) => {
7
6
  const progress = React.useRef(new Animated.Value(1)).current;
@@ -21,22 +20,54 @@ export const CountdownTimer = ({ duration, currentTime }) => {
21
20
  });
22
21
 
23
22
  return (
24
- <View style={styles.timerContainer}>
25
- <View style={styles.timerCircle}>
26
- <Animated.View style={styles.timerProgressContainer}>
23
+ <View style={{
24
+ marginTop: 20,
25
+ alignItems: 'center',
26
+ }}>
27
+ <View style={{
28
+ width: 80,
29
+ height: 80,
30
+ borderRadius: 40,
31
+ backgroundColor: 'rgba(255, 255, 255, 0.2)',
32
+ justifyContent: 'center',
33
+ alignItems: 'center',
34
+ position: 'relative',
35
+ }}>
36
+ <Animated.View style={{
37
+ position: 'absolute',
38
+ width: '100%',
39
+ height: '100%',
40
+ }}>
27
41
  <Animated.View
28
- style={[
29
- styles.timerProgress,
42
+ style={
30
43
  {
31
44
  strokeDashoffset,
32
45
  transform: [{ rotate: '-90deg' }],
46
+ width: 76,
47
+ height: 76,
48
+ borderRadius: 38,
49
+ borderWidth: 3,
50
+ borderColor: COLORS.light,
51
+ borderLeftColor: 'transparent',
52
+ borderBottomColor: 'transparent',
33
53
  }
34
- ]}
54
+ }
35
55
  />
36
56
  </Animated.View>
37
- <Text style={styles.timerText}>{currentTime}s</Text>
57
+ <Text style={{
58
+ fontSize: 20,
59
+ fontWeight: '700',
60
+ color: COLORS.light,
61
+ fontFamily: Platform.OS === 'ios' ? 'Helvetica Neue' : 'sans-serif-medium',
62
+ }}>{currentTime}s</Text>
38
63
  </View>
39
- <Text style={styles.timerLabel}>Remaining</Text>
64
+ <Text style={{
65
+ fontSize: 14,
66
+ color: COLORS.light,
67
+ marginTop: 5,
68
+ opacity: 0.8,
69
+ fontFamily: Platform.OS === 'ios' ? 'Helvetica Neue' : 'sans-serif',
70
+ }}>Remaining</Text>
40
71
  </View>
41
72
  );
42
73
  };