@hexar/biometric-identity-sdk-react-native 1.17.0 → 1.19.0
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/dist/components/CameraCapture.d.ts.map +1 -1
- package/dist/components/CameraCapture.js +37 -4
- package/dist/components/VideoRecorder.d.ts.map +1 -1
- package/dist/components/VideoRecorder.js +21 -1
- package/package.json +1 -1
- package/src/components/CameraCapture.tsx +42 -7
- package/src/components/VideoRecorder.tsx +22 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CameraCapture.d.ts","sourceRoot":"","sources":["../../src/components/CameraCapture.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,
|
|
1
|
+
{"version":3,"file":"CameraCapture.d.ts","sourceRoot":"","sources":["../../src/components/CameraCapture.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAmD,MAAM,OAAO,CAAC;AAaxE,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAmC,MAAM,oCAAoC,CAAC;AAIrH,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,OAAO,GAAG,MAAM,CAAC;IACvB,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,SAAS,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,QAAQ,EAAE,MAAM,IAAI,CAAC;CACtB;AAED,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CAwStD,CAAC;AA0JF,eAAe,aAAa,CAAC"}
|
|
@@ -53,8 +53,12 @@ const CameraCapture = ({ mode, theme, language, onCapture, onCancel, }) => {
|
|
|
53
53
|
(0, biometric_identity_sdk_core_1.setLanguage)(language);
|
|
54
54
|
}
|
|
55
55
|
const strings = (0, biometric_identity_sdk_core_1.getStrings)();
|
|
56
|
-
// Get camera device (back camera for document capture)
|
|
57
|
-
|
|
56
|
+
// Get camera device (back camera for document capture).
|
|
57
|
+
// Prefer wide-angle (main) camera — some Android devices default to
|
|
58
|
+
// macro/depth sensors that have fixed focus and produce blurry images.
|
|
59
|
+
const device = (0, react_native_vision_camera_1.useCameraDevice)('back', {
|
|
60
|
+
physicalDevices: ['wide-angle-camera', 'ultra-wide-angle-camera', 'telephoto-camera'],
|
|
61
|
+
});
|
|
58
62
|
const [deviceReady, setDeviceReady] = (0, react_1.useState)(!!device);
|
|
59
63
|
(0, react_1.useEffect)(() => {
|
|
60
64
|
checkPermissions();
|
|
@@ -68,6 +72,24 @@ const CameraCapture = ({ mode, theme, language, onCapture, onCancel, }) => {
|
|
|
68
72
|
const timeout = setTimeout(() => setDeviceReady(true), 5000);
|
|
69
73
|
return () => clearTimeout(timeout);
|
|
70
74
|
}, [device]);
|
|
75
|
+
// Trigger autofocus on the center of the viewfinder.
|
|
76
|
+
// Some Android devices (e.g. Ulefone Armor series) don't auto-focus
|
|
77
|
+
// without an explicit programmatic trigger.
|
|
78
|
+
const triggerFocus = (0, react_1.useCallback)(() => {
|
|
79
|
+
try {
|
|
80
|
+
cameraRef.current?.focus({ x: width / 2, y: height / 2 });
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// focus() not supported or camera not ready — safe to ignore
|
|
84
|
+
}
|
|
85
|
+
}, []);
|
|
86
|
+
// Trigger focus once camera is ready
|
|
87
|
+
(0, react_1.useEffect)(() => {
|
|
88
|
+
if (device && hasPermission) {
|
|
89
|
+
const timer = setTimeout(triggerFocus, 500);
|
|
90
|
+
return () => clearTimeout(timer);
|
|
91
|
+
}
|
|
92
|
+
}, [device, hasPermission, triggerFocus]);
|
|
71
93
|
const checkPermissions = async () => {
|
|
72
94
|
try {
|
|
73
95
|
// First check if we already have permission from the hook
|
|
@@ -99,11 +121,22 @@ const CameraCapture = ({ mode, theme, language, onCapture, onCancel, }) => {
|
|
|
99
121
|
react_native_1.Alert.alert('Camera Permission Required', errorMsg);
|
|
100
122
|
}
|
|
101
123
|
};
|
|
124
|
+
const handleTapToFocus = (0, react_1.useCallback)((evt) => {
|
|
125
|
+
try {
|
|
126
|
+
cameraRef.current?.focus({ x: evt.nativeEvent.locationX, y: evt.nativeEvent.locationY });
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
// ignore — device may not support tap-to-focus
|
|
130
|
+
}
|
|
131
|
+
}, []);
|
|
102
132
|
const handleCapture = async () => {
|
|
103
133
|
if (isCapturing || !cameraRef.current || !device)
|
|
104
134
|
return;
|
|
105
135
|
try {
|
|
106
136
|
setIsCapturing(true);
|
|
137
|
+
// Trigger focus before capture to ensure sharpness
|
|
138
|
+
triggerFocus();
|
|
139
|
+
await new Promise(r => setTimeout(r, 300));
|
|
107
140
|
// Capture photo using react-native-vision-camera
|
|
108
141
|
const photo = await cameraRef.current.takePhoto({
|
|
109
142
|
flash: 'off',
|
|
@@ -195,9 +228,9 @@ const CameraCapture = ({ mode, theme, language, onCapture, onCancel, }) => {
|
|
|
195
228
|
react_1.default.createElement(react_native_1.Text, { style: styles.buttonText }, strings.common.cancel || 'Cancel')))));
|
|
196
229
|
}
|
|
197
230
|
return (react_1.default.createElement(react_native_1.View, { style: styles.container },
|
|
198
|
-
react_1.default.createElement(react_native_1.
|
|
231
|
+
react_1.default.createElement(react_native_1.TouchableOpacity, { style: styles.cameraContainer, activeOpacity: 1, onPress: handleTapToFocus },
|
|
199
232
|
react_1.default.createElement(react_native_vision_camera_1.Camera, { ref: cameraRef, style: react_native_1.StyleSheet.absoluteFill, device: device, isActive: true, photo: true }),
|
|
200
|
-
react_1.default.createElement(react_native_1.View, { style: styles.overlay },
|
|
233
|
+
react_1.default.createElement(react_native_1.View, { style: styles.overlay, pointerEvents: "none" },
|
|
201
234
|
react_1.default.createElement(react_native_1.View, { style: styles.frameContainer },
|
|
202
235
|
react_1.default.createElement(react_native_1.View, { style: styles.frame },
|
|
203
236
|
react_1.default.createElement(react_native_1.View, { style: [styles.corner, styles.cornerTopLeft] }),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VideoRecorder.d.ts","sourceRoot":"","sources":["../../src/components/VideoRecorder.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAmD,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"VideoRecorder.d.ts","sourceRoot":"","sources":["../../src/components/VideoRecorder.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAmD,MAAM,OAAO,CAAC;AAexE,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAE,iBAAiB,EAAmC,MAAM,oCAAoC,CAAC;AAE1I,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,sEAAsE;IACtE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,sCAAsC;IACtC,YAAY,CAAC,EAAE,mBAAmB,EAAE,CAAC;IACrC,0CAA0C;IAC1C,UAAU,CAAC,EAAE,eAAe,EAAE,CAAC;IAC/B,wCAAwC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kDAAkD;IAClD,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,wCAAwC;IACxC,UAAU,EAAE,CAAC,SAAS,EAAE,oBAAoB,KAAK,IAAI,CAAC;IACtD,iCAAiC;IACjC,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,gDAAgD;IAChD,iBAAiB,CAAC,EAAE,MAAM,OAAO,CAAC,eAAe,EAAE,CAAC,CAAC;CACtD;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,oBAAoB,EAAE,OAAO,CAAC;IAC9B,YAAY,EAAE,MAAM,CAAC;IACrB,mBAAmB,EAAE,MAAM,EAAE,CAAC;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AA2CD,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CAy/BtD,CAAC;AA4PF,eAAe,aAAa,CAAC"}
|
|
@@ -103,7 +103,11 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
|
|
|
103
103
|
const [hasPermission, setHasPermission] = (0, react_1.useState)(false);
|
|
104
104
|
const cameraRef = (0, react_1.useRef)(null);
|
|
105
105
|
const { hasPermission: cameraPermission, requestPermission } = (0, react_native_vision_camera_1.useCameraPermission)();
|
|
106
|
-
|
|
106
|
+
// Prefer wide-angle (main) front camera — some Android devices expose
|
|
107
|
+
// multiple front sensors and the default may lack autofocus.
|
|
108
|
+
const device = (0, react_native_vision_camera_1.useCameraDevice)('front', {
|
|
109
|
+
physicalDevices: ['wide-angle-camera', 'ultra-wide-angle-camera'],
|
|
110
|
+
});
|
|
107
111
|
const [deviceReady, setDeviceReady] = (0, react_1.useState)(!!device);
|
|
108
112
|
const fadeAnim = (0, react_1.useRef)(new react_native_1.Animated.Value(0)).current;
|
|
109
113
|
const scaleAnim = (0, react_1.useRef)(new react_native_1.Animated.Value(1)).current;
|
|
@@ -116,6 +120,17 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
|
|
|
116
120
|
const videoRecordingRef = (0, react_1.useRef)(null);
|
|
117
121
|
const isRecordingRef = (0, react_1.useRef)(false);
|
|
118
122
|
const recordingTimeoutRef = (0, react_1.useRef)(null);
|
|
123
|
+
// Trigger autofocus on center — some Android devices (e.g. Ulefone Armor)
|
|
124
|
+
// don't auto-focus without an explicit programmatic trigger.
|
|
125
|
+
const triggerFocus = (0, react_1.useCallback)(() => {
|
|
126
|
+
try {
|
|
127
|
+
const { width: w, height: h } = react_native_1.Dimensions.get('window');
|
|
128
|
+
cameraRef.current?.focus({ x: w / 2, y: h / 2 });
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
// focus() not supported or camera not ready — safe to ignore
|
|
132
|
+
}
|
|
133
|
+
}, []);
|
|
119
134
|
const minDurationMs = 8000;
|
|
120
135
|
const totalDuration = duration || Math.max(minDurationMs, challenges.reduce((sum, c) => sum + c.duration_ms, 0) + 2000);
|
|
121
136
|
(0, react_1.useEffect)(() => {
|
|
@@ -220,6 +235,9 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
|
|
|
220
235
|
(0, react_1.useEffect)(() => {
|
|
221
236
|
if (phase !== 'countdown')
|
|
222
237
|
return;
|
|
238
|
+
// Trigger focus at the start of countdown so the camera has ~3s to lock focus
|
|
239
|
+
if (countdown === 3)
|
|
240
|
+
triggerFocus();
|
|
223
241
|
if (countdown > 0) {
|
|
224
242
|
react_native_1.Animated.sequence([
|
|
225
243
|
react_native_1.Animated.timing(scaleAnim, {
|
|
@@ -639,6 +657,8 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
|
|
|
639
657
|
setPhase('recording');
|
|
640
658
|
recordingStartTime.current = Date.now();
|
|
641
659
|
isRecordingRef.current = true;
|
|
660
|
+
// Ensure camera is focused before recording
|
|
661
|
+
triggerFocus();
|
|
642
662
|
// Reset completed challenges ref when starting new recording
|
|
643
663
|
completedChallengesRef.current = [];
|
|
644
664
|
setCompletedChallenges([]);
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Handles ID document photo capture
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import React, { useState, useRef, useEffect } from 'react';
|
|
6
|
+
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
|
7
7
|
import {
|
|
8
8
|
View,
|
|
9
9
|
Text,
|
|
@@ -45,8 +45,12 @@ export const CameraCapture: React.FC<CameraCaptureProps> = ({
|
|
|
45
45
|
}
|
|
46
46
|
const strings = getStrings();
|
|
47
47
|
|
|
48
|
-
// Get camera device (back camera for document capture)
|
|
49
|
-
|
|
48
|
+
// Get camera device (back camera for document capture).
|
|
49
|
+
// Prefer wide-angle (main) camera — some Android devices default to
|
|
50
|
+
// macro/depth sensors that have fixed focus and produce blurry images.
|
|
51
|
+
const device = useCameraDevice('back', {
|
|
52
|
+
physicalDevices: ['wide-angle-camera', 'ultra-wide-angle-camera', 'telephoto-camera'],
|
|
53
|
+
});
|
|
50
54
|
const [deviceReady, setDeviceReady] = useState(!!device);
|
|
51
55
|
|
|
52
56
|
useEffect(() => {
|
|
@@ -63,6 +67,25 @@ export const CameraCapture: React.FC<CameraCaptureProps> = ({
|
|
|
63
67
|
return () => clearTimeout(timeout);
|
|
64
68
|
}, [device]);
|
|
65
69
|
|
|
70
|
+
// Trigger autofocus on the center of the viewfinder.
|
|
71
|
+
// Some Android devices (e.g. Ulefone Armor series) don't auto-focus
|
|
72
|
+
// without an explicit programmatic trigger.
|
|
73
|
+
const triggerFocus = useCallback(() => {
|
|
74
|
+
try {
|
|
75
|
+
cameraRef.current?.focus({ x: width / 2, y: height / 2 });
|
|
76
|
+
} catch {
|
|
77
|
+
// focus() not supported or camera not ready — safe to ignore
|
|
78
|
+
}
|
|
79
|
+
}, []);
|
|
80
|
+
|
|
81
|
+
// Trigger focus once camera is ready
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (device && hasPermission) {
|
|
84
|
+
const timer = setTimeout(triggerFocus, 500);
|
|
85
|
+
return () => clearTimeout(timer);
|
|
86
|
+
}
|
|
87
|
+
}, [device, hasPermission, triggerFocus]);
|
|
88
|
+
|
|
66
89
|
const checkPermissions = async () => {
|
|
67
90
|
try {
|
|
68
91
|
// First check if we already have permission from the hook
|
|
@@ -98,12 +121,24 @@ export const CameraCapture: React.FC<CameraCaptureProps> = ({
|
|
|
98
121
|
}
|
|
99
122
|
};
|
|
100
123
|
|
|
124
|
+
const handleTapToFocus = useCallback((evt: any) => {
|
|
125
|
+
try {
|
|
126
|
+
cameraRef.current?.focus({ x: evt.nativeEvent.locationX, y: evt.nativeEvent.locationY });
|
|
127
|
+
} catch {
|
|
128
|
+
// ignore — device may not support tap-to-focus
|
|
129
|
+
}
|
|
130
|
+
}, []);
|
|
131
|
+
|
|
101
132
|
const handleCapture = async () => {
|
|
102
133
|
if (isCapturing || !cameraRef.current || !device) return;
|
|
103
134
|
|
|
104
135
|
try {
|
|
105
136
|
setIsCapturing(true);
|
|
106
137
|
|
|
138
|
+
// Trigger focus before capture to ensure sharpness
|
|
139
|
+
triggerFocus();
|
|
140
|
+
await new Promise(r => setTimeout(r, 300));
|
|
141
|
+
|
|
107
142
|
// Capture photo using react-native-vision-camera
|
|
108
143
|
const photo = await cameraRef.current.takePhoto({
|
|
109
144
|
flash: 'off',
|
|
@@ -231,8 +266,8 @@ export const CameraCapture: React.FC<CameraCaptureProps> = ({
|
|
|
231
266
|
|
|
232
267
|
return (
|
|
233
268
|
<View style={styles.container}>
|
|
234
|
-
{/* Camera View */}
|
|
235
|
-
<
|
|
269
|
+
{/* Camera View — tap anywhere to trigger focus */}
|
|
270
|
+
<TouchableOpacity style={styles.cameraContainer} activeOpacity={1} onPress={handleTapToFocus}>
|
|
236
271
|
<Camera
|
|
237
272
|
ref={cameraRef}
|
|
238
273
|
style={StyleSheet.absoluteFill}
|
|
@@ -242,7 +277,7 @@ export const CameraCapture: React.FC<CameraCaptureProps> = ({
|
|
|
242
277
|
/>
|
|
243
278
|
|
|
244
279
|
{/* Document Frame Overlay */}
|
|
245
|
-
<View style={styles.overlay}>
|
|
280
|
+
<View style={styles.overlay} pointerEvents="none">
|
|
246
281
|
<View style={styles.frameContainer}>
|
|
247
282
|
<View style={styles.frame}>
|
|
248
283
|
<View style={[styles.corner, styles.cornerTopLeft]} />
|
|
@@ -252,7 +287,7 @@ export const CameraCapture: React.FC<CameraCaptureProps> = ({
|
|
|
252
287
|
</View>
|
|
253
288
|
</View>
|
|
254
289
|
</View>
|
|
255
|
-
</
|
|
290
|
+
</TouchableOpacity>
|
|
256
291
|
|
|
257
292
|
{/* Instructions */}
|
|
258
293
|
<View style={styles.instructionsContainer}>
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
Platform,
|
|
15
15
|
Alert,
|
|
16
16
|
ActivityIndicator,
|
|
17
|
+
Dimensions,
|
|
17
18
|
} from 'react-native';
|
|
18
19
|
import { Camera, useCameraDevice, useCameraPermission } from 'react-native-vision-camera';
|
|
19
20
|
import { request, PERMISSIONS, RESULTS } from 'react-native-permissions';
|
|
@@ -131,7 +132,11 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
131
132
|
const [hasPermission, setHasPermission] = useState(false);
|
|
132
133
|
const cameraRef = useRef<Camera>(null);
|
|
133
134
|
const { hasPermission: cameraPermission, requestPermission } = useCameraPermission();
|
|
134
|
-
|
|
135
|
+
// Prefer wide-angle (main) front camera — some Android devices expose
|
|
136
|
+
// multiple front sensors and the default may lack autofocus.
|
|
137
|
+
const device = useCameraDevice('front', {
|
|
138
|
+
physicalDevices: ['wide-angle-camera', 'ultra-wide-angle-camera'],
|
|
139
|
+
});
|
|
135
140
|
const [deviceReady, setDeviceReady] = useState(!!device);
|
|
136
141
|
|
|
137
142
|
const fadeAnim = useRef(new Animated.Value(0)).current;
|
|
@@ -146,6 +151,17 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
146
151
|
const videoRecordingRef = useRef<any>(null);
|
|
147
152
|
const isRecordingRef = useRef<boolean>(false);
|
|
148
153
|
const recordingTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
154
|
+
// Trigger autofocus on center — some Android devices (e.g. Ulefone Armor)
|
|
155
|
+
// don't auto-focus without an explicit programmatic trigger.
|
|
156
|
+
const triggerFocus = useCallback(() => {
|
|
157
|
+
try {
|
|
158
|
+
const { width: w, height: h } = Dimensions.get('window');
|
|
159
|
+
cameraRef.current?.focus({ x: w / 2, y: h / 2 });
|
|
160
|
+
} catch {
|
|
161
|
+
// focus() not supported or camera not ready — safe to ignore
|
|
162
|
+
}
|
|
163
|
+
}, []);
|
|
164
|
+
|
|
149
165
|
const minDurationMs = 8000;
|
|
150
166
|
const totalDuration = duration || Math.max(
|
|
151
167
|
minDurationMs,
|
|
@@ -260,6 +276,9 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
260
276
|
useEffect(() => {
|
|
261
277
|
if (phase !== 'countdown') return;
|
|
262
278
|
|
|
279
|
+
// Trigger focus at the start of countdown so the camera has ~3s to lock focus
|
|
280
|
+
if (countdown === 3) triggerFocus();
|
|
281
|
+
|
|
263
282
|
if (countdown > 0) {
|
|
264
283
|
Animated.sequence([
|
|
265
284
|
Animated.timing(scaleAnim, {
|
|
@@ -745,6 +764,8 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
745
764
|
setPhase('recording');
|
|
746
765
|
recordingStartTime.current = Date.now();
|
|
747
766
|
isRecordingRef.current = true;
|
|
767
|
+
// Ensure camera is focused before recording
|
|
768
|
+
triggerFocus();
|
|
748
769
|
// Reset completed challenges ref when starting new recording
|
|
749
770
|
completedChallengesRef.current = [];
|
|
750
771
|
setCompletedChallenges([]);
|