@sanctum-key/react-native-sdk 1.0.13 → 1.0.15
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/EnhancedCameraView.d.ts.map +1 -1
- package/build/src/components/EnhancedCameraView.js +148 -8
- package/build/src/components/EnhancedCameraView.js.map +1 -1
- package/build/src/components/KYCElements/IDCardCapture.d.ts.map +1 -1
- package/build/src/components/KYCElements/IDCardCapture.js +168 -115
- package/build/src/components/KYCElements/IDCardCapture.js.map +1 -1
- package/build/src/utils/cropByObb.d.ts +0 -12
- package/build/src/utils/cropByObb.d.ts.map +1 -1
- package/build/src/utils/cropByObb.js +73 -98
- package/build/src/utils/cropByObb.js.map +1 -1
- package/package.json +1 -1
- package/src/components/EnhancedCameraView.tsx +190 -33
- package/src/components/KYCElements/IDCardCapture.tsx +253 -182
- package/src/utils/cropByObb.ts +90 -127
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useEffect, useMemo, useState } from 'react';
|
|
2
|
-
import { View, Text, StyleSheet, Image, ScrollView, Platform, ActivityIndicator, TouchableOpacity } from 'react-native';
|
|
2
|
+
import { View, Text, StyleSheet, Image, ScrollView, Platform, ActivityIndicator, TouchableOpacity } from 'react-native';
|
|
3
3
|
import { showAlert } from '../../utils/platformAlert';
|
|
4
4
|
import { EnhancedCameraView } from '../EnhancedCameraView';
|
|
5
5
|
import { TemplateComponent, LocalizedText, GovernmentDocumentType, ISilentCaptureResult, IBbox, GovernmentDocumentTypeShorted, GovernmentDocumentTypeBackend } from '../../types/KYC.types';
|
|
@@ -29,8 +29,10 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
29
29
|
const [isProcessingCapture, setIsProcessingCapture] = useState(false);
|
|
30
30
|
const [processingImagePath, setProcessingImagePath] = useState<string | null>(null);
|
|
31
31
|
|
|
32
|
-
// 🚨 ADDED: Key to force camera re-mount
|
|
33
32
|
const [cameraKey, setCameraKey] = useState(0);
|
|
33
|
+
const [isRebootingCamera, setIsRebootingCamera] = useState(false);
|
|
34
|
+
// Web specific state
|
|
35
|
+
const [cameraType, setCameraType] = useState<'back' | 'front'>('back');
|
|
34
36
|
|
|
35
37
|
const documentTypeMapping: Record<string, GovernmentDocumentType> = { 'nationalId': 'national_id', 'passport': 'passport', 'driversLicense': 'drivers_licence', 'residencePermit': 'permanent_residence', 'healthInsuranceCard': 'health_insurance_card', };
|
|
36
38
|
const { actions, state, env } = useTemplateKYCFlowContext();
|
|
@@ -54,18 +56,18 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
54
56
|
|
|
55
57
|
const countryData = useMemo(() => countrySelectionData, [countrySelectionData]);
|
|
56
58
|
|
|
57
|
-
const [isRebootingCamera, setIsRebootingCamera] = useState(false);
|
|
58
|
-
|
|
59
59
|
const refreshCamera = () => {
|
|
60
60
|
setIsRebootingCamera(true);
|
|
61
|
-
|
|
62
61
|
setTimeout(() => {
|
|
63
62
|
setCameraKey(prev => prev + 1);
|
|
64
63
|
setIsRebootingCamera(false);
|
|
65
64
|
}, 500);
|
|
66
65
|
};
|
|
67
66
|
|
|
68
|
-
|
|
67
|
+
const toggleCameraLens = () => {
|
|
68
|
+
setCameraType(prev => prev === 'back' ? 'front' : 'back');
|
|
69
|
+
refreshCamera();
|
|
70
|
+
};
|
|
69
71
|
|
|
70
72
|
useEffect(() => {
|
|
71
73
|
if (value && Object.keys(value).length > 0) {
|
|
@@ -85,16 +87,21 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
85
87
|
const cameraConfig = useMemo(() => {
|
|
86
88
|
const instructions = selectedDocumentType ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).instructions.en : getDocumentTypeInfo(selectedDocumentType.type).instructions.fr) : getLocalizedText(component.instructions as Record<string, LocalizedText>);
|
|
87
89
|
return {
|
|
88
|
-
cameraType: '
|
|
89
|
-
|
|
90
|
+
cameraType: Platform.OS === 'web' ? cameraType : 'back',
|
|
91
|
+
flashMode: 'auto' as const,
|
|
92
|
+
overlay: {
|
|
93
|
+
guideText: instructions,
|
|
94
|
+
bbox: { xMin: 5, yMin: 15, xMax: 95, yMax: 85, borderColor: '#2DBD60', borderWidth: 3, cornerRadius: 8 }
|
|
95
|
+
}
|
|
90
96
|
};
|
|
91
|
-
}, [selectedDocumentType, locale, component.instructions]);
|
|
97
|
+
}, [selectedDocumentType, locale, component.instructions, cameraType]);
|
|
92
98
|
|
|
93
99
|
const retakePicture = (sideToRetake: 'front' | 'back') => {
|
|
94
100
|
setIsProcessingCapture(false);
|
|
95
101
|
setProcessingImagePath(null);
|
|
96
102
|
setSilentCaptureResult({ path: '', success: false, isAnalyzing: false, error: '', templatePath: '', mrz: '', bbox: undefined });
|
|
97
103
|
setShowCamera(true);
|
|
104
|
+
refreshCamera();
|
|
98
105
|
actions.showCustomStepper(false);
|
|
99
106
|
setCapturedImages((prev) => { const newState = { ...prev }; delete newState[sideToRetake]; return newState; });
|
|
100
107
|
if (value) { const newValue = { ...value }; delete newValue[sideToRetake]; onValueChange(newValue); }
|
|
@@ -149,21 +156,36 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
149
156
|
if (isProcessingCapture) return;
|
|
150
157
|
setIsProcessingCapture(true);
|
|
151
158
|
setProcessingImagePath(capturePath);
|
|
159
|
+
|
|
152
160
|
try {
|
|
153
161
|
let imagePathForUpload = capturePath;
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
162
|
+
|
|
163
|
+
// 🚨 THE FIX: Ignore the backend's broken absolute pixels.
|
|
164
|
+
// Calculate the crop using the Green UI Frame percentages (0.0 to 1.0)
|
|
165
|
+
const overlayBbox = cameraConfig.overlay.bbox;
|
|
166
|
+
const uiCropBbox = {
|
|
167
|
+
minX: overlayBbox.xMin / 100,
|
|
168
|
+
minY: overlayBbox.yMin / 100,
|
|
169
|
+
width: (overlayBbox.xMax - overlayBbox.xMin) / 100,
|
|
170
|
+
height: (overlayBbox.yMax - overlayBbox.yMin) / 100,
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
// Apply the crop using the UI percentages and a tight 2% tolerance
|
|
175
|
+
imagePathForUpload = await cropImageWithBBoxWithTolerance(capturePath, uiCropBbox, 0.02);
|
|
176
|
+
} catch (e) {
|
|
177
|
+
console.warn("Crop failed, falling back to original image", e);
|
|
178
|
+
imagePathForUpload = capturePath; // Fallback to raw image if crop fails
|
|
160
179
|
}
|
|
180
|
+
|
|
161
181
|
const base64 = await pathToBase64(imagePathForUpload);
|
|
162
182
|
const newImages = {
|
|
163
183
|
...capturedImages,
|
|
164
184
|
[currentSide]: { dir: imagePathForUpload, file: base64, mrz: verified.mrz || "", templatePath: verified.templatePath },
|
|
165
185
|
};
|
|
186
|
+
|
|
166
187
|
setCapturedImages(newImages);
|
|
188
|
+
|
|
167
189
|
if (verified.country && verified.documentType) {
|
|
168
190
|
onValueChange({
|
|
169
191
|
...newImages,
|
|
@@ -171,6 +193,7 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
171
193
|
documentType: GovernmentDocumentTypeBackend[verified.documentType as keyof typeof GovernmentDocumentTypeBackend] || '',
|
|
172
194
|
});
|
|
173
195
|
}
|
|
196
|
+
|
|
174
197
|
setTimeout(() => {
|
|
175
198
|
setShowCamera(false);
|
|
176
199
|
actions.showCustomStepper(true);
|
|
@@ -178,8 +201,11 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
178
201
|
setIsProcessingCapture(false);
|
|
179
202
|
setProcessingImagePath(null);
|
|
180
203
|
}, 600);
|
|
204
|
+
|
|
181
205
|
} catch (e: any) {
|
|
182
|
-
|
|
206
|
+
console.error("Backend Error:", e);
|
|
207
|
+
const friendlyError = state.currentLanguage === 'en' ? 'Unable to process document. Please try again.' : 'Impossible de traiter le document. Veuillez réessayer.';
|
|
208
|
+
showAlert('Error', friendlyError);
|
|
183
209
|
setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: false, success: false }));
|
|
184
210
|
setIsProcessingCapture(false);
|
|
185
211
|
setProcessingImagePath(null);
|
|
@@ -236,9 +262,29 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
236
262
|
if (bbox) setBboxBySide(prev => ({ ...prev, [currentSide]: bbox }));
|
|
237
263
|
await autoCapture(result.path, verifiedResult);
|
|
238
264
|
} catch (error: any) {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
265
|
+
// 1. Keep the technical log for your debugging console
|
|
266
|
+
console.error("Backend Verification Error:", error);
|
|
267
|
+
|
|
268
|
+
// 2. Define a map of technical keywords to user-friendly messages
|
|
269
|
+
const rawMessage = (error?.message || '').toLowerCase();
|
|
270
|
+
|
|
271
|
+
let userFriendlyMessage ='Verification failed. Please try again.';
|
|
272
|
+
|
|
273
|
+
if (rawMessage.includes('impossible') || rawMessage.includes('unreadable')) {
|
|
274
|
+
userFriendlyMessage = 'Document unreadable. Please hold the camera steady in good light.';
|
|
275
|
+
} else if (rawMessage.includes('country mismatch') || rawMessage.includes('pays sélectionné')) {
|
|
276
|
+
userFriendlyMessage = 'The document does not match your selected country.';
|
|
277
|
+
} else if (rawMessage.includes('too far') || rawMessage.includes('card_not_fully_in_frame')) {
|
|
278
|
+
userFriendlyMessage = 'Move closer to the document and ensure all corners are visible.';
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// 3. Set ONLY the friendly message to the UI state
|
|
282
|
+
setSilentCaptureResult(prev => ({
|
|
283
|
+
...prev,
|
|
284
|
+
isAnalyzing: false,
|
|
285
|
+
success: false,
|
|
286
|
+
error: userFriendlyMessage
|
|
287
|
+
}));
|
|
242
288
|
}
|
|
243
289
|
}
|
|
244
290
|
};
|
|
@@ -266,134 +312,157 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
266
312
|
);
|
|
267
313
|
}
|
|
268
314
|
|
|
315
|
+
// --- CAMERA RENDER ---
|
|
316
|
+
if (showCamera) {
|
|
317
|
+
const isBusy = isProcessingCapture;
|
|
269
318
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
319
|
+
if (isRebootingCamera) {
|
|
320
|
+
return (
|
|
321
|
+
<View style={[styles.root, { justifyContent: 'center', alignItems: 'center', backgroundColor: '#000' }]}>
|
|
322
|
+
<ActivityIndicator size="large" color="#2DBD60" />
|
|
323
|
+
<Text style={{ color: 'white', marginTop: 20, fontSize: 16 }}>Initializing Camera...</Text>
|
|
324
|
+
</View>
|
|
325
|
+
);
|
|
326
|
+
}
|
|
273
327
|
|
|
274
|
-
if (isRebootingCamera) {
|
|
275
328
|
return (
|
|
276
|
-
<View style={
|
|
277
|
-
<
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
return (
|
|
284
|
-
<View style={styles.root}>
|
|
285
|
-
<View style={[styles.cameraWrapper, { flex: 1, minHeight: 400 }]}>
|
|
286
|
-
|
|
287
|
-
<View style={styles.headerContainer}>
|
|
288
|
-
<Text style={styles.headerTitle}>
|
|
289
|
-
{selectedDocumentType ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).name.en : getDocumentTypeInfo(selectedDocumentType.type).name.fr) : ''}
|
|
290
|
-
</Text>
|
|
291
|
-
<View style={styles.stepBadge}>
|
|
292
|
-
<Text style={styles.stepText}>
|
|
293
|
-
{t('kyc.idCardCapture.captureTitle', { side: currentSide === 'front' ? locale === 'en' ? 'Front' : 'Recto' : locale === 'en' ? 'Back' : 'Verso' })}
|
|
329
|
+
<View style={styles.root}>
|
|
330
|
+
<View style={[styles.cameraWrapper, { flex: 1 }]}>
|
|
331
|
+
|
|
332
|
+
<View style={styles.headerContainer}>
|
|
333
|
+
<Text style={styles.headerTitle}>
|
|
334
|
+
{selectedDocumentType ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).name.en : getDocumentTypeInfo(selectedDocumentType.type).name.fr) : ''}
|
|
294
335
|
</Text>
|
|
336
|
+
<View style={styles.stepBadge}>
|
|
337
|
+
<Text style={styles.stepText}>
|
|
338
|
+
{t('kyc.idCardCapture.captureTitle', { side: currentSide === 'front' ? locale === 'en' ? 'Front' : 'Recto' : locale === 'en' ? 'Back' : 'Verso' })}
|
|
339
|
+
</Text>
|
|
340
|
+
</View>
|
|
295
341
|
</View>
|
|
296
|
-
</View>
|
|
297
|
-
|
|
298
|
-
<View style={[styles.cameraFeedContainer, { flex: 1, backgroundColor: '#000' }]}>
|
|
299
342
|
|
|
300
|
-
<
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
silentCaptureResult={silentCaptureResult}
|
|
309
|
-
overlayComponent={
|
|
310
|
-
<>
|
|
311
|
-
{/* We ONLY put the ID frame here, because if the camera fails, we don't care if the frame fails too */}
|
|
312
|
-
<IdCardOverlay
|
|
313
|
-
xMin={cameraConfig.overlay.bbox.xMin} yMin={cameraConfig.overlay.bbox.yMin} xMax={cameraConfig.overlay.bbox.xMax} yMax={cameraConfig.overlay.bbox.yMax}
|
|
314
|
-
instructions={cameraConfig.overlay.guideText}
|
|
315
|
-
cornerOpacity={cameraConfig.overlay.bbox.cornerRadius || 0 as number}
|
|
316
|
-
isSuccess={silentCaptureResult.success}
|
|
317
|
-
language={state.currentLanguage}
|
|
318
|
-
stepperProps={{
|
|
319
|
-
back: () => {
|
|
320
|
-
if (currentSide === 'back') {
|
|
321
|
-
setCurrentSide('front');
|
|
322
|
-
setShowCamera(false);
|
|
323
|
-
setIsProcessingCapture(false);
|
|
324
|
-
setProcessingImagePath(null);
|
|
325
|
-
if (capturedImages['front']?.dir) {
|
|
326
|
-
const frontImage = capturedImages['front'];
|
|
327
|
-
setSilentCaptureResult((prev) => ({ ...prev, path: frontImage.dir, success: true, isAnalyzing: false, error: '', mrz: frontImage.mrz || '', templatePath: frontImage.templatePath || '', bbox: bboxBySide['front'] }));
|
|
328
|
-
} else { setSilentCaptureResult((prev) => ({ ...prev, path: '', success: false, isAnalyzing: false, error: '', templatePath: '' })); }
|
|
329
|
-
} else { actions.previousComponent(); }
|
|
330
|
-
},
|
|
331
|
-
selectedDocumentType: selectedDocumentType ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).name.en : getDocumentTypeInfo(selectedDocumentType.type).name.fr) : '',
|
|
332
|
-
step: state.currentComponentIndex + 1, totalSteps: state.template.components.length, side: currentSide,
|
|
333
|
-
}}
|
|
334
|
-
/>
|
|
335
|
-
</>
|
|
336
|
-
}
|
|
337
|
-
/>
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
{!isBusy && silentCaptureResult.isAnalyzing && (
|
|
341
|
-
<View style={styles.topAnalyzingPillContainer}>
|
|
342
|
-
<View style={styles.topAnalyzingPill}>
|
|
343
|
-
<ActivityIndicator size="small" color="white" />
|
|
344
|
-
<Text style={styles.analyzingPillText}>
|
|
345
|
-
{state.currentLanguage === 'en' ? 'Scanning...' : 'Analyse...'}
|
|
346
|
-
</Text>
|
|
343
|
+
<View style={[styles.cameraFeedContainer, { flex: 1, backgroundColor: '#000' }]}>
|
|
344
|
+
|
|
345
|
+
{/* WEB ONLY: Flip Camera Top Button */}
|
|
346
|
+
{Platform.OS === 'web' && (
|
|
347
|
+
<View style={styles.webTopControls}>
|
|
348
|
+
<TouchableOpacity style={styles.webFlipButton} onPress={toggleCameraLens}>
|
|
349
|
+
<Text style={styles.webFlipText}>🔄 Flip Lens</Text>
|
|
350
|
+
</TouchableOpacity>
|
|
347
351
|
</View>
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
{
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
352
|
+
)}
|
|
353
|
+
|
|
354
|
+
<EnhancedCameraView
|
|
355
|
+
key={`${currentSide}-${cameraKey}`}
|
|
356
|
+
showCamera={true}
|
|
357
|
+
isProcessing={isBusy}
|
|
358
|
+
cameraType={cameraConfig.cameraType}
|
|
359
|
+
style={StyleSheet.absoluteFillObject}
|
|
360
|
+
onError={handleError}
|
|
361
|
+
onSilentCapture={handleSilentCapture}
|
|
362
|
+
silentCaptureResult={silentCaptureResult}
|
|
363
|
+
overlayComponent={
|
|
364
|
+
<>
|
|
365
|
+
<IdCardOverlay
|
|
366
|
+
xMin={cameraConfig.overlay.bbox.xMin} yMin={cameraConfig.overlay.bbox.yMin} xMax={cameraConfig.overlay.bbox.xMax} yMax={cameraConfig.overlay.bbox.yMax}
|
|
367
|
+
instructions={cameraConfig.overlay.guideText}
|
|
368
|
+
cornerOpacity={cameraConfig.overlay.bbox.cornerRadius || 0 as number}
|
|
369
|
+
isSuccess={silentCaptureResult.success}
|
|
370
|
+
language={state.currentLanguage}
|
|
371
|
+
stepperProps={{
|
|
372
|
+
back: () => {
|
|
373
|
+
if (currentSide === 'back') {
|
|
374
|
+
setCurrentSide('front');
|
|
375
|
+
setShowCamera(false);
|
|
376
|
+
setIsProcessingCapture(false);
|
|
377
|
+
setProcessingImagePath(null);
|
|
378
|
+
if (capturedImages['front']?.dir) {
|
|
379
|
+
const frontImage = capturedImages['front'];
|
|
380
|
+
setSilentCaptureResult((prev) => ({ ...prev, path: frontImage.dir, success: true, isAnalyzing: false, error: '', mrz: frontImage.mrz || '', templatePath: frontImage.templatePath || '', bbox: bboxBySide['front'] }));
|
|
381
|
+
} else { setSilentCaptureResult((prev) => ({ ...prev, path: '', success: false, isAnalyzing: false, error: '', templatePath: '' })); }
|
|
382
|
+
} else { actions.previousComponent(); }
|
|
383
|
+
},
|
|
384
|
+
selectedDocumentType: selectedDocumentType ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).name.en : getDocumentTypeInfo(selectedDocumentType.type).name.fr) : '',
|
|
385
|
+
step: state.currentComponentIndex + 1, totalSteps: state.template.components.length, side: currentSide,
|
|
386
|
+
}}
|
|
387
|
+
/>
|
|
388
|
+
</>
|
|
389
|
+
}
|
|
390
|
+
/>
|
|
391
|
+
|
|
392
|
+
{!isBusy && silentCaptureResult.isAnalyzing && (
|
|
393
|
+
<View style={styles.topAnalyzingPillContainer}>
|
|
394
|
+
<View style={styles.topAnalyzingPill}>
|
|
395
|
+
<ActivityIndicator size="small" color="white" />
|
|
396
|
+
<Text style={styles.analyzingPillText}>
|
|
397
|
+
{state.currentLanguage === 'en' ? 'Scanning...' : 'Analyse...'}
|
|
398
|
+
</Text>
|
|
399
|
+
</View>
|
|
365
400
|
</View>
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
401
|
+
)}
|
|
402
|
+
|
|
403
|
+
{isBusy && (
|
|
404
|
+
<View style={[StyleSheet.absoluteFillObject, { zIndex: 9999 }]}>
|
|
405
|
+
{processingImagePath && (
|
|
406
|
+
<Image
|
|
407
|
+
source={{ uri: processingImagePath.startsWith('file://') ? processingImagePath : `file://${processingImagePath}` }}
|
|
408
|
+
style={StyleSheet.absoluteFillObject}
|
|
409
|
+
resizeMode="cover"
|
|
410
|
+
/>
|
|
411
|
+
)}
|
|
412
|
+
<View style={styles.processingOverlay}>
|
|
413
|
+
<ActivityIndicator size="large" color="#2DBD60" />
|
|
414
|
+
<Text style={styles.processingText}>
|
|
415
|
+
{state.currentLanguage === 'en' ? 'Perfect!\nProcessing Document...' : 'Parfait!\nTraitement du document...'}
|
|
416
|
+
</Text>
|
|
417
|
+
</View>
|
|
418
|
+
</View>
|
|
419
|
+
)}
|
|
420
|
+
|
|
421
|
+
{/* 🚨 THE ESCAPE HATCH: Branching UI for Web vs Mobile */}
|
|
422
|
+
{!isBusy && Platform.OS === 'web' ? (
|
|
423
|
+
<View style={styles.webBottomControlBar}>
|
|
424
|
+
{silentCaptureResult.error ? (
|
|
425
|
+
<View style={styles.floatingErrorBanner}>
|
|
426
|
+
<Text style={styles.floatingErrorText}>⚠️ {silentCaptureResult.error}</Text>
|
|
427
|
+
</View>
|
|
428
|
+
) : <View style={{ height: 10 }} />}
|
|
429
|
+
|
|
430
|
+
<View style={styles.webActionButtonsRow}>
|
|
431
|
+
<TouchableOpacity style={styles.webSecondaryButton} onPress={() => setShowCamera(false)}>
|
|
432
|
+
<Text style={styles.webSecondaryButtonText}>Cancel</Text>
|
|
433
|
+
</TouchableOpacity>
|
|
434
|
+
<TouchableOpacity style={styles.webPrimaryButton} onPress={refreshCamera}>
|
|
435
|
+
<Text style={styles.webPrimaryButtonText}>↻ Refresh Camera</Text>
|
|
436
|
+
</TouchableOpacity>
|
|
437
|
+
</View>
|
|
438
|
+
</View>
|
|
439
|
+
) : !isBusy ? (
|
|
440
|
+
<View style={styles.escapeHatchContainer}>
|
|
441
|
+
<TouchableOpacity style={styles.fallbackRefreshButton} onPress={refreshCamera}>
|
|
442
|
+
<Text style={styles.fallbackRefreshText}>↻ Refresh Camera</Text>
|
|
443
|
+
</TouchableOpacity>
|
|
444
|
+
<TouchableOpacity
|
|
445
|
+
style={[styles.fallbackRefreshButton, { marginTop: 15, backgroundColor: 'rgba(220, 38, 38, 0.8)', borderColor: '#DC2626' }]}
|
|
446
|
+
onPress={() => setShowCamera(false)}
|
|
447
|
+
>
|
|
448
|
+
<Text style={styles.fallbackRefreshText}>Cancel / Go Back</Text>
|
|
449
|
+
</TouchableOpacity>
|
|
450
|
+
</View>
|
|
451
|
+
) : null}
|
|
452
|
+
|
|
453
|
+
{silentCaptureResult.error && !isBusy && Platform.OS !== 'web' ? (
|
|
454
|
+
<View style={[styles.floatingErrorBanner, { zIndex: 10000 }]}>
|
|
455
|
+
<Text style={styles.floatingErrorText}>⚠️ {silentCaptureResult.error}</Text>
|
|
456
|
+
</View>
|
|
457
|
+
) : null}
|
|
390
458
|
|
|
459
|
+
</View>
|
|
391
460
|
</View>
|
|
392
461
|
</View>
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
}
|
|
462
|
+
);
|
|
463
|
+
}
|
|
396
464
|
|
|
465
|
+
// --- PREVIEW RENDER ---
|
|
397
466
|
return (
|
|
398
467
|
<View style={styles.root}>
|
|
399
468
|
<View style={styles.previewContainer}>
|
|
@@ -405,6 +474,7 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
405
474
|
<Text style={{ fontSize: 14, color: '#666', textAlign: 'center', marginBottom: 16, lineHeight: 22 }}>
|
|
406
475
|
{getLocalizedText(component.instructions)}
|
|
407
476
|
</Text>
|
|
477
|
+
|
|
408
478
|
<View style={{ alignItems: 'center', justifyContent: 'center', flexDirection: "column", gap: 16 }}>
|
|
409
479
|
{silentCaptureResult?.error === 'TOO_FAR_AWAY' && (
|
|
410
480
|
<View style={styles.warningBanner}>
|
|
@@ -413,6 +483,8 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
413
483
|
</Text>
|
|
414
484
|
</View>
|
|
415
485
|
)}
|
|
486
|
+
|
|
487
|
+
{/* 🚨 PREVIEW CONTAINER WITH PLATFORM OVERRIDES */}
|
|
416
488
|
<View style={styles.imagePreviewWrapper}>
|
|
417
489
|
{capturedImages[currentSide]?.dir ? (
|
|
418
490
|
<Image source={{ uri: capturedImages[currentSide].dir }} style={styles.previewImage} />
|
|
@@ -420,10 +492,11 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
420
492
|
<Image source={{ uri: silentCaptureResult.path }} style={styles.previewImage} />
|
|
421
493
|
) : null}
|
|
422
494
|
</View>
|
|
495
|
+
|
|
423
496
|
{!capturedImages[currentSide]?.dir && (
|
|
424
497
|
<Button
|
|
425
498
|
title={state.currentLanguage === "en" ? "Start Scanning" : "Commencer la numérisation"}
|
|
426
|
-
onPress={() => { setShowCamera(true); actions.showCustomStepper(false); }}
|
|
499
|
+
onPress={() => { setShowCamera(true); refreshCamera(); actions.showCustomStepper(false); }}
|
|
427
500
|
variant="primary" size="large" fullWidth
|
|
428
501
|
/>
|
|
429
502
|
)}
|
|
@@ -436,6 +509,7 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
436
509
|
actions.nextComponent();
|
|
437
510
|
} else {
|
|
438
511
|
setShowCamera(true);
|
|
512
|
+
refreshCamera();
|
|
439
513
|
setCurrentSide('back');
|
|
440
514
|
setSilentCaptureResult({ success: false, isAnalyzing: false, path: '', error: '', templatePath: undefined });
|
|
441
515
|
setIsProcessingCapture(false);
|
|
@@ -455,38 +529,69 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
455
529
|
const styles = StyleSheet.create({
|
|
456
530
|
root: {
|
|
457
531
|
flex: 1, width: '100%', backgroundColor: 'transparent', alignSelf: 'center',
|
|
458
|
-
...
|
|
532
|
+
...Platform.select({
|
|
533
|
+
web: { minHeight: '85vh', justifyContent: 'center', alignItems: 'center', backdropFilter: 'blur(8px)' } as any,
|
|
534
|
+
})
|
|
459
535
|
},
|
|
460
536
|
cameraWrapper: {
|
|
461
|
-
width: '100%', backgroundColor: '#
|
|
462
|
-
...
|
|
537
|
+
width: '100%', backgroundColor: '#000000', overflow: 'hidden',
|
|
538
|
+
...Platform.select({
|
|
539
|
+
web: { maxWidth: 550, height: '80vh', minHeight: 600, borderRadius: 24, shadowColor: '#000', shadowOffset: { width: 0, height: 20 }, shadowOpacity: 0.25, shadowRadius: 35, elevation: 24 } as any,
|
|
540
|
+
default: { flex: 1 }
|
|
541
|
+
})
|
|
463
542
|
},
|
|
464
543
|
headerContainer: {
|
|
465
544
|
flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 24, paddingVertical: 18, backgroundColor: '#FFFFFF', borderBottomWidth: 1, borderBottomColor: '#F1F5F9', zIndex: 10,
|
|
466
|
-
...
|
|
545
|
+
...Platform.select({ web: { display: 'flex' }, default: { display: 'none' } })
|
|
467
546
|
},
|
|
468
547
|
headerTitle: { fontSize: 18, fontWeight: '700', color: '#0F172A', },
|
|
469
548
|
stepBadge: { backgroundColor: '#F1F5F9', paddingHorizontal: 12, paddingVertical: 6, borderRadius: 20, },
|
|
470
549
|
stepText: { fontSize: 13, fontWeight: '600', color: '#64748B', },
|
|
471
550
|
cameraFeedContainer: { flex: 1, position: 'relative', backgroundColor: '#000', },
|
|
472
551
|
camera: { flex: 1, },
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
},
|
|
476
|
-
|
|
552
|
+
|
|
553
|
+
// MOBILE: Escape Hatch layout
|
|
554
|
+
escapeHatchContainer: { position: 'absolute', bottom: 40, left: 0, right: 0, alignItems: 'center', justifyContent: 'center', zIndex: 99999 },
|
|
555
|
+
fallbackRefreshButton: { backgroundColor: 'rgba(0, 0, 0, 0.8)', borderWidth: 1, borderColor: 'rgba(255, 255, 255, 0.5)', paddingVertical: 12, paddingHorizontal: 24, borderRadius: 24, shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.3, shadowRadius: 5, elevation: 8 },
|
|
556
|
+
fallbackRefreshText: { color: '#FFFFFF', fontWeight: 'bold', fontSize: 16 },
|
|
557
|
+
|
|
558
|
+
// WEB: Control Bar layout
|
|
559
|
+
webBottomControlBar: { position: 'absolute', bottom: 0, left: 0, right: 0, paddingHorizontal: 20, paddingBottom: 20, paddingTop: 20, backgroundColor: 'rgba(0,0,0,0.6)', zIndex: 99999 },
|
|
560
|
+
webActionButtonsRow: { flexDirection: 'row', justifyContent: 'space-between', gap: 12, marginTop: 12 },
|
|
561
|
+
webPrimaryButton: { flex: 1, backgroundColor: '#2DBD60', paddingVertical: 14, borderRadius: 12, alignItems: 'center' },
|
|
562
|
+
webPrimaryButtonText: { color: 'white', fontWeight: 'bold', fontSize: 16 },
|
|
563
|
+
webSecondaryButton: { flex: 1, backgroundColor: 'rgba(255,255,255,0.2)', borderWidth: 1, borderColor: 'rgba(255,255,255,0.4)', paddingVertical: 14, borderRadius: 12, alignItems: 'center' },
|
|
564
|
+
webSecondaryButtonText: { color: 'white', fontWeight: 'bold', fontSize: 16 },
|
|
565
|
+
|
|
566
|
+
// WEB: Flip Controls
|
|
567
|
+
webTopControls: { position: 'absolute', top: 20, right: 20, zIndex: 9999 },
|
|
568
|
+
webFlipButton: { backgroundColor: 'rgba(0,0,0,0.5)', paddingHorizontal: 16, paddingVertical: 10, borderRadius: 20, borderWidth: 1, borderColor: 'rgba(255,255,255,0.3)' },
|
|
569
|
+
webFlipText: { color: 'white', fontWeight: 'bold', fontSize: 14 },
|
|
570
|
+
|
|
477
571
|
previewContainer: {
|
|
478
572
|
width: '100%', backgroundColor: 'white', borderRadius: 12, paddingVertical: 24, paddingHorizontal: 20, shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.1, shadowRadius: 12, elevation: 8,
|
|
479
|
-
...
|
|
573
|
+
...Platform.select({ web: { alignSelf: 'center', maxWidth: 600 }, default: { margin: 10, width: '95%' }})
|
|
480
574
|
},
|
|
481
575
|
previewItemContainer: { flexGrow: 1, },
|
|
482
576
|
title: { fontSize: 24, fontWeight: 'bold', color: '#333', marginBottom: 8, textAlign: 'center' },
|
|
483
577
|
description: { fontSize: 16, color: '#666', textAlign: 'center', marginBottom: 24, lineHeight: 22 },
|
|
484
578
|
sideContainer: { marginBottom: 24 },
|
|
485
579
|
sideTitle: { fontSize: 25, fontWeight: 'bold', color: '#000', marginBottom: 12, textAlign: 'center' },
|
|
486
|
-
|
|
580
|
+
|
|
581
|
+
imagePreviewWrapper: {
|
|
582
|
+
width: '100%', borderRadius: 12, padding: 1, overflow: 'hidden', shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.18, shadowRadius: 8, elevation: 8, backgroundColor: '#f0f0f0',
|
|
583
|
+
...Platform.select({
|
|
584
|
+
web: { aspectRatio: 1.59, height: 'auto' } as any, // 🚨 Perfect ID ratio on web
|
|
585
|
+
default: { height: 220 } // 🚨 Strict original height on mobile
|
|
586
|
+
})
|
|
587
|
+
},
|
|
487
588
|
previewImage: { width: '100%', height: '100%', borderRadius: 12, resizeMode: 'cover' },
|
|
589
|
+
|
|
488
590
|
floatingErrorBanner: {
|
|
489
|
-
|
|
591
|
+
backgroundColor: '#FEF2F2', borderWidth: 1, borderColor: '#FCA5A5', paddingVertical: 12, paddingHorizontal: 16, borderRadius: 12, alignItems: 'center', justifyContent: 'center', width: '100%',
|
|
592
|
+
...Platform.select({
|
|
593
|
+
default: { position: 'absolute', top: Platform.OS === 'ios' ? 90 : 70, left: 24, right: 24, zIndex: 10000 }
|
|
594
|
+
})
|
|
490
595
|
},
|
|
491
596
|
floatingErrorText: { color: '#991B1B', fontSize: 14, fontWeight: '700', textAlign: 'center' },
|
|
492
597
|
processingOverlay: { flex: 1, backgroundColor: 'rgba(0, 0, 0, 0.6)', justifyContent: 'center', alignItems: 'center', zIndex: 9999 },
|
|
@@ -496,39 +601,5 @@ const styles = StyleSheet.create({
|
|
|
496
601
|
errorText: { color: '#dc2626', fontSize: 14, marginTop: 8, textAlign: 'center' },
|
|
497
602
|
topAnalyzingPillContainer: { position: 'absolute', top: Platform.OS === 'android' ? 60 : 50, left: 0, right: 0, alignItems: 'center', zIndex: 100 },
|
|
498
603
|
topAnalyzingPill: { flexDirection: 'row', alignItems: 'center', backgroundColor: 'rgba(0,0,0,0.6)', paddingVertical: 8, paddingHorizontal: 16, borderRadius: 20, gap: 8 },
|
|
499
|
-
analyzingPillText: { color: 'white', fontSize: 14, fontWeight: 'bold' }
|
|
500
|
-
escapeHatchContainer: {
|
|
501
|
-
position: 'absolute',
|
|
502
|
-
bottom: 40,
|
|
503
|
-
left: 0,
|
|
504
|
-
right: 0,
|
|
505
|
-
alignItems: 'center',
|
|
506
|
-
justifyContent: 'center',
|
|
507
|
-
zIndex: 99999, // Guarantees it is the top-most element
|
|
508
|
-
},
|
|
509
|
-
fallbackRefreshButton: {
|
|
510
|
-
backgroundColor: 'rgba(0, 0, 0, 0.8)', // Darker so it's visible on white or black
|
|
511
|
-
borderWidth: 1,
|
|
512
|
-
borderColor: 'rgba(255, 255, 255, 0.5)',
|
|
513
|
-
paddingVertical: 12,
|
|
514
|
-
paddingHorizontal: 24,
|
|
515
|
-
borderRadius: 24,
|
|
516
|
-
shadowColor: '#000',
|
|
517
|
-
shadowOffset: { width: 0, height: 4 },
|
|
518
|
-
shadowOpacity: 0.3,
|
|
519
|
-
shadowRadius: 5,
|
|
520
|
-
elevation: 8,
|
|
521
|
-
},
|
|
522
|
-
fallbackRefreshText: {
|
|
523
|
-
color: '#FFFFFF',
|
|
524
|
-
fontWeight: 'bold',
|
|
525
|
-
fontSize: 16,
|
|
526
|
-
},
|
|
527
|
-
absoluteFillObject: {
|
|
528
|
-
position: 'absolute',
|
|
529
|
-
top: 0,
|
|
530
|
-
left: 0,
|
|
531
|
-
right: 0,
|
|
532
|
-
bottom: 0,
|
|
533
|
-
},
|
|
604
|
+
analyzingPillText: { color: 'white', fontSize: 14, fontWeight: 'bold' }
|
|
534
605
|
});
|