@hexar/biometric-identity-sdk-react-native 1.21.0 → 1.23.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":"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,CAigCtD,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,CAqiCtD,CAAC;AA4PF,eAAe,aAAa,CAAC"}
@@ -101,12 +101,21 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
101
101
  const framesRef = (0, react_1.useRef)([]);
102
102
  const [guidanceText, setGuidanceText] = (0, react_1.useState)(null);
103
103
  const [hasPermission, setHasPermission] = (0, react_1.useState)(false);
104
+ const [isCameraReady, setIsCameraReady] = (0, react_1.useState)(false);
105
+ // Controlled via stopRecording — set to false before calling onComplete so
106
+ // VisionCamera tears down the native session and any in-flight takePhoto()
107
+ // calls fail gracefully instead of crashing with IllegalViewOperationException
108
+ // when the parent unmounts the <Camera> view underneath them.
109
+ const [isCameraActive, setIsCameraActive] = (0, react_1.useState)(true);
104
110
  const cameraRef = (0, react_1.useRef)(null);
105
111
  const { hasPermission: cameraPermission, requestPermission } = (0, react_native_vision_camera_1.useCameraPermission)();
106
112
  // Prefer wide-angle (main) front camera — some Android devices expose
107
113
  // multiple front sensors and the default may lack autofocus.
114
+ // Only request wide-angle — ultra-wide opens a multi-physical-camera session
115
+ // that is slow to release, causing CameraViewModule.findCameraView to throw
116
+ // IllegalViewOperationException when the component unmounts after recording.
108
117
  const device = (0, react_native_vision_camera_1.useCameraDevice)('front', {
109
- physicalDevices: ['wide-angle-camera', 'ultra-wide-angle-camera'],
118
+ physicalDevices: ['wide-angle-camera'],
110
119
  });
111
120
  const [deviceReady, setDeviceReady] = (0, react_1.useState)(!!device);
112
121
  const fadeAnim = (0, react_1.useRef)(new react_native_1.Animated.Value(0)).current;
@@ -233,7 +242,11 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
233
242
  initChallenges();
234
243
  }, [propChallenges, instructions, onFetchChallenges, smartMode, duration, language]);
235
244
  (0, react_1.useEffect)(() => {
236
- if (phase !== 'countdown')
245
+ // Don't start countdown until the native camera session is ready.
246
+ // On Motorola and other slow-to-initialize Android OEMs, isActive becoming
247
+ // true before the native view is registered causes IllegalViewOperationException
248
+ // inside CameraViewModule.findCameraView. We wait for onInitialized to fire.
249
+ if (phase !== 'countdown' || !isCameraReady)
237
250
  return;
238
251
  // Trigger focus at the start of countdown so the camera has ~3s to lock focus
239
252
  if (countdown === 3)
@@ -257,7 +270,7 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
257
270
  else {
258
271
  startRecording();
259
272
  }
260
- }, [countdown, phase]);
273
+ }, [countdown, phase, isCameraReady]);
261
274
  (0, react_1.useEffect)(() => {
262
275
  if (phase === 'recording') {
263
276
  react_native_1.Animated.loop(react_native_1.Animated.sequence([
@@ -302,6 +315,7 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
302
315
  setChallengeProgress(0);
303
316
  setOverallProgress(0);
304
317
  recordingStartTime.current = 0;
318
+ setIsCameraActive(true);
305
319
  setPhase('countdown');
306
320
  setCountdown(3);
307
321
  }, []);
@@ -309,6 +323,19 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
309
323
  setPhase('loading');
310
324
  react_native_1.Alert.alert('Recording Error', 'Failed to record video. Please try again.', [{ text: 'OK', onPress: onCancel }]);
311
325
  }, [onCancel]);
326
+ // Safety timeout: if the camera session never fires onInitialized within
327
+ // 10 seconds, treat it as a hardware failure and send the user back.
328
+ (0, react_1.useEffect)(() => {
329
+ if (isCameraReady)
330
+ return;
331
+ const timeout = setTimeout(() => {
332
+ if (!isCameraReady) {
333
+ biometric_identity_sdk_core_1.logger.error('Camera onInitialized timed out — hardware may be unavailable');
334
+ handleRecordingError(new Error('Camera initialization timed out'));
335
+ }
336
+ }, 10000);
337
+ return () => clearTimeout(timeout);
338
+ }, [isCameraReady, handleRecordingError]);
312
339
  const handleVideoComplete = (0, react_1.useCallback)(async (video) => {
313
340
  if (phase === 'loading' && !video) {
314
341
  return;
@@ -362,11 +389,11 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
362
389
  };
363
390
  biometric_identity_sdk_core_1.logger.info('Video recording completed:', result.frames.length, 'frames,', (actualDuration / 1000).toFixed(1) + 's', 'challenges:', currentCompletedChallenges.length);
364
391
  isRecordingRef.current = false;
365
- // Delay onComplete so Vision Camera's native session (especially
366
- // multi-physical-camera sessions opened via physicalDevices) has time
367
- // to fully close before the parent unmounts the <Camera> view.
368
- // Without this delay, CameraViewModule.findCameraView throws
369
- // IllegalViewOperationException on Android 13 (SDK 33).
392
+ // Deactivate the camera first so VisionCamera tears down its native
393
+ // session. Any in-flight takePhoto() calls will then fail gracefully
394
+ // rather than crashing with IllegalViewOperationException when the
395
+ // parent unmounts the <Camera> view underneath them.
396
+ setIsCameraActive(false);
370
397
  setTimeout(() => onComplete(result), 250);
371
398
  }
372
399
  catch (error) {
@@ -518,6 +545,7 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
518
545
  sessionId,
519
546
  };
520
547
  setPhase('processing');
548
+ setIsCameraActive(false);
521
549
  setTimeout(() => onComplete(result), 250);
522
550
  }
523
551
  else {
@@ -531,6 +559,7 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
531
559
  sessionId,
532
560
  };
533
561
  setPhase('processing');
562
+ setIsCameraActive(false);
534
563
  setTimeout(() => onComplete(result), 250);
535
564
  }
536
565
  }
@@ -562,6 +591,7 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
562
591
  challengesCompleted: currentCompletedChallenges,
563
592
  sessionId,
564
593
  };
594
+ setIsCameraActive(false);
565
595
  setTimeout(() => {
566
596
  onComplete(result);
567
597
  }, 250);
@@ -608,6 +638,7 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
608
638
  sessionId,
609
639
  };
610
640
  setPhase('processing');
641
+ setIsCameraActive(false);
611
642
  setTimeout(() => onComplete(result), 250);
612
643
  }
613
644
  }
@@ -789,7 +820,10 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
789
820
  }
790
821
  return (react_1.default.createElement(react_native_1.View, { style: styles.container },
791
822
  react_1.default.createElement(react_native_1.View, { style: styles.cameraContainer },
792
- react_1.default.createElement(react_native_vision_camera_1.Camera, { ref: cameraRef, style: react_native_1.StyleSheet.absoluteFill, device: device, isActive: phase === 'recording' || phase === 'countdown', video: true, photo: true, audio: false }),
823
+ react_1.default.createElement(react_native_vision_camera_1.Camera, { ref: cameraRef, style: react_native_1.StyleSheet.absoluteFill, device: device, isActive: isCameraActive, video: true, photo: true, audio: false, onInitialized: () => setIsCameraReady(true), onError: (error) => {
824
+ biometric_identity_sdk_core_1.logger.error('Camera hardware error:', error);
825
+ handleRecordingError(error);
826
+ } }),
793
827
  react_1.default.createElement(react_native_1.View, { style: styles.overlay },
794
828
  react_1.default.createElement(react_native_1.View, { style: [
795
829
  styles.faceOval,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hexar/biometric-identity-sdk-react-native",
3
- "version": "1.21.0",
3
+ "version": "1.23.0",
4
4
  "description": "React Native wrapper for Biometric Identity SDK",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -130,12 +130,21 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
130
130
  const framesRef = useRef<string[]>([]);
131
131
  const [guidanceText, setGuidanceText] = useState<string | null>(null);
132
132
  const [hasPermission, setHasPermission] = useState(false);
133
+ const [isCameraReady, setIsCameraReady] = useState(false);
134
+ // Controlled via stopRecording — set to false before calling onComplete so
135
+ // VisionCamera tears down the native session and any in-flight takePhoto()
136
+ // calls fail gracefully instead of crashing with IllegalViewOperationException
137
+ // when the parent unmounts the <Camera> view underneath them.
138
+ const [isCameraActive, setIsCameraActive] = useState(true);
133
139
  const cameraRef = useRef<Camera>(null);
134
140
  const { hasPermission: cameraPermission, requestPermission } = useCameraPermission();
135
141
  // Prefer wide-angle (main) front camera — some Android devices expose
136
142
  // multiple front sensors and the default may lack autofocus.
143
+ // Only request wide-angle — ultra-wide opens a multi-physical-camera session
144
+ // that is slow to release, causing CameraViewModule.findCameraView to throw
145
+ // IllegalViewOperationException when the component unmounts after recording.
137
146
  const device = useCameraDevice('front', {
138
- physicalDevices: ['wide-angle-camera', 'ultra-wide-angle-camera'],
147
+ physicalDevices: ['wide-angle-camera'],
139
148
  });
140
149
  const [deviceReady, setDeviceReady] = useState(!!device);
141
150
 
@@ -274,7 +283,11 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
274
283
  }, [propChallenges, instructions, onFetchChallenges, smartMode, duration, language]);
275
284
 
276
285
  useEffect(() => {
277
- if (phase !== 'countdown') return;
286
+ // Don't start countdown until the native camera session is ready.
287
+ // On Motorola and other slow-to-initialize Android OEMs, isActive becoming
288
+ // true before the native view is registered causes IllegalViewOperationException
289
+ // inside CameraViewModule.findCameraView. We wait for onInitialized to fire.
290
+ if (phase !== 'countdown' || !isCameraReady) return;
278
291
 
279
292
  // Trigger focus at the start of countdown so the camera has ~3s to lock focus
280
293
  if (countdown === 3) triggerFocus();
@@ -298,7 +311,7 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
298
311
  } else {
299
312
  startRecording();
300
313
  }
301
- }, [countdown, phase]);
314
+ }, [countdown, phase, isCameraReady]);
302
315
 
303
316
  useEffect(() => {
304
317
  if (phase === 'recording') {
@@ -351,6 +364,7 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
351
364
  setChallengeProgress(0);
352
365
  setOverallProgress(0);
353
366
  recordingStartTime.current = 0;
367
+ setIsCameraActive(true);
354
368
  setPhase('countdown');
355
369
  setCountdown(3);
356
370
  }, []);
@@ -364,6 +378,19 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
364
378
  );
365
379
  }, [onCancel]);
366
380
 
381
+ // Safety timeout: if the camera session never fires onInitialized within
382
+ // 10 seconds, treat it as a hardware failure and send the user back.
383
+ useEffect(() => {
384
+ if (isCameraReady) return;
385
+ const timeout = setTimeout(() => {
386
+ if (!isCameraReady) {
387
+ logger.error('Camera onInitialized timed out — hardware may be unavailable');
388
+ handleRecordingError(new Error('Camera initialization timed out'));
389
+ }
390
+ }, 10000);
391
+ return () => clearTimeout(timeout);
392
+ }, [isCameraReady, handleRecordingError]);
393
+
367
394
  const handleVideoComplete = useCallback(async (video: any) => {
368
395
  if (phase === 'loading' && !video) {
369
396
  return;
@@ -432,11 +459,11 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
432
459
  logger.info('Video recording completed:', result.frames.length, 'frames,', (actualDuration / 1000).toFixed(1) + 's', 'challenges:', currentCompletedChallenges.length);
433
460
 
434
461
  isRecordingRef.current = false;
435
- // Delay onComplete so Vision Camera's native session (especially
436
- // multi-physical-camera sessions opened via physicalDevices) has time
437
- // to fully close before the parent unmounts the <Camera> view.
438
- // Without this delay, CameraViewModule.findCameraView throws
439
- // IllegalViewOperationException on Android 13 (SDK 33).
462
+ // Deactivate the camera first so VisionCamera tears down its native
463
+ // session. Any in-flight takePhoto() calls will then fail gracefully
464
+ // rather than crashing with IllegalViewOperationException when the
465
+ // parent unmounts the <Camera> view underneath them.
466
+ setIsCameraActive(false);
440
467
  setTimeout(() => onComplete(result), 250);
441
468
  } catch (error) {
442
469
  logger.error('Error processing video:', error);
@@ -604,6 +631,7 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
604
631
  sessionId,
605
632
  };
606
633
  setPhase('processing');
634
+ setIsCameraActive(false);
607
635
  setTimeout(() => onComplete(result), 250);
608
636
  } else {
609
637
  logger.info('Stopping recording with frames (fallback):', currentFrames.length);
@@ -616,6 +644,7 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
616
644
  sessionId,
617
645
  };
618
646
  setPhase('processing');
647
+ setIsCameraActive(false);
619
648
  setTimeout(() => onComplete(result), 250);
620
649
  }
621
650
  }
@@ -654,6 +683,7 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
654
683
  sessionId,
655
684
  };
656
685
 
686
+ setIsCameraActive(false);
657
687
  setTimeout(() => {
658
688
  onComplete(result);
659
689
  }, 250);
@@ -705,6 +735,7 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
705
735
  sessionId,
706
736
  };
707
737
  setPhase('processing');
738
+ setIsCameraActive(false);
708
739
  setTimeout(() => onComplete(result), 250);
709
740
  }
710
741
  }
@@ -960,10 +991,15 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
960
991
  ref={cameraRef}
961
992
  style={StyleSheet.absoluteFill}
962
993
  device={device}
963
- isActive={phase === 'recording' || phase === 'countdown'}
994
+ isActive={isCameraActive}
964
995
  video={true}
965
996
  photo={true}
966
997
  audio={false}
998
+ onInitialized={() => setIsCameraReady(true)}
999
+ onError={(error) => {
1000
+ logger.error('Camera hardware error:', error);
1001
+ handleRecordingError(error);
1002
+ }}
967
1003
  />
968
1004
 
969
1005
  {/* Face Oval Overlay */}