react-native-biometric-verifier 0.0.18 → 0.0.20
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/CaptureImageWithoutEdit.js +373 -275
- package/src/components/Card.js +6 -6
- package/src/components/CountdownTimer.js +3 -3
- package/src/components/Notification.js +6 -6
- package/src/components/StepIndicator.js +7 -7
- package/src/hooks/useCountdown.js +7 -7
- package/src/hooks/useFaceDetectionFrameProcessor.js +378 -261
- package/src/hooks/useImageProcessing.js +6 -6
- package/src/hooks/useNotifyMessage.js +4 -4
- package/src/index.js +32 -49
- package/src/utils/Global.js +48 -0
- package/src/utils/getLoaderGif.js +3 -3
- package/src/utils/constants.js +0 -73
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useCallback } from 'react';
|
|
2
2
|
import ImageResizer from 'react-native-image-resizer';
|
|
3
3
|
import RNFS from 'react-native-fs';
|
|
4
|
-
import {
|
|
4
|
+
import { Global } from '../utils/Global';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Custom hook to process images: resize and convert to Base64.
|
|
@@ -38,10 +38,10 @@ export const useImageProcessing = () => {
|
|
|
38
38
|
try {
|
|
39
39
|
resizedImage = await ImageResizer.createResizedImage(
|
|
40
40
|
uri,
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
Global.ImageResize.width,
|
|
42
|
+
Global.ImageResize.height,
|
|
43
|
+
Global.ImageResize.format, // 'JPEG' or 'PNG'
|
|
44
|
+
Global.ImageResize.quality, // e.g., 80
|
|
45
45
|
0, // Rotation
|
|
46
46
|
undefined, // Output path (let library choose)
|
|
47
47
|
false // Keep EXIF metadata
|
|
@@ -64,7 +64,7 @@ export const useImageProcessing = () => {
|
|
|
64
64
|
|
|
65
65
|
// Optionally prepend MIME type
|
|
66
66
|
if (includeMimeType) {
|
|
67
|
-
const mimeType =
|
|
67
|
+
const mimeType = Global.ImageResize.format.toLowerCase() === 'png' ? 'image/png' : 'image/jpeg';
|
|
68
68
|
base64Data = `data:${mimeType};base64,${base64Data}`;
|
|
69
69
|
}
|
|
70
70
|
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
ToastAndroid,
|
|
8
8
|
Alert,
|
|
9
9
|
} from 'react-native';
|
|
10
|
-
import {
|
|
10
|
+
import { Global } from '../utils/Global';
|
|
11
11
|
|
|
12
12
|
export const useNotifyMessage = () => {
|
|
13
13
|
const [notification, setNotification] = useState({
|
|
@@ -22,9 +22,9 @@ export const useNotifyMessage = () => {
|
|
|
22
22
|
|
|
23
23
|
// Styles for notification container based on type
|
|
24
24
|
const getNotificationStyle = useCallback((type) => {
|
|
25
|
-
let backgroundColor =
|
|
26
|
-
if (type === 'success') backgroundColor =
|
|
27
|
-
if (type === 'error') backgroundColor =
|
|
25
|
+
let backgroundColor = Global.AppTheme.info;
|
|
26
|
+
if (type === 'success') backgroundColor = Global.AppTheme.success;
|
|
27
|
+
if (type === 'error') backgroundColor = Global.AppTheme.error;
|
|
28
28
|
|
|
29
29
|
return {
|
|
30
30
|
position: 'absolute',
|
package/src/index.js
CHANGED
|
@@ -27,13 +27,7 @@ import { useSafeCallback } from "./hooks/useSafeCallback";
|
|
|
27
27
|
|
|
28
28
|
// Utils
|
|
29
29
|
import { getDistanceInMeters } from "./utils/distanceCalculator";
|
|
30
|
-
import {
|
|
31
|
-
ANIMATION_STATES,
|
|
32
|
-
COLORS,
|
|
33
|
-
COUNTDOWN_DURATION,
|
|
34
|
-
MAX_DISTANCE_METERS,
|
|
35
|
-
LOADING_TYPES,
|
|
36
|
-
} from "./utils/constants";
|
|
30
|
+
import { Global } from "./utils/Global";
|
|
37
31
|
import networkServiceCall from "./utils/NetworkServiceCall";
|
|
38
32
|
import { getLoaderGif } from "./utils/getLoaderGif";
|
|
39
33
|
|
|
@@ -46,7 +40,7 @@ import CaptureImageWithoutEdit from "./components/CaptureImageWithoutEdit";
|
|
|
46
40
|
import StepIndicator from "./components/StepIndicator";
|
|
47
41
|
|
|
48
42
|
const BiometricModal = React.memo(
|
|
49
|
-
({ data, qrscan = false, callback, apiurl, onclose, frameProcessorFps }) => {
|
|
43
|
+
({ data, qrscan = false, callback, apiurl, onclose, frameProcessorFps, livenessLevel }) => {
|
|
50
44
|
const navigation = useNavigation();
|
|
51
45
|
|
|
52
46
|
// Custom hooks
|
|
@@ -61,10 +55,10 @@ const BiometricModal = React.memo(
|
|
|
61
55
|
const [cameraType, setCameraType] = useState("front");
|
|
62
56
|
const [state, setState] = useState({
|
|
63
57
|
isLoading: false,
|
|
64
|
-
loadingType:
|
|
58
|
+
loadingType: Global.LoadingTypes.none,
|
|
65
59
|
currentStep: "Start",
|
|
66
60
|
employeeData: null,
|
|
67
|
-
animationState:
|
|
61
|
+
animationState: Global.AnimationStates.faceScan,
|
|
68
62
|
});
|
|
69
63
|
|
|
70
64
|
// Refs
|
|
@@ -174,10 +168,10 @@ const BiometricModal = React.memo(
|
|
|
174
168
|
|
|
175
169
|
setState({
|
|
176
170
|
isLoading: false,
|
|
177
|
-
loadingType:
|
|
171
|
+
loadingType: Global.LoadingTypes.none,
|
|
178
172
|
currentStep: "Start",
|
|
179
173
|
employeeData: null,
|
|
180
|
-
animationState:
|
|
174
|
+
animationState: Global.AnimationStates.faceScan,
|
|
181
175
|
});
|
|
182
176
|
|
|
183
177
|
setModalVisible(false);
|
|
@@ -200,9 +194,9 @@ const BiometricModal = React.memo(
|
|
|
200
194
|
|
|
201
195
|
notifyMessage(message, "error");
|
|
202
196
|
updateState({
|
|
203
|
-
animationState:
|
|
197
|
+
animationState: Global.AnimationStates.error,
|
|
204
198
|
isLoading: false,
|
|
205
|
-
loadingType:
|
|
199
|
+
loadingType: Global.LoadingTypes.none,
|
|
206
200
|
});
|
|
207
201
|
|
|
208
202
|
if (resetTimeoutRef.current) {
|
|
@@ -256,8 +250,8 @@ const BiometricModal = React.memo(
|
|
|
256
250
|
|
|
257
251
|
updateState({
|
|
258
252
|
isLoading: true,
|
|
259
|
-
loadingType:
|
|
260
|
-
animationState:
|
|
253
|
+
loadingType: Global.LoadingTypes.faceRecognition,
|
|
254
|
+
animationState: Global.AnimationStates.processing,
|
|
261
255
|
});
|
|
262
256
|
|
|
263
257
|
InteractionManager.runAfterInteractions(async () => {
|
|
@@ -266,7 +260,7 @@ const BiometricModal = React.memo(
|
|
|
266
260
|
try {
|
|
267
261
|
console.log("🖼️ Converting image to base64");
|
|
268
262
|
updateState({
|
|
269
|
-
loadingType:
|
|
263
|
+
loadingType: Global.LoadingTypes.imageProcessing,
|
|
270
264
|
});
|
|
271
265
|
|
|
272
266
|
base64 = await convertImageToBase64(selfie?.uri);
|
|
@@ -290,7 +284,7 @@ const BiometricModal = React.memo(
|
|
|
290
284
|
console.log("🌐 Calling face recognition API:", buttonapi);
|
|
291
285
|
|
|
292
286
|
updateState({
|
|
293
|
-
loadingType:
|
|
287
|
+
loadingType: Global.LoadingTypes.networkRequest,
|
|
294
288
|
});
|
|
295
289
|
|
|
296
290
|
const response = await networkServiceCall(
|
|
@@ -308,9 +302,9 @@ const BiometricModal = React.memo(
|
|
|
308
302
|
|
|
309
303
|
updateState({
|
|
310
304
|
employeeData: response.data?.data || null,
|
|
311
|
-
animationState:
|
|
305
|
+
animationState: Global.AnimationStates.success,
|
|
312
306
|
isLoading: false,
|
|
313
|
-
loadingType:
|
|
307
|
+
loadingType: Global.LoadingTypes.none,
|
|
314
308
|
});
|
|
315
309
|
|
|
316
310
|
notifyMessage("Identity verified successfully!", "success");
|
|
@@ -367,15 +361,15 @@ const BiometricModal = React.memo(
|
|
|
367
361
|
if (!validateApiUrl()) return;
|
|
368
362
|
|
|
369
363
|
updateState({
|
|
370
|
-
animationState:
|
|
364
|
+
animationState: Global.AnimationStates.processing,
|
|
371
365
|
isLoading: true,
|
|
372
|
-
loadingType:
|
|
366
|
+
loadingType: Global.LoadingTypes.locationVerification,
|
|
373
367
|
});
|
|
374
368
|
|
|
375
369
|
try {
|
|
376
370
|
console.log("📍 Requesting location permission");
|
|
377
371
|
updateState({
|
|
378
|
-
loadingType:
|
|
372
|
+
loadingType: Global.LoadingTypes.locationPermission,
|
|
379
373
|
});
|
|
380
374
|
|
|
381
375
|
const hasPermission = await requestLocationPermission();
|
|
@@ -400,7 +394,7 @@ const BiometricModal = React.memo(
|
|
|
400
394
|
console.log("📋 QR code content:", qrString);
|
|
401
395
|
|
|
402
396
|
updateState({
|
|
403
|
-
loadingType:
|
|
397
|
+
loadingType: Global.LoadingTypes.gettingLocation,
|
|
404
398
|
});
|
|
405
399
|
|
|
406
400
|
const location = await getCurrentLocation();
|
|
@@ -422,7 +416,7 @@ const BiometricModal = React.memo(
|
|
|
422
416
|
|
|
423
417
|
if (validCoords && validDev) {
|
|
424
418
|
updateState({
|
|
425
|
-
loadingType:
|
|
419
|
+
loadingType: Global.LoadingTypes.calculateDistance,
|
|
426
420
|
});
|
|
427
421
|
|
|
428
422
|
const distance = getDistanceInMeters(
|
|
@@ -438,15 +432,15 @@ const BiometricModal = React.memo(
|
|
|
438
432
|
"meters"
|
|
439
433
|
);
|
|
440
434
|
|
|
441
|
-
if (distance <=
|
|
435
|
+
if (distance <= Global.MaxDistanceMeters) {
|
|
442
436
|
console.log("✅ Location verified successfully");
|
|
443
437
|
safeCallback(responseRef.current);
|
|
444
438
|
notifyMessage("Location verified successfully!", "success");
|
|
445
439
|
|
|
446
440
|
updateState({
|
|
447
|
-
animationState:
|
|
441
|
+
animationState: Global.AnimationStates.success,
|
|
448
442
|
isLoading: false,
|
|
449
|
-
loadingType:
|
|
443
|
+
loadingType: Global.LoadingTypes.none,
|
|
450
444
|
});
|
|
451
445
|
|
|
452
446
|
if (resetTimeoutRef.current) {
|
|
@@ -517,7 +511,7 @@ const BiometricModal = React.memo(
|
|
|
517
511
|
console.log("👤 Starting face scan");
|
|
518
512
|
updateState({
|
|
519
513
|
currentStep: "Identity Verification",
|
|
520
|
-
animationState:
|
|
514
|
+
animationState: Global.AnimationStates.faceScan,
|
|
521
515
|
});
|
|
522
516
|
setCameraType("front");
|
|
523
517
|
}, [updateState]);
|
|
@@ -527,21 +521,11 @@ const BiometricModal = React.memo(
|
|
|
527
521
|
console.log("📍 Starting QR code scan");
|
|
528
522
|
updateState({
|
|
529
523
|
currentStep: "Location Verification",
|
|
530
|
-
animationState:
|
|
524
|
+
animationState: Global.AnimationStates.qrScan,
|
|
531
525
|
});
|
|
532
526
|
setCameraType("back");
|
|
533
527
|
}, [updateState]);
|
|
534
528
|
|
|
535
|
-
// Toggle camera
|
|
536
|
-
const toggleCamera = useCallback(() => {
|
|
537
|
-
console.log("🔄 Toggling camera");
|
|
538
|
-
setCameraType((prevType) => {
|
|
539
|
-
const newType = prevType === "front" ? "back" : "front";
|
|
540
|
-
console.log("📷 Switching to camera:", newType);
|
|
541
|
-
return newType;
|
|
542
|
-
});
|
|
543
|
-
}, []);
|
|
544
|
-
|
|
545
529
|
// Start the verification process
|
|
546
530
|
const startProcess = useCallback(() => {
|
|
547
531
|
console.log("🚀 Starting verification process");
|
|
@@ -571,8 +555,8 @@ const BiometricModal = React.memo(
|
|
|
571
555
|
const shouldShowCamera =
|
|
572
556
|
(state.currentStep === "Identity Verification" ||
|
|
573
557
|
state.currentStep === "Location Verification") &&
|
|
574
|
-
state.animationState !==
|
|
575
|
-
state.animationState !==
|
|
558
|
+
state.animationState !== Global.AnimationStates.success &&
|
|
559
|
+
state.animationState !== Global.AnimationStates.error;
|
|
576
560
|
|
|
577
561
|
return (
|
|
578
562
|
<Modal
|
|
@@ -589,11 +573,10 @@ const BiometricModal = React.memo(
|
|
|
589
573
|
<CaptureImageWithoutEdit
|
|
590
574
|
cameraType={cameraType}
|
|
591
575
|
onCapture={handleImageCapture}
|
|
592
|
-
onToggleCamera={toggleCamera}
|
|
593
576
|
showCodeScanner={state.currentStep === "Location Verification"}
|
|
594
577
|
isLoading={state.isLoading}
|
|
595
|
-
currentStep={state.currentStep}
|
|
596
578
|
frameProcessorFps={frameProcessorFps}
|
|
579
|
+
livenessLevel={livenessLevel}
|
|
597
580
|
/>
|
|
598
581
|
</View>
|
|
599
582
|
)}
|
|
@@ -605,7 +588,7 @@ const BiometricModal = React.memo(
|
|
|
605
588
|
accessibilityLabel="Close modal"
|
|
606
589
|
hitSlop={{ top: 20, bottom: 20, left: 20, right: 20 }}
|
|
607
590
|
>
|
|
608
|
-
<Icon name="close" size={24} color={
|
|
591
|
+
<Icon name="close" size={24} color={Global.AppTheme.light} />
|
|
609
592
|
</TouchableOpacity>
|
|
610
593
|
|
|
611
594
|
<View style={styles.topContainer}>
|
|
@@ -639,7 +622,7 @@ const BiometricModal = React.memo(
|
|
|
639
622
|
|
|
640
623
|
<View style={styles.timerContainer}>
|
|
641
624
|
<CountdownTimer
|
|
642
|
-
duration={
|
|
625
|
+
duration={Global.CountdownDuration}
|
|
643
626
|
currentTime={countdown}
|
|
644
627
|
/>
|
|
645
628
|
</View>
|
|
@@ -657,7 +640,7 @@ const BiometricModal = React.memo(
|
|
|
657
640
|
const styles = StyleSheet.create({
|
|
658
641
|
modalContainer: {
|
|
659
642
|
flex: 1,
|
|
660
|
-
backgroundColor:
|
|
643
|
+
backgroundColor: Global.AppTheme.modalBackground || 'rgba(0, 0, 0, 0.85)',
|
|
661
644
|
},
|
|
662
645
|
cameraContainer: {
|
|
663
646
|
position: 'absolute',
|
|
@@ -694,7 +677,7 @@ const styles = StyleSheet.create({
|
|
|
694
677
|
title: {
|
|
695
678
|
fontSize: 26,
|
|
696
679
|
fontWeight: '700',
|
|
697
|
-
color:
|
|
680
|
+
color: Global.AppTheme.textLight || Global.AppTheme.light,
|
|
698
681
|
marginBottom: 5,
|
|
699
682
|
textAlign: 'left',
|
|
700
683
|
fontFamily: Platform.OS === 'ios' ? 'Helvetica Neue' : 'sans-serif',
|
|
@@ -704,7 +687,7 @@ const styles = StyleSheet.create({
|
|
|
704
687
|
},
|
|
705
688
|
subtitle: {
|
|
706
689
|
fontSize: 18,
|
|
707
|
-
color:
|
|
690
|
+
color: Global.AppTheme.textLight || Global.AppTheme.light,
|
|
708
691
|
marginBottom: 0,
|
|
709
692
|
fontWeight: '600',
|
|
710
693
|
textAlign: 'left',
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export class Global {
|
|
2
|
+
// ====== APP CONSTANTS ======
|
|
3
|
+
static AppTheme = {
|
|
4
|
+
primary: '#6C63FF',
|
|
5
|
+
primaryLight: '#8A85FF',
|
|
6
|
+
success: '#4CAF50',
|
|
7
|
+
error: '#F44336',
|
|
8
|
+
warning: '#FF9800',
|
|
9
|
+
info: '#2196F3',
|
|
10
|
+
dark: '#2D3748',
|
|
11
|
+
light: '#F7FAFC',
|
|
12
|
+
gray: '#A0AEC0',
|
|
13
|
+
background: '#F8F9FA',
|
|
14
|
+
cardBackground: '#FFFFFF',
|
|
15
|
+
textLight: '#FFFFFF',
|
|
16
|
+
shadow: '#00000033', // semi-transparent shadow
|
|
17
|
+
modalBackground: 'rgba(0, 0, 0, 0.85)',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
static LoadingTypes = {
|
|
21
|
+
none: 'none',
|
|
22
|
+
imageProcessing: 'imageProcessing',
|
|
23
|
+
faceRecognition: 'faceRecognition',
|
|
24
|
+
networkRequest: 'networkRequest',
|
|
25
|
+
locationPermission: 'locationPermission',
|
|
26
|
+
gettingLocation: 'gettingLocation',
|
|
27
|
+
calculateDistance: 'calculateDistance',
|
|
28
|
+
locationVerification: 'locationVerification',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
static AnimationStates = {
|
|
32
|
+
faceScan: 'faceScan',
|
|
33
|
+
qrScan: 'qrScan',
|
|
34
|
+
processing: 'processing',
|
|
35
|
+
success: 'success',
|
|
36
|
+
error: 'error',
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
static ImageResize = {
|
|
40
|
+
width: 640,
|
|
41
|
+
height: 640,
|
|
42
|
+
format: 'JPEG', // 'PNG' or 'JPEG'
|
|
43
|
+
quality: 85, // 0–100
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
static CountdownDuration = 100; // seconds
|
|
47
|
+
static MaxDistanceMeters = 100; // Max allowed distance for QR verification
|
|
48
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Global } from "./Global";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Decides which GIF should be shown based on animationState or currentStep
|
|
@@ -13,14 +13,14 @@ export const getLoaderGif = (animationState, currentStep,APIURL) => {
|
|
|
13
13
|
`${APIURL}file/getCommonFile/image/Location.gif`;
|
|
14
14
|
|
|
15
15
|
if (
|
|
16
|
-
animationState ===
|
|
16
|
+
animationState === Global.AnimationStates.faceScan ||
|
|
17
17
|
currentStep === 'Identity Verification'
|
|
18
18
|
) {
|
|
19
19
|
return { uri: FaceGifUrl };
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
if (
|
|
23
|
-
animationState ===
|
|
23
|
+
animationState === Global.AnimationStates.qrScan ||
|
|
24
24
|
currentStep === 'Location Verification'
|
|
25
25
|
) {
|
|
26
26
|
return { uri: LocationGifUrl };
|
package/src/utils/constants.js
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
// ====== COLORS ======
|
|
2
|
-
export const COLORS = {
|
|
3
|
-
primary: '#6C63FF',
|
|
4
|
-
primaryLight: '#8A85FF',
|
|
5
|
-
success: '#4CAF50',
|
|
6
|
-
error: '#F44336',
|
|
7
|
-
warning: '#FF9800',
|
|
8
|
-
info: '#2196F3',
|
|
9
|
-
dark: '#2D3748',
|
|
10
|
-
light: '#F7FAFC',
|
|
11
|
-
gray: '#A0AEC0',
|
|
12
|
-
background: '#F8F9FA',
|
|
13
|
-
cardBackground: '#FFFFFF',
|
|
14
|
-
textLight: '#FFFFFF',
|
|
15
|
-
shadow: '#00000033', // semi-transparent shadow
|
|
16
|
-
modalBackground: 'rgba(0, 0, 0, 0.85)',
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
// ====== LOADING TYPES ======
|
|
20
|
-
export const LOADING_TYPES = {
|
|
21
|
-
NONE: 'NONE',
|
|
22
|
-
CAMERA_INIT: 'CAMERA_INIT',
|
|
23
|
-
CAPTURING_PHOTO: 'CAPTURING_PHOTO',
|
|
24
|
-
IMAGE_PROCESSING: 'IMAGE_PROCESSING',
|
|
25
|
-
FACE_RECOGNITION: 'FACE_RECOGNITION',
|
|
26
|
-
NETWORK_REQUEST: 'NETWORK_REQUEST',
|
|
27
|
-
LOCATION_PERMISSION: 'LOCATION_PERMISSION',
|
|
28
|
-
GETTING_LOCATION: 'GETTING_LOCATION',
|
|
29
|
-
CALCULATING_DISTANCE: 'CALCULATING_DISTANCE',
|
|
30
|
-
LOCATION_VERIFICATION: 'LOCATION_VERIFICATION',
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
// ====== ANIMATION STATES ======
|
|
34
|
-
export const ANIMATION_STATES = {
|
|
35
|
-
FACE_SCAN: 'faceScan',
|
|
36
|
-
QR_SCAN: 'qrScan',
|
|
37
|
-
PROCESSING: 'processing',
|
|
38
|
-
SUCCESS: 'success',
|
|
39
|
-
ERROR: 'error',
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
// ====== IMAGE RESIZE CONFIG ======
|
|
43
|
-
export const IMAGE_RESIZE = {
|
|
44
|
-
width: 640,
|
|
45
|
-
height: 640,
|
|
46
|
-
format: 'JPEG', // 'PNG' or 'JPEG'
|
|
47
|
-
quality: 85, // 0-100
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
// ====== CONSTANTS ======
|
|
51
|
-
export const COUNTDOWN_DURATION = 100; // seconds
|
|
52
|
-
export const MAX_DISTANCE_METERS = 100; // Max allowed distance for QR verification
|
|
53
|
-
|
|
54
|
-
// ====== NOTIFICATION SETTINGS ======
|
|
55
|
-
export const NOTIFICATION = {
|
|
56
|
-
DEFAULT_DURATION: 3000, // ms
|
|
57
|
-
FADE_DURATION: 300, // ms
|
|
58
|
-
SLIDE_DISTANCE: 20, // px
|
|
59
|
-
VIBRATION_DURATION: 100, // ms
|
|
60
|
-
TOAST_OFFSET: 80, // Android Toast vertical offset
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
// ====== QR VERIFICATION SETTINGS ======
|
|
64
|
-
export const QR_VERIFICATION = {
|
|
65
|
-
MAX_ATTEMPTS: 3, // Maximum retries for QR scan
|
|
66
|
-
TIMEOUT: 10000, // Timeout in ms per scan
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
// ====== PLATFORM ======
|
|
70
|
-
export const PLATFORM = {
|
|
71
|
-
IOS: 'ios',
|
|
72
|
-
ANDROID: 'android',
|
|
73
|
-
};
|