react-native-biometric-verifier 0.0.1
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/README.md +20 -0
- package/package.json +32 -0
- package/src/Asset/Icons/Female.png +0 -0
- package/src/Asset/Icons/FemaleI(1).png +0 -0
- package/src/Asset/Icons/Male(2).png +0 -0
- package/src/Asset/Icons/arrowleft.png +0 -0
- package/src/Asset/Icons/barcode2.png +0 -0
- package/src/Asset/Icons/calendar.png +0 -0
- package/src/Asset/Icons/downarrow.png +0 -0
- package/src/Asset/Icons/male.png +0 -0
- package/src/Asset/Icons/stethoscope.jpg +0 -0
- package/src/Asset/Icons/user.png +0 -0
- package/src/Asset/Icons/youtube.png +0 -0
- package/src/Asset/gif/Pulse.gif +0 -0
- package/src/Asset/gif/Search_blue.gif +0 -0
- package/src/Asset/gif/hb.gif +0 -0
- package/src/Asset/gif/heartpulse.gif +0 -0
- package/src/Asset/gif/lab_1.gif +0 -0
- package/src/Asset/gif/loader.gif +0 -0
- package/src/Asset/gif/overwrite.gif +0 -0
- package/src/Asset/gif/pharmacist.gif +0 -0
- package/src/Asset/images/AdamsNeilson.jpg +0 -0
- package/src/Asset/images/AyurvedicHospital.jpg +0 -0
- package/src/Asset/images/DevamathaLogo.jpg +0 -0
- package/src/Asset/images/IMA.png +0 -0
- package/src/Asset/images/amalainstitute.png +0 -0
- package/src/Asset/images/amalainstituteN.png +0 -0
- package/src/Asset/images/amalainstituteX.png +0 -0
- package/src/Asset/images/amalalogo.jpg +0 -0
- package/src/Asset/images/amalalogoN.jpg +0 -0
- package/src/Asset/images/arrow-collapse.png +0 -0
- package/src/Asset/images/arrow-expand.png +0 -0
- package/src/Asset/images/audio.png +0 -0
- package/src/Asset/images/camera.png +0 -0
- package/src/Asset/images/camera_red.png +0 -0
- package/src/Asset/images/companyaddress_transparent.png +0 -0
- package/src/Asset/images/companyname_transparent.png +0 -0
- package/src/Asset/images/download.png +0 -0
- package/src/Asset/images/jes.png +0 -0
- package/src/Asset/images/lefteye.jpg +0 -0
- package/src/Asset/images/mundakayamlogo.png +0 -0
- package/src/Asset/images/nirmalanabh.jpeg +0 -0
- package/src/Asset/images/pregnantlady.png +0 -0
- package/src/Asset/images/prescriptioneye.png +0 -0
- package/src/Asset/images/rblogo.png +0 -0
- package/src/Asset/images/righteye.jpg +0 -0
- package/src/Asset/images/rtlogo.png +0 -0
- package/src/Asset/images/video_red.png +0 -0
- package/src/Asset/images/vimaljyothi.png +0 -0
- package/src/components/CountdownTimer.js +42 -0
- package/src/components/EmployeeCard.js +61 -0
- package/src/components/Notification.js +63 -0
- package/src/components/StateIndicator.js +112 -0
- package/src/components/styles.js +182 -0
- package/src/hooks/useCountdown.js +72 -0
- package/src/hooks/useGeolocation.js +72 -0
- package/src/hooks/useImageProcessing.js +79 -0
- package/src/hooks/useNotifyMessage.js +131 -0
- package/src/index.js +283 -0
- package/src/utils/Global.js +6 -0
- package/src/utils/NetworkServiceCall.js +35 -0
- package/src/utils/constants.js +69 -0
- package/src/utils/distanceCalculator.js +25 -0
- package/src/utils/logger.js +7 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { useRef, useState, useEffect, useCallback } from 'react';
|
|
2
|
+
import { COUNTDOWN_DURATION } from '../utils/constants';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Custom hook for a countdown timer.
|
|
6
|
+
*
|
|
7
|
+
* @param {Function} onExpire - Callback fired when countdown reaches zero.
|
|
8
|
+
* @returns {Object} countdown, startCountdown, resetCountdown
|
|
9
|
+
*/
|
|
10
|
+
export const useCountdown = (onExpire) => {
|
|
11
|
+
const [countdown, setCountdown] = useState(COUNTDOWN_DURATION);
|
|
12
|
+
const timerRef = useRef(null);
|
|
13
|
+
const countdownRef = useRef(COUNTDOWN_DURATION);
|
|
14
|
+
|
|
15
|
+
// Start or restart the countdown
|
|
16
|
+
const startCountdown = useCallback(() => {
|
|
17
|
+
try {
|
|
18
|
+
// Reset countdown
|
|
19
|
+
countdownRef.current = COUNTDOWN_DURATION;
|
|
20
|
+
setCountdown(COUNTDOWN_DURATION);
|
|
21
|
+
|
|
22
|
+
// Clear any existing timer
|
|
23
|
+
if (timerRef.current) clearInterval(timerRef.current);
|
|
24
|
+
|
|
25
|
+
// Start new timer
|
|
26
|
+
timerRef.current = setInterval(() => {
|
|
27
|
+
countdownRef.current -= 1;
|
|
28
|
+
|
|
29
|
+
if (countdownRef.current <= 0) {
|
|
30
|
+
clearInterval(timerRef.current);
|
|
31
|
+
timerRef.current = null;
|
|
32
|
+
|
|
33
|
+
// Safely call onExpire if it's a function
|
|
34
|
+
if (typeof onExpire === 'function') {
|
|
35
|
+
onExpire();
|
|
36
|
+
}
|
|
37
|
+
} else {
|
|
38
|
+
setCountdown(countdownRef.current);
|
|
39
|
+
}
|
|
40
|
+
}, 1000);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.error('Error in startCountdown:', error);
|
|
43
|
+
}
|
|
44
|
+
}, [onExpire]);
|
|
45
|
+
|
|
46
|
+
// Reset countdown to initial duration
|
|
47
|
+
const resetCountdown = useCallback(() => {
|
|
48
|
+
try {
|
|
49
|
+
countdownRef.current = COUNTDOWN_DURATION;
|
|
50
|
+
setCountdown(COUNTDOWN_DURATION);
|
|
51
|
+
|
|
52
|
+
if (timerRef.current) {
|
|
53
|
+
clearInterval(timerRef.current);
|
|
54
|
+
timerRef.current = null;
|
|
55
|
+
}
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.error('Error in resetCountdown:', error);
|
|
58
|
+
}
|
|
59
|
+
}, []);
|
|
60
|
+
|
|
61
|
+
// Clean up on unmount
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
return () => {
|
|
64
|
+
if (timerRef.current) {
|
|
65
|
+
clearInterval(timerRef.current);
|
|
66
|
+
timerRef.current = null;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}, []);
|
|
70
|
+
|
|
71
|
+
return { countdown, startCountdown, resetCountdown };
|
|
72
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
import { Platform, PermissionsAndroid } from 'react-native';
|
|
3
|
+
import Geolocation from 'react-native-geolocation-service';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Custom hook for requesting location permission and fetching current location.
|
|
7
|
+
*
|
|
8
|
+
* @param {Function} notifyMessage - Callback to show notifications (message, type).
|
|
9
|
+
* @returns {Object} { requestLocationPermission, getCurrentLocation }
|
|
10
|
+
*/
|
|
11
|
+
export const useGeolocation = (notifyMessage) => {
|
|
12
|
+
/**
|
|
13
|
+
* Requests location permission (Android only).
|
|
14
|
+
* @returns {Promise<boolean>} - True if permission granted, else false.
|
|
15
|
+
*/
|
|
16
|
+
const requestLocationPermission = useCallback(async () => {
|
|
17
|
+
if (Platform.OS !== 'android') return true;
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const granted = await PermissionsAndroid.request(
|
|
21
|
+
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
|
|
22
|
+
{
|
|
23
|
+
title: 'Location Permission Required',
|
|
24
|
+
message: 'This app needs your location to verify QR code scans.',
|
|
25
|
+
buttonPositive: 'OK',
|
|
26
|
+
buttonNegative: 'Cancel',
|
|
27
|
+
}
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const hasPermission = granted === PermissionsAndroid.RESULTS.GRANTED;
|
|
31
|
+
|
|
32
|
+
if (!hasPermission) {
|
|
33
|
+
notifyMessage?.('Cannot get location without permission.', 'error');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return hasPermission;
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.error('Location permission error:', error);
|
|
39
|
+
notifyMessage?.('Location permission error.', 'error');
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}, [notifyMessage]);
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Fetches the current location of the device.
|
|
46
|
+
* @returns {Promise<GeolocationCoordinates>}
|
|
47
|
+
*/
|
|
48
|
+
const getCurrentLocation = useCallback(
|
|
49
|
+
() =>
|
|
50
|
+
new Promise((resolve, reject) => {
|
|
51
|
+
Geolocation.getCurrentPosition(
|
|
52
|
+
(position) => {
|
|
53
|
+
resolve(position.coords);
|
|
54
|
+
},
|
|
55
|
+
(error) => {
|
|
56
|
+
console.error('Error getting location:', error);
|
|
57
|
+
notifyMessage?.('Unable to fetch current location.', 'error');
|
|
58
|
+
reject(error);
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
enableHighAccuracy: true,
|
|
62
|
+
timeout: 15000,
|
|
63
|
+
maximumAge: 10000,
|
|
64
|
+
distanceFilter: 1,
|
|
65
|
+
}
|
|
66
|
+
);
|
|
67
|
+
}),
|
|
68
|
+
[notifyMessage]
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
return { requestLocationPermission, getCurrentLocation };
|
|
72
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
import ImageResizer from 'react-native-image-resizer';
|
|
3
|
+
import RNFS from 'react-native-fs';
|
|
4
|
+
import { IMAGE_RESIZE } from '../utils/constants';
|
|
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
|
+
// Validate input
|
|
22
|
+
if (!uri || typeof uri !== 'string') {
|
|
23
|
+
throw new Error('Invalid image URI provided.');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Optional: Check file info (skip if unable to fetch)
|
|
27
|
+
try {
|
|
28
|
+
const fileInfo = await RNFS.stat(uri);
|
|
29
|
+
if (!fileInfo) {
|
|
30
|
+
console.warn('Unable to fetch file info. Proceeding with resize.');
|
|
31
|
+
}
|
|
32
|
+
} catch {
|
|
33
|
+
console.warn('Could not check file info; skipping original size check.');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Resize image
|
|
37
|
+
let resizedImage;
|
|
38
|
+
try {
|
|
39
|
+
resizedImage = await ImageResizer.createResizedImage(
|
|
40
|
+
uri,
|
|
41
|
+
IMAGE_RESIZE.width,
|
|
42
|
+
IMAGE_RESIZE.height,
|
|
43
|
+
IMAGE_RESIZE.format, // 'JPEG' or 'PNG'
|
|
44
|
+
IMAGE_RESIZE.quality, // e.g., 80
|
|
45
|
+
0, // Rotation
|
|
46
|
+
undefined, // Output path (let library choose)
|
|
47
|
+
false // Keep EXIF metadata
|
|
48
|
+
);
|
|
49
|
+
} catch (resizeError) {
|
|
50
|
+
throw new Error(`Failed to resize image: ${resizeError.message}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!resizedImage?.uri) {
|
|
54
|
+
throw new Error('Image resizing returned an invalid result.');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Convert resized image to Base64
|
|
58
|
+
let base64Data;
|
|
59
|
+
try {
|
|
60
|
+
base64Data = await RNFS.readFile(resizedImage.uri, 'base64');
|
|
61
|
+
} catch (readError) {
|
|
62
|
+
throw new Error(`Failed to read resized image file: ${readError.message}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Optionally prepend MIME type
|
|
66
|
+
if (includeMimeType) {
|
|
67
|
+
const mimeType = IMAGE_RESIZE.format.toLowerCase() === 'png' ? 'image/png' : 'image/jpeg';
|
|
68
|
+
base64Data = `data:${mimeType};base64,${base64Data}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return base64Data;
|
|
72
|
+
} catch (error) {
|
|
73
|
+
console.error('Error in convertImageToBase64:', error.message || error);
|
|
74
|
+
throw error; // Rethrow for caller handling
|
|
75
|
+
}
|
|
76
|
+
}, []);
|
|
77
|
+
|
|
78
|
+
return { convertImageToBase64 };
|
|
79
|
+
};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { useState, useRef, useCallback } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Animated,
|
|
4
|
+
Vibration,
|
|
5
|
+
DeviceEventEmitter,
|
|
6
|
+
Platform,
|
|
7
|
+
ToastAndroid,
|
|
8
|
+
Alert,
|
|
9
|
+
} from 'react-native';
|
|
10
|
+
import { COLORS } from '../utils/constants';
|
|
11
|
+
|
|
12
|
+
export const useNotifyMessage = () => {
|
|
13
|
+
const [notification, setNotification] = useState({
|
|
14
|
+
visible: false,
|
|
15
|
+
message: '',
|
|
16
|
+
type: 'info',
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const fadeAnim = useRef(new Animated.Value(0)).current;
|
|
20
|
+
const slideAnim = useRef(new Animated.Value(-20)).current;
|
|
21
|
+
|
|
22
|
+
// Styles for notification container based on type
|
|
23
|
+
const getNotificationStyle = useCallback((type) => {
|
|
24
|
+
let backgroundColor = COLORS.info;
|
|
25
|
+
if (type === 'success') backgroundColor = COLORS.success;
|
|
26
|
+
if (type === 'error') backgroundColor = COLORS.error;
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
position: 'absolute',
|
|
30
|
+
top: 20,
|
|
31
|
+
alignSelf: 'center',
|
|
32
|
+
paddingHorizontal: 16,
|
|
33
|
+
paddingVertical: 12,
|
|
34
|
+
backgroundColor,
|
|
35
|
+
borderRadius: 10,
|
|
36
|
+
shadowColor: '#000',
|
|
37
|
+
shadowOffset: { width: 0, height: 2 },
|
|
38
|
+
shadowOpacity: 0.25,
|
|
39
|
+
shadowRadius: 3.84,
|
|
40
|
+
elevation: 5,
|
|
41
|
+
minWidth: 200,
|
|
42
|
+
maxWidth: '90%',
|
|
43
|
+
};
|
|
44
|
+
}, []);
|
|
45
|
+
|
|
46
|
+
// Styles for notification text
|
|
47
|
+
const notificationTextStyle = useCallback(() => ({
|
|
48
|
+
color: '#fff',
|
|
49
|
+
fontSize: 16,
|
|
50
|
+
fontWeight: '600',
|
|
51
|
+
textAlign: 'center',
|
|
52
|
+
}), []);
|
|
53
|
+
|
|
54
|
+
const showNotification = useCallback((message, type = 'info') => {
|
|
55
|
+
setNotification({ visible: true, message, type });
|
|
56
|
+
|
|
57
|
+
fadeAnim.setValue(0);
|
|
58
|
+
slideAnim.setValue(-20);
|
|
59
|
+
|
|
60
|
+
Animated.parallel([
|
|
61
|
+
Animated.timing(fadeAnim, {
|
|
62
|
+
toValue: 1,
|
|
63
|
+
duration: 300,
|
|
64
|
+
useNativeDriver: false,
|
|
65
|
+
}),
|
|
66
|
+
Animated.timing(slideAnim, {
|
|
67
|
+
toValue: 0,
|
|
68
|
+
duration: 300,
|
|
69
|
+
useNativeDriver: false,
|
|
70
|
+
}),
|
|
71
|
+
]).start(() => {
|
|
72
|
+
setTimeout(() => {
|
|
73
|
+
Animated.parallel([
|
|
74
|
+
Animated.timing(fadeAnim, {
|
|
75
|
+
toValue: 0,
|
|
76
|
+
duration: 300,
|
|
77
|
+
useNativeDriver: false,
|
|
78
|
+
}),
|
|
79
|
+
Animated.timing(slideAnim, {
|
|
80
|
+
toValue: 20,
|
|
81
|
+
duration: 300,
|
|
82
|
+
useNativeDriver: false,
|
|
83
|
+
}),
|
|
84
|
+
]).start(() =>
|
|
85
|
+
setNotification({ visible: false, message: '', type: 'info' })
|
|
86
|
+
);
|
|
87
|
+
}, 3000);
|
|
88
|
+
});
|
|
89
|
+
}, [fadeAnim, slideAnim]);
|
|
90
|
+
|
|
91
|
+
const notifyMessage = useCallback(
|
|
92
|
+
(msg, type = 'info') => {
|
|
93
|
+
try {
|
|
94
|
+
if (type === 'error' || type === 'success') {
|
|
95
|
+
Vibration.vibrate(100);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
showNotification(msg, type);
|
|
99
|
+
|
|
100
|
+
DeviceEventEmitter.emit('event.testToast', {
|
|
101
|
+
displaytext: msg,
|
|
102
|
+
datavalue: type,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
if (Platform.OS === 'android') {
|
|
106
|
+
ToastAndroid.showWithGravityAndOffset(
|
|
107
|
+
msg,
|
|
108
|
+
ToastAndroid.LONG,
|
|
109
|
+
ToastAndroid.BOTTOM,
|
|
110
|
+
0,
|
|
111
|
+
80
|
|
112
|
+
);
|
|
113
|
+
} else {
|
|
114
|
+
Alert.alert(type.toUpperCase(), msg);
|
|
115
|
+
}
|
|
116
|
+
} catch (error) {
|
|
117
|
+
console.error('Error in notifyMessage:', error);
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
[showNotification]
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
notification,
|
|
125
|
+
fadeAnim,
|
|
126
|
+
slideAnim,
|
|
127
|
+
notifyMessage,
|
|
128
|
+
getNotificationStyle,
|
|
129
|
+
notificationTextStyle,
|
|
130
|
+
};
|
|
131
|
+
};
|
package/src/index.js
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
TouchableOpacity,
|
|
5
|
+
Text,
|
|
6
|
+
Modal,
|
|
7
|
+
Dimensions,
|
|
8
|
+
} from 'react-native';
|
|
9
|
+
import Icon from 'react-native-vector-icons/MaterialIcons';
|
|
10
|
+
|
|
11
|
+
import { useCountdown } from './hooks/useCountdown';
|
|
12
|
+
import { useGeolocation } from './hooks/useGeolocation';
|
|
13
|
+
import { useImageProcessing } from './hooks/useImageProcessing';
|
|
14
|
+
import { useNotifyMessage } from './hooks/useNotifyMessage';
|
|
15
|
+
import { createLogger } from './utils/logger';
|
|
16
|
+
import { getDistanceInMeters } from './utils/distanceCalculator';
|
|
17
|
+
import {
|
|
18
|
+
ANIMATION_STATES,
|
|
19
|
+
COLORS,
|
|
20
|
+
COUNTDOWN_DURATION,
|
|
21
|
+
MAX_DISTANCE_METERS,
|
|
22
|
+
RECOGNIZE_URL,
|
|
23
|
+
} from './utils/constants';
|
|
24
|
+
|
|
25
|
+
import { CountdownTimer } from './components/CountdownTimer';
|
|
26
|
+
import { EmployeeCard } from './components/EmployeeCard';
|
|
27
|
+
import { Notification } from './components/Notification';
|
|
28
|
+
import { StateIndicator } from './components/StateIndicator';
|
|
29
|
+
import { styles } from './components/styles';
|
|
30
|
+
import { useNavigation } from '@react-navigation/native';
|
|
31
|
+
import networkServiceCall from './utils/NetworkServiceCall';
|
|
32
|
+
|
|
33
|
+
const BiometricVerificationModal = React.memo(({ data, callback }) => {
|
|
34
|
+
const navigation = useNavigation();
|
|
35
|
+
|
|
36
|
+
const { countdown, startCountdown, resetCountdown } = useCountdown();
|
|
37
|
+
const { requestLocationPermission, getCurrentLocation } = useGeolocation();
|
|
38
|
+
const { convertImageToBase64 } = useImageProcessing();
|
|
39
|
+
const { notification, fadeAnim, slideAnim, notifyMessage } = useNotifyMessage();
|
|
40
|
+
|
|
41
|
+
const [state, setState] = useState({
|
|
42
|
+
isLoading: false,
|
|
43
|
+
currentStep: 'Start',
|
|
44
|
+
employeeData: null,
|
|
45
|
+
modalVisible: false,
|
|
46
|
+
animationState: ANIMATION_STATES.FACE_SCAN,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const dataRef = useRef(data);
|
|
50
|
+
const mountedRef = useRef(true);
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
return () => {
|
|
54
|
+
mountedRef.current = false;
|
|
55
|
+
};
|
|
56
|
+
}, []);
|
|
57
|
+
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
dataRef.current = data;
|
|
60
|
+
}, [data]);
|
|
61
|
+
|
|
62
|
+
const updateState = useCallback((newState) => {
|
|
63
|
+
if (mountedRef.current) {
|
|
64
|
+
setState(prev => ({ ...prev, ...newState }));
|
|
65
|
+
}
|
|
66
|
+
}, []);
|
|
67
|
+
|
|
68
|
+
const resetState = useCallback(() => {
|
|
69
|
+
updateState({
|
|
70
|
+
currentStep: 'Start',
|
|
71
|
+
employeeData: null,
|
|
72
|
+
animationState: ANIMATION_STATES.FACE_SCAN,
|
|
73
|
+
isLoading: false,
|
|
74
|
+
});
|
|
75
|
+
resetCountdown();
|
|
76
|
+
}, [resetCountdown, updateState]);
|
|
77
|
+
|
|
78
|
+
const handleCountdownFinish = useCallback(() => {
|
|
79
|
+
notifyMessage('Time is up! Please try again.', 'error');
|
|
80
|
+
updateState({
|
|
81
|
+
modalVisible: false,
|
|
82
|
+
animationState: ANIMATION_STATES.ERROR,
|
|
83
|
+
});
|
|
84
|
+
resetState();
|
|
85
|
+
navigation.goBack();
|
|
86
|
+
}, [navigation, notifyMessage, resetState, updateState]);
|
|
87
|
+
|
|
88
|
+
const uploadFaceScan = useCallback(async (selfie) => {
|
|
89
|
+
const currentData = dataRef.current?.data?.userdata?.hrenemp;
|
|
90
|
+
const base64 = await convertImageToBase64(selfie.uri);
|
|
91
|
+
if (!currentData) {
|
|
92
|
+
notifyMessage('Employee data not found.', 'error');
|
|
93
|
+
updateState({ animationState: ANIMATION_STATES.ERROR });
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (!base64) {
|
|
97
|
+
notifyMessage('convert Image To Base64 Failed.', 'error');
|
|
98
|
+
updateState({ animationState: ANIMATION_STATES.ERROR });
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
updateState({
|
|
103
|
+
isLoading: true,
|
|
104
|
+
animationState: ANIMATION_STATES.PROCESSING,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const body = { image: base64 }
|
|
109
|
+
const header = { hrenemp: currentData }
|
|
110
|
+
const response = await networkServiceCall("POST",RECOGNIZE_URL,header,body);
|
|
111
|
+
if (response.httpstatus === 200) {
|
|
112
|
+
notifyMessage('Identity verified successfully!', 'success');
|
|
113
|
+
updateState({
|
|
114
|
+
employeeData: response.data?.data,
|
|
115
|
+
animationState: ANIMATION_STATES.SUCCESS,
|
|
116
|
+
});
|
|
117
|
+
setTimeout(() => startQRCodeScan(), 1500);
|
|
118
|
+
} else {
|
|
119
|
+
updateState({ animationState: ANIMATION_STATES.ERROR });
|
|
120
|
+
notifyMessage(response.data?.error?.message || 'Face not recognized. Please try again.', 'error');
|
|
121
|
+
}
|
|
122
|
+
} catch (error) {
|
|
123
|
+
updateState({ animationState: ANIMATION_STATES.ERROR });
|
|
124
|
+
notifyMessage('Connection error. Please check your network.', 'error');
|
|
125
|
+
} finally {
|
|
126
|
+
updateState({ isLoading: false });
|
|
127
|
+
}
|
|
128
|
+
}, [convertImageToBase64, notifyMessage, startQRCodeScan, updateState]);
|
|
129
|
+
|
|
130
|
+
const handleStartFaceScan = useCallback(() => {
|
|
131
|
+
updateState({
|
|
132
|
+
currentStep: 'Identity Verification',
|
|
133
|
+
animationState: ANIMATION_STATES.FACE_SCAN,
|
|
134
|
+
});
|
|
135
|
+
navigation.navigate('CCaptureImageWithoutEdit', {
|
|
136
|
+
facedetection: true,
|
|
137
|
+
cameratype: 'front',
|
|
138
|
+
onSelect: uploadFaceScan,
|
|
139
|
+
});
|
|
140
|
+
}, [navigation, updateState, uploadFaceScan]);
|
|
141
|
+
|
|
142
|
+
const startQRCodeScan = useCallback(() => {
|
|
143
|
+
updateState({
|
|
144
|
+
currentStep: 'Location Verification',
|
|
145
|
+
animationState: ANIMATION_STATES.QR_SCAN,
|
|
146
|
+
});
|
|
147
|
+
navigation.navigate('CCaptureImageWithoutEdit', {
|
|
148
|
+
hidebuttons: true,
|
|
149
|
+
cameratype: 'back',
|
|
150
|
+
cameramoduletype: 2,
|
|
151
|
+
onSelect: handleQRScanned,
|
|
152
|
+
});
|
|
153
|
+
}, [navigation, updateState, handleQRScanned]);
|
|
154
|
+
|
|
155
|
+
const handleQRScanned = useCallback(async (qrCodeData) => {
|
|
156
|
+
updateState({
|
|
157
|
+
animationState: ANIMATION_STATES.PROCESSING,
|
|
158
|
+
isLoading: true
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
const hasPermission = await requestLocationPermission();
|
|
163
|
+
if (!hasPermission) {
|
|
164
|
+
notifyMessage('Location permission not granted.', 'error');
|
|
165
|
+
updateState({ animationState: ANIMATION_STATES.ERROR });
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const qrString = typeof qrCodeData === 'object' ? qrCodeData.data : qrCodeData;
|
|
170
|
+
if (!qrString || typeof qrString !== 'string') {
|
|
171
|
+
notifyMessage('Invalid QR code. Please try again.', 'error');
|
|
172
|
+
updateState({ animationState: ANIMATION_STATES.ERROR });
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const location = await getCurrentLocation();
|
|
177
|
+
const [latStr, lngStr] = qrString.split(',');
|
|
178
|
+
const lat = parseFloat(latStr);
|
|
179
|
+
const lng = parseFloat(lngStr);
|
|
180
|
+
|
|
181
|
+
const validCoords = !isNaN(lat) && !isNaN(lng);
|
|
182
|
+
const validDev = !isNaN(location.latitude) && !isNaN(location.longitude);
|
|
183
|
+
|
|
184
|
+
if (validCoords && validDev) {
|
|
185
|
+
const distance = getDistanceInMeters(
|
|
186
|
+
lat,
|
|
187
|
+
lng,
|
|
188
|
+
location.latitude,
|
|
189
|
+
location.longitude
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
if (distance <= MAX_DISTANCE_METERS) {
|
|
193
|
+
callback?.(dataRef.current);
|
|
194
|
+
notifyMessage('Location verified successfully!', 'success');
|
|
195
|
+
updateState({ animationState: ANIMATION_STATES.SUCCESS });
|
|
196
|
+
|
|
197
|
+
setTimeout(() => {
|
|
198
|
+
updateState({ modalVisible: false });
|
|
199
|
+
resetState();
|
|
200
|
+
}, 1500);
|
|
201
|
+
} else {
|
|
202
|
+
notifyMessage(`Location mismatch (${distance.toFixed(0)}m away).`, 'error');
|
|
203
|
+
updateState({ animationState: ANIMATION_STATES.ERROR });
|
|
204
|
+
resetState();
|
|
205
|
+
}
|
|
206
|
+
} else {
|
|
207
|
+
notifyMessage('Invalid coordinates in QR code.', 'error');
|
|
208
|
+
updateState({ animationState: ANIMATION_STATES.ERROR });
|
|
209
|
+
resetState();
|
|
210
|
+
}
|
|
211
|
+
} catch (error) {
|
|
212
|
+
notifyMessage('Unable to verify location. Please try again.', 'error');
|
|
213
|
+
updateState({ animationState: ANIMATION_STATES.ERROR });
|
|
214
|
+
resetState();
|
|
215
|
+
} finally {
|
|
216
|
+
updateState({ isLoading: false });
|
|
217
|
+
}
|
|
218
|
+
}, [callback, getCurrentLocation, notifyMessage, requestLocationPermission, resetState, updateState]);
|
|
219
|
+
|
|
220
|
+
const startProcess = useCallback(() => {
|
|
221
|
+
startCountdown(COUNTDOWN_DURATION, handleCountdownFinish);
|
|
222
|
+
handleStartFaceScan();
|
|
223
|
+
}, [handleCountdownFinish, handleStartFaceScan, startCountdown]);
|
|
224
|
+
|
|
225
|
+
useEffect(() => {
|
|
226
|
+
if (data) {
|
|
227
|
+
updateState({ modalVisible: true });
|
|
228
|
+
startProcess();
|
|
229
|
+
}
|
|
230
|
+
}, [data, startProcess, updateState]);
|
|
231
|
+
|
|
232
|
+
return (
|
|
233
|
+
<Modal
|
|
234
|
+
visible={state.modalVisible}
|
|
235
|
+
animationType="slide"
|
|
236
|
+
transparent
|
|
237
|
+
onRequestClose={() => {
|
|
238
|
+
updateState({ modalVisible: false });
|
|
239
|
+
resetState();
|
|
240
|
+
}}
|
|
241
|
+
statusBarTranslucent={true}
|
|
242
|
+
>
|
|
243
|
+
<View style={styles.modalBg}>
|
|
244
|
+
<TouchableOpacity
|
|
245
|
+
style={styles.close}
|
|
246
|
+
onPress={() => {
|
|
247
|
+
updateState({ modalVisible: false });
|
|
248
|
+
resetState();
|
|
249
|
+
}}
|
|
250
|
+
accessibilityLabel="Close modal"
|
|
251
|
+
hitSlop={{ top: 20, bottom: 20, left: 20, right: 20 }}
|
|
252
|
+
>
|
|
253
|
+
<Icon name="close" size={24} color={COLORS.light} />
|
|
254
|
+
</TouchableOpacity>
|
|
255
|
+
|
|
256
|
+
<Text style={styles.title}>Biometric Verification</Text>
|
|
257
|
+
<Text style={styles.subTitle}>{state.currentStep}</Text>
|
|
258
|
+
|
|
259
|
+
<StateIndicator state={state.animationState} size={120} />
|
|
260
|
+
|
|
261
|
+
{state.employeeData && (
|
|
262
|
+
<EmployeeCard
|
|
263
|
+
employeeData={state.employeeData}
|
|
264
|
+
hrenemp={dataRef.current?.data?.userdata?.hrenemp}
|
|
265
|
+
/>
|
|
266
|
+
)}
|
|
267
|
+
|
|
268
|
+
<Notification
|
|
269
|
+
notification={notification}
|
|
270
|
+
fadeAnim={fadeAnim}
|
|
271
|
+
slideAnim={slideAnim}
|
|
272
|
+
/>
|
|
273
|
+
|
|
274
|
+
<CountdownTimer
|
|
275
|
+
duration={COUNTDOWN_DURATION}
|
|
276
|
+
currentTime={countdown}
|
|
277
|
+
/>
|
|
278
|
+
</View>
|
|
279
|
+
</Modal>
|
|
280
|
+
);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
export default BiometricVerificationModal;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// networkServiceCall.js
|
|
2
|
+
const networkServiceCall = async (method, url, extraHeaders = {}, body = {}) => {
|
|
3
|
+
try {
|
|
4
|
+
const dataset = {
|
|
5
|
+
method: method.toUpperCase(),
|
|
6
|
+
headers: {
|
|
7
|
+
'Content-Type': 'application/json',
|
|
8
|
+
...extraHeaders,
|
|
9
|
+
},
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
if (method.toUpperCase() !== 'GET') {
|
|
13
|
+
dataset.body = JSON.stringify(body);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
console.log("🌐 NetworkServiceCall Request:");
|
|
17
|
+
console.log("➡️ URL:", url);
|
|
18
|
+
console.log("➡️ Headers:", dataset.headers);
|
|
19
|
+
if (dataset.body) console.log("➡️ Body:", dataset.body);
|
|
20
|
+
|
|
21
|
+
const response = await fetch(url, dataset);
|
|
22
|
+
|
|
23
|
+
console.log("🌐 Response Status:", response);
|
|
24
|
+
|
|
25
|
+
const result = await response.json();
|
|
26
|
+
console.log("✅ API Success:", result);
|
|
27
|
+
return result;
|
|
28
|
+
|
|
29
|
+
} catch (error) {
|
|
30
|
+
console.error("🚨 NetworkServiceCall Error:", error.message);
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export default networkServiceCall;
|