@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.
@@ -1 +1 @@
1
- {"version":3,"file":"CameraCapture.d.ts","sourceRoot":"","sources":["../../src/components/CameraCapture.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAsC,MAAM,OAAO,CAAC;AAa3D,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,CAqQtD,CAAC;AA0JF,eAAe,aAAa,CAAC"}
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
- const device = (0, react_native_vision_camera_1.useCameraDevice)('back');
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.View, { style: styles.cameraContainer },
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;AAcxE,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,CAq+BtD,CAAC;AA4PF,eAAe,aAAa,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
- const device = (0, react_native_vision_camera_1.useCameraDevice)('front');
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hexar/biometric-identity-sdk-react-native",
3
- "version": "1.17.0",
3
+ "version": "1.19.0",
4
4
  "description": "React Native wrapper for Biometric Identity SDK",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -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
- const device = useCameraDevice('back');
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
- <View style={styles.cameraContainer}>
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
- </View>
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
- const device = useCameraDevice('front');
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([]);