@sanctum-key/react-native-sdk 1.0.49 → 1.0.50
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/build/package.json +1 -1
- package/build/src/components/KYCElements/CountrySelection.d.ts.map +1 -1
- package/build/src/components/KYCElements/CountrySelection.js +33 -108
- package/build/src/components/KYCElements/CountrySelection.js.map +1 -1
- package/build/src/components/KYCElements/CountrySelectionTemplate.d.ts.map +1 -1
- package/build/src/components/KYCElements/CountrySelectionTemplate.js +50 -164
- package/build/src/components/KYCElements/CountrySelectionTemplate.js.map +1 -1
- package/build/src/components/KYCElements/DocumentTypeSelection.d.ts.map +1 -1
- package/build/src/components/KYCElements/DocumentTypeSelection.js +32 -109
- package/build/src/components/KYCElements/DocumentTypeSelection.js.map +1 -1
- package/build/src/components/KYCElements/IDCardCapture.d.ts.map +1 -1
- package/build/src/components/KYCElements/IDCardCapture.js +58 -25
- package/build/src/components/KYCElements/IDCardCapture.js.map +1 -1
- package/build/src/components/KYCElements/InitializationStep.d.ts.map +1 -1
- package/build/src/components/KYCElements/InitializationStep.js +13 -1
- package/build/src/components/KYCElements/InitializationStep.js.map +1 -1
- package/build/src/components/KYCElements/LocationCaptureTemplate.d.ts.map +1 -1
- package/build/src/components/KYCElements/LocationCaptureTemplate.js +46 -157
- package/build/src/components/KYCElements/LocationCaptureTemplate.js.map +1 -1
- package/build/src/components/KYCElements/SelfieCapture.d.ts.map +1 -1
- package/build/src/components/KYCElements/SelfieCapture.js +28 -24
- package/build/src/components/KYCElements/SelfieCapture.js.map +1 -1
- package/build/src/components/KYCElements/VerificationProgressTemplate.d.ts.map +1 -1
- package/build/src/components/KYCElements/VerificationProgressTemplate.js +0 -4
- package/build/src/components/KYCElements/VerificationProgressTemplate.js.map +1 -1
- package/build/src/components/SelfieCapture.d.ts.map +1 -1
- package/build/src/components/SelfieCapture.js +33 -90
- package/build/src/components/SelfieCapture.js.map +1 -1
- package/build/src/components/TemplateKYCExample.d.ts.map +1 -1
- package/build/src/components/TemplateKYCExample.js +69 -120
- package/build/src/components/TemplateKYCExample.js.map +1 -1
- package/build/src/components/TemplateKYCFlowRefactored.d.ts +0 -2
- package/build/src/components/TemplateKYCFlowRefactored.d.ts.map +1 -1
- package/build/src/components/TemplateKYCFlowRefactored.js +95 -210
- package/build/src/components/TemplateKYCFlowRefactored.js.map +1 -1
- package/build/src/hooks/useTemplateKYCFlow.d.ts.map +1 -1
- package/build/src/hooks/useTemplateKYCFlow.js.map +1 -1
- package/build/src/types/KYC.types.d.ts.map +1 -1
- package/build/src/types/KYC.types.js.map +1 -1
- package/build/src/utils/logger.d.ts +12 -4
- package/build/src/utils/logger.d.ts.map +1 -1
- package/build/src/utils/logger.js +156 -28
- package/build/src/utils/logger.js.map +1 -1
- package/package.json +1 -1
- package/src/components/KYCElements/CountrySelection.tsx +36 -109
- package/src/components/KYCElements/CountrySelectionTemplate.tsx +67 -266
- package/src/components/KYCElements/DocumentTypeSelection.tsx +34 -110
- package/src/components/KYCElements/IDCardCapture.tsx +93 -39
- package/src/components/KYCElements/InitializationStep.tsx +17 -2
- package/src/components/KYCElements/LocationCaptureTemplate.tsx +53 -170
- package/src/components/KYCElements/SelfieCapture.tsx +50 -58
- package/src/components/KYCElements/VerificationProgressTemplate.tsx +1 -4
- package/src/components/SelfieCapture.tsx +44 -100
- package/src/components/TemplateKYCExample.tsx +94 -145
- package/src/components/TemplateKYCFlowRefactored.tsx +105 -239
- package/src/hooks/useTemplateKYCFlow.tsx +1 -0
- package/src/types/KYC.types.ts +0 -1
- package/src/utils/logger.ts +182 -29
|
@@ -8,6 +8,7 @@ import * as Location from 'expo-location';
|
|
|
8
8
|
import { useTemplateKYCFlowContext } from '../../hooks/useTemplateKYCFlow';
|
|
9
9
|
import { useI18n } from '../../hooks/useI18n';
|
|
10
10
|
import { showAlert } from '../../utils/platformAlert';
|
|
11
|
+
import { sdkLogger } from '../../utils/logger'; // 🚨 IMPORT SDK LOGGER
|
|
11
12
|
|
|
12
13
|
interface LocationCaptureTemplateProps {
|
|
13
14
|
component: TemplateComponent;
|
|
@@ -27,17 +28,21 @@ export const LocationCaptureTemplate: React.FC<LocationCaptureTemplateProps> = (
|
|
|
27
28
|
const { t } = useI18n();
|
|
28
29
|
const [isRequesting, setIsRequesting] = useState(false);
|
|
29
30
|
const [currentLocation, setCurrentLocation] = useState<{ latitude: number; longitude: number; accuracy: number } | null>(value || null);
|
|
30
|
-
const { actions } = useTemplateKYCFlowContext();
|
|
31
|
+
const { actions, state } = useTemplateKYCFlowContext();
|
|
31
32
|
const config = component.config as LocationConfig;
|
|
32
33
|
|
|
34
|
+
// 🚨 LOG: Track when the user reaches this step
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (state.session?.session_id) {
|
|
37
|
+
sdkLogger.info("User started location capture step");
|
|
38
|
+
}
|
|
39
|
+
}, [state.session?.session_id]);
|
|
40
|
+
|
|
33
41
|
const requestLocationPermission = async (): Promise<boolean> => {
|
|
34
42
|
try {
|
|
35
|
-
// On web, use browser's geolocation API directly
|
|
36
43
|
if (Platform.OS === 'web') {
|
|
37
44
|
return await requestWebGeolocationPermission();
|
|
38
45
|
}
|
|
39
|
-
|
|
40
|
-
// On native, use expo-location
|
|
41
46
|
const { status } = await Location.requestForegroundPermissionsAsync();
|
|
42
47
|
return status === 'granted';
|
|
43
48
|
} catch (err) {
|
|
@@ -48,39 +53,32 @@ export const LocationCaptureTemplate: React.FC<LocationCaptureTemplateProps> = (
|
|
|
48
53
|
|
|
49
54
|
const requestWebGeolocationPermission = async (): Promise<boolean> => {
|
|
50
55
|
if (!navigator.geolocation) {
|
|
51
|
-
console.error('Geolocation is not supported by this browser');
|
|
52
56
|
return false;
|
|
53
57
|
}
|
|
54
|
-
|
|
55
58
|
try {
|
|
56
|
-
// Check if permission API is available
|
|
57
59
|
if ('permissions' in navigator) {
|
|
58
60
|
const result = await navigator.permissions.query({ name: 'geolocation' as PermissionName });
|
|
59
61
|
return result.state === 'granted' || result.state === 'prompt';
|
|
60
62
|
}
|
|
61
|
-
|
|
62
|
-
// Fallback: assume we can request permission
|
|
63
63
|
return true;
|
|
64
64
|
} catch (err) {
|
|
65
|
-
console.warn('Could not check geolocation permission:', err);
|
|
66
|
-
// Assume we can try to request
|
|
67
65
|
return true;
|
|
68
66
|
}
|
|
69
67
|
};
|
|
70
68
|
|
|
71
69
|
const getCurrentLocation = async () => {
|
|
72
70
|
try {
|
|
71
|
+
sdkLogger.info("User initiated location request"); // 🚨 LOG
|
|
73
72
|
setIsRequesting(true);
|
|
74
73
|
|
|
75
|
-
// Web platform: use browser geolocation API
|
|
76
74
|
if (Platform.OS === 'web') {
|
|
77
75
|
await getWebGeolocation();
|
|
78
76
|
return;
|
|
79
77
|
}
|
|
80
78
|
|
|
81
|
-
// Native platform: use expo-location
|
|
82
79
|
const hasLocationPermission = await requestLocationPermission();
|
|
83
80
|
if (!hasLocationPermission) {
|
|
81
|
+
sdkLogger.warn("Location permission denied by user"); // 🚨 LOG
|
|
84
82
|
showAlert(
|
|
85
83
|
t('kyc.locationCapture.permissionDeniedTitle'),
|
|
86
84
|
t('kyc.locationCapture.permissionDeniedMessage')
|
|
@@ -89,9 +87,9 @@ export const LocationCaptureTemplate: React.FC<LocationCaptureTemplateProps> = (
|
|
|
89
87
|
return;
|
|
90
88
|
}
|
|
91
89
|
|
|
92
|
-
// Vérifier si la localisation est activée
|
|
93
90
|
const isLocationEnabled = await Location.hasServicesEnabledAsync();
|
|
94
91
|
if (!isLocationEnabled) {
|
|
92
|
+
sdkLogger.warn("Location services are disabled on device"); // 🚨 LOG
|
|
95
93
|
showAlert(
|
|
96
94
|
t('kyc.locationCapture.locationDisabledTitle'),
|
|
97
95
|
t('kyc.locationCapture.locationDisabledMessage')
|
|
@@ -100,7 +98,6 @@ export const LocationCaptureTemplate: React.FC<LocationCaptureTemplateProps> = (
|
|
|
100
98
|
return;
|
|
101
99
|
}
|
|
102
100
|
|
|
103
|
-
// Récupérer la localisation actuelle
|
|
104
101
|
const location = await Location.getCurrentPositionAsync({
|
|
105
102
|
accuracy: config.accuracy === 'high' ? Location.Accuracy.High : Location.Accuracy.Balanced,
|
|
106
103
|
timeInterval: 5000,
|
|
@@ -113,12 +110,13 @@ export const LocationCaptureTemplate: React.FC<LocationCaptureTemplateProps> = (
|
|
|
113
110
|
accuracy: location.coords.accuracy || 50,
|
|
114
111
|
};
|
|
115
112
|
|
|
116
|
-
//
|
|
113
|
+
// 🚨 LOG: Success (coordinates omitted for privacy)
|
|
114
|
+
sdkLogger.info("Location successfully captured", { accuracy: locationData.accuracy, platform: 'native' });
|
|
115
|
+
|
|
117
116
|
setCurrentLocation(locationData);
|
|
118
117
|
onValueChange(locationData);
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
console.error('Erreur lors de la récupération de la localisation:', error);
|
|
118
|
+
} catch (error: any) {
|
|
119
|
+
sdkLogger.error("Failed to capture native location", { error: error?.message }); // 🚨 LOG
|
|
122
120
|
showAlert(t('kyc.locationCapture.errorTitle'), t('kyc.locationCapture.errorMessage'));
|
|
123
121
|
} finally {
|
|
124
122
|
setIsRequesting(false);
|
|
@@ -128,6 +126,7 @@ export const LocationCaptureTemplate: React.FC<LocationCaptureTemplateProps> = (
|
|
|
128
126
|
const getWebGeolocation = async () => {
|
|
129
127
|
return new Promise<void>((resolve, reject) => {
|
|
130
128
|
if (!navigator.geolocation) {
|
|
129
|
+
sdkLogger.error("Web Geolocation not supported by browser"); // 🚨 LOG
|
|
131
130
|
showAlert(
|
|
132
131
|
t('kyc.locationCapture.errorTitle'),
|
|
133
132
|
'Geolocation is not supported by your browser. Please use a modern browser or enable location services.'
|
|
@@ -149,19 +148,21 @@ export const LocationCaptureTemplate: React.FC<LocationCaptureTemplateProps> = (
|
|
|
149
148
|
longitude: position.coords.longitude,
|
|
150
149
|
accuracy: position.coords.accuracy,
|
|
151
150
|
};
|
|
151
|
+
|
|
152
|
+
// 🚨 LOG: Success (coordinates omitted for privacy)
|
|
153
|
+
sdkLogger.info("Location successfully captured", { accuracy: locationData.accuracy, platform: 'web' });
|
|
152
154
|
|
|
153
155
|
setCurrentLocation(locationData);
|
|
154
156
|
onValueChange(locationData);
|
|
155
157
|
resolve();
|
|
156
158
|
},
|
|
157
159
|
(error) => {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
+
sdkLogger.error("Web Geolocation error", { errorCode: error.code, errorMessage: error.message }); // 🚨 LOG
|
|
161
|
+
|
|
160
162
|
let errorMessage = t('kyc.locationCapture.errorMessage');
|
|
161
|
-
|
|
162
163
|
switch (error.code) {
|
|
163
164
|
case error.PERMISSION_DENIED:
|
|
164
|
-
errorMessage = Platform.OS === 'web'
|
|
165
|
+
errorMessage = Platform.OS === 'web'
|
|
165
166
|
? 'Location permission denied. Please enable location access in your browser settings and try again.'
|
|
166
167
|
: t('kyc.locationCapture.permissionDeniedMessage');
|
|
167
168
|
break;
|
|
@@ -172,7 +173,6 @@ export const LocationCaptureTemplate: React.FC<LocationCaptureTemplateProps> = (
|
|
|
172
173
|
errorMessage = 'Location request timed out. Please try again.';
|
|
173
174
|
break;
|
|
174
175
|
}
|
|
175
|
-
|
|
176
176
|
showAlert(t('kyc.locationCapture.errorTitle'), errorMessage);
|
|
177
177
|
reject(error);
|
|
178
178
|
},
|
|
@@ -181,25 +181,20 @@ export const LocationCaptureTemplate: React.FC<LocationCaptureTemplateProps> = (
|
|
|
181
181
|
});
|
|
182
182
|
};
|
|
183
183
|
|
|
184
|
-
// Passer automatiquement à l'étape suivante quand la localisation est obtenue
|
|
185
184
|
useEffect(() => {
|
|
186
185
|
if (currentLocation && !isRequesting) {
|
|
187
|
-
// Délai court pour permettre à l'utilisateur de voir la localisation obtenue
|
|
188
186
|
const timer = setTimeout(() => {
|
|
189
187
|
actions.nextComponent();
|
|
190
188
|
}, 1500);
|
|
191
|
-
|
|
192
189
|
return () => clearTimeout(timer);
|
|
193
190
|
}
|
|
194
191
|
return undefined;
|
|
195
192
|
}, [currentLocation, isRequesting, actions]);
|
|
196
193
|
|
|
197
|
-
|
|
198
|
-
|
|
199
194
|
return (
|
|
200
195
|
<View style={styles.container}>
|
|
201
196
|
<View style={{ flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }}>
|
|
202
|
-
<View style={{ position: 'relative', width: 70
|
|
197
|
+
<View style={{ position: 'relative', width: 70 }}>
|
|
203
198
|
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'center', width: 60, height: 60, backgroundColor: '#2DBE60', borderRadius: 100 }}>
|
|
204
199
|
<ShieldIcon />
|
|
205
200
|
</View>
|
|
@@ -207,13 +202,15 @@ export const LocationCaptureTemplate: React.FC<LocationCaptureTemplateProps> = (
|
|
|
207
202
|
<GpsIcon />
|
|
208
203
|
</View>
|
|
209
204
|
</View>
|
|
205
|
+
|
|
210
206
|
<View style={{ flexDirection: 'column', alignItems: 'center', justifyContent: 'center', marginTop: 36 }}>
|
|
211
207
|
<Text style={{ fontSize: 24, fontWeight: 'bold', color: '#333', marginBottom: 8 }}>{t('kyc.locationCapture.title')}</Text>
|
|
212
208
|
<Text style={{ fontSize: 16, color: '#666', textAlign: 'center', marginBottom: 24, lineHeight: 22 }}>
|
|
213
209
|
{t('kyc.locationCapture.description')}
|
|
214
210
|
</Text>
|
|
215
211
|
</View>
|
|
216
|
-
|
|
212
|
+
|
|
213
|
+
<View style={{ flexDirection: 'column', marginTop: 36, backgroundColor: "#F9FAFB", padding: 12, borderRadius: 12, width: '100%' }}>
|
|
217
214
|
<Text style={{ fontSize: 16, color: 'black', fontWeight: 'bold', textAlign: 'left', lineHeight: 22 }}>
|
|
218
215
|
{t('kyc.locationCapture.howThisHelps')}
|
|
219
216
|
</Text>
|
|
@@ -231,148 +228,34 @@ export const LocationCaptureTemplate: React.FC<LocationCaptureTemplateProps> = (
|
|
|
231
228
|
style={{ paddingVertical: 20, marginTop: 36 }}
|
|
232
229
|
/>
|
|
233
230
|
)}
|
|
234
|
-
|
|
235
|
-
<View style={{
|
|
236
|
-
height: 12,
|
|
237
|
-
}} />
|
|
231
|
+
<View style={{ height: 12 }} />
|
|
238
232
|
</View>
|
|
239
233
|
</View>
|
|
240
234
|
);
|
|
241
235
|
};
|
|
242
236
|
|
|
243
237
|
const styles = StyleSheet.create({
|
|
244
|
-
container: {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
},
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
},
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
},
|
|
269
|
-
locationContainer: {
|
|
270
|
-
flex: 1,
|
|
271
|
-
justifyContent: 'center',
|
|
272
|
-
alignItems: 'center',
|
|
273
|
-
padding: 24,
|
|
274
|
-
},
|
|
275
|
-
iconContainer: {
|
|
276
|
-
marginBottom: 24,
|
|
277
|
-
},
|
|
278
|
-
locationIcon: {
|
|
279
|
-
fontSize: 80,
|
|
280
|
-
},
|
|
281
|
-
locationRequest: {
|
|
282
|
-
alignItems: 'center',
|
|
283
|
-
width: '100%',
|
|
284
|
-
},
|
|
285
|
-
locationRequestText: {
|
|
286
|
-
fontSize: 16,
|
|
287
|
-
color: '#666',
|
|
288
|
-
textAlign: 'center',
|
|
289
|
-
marginBottom: 24,
|
|
290
|
-
},
|
|
291
|
-
locationButton: {
|
|
292
|
-
paddingHorizontal: 32,
|
|
293
|
-
paddingVertical: 16,
|
|
294
|
-
borderRadius: 8,
|
|
295
|
-
minWidth: 200,
|
|
296
|
-
alignItems: 'center',
|
|
297
|
-
},
|
|
298
|
-
locationButtonDisabled: {
|
|
299
|
-
opacity: 0.6,
|
|
300
|
-
},
|
|
301
|
-
locationButtonText: {
|
|
302
|
-
color: 'white',
|
|
303
|
-
fontWeight: '600',
|
|
304
|
-
fontSize: 16,
|
|
305
|
-
},
|
|
306
|
-
buttonContainer: {
|
|
307
|
-
flexDirection: 'row',
|
|
308
|
-
justifyContent: 'space-around',
|
|
309
|
-
alignItems: 'center',
|
|
310
|
-
marginTop: 24,
|
|
311
|
-
width: '100%',
|
|
312
|
-
},
|
|
313
|
-
primaryButton: {
|
|
314
|
-
paddingHorizontal: 32,
|
|
315
|
-
paddingVertical: 16,
|
|
316
|
-
borderRadius: 8,
|
|
317
|
-
minWidth: 120,
|
|
318
|
-
alignItems: 'center',
|
|
319
|
-
},
|
|
320
|
-
primaryButtonText: {
|
|
321
|
-
color: 'white',
|
|
322
|
-
fontWeight: '600',
|
|
323
|
-
fontSize: 16,
|
|
324
|
-
},
|
|
325
|
-
secondaryButton: {
|
|
326
|
-
backgroundColor: '#e5e5e5',
|
|
327
|
-
paddingHorizontal: 32,
|
|
328
|
-
paddingVertical: 16,
|
|
329
|
-
borderRadius: 8,
|
|
330
|
-
minWidth: 120,
|
|
331
|
-
alignItems: 'center',
|
|
332
|
-
},
|
|
333
|
-
secondaryButtonText: {
|
|
334
|
-
color: '#333',
|
|
335
|
-
fontWeight: '600',
|
|
336
|
-
fontSize: 16,
|
|
337
|
-
},
|
|
338
|
-
requiredContainer: {
|
|
339
|
-
backgroundColor: '#fff3cd',
|
|
340
|
-
padding: 16,
|
|
341
|
-
borderRadius: 8,
|
|
342
|
-
marginTop: 16,
|
|
343
|
-
},
|
|
344
|
-
requiredText: {
|
|
345
|
-
fontSize: 16,
|
|
346
|
-
fontWeight: '600',
|
|
347
|
-
color: '#856404',
|
|
348
|
-
marginBottom: 8,
|
|
349
|
-
},
|
|
350
|
-
requiredSubtext: {
|
|
351
|
-
fontSize: 14,
|
|
352
|
-
color: '#856404',
|
|
353
|
-
lineHeight: 20,
|
|
354
|
-
},
|
|
355
|
-
accuracyContainer: {
|
|
356
|
-
backgroundColor: '#f0f9f0',
|
|
357
|
-
padding: 16,
|
|
358
|
-
borderRadius: 8,
|
|
359
|
-
marginTop: 16,
|
|
360
|
-
},
|
|
361
|
-
accuracyTitle: {
|
|
362
|
-
fontSize: 16,
|
|
363
|
-
fontWeight: '600',
|
|
364
|
-
color: '#2DBD60',
|
|
365
|
-
marginBottom: 8,
|
|
366
|
-
},
|
|
367
|
-
accuracyText: {
|
|
368
|
-
fontSize: 14,
|
|
369
|
-
color: '#666',
|
|
370
|
-
lineHeight: 20,
|
|
371
|
-
},
|
|
372
|
-
errorText: {
|
|
373
|
-
color: '#dc2626',
|
|
374
|
-
fontSize: 14,
|
|
375
|
-
marginTop: 8,
|
|
376
|
-
textAlign: 'center',
|
|
377
|
-
},
|
|
378
|
-
});
|
|
238
|
+
container: { padding: 16, backgroundColor: 'white', marginHorizontal: 12, marginTop: 12, borderRadius: 12, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.15, maxWidth: 760 },
|
|
239
|
+
title: { fontSize: 24, fontWeight: 'bold', color: '#333', marginBottom: 8, textAlign: 'center' },
|
|
240
|
+
description: { fontSize: 16, color: '#666', textAlign: 'center', marginBottom: 24, lineHeight: 22 },
|
|
241
|
+
locationContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 24 },
|
|
242
|
+
iconContainer: { marginBottom: 24 },
|
|
243
|
+
locationIcon: { fontSize: 80 },
|
|
244
|
+
locationRequest: { alignItems: 'center', width: '100%' },
|
|
245
|
+
locationRequestText: { fontSize: 16, color: '#666', textAlign: 'center', marginBottom: 24 },
|
|
246
|
+
locationButton: { paddingHorizontal: 32, paddingVertical: 16, borderRadius: 8, minWidth: 200, alignItems: 'center' },
|
|
247
|
+
locationButtonDisabled: { opacity: 0.6 },
|
|
248
|
+
locationButtonText: { color: 'white', fontWeight: '600', fontSize: 16 },
|
|
249
|
+
buttonContainer: { flexDirection: 'row', justifyContent: 'space-around', alignItems: 'center', marginTop: 24, width: '100%' },
|
|
250
|
+
primaryButton: { paddingHorizontal: 32, paddingVertical: 16, borderRadius: 8, minWidth: 120, alignItems: 'center' },
|
|
251
|
+
primaryButtonText: { color: 'white', fontWeight: '600', fontSize: 16 },
|
|
252
|
+
secondaryButton: { backgroundColor: '#e5e5e5', paddingHorizontal: 32, paddingVertical: 16, borderRadius: 8, minWidth: 120, alignItems: 'center' },
|
|
253
|
+
secondaryButtonText: { color: '#333', fontWeight: '600', fontSize: 16 },
|
|
254
|
+
requiredContainer: { backgroundColor: '#fff3cd', padding: 16, borderRadius: 8, marginTop: 16 },
|
|
255
|
+
requiredText: { fontSize: 16, fontWeight: '600', color: '#856404', marginBottom: 8 },
|
|
256
|
+
requiredSubtext: { fontSize: 14, color: '#856404', lineHeight: 20 },
|
|
257
|
+
accuracyContainer: { backgroundColor: '#f0f9f0', padding: 16, borderRadius: 8, marginTop: 16 },
|
|
258
|
+
accuracyTitle: { fontSize: 16, fontWeight: '600', color: '#2DBD60', marginBottom: 8 },
|
|
259
|
+
accuracyText: { fontSize: 14, color: '#666', lineHeight: 20 },
|
|
260
|
+
errorText: { color: '#dc2626', fontSize: 14, marginTop: 8, textAlign: 'center' },
|
|
261
|
+
});
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
2
|
import { View, Text, TouchableOpacity, StyleSheet, Image, ActivityIndicator, Platform } from 'react-native';
|
|
3
3
|
import { showAlert } from '../../utils/platformAlert';
|
|
4
4
|
import { EnhancedCameraView } from '../EnhancedCameraView';
|
|
5
|
-
import SelfieOverlay from '../OverLay/SelfieOverlay';
|
|
5
|
+
import SelfieOverlay from '../OverLay/SelfieOverlay';
|
|
6
6
|
import { KYCElement, ISilentCaptureResult } from '../../types/KYC.types';
|
|
7
|
+
import { sdkLogger } from '../../utils/logger';
|
|
8
|
+
import { useTemplateKYCFlowContext } from '../../hooks/useTemplateKYCFlow';
|
|
7
9
|
|
|
8
10
|
interface SelfieCaptureProps {
|
|
9
11
|
element: KYCElement;
|
|
@@ -20,11 +22,16 @@ export const SelfieCapture: React.FC<SelfieCaptureProps> = ({
|
|
|
20
22
|
}) => {
|
|
21
23
|
const [capturedImage, setCapturedImage] = useState<string | null>(value || null);
|
|
22
24
|
const [showCamera, setShowCamera] = useState(false);
|
|
23
|
-
|
|
24
|
-
// 🚨 AUTO-CAPTURE STATES
|
|
25
|
-
const [isProcessingCapture, setIsProcessingCapture] = useState(false);
|
|
25
|
+
const [isProcessingCapture, setIsProcessingCapture] = useState(false);
|
|
26
26
|
const [processingImagePath, setProcessingImagePath] = useState<string | null>(null);
|
|
27
27
|
const [silentCaptureResult, setSilentCaptureResult] = useState<ISilentCaptureResult>({ success: false, isAnalyzing: false });
|
|
28
|
+
const { state } = useTemplateKYCFlowContext();
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (state.session?.session_id) {
|
|
32
|
+
sdkLogger.info("User started selfie verification step");
|
|
33
|
+
}
|
|
34
|
+
}, [state.session?.session_id]);
|
|
28
35
|
|
|
29
36
|
const cameraConfig = element.cameraConfig || {
|
|
30
37
|
aspectRatio: 1,
|
|
@@ -33,25 +40,25 @@ export const SelfieCapture: React.FC<SelfieCaptureProps> = ({
|
|
|
33
40
|
cameraType: 'front',
|
|
34
41
|
allowRetake: true,
|
|
35
42
|
maxRetakes: 3,
|
|
36
|
-
overlay: {
|
|
37
|
-
showGuide: true,
|
|
38
|
-
guideText: 'Placez votre visage dans l\'ovale',
|
|
39
|
-
}
|
|
43
|
+
overlay: { showGuide: true, guideText: 'Placez votre visage dans l\'ovale' }
|
|
40
44
|
};
|
|
41
45
|
|
|
42
46
|
// --- THE AUTO CAPTURE LOGIC ---
|
|
43
47
|
const autoCapture = async (capturePath: string) => {
|
|
44
48
|
if (isProcessingCapture) return;
|
|
45
49
|
|
|
50
|
+
sdkLogger.info("Valid selfie frame detected, processing image...");
|
|
51
|
+
|
|
46
52
|
// 1. Freeze UI and show the captured selfie
|
|
47
|
-
setIsProcessingCapture(true);
|
|
53
|
+
setIsProcessingCapture(true);
|
|
48
54
|
setProcessingImagePath(capturePath);
|
|
49
55
|
|
|
50
56
|
try {
|
|
51
57
|
setCapturedImage(capturePath);
|
|
52
|
-
|
|
53
58
|
// Save data to parent component
|
|
54
|
-
await onValueChange(capturePath);
|
|
59
|
+
await onValueChange(capturePath);
|
|
60
|
+
|
|
61
|
+
sdkLogger.info("Selfie successfully captured and saved");
|
|
55
62
|
|
|
56
63
|
// 2. Short delay for UX success feeling
|
|
57
64
|
setTimeout(() => {
|
|
@@ -60,8 +67,8 @@ export const SelfieCapture: React.FC<SelfieCaptureProps> = ({
|
|
|
60
67
|
setIsProcessingCapture(false);
|
|
61
68
|
setProcessingImagePath(null);
|
|
62
69
|
}, 800);
|
|
63
|
-
|
|
64
70
|
} catch (e: any) {
|
|
71
|
+
sdkLogger.error("Failed to process and save selfie", { error: e?.message }); // 🚨 LOG
|
|
65
72
|
showAlert('Erreur', e?.message || 'Impossible de sauvegarder le selfie');
|
|
66
73
|
setSilentCaptureResult({ success: false, isAnalyzing: false });
|
|
67
74
|
setIsProcessingCapture(false);
|
|
@@ -75,28 +82,25 @@ export const SelfieCapture: React.FC<SelfieCaptureProps> = ({
|
|
|
75
82
|
|
|
76
83
|
if (result.success && result.path) {
|
|
77
84
|
setSilentCaptureResult((prev) => ({ ...prev, isAnalyzing: true, success: false, error: '' }));
|
|
78
|
-
|
|
79
85
|
try {
|
|
80
86
|
// Since camera returned success (clear frame), trigger auto-capture immediately
|
|
81
87
|
await autoCapture(result.path);
|
|
82
88
|
} catch (error: any) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
isAnalyzing: false,
|
|
86
|
-
success: false,
|
|
87
|
-
error: error?.message || 'Visage non détecté'
|
|
88
|
-
}));
|
|
89
|
+
sdkLogger.warn("Selfie auto-capture evaluation failed", { error: error?.message }); // 🚨 LOG
|
|
90
|
+
setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: false, success: false, error: error?.message || 'Visage non détecté' }));
|
|
89
91
|
}
|
|
90
92
|
}
|
|
91
93
|
};
|
|
92
94
|
|
|
93
95
|
const handleError = (event: { message: string }) => {
|
|
96
|
+
sdkLogger.error("Selfie camera hardware/permission error", { error: event.message }); // 🚨 LOG
|
|
94
97
|
showAlert('Erreur', event.message);
|
|
95
98
|
setShowCamera(false);
|
|
96
99
|
setIsProcessingCapture(false);
|
|
97
100
|
};
|
|
98
101
|
|
|
99
102
|
const retakePicture = () => {
|
|
103
|
+
sdkLogger.info("User chose to retake selfie");
|
|
100
104
|
setIsProcessingCapture(false);
|
|
101
105
|
setProcessingImagePath(null);
|
|
102
106
|
setSilentCaptureResult({ success: false, isAnalyzing: false });
|
|
@@ -107,13 +111,12 @@ export const SelfieCapture: React.FC<SelfieCaptureProps> = ({
|
|
|
107
111
|
|
|
108
112
|
// --- LIVE CAMERA RENDER ---
|
|
109
113
|
if (showCamera) {
|
|
110
|
-
const isBusy = isProcessingCapture;
|
|
111
|
-
|
|
114
|
+
const isBusy = isProcessingCapture;
|
|
112
115
|
return (
|
|
113
116
|
<View style={styles.cameraContainerFullScreen}>
|
|
114
117
|
<EnhancedCameraView
|
|
115
118
|
showCamera={true}
|
|
116
|
-
isProcessing={isBusy}
|
|
119
|
+
isProcessing={isBusy}
|
|
117
120
|
cameraType={cameraConfig.cameraType as 'front' | 'back'}
|
|
118
121
|
style={styles.camera}
|
|
119
122
|
onError={handleError}
|
|
@@ -123,43 +126,39 @@ export const SelfieCapture: React.FC<SelfieCaptureProps> = ({
|
|
|
123
126
|
<>
|
|
124
127
|
{/* Subtle scanning indicator */}
|
|
125
128
|
{!isBusy && silentCaptureResult.isAnalyzing && (
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
129
|
+
<View style={styles.topAnalyzingPillContainer}>
|
|
130
|
+
<View style={styles.topAnalyzingPill}>
|
|
131
|
+
<ActivityIndicator size="small" color="white" />
|
|
132
|
+
<Text style={styles.analyzingPillText}>Analyse du visage...</Text>
|
|
133
|
+
</View>
|
|
134
|
+
</View>
|
|
132
135
|
)}
|
|
133
136
|
|
|
134
137
|
{/* Success Overlay with Frozen Perfect Selfie */}
|
|
135
138
|
{isBusy && (
|
|
136
139
|
<View style={StyleSheet.absoluteFillObject}>
|
|
137
140
|
{processingImagePath && (
|
|
138
|
-
<Image
|
|
139
|
-
source={{ uri: processingImagePath.startsWith('file://') ? processingImagePath : `file://${processingImagePath}` }}
|
|
140
|
-
style={StyleSheet.absoluteFillObject}
|
|
141
|
-
resizeMode="cover"
|
|
141
|
+
<Image
|
|
142
|
+
source={{ uri: processingImagePath.startsWith('file://') ? processingImagePath : `file://${processingImagePath}` }}
|
|
143
|
+
style={StyleSheet.absoluteFillObject}
|
|
144
|
+
resizeMode="cover"
|
|
142
145
|
/>
|
|
143
146
|
)}
|
|
144
147
|
<View style={styles.processingOverlay}>
|
|
145
148
|
<ActivityIndicator size="large" color="#2DBD60" />
|
|
146
|
-
<Text style={styles.processingText}>
|
|
147
|
-
Parfait!{"\n"}Traitement du selfie...
|
|
148
|
-
</Text>
|
|
149
|
+
<Text style={styles.processingText}> Parfait!{"\n"}Traitement du selfie... </Text>
|
|
149
150
|
</View>
|
|
150
151
|
</View>
|
|
151
152
|
)}
|
|
152
153
|
|
|
153
154
|
{!isBusy && (
|
|
154
|
-
<SelfieOverlay
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
instructions=""
|
|
162
|
-
language="en"
|
|
155
|
+
<SelfieOverlay
|
|
156
|
+
xMin={0} yMin={0} xMax={100} yMax={100}
|
|
157
|
+
orientation="center"
|
|
158
|
+
isSuccess={isBusy}
|
|
159
|
+
instructionsTile={element.title || "Selfie"}
|
|
160
|
+
instructionsSubtitle={cameraConfig.overlay?.guideText || "Placez votre visage dans l'ovale"}
|
|
161
|
+
cornerOpacity={1} instructions="" language="en"
|
|
163
162
|
/>
|
|
164
163
|
)}
|
|
165
164
|
</>
|
|
@@ -176,12 +175,11 @@ export const SelfieCapture: React.FC<SelfieCaptureProps> = ({
|
|
|
176
175
|
);
|
|
177
176
|
}
|
|
178
177
|
|
|
179
|
-
// --- PREVIEW RENDER ---
|
|
180
178
|
return (
|
|
181
179
|
<View style={styles.container}>
|
|
182
180
|
<Text style={styles.title}>{element.title}</Text>
|
|
183
181
|
<Text style={styles.description}>{element.description}</Text>
|
|
184
|
-
|
|
182
|
+
|
|
185
183
|
{capturedImage ? (
|
|
186
184
|
<>
|
|
187
185
|
<View style={styles.imageContainer}>
|
|
@@ -202,8 +200,11 @@ export const SelfieCapture: React.FC<SelfieCaptureProps> = ({
|
|
|
202
200
|
) : (
|
|
203
201
|
<View style={styles.cameraPlaceholderWrapper}>
|
|
204
202
|
<TouchableOpacity
|
|
205
|
-
style={styles.cameraPlaceholder}
|
|
206
|
-
onPress={() =>
|
|
203
|
+
style={styles.cameraPlaceholder}
|
|
204
|
+
onPress={() => {
|
|
205
|
+
sdkLogger.info("User opened selfie camera"); // 🚨 LOG
|
|
206
|
+
setShowCamera(true);
|
|
207
|
+
}}
|
|
207
208
|
>
|
|
208
209
|
<Text style={styles.cameraPlaceholderText}>🤳</Text>
|
|
209
210
|
<Text style={styles.cameraPlaceholderLabel}>
|
|
@@ -219,37 +220,28 @@ export const SelfieCapture: React.FC<SelfieCaptureProps> = ({
|
|
|
219
220
|
};
|
|
220
221
|
|
|
221
222
|
const styles = StyleSheet.create({
|
|
222
|
-
// 🚨 UI FIX: The camera MUST be full screen so your SelfieOverlay math matches the device screen
|
|
223
223
|
cameraContainerFullScreen: { ...StyleSheet.absoluteFillObject, backgroundColor: 'black', zIndex: 999 },
|
|
224
224
|
camera: { flex: 1 },
|
|
225
|
-
|
|
226
225
|
container: { flex: 1, padding: 16, height: 500 },
|
|
227
226
|
title: { fontSize: 24, fontWeight: 'bold', color: '#333', marginBottom: 8, textAlign: 'center' },
|
|
228
227
|
description: { fontSize: 16, color: '#666', textAlign: 'center', marginBottom: 24, lineHeight: 22 },
|
|
229
|
-
|
|
230
228
|
cameraPlaceholderWrapper: { flex: 1, borderRadius: 12, overflow: 'hidden', marginBottom: 24 },
|
|
231
229
|
imageContainer: { flex: 1, borderRadius: 12, overflow: 'hidden', marginBottom: 24, backgroundColor: '#000' },
|
|
232
230
|
capturedImage: { width: '100%', height: '100%', resizeMode: 'cover' },
|
|
233
|
-
|
|
234
231
|
buttonContainer: { flexDirection: 'row', justifyContent: 'space-around', alignItems: 'center', paddingHorizontal: 16, marginBottom: 24 },
|
|
235
232
|
primaryButton: { backgroundColor: '#2DBD60', paddingHorizontal: 32, paddingVertical: 16, borderRadius: 12 },
|
|
236
233
|
primaryButtonText: { color: 'white', fontWeight: 'bold', fontSize: 16 },
|
|
237
234
|
secondaryButton: { backgroundColor: '#e5e5e5', paddingHorizontal: 32, paddingVertical: 16, borderRadius: 12 },
|
|
238
235
|
secondaryButtonText: { color: '#333', fontWeight: 'bold', fontSize: 16 },
|
|
239
|
-
|
|
240
236
|
errorText: { color: '#dc2626', fontSize: 14, marginTop: 8, textAlign: 'center' },
|
|
241
|
-
|
|
242
237
|
cameraPlaceholder: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#f5f5f5', borderRadius: 12, borderWidth: 2, borderColor: '#e0e0e0', borderStyle: 'dashed' },
|
|
243
238
|
cameraPlaceholderText: { fontSize: 50, marginBottom: 12 },
|
|
244
239
|
cameraPlaceholderLabel: { fontSize: 16, color: '#666', textAlign: 'center', fontWeight: '500' },
|
|
245
|
-
|
|
246
240
|
topAnalyzingPillContainer: { position: 'absolute', top: Platform.OS === 'android' ? 60 : 50, left: 0, right: 0, alignItems: 'center', zIndex: 100 },
|
|
247
241
|
topAnalyzingPill: { flexDirection: 'row', alignItems: 'center', backgroundColor: 'rgba(0,0,0,0.6)', paddingVertical: 8, paddingHorizontal: 16, borderRadius: 20, gap: 8 },
|
|
248
242
|
analyzingPillText: { color: 'white', fontSize: 14, fontWeight: 'bold' },
|
|
249
|
-
|
|
250
243
|
processingOverlay: { flex: 1, backgroundColor: 'rgba(0, 0, 0, 0.7)', justifyContent: 'center', alignItems: 'center', zIndex: 9999 },
|
|
251
244
|
processingText: { color: '#FFF', fontSize: 20, fontWeight: 'bold', marginTop: 16, textAlign: 'center', lineHeight: 28 },
|
|
252
|
-
|
|
253
245
|
floatingErrorBanner: { position: 'absolute', top: 100, left: '10%', right: '10%', backgroundColor: 'rgba(220, 38, 38, 0.95)', paddingVertical: 12, paddingHorizontal: 16, borderRadius: 8, alignItems: 'center', justifyContent: 'center', shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.3, shadowRadius: 5, elevation: 8, zIndex: 100 },
|
|
254
246
|
floatingErrorText: { color: 'white', fontSize: 14, fontWeight: '700', textAlign: 'center' },
|
|
255
247
|
});
|
|
@@ -37,7 +37,6 @@ export const VerificationProgressTemplate: React.FC<VerificationProgressTemplate
|
|
|
37
37
|
logger.log(`Starting verification check (attempt ${currentRetryCount + 1}/${MAX_RETRIES})...`);
|
|
38
38
|
const result: any = await kycService.getVerificationResult(state.session.session_id);
|
|
39
39
|
|
|
40
|
-
// 🚨 FIX 1: Clean the root status by removing trailing periods (e.g., "Pending." -> "pending")
|
|
41
40
|
const rootStatus = (result?.status || '').toLowerCase().trim().replace(/\.$/, '');
|
|
42
41
|
|
|
43
42
|
// FAILED CONDITION
|
|
@@ -76,8 +75,7 @@ export const VerificationProgressTemplate: React.FC<VerificationProgressTemplate
|
|
|
76
75
|
|
|
77
76
|
logger.log(`Parsed Root Status: [${rootStatus}], Inner Status: [${vStatus}], Decision: [${vDecision}]`);
|
|
78
77
|
|
|
79
|
-
|
|
80
|
-
// If the server explicitly says it's pending, we poll. This prevents false "completed" triggers.
|
|
78
|
+
|
|
81
79
|
if (
|
|
82
80
|
rootStatus === 'pending' || rootStatus === 'processing' || rootStatus === 'in_progress' ||
|
|
83
81
|
vStatus === 'processing' || vStatus === 'pending' || vStatus === 'in_progress' || vStatus === 'initiated' || !vStatus
|
|
@@ -200,7 +198,6 @@ export const VerificationProgressTemplate: React.FC<VerificationProgressTemplate
|
|
|
200
198
|
};
|
|
201
199
|
}, [isSuccess, actions]);
|
|
202
200
|
|
|
203
|
-
// --- RENDER LOGIC ---
|
|
204
201
|
|
|
205
202
|
if (isSuccess) {
|
|
206
203
|
return (
|