@sanctum-key/react-native-sdk 1.0.14 → 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.
@@ -1 +1 @@
1
- {"version":3,"file":"cropByObb.js","sourceRoot":"","sources":["../../../src/utils/cropByObb.ts"],"names":[],"mappings":"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","sourcesContent":["import { Image as RNImage, Platform } from 'react-native';\nimport * as ImageManipulator from \"expo-image-manipulator\";\nimport { truncateFields } from '../modules/api/KYCService';\nimport { logger } from './logger';\n\ntype Point = [number, number];\n\n/** OBB confidence below this = card not fully in frame; don't crop, give user feedback. */\nexport const OBB_CONFIDENCE_THRESHOLD = 0.85;\n\n/**\n * card_obb format from API:\n * - Legacy: [ [ [p1,p2,p3,p4], confidence ] ]\n * - New: [ { obb: [p1,p2,p3,p4], confidence, card_in_frame?, cropped_sides? } ]\n * Returns confidence in [0,1] or null if not present.\n */\nexport function getObbConfidence(cardObb: any): number | null {\n if (!cardObb || !Array.isArray(cardObb) || cardObb.length === 0) return null;\n const first = cardObb[0];\n\n // New format: first is an object with a confidence field\n if (first && typeof first === 'object' && 'confidence' in first) {\n const score = (first as any).confidence;\n return typeof score === 'number' ? score : null;\n }\n\n // Legacy format: [ [points], score ]\n if (!Array.isArray(first) || first.length < 2) return null;\n const score = first[1];\n return typeof score === 'number' ? score : null;\n}\n\n/**\n * Extracts the card_in_frame flag from card_obb when present.\n * Returns:\n * - true => card fully in frame\n * - false => card not fully in frame\n * - null => field not present (backward compatibility)\n */\n/**\n * Extracts the framing data from card_obb.\n * STRICT CHECK: Returns true ONLY if card_in_frame is true AND cropped_sides is empty.\n * Returns:\n * - true => card fully in frame (no sides cropped)\n * - false => card not fully in frame OR edges are cropped\n * - null => field not present (backward compatibility)\n */\nexport function getCardInFrame(cardObb: any): boolean | null {\n if (!cardObb) return null;\n\n // card_obb can be an array of objects or a single object\n const first = Array.isArray(cardObb) ? cardObb[0] : cardObb;\n\n if (first && typeof first === 'object' && 'card_in_frame' in first) {\n const isCardInFrame = first.card_in_frame === true;\n const hasCroppedSides = Array.isArray(first.cropped_sides) && first.cropped_sides.length > 0;\n\n // If the API explicitly says it's not in frame, OR if it lists cropped edges, reject it.\n if (!isCardInFrame || hasCroppedSides) {\n if (hasCroppedSides) {\n console.warn(`Card is cropped on sides: ${first.cropped_sides.join(', ')}`);\n }\n return false; \n }\n\n // If we made it here, card_in_frame is true AND cropped_sides is empty.\n return true; \n }\n\n // Backward compatibility check\n if (cardObb && typeof cardObb === 'object' && 'card_in_frame' in cardObb) {\n const value = (cardObb as any).card_in_frame;\n return typeof value === 'boolean' ? value : null;\n }\n\n return null;\n}\n\n// Compute axis-aligned bounding box from oriented quadrilateral\nfunction computeAabb(points: Point[]) {\n const xs = points.map(p => p[0]);\n const ys = points.map(p => p[1]);\n const minX = Math.min(...xs);\n const minY = Math.min(...ys);\n const maxX = Math.max(...xs);\n const maxY = Math.max(...ys);\n return { minX, minY, maxX, maxY, width: maxX - minX, height: maxY - minY };\n}\n\n// Web-only crop using Canvas; returns dataURL (base64)\nasync function cropWeb(uri: string, points: Point[]): Promise<string> {\n return new Promise((resolve, reject) => {\n const img = new Image();\n img.crossOrigin = 'anonymous';\n img.onload = () => {\n const { minX, minY, width, height } = computeAabb(points);\n const canvas = document.createElement('canvas');\n canvas.width = Math.max(1, Math.round(width));\n canvas.height = Math.max(1, Math.round(height));\n const ctx = canvas.getContext('2d');\n if (!ctx) return reject(new Error('Canvas context not available'));\n ctx.drawImage(\n img,\n minX, // sx\n minY, // sy\n width, // sw\n height, // sh\n 0,\n 0,\n canvas.width,\n canvas.height\n );\n resolve(canvas.toDataURL('image/jpeg', 0.92));\n };\n img.onerror = (e) => reject(e);\n img.src = uri;\n });\n}\n\n// Fallback: return original for native (no dependency added); caller can still use bbox to draw overlay\nexport async function cropByObb(uri: string, cardObb: any): Promise<{ base64?: string; bbox?: { minX: number; minY: number; width: number; height: number } }> {\n try {\n // card_obb format: [ [ [p1,p2,p3,p4], score ] ] — if confidence too low, don't crop (send full image)\n const confidence = getObbConfidence(cardObb);\n if (confidence !== null && confidence < OBB_CONFIDENCE_THRESHOLD) {\n return {};\n }\n const first = Array.isArray(cardObb) ? cardObb[0] : null;\n const points = Array.isArray(first?.[0]) ? (first[0] as Point[]) : null;\n if (!points || points.length !== 4) return {};\n\n const { minX, minY, width, height } = computeAabb(points);\n\n if (Platform.OS === 'web') {\n const base64 = await cropWeb(uri, points);\n return { base64, bbox: { minX, minY, width, height } };\n }\n // Native: return bbox only; keep base64 undefined\n return { bbox: { minX, minY, width, height } };\n } catch (e) {\n return {};\n }\n}\n\n\n\n\n// exemple d'appel :\nexport async function cropImageWithBBox(uri: string, bbox: any) {\n // 1️⃣ Récupère la taille originale de l'image\n await RNImage.getSize(uri, (width, height) => {\n console.log(\"Image originale:\", width, height);\n });\n\n // // 2️⃣ Suppose que ton bbox vient d'une image affichée dans `displayedSize`\n // const scaleX = originalWidth / displayedSize.width;\n // const scaleY = originalHeight / displayedSize.height;\n\n // 3️⃣ Convertir le bbox à la taille réelle scale 0.10 = 10%\n const crop = {\n originX: bbox.minX,\n originY: bbox.minY,\n width: bbox.width,\n height: bbox.height,\n };\n\n // 4️⃣ Appliquer le crop\n // @ts-ignore - manipulateAsync is deprecated but still functional, new API (useImageManipulator) is a React hook and not suitable for utility functions\n const result = await ImageManipulator.manipulateAsync(\n uri,\n [{ crop }],\n { compress: 1, format: ImageManipulator.SaveFormat.PNG }\n );\n\n console.log(\"Image recadrée:\", result.uri);\n return result.uri;\n}\n\n// Fonction pour rogner avec une tolérance de 10% autour du bbox\nexport async function cropImageWithBBoxWithTolerance(uri: string, bbox: any, tolerance: number = 0.1): Promise<string> {\n console.log(\"cropImageWithBBoxWithTolerance\", JSON.stringify(truncateFields({ uri, bbox, tolerance }), null, 2));\n \n return new Promise<string>((resolve, reject) => {\n let isResolved = false;\n \n // Timeout de sécurité (15 secondes)\n const timeout = setTimeout(() => {\n if (!isResolved) {\n isResolved = true;\n console.warn(\"Timeout lors du rognage avec tolérance, utilisation de l'image originale\");\n resolve(uri); // Retourner l'URI original en cas de timeout\n }\n }, 15000);\n\n // 1️⃣ Récupère la taille originale de l'image\n RNImage.getSize(uri, (imageWidth, imageHeight) => {\n if (isResolved) return;\n \n // 2️⃣ Calculer la tolérance en pixels (10% de chaque côté)\n const toleranceX = bbox.width * tolerance;\n const toleranceY = bbox.height * tolerance;\n\n // 3️⃣ Ajuster le bbox avec la tolérance\n // Calculer les nouvelles coordonnées avec tolérance\n const newMinX = Math.max(0, bbox.minX - toleranceX);\n const newMinY = Math.max(0, bbox.minY - toleranceY);\n const newMaxX = Math.min(imageWidth, bbox.minX + bbox.width + toleranceX);\n const newMaxY = Math.min(imageHeight, bbox.minY + bbox.height + toleranceY);\n\n // Calculer la largeur et hauteur finales\n const finalWidth = newMaxX - newMinX;\n const finalHeight = newMaxY - newMinY;\n\n const crop = {\n originX: newMinX,\n originY: newMinY,\n width: finalWidth,\n height: finalHeight,\n };\n\n logger.log(\"cropImageWithBBoxWithTolerance - crop params\", JSON.stringify(truncateFields({ uri, crop, tolerance, imageWidth, imageHeight }), null, 2));\n\n // 4️⃣ Appliquer le crop avec tolérance\n // @ts-ignore - manipulateAsync is deprecated but still functional, new API (useImageManipulator) is a React hook and not suitable for utility functions\n ImageManipulator.manipulateAsync(\n uri,\n [{ crop }],\n { compress: 0.95, format: ImageManipulator.SaveFormat.JPEG }\n )\n .then((result) => {\n if (isResolved) return;\n isResolved = true;\n clearTimeout(timeout);\n console.log(\"Image recadrée avec tolérance:\", result.uri);\n resolve(result.uri);\n })\n .catch((error) => {\n if (isResolved) return;\n isResolved = true;\n clearTimeout(timeout);\n console.error(\"Erreur lors du rognage avec tolérance:\", error);\n // En cas d'erreur, retourner l'URI original au lieu de rejeter\n resolve(uri);\n });\n }, (error) => {\n if (isResolved) return;\n isResolved = true;\n clearTimeout(timeout);\n console.error(\"Erreur lors de la récupération de la taille de l'image:\", error);\n // En cas d'erreur, retourner l'URI original au lieu de rejeter\n resolve(uri);\n });\n });\n}\n"]}
1
+ {"version":3,"file":"cropByObb.js","sourceRoot":"","sources":["../../../src/utils/cropByObb.ts"],"names":[],"mappings":"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","sourcesContent":["import { Image as RNImage, Platform } from 'react-native';\nimport * as ImageManipulator from \"expo-image-manipulator\";\n\n\ntype Point = [number, number];\n\n/** OBB confidence below this = card not fully in frame; don't crop, give user feedback. */\nexport const OBB_CONFIDENCE_THRESHOLD = 0.85;\n\n/**\n * card_obb format from API:\n * - Legacy: [ [ [p1,p2,p3,p4], confidence ] ]\n * - New: [ { obb: [p1,p2,p3,p4], confidence, card_in_frame?, cropped_sides? } ]\n * Returns confidence in [0,1] or null if not present.\n */\nexport function getObbConfidence(cardObb: any): number | null {\n if (!cardObb || !Array.isArray(cardObb) || cardObb.length === 0) return null;\n const first = cardObb[0];\n\n // New format: first is an object with a confidence field\n if (first && typeof first === 'object' && 'confidence' in first) {\n const score = (first as any).confidence;\n return typeof score === 'number' ? score : null;\n }\n\n // Legacy format: [ [points], score ]\n if (!Array.isArray(first) || first.length < 2) return null;\n const score = first[1];\n return typeof score === 'number' ? score : null;\n}\n\n/**\n * Extracts the framing data from card_obb.\n * STRICT CHECK: Returns true ONLY if card_in_frame is true AND cropped_sides is empty.\n */\nexport function getCardInFrame(cardObb: any): boolean | null {\n if (!cardObb) return null;\n \n // card_obb can be an array of objects or a single object\n const first = Array.isArray(cardObb) ? cardObb[0] : cardObb;\n\n if (first && typeof first === 'object' && 'card_in_frame' in first) {\n const isCardInFrame = first.card_in_frame === true;\n const hasCroppedSides = Array.isArray(first.cropped_sides) && first.cropped_sides.length > 0;\n\n // If the API explicitly says it's not in frame, OR if it lists cropped edges, reject it.\n if (!isCardInFrame || hasCroppedSides) {\n if (hasCroppedSides) {\n console.warn(`Card is cropped on sides: ${first.cropped_sides.join(', ')}`);\n }\n return false;\n }\n\n // If we made it here, card_in_frame is true AND cropped_sides is empty.\n return true;\n }\n\n // Backward compatibility check\n if (cardObb && typeof cardObb === 'object' && 'card_in_frame' in cardObb) {\n const value = (cardObb as any).card_in_frame;\n return typeof value === 'boolean' ? value : null;\n }\n\n return null;\n}\n\n// Compute axis-aligned bounding box from oriented quadrilateral\nfunction computeAabb(points: Point[]) {\n const xs = points.map(p => p[0]);\n const ys = points.map(p => p[1]);\n const minX = Math.min(...xs);\n const minY = Math.min(...ys);\n const maxX = Math.max(...xs);\n const maxY = Math.max(...ys);\n return { minX, minY, maxX, maxY, width: maxX - minX, height: maxY - minY };\n}\n\n// Web-only crop using Canvas; returns dataURL (base64)\nasync function cropWeb(uri: string, points: Point[]): Promise<string> {\n return new Promise((resolve, reject) => {\n const img = new Image();\n img.crossOrigin = 'anonymous';\n img.onload = () => {\n const { minX, minY, width, height } = computeAabb(points);\n const canvas = document.createElement('canvas');\n canvas.width = Math.max(1, Math.round(width));\n canvas.height = Math.max(1, Math.round(height));\n const ctx = canvas.getContext('2d');\n if (!ctx) return reject(new Error('Canvas context not available'));\n ctx.drawImage(\n img,\n minX, minY, width, height, // Source \n 0, 0, canvas.width, canvas.height // Destination\n );\n resolve(canvas.toDataURL('image/jpeg', 0.92));\n };\n img.onerror = (e) => reject(e);\n img.src = uri;\n });\n}\n\n// Fallback: return original for native\nexport async function cropByObb(uri: string, cardObb: any): Promise<{ base64?: string; bbox?: { minX: number; minY: number; width: number; height: number } }> {\n try {\n const confidence = getObbConfidence(cardObb);\n if (confidence !== null && confidence < OBB_CONFIDENCE_THRESHOLD) {\n return {};\n }\n const first = Array.isArray(cardObb) ? cardObb[0] : null;\n const points = Array.isArray(first?.[0]) ? (first[0] as Point[]) : null;\n if (!points || points.length !== 4) return {};\n\n const { minX, minY, width, height } = computeAabb(points);\n\n if (Platform.OS === 'web') {\n const base64 = await cropWeb(uri, points);\n return { base64, bbox: { minX, minY, width, height } };\n }\n \n return { bbox: { minX, minY, width, height } };\n } catch (e) {\n return {};\n }\n}\n\n\nexport async function cropImageWithBBoxWithTolerance(uri: string, bbox: any, tolerance: number = 0.02): Promise<string> {\n return new Promise<string>((resolve) => {\n let isResolved = false;\n\n // Safety Timeout (10 seconds)\n const timeout = setTimeout(() => {\n if (!isResolved) {\n isResolved = true;\n console.warn(\"Crop timeout, returning original.\");\n resolve(uri);\n }\n }, 10000);\n\n try {\n RNImage.getSize(uri, async (actualWidth, actualHeight) => {\n if (isResolved) return;\n\n // 1. Detect if bbox is percentage (e.g., 0.15) or absolute pixels (e.g., 800)\n // Since we pass UI percentages, this will be true.\n const isPercentageBased = bbox.width <= 1.5 && bbox.height <= 1.5;\n\n let pixelMinX, pixelMinY, pixelWidth, pixelHeight;\n\n if (isPercentageBased) {\n pixelMinX = bbox.minX * actualWidth;\n pixelMinY = bbox.minY * actualHeight;\n pixelWidth = bbox.width * actualWidth;\n pixelHeight = bbox.height * actualHeight;\n } else {\n // Fallback if backend pixels somehow bypass the check\n pixelMinX = bbox.minX;\n pixelMinY = bbox.minY;\n pixelWidth = bbox.width;\n pixelHeight = bbox.height;\n }\n\n // 2. Add Tolerance (Expands the box slightly to avoid cutting off physical card edges)\n const toleranceX = pixelWidth * tolerance;\n const toleranceY = pixelHeight * tolerance;\n\n // Clamp values to ensure we don't try to crop outside the image bounds\n const newMinX = Math.max(0, pixelMinX - toleranceX);\n const newMinY = Math.max(0, pixelMinY - toleranceY);\n const newMaxX = Math.min(actualWidth, pixelMinX + pixelWidth + toleranceX);\n const newMaxY = Math.min(actualHeight, pixelMinY + pixelHeight + toleranceY);\n\n const finalCrop = {\n originX: Math.floor(newMinX),\n originY: Math.floor(newMinY),\n width: Math.floor(newMaxX - newMinX),\n height: Math.floor(newMaxY - newMinY),\n };\n\n // 3. Perform Final Crop\n try {\n // @ts-ignore - Required for utility functions outside React components\n const result = await ImageManipulator.manipulateAsync(\n uri,\n [{ crop: finalCrop }],\n { compress: 0.95, format: ImageManipulator.SaveFormat.JPEG }\n );\n \n if (!isResolved) {\n isResolved = true;\n clearTimeout(timeout);\n resolve(result.uri);\n }\n } catch (err) {\n if (!isResolved) {\n isResolved = true;\n clearTimeout(timeout);\n console.error(\"ImageManipulator failed, returning raw image\", err);\n resolve(uri);\n }\n }\n }, () => {\n if (!isResolved) {\n isResolved = true;\n clearTimeout(timeout);\n resolve(uri);\n }\n });\n } catch (e) {\n if (!isResolved) {\n isResolved = true;\n clearTimeout(timeout);\n resolve(uri);\n }\n }\n });\n}"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanctum-key/react-native-sdk",
3
- "version": "1.0.14",
3
+ "version": "1.0.15",
4
4
  "description": "Sanctum Key React Native SDK",
5
5
  "main": "build/src/index.js",
6
6
  "types": "build/src/index.d.ts",
@@ -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,36 @@ 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
+ } catch (e) {
177
+ console.warn("Crop failed, falling back to original image", e);
178
+ imagePathForUpload = capturePath; // Fallback to raw image if crop fails
164
179
  }
180
+
165
181
  const base64 = await pathToBase64(imagePathForUpload);
166
182
  const newImages = {
167
183
  ...capturedImages,
168
184
  [currentSide]: { dir: imagePathForUpload, file: base64, mrz: verified.mrz || "", templatePath: verified.templatePath },
169
185
  };
186
+
170
187
  setCapturedImages(newImages);
188
+
171
189
  if (verified.country && verified.documentType) {
172
190
  onValueChange({
173
191
  ...newImages,
@@ -175,6 +193,7 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
175
193
  documentType: GovernmentDocumentTypeBackend[verified.documentType as keyof typeof GovernmentDocumentTypeBackend] || '',
176
194
  });
177
195
  }
196
+
178
197
  setTimeout(() => {
179
198
  setShowCamera(false);
180
199
  actions.showCustomStepper(true);
@@ -182,6 +201,7 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
182
201
  setIsProcessingCapture(false);
183
202
  setProcessingImagePath(null);
184
203
  }, 600);
204
+
185
205
  } catch (e: any) {
186
206
  console.error("Backend Error:", e);
187
207
  const friendlyError = state.currentLanguage === 'en' ? 'Unable to process document. Please try again.' : 'Impossible de traiter le document. Veuillez réessayer.';
@@ -242,15 +262,29 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
242
262
  if (bbox) setBboxBySide(prev => ({ ...prev, [currentSide]: bbox }));
243
263
  await autoCapture(result.path, verifiedResult);
244
264
  } catch (error: any) {
245
- console.error("Backend Error:", error);
265
+ // 1. Keep the technical log for your debugging console
266
+ console.error("Backend Verification Error:", error);
246
267
 
247
- const isCardNotFullyInFrame = error?.message === 'CARD_NOT_FULLY_IN_FRAME' || error?.message?.includes('entirement');
268
+ // 2. Define a map of technical keywords to user-friendly messages
269
+ const rawMessage = (error?.message || '').toLowerCase();
248
270
 
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 }));
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
+ }));
254
288
  }
255
289
  }
256
290
  };