@test-web/react-native-sdk 1.0.1 → 2.1.0
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 +543 -26
- package/assets/images/Chrome-logo.svg +1 -0
- package/assets/images/Firefox-logo.png +0 -0
- package/assets/images/IDM-logo.jpg +0 -0
- package/assets/images/MRZOverlay.png +0 -0
- package/assets/images/Safari-logo.png +0 -0
- package/assets/images/aadhar.png +0 -0
- package/assets/images/camera-bg.png +0 -0
- package/assets/images/card-overlay-back.png +0 -0
- package/assets/images/card-overlay.png +0 -0
- package/assets/images/card-scan-back-icon.jpg +0 -0
- package/assets/images/card-scan-front-icon.png +0 -0
- package/assets/images/card-scan-icon-aadhaar-1.png +0 -0
- package/assets/images/card-scan-icon-aadhaar-back.png +0 -0
- package/assets/images/card-scan-icon-aadhaar-scan-qr.png +0 -0
- package/assets/images/card-scan-icon-aadhaar.png +0 -0
- package/assets/images/card-scan-icon-can-pr.png +0 -0
- package/assets/images/card-scan-icon-default-back.png +0 -0
- package/assets/images/card-scan-icon-dl.png +0 -0
- package/assets/images/card-scan-icon-greencard-back.jpg +0 -0
- package/assets/images/card-scan-icon-greencard.jpg +0 -0
- package/assets/images/card-scan-icon-hc.png +0 -0
- package/assets/images/card-scan-icon-ni-argentina-back.jpg +0 -0
- package/assets/images/card-scan-icon-ni-argentina-old.png +0 -0
- package/assets/images/card-scan-icon-ni-argentina.jpg +0 -0
- package/assets/images/card-scan-icon-ni-barcode.jpg +0 -0
- package/assets/images/card-scan-icon-ni-brazil-back.jpg +0 -0
- package/assets/images/card-scan-icon-ni-brazil.jpg +0 -0
- package/assets/images/card-scan-icon-ni-dominican-republic-back.png +0 -0
- package/assets/images/card-scan-icon-ni-dominican-republic-front.png +0 -0
- package/assets/images/card-scan-icon-ni-dominican-republic-mrz.png +0 -0
- package/assets/images/card-scan-icon-ni-dominicanaRepublic-back.jpg +0 -0
- package/assets/images/card-scan-icon-ni-france-back.png +0 -0
- package/assets/images/card-scan-icon-ni-france-front.png +0 -0
- package/assets/images/card-scan-icon-ni-france-scan-mrz.png +0 -0
- package/assets/images/card-scan-icon-ni-germany-back.jpg +0 -0
- package/assets/images/card-scan-icon-ni-germany.jpg +0 -0
- package/assets/images/card-scan-icon-ni-paraguay-back.png +0 -0
- package/assets/images/card-scan-icon-ni-paraguay-front.png +0 -0
- package/assets/images/card-scan-icon-ni-paraguay-scan-mrz.png +0 -0
- package/assets/images/card-scan-icon-ni-uae-back.png +0 -0
- package/assets/images/card-scan-icon-ni-uae-front.png +0 -0
- package/assets/images/card-scan-icon-ni-uae-scan-mrz.png +0 -0
- package/assets/images/card-scan-icon-ni-uganda-front.png +0 -0
- package/assets/images/card-scan-icon-ni-uganda-scan-mrz.png +0 -0
- package/assets/images/card-scan-icon-ni-ukrain-back.png +0 -0
- package/assets/images/card-scan-icon-ni-ukrain-front.png +0 -0
- package/assets/images/card-scan-icon-ni-ukrain-scan-mrz.png +0 -0
- package/assets/images/card-scan-icon-ni.png +0 -0
- package/assets/images/card-scan-icon-old.jpg +0 -0
- package/assets/images/card-scan-icon-pan.png +0 -0
- package/assets/images/card-scan-icon-passport-card-back.jpg +0 -0
- package/assets/images/card-scan-icon-passport-card.jpg +0 -0
- package/assets/images/card-scan-icon-passport-old.png +0 -0
- package/assets/images/card-scan-icon-passport.png +0 -0
- package/assets/images/card-scan-icon-pr.png +0 -0
- package/assets/images/card-scan-icon.jpg +0 -0
- package/assets/images/check.png +0 -0
- package/assets/images/chrome-animation-GPS-permissions-setting.gif +0 -0
- package/assets/images/chrome-animation-camera-permissions-setting.gif +0 -0
- package/assets/images/denied.png +0 -0
- package/assets/images/dl.png +0 -0
- package/assets/images/driver-license.png +0 -0
- package/assets/images/firefox-animation-permissions-setting.gif +0 -0
- package/assets/images/flashlight_on.png +0 -0
- package/assets/images/gallery.png +0 -0
- package/assets/images/greencard.png +0 -0
- package/assets/images/header.jpg +0 -0
- package/assets/images/health-card.png +0 -0
- package/assets/images/ic_camera_front_white_36px.svg +4 -0
- package/assets/images/ic_camera_rear_white_36px.svg +4 -0
- package/assets/images/ic_fullscreen_exit_white_48px.svg +4 -0
- package/assets/images/ic_fullscreen_white_48px.svg +4 -0
- package/assets/images/ic_photo_camera_white_48px.svg +5 -0
- package/assets/images/id-card.png +0 -0
- package/assets/images/idcardimg.png +0 -0
- package/assets/images/idmval-barcode.png +0 -0
- package/assets/images/information.png +0 -0
- package/assets/images/loader.gif +0 -0
- package/assets/images/loading.svg +1 -0
- package/assets/images/logo.jpg +0 -0
- package/assets/images/logo.png +0 -0
- package/assets/images/mrz-back.png +0 -0
- package/assets/images/mrz-ni.png +0 -0
- package/assets/images/mrz.png +0 -0
- package/assets/images/mrz1.png +0 -0
- package/assets/images/mrz_old.png +0 -0
- package/assets/images/mrz_small.png +0 -0
- package/assets/images/national-id.png +0 -0
- package/assets/images/nationalID.png +0 -0
- package/assets/images/no-wifi.png +0 -0
- package/assets/images/passport-card.png +0 -0
- package/assets/images/passport.png +0 -0
- package/assets/images/permit-card.png +0 -0
- package/assets/images/photo-overlay.png +0 -0
- package/assets/images/placeholder.jpg +0 -0
- package/assets/images/qr-code.png +0 -0
- package/assets/images/right-checkmark.jpg +0 -0
- package/assets/images/selfie.jpg +0 -0
- package/assets/images/showing-sec.png +0 -0
- package/assets/images/spinner.gif +0 -0
- package/assets/images/splash-icon.png +0 -0
- package/assets/images/take-selfie.jpg +0 -0
- package/assets/images/torch_off.png +0 -0
- package/assets/images/warning-icon.jpg +0 -0
- package/assets/images/warning-stick.jpg +0 -0
- package/assets/images/wrong-checkmark.jpg +0 -0
- package/package.json +31 -4
- package/src/apis/index.ts +338 -17
- package/src/components/common/Loader.tsx +16 -2
- package/src/config/apiConfig.ts +6 -0
- package/src/index.tsx +123 -7
- package/src/screens/BarcodeCapture.tsx +64 -12
- package/src/screens/DocumentCaptureBack.tsx +133 -24
- package/src/screens/DocumentCaptureFront.tsx +146 -24
- package/src/screens/MrzCapture.tsx +77 -12
- package/src/screens/SelectDocuments.tsx +37 -56
- package/src/screens/SelfieCapture.tsx +114 -18
- package/src/screens/ThankYou.tsx +34 -1
- package/src/services/getUserData.ts +111 -0
- package/src/types/IDMConf.ts +81 -7
- package/src/utils/base64.ts +25 -0
- package/src/utils/flowManager.ts +138 -0
- package/src/utils/imageProcessor.ts +96 -0
- package/src/utils/index.ts +18 -0
|
@@ -1,54 +1,176 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
2
|
-
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
|
|
1
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { View, Text, TouchableOpacity, StyleSheet, Image, Alert } from 'react-native';
|
|
3
3
|
import { useNavigation } from '@react-navigation/native';
|
|
4
4
|
import getStyles from '../styles/DocumentCaptureStyles';
|
|
5
5
|
import { useTheme } from '../context/ThemeContext';
|
|
6
6
|
import { useOrientation } from '../hooks/useOrientation';
|
|
7
|
+
import { useIDM } from '../context/IDMConfigurationContext';
|
|
7
8
|
import Loader from '../components/common/Loader';
|
|
9
|
+
import { captureDocumentFront } from '../apis';
|
|
10
|
+
import { processImageToBase64, validateImagePath } from '../utils/imageProcessor';
|
|
11
|
+
import { createFlowManager } from '../utils/flowManager';
|
|
8
12
|
|
|
9
13
|
export default function DocumentCaptureFront() {
|
|
10
14
|
const { theme } = useTheme();
|
|
15
|
+
const { idmConf, setIDMConf } = useIDM();
|
|
11
16
|
const orientation = useOrientation();
|
|
12
17
|
const styles = getStyles(theme, orientation);
|
|
13
18
|
const navigation = useNavigation();
|
|
14
19
|
const [loading, setLoading] = useState(false);
|
|
20
|
+
const [hasPermission, setHasPermission] = useState(false);
|
|
21
|
+
const [device, setDevice] = useState<any>(null);
|
|
22
|
+
const cameraRef = useRef<any>(null);
|
|
23
|
+
|
|
24
|
+
// Initialize camera
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
const initializeCamera = async () => {
|
|
27
|
+
try {
|
|
28
|
+
// Try to import camera libraries
|
|
29
|
+
const { Camera, useCameraDevice, useCameraPermission } = require('react-native-vision-camera');
|
|
30
|
+
|
|
31
|
+
// Check permission
|
|
32
|
+
const { hasPermission: hasPerm, requestPermission } = useCameraPermission();
|
|
33
|
+
|
|
34
|
+
if (!hasPerm) {
|
|
35
|
+
const granted = await requestPermission();
|
|
36
|
+
if (!granted) {
|
|
37
|
+
navigation.navigate('CameraPermission' as never);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
setHasPermission(true);
|
|
43
|
+
|
|
44
|
+
// Get back camera device
|
|
45
|
+
const backDevice = useCameraDevice('back');
|
|
46
|
+
if (!backDevice) {
|
|
47
|
+
navigation.navigate('NoCameraFound' as never);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
setDevice(backDevice);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
console.warn('Camera initialization failed:', error);
|
|
54
|
+
// Fallback for testing without camera
|
|
55
|
+
setHasPermission(true);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
initializeCamera();
|
|
60
|
+
}, [navigation]);
|
|
61
|
+
|
|
62
|
+
// Determine next navigation step based on metadata
|
|
63
|
+
const handleNextStep = () => {
|
|
64
|
+
const metaData = idmConf?.selectedCountryDetails?.selectedMetaData;
|
|
65
|
+
|
|
66
|
+
if (!metaData) {
|
|
67
|
+
navigation.navigate('ThankYou' as never);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const flowManager = createFlowManager(metaData);
|
|
72
|
+
const nextScreen = flowManager.getNextScreenAfterFront();
|
|
73
|
+
navigation.navigate(nextScreen as never);
|
|
74
|
+
};
|
|
15
75
|
|
|
16
76
|
const takePhoto = async () => {
|
|
77
|
+
if (!cameraRef.current) {
|
|
78
|
+
// Fallback for testing without camera
|
|
79
|
+
Alert.alert('Camera Not Available', 'Camera is not available in this environment.');
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
17
83
|
try {
|
|
18
84
|
setLoading(true);
|
|
19
|
-
|
|
20
|
-
|
|
85
|
+
|
|
86
|
+
const photo = await cameraRef.current.takePhoto();
|
|
87
|
+
|
|
88
|
+
if (!validateImagePath(photo?.path)) {
|
|
89
|
+
Alert.alert('Error', 'Failed to capture image');
|
|
21
90
|
setLoading(false);
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Process image to base64
|
|
95
|
+
const base64Image = await processImageToBase64(photo.path);
|
|
96
|
+
|
|
97
|
+
// Prepare photo data for API
|
|
98
|
+
const photoData = {
|
|
99
|
+
latitude: String(idmConf.userDetails?.location?.coords?.latitude || ''),
|
|
100
|
+
longitude: String(idmConf.userDetails?.location?.coords?.longitude || ''),
|
|
101
|
+
token: String(idmConf.verificationCode || ''),
|
|
102
|
+
persistLoc: 'false',
|
|
103
|
+
metadataIndex: String(idmConf.selectedCountryDetails?.selectedMetaData?.id || ''),
|
|
104
|
+
file: base64Image,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// Call API to capture document front
|
|
108
|
+
const result = await captureDocumentFront(idmConf, photoData);
|
|
109
|
+
|
|
110
|
+
if (result.status) {
|
|
111
|
+
// Update configuration with response
|
|
112
|
+
setIDMConf({
|
|
113
|
+
...idmConf,
|
|
114
|
+
requestConfiguration: result,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Navigate to next step based on metadata
|
|
118
|
+
handleNextStep();
|
|
119
|
+
} else {
|
|
120
|
+
// Handle error - navigate to retake screen
|
|
121
|
+
navigation.navigate('RetakeSelfie' as never, {
|
|
122
|
+
errorMessage: result.errorMessage || 'Failed to process document',
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
} catch (error: any) {
|
|
26
126
|
console.error('Error taking photo:', error);
|
|
127
|
+
Alert.alert('Error', 'Failed to capture or process the image.');
|
|
128
|
+
} finally {
|
|
27
129
|
setLoading(false);
|
|
28
130
|
}
|
|
29
131
|
};
|
|
30
132
|
|
|
133
|
+
// Render camera if available
|
|
134
|
+
const renderCamera = () => {
|
|
135
|
+
try {
|
|
136
|
+
const { Camera } = require('react-native-vision-camera');
|
|
137
|
+
|
|
138
|
+
if (hasPermission && device) {
|
|
139
|
+
return (
|
|
140
|
+
<Camera
|
|
141
|
+
ref={cameraRef}
|
|
142
|
+
style={StyleSheet.absoluteFill}
|
|
143
|
+
device={device}
|
|
144
|
+
isActive={true}
|
|
145
|
+
photo={true}
|
|
146
|
+
/>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
} catch (error) {
|
|
150
|
+
console.warn('Camera render failed:', error);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Fallback placeholder
|
|
154
|
+
return (
|
|
155
|
+
<View style={[StyleSheet.absoluteFill, { backgroundColor: '#000' }]}>
|
|
156
|
+
<Text style={{ color: '#fff', textAlign: 'center', marginTop: 100 }}>
|
|
157
|
+
Camera Preview
|
|
158
|
+
</Text>
|
|
159
|
+
</View>
|
|
160
|
+
);
|
|
161
|
+
};
|
|
162
|
+
|
|
31
163
|
return (
|
|
32
164
|
<View style={styles.container}>
|
|
33
165
|
<Text style={styles.topLabel}>Document Front Side</Text>
|
|
34
166
|
|
|
35
|
-
{
|
|
36
|
-
<View style={StyleSheet.absoluteFill}>
|
|
37
|
-
<View
|
|
38
|
-
style={[StyleSheet.absoluteFill, { backgroundColor: '#000' }]}
|
|
39
|
-
></View>
|
|
40
|
-
</View>
|
|
167
|
+
{renderCamera()}
|
|
41
168
|
|
|
42
|
-
{/* Card overlay
|
|
43
|
-
<
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
borderWidth: 2,
|
|
48
|
-
borderColor: '#fff',
|
|
49
|
-
backgroundColor: 'transparent',
|
|
50
|
-
},
|
|
51
|
-
]}
|
|
169
|
+
{/* Card overlay */}
|
|
170
|
+
<Image
|
|
171
|
+
source={require('../../assets/images/card-overlay.png')}
|
|
172
|
+
style={styles.cardoverlay}
|
|
173
|
+
resizeMode="cover"
|
|
52
174
|
/>
|
|
53
175
|
|
|
54
176
|
<Text style={styles.bottomLabel}>
|
|
@@ -1,36 +1,88 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
2
|
-
import { View, Text, StyleSheet } from 'react-native';
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { View, Text, StyleSheet, Alert } from 'react-native';
|
|
3
3
|
import { useNavigation } from '@react-navigation/native';
|
|
4
4
|
import { useTheme } from '../context/ThemeContext';
|
|
5
5
|
import { useOrientation } from '../hooks/useOrientation';
|
|
6
|
+
import { useIDM } from '../context/IDMConfigurationContext';
|
|
6
7
|
import getStyles from '../styles/ScannerStyles';
|
|
7
8
|
import Loader from '../components/common/Loader';
|
|
9
|
+
import { captureMRZ } from '../apis';
|
|
10
|
+
import { createFlowManager } from '../utils/flowManager';
|
|
8
11
|
|
|
9
12
|
export default function MrzCapture() {
|
|
10
13
|
const { theme } = useTheme();
|
|
14
|
+
const { idmConf, setIDMConf } = useIDM();
|
|
11
15
|
const orientation = useOrientation();
|
|
12
16
|
const styles = getStyles(theme, orientation);
|
|
13
17
|
const navigation = useNavigation();
|
|
14
18
|
const [loading, setLoading] = useState(false);
|
|
19
|
+
const [scannedData, setScannedData] = useState<string | null>(null);
|
|
15
20
|
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
const
|
|
21
|
+
// Get MRZ engine type from metadata
|
|
22
|
+
const getMRZEngine = () => {
|
|
23
|
+
const metadata = idmConf?.selectedCountryDetails?.selectedMetaData;
|
|
24
|
+
if (!metadata) return 'TD3';
|
|
25
|
+
|
|
26
|
+
const flowManager = createFlowManager(metadata);
|
|
27
|
+
return flowManager.getMRZEngine();
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Handle MRZ scan
|
|
31
|
+
const handleMRZScan = async (mrzCode: string) => {
|
|
32
|
+
if (loading || !mrzCode) return;
|
|
33
|
+
|
|
34
|
+
try {
|
|
19
35
|
setLoading(true);
|
|
20
|
-
|
|
21
|
-
|
|
36
|
+
|
|
37
|
+
const mrzData = {
|
|
38
|
+
token: String(idmConf.verificationCode || ''),
|
|
39
|
+
code: mrzCode,
|
|
40
|
+
engine: getMRZEngine(),
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Call API to submit MRZ
|
|
44
|
+
const result = await captureMRZ(idmConf, mrzData);
|
|
45
|
+
|
|
46
|
+
if (result.status) {
|
|
47
|
+
// Update configuration with response
|
|
48
|
+
setIDMConf({
|
|
49
|
+
...idmConf,
|
|
50
|
+
requestConfiguration: { ...idmConf.requestConfiguration, ...result },
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Navigate to ThankYou
|
|
22
54
|
navigation.navigate('ThankYou' as never);
|
|
23
|
-
}
|
|
55
|
+
} else {
|
|
56
|
+
Alert.alert('Error', result.errorMessage || 'Failed to process MRZ');
|
|
57
|
+
setLoading(false);
|
|
58
|
+
}
|
|
59
|
+
} catch (error: any) {
|
|
60
|
+
console.error('Error processing MRZ:', error);
|
|
61
|
+
Alert.alert('Error', 'Failed to process MRZ');
|
|
62
|
+
setLoading(false);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// Simulate MRZ scanning (replace with actual MRZ scanner library)
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
// TODO: Integrate actual MRZ scanning library (e.g., @react-native-ml-kit/text-recognition)
|
|
69
|
+
// For now, simulate scanning after 3 seconds
|
|
70
|
+
const timer = setTimeout(() => {
|
|
71
|
+
const simulatedMRZ =
|
|
72
|
+
'P<UTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<<\nL898902C36UTO7408122F1204159ZE184226B<<<<<10';
|
|
73
|
+
setScannedData(simulatedMRZ);
|
|
74
|
+
handleMRZScan(simulatedMRZ);
|
|
24
75
|
}, 3000);
|
|
25
76
|
|
|
26
77
|
return () => clearTimeout(timer);
|
|
27
|
-
|
|
78
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
79
|
+
}, []);
|
|
28
80
|
|
|
29
81
|
return (
|
|
30
82
|
<View style={styles.container}>
|
|
31
83
|
<Text style={styles.topLabel}>Align the MRZ code in the box</Text>
|
|
32
84
|
|
|
33
|
-
{/* Camera placeholder */}
|
|
85
|
+
{/* Camera placeholder - Replace with actual camera view */}
|
|
34
86
|
<View style={[StyleSheet.absoluteFill, { backgroundColor: '#000' }]}>
|
|
35
87
|
<Text
|
|
36
88
|
style={{
|
|
@@ -40,8 +92,21 @@ export default function MrzCapture() {
|
|
|
40
92
|
fontSize: 16,
|
|
41
93
|
}}
|
|
42
94
|
>
|
|
43
|
-
MRZ
|
|
95
|
+
{loading ? 'Processing...' : `Scanning MRZ (${getMRZEngine()})...`}
|
|
44
96
|
</Text>
|
|
97
|
+
{scannedData && (
|
|
98
|
+
<Text
|
|
99
|
+
style={{
|
|
100
|
+
color: '#0f0',
|
|
101
|
+
textAlign: 'center',
|
|
102
|
+
marginTop: 20,
|
|
103
|
+
fontSize: 10,
|
|
104
|
+
paddingHorizontal: 20,
|
|
105
|
+
}}
|
|
106
|
+
>
|
|
107
|
+
Detected MRZ
|
|
108
|
+
</Text>
|
|
109
|
+
)}
|
|
45
110
|
</View>
|
|
46
111
|
|
|
47
112
|
{/* MRZ overlay box */}
|
|
@@ -53,7 +118,7 @@ export default function MrzCapture() {
|
|
|
53
118
|
width: 380,
|
|
54
119
|
height: 78,
|
|
55
120
|
borderWidth: 2,
|
|
56
|
-
borderColor: '#00ff00',
|
|
121
|
+
borderColor: scannedData ? '#00ff00' : '#ffffff',
|
|
57
122
|
backgroundColor: 'transparent',
|
|
58
123
|
}}
|
|
59
124
|
/>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useNavigation } from '@react-navigation/native';
|
|
2
|
-
import React, { useState, useMemo, useCallback } from 'react';
|
|
2
|
+
import React, { useState, useMemo, useCallback, useEffect } from 'react';
|
|
3
3
|
import {
|
|
4
4
|
Alert,
|
|
5
5
|
Pressable,
|
|
@@ -15,45 +15,7 @@ import { useTheme } from '../context/ThemeContext';
|
|
|
15
15
|
import { useOrientation } from '../hooks/useOrientation';
|
|
16
16
|
import { useIDM } from '../context/IDMConfigurationContext';
|
|
17
17
|
import { useKeyboard } from '../context/KeyboardContext';
|
|
18
|
-
|
|
19
|
-
// Static country data for demo
|
|
20
|
-
const STATIC_COUNTRIES = [
|
|
21
|
-
{
|
|
22
|
-
label: 'United States',
|
|
23
|
-
value: 'US',
|
|
24
|
-
metadata: [
|
|
25
|
-
{ type: 'DL', id: '1' },
|
|
26
|
-
{ type: 'PP', id: '2' },
|
|
27
|
-
{ type: 'NI', id: '3' },
|
|
28
|
-
],
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
label: 'Canada',
|
|
32
|
-
value: 'CA',
|
|
33
|
-
metadata: [
|
|
34
|
-
{ type: 'DL', id: '4' },
|
|
35
|
-
{ type: 'PP', id: '5' },
|
|
36
|
-
],
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
label: 'United Kingdom',
|
|
40
|
-
value: 'GB',
|
|
41
|
-
metadata: [
|
|
42
|
-
{ type: 'DL', id: '6' },
|
|
43
|
-
{ type: 'PP', id: '7' },
|
|
44
|
-
{ type: 'NI', id: '8' },
|
|
45
|
-
],
|
|
46
|
-
},
|
|
47
|
-
];
|
|
48
|
-
|
|
49
|
-
// Document type labels mapping
|
|
50
|
-
const DOCUMENT_LABELS: Record<string, string> = {
|
|
51
|
-
DL: "Driver's License",
|
|
52
|
-
PP: 'Passport',
|
|
53
|
-
NI: 'National ID',
|
|
54
|
-
PC: 'Passport Card',
|
|
55
|
-
GC: 'Green Card',
|
|
56
|
-
};
|
|
18
|
+
import { getDocumentLabel } from '../utils';
|
|
57
19
|
|
|
58
20
|
export default function SelectDocuments() {
|
|
59
21
|
const { theme } = useTheme();
|
|
@@ -62,27 +24,38 @@ export default function SelectDocuments() {
|
|
|
62
24
|
const orientation = useOrientation();
|
|
63
25
|
const styles = getStyles(theme, orientation);
|
|
64
26
|
const navigation = useNavigation();
|
|
65
|
-
|
|
27
|
+
|
|
28
|
+
const [filteredCountries, setFilteredCountries] = useState<any[]>([]);
|
|
66
29
|
const [search, setSearch] = useState('');
|
|
67
30
|
const [selectedCountry, setSelectedCountry] = useState<string | null>(null);
|
|
68
31
|
const [dropdownVisible, setDropdownVisible] = useState(false);
|
|
69
32
|
const [selectedIdType, setSelectedIdType] = useState<string | null>(null);
|
|
70
33
|
const [idTypeDropdownVisible, setIdTypeDropdownVisible] = useState(false);
|
|
71
34
|
|
|
35
|
+
// Initialize filtered countries from context
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (idmConf.countryDetails) {
|
|
38
|
+
setFilteredCountries(idmConf.countryDetails);
|
|
39
|
+
}
|
|
40
|
+
}, [idmConf.countryDetails]);
|
|
41
|
+
|
|
72
42
|
// Memoize ID card types based on selected country
|
|
73
43
|
const idCardTypes = useMemo(() => {
|
|
74
|
-
if (!selectedCountry) return [];
|
|
75
|
-
|
|
44
|
+
if (!selectedCountry || !idmConf.countryDetails) return [];
|
|
45
|
+
|
|
46
|
+
const country = idmConf.countryDetails.find((c) => String(c.value) === String(selectedCountry));
|
|
76
47
|
return country?.metadata.map((doc) => ({
|
|
77
|
-
label:
|
|
48
|
+
label: getDocumentLabel(doc.type),
|
|
78
49
|
value: doc.type,
|
|
50
|
+
metadata: doc,
|
|
79
51
|
})) || [];
|
|
80
|
-
}, [selectedCountry]);
|
|
52
|
+
}, [selectedCountry, idmConf.countryDetails]);
|
|
81
53
|
|
|
82
54
|
// Memoize selected country label
|
|
83
55
|
const selectedCountryLabel = useMemo(() => {
|
|
84
|
-
|
|
85
|
-
|
|
56
|
+
if (!idmConf.countryDetails) return null;
|
|
57
|
+
return idmConf.countryDetails.find((c) => String(c.value) === String(selectedCountry))?.label;
|
|
58
|
+
}, [selectedCountry, idmConf.countryDetails]);
|
|
86
59
|
|
|
87
60
|
// Memoize selected ID type label
|
|
88
61
|
const selectedIdTypeLabel = useMemo(() => {
|
|
@@ -104,23 +77,29 @@ export default function SelectDocuments() {
|
|
|
104
77
|
setDropdownVisible(false);
|
|
105
78
|
setSearch('');
|
|
106
79
|
setSelectedIdType(null);
|
|
107
|
-
|
|
80
|
+
|
|
81
|
+
if (idmConf.countryDetails) {
|
|
82
|
+
setFilteredCountries(idmConf.countryDetails);
|
|
83
|
+
}
|
|
108
84
|
|
|
109
|
-
const selectedCountryData =
|
|
85
|
+
const selectedCountryData = idmConf.countryDetails?.find((c) => String(c.value) === String(value));
|
|
110
86
|
updateIDMConf({ selectedCountryDetails: selectedCountryData || null });
|
|
111
|
-
}, [updateIDMConf]);
|
|
87
|
+
}, [idmConf.countryDetails, updateIDMConf]);
|
|
112
88
|
|
|
113
89
|
const selectIdType = useCallback((value: string) => {
|
|
114
90
|
setSelectedIdType(value);
|
|
115
|
-
|
|
91
|
+
|
|
92
|
+
const selectedCountryData = idmConf.countryDetails?.find((c) => String(c.value) === String(selectedCountry));
|
|
93
|
+
const selectedMetaData = selectedCountryData?.metadata?.find((c) => c.type === value);
|
|
94
|
+
|
|
116
95
|
updateIDMConf({
|
|
117
96
|
selectedCountryDetails: {
|
|
118
97
|
...selectedCountryData,
|
|
119
|
-
selectedMetaData
|
|
98
|
+
selectedMetaData,
|
|
120
99
|
},
|
|
121
100
|
});
|
|
122
101
|
setIdTypeDropdownVisible(false);
|
|
123
|
-
}, [selectedCountry, updateIDMConf]);
|
|
102
|
+
}, [selectedCountry, idmConf.countryDetails, updateIDMConf]);
|
|
124
103
|
|
|
125
104
|
const handleContinue = useCallback(() => {
|
|
126
105
|
if (!selectedCountry || !selectedIdType) {
|
|
@@ -132,11 +111,13 @@ export default function SelectDocuments() {
|
|
|
132
111
|
|
|
133
112
|
const handleSearch = useCallback((text: string) => {
|
|
134
113
|
setSearch(text);
|
|
135
|
-
|
|
114
|
+
if (!idmConf.countryDetails) return;
|
|
115
|
+
|
|
116
|
+
const filtered = idmConf.countryDetails.filter((c) =>
|
|
136
117
|
c.label.toLowerCase().includes(text.toLowerCase())
|
|
137
118
|
);
|
|
138
119
|
setFilteredCountries(filtered);
|
|
139
|
-
}, []);
|
|
120
|
+
}, [idmConf.countryDetails]);
|
|
140
121
|
|
|
141
122
|
const closeDropdowns = useCallback(() => {
|
|
142
123
|
setDropdownVisible(false);
|
|
@@ -179,7 +160,7 @@ export default function SelectDocuments() {
|
|
|
179
160
|
{filteredCountries.map((item) => (
|
|
180
161
|
<Pressable
|
|
181
162
|
key={item.value}
|
|
182
|
-
onPress={() => selectCountry(item.value)}
|
|
163
|
+
onPress={() => selectCountry(String(item.value))}
|
|
183
164
|
style={styles.dropdownItem}
|
|
184
165
|
>
|
|
185
166
|
<Text style={styles.dopDownLable}>{item.label}</Text>
|
|
@@ -1,38 +1,134 @@
|
|
|
1
|
-
import React, { useState, useCallback } from 'react';
|
|
1
|
+
import React, { useState, useCallback, useEffect, useRef } from 'react';
|
|
2
2
|
import { useNavigation } from '@react-navigation/native';
|
|
3
|
-
import { TouchableOpacity, View, StyleSheet, Text } from 'react-native';
|
|
3
|
+
import { TouchableOpacity, View, StyleSheet, Text, Alert } from 'react-native';
|
|
4
4
|
import getStyles from '../styles/SelfieCaptureStyles';
|
|
5
5
|
import { useTheme } from '../context/ThemeContext';
|
|
6
6
|
import { useOrientation } from '../hooks/useOrientation';
|
|
7
|
+
import { useIDM } from '../context/IDMConfigurationContext';
|
|
7
8
|
import Loader from '../components/common/Loader';
|
|
9
|
+
import { processImageToBase64, validateImagePath } from '../utils/imageProcessor';
|
|
8
10
|
|
|
9
11
|
export default function SelfieCapture() {
|
|
10
12
|
const { theme } = useTheme();
|
|
13
|
+
const { idmConf, setIDMConf } = useIDM();
|
|
11
14
|
const orientation = useOrientation();
|
|
12
15
|
const styles = getStyles(theme, orientation);
|
|
13
16
|
const navigation = useNavigation();
|
|
14
17
|
const [loading, setLoading] = useState(false);
|
|
18
|
+
const [hasPermission, setHasPermission] = useState(false);
|
|
19
|
+
const [device, setDevice] = useState<any>(null);
|
|
20
|
+
const cameraRef = useRef<any>(null);
|
|
15
21
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
// Initialize camera
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
const initializeCamera = async () => {
|
|
25
|
+
try {
|
|
26
|
+
const { Camera, useCameraDevice, useCameraPermission } = require('react-native-vision-camera');
|
|
27
|
+
|
|
28
|
+
const { hasPermission: hasPerm, requestPermission } = useCameraPermission();
|
|
29
|
+
|
|
30
|
+
if (!hasPerm) {
|
|
31
|
+
const granted = await requestPermission();
|
|
32
|
+
if (!granted) {
|
|
33
|
+
navigation.navigate('CameraPermission' as never);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
setHasPermission(true);
|
|
39
|
+
|
|
40
|
+
// Get front camera for selfie
|
|
41
|
+
const frontDevice = useCameraDevice('front');
|
|
42
|
+
if (!frontDevice) {
|
|
43
|
+
navigation.navigate('NoCameraFound' as never);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
setDevice(frontDevice);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.warn('Camera initialization failed:', error);
|
|
50
|
+
setHasPermission(true);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
initializeCamera();
|
|
24
55
|
}, [navigation]);
|
|
25
56
|
|
|
57
|
+
const takePhoto = useCallback(async () => {
|
|
58
|
+
if (!cameraRef.current) {
|
|
59
|
+
// Fallback for testing without camera
|
|
60
|
+
Alert.alert('Camera Not Available', 'Camera is not available. Proceeding to next step.');
|
|
61
|
+
navigation.navigate('SelectDocuments' as never);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
setLoading(true);
|
|
67
|
+
|
|
68
|
+
const photo = await cameraRef.current.takePhoto();
|
|
69
|
+
|
|
70
|
+
if (!validateImagePath(photo?.path)) {
|
|
71
|
+
Alert.alert('Error', 'Failed to capture selfie');
|
|
72
|
+
setLoading(false);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Process image to base64
|
|
77
|
+
const base64Image = await processImageToBase64(photo.path);
|
|
78
|
+
|
|
79
|
+
// TODO: Integrate with liveness detection API
|
|
80
|
+
// For now, just store the selfie and proceed
|
|
81
|
+
setIDMConf({
|
|
82
|
+
...idmConf,
|
|
83
|
+
userDetails: {
|
|
84
|
+
...idmConf.userDetails,
|
|
85
|
+
selfieImage: base64Image,
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Navigate to document selection
|
|
90
|
+
navigation.navigate('SelectDocuments' as never);
|
|
91
|
+
} catch (error: any) {
|
|
92
|
+
console.error('Error taking selfie:', error);
|
|
93
|
+
Alert.alert('Error', 'Failed to capture selfie');
|
|
94
|
+
} finally {
|
|
95
|
+
setLoading(false);
|
|
96
|
+
}
|
|
97
|
+
}, [idmConf, setIDMConf, navigation]);
|
|
98
|
+
|
|
99
|
+
// Render camera if available
|
|
100
|
+
const renderCamera = () => {
|
|
101
|
+
try {
|
|
102
|
+
const { Camera } = require('react-native-vision-camera');
|
|
103
|
+
|
|
104
|
+
if (hasPermission && device) {
|
|
105
|
+
return (
|
|
106
|
+
<Camera
|
|
107
|
+
ref={cameraRef}
|
|
108
|
+
style={StyleSheet.absoluteFill}
|
|
109
|
+
device={device}
|
|
110
|
+
isActive={true}
|
|
111
|
+
photo={true}
|
|
112
|
+
/>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
} catch (error) {
|
|
116
|
+
console.warn('Camera render failed:', error);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Fallback placeholder
|
|
120
|
+
return (
|
|
121
|
+
<View style={[StyleSheet.absoluteFill, { backgroundColor: '#000' }]}>
|
|
122
|
+
<Text style={[styles.text, { color: '#fff', marginTop: 100 }]}>
|
|
123
|
+
Camera Preview
|
|
124
|
+
</Text>
|
|
125
|
+
</View>
|
|
126
|
+
);
|
|
127
|
+
};
|
|
128
|
+
|
|
26
129
|
return (
|
|
27
130
|
<View style={styles.container}>
|
|
28
|
-
{
|
|
29
|
-
<View style={StyleSheet.absoluteFill}>
|
|
30
|
-
<View style={[StyleSheet.absoluteFill, { backgroundColor: '#000' }]}>
|
|
31
|
-
<Text style={[styles.text, { color: '#fff', marginTop: 100 }]}>
|
|
32
|
-
Camera View (Static)
|
|
33
|
-
</Text>
|
|
34
|
-
</View>
|
|
35
|
-
</View>
|
|
131
|
+
{renderCamera()}
|
|
36
132
|
|
|
37
133
|
{/* Camera overlay - Add your overlay image to assets/images/photo-overlay.png */}
|
|
38
134
|
{/* <Image
|