react-native-biometric-verifier 0.0.57 → 0.0.59
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/Notification.js +14 -38
- package/src/hooks/useGeolocation.js +102 -42
package/package.json
CHANGED
|
@@ -1,32 +1,23 @@
|
|
|
1
1
|
import React, { useEffect, useRef } from 'react';
|
|
2
2
|
import { Animated, Text, Platform, StyleSheet } from 'react-native';
|
|
3
3
|
import Icon from 'react-native-vector-icons/MaterialIcons';
|
|
4
|
-
import PropTypes from 'prop-types';
|
|
5
4
|
import { Global } from '../utils/Global';
|
|
6
5
|
|
|
7
|
-
export const Notification = ({ notification, fadeAnim, slideAnim }) => {
|
|
8
|
-
|
|
9
|
-
console.warn('Notification: Invalid or missing notification object');
|
|
10
|
-
return null;
|
|
11
|
-
}
|
|
6
|
+
export const Notification = ({ notification = {}, fadeAnim, slideAnim }) => {
|
|
7
|
+
const { visible = false, type = 'info', message = '' } = notification;
|
|
12
8
|
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
// Animations (must ALWAYS exist)
|
|
10
|
+
const scaleAnim = useRef(new Animated.Value(1)).current;
|
|
11
|
+
const shakeAnim = useRef(new Animated.Value(0)).current;
|
|
15
12
|
|
|
16
|
-
// Icon and color mapping
|
|
17
13
|
const iconMap = {
|
|
18
14
|
success: { name: 'check-circle', color: Global.AppTheme.success },
|
|
19
15
|
error: { name: 'error', color: Global.AppTheme.error },
|
|
20
16
|
info: { name: 'info', color: Global.AppTheme.info },
|
|
21
17
|
};
|
|
22
18
|
|
|
23
|
-
const { name: iconName, color: iconColor } =
|
|
24
|
-
|
|
25
|
-
// Heartbeat animation (scale in/out)
|
|
26
|
-
const scaleAnim = useRef(new Animated.Value(1)).current;
|
|
27
|
-
|
|
28
|
-
// Shake animation (rotation wiggle)
|
|
29
|
-
const shakeAnim = useRef(new Animated.Value(0)).current;
|
|
19
|
+
const { name: iconName, color: iconColor } =
|
|
20
|
+
iconMap[type] || iconMap.info;
|
|
30
21
|
|
|
31
22
|
useEffect(() => {
|
|
32
23
|
const heartbeat = Animated.loop(
|
|
@@ -78,20 +69,20 @@ export const Notification = ({ notification, fadeAnim, slideAnim }) => {
|
|
|
78
69
|
heartbeat.stop();
|
|
79
70
|
shake.stop();
|
|
80
71
|
};
|
|
81
|
-
}, [visible, type
|
|
72
|
+
}, [visible, type]);
|
|
82
73
|
|
|
83
|
-
|
|
84
|
-
const shakeInterpolate = shakeAnim.interpolate({
|
|
74
|
+
const shakeRotate = shakeAnim.interpolate({
|
|
85
75
|
inputRange: [-1, 1],
|
|
86
76
|
outputRange: ['-10deg', '10deg'],
|
|
87
77
|
});
|
|
88
78
|
|
|
89
79
|
return (
|
|
90
80
|
<Animated.View
|
|
81
|
+
pointerEvents={visible ? 'auto' : 'none'}
|
|
91
82
|
style={[
|
|
92
83
|
styles.container,
|
|
93
84
|
{
|
|
94
|
-
opacity:
|
|
85
|
+
opacity: visible ? 1 : 0,
|
|
95
86
|
transform: [
|
|
96
87
|
{ translateY: slideAnim instanceof Animated.Value ? slideAnim : 0 },
|
|
97
88
|
],
|
|
@@ -102,12 +93,13 @@ export const Notification = ({ notification, fadeAnim, slideAnim }) => {
|
|
|
102
93
|
style={{
|
|
103
94
|
transform: [
|
|
104
95
|
{ scale: scaleAnim },
|
|
105
|
-
...(type === 'error' ? [{ rotate:
|
|
96
|
+
...(type === 'error' ? [{ rotate: shakeRotate }] : []),
|
|
106
97
|
],
|
|
107
98
|
}}
|
|
108
99
|
>
|
|
109
|
-
<Icon name={iconName} size={50} color={iconColor}
|
|
100
|
+
<Icon name={iconName} size={50} color={iconColor} />
|
|
110
101
|
</Animated.View>
|
|
102
|
+
|
|
111
103
|
<Text style={styles.message}>
|
|
112
104
|
{message || 'No message provided'}
|
|
113
105
|
</Text>
|
|
@@ -142,20 +134,4 @@ const styles = StyleSheet.create({
|
|
|
142
134
|
},
|
|
143
135
|
});
|
|
144
136
|
|
|
145
|
-
Notification.propTypes = {
|
|
146
|
-
notification: PropTypes.shape({
|
|
147
|
-
visible: PropTypes.bool.isRequired,
|
|
148
|
-
type: PropTypes.oneOf(['success', 'error', 'info']),
|
|
149
|
-
message: PropTypes.string,
|
|
150
|
-
}),
|
|
151
|
-
fadeAnim: PropTypes.instanceOf(Animated.Value),
|
|
152
|
-
slideAnim: PropTypes.instanceOf(Animated.Value),
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
Notification.defaultProps = {
|
|
156
|
-
notification: { visible: false, type: 'info', message: '' },
|
|
157
|
-
fadeAnim: new Animated.Value(1),
|
|
158
|
-
slideAnim: new Animated.Value(0),
|
|
159
|
-
};
|
|
160
|
-
|
|
161
137
|
export default Notification;
|
|
@@ -1,71 +1,131 @@
|
|
|
1
1
|
import { useCallback } from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
Platform,
|
|
4
|
+
PermissionsAndroid,
|
|
5
|
+
Linking,
|
|
6
|
+
} from 'react-native';
|
|
3
7
|
import Geolocation from 'react-native-geolocation-service';
|
|
4
8
|
|
|
5
9
|
/**
|
|
6
|
-
*
|
|
10
|
+
* High-accuracy geolocation hook with GPS warm-up
|
|
7
11
|
*
|
|
8
|
-
* @param {Function} notifyMessage -
|
|
9
|
-
* @returns {Object} { requestLocationPermission, getCurrentLocation }
|
|
12
|
+
* @param {Function} notifyMessage - (message, type) => void
|
|
10
13
|
*/
|
|
11
14
|
export const useGeolocation = (notifyMessage) => {
|
|
12
15
|
/**
|
|
13
|
-
*
|
|
14
|
-
* @returns {Promise<boolean>} - True if permission granted, else false.
|
|
16
|
+
* Request location permission (Android only)
|
|
15
17
|
*/
|
|
16
18
|
const requestLocationPermission = useCallback(async () => {
|
|
17
|
-
if (Platform.OS !== 'android')
|
|
19
|
+
if (Platform.OS !== 'android') {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
18
22
|
|
|
19
23
|
try {
|
|
20
|
-
const
|
|
24
|
+
const result = await PermissionsAndroid.requestMultiple([
|
|
21
25
|
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
message: 'This app needs your location to verify QR code scans.',
|
|
25
|
-
buttonPositive: 'OK',
|
|
26
|
-
buttonNegative: 'Cancel',
|
|
27
|
-
}
|
|
28
|
-
);
|
|
26
|
+
PermissionsAndroid.PERMISSIONS.ACCESS_COARSE_LOCATION,
|
|
27
|
+
]);
|
|
29
28
|
|
|
30
|
-
const
|
|
29
|
+
const fineGranted =
|
|
30
|
+
result[PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION] ===
|
|
31
|
+
PermissionsAndroid.RESULTS.GRANTED;
|
|
31
32
|
|
|
32
|
-
if (!
|
|
33
|
-
notifyMessage?.(
|
|
33
|
+
if (!fineGranted) {
|
|
34
|
+
notifyMessage?.(
|
|
35
|
+
'Precise location permission is required for accurate location.',
|
|
36
|
+
'error'
|
|
37
|
+
);
|
|
38
|
+
return false;
|
|
34
39
|
}
|
|
35
40
|
|
|
36
|
-
return
|
|
41
|
+
return true;
|
|
37
42
|
} catch (error) {
|
|
38
|
-
console.error('
|
|
43
|
+
console.error('Permission error:', error);
|
|
39
44
|
notifyMessage?.('Location permission error.', 'error');
|
|
40
45
|
return false;
|
|
41
46
|
}
|
|
42
47
|
}, [notifyMessage]);
|
|
43
48
|
|
|
44
49
|
/**
|
|
45
|
-
*
|
|
46
|
-
* @returns {Promise<GeolocationCoordinates>}
|
|
50
|
+
* Get current location with GPS warm-up
|
|
47
51
|
*/
|
|
48
|
-
const getCurrentLocation = useCallback(
|
|
49
|
-
() =>
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
const getCurrentLocation = useCallback(() => {
|
|
53
|
+
return new Promise(async (resolve, reject) => {
|
|
54
|
+
const hasPermission = await requestLocationPermission();
|
|
55
|
+
if (!hasPermission) {
|
|
56
|
+
return reject('Permission denied');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let watchId = null;
|
|
60
|
+
let timeoutId = null;
|
|
61
|
+
|
|
62
|
+
const cleanup = () => {
|
|
63
|
+
if (watchId !== null) {
|
|
64
|
+
Geolocation.clearWatch(watchId);
|
|
65
|
+
}
|
|
66
|
+
if (timeoutId) {
|
|
67
|
+
clearTimeout(timeoutId);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Safety timeout (avoid infinite GPS wait)
|
|
72
|
+
timeoutId = setTimeout(() => {
|
|
73
|
+
cleanup();
|
|
74
|
+
notifyMessage?.(
|
|
75
|
+
'Unable to get accurate location. Please try again outdoors.',
|
|
76
|
+
'error'
|
|
77
|
+
);
|
|
78
|
+
reject('Location timeout');
|
|
79
|
+
}, 25000);
|
|
80
|
+
|
|
81
|
+
watchId = Geolocation.watchPosition(
|
|
82
|
+
(position) => {
|
|
83
|
+
const { accuracy } = position.coords;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Accept the location only when GPS is accurate enough
|
|
87
|
+
* Typical threshold: 15–25 meters
|
|
88
|
+
*/
|
|
89
|
+
if (accuracy && accuracy <= 20) {
|
|
90
|
+
cleanup();
|
|
53
91
|
resolve(position.coords);
|
|
54
|
-
},
|
|
55
|
-
(error) => {
|
|
56
|
-
notifyMessage?.('Unable to fetch current location.', 'error');
|
|
57
|
-
reject(error);
|
|
58
|
-
},
|
|
59
|
-
{
|
|
60
|
-
enableHighAccuracy: true,
|
|
61
|
-
timeout: 15000,
|
|
62
|
-
maximumAge: 10000,
|
|
63
|
-
distanceFilter: 1,
|
|
64
92
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
93
|
+
},
|
|
94
|
+
(error) => {
|
|
95
|
+
cleanup();
|
|
96
|
+
|
|
97
|
+
// GPS disabled
|
|
98
|
+
if (error.code === 2) {
|
|
99
|
+
notifyMessage?.(
|
|
100
|
+
'Please enable GPS for accurate location.',
|
|
101
|
+
'warning'
|
|
102
|
+
);
|
|
103
|
+
Linking.openSettings();
|
|
104
|
+
} else {
|
|
105
|
+
notifyMessage?.('Unable to fetch location.', 'error');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
reject(error);
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
enableHighAccuracy: true,
|
|
112
|
+
accuracy: {
|
|
113
|
+
android: 'high',
|
|
114
|
+
ios: 'best',
|
|
115
|
+
},
|
|
116
|
+
distanceFilter: 0,
|
|
117
|
+
interval: 1000,
|
|
118
|
+
fastestInterval: 500,
|
|
119
|
+
forceRequestLocation: true,
|
|
120
|
+
showLocationDialog: true,
|
|
121
|
+
maximumAge: 0,
|
|
122
|
+
}
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
}, [notifyMessage, requestLocationPermission]);
|
|
69
126
|
|
|
70
|
-
return {
|
|
127
|
+
return {
|
|
128
|
+
requestLocationPermission,
|
|
129
|
+
getCurrentLocation,
|
|
130
|
+
};
|
|
71
131
|
};
|