react-native-biometric-verifier 0.0.13 → 0.0.14
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/package.json
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// CaptureImageWithoutEdit.js
|
|
1
2
|
import React, { useRef, useEffect, useState, useCallback } from 'react';
|
|
2
3
|
import {
|
|
3
4
|
View,
|
|
@@ -6,6 +7,8 @@ import {
|
|
|
6
7
|
StyleSheet,
|
|
7
8
|
ActivityIndicator,
|
|
8
9
|
Platform,
|
|
10
|
+
AppState,
|
|
11
|
+
BackHandler,
|
|
9
12
|
} from 'react-native';
|
|
10
13
|
import Icon from 'react-native-vector-icons/MaterialIcons';
|
|
11
14
|
import {
|
|
@@ -21,10 +24,11 @@ import { useFaceDetector } from 'react-native-vision-camera-face-detector';
|
|
|
21
24
|
import { COLORS } from "../utils/constants";
|
|
22
25
|
|
|
23
26
|
// Constants for configuration
|
|
24
|
-
const FACE_STABILITY_THRESHOLD =
|
|
25
|
-
const FACE_MOVEMENT_THRESHOLD =
|
|
26
|
-
const
|
|
27
|
-
const
|
|
27
|
+
const FACE_STABILITY_THRESHOLD = 4; // how many stable samples required
|
|
28
|
+
const FACE_MOVEMENT_THRESHOLD = 18; // pixel threshold (adjust as needed)
|
|
29
|
+
const FRAME_PROCESSOR_MIN_INTERVAL_MS = 700; // throttle; process at most every ~700ms
|
|
30
|
+
const FRAME_PROCESSOR_FPS = 1;
|
|
31
|
+
const MIN_FACE_SIZE = 0.2;
|
|
28
32
|
|
|
29
33
|
const CaptureImageWithoutEdit = React.memo(
|
|
30
34
|
({
|
|
@@ -40,139 +44,193 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
40
44
|
const [cameraPermission, setCameraPermission] = useState('not-determined');
|
|
41
45
|
const [showCamera, setShowCamera] = useState(false);
|
|
42
46
|
const [cameraInitialized, setCameraInitialized] = useState(false);
|
|
43
|
-
|
|
44
|
-
// Face detection states
|
|
45
|
-
const [faces, setFaces] = useState([]);
|
|
47
|
+
|
|
48
|
+
// Face detection states (JS side holds minimal info only)
|
|
49
|
+
const [faces, setFaces] = useState([]); // we store minimal face rectangles here
|
|
46
50
|
const [singleFaceDetected, setSingleFaceDetected] = useState(false);
|
|
47
51
|
const [cameraError, setCameraError] = useState(null);
|
|
48
|
-
|
|
49
|
-
//
|
|
50
|
-
const stableCounter = useRef(0);
|
|
51
|
-
const lastBounds = useRef(null);
|
|
52
|
+
|
|
53
|
+
// refs & lifecycle
|
|
52
54
|
const captured = useRef(false);
|
|
53
|
-
const
|
|
54
|
-
const
|
|
55
|
+
const appState = useRef(AppState.currentState);
|
|
56
|
+
const isMounted = useRef(true);
|
|
55
57
|
|
|
58
|
+
// Face detector options
|
|
56
59
|
const faceDetectionOptions = {
|
|
57
|
-
performanceMode: '
|
|
58
|
-
landmarkMode: '
|
|
59
|
-
contourMode: '
|
|
60
|
-
classificationMode: '
|
|
60
|
+
performanceMode: 'fast',
|
|
61
|
+
landmarkMode: 'none',
|
|
62
|
+
contourMode: 'none',
|
|
63
|
+
classificationMode: 'none',
|
|
61
64
|
minFaceSize: MIN_FACE_SIZE,
|
|
62
65
|
};
|
|
63
|
-
|
|
64
66
|
const { detectFaces } = useFaceDetector(faceDetectionOptions);
|
|
65
67
|
|
|
68
|
+
// Code scanner (unchanged)
|
|
66
69
|
const codeScanner = useCodeScanner({
|
|
67
|
-
codeTypes: ['qr', 'ean-13'
|
|
70
|
+
codeTypes: ['qr', 'ean-13'],
|
|
68
71
|
onCodeScanned: (codes) => {
|
|
69
72
|
if (showCodeScanner && codes && codes[0]?.value && !isLoading) {
|
|
70
|
-
console.log('
|
|
73
|
+
console.log('QR Code scanned:', codes[0].value);
|
|
71
74
|
onCapture(codes[0].value);
|
|
72
75
|
}
|
|
73
76
|
},
|
|
74
77
|
});
|
|
75
78
|
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const { bounds } = face;
|
|
79
|
+
//
|
|
80
|
+
// --- Worklet -> JS callbacks (small payloads only) ---
|
|
81
|
+
//
|
|
82
|
+
// Called when the worklet believes a face is stable enough to capture.
|
|
83
|
+
const onStableFaceDetected = useCallback((faceRect) => {
|
|
84
|
+
// faceRect is a tiny object: { x, y, width, height }
|
|
85
|
+
if (!isMounted.current) return;
|
|
86
|
+
if (captured.current) return;
|
|
85
87
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
88
|
+
// Lock capture to avoid duplicate triggers
|
|
89
|
+
captured.current = true;
|
|
90
|
+
setSingleFaceDetected(true);
|
|
91
|
+
setFaces([faceRect]); // minimal UI update
|
|
89
92
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
// Kick off actual photo capture on JS thread
|
|
94
|
+
(async () => {
|
|
95
|
+
try {
|
|
96
|
+
if (!cameraRef.current) {
|
|
97
|
+
throw new Error('Camera ref not available');
|
|
95
98
|
}
|
|
96
|
-
}
|
|
97
99
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
filetype: 'image/jpeg',
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
onCapture(photoData);
|
|
123
|
-
|
|
124
|
-
} catch (e) {
|
|
125
|
-
console.error('Capture error:', e);
|
|
126
|
-
captured.current = false; // Reset capture lock on error
|
|
127
|
-
setCameraError('Failed to capture image. Please try again.');
|
|
128
|
-
}
|
|
129
|
-
})();
|
|
100
|
+
const photo = await cameraRef.current.takePhoto({
|
|
101
|
+
flash: 'off',
|
|
102
|
+
qualityPrioritization: 'balanced',
|
|
103
|
+
enableShutterSound: false,
|
|
104
|
+
skipMetadata: true,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const photopath = `file://${photo.path}`;
|
|
108
|
+
const fileName = photopath.substr(photopath.lastIndexOf('/') + 1);
|
|
109
|
+
const photoData = {
|
|
110
|
+
uri: photopath,
|
|
111
|
+
filename: fileName,
|
|
112
|
+
filetype: 'image/jpeg',
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// send captured photo upstream
|
|
116
|
+
onCapture(photoData);
|
|
117
|
+
} catch (e) {
|
|
118
|
+
console.error('Capture error:', e);
|
|
119
|
+
captured.current = false; // allow retries
|
|
120
|
+
setCameraError('Failed to capture image. Please try again.');
|
|
130
121
|
}
|
|
122
|
+
})();
|
|
123
|
+
}, [onCapture]);
|
|
124
|
+
|
|
125
|
+
// Called to update face count / progress minimally for UI
|
|
126
|
+
const onFacesUpdate = useCallback((payload) => {
|
|
127
|
+
// payload: { count, progress? }
|
|
128
|
+
if (!isMounted.current) return;
|
|
129
|
+
const { count, progress } = payload;
|
|
130
|
+
if (count === 1) {
|
|
131
|
+
setSingleFaceDetected(true);
|
|
132
|
+
setFaces(prev => {
|
|
133
|
+
// keep existing rectangle if present; otherwise set a placeholder
|
|
134
|
+
if (prev.length === 1) return prev;
|
|
135
|
+
return [{ x: 0, y: 0, width: 0, height: 0 }];
|
|
136
|
+
});
|
|
131
137
|
} else {
|
|
132
|
-
|
|
133
|
-
setFaces(
|
|
134
|
-
setSingleFaceDetected(detectedFaces.length === 1);
|
|
135
|
-
stableCounter.current = 0;
|
|
136
|
-
lastBounds.current = null;
|
|
137
|
-
|
|
138
|
-
// Clear any pending timeout
|
|
139
|
-
if (faceDetectionTimeout.current) {
|
|
140
|
-
clearTimeout(faceDetectionTimeout.current);
|
|
141
|
-
}
|
|
138
|
+
setSingleFaceDetected(false);
|
|
139
|
+
setFaces([]);
|
|
142
140
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
141
|
+
// progress not necessary to store but could be used to show UI progress if desired
|
|
142
|
+
}, []);
|
|
143
|
+
|
|
144
|
+
// Create runOnJS wrappers (these are the functions that we'll call from the worklet)
|
|
145
|
+
// Note: Worklets.createRunOnJS returns a callable function which is available inside the worklet.
|
|
146
|
+
const runOnStable = Worklets.createRunOnJS(onStableFaceDetected);
|
|
147
|
+
const runOnFaces = Worklets.createRunOnJS(onFacesUpdate);
|
|
146
148
|
|
|
147
|
-
//
|
|
149
|
+
//
|
|
150
|
+
// --- Frame processor (worklet) ---
|
|
151
|
+
//
|
|
152
|
+
// The heavy detection is done here, with stability tracking inside the worklet.
|
|
153
|
+
// The worklet calls runOnStable(faceRect) only when it's time to capture.
|
|
148
154
|
const frameProcessor = useFrameProcessor((frame) => {
|
|
149
155
|
'worklet';
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
156
|
+
// prevent processing when scanning QR or when we already captured
|
|
157
|
+
if (global.__showCodeScanner || global.__captured) return;
|
|
158
|
+
|
|
159
|
+
// throttle processing to avoid overloading UI thread / CPU
|
|
160
|
+
const now = performance.now();
|
|
161
|
+
const last = global.__lastProcessedTime || 0;
|
|
162
|
+
if (now - last < FRAME_PROCESSOR_MIN_INTERVAL_MS) return;
|
|
163
|
+
global.__lastProcessedTime = now;
|
|
164
|
+
|
|
153
165
|
try {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
166
|
+
// run face detection (this returns native objects)
|
|
167
|
+
const detected = detectFaces(frame); // array
|
|
168
|
+
|
|
169
|
+
const len = detected.length;
|
|
170
|
+
|
|
171
|
+
if (len === 1 && !global.__captured) {
|
|
172
|
+
// single face logic and stability tracking inside the worklet
|
|
173
|
+
const f = detected[0];
|
|
174
|
+
const bounds = f.bounds; // { x, y, width, height }
|
|
175
|
+
|
|
176
|
+
// initialize worklet-side state if necessary
|
|
177
|
+
if (!global.__lastBounds) {
|
|
178
|
+
global.__lastBounds = bounds;
|
|
179
|
+
global.__stableCount = 1;
|
|
180
|
+
} else {
|
|
181
|
+
const dx = Math.abs(bounds.x - global.__lastBounds.x);
|
|
182
|
+
const dy = Math.abs(bounds.y - global.__lastBounds.y);
|
|
183
|
+
|
|
184
|
+
if (dx < FACE_MOVEMENT_THRESHOLD && dy < FACE_MOVEMENT_THRESHOLD) {
|
|
185
|
+
global.__stableCount = (global.__stableCount || 0) + 1;
|
|
186
|
+
} else {
|
|
187
|
+
global.__stableCount = 0;
|
|
188
|
+
}
|
|
189
|
+
global.__lastBounds = bounds;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// If stable long enough -> trigger JS capture (only send very small payload)
|
|
193
|
+
if ((global.__stableCount || 0) >= FACE_STABILITY_THRESHOLD) {
|
|
194
|
+
// mark captured on worklet side to stop further processing immediately
|
|
195
|
+
global.__captured = true;
|
|
196
|
+
|
|
197
|
+
// send only minimal rectangle data to JS to avoid expensive bridging
|
|
198
|
+
const faceRect = {
|
|
199
|
+
x: Math.round(global.__lastBounds.x),
|
|
200
|
+
y: Math.round(global.__lastBounds.y),
|
|
201
|
+
width: Math.round(global.__lastBounds.width),
|
|
202
|
+
height: Math.round(global.__lastBounds.height),
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
// runOnStable is the JS callback created above
|
|
206
|
+
runOnStable(faceRect);
|
|
207
|
+
} else {
|
|
208
|
+
// optionally send lightweight updates for UI (count & progress)
|
|
209
|
+
runOnFaces({ count: 1, progress: global.__stableCount || 0 });
|
|
210
|
+
}
|
|
159
211
|
} else {
|
|
160
|
-
|
|
212
|
+
// no face or multiple faces -> reset worklet-side stability trackers
|
|
213
|
+
global.__lastBounds = null;
|
|
214
|
+
global.__stableCount = 0;
|
|
215
|
+
// notify JS about the number of faces (minimal payload)
|
|
216
|
+
runOnFaces({ count: len });
|
|
161
217
|
}
|
|
162
|
-
} catch (
|
|
163
|
-
console.
|
|
164
|
-
|
|
218
|
+
} catch (e) {
|
|
219
|
+
// Avoid console.log heavy operations in worklet; just swallow or set a simple global flag
|
|
220
|
+
// If you need debugging, temporarily put a runOnFaces({ debug: String(e) }) but avoid in production.
|
|
165
221
|
}
|
|
166
222
|
}, [detectFaces, showCodeScanner]);
|
|
167
223
|
|
|
168
|
-
//
|
|
224
|
+
//
|
|
225
|
+
// --- Effect: app lifecycle & camera initialization ---
|
|
226
|
+
//
|
|
169
227
|
useEffect(() => {
|
|
170
|
-
|
|
171
|
-
|
|
228
|
+
isMounted.current = true;
|
|
229
|
+
|
|
172
230
|
const initializeCamera = async () => {
|
|
173
231
|
try {
|
|
174
|
-
if (!isMounted) return;
|
|
175
|
-
|
|
232
|
+
if (!isMounted.current) return;
|
|
233
|
+
|
|
176
234
|
const permission = await Camera.requestCameraPermission();
|
|
177
235
|
setCameraPermission(permission);
|
|
178
236
|
|
|
@@ -182,17 +240,18 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
182
240
|
|
|
183
241
|
if (device) {
|
|
184
242
|
setCameraDevice(device);
|
|
185
|
-
|
|
243
|
+
// short delay so camera UI transitions cleanly
|
|
244
|
+
setTimeout(() => setShowCamera(true), 100);
|
|
186
245
|
} else {
|
|
187
|
-
console.error('
|
|
246
|
+
console.error('No camera device found for type:', cameraType);
|
|
188
247
|
setCameraError('Camera not available on this device');
|
|
189
248
|
}
|
|
190
249
|
} else {
|
|
191
250
|
setCameraError('Camera permission denied');
|
|
192
251
|
}
|
|
193
252
|
} catch (error) {
|
|
194
|
-
console.error('
|
|
195
|
-
if (isMounted) {
|
|
253
|
+
console.error('Camera init failed:', error);
|
|
254
|
+
if (isMounted.current) {
|
|
196
255
|
setCameraError('Failed to initialize camera');
|
|
197
256
|
}
|
|
198
257
|
}
|
|
@@ -201,85 +260,145 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
201
260
|
initializeCamera();
|
|
202
261
|
|
|
203
262
|
return () => {
|
|
204
|
-
isMounted = false;
|
|
263
|
+
isMounted.current = false;
|
|
205
264
|
setShowCamera(false);
|
|
206
|
-
// Clean up timeout on unmount
|
|
207
|
-
if (faceDetectionTimeout.current) {
|
|
208
|
-
clearTimeout(faceDetectionTimeout.current);
|
|
209
|
-
}
|
|
210
265
|
};
|
|
211
266
|
}, [cameraType]);
|
|
212
|
-
|
|
213
|
-
const format = useCameraFormat(cameraDevice, [
|
|
214
|
-
{ photoResolution: { width: 640, height: 640 } },
|
|
215
|
-
]);
|
|
216
267
|
|
|
268
|
+
// app state change
|
|
269
|
+
useEffect(() => {
|
|
270
|
+
const subscription = AppState.addEventListener('change', nextAppState => {
|
|
271
|
+
if (appState.current.match(/inactive|background/) && nextAppState === 'active') {
|
|
272
|
+
// App came to the foreground
|
|
273
|
+
if (cameraPermission === 'granted') {
|
|
274
|
+
setShowCamera(true);
|
|
275
|
+
}
|
|
276
|
+
} else if (nextAppState.match(/inactive|background/)) {
|
|
277
|
+
setShowCamera(false);
|
|
278
|
+
}
|
|
279
|
+
appState.current = nextAppState;
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
return () => subscription.remove();
|
|
283
|
+
}, [cameraPermission]);
|
|
284
|
+
|
|
285
|
+
// android back handler (same as before)
|
|
286
|
+
useEffect(() => {
|
|
287
|
+
const backHandler = BackHandler.addEventListener('hardwareBackPress', () => false);
|
|
288
|
+
return () => backHandler.remove();
|
|
289
|
+
}, []);
|
|
290
|
+
|
|
291
|
+
// handle camera errors
|
|
217
292
|
const handleCameraError = useCallback((error) => {
|
|
218
293
|
console.error('Camera error:', error);
|
|
219
294
|
let errorMessage = 'Camera error occurred';
|
|
220
|
-
|
|
295
|
+
|
|
221
296
|
if (error instanceof CameraRuntimeError) {
|
|
222
297
|
switch (error.code) {
|
|
223
298
|
case 'session/configuration-failed':
|
|
224
|
-
errorMessage = 'Camera configuration failed';
|
|
299
|
+
errorMessage = 'Camera configuration failed. Please try again.';
|
|
225
300
|
break;
|
|
226
301
|
case 'device/not-available':
|
|
227
302
|
errorMessage = 'Camera not available';
|
|
228
303
|
break;
|
|
229
304
|
case 'permission/microphone-not-granted':
|
|
230
|
-
errorMessage = '
|
|
305
|
+
errorMessage = 'Camera permission required';
|
|
231
306
|
break;
|
|
232
307
|
default:
|
|
233
308
|
errorMessage = `Camera error: ${error.message}`;
|
|
234
309
|
}
|
|
235
310
|
}
|
|
236
|
-
|
|
311
|
+
|
|
237
312
|
setCameraError(errorMessage);
|
|
313
|
+
setShowCamera(false);
|
|
238
314
|
}, []);
|
|
239
315
|
|
|
240
316
|
const handleCameraInitialized = useCallback(() => {
|
|
241
|
-
console.log('Camera initialized successfully');
|
|
242
317
|
setCameraInitialized(true);
|
|
243
318
|
setCameraError(null);
|
|
244
319
|
}, []);
|
|
245
320
|
|
|
321
|
+
// format selection (unchanged)
|
|
322
|
+
const format = useCameraFormat(cameraDevice, [
|
|
323
|
+
{ videoResolution: { width: 640, height: 640 } },
|
|
324
|
+
{ fps: 30 },
|
|
325
|
+
]);
|
|
326
|
+
|
|
327
|
+
// camera placeholder / UI (kept the same)
|
|
246
328
|
const renderCameraPlaceholder = () => (
|
|
247
329
|
<View style={styles.cameraContainer}>
|
|
248
330
|
{cameraError ? (
|
|
249
331
|
<View style={styles.errorContainer}>
|
|
250
332
|
<Icon name="error-outline" size={40} color={COLORS.error} />
|
|
251
333
|
<Text style={styles.errorText}>{cameraError}</Text>
|
|
252
|
-
<TouchableOpacity
|
|
334
|
+
<TouchableOpacity
|
|
253
335
|
style={styles.retryButton}
|
|
254
336
|
onPress={() => {
|
|
255
337
|
setCameraError(null);
|
|
256
338
|
setShowCamera(false);
|
|
257
|
-
setTimeout(() =>
|
|
339
|
+
setTimeout(() => {
|
|
340
|
+
if (isMounted.current) {
|
|
341
|
+
setShowCamera(true);
|
|
342
|
+
}
|
|
343
|
+
}, 500);
|
|
258
344
|
}}
|
|
259
345
|
>
|
|
260
346
|
<Text style={styles.retryButtonText}>Retry</Text>
|
|
261
347
|
</TouchableOpacity>
|
|
262
348
|
</View>
|
|
263
349
|
) : cameraPermission === 'denied' ? (
|
|
264
|
-
<
|
|
265
|
-
|
|
266
|
-
|
|
350
|
+
<View style={styles.placeholderContainer}>
|
|
351
|
+
<Icon name="camera-off" size={40} color={COLORS.light} />
|
|
352
|
+
<Text style={styles.placeholderText}>
|
|
353
|
+
Camera permission required. Please enable in settings.
|
|
354
|
+
</Text>
|
|
355
|
+
</View>
|
|
267
356
|
) : cameraPermission === 'not-determined' ? (
|
|
268
|
-
<
|
|
269
|
-
|
|
270
|
-
|
|
357
|
+
<View style={styles.placeholderContainer}>
|
|
358
|
+
<ActivityIndicator size="large" color={COLORS.primary} />
|
|
359
|
+
<Text style={styles.placeholderText}>
|
|
360
|
+
Requesting camera access...
|
|
361
|
+
</Text>
|
|
362
|
+
</View>
|
|
271
363
|
) : !cameraDevice ? (
|
|
272
|
-
<
|
|
364
|
+
<View style={styles.placeholderContainer}>
|
|
365
|
+
<Icon name="camera-alt" size={40} color={COLORS.light} />
|
|
366
|
+
<Text style={styles.placeholderText}>Camera not available</Text>
|
|
367
|
+
</View>
|
|
273
368
|
) : (
|
|
274
|
-
<
|
|
369
|
+
<View style={styles.placeholderContainer}>
|
|
370
|
+
<ActivityIndicator size="large" color={COLORS.primary} />
|
|
371
|
+
<Text style={styles.placeholderText}>Initializing camera...</Text>
|
|
372
|
+
</View>
|
|
275
373
|
)}
|
|
276
374
|
</View>
|
|
277
375
|
);
|
|
278
376
|
|
|
279
|
-
const shouldRenderCamera = showCamera &&
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
377
|
+
const shouldRenderCamera = showCamera &&
|
|
378
|
+
cameraPermission === 'granted' &&
|
|
379
|
+
cameraDevice &&
|
|
380
|
+
!cameraError;
|
|
381
|
+
|
|
382
|
+
// keep a global-ish small flag for the worklet to know about codeScanner state
|
|
383
|
+
useEffect(() => {
|
|
384
|
+
// make accessible inside the worklet
|
|
385
|
+
global.__showCodeScanner = !!showCodeScanner;
|
|
386
|
+
return () => {
|
|
387
|
+
global.__showCodeScanner = false;
|
|
388
|
+
};
|
|
389
|
+
}, [showCodeScanner]);
|
|
390
|
+
|
|
391
|
+
// When component unmounts, clear any worklet globals to avoid stale state
|
|
392
|
+
useEffect(() => {
|
|
393
|
+
return () => {
|
|
394
|
+
// Clear worklet globals
|
|
395
|
+
global.__lastProcessedTime = 0;
|
|
396
|
+
global.__lastBounds = null;
|
|
397
|
+
global.__stableCount = 0;
|
|
398
|
+
global.__captured = false;
|
|
399
|
+
global.__showCodeScanner = false;
|
|
400
|
+
};
|
|
401
|
+
}, []);
|
|
283
402
|
|
|
284
403
|
return (
|
|
285
404
|
<View style={styles.cameraContainer}>
|
|
@@ -288,18 +407,20 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
288
407
|
ref={cameraRef}
|
|
289
408
|
style={styles.camera}
|
|
290
409
|
device={cameraDevice}
|
|
291
|
-
isActive={showCamera && !isLoading}
|
|
410
|
+
isActive={showCamera && !isLoading && appState.current === 'active'}
|
|
292
411
|
photo={true}
|
|
293
412
|
format={format}
|
|
294
413
|
codeScanner={showCodeScanner ? codeScanner : undefined}
|
|
414
|
+
// only attach frameProcessor when not scanning codes and camera is initialized
|
|
295
415
|
frameProcessor={!showCodeScanner && cameraInitialized ? frameProcessor : undefined}
|
|
296
416
|
frameProcessorFps={FRAME_PROCESSOR_FPS}
|
|
297
417
|
onInitialized={handleCameraInitialized}
|
|
298
418
|
onError={handleCameraError}
|
|
299
|
-
enableZoomGesture={
|
|
300
|
-
exposure={
|
|
419
|
+
enableZoomGesture={false}
|
|
420
|
+
exposure={0}
|
|
301
421
|
pixelFormat="yuv"
|
|
302
|
-
preset="
|
|
422
|
+
preset="medium"
|
|
423
|
+
orientation="portrait"
|
|
303
424
|
/>
|
|
304
425
|
) : (
|
|
305
426
|
renderCameraPlaceholder()
|
|
@@ -346,13 +467,12 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
346
467
|
<Icon name="flip-camera-ios" size={28} color={COLORS.light} />
|
|
347
468
|
</TouchableOpacity>
|
|
348
469
|
)}
|
|
349
|
-
</View>
|
|
470
|
+
</View>
|
|
350
471
|
</View>
|
|
351
472
|
);
|
|
352
473
|
}
|
|
353
474
|
);
|
|
354
475
|
|
|
355
|
-
// Add display name for better debugging
|
|
356
476
|
CaptureImageWithoutEdit.displayName = 'CaptureImageWithoutEdit';
|
|
357
477
|
|
|
358
478
|
const styles = StyleSheet.create({
|
|
@@ -368,6 +488,12 @@ const styles = StyleSheet.create({
|
|
|
368
488
|
flex: 1,
|
|
369
489
|
width: '100%',
|
|
370
490
|
},
|
|
491
|
+
placeholderContainer: {
|
|
492
|
+
flex: 1,
|
|
493
|
+
justifyContent: 'center',
|
|
494
|
+
alignItems: 'center',
|
|
495
|
+
padding: 20,
|
|
496
|
+
},
|
|
371
497
|
errorContainer: {
|
|
372
498
|
flex: 1,
|
|
373
499
|
justifyContent: 'center',
|
|
@@ -394,8 +520,7 @@ const styles = StyleSheet.create({
|
|
|
394
520
|
color: COLORS.light,
|
|
395
521
|
fontSize: 16,
|
|
396
522
|
textAlign: 'center',
|
|
397
|
-
|
|
398
|
-
padding: 20,
|
|
523
|
+
marginTop: 16,
|
|
399
524
|
},
|
|
400
525
|
cameraControls: {
|
|
401
526
|
position: 'absolute',
|
|
@@ -454,4 +579,4 @@ const styles = StyleSheet.create({
|
|
|
454
579
|
},
|
|
455
580
|
});
|
|
456
581
|
|
|
457
|
-
export default CaptureImageWithoutEdit;
|
|
582
|
+
export default CaptureImageWithoutEdit;
|