@test-web/react-native-sdk 1.0.0 → 2.0.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 +824 -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 +44 -8
- 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 +154 -24
- package/src/screens/DocumentCaptureBack.tsx +133 -24
- package/src/screens/DocumentCaptureFront.tsx +146 -24
- package/src/screens/MrzCapture.tsx +205 -16
- 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,36 +1,204 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
2
|
-
import { View, Text, StyleSheet } from 'react-native';
|
|
1
|
+
import React, { useState, useEffect, useRef } 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);
|
|
20
|
+
const [isScanning, setIsScanning] = useState(true);
|
|
21
|
+
const [hasPermission, setHasPermission] = useState(false);
|
|
22
|
+
const [device, setDevice] = useState<any>(null);
|
|
23
|
+
const cameraRef = useRef<any>(null);
|
|
24
|
+
const frameProcessorRef = useRef<any>(null);
|
|
15
25
|
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
const
|
|
26
|
+
// Get MRZ engine type from metadata
|
|
27
|
+
const getMRZEngine = () => {
|
|
28
|
+
const metadata = idmConf?.selectedCountryDetails?.selectedMetaData;
|
|
29
|
+
if (!metadata) return 'TD3';
|
|
30
|
+
|
|
31
|
+
const flowManager = createFlowManager(metadata);
|
|
32
|
+
return flowManager.getMRZEngine();
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Initialize camera with ML Kit
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
let isMounted = true;
|
|
38
|
+
|
|
39
|
+
const initializeCamera = async () => {
|
|
40
|
+
try {
|
|
41
|
+
const { Camera, useCameraDevice, useCameraPermission } = require('react-native-vision-camera');
|
|
42
|
+
|
|
43
|
+
const { hasPermission: hasPerm, requestPermission } = useCameraPermission();
|
|
44
|
+
|
|
45
|
+
if (!hasPerm) {
|
|
46
|
+
const granted = await requestPermission();
|
|
47
|
+
if (!granted) {
|
|
48
|
+
navigation.navigate('CameraPermission' as never);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (isMounted) {
|
|
54
|
+
setHasPermission(true);
|
|
55
|
+
|
|
56
|
+
const backDevice = useCameraDevice('back');
|
|
57
|
+
if (!backDevice) {
|
|
58
|
+
navigation.navigate('NoCameraFound' as never);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
setDevice(backDevice);
|
|
63
|
+
}
|
|
64
|
+
} catch (error) {
|
|
65
|
+
console.warn('Camera initialization failed:', error);
|
|
66
|
+
if (isMounted) {
|
|
67
|
+
simulateMRZScan();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
initializeCamera();
|
|
73
|
+
|
|
74
|
+
return () => {
|
|
75
|
+
isMounted = false;
|
|
76
|
+
setIsScanning(false);
|
|
77
|
+
};
|
|
78
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
79
|
+
}, []);
|
|
80
|
+
|
|
81
|
+
// Process MRZ with ML Kit Text Recognition
|
|
82
|
+
const processMRZWithMLKit = async (frame: any) => {
|
|
83
|
+
if (!isScanning) return;
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const { recognizeText } = require('@react-native-ml-kit/text-recognition');
|
|
87
|
+
|
|
88
|
+
const result = await recognizeText(frame);
|
|
89
|
+
|
|
90
|
+
if (result && result.text) {
|
|
91
|
+
// Check if text contains MRZ pattern
|
|
92
|
+
const lines = result.text.split('\n');
|
|
93
|
+
const mrzLines = lines.filter((line: string) =>
|
|
94
|
+
line.length >= 30 && /^[A-Z0-9<]+$/.test(line)
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
if (mrzLines.length >= 2) {
|
|
98
|
+
const mrzCode = mrzLines.join('\n');
|
|
99
|
+
handleMRZScanned(mrzCode);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
} catch (error) {
|
|
103
|
+
console.warn('ML Kit text recognition error:', error);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// Handle MRZ scan result
|
|
108
|
+
const handleMRZScanned = async (mrzCode: string) => {
|
|
109
|
+
if (loading || !mrzCode || !isScanning) return;
|
|
110
|
+
|
|
111
|
+
setIsScanning(false);
|
|
112
|
+
setScannedData(mrzCode);
|
|
113
|
+
|
|
114
|
+
try {
|
|
19
115
|
setLoading(true);
|
|
20
|
-
|
|
21
|
-
|
|
116
|
+
|
|
117
|
+
const mrzData = {
|
|
118
|
+
token: String(idmConf.verificationCode || ''),
|
|
119
|
+
code: mrzCode,
|
|
120
|
+
engine: getMRZEngine(),
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// Call API to submit MRZ
|
|
124
|
+
const result = await captureMRZ(idmConf, mrzData);
|
|
125
|
+
|
|
126
|
+
if (result.status) {
|
|
127
|
+
// Update configuration with response
|
|
128
|
+
setIDMConf({
|
|
129
|
+
...idmConf,
|
|
130
|
+
requestConfiguration: { ...idmConf.requestConfiguration, ...result },
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Navigate to ThankYou
|
|
22
134
|
navigation.navigate('ThankYou' as never);
|
|
23
|
-
}
|
|
135
|
+
} else {
|
|
136
|
+
Alert.alert('Error', result.errorMessage || 'Failed to process MRZ', [
|
|
137
|
+
{
|
|
138
|
+
text: 'Retry',
|
|
139
|
+
onPress: () => {
|
|
140
|
+
setIsScanning(true);
|
|
141
|
+
setScannedData(null);
|
|
142
|
+
setLoading(false);
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
]);
|
|
146
|
+
}
|
|
147
|
+
} catch (error: any) {
|
|
148
|
+
console.error('Error processing MRZ:', error);
|
|
149
|
+
Alert.alert('Error', 'Failed to process MRZ', [
|
|
150
|
+
{
|
|
151
|
+
text: 'Retry',
|
|
152
|
+
onPress: () => {
|
|
153
|
+
setIsScanning(true);
|
|
154
|
+
setScannedData(null);
|
|
155
|
+
setLoading(false);
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
]);
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// Fallback simulation if ML Kit not available
|
|
163
|
+
const simulateMRZScan = () => {
|
|
164
|
+
const timer = setTimeout(() => {
|
|
165
|
+
if (isScanning) {
|
|
166
|
+
const simulatedMRZ =
|
|
167
|
+
'P<UTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<<\nL898902C36UTO7408122F1204159ZE184226B<<<<<10';
|
|
168
|
+
handleMRZScanned(simulatedMRZ);
|
|
169
|
+
}
|
|
24
170
|
}, 3000);
|
|
25
171
|
|
|
26
172
|
return () => clearTimeout(timer);
|
|
27
|
-
}
|
|
173
|
+
};
|
|
28
174
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
175
|
+
// Render camera with frame processor
|
|
176
|
+
const renderCamera = () => {
|
|
177
|
+
try {
|
|
178
|
+
const { Camera } = require('react-native-vision-camera');
|
|
179
|
+
|
|
180
|
+
if (hasPermission && device) {
|
|
181
|
+
return (
|
|
182
|
+
<Camera
|
|
183
|
+
ref={cameraRef}
|
|
184
|
+
style={StyleSheet.absoluteFill}
|
|
185
|
+
device={device}
|
|
186
|
+
isActive={isScanning}
|
|
187
|
+
frameProcessor={(frame) => {
|
|
188
|
+
'worklet';
|
|
189
|
+
if (isScanning) {
|
|
190
|
+
processMRZWithMLKit(frame);
|
|
191
|
+
}
|
|
192
|
+
}}
|
|
193
|
+
/>
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
} catch (error) {
|
|
197
|
+
console.warn('Camera render failed:', error);
|
|
198
|
+
}
|
|
32
199
|
|
|
33
|
-
|
|
200
|
+
// Fallback UI
|
|
201
|
+
return (
|
|
34
202
|
<View style={[StyleSheet.absoluteFill, { backgroundColor: '#000' }]}>
|
|
35
203
|
<Text
|
|
36
204
|
style={{
|
|
@@ -40,9 +208,30 @@ export default function MrzCapture() {
|
|
|
40
208
|
fontSize: 16,
|
|
41
209
|
}}
|
|
42
210
|
>
|
|
43
|
-
MRZ
|
|
211
|
+
{loading ? 'Processing...' : `Scanning MRZ (${getMRZEngine()})...`}
|
|
44
212
|
</Text>
|
|
213
|
+
{scannedData && (
|
|
214
|
+
<Text
|
|
215
|
+
style={{
|
|
216
|
+
color: '#0f0',
|
|
217
|
+
textAlign: 'center',
|
|
218
|
+
marginTop: 20,
|
|
219
|
+
fontSize: 10,
|
|
220
|
+
paddingHorizontal: 20,
|
|
221
|
+
}}
|
|
222
|
+
>
|
|
223
|
+
Detected MRZ
|
|
224
|
+
</Text>
|
|
225
|
+
)}
|
|
45
226
|
</View>
|
|
227
|
+
);
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
return (
|
|
231
|
+
<View style={styles.container}>
|
|
232
|
+
<Text style={styles.topLabel}>Align the MRZ code in the box</Text>
|
|
233
|
+
|
|
234
|
+
{renderCamera()}
|
|
46
235
|
|
|
47
236
|
{/* MRZ overlay box */}
|
|
48
237
|
<View
|
|
@@ -53,12 +242,12 @@ export default function MrzCapture() {
|
|
|
53
242
|
width: 380,
|
|
54
243
|
height: 78,
|
|
55
244
|
borderWidth: 2,
|
|
56
|
-
borderColor: '#00ff00',
|
|
245
|
+
borderColor: scannedData ? '#00ff00' : '#ffffff',
|
|
57
246
|
backgroundColor: 'transparent',
|
|
58
247
|
}}
|
|
59
248
|
/>
|
|
60
249
|
|
|
61
|
-
{loading && <Loader />}
|
|
250
|
+
{loading && <Loader message="Processing MRZ..." />}
|
|
62
251
|
</View>
|
|
63
252
|
);
|
|
64
253
|
}
|
|
@@ -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
|
package/src/screens/ThankYou.tsx
CHANGED
|
@@ -1,14 +1,46 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
2
|
import { View } from 'react-native';
|
|
3
3
|
import ThemedText from '../components/ui/ThemedText';
|
|
4
4
|
import getStyles from '../styles/ThankYouStyles';
|
|
5
5
|
import { useTheme } from '../context/ThemeContext';
|
|
6
6
|
import { useOrientation } from '../hooks/useOrientation';
|
|
7
|
+
import { useIDM } from '../context/IDMConfigurationContext';
|
|
8
|
+
import Loader from '../components/common/Loader';
|
|
9
|
+
import { completeVerification } from '../apis';
|
|
7
10
|
|
|
8
11
|
export default function ThankYou() {
|
|
9
12
|
const { theme } = useTheme();
|
|
13
|
+
const { idmConf } = useIDM();
|
|
10
14
|
const orientation = useOrientation();
|
|
11
15
|
const styles = getStyles(theme, orientation);
|
|
16
|
+
const [loading, setLoading] = useState(false);
|
|
17
|
+
const [completed, setCompleted] = useState(false);
|
|
18
|
+
|
|
19
|
+
// Call complete verification API
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
const finalizeVerification = async () => {
|
|
22
|
+
if (completed || !idmConf.verificationCode) return;
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
setLoading(true);
|
|
26
|
+
const result = await completeVerification(idmConf);
|
|
27
|
+
console.log('Verification completed:', result);
|
|
28
|
+
setCompleted(true);
|
|
29
|
+
} catch (error: any) {
|
|
30
|
+
console.error('Error completing verification:', error);
|
|
31
|
+
// Don't show error to user, just log it
|
|
32
|
+
} finally {
|
|
33
|
+
setLoading(false);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Call after a short delay to ensure all data is submitted
|
|
38
|
+
const timer = setTimeout(() => {
|
|
39
|
+
finalizeVerification();
|
|
40
|
+
}, 1000);
|
|
41
|
+
|
|
42
|
+
return () => clearTimeout(timer);
|
|
43
|
+
}, [idmConf, completed]);
|
|
12
44
|
|
|
13
45
|
return (
|
|
14
46
|
<View style={styles.container}>
|
|
@@ -18,6 +50,7 @@ export default function ThankYou() {
|
|
|
18
50
|
<ThemedText style={styles.subtitle} type="title">
|
|
19
51
|
Your information{'\n'}has been uploaded
|
|
20
52
|
</ThemedText>
|
|
53
|
+
{loading && <Loader message="Finalizing verification..." />}
|
|
21
54
|
</View>
|
|
22
55
|
);
|
|
23
56
|
}
|