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 +1 -1
- package/src/components/CCard.js +136 -0
- package/src/components/CaptureImageWithoutEdit.js +457 -0
- package/src/components/CountdownTimer.js +40 -9
- package/src/components/Loader.js +159 -97
- package/src/components/Notification.js +43 -15
- package/src/components/StepIcon.js +71 -0
- package/src/components/StepIndicator.js +91 -0
- package/src/hooks/useNotifyMessage.js +28 -6
- package/src/index.js +329 -78
- package/src/utils/constants.js +15 -0
- package/src/components/EmployeeCard.js +0 -61
- package/src/components/styles.js +0 -200
package/package.json
CHANGED
|
@@ -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={
|
|
25
|
-
|
|
26
|
-
|
|
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={
|
|
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={
|
|
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
|
};
|