@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.
@@ -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
- ...result,
89
- path: result.path || photo.path,
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); // 1.5s gives the hardware more time to stabilize between shots
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
- // --- RENDERERS ---
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', // Keep strictly 'back' on mobile
90
+ cameraType: Platform.OS === 'web' ? cameraType : 'back',
91
91
  flashMode: 'auto' as const,
92
- overlay: { guideText: instructions, bbox: { xMin: 15, yMin: 20, xMax: 85, yMax: 70, borderColor: '#2DBD60', borderWidth: 3, cornerRadius: 8 } }
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
- if (verified.bbox) {
159
- try {
160
- imagePathForUpload = await cropImageWithBBoxWithTolerance(capturePath, verified.bbox, 0.15);
161
- } catch {
162
- imagePathForUpload = capturePath;
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
- console.error("Backend Error:", error);
266
+ // 1. Keep the technical log for your debugging console
267
+ console.error("Backend Verification Error:", error);
246
268
 
247
- const isCardNotFullyInFrame = error?.message === 'CARD_NOT_FULLY_IN_FRAME' || error?.message?.includes('entirement');
269
+ // 2. Define a map of technical keywords to user-friendly messages
270
+ const rawMessage = (error?.message || '').toLowerCase();
248
271
 
249
- const errorMessage = isCardNotFullyInFrame
250
- ? t('kyc.idCardCapture.cardNotFullyInFrame')
251
- : (t('errors.genericVerificationFailed') || 'Verification failed. Please ensure the document is clear and try again.');
252
-
253
- setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: false, success: false, error: errorMessage }));
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
  };