@sanctum-key/react-native-sdk 1.0.14 → 1.0.16
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 +43 -15
- package/build/src/components/KYCElements/IDCardCapture.js.map +1 -1
- package/build/src/utils/cropByObb.d.ts +2 -12
- package/build/src/utils/cropByObb.d.ts.map +1 -1
- package/build/src/utils/cropByObb.js +151 -104
- 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 +50 -15
- package/src/utils/cropByObb.ts +188 -125
|
@@ -1,34 +1,201 @@
|
|
|
1
|
+
// import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
// import { View, StyleSheet, Text, AppState } from 'react-native';
|
|
3
|
+
// import { Camera, useCameraDevice, useCameraFormat } from 'react-native-vision-camera';
|
|
4
|
+
// import VisionCameraModule from '../modules/camera/VisionCameraModule';
|
|
5
|
+
// import { useI18n } from '../hooks/useI18n';
|
|
6
|
+
// import { EnhancedCameraViewProps } from './OverLay/type';
|
|
7
|
+
// import { Button } from './ui/Button';
|
|
8
|
+
|
|
9
|
+
// export const EnhancedCameraView: React.FC<EnhancedCameraViewProps> = ({
|
|
10
|
+
// showCamera,
|
|
11
|
+
// cameraType: initialCameraType = 'front',
|
|
12
|
+
// style,
|
|
13
|
+
// onError,
|
|
14
|
+
// onSilentCapture,
|
|
15
|
+
// silentCaptureResult,
|
|
16
|
+
// isProcessing = false,
|
|
17
|
+
// overlayComponent,
|
|
18
|
+
// }) => {
|
|
19
|
+
// const { t } = useI18n();
|
|
20
|
+
// const camera = useRef<Camera>(null);
|
|
21
|
+
|
|
22
|
+
// const isCapturingRef = useRef(false);
|
|
23
|
+
// const isProcessingRef = useRef(isProcessing);
|
|
24
|
+
|
|
25
|
+
// const [cameraType] = useState<'front' | 'back'>(initialCameraType);
|
|
26
|
+
// const [hasPermission, setHasPermission] = useState<boolean | null>(null);
|
|
27
|
+
// const [isInitialized, setIsInitialized] = useState(false);
|
|
28
|
+
// const [refreshCamera, setRefreshCamera] = useState(false);
|
|
29
|
+
// const [layout, setLayout] = useState({ width: 0, height: 0 });
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
// const device = useCameraDevice(cameraType, {
|
|
33
|
+
// physicalDevices: [
|
|
34
|
+
// 'wide-angle-camera' // Explicitly request standard 1x lens
|
|
35
|
+
// ]
|
|
36
|
+
// });
|
|
37
|
+
|
|
38
|
+
// const targetRatio = layout.width > 0 ? layout.height / layout.width : 16 / 9;
|
|
39
|
+
|
|
40
|
+
// const format = useCameraFormat(device, [
|
|
41
|
+
// { photoAspectRatio: targetRatio },
|
|
42
|
+
// { photoResolution: 'max' }
|
|
43
|
+
// ]);
|
|
44
|
+
|
|
45
|
+
// useEffect(() => {
|
|
46
|
+
// isProcessingRef.current = isProcessing;
|
|
47
|
+
// }, [isProcessing]);
|
|
48
|
+
|
|
49
|
+
// const checkPermissions = async () => {
|
|
50
|
+
// try {
|
|
51
|
+
// const hasAllPermissions = await VisionCameraModule.hasAllPermissions();
|
|
52
|
+
// if (!hasAllPermissions) {
|
|
53
|
+
// const granted = await VisionCameraModule.requestAllPermissions();
|
|
54
|
+
// if (!granted) {
|
|
55
|
+
// setHasPermission(false);
|
|
56
|
+
// onError?.({ message: t('camera.permissionRequired') });
|
|
57
|
+
// return;
|
|
58
|
+
// }
|
|
59
|
+
// }
|
|
60
|
+
// setHasPermission(true);
|
|
61
|
+
// } catch (error) {
|
|
62
|
+
// setHasPermission(false);
|
|
63
|
+
// onError?.({ message: t('camera.errorOccurred') });
|
|
64
|
+
// }
|
|
65
|
+
// };
|
|
66
|
+
|
|
67
|
+
// useEffect(() => {
|
|
68
|
+
// if (showCamera) checkPermissions();
|
|
69
|
+
// }, [showCamera, refreshCamera]);
|
|
70
|
+
|
|
71
|
+
// useEffect(() => {
|
|
72
|
+
// const subscription = AppState.addEventListener('change', nextAppState => {
|
|
73
|
+
// if (nextAppState === 'active' && showCamera && hasPermission === false) {
|
|
74
|
+
// checkPermissions();
|
|
75
|
+
// }
|
|
76
|
+
// });
|
|
77
|
+
// return () => subscription.remove();
|
|
78
|
+
// }, [showCamera, hasPermission]);
|
|
79
|
+
|
|
80
|
+
// const onInitialized = useCallback(() => setIsInitialized(true), []);
|
|
81
|
+
// const onCameraError = useCallback((error: any) => {
|
|
82
|
+
// onError?.({ message: error.message || t('camera.errorOccurred') });
|
|
83
|
+
// }, [onError, t]);
|
|
84
|
+
|
|
85
|
+
// const captureSilentPhoto = useCallback(async () => {
|
|
86
|
+
// if (!camera.current || !isInitialized || isProcessingRef.current || isCapturingRef.current) return;
|
|
87
|
+
// if (silentCaptureResult?.isAnalyzing) return;
|
|
88
|
+
|
|
89
|
+
// try {
|
|
90
|
+
// isCapturingRef.current = true;
|
|
91
|
+
// const photo = await camera.current.takePhoto({
|
|
92
|
+
// enableShutterSound: false,
|
|
93
|
+
// flash: 'off',
|
|
94
|
+
// });
|
|
95
|
+
// const result = await VisionCameraModule.processPhotoResult(photo);
|
|
96
|
+
|
|
97
|
+
// onSilentCapture?.({
|
|
98
|
+
// ...result,
|
|
99
|
+
// path: result.path || photo.path,
|
|
100
|
+
// });
|
|
101
|
+
// } catch (error) {
|
|
102
|
+
// // Silent background fail
|
|
103
|
+
// } finally {
|
|
104
|
+
// isCapturingRef.current = false;
|
|
105
|
+
// }
|
|
106
|
+
// }, [isInitialized, onSilentCapture, silentCaptureResult]);
|
|
107
|
+
|
|
108
|
+
// useEffect(() => {
|
|
109
|
+
// if (!showCamera || !isInitialized || isProcessing) return;
|
|
110
|
+
// let isActive = true;
|
|
111
|
+
// let intervalId: ReturnType<typeof setInterval>;
|
|
112
|
+
|
|
113
|
+
// const warmupTimer = setTimeout(() => {
|
|
114
|
+
// if (!isActive) return;
|
|
115
|
+
// intervalId = setInterval(() => {
|
|
116
|
+
// captureSilentPhoto();
|
|
117
|
+
// }, 1500);
|
|
118
|
+
// }, 1000);
|
|
119
|
+
|
|
120
|
+
// return () => {
|
|
121
|
+
// isActive = false;
|
|
122
|
+
// clearTimeout(warmupTimer);
|
|
123
|
+
// if (intervalId) clearInterval(intervalId);
|
|
124
|
+
// };
|
|
125
|
+
// }, [showCamera, isInitialized, isProcessing, captureSilentPhoto]);
|
|
126
|
+
|
|
127
|
+
// if (hasPermission === null) return <View style={[styles.container, style]} />;
|
|
128
|
+
|
|
129
|
+
// if (hasPermission === false) {
|
|
130
|
+
// return (
|
|
131
|
+
// <View style={[styles.container, style, { justifyContent: 'center', alignItems: 'center' }]}>
|
|
132
|
+
// <Text style={styles.permissionMessage}>{t('camera.permissionRequired')}</Text>
|
|
133
|
+
// <Button title="Refresh Camera" onPress={() => setRefreshCamera(prev => !prev)} variant="primary" />
|
|
134
|
+
// </View>
|
|
135
|
+
// );
|
|
136
|
+
// }
|
|
137
|
+
|
|
138
|
+
// if (!device || !showCamera) return <View style={[styles.container, style]} />;
|
|
139
|
+
|
|
140
|
+
// return (
|
|
141
|
+
// <View
|
|
142
|
+
// style={[styles.container, style]}
|
|
143
|
+
// onLayout={(e) => setLayout({ width: e.nativeEvent.layout.width, height: e.nativeEvent.layout.height })}
|
|
144
|
+
// >
|
|
145
|
+
// <Camera
|
|
146
|
+
// ref={camera}
|
|
147
|
+
// style={StyleSheet.absoluteFill}
|
|
148
|
+
// device={device}
|
|
149
|
+
// format={format}
|
|
150
|
+
// resizeMode="cover"
|
|
151
|
+
// isActive={showCamera && !isProcessing}
|
|
152
|
+
// photo={true}
|
|
153
|
+
// video={false}
|
|
154
|
+
// audio={false}
|
|
155
|
+
// onInitialized={onInitialized}
|
|
156
|
+
// onError={onCameraError}
|
|
157
|
+
// />
|
|
158
|
+
// {overlayComponent}
|
|
159
|
+
// </View>
|
|
160
|
+
// );
|
|
161
|
+
// };
|
|
162
|
+
|
|
163
|
+
// const styles = StyleSheet.create({
|
|
164
|
+
// container: { flex: 1, backgroundColor: 'black' },
|
|
165
|
+
// permissionMessage: { color: 'white', textAlign: 'center', margin: 20, fontSize: 16 },
|
|
166
|
+
// });
|
|
167
|
+
|
|
1
168
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
169
|
import { View, StyleSheet, Text, AppState } from 'react-native';
|
|
3
|
-
import { Camera, useCameraDevice } from 'react-native-vision-camera';
|
|
170
|
+
import { Camera, useCameraDevice } from 'react-native-vision-camera';
|
|
4
171
|
import VisionCameraModule from '../modules/camera/VisionCameraModule';
|
|
5
172
|
import { useI18n } from '../hooks/useI18n';
|
|
6
173
|
import { EnhancedCameraViewProps } from './OverLay/type';
|
|
7
174
|
import { Button } from './ui/Button';
|
|
8
175
|
|
|
9
|
-
export const EnhancedCameraView: React.FC<EnhancedCameraViewProps> = ({
|
|
176
|
+
export const EnhancedCameraView: React.FC<EnhancedCameraViewProps> = ({
|
|
10
177
|
showCamera,
|
|
11
178
|
cameraType: initialCameraType = 'front',
|
|
12
179
|
style,
|
|
13
180
|
onError,
|
|
14
|
-
onSilentCapture,
|
|
181
|
+
onSilentCapture,
|
|
15
182
|
silentCaptureResult,
|
|
16
|
-
isProcessing = false,
|
|
183
|
+
isProcessing = false,
|
|
17
184
|
overlayComponent,
|
|
18
185
|
}) => {
|
|
19
186
|
const { t } = useI18n();
|
|
20
187
|
const camera = useRef<Camera>(null);
|
|
21
|
-
|
|
188
|
+
|
|
22
189
|
const isCapturingRef = useRef(false);
|
|
23
190
|
const isProcessingRef = useRef(isProcessing);
|
|
24
191
|
|
|
25
192
|
const [cameraType] = useState<'front' | 'back'>(initialCameraType);
|
|
26
|
-
|
|
27
|
-
// 🚨 BUG FIX: Initialize to null to prevent the "Flicker" on retake
|
|
28
193
|
const [hasPermission, setHasPermission] = useState<boolean | null>(null);
|
|
29
194
|
const [isInitialized, setIsInitialized] = useState(false);
|
|
30
195
|
const [refreshCamera, setRefreshCamera] = useState(false);
|
|
31
196
|
|
|
197
|
+
// 🚨 THE FIX: Removed the strict physical device filtering and custom format.
|
|
198
|
+
// This allows Android to select its own supported, stable hardware stream, preventing crashes.
|
|
32
199
|
const device = useCameraDevice(cameraType);
|
|
33
200
|
|
|
34
201
|
useEffect(() => {
|
|
@@ -76,27 +243,25 @@ export const EnhancedCameraView: React.FC<EnhancedCameraViewProps> = ({
|
|
|
76
243
|
if (silentCaptureResult?.isAnalyzing) return;
|
|
77
244
|
|
|
78
245
|
try {
|
|
79
|
-
isCapturingRef.current = true;
|
|
246
|
+
isCapturingRef.current = true;
|
|
80
247
|
const photo = await camera.current.takePhoto({
|
|
81
|
-
enableShutterSound: false,
|
|
82
|
-
flash: 'off',
|
|
248
|
+
enableShutterSound: false,
|
|
249
|
+
flash: 'off',
|
|
83
250
|
});
|
|
84
|
-
|
|
85
|
-
const result = await VisionCameraModule.processPhotoResult(photo);
|
|
86
251
|
|
|
252
|
+
const result = await VisionCameraModule.processPhotoResult(photo);
|
|
253
|
+
|
|
87
254
|
onSilentCapture?.({
|
|
88
|
-
|
|
89
|
-
|
|
255
|
+
...result,
|
|
256
|
+
path: result.path || photo.path,
|
|
90
257
|
});
|
|
91
|
-
|
|
92
258
|
} catch (error) {
|
|
93
259
|
// Silent background fail
|
|
94
260
|
} finally {
|
|
95
|
-
isCapturingRef.current = false;
|
|
261
|
+
isCapturingRef.current = false;
|
|
96
262
|
}
|
|
97
263
|
}, [isInitialized, onSilentCapture, silentCaptureResult]);
|
|
98
264
|
|
|
99
|
-
// 🚨 BUG FIX: The Warm-up Timer (Fixes Blurry Images)
|
|
100
265
|
useEffect(() => {
|
|
101
266
|
if (!showCamera || !isInitialized || isProcessing) return;
|
|
102
267
|
|
|
@@ -107,9 +272,9 @@ export const EnhancedCameraView: React.FC<EnhancedCameraViewProps> = ({
|
|
|
107
272
|
if (!isActive) return;
|
|
108
273
|
intervalId = setInterval(() => {
|
|
109
274
|
captureSilentPhoto();
|
|
110
|
-
}, 1500);
|
|
111
|
-
}, 1000);
|
|
112
|
-
|
|
275
|
+
}, 1500);
|
|
276
|
+
}, 1000);
|
|
277
|
+
|
|
113
278
|
return () => {
|
|
114
279
|
isActive = false;
|
|
115
280
|
clearTimeout(warmupTimer);
|
|
@@ -117,22 +282,13 @@ export const EnhancedCameraView: React.FC<EnhancedCameraViewProps> = ({
|
|
|
117
282
|
};
|
|
118
283
|
}, [showCamera, isInitialized, isProcessing, captureSilentPhoto]);
|
|
119
284
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
// 🚨 BUG FIX: Show nothing while checking permissions (Stops flicker)
|
|
123
|
-
if (hasPermission === null) {
|
|
124
|
-
return <View style={[styles.container, style]} />;
|
|
125
|
-
}
|
|
285
|
+
if (hasPermission === null) return <View style={[styles.container, style]} />;
|
|
126
286
|
|
|
127
287
|
if (hasPermission === false) {
|
|
128
288
|
return (
|
|
129
289
|
<View style={[styles.container, style, { justifyContent: 'center', alignItems: 'center' }]}>
|
|
130
290
|
<Text style={styles.permissionMessage}>{t('camera.permissionRequired')}</Text>
|
|
131
|
-
<Button
|
|
132
|
-
title="Refresh Camera"
|
|
133
|
-
onPress={() => setRefreshCamera(prev => !prev)}
|
|
134
|
-
variant="primary"
|
|
135
|
-
/>
|
|
291
|
+
<Button title="Refresh Camera" onPress={() => setRefreshCamera(prev => !prev)} variant="primary" />
|
|
136
292
|
</View>
|
|
137
293
|
);
|
|
138
294
|
}
|
|
@@ -145,10 +301,11 @@ export const EnhancedCameraView: React.FC<EnhancedCameraViewProps> = ({
|
|
|
145
301
|
ref={camera}
|
|
146
302
|
style={StyleSheet.absoluteFill}
|
|
147
303
|
device={device}
|
|
304
|
+
resizeMode="cover"
|
|
148
305
|
isActive={showCamera && !isProcessing}
|
|
149
306
|
photo={true}
|
|
150
|
-
video={false}
|
|
151
|
-
audio={false}
|
|
307
|
+
video={false}
|
|
308
|
+
audio={false}
|
|
152
309
|
onInitialized={onInitialized}
|
|
153
310
|
onError={onCameraError}
|
|
154
311
|
/>
|
|
@@ -87,9 +87,12 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
87
87
|
const cameraConfig = useMemo(() => {
|
|
88
88
|
const instructions = selectedDocumentType ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).instructions.en : getDocumentTypeInfo(selectedDocumentType.type).instructions.fr) : getLocalizedText(component.instructions as Record<string, LocalizedText>);
|
|
89
89
|
return {
|
|
90
|
-
cameraType: Platform.OS === 'web' ? cameraType : 'back',
|
|
90
|
+
cameraType: Platform.OS === 'web' ? cameraType : 'back',
|
|
91
91
|
flashMode: 'auto' as const,
|
|
92
|
-
overlay: {
|
|
92
|
+
overlay: {
|
|
93
|
+
guideText: instructions,
|
|
94
|
+
bbox: { xMin: 5, yMin: 15, xMax: 95, yMax: 85, borderColor: '#2DBD60', borderWidth: 3, cornerRadius: 8 }
|
|
95
|
+
}
|
|
93
96
|
};
|
|
94
97
|
}, [selectedDocumentType, locale, component.instructions, cameraType]);
|
|
95
98
|
|
|
@@ -153,21 +156,37 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
153
156
|
if (isProcessingCapture) return;
|
|
154
157
|
setIsProcessingCapture(true);
|
|
155
158
|
setProcessingImagePath(capturePath);
|
|
159
|
+
|
|
156
160
|
try {
|
|
157
161
|
let imagePathForUpload = capturePath;
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
+
// imagePathForUpload = await cropToCenterScanArea(capturePath, 0.91, 1.59);
|
|
177
|
+
} catch (e) {
|
|
178
|
+
console.warn("Crop failed, falling back to original image", e);
|
|
179
|
+
imagePathForUpload = capturePath; // Fallback to raw image if crop fails
|
|
164
180
|
}
|
|
181
|
+
|
|
165
182
|
const base64 = await pathToBase64(imagePathForUpload);
|
|
166
183
|
const newImages = {
|
|
167
184
|
...capturedImages,
|
|
168
185
|
[currentSide]: { dir: imagePathForUpload, file: base64, mrz: verified.mrz || "", templatePath: verified.templatePath },
|
|
169
186
|
};
|
|
187
|
+
|
|
170
188
|
setCapturedImages(newImages);
|
|
189
|
+
|
|
171
190
|
if (verified.country && verified.documentType) {
|
|
172
191
|
onValueChange({
|
|
173
192
|
...newImages,
|
|
@@ -175,6 +194,7 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
175
194
|
documentType: GovernmentDocumentTypeBackend[verified.documentType as keyof typeof GovernmentDocumentTypeBackend] || '',
|
|
176
195
|
});
|
|
177
196
|
}
|
|
197
|
+
|
|
178
198
|
setTimeout(() => {
|
|
179
199
|
setShowCamera(false);
|
|
180
200
|
actions.showCustomStepper(true);
|
|
@@ -182,6 +202,7 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
182
202
|
setIsProcessingCapture(false);
|
|
183
203
|
setProcessingImagePath(null);
|
|
184
204
|
}, 600);
|
|
205
|
+
|
|
185
206
|
} catch (e: any) {
|
|
186
207
|
console.error("Backend Error:", e);
|
|
187
208
|
const friendlyError = state.currentLanguage === 'en' ? 'Unable to process document. Please try again.' : 'Impossible de traiter le document. Veuillez réessayer.';
|
|
@@ -242,15 +263,29 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
242
263
|
if (bbox) setBboxBySide(prev => ({ ...prev, [currentSide]: bbox }));
|
|
243
264
|
await autoCapture(result.path, verifiedResult);
|
|
244
265
|
} catch (error: any) {
|
|
245
|
-
|
|
266
|
+
// 1. Keep the technical log for your debugging console
|
|
267
|
+
console.error("Backend Verification Error:", error);
|
|
246
268
|
|
|
247
|
-
|
|
269
|
+
// 2. Define a map of technical keywords to user-friendly messages
|
|
270
|
+
const rawMessage = (error?.message || '').toLowerCase();
|
|
248
271
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
272
|
+
let userFriendlyMessage ='Verification failed. Please try again.';
|
|
273
|
+
|
|
274
|
+
if (rawMessage.includes('impossible') || rawMessage.includes('unreadable')) {
|
|
275
|
+
userFriendlyMessage = 'Document unreadable. Please hold the camera steady in good light.';
|
|
276
|
+
} else if (rawMessage.includes('country mismatch') || rawMessage.includes('pays sélectionné')) {
|
|
277
|
+
userFriendlyMessage = 'The document does not match your selected country.';
|
|
278
|
+
} else if (rawMessage.includes('too far') || rawMessage.includes('card_not_fully_in_frame')) {
|
|
279
|
+
userFriendlyMessage = 'Move closer to the document and ensure all corners are visible.';
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// 3. Set ONLY the friendly message to the UI state
|
|
283
|
+
setSilentCaptureResult(prev => ({
|
|
284
|
+
...prev,
|
|
285
|
+
isAnalyzing: false,
|
|
286
|
+
success: false,
|
|
287
|
+
error: userFriendlyMessage
|
|
288
|
+
}));
|
|
254
289
|
}
|
|
255
290
|
}
|
|
256
291
|
};
|