@hexar/biometric-identity-sdk-react-native 1.20.0 → 1.22.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,CAy/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,CAwhCtD,CAAC;AA4PF,eAAe,aAAa,CAAC"}
@@ -101,6 +101,7 @@ 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);
104
105
  const cameraRef = (0, react_1.useRef)(null);
105
106
  const { hasPermission: cameraPermission, requestPermission } = (0, react_native_vision_camera_1.useCameraPermission)();
106
107
  // Prefer wide-angle (main) front camera — some Android devices expose
@@ -233,7 +234,11 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
233
234
  initChallenges();
234
235
  }, [propChallenges, instructions, onFetchChallenges, smartMode, duration, language]);
235
236
  (0, react_1.useEffect)(() => {
236
- if (phase !== 'countdown')
237
+ // Don't start countdown until the native camera session is ready.
238
+ // On Motorola and other slow-to-initialize Android OEMs, isActive becoming
239
+ // true before the native view is registered causes IllegalViewOperationException
240
+ // inside CameraViewModule.findCameraView. We wait for onInitialized to fire.
241
+ if (phase !== 'countdown' || !isCameraReady)
237
242
  return;
238
243
  // Trigger focus at the start of countdown so the camera has ~3s to lock focus
239
244
  if (countdown === 3)
@@ -257,7 +262,7 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
257
262
  else {
258
263
  startRecording();
259
264
  }
260
- }, [countdown, phase]);
265
+ }, [countdown, phase, isCameraReady]);
261
266
  (0, react_1.useEffect)(() => {
262
267
  if (phase === 'recording') {
263
268
  react_native_1.Animated.loop(react_native_1.Animated.sequence([
@@ -309,6 +314,19 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
309
314
  setPhase('loading');
310
315
  react_native_1.Alert.alert('Recording Error', 'Failed to record video. Please try again.', [{ text: 'OK', onPress: onCancel }]);
311
316
  }, [onCancel]);
317
+ // Safety timeout: if the camera session never fires onInitialized within
318
+ // 10 seconds, treat it as a hardware failure and send the user back.
319
+ (0, react_1.useEffect)(() => {
320
+ if (isCameraReady)
321
+ return;
322
+ const timeout = setTimeout(() => {
323
+ if (!isCameraReady) {
324
+ biometric_identity_sdk_core_1.logger.error('Camera onInitialized timed out — hardware may be unavailable');
325
+ handleRecordingError(new Error('Camera initialization timed out'));
326
+ }
327
+ }, 10000);
328
+ return () => clearTimeout(timeout);
329
+ }, [isCameraReady, handleRecordingError]);
312
330
  const handleVideoComplete = (0, react_1.useCallback)(async (video) => {
313
331
  if (phase === 'loading' && !video) {
314
332
  return;
@@ -362,7 +380,12 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
362
380
  };
363
381
  biometric_identity_sdk_core_1.logger.info('Video recording completed:', result.frames.length, 'frames,', (actualDuration / 1000).toFixed(1) + 's', 'challenges:', currentCompletedChallenges.length);
364
382
  isRecordingRef.current = false;
365
- onComplete(result);
383
+ // Delay onComplete so Vision Camera's native session (especially
384
+ // multi-physical-camera sessions opened via physicalDevices) has time
385
+ // to fully close before the parent unmounts the <Camera> view.
386
+ // Without this delay, CameraViewModule.findCameraView throws
387
+ // IllegalViewOperationException on Android 13 (SDK 33).
388
+ setTimeout(() => onComplete(result), 250);
366
389
  }
367
390
  catch (error) {
368
391
  biometric_identity_sdk_core_1.logger.error('Error processing video:', error);
@@ -512,7 +535,8 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
512
535
  challengesCompleted: currentCompletedChallenges,
513
536
  sessionId,
514
537
  };
515
- onComplete(result);
538
+ setPhase('processing');
539
+ setTimeout(() => onComplete(result), 250);
516
540
  }
517
541
  else {
518
542
  biometric_identity_sdk_core_1.logger.info('Stopping recording with frames (fallback):', currentFrames.length);
@@ -524,7 +548,8 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
524
548
  challengesCompleted: currentCompletedChallenges,
525
549
  sessionId,
526
550
  };
527
- onComplete(result);
551
+ setPhase('processing');
552
+ setTimeout(() => onComplete(result), 250);
528
553
  }
529
554
  }
530
555
  }
@@ -557,7 +582,7 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
557
582
  };
558
583
  setTimeout(() => {
559
584
  onComplete(result);
560
- }, 100);
585
+ }, 250);
561
586
  }
562
587
  }, [frames, completedChallenges, challenges, sessionId, onComplete, minDurationMs, resetAndRetry, strings]);
563
588
  const runChallenge = (0, react_1.useCallback)((index) => {
@@ -600,7 +625,8 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
600
625
  challengesCompleted: currentCompletedChallenges,
601
626
  sessionId,
602
627
  };
603
- onComplete(result);
628
+ setPhase('processing');
629
+ setTimeout(() => onComplete(result), 250);
604
630
  }
605
631
  }
606
632
  return;
@@ -781,7 +807,10 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
781
807
  }
782
808
  return (react_1.default.createElement(react_native_1.View, { style: styles.container },
783
809
  react_1.default.createElement(react_native_1.View, { style: styles.cameraContainer },
784
- 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 }),
810
+ react_1.default.createElement(react_native_vision_camera_1.Camera, { ref: cameraRef, style: react_native_1.StyleSheet.absoluteFill, device: device, isActive: true, video: true, photo: true, audio: false, onInitialized: () => setIsCameraReady(true), onError: (error) => {
811
+ biometric_identity_sdk_core_1.logger.error('Camera hardware error:', error);
812
+ handleRecordingError(error);
813
+ } }),
785
814
  react_1.default.createElement(react_native_1.View, { style: styles.overlay },
786
815
  react_1.default.createElement(react_native_1.View, { style: [
787
816
  styles.faceOval,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hexar/biometric-identity-sdk-react-native",
3
- "version": "1.20.0",
3
+ "version": "1.22.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,6 +130,7 @@ 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);
133
134
  const cameraRef = useRef<Camera>(null);
134
135
  const { hasPermission: cameraPermission, requestPermission } = useCameraPermission();
135
136
  // Prefer wide-angle (main) front camera — some Android devices expose
@@ -274,7 +275,11 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
274
275
  }, [propChallenges, instructions, onFetchChallenges, smartMode, duration, language]);
275
276
 
276
277
  useEffect(() => {
277
- if (phase !== 'countdown') return;
278
+ // Don't start countdown until the native camera session is ready.
279
+ // On Motorola and other slow-to-initialize Android OEMs, isActive becoming
280
+ // true before the native view is registered causes IllegalViewOperationException
281
+ // inside CameraViewModule.findCameraView. We wait for onInitialized to fire.
282
+ if (phase !== 'countdown' || !isCameraReady) return;
278
283
 
279
284
  // Trigger focus at the start of countdown so the camera has ~3s to lock focus
280
285
  if (countdown === 3) triggerFocus();
@@ -298,7 +303,7 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
298
303
  } else {
299
304
  startRecording();
300
305
  }
301
- }, [countdown, phase]);
306
+ }, [countdown, phase, isCameraReady]);
302
307
 
303
308
  useEffect(() => {
304
309
  if (phase === 'recording') {
@@ -364,6 +369,19 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
364
369
  );
365
370
  }, [onCancel]);
366
371
 
372
+ // Safety timeout: if the camera session never fires onInitialized within
373
+ // 10 seconds, treat it as a hardware failure and send the user back.
374
+ useEffect(() => {
375
+ if (isCameraReady) return;
376
+ const timeout = setTimeout(() => {
377
+ if (!isCameraReady) {
378
+ logger.error('Camera onInitialized timed out — hardware may be unavailable');
379
+ handleRecordingError(new Error('Camera initialization timed out'));
380
+ }
381
+ }, 10000);
382
+ return () => clearTimeout(timeout);
383
+ }, [isCameraReady, handleRecordingError]);
384
+
367
385
  const handleVideoComplete = useCallback(async (video: any) => {
368
386
  if (phase === 'loading' && !video) {
369
387
  return;
@@ -432,7 +450,12 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
432
450
  logger.info('Video recording completed:', result.frames.length, 'frames,', (actualDuration / 1000).toFixed(1) + 's', 'challenges:', currentCompletedChallenges.length);
433
451
 
434
452
  isRecordingRef.current = false;
435
- onComplete(result);
453
+ // Delay onComplete so Vision Camera's native session (especially
454
+ // multi-physical-camera sessions opened via physicalDevices) has time
455
+ // to fully close before the parent unmounts the <Camera> view.
456
+ // Without this delay, CameraViewModule.findCameraView throws
457
+ // IllegalViewOperationException on Android 13 (SDK 33).
458
+ setTimeout(() => onComplete(result), 250);
436
459
  } catch (error) {
437
460
  logger.error('Error processing video:', error);
438
461
  setPhase('recording');
@@ -598,7 +621,8 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
598
621
  challengesCompleted: currentCompletedChallenges,
599
622
  sessionId,
600
623
  };
601
- onComplete(result);
624
+ setPhase('processing');
625
+ setTimeout(() => onComplete(result), 250);
602
626
  } else {
603
627
  logger.info('Stopping recording with frames (fallback):', currentFrames.length);
604
628
  const result: VideoRecordingResult = {
@@ -609,7 +633,8 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
609
633
  challengesCompleted: currentCompletedChallenges,
610
634
  sessionId,
611
635
  };
612
- onComplete(result);
636
+ setPhase('processing');
637
+ setTimeout(() => onComplete(result), 250);
613
638
  }
614
639
  }
615
640
  } else {
@@ -649,7 +674,7 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
649
674
 
650
675
  setTimeout(() => {
651
676
  onComplete(result);
652
- }, 100);
677
+ }, 250);
653
678
  }
654
679
  }, [frames, completedChallenges, challenges, sessionId, onComplete, minDurationMs, resetAndRetry, strings]);
655
680
 
@@ -697,7 +722,8 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
697
722
  challengesCompleted: currentCompletedChallenges,
698
723
  sessionId,
699
724
  };
700
- onComplete(result);
725
+ setPhase('processing');
726
+ setTimeout(() => onComplete(result), 250);
701
727
  }
702
728
  }
703
729
  return;
@@ -952,10 +978,15 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
952
978
  ref={cameraRef}
953
979
  style={StyleSheet.absoluteFill}
954
980
  device={device}
955
- isActive={phase === 'recording' || phase === 'countdown'}
981
+ isActive={true}
956
982
  video={true}
957
983
  photo={true}
958
984
  audio={false}
985
+ onInitialized={() => setIsCameraReady(true)}
986
+ onError={(error) => {
987
+ logger.error('Camera hardware error:', error);
988
+ handleRecordingError(error);
989
+ }}
959
990
  />
960
991
 
961
992
  {/* Face Oval Overlay */}