@hexar/biometric-identity-sdk-react-native 1.1.17 → 1.1.19

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;AAaxE,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;AAiDD,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CAi2BtD,CAAC;AA4OF,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;AAaxE,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;AAiDD,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CAw6BtD,CAAC;AA4OF,eAAe,aAAa,CAAC"}
@@ -102,6 +102,7 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
102
102
  const [challengeProgress, setChallengeProgress] = (0, react_1.useState)(0);
103
103
  const [overallProgress, setOverallProgress] = (0, react_1.useState)(0);
104
104
  const [completedChallenges, setCompletedChallenges] = (0, react_1.useState)([]);
105
+ const completedChallengesRef = (0, react_1.useRef)([]);
105
106
  const [frames, setFrames] = (0, react_1.useState)([]);
106
107
  const framesRef = (0, react_1.useRef)([]);
107
108
  const [hasPermission, setHasPermission] = (0, react_1.useState)(false);
@@ -200,6 +201,13 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
200
201
  },
201
202
  ];
202
203
  }
204
+ biometric_identity_sdk_core_1.logger.info('VideoRecorder: Challenges initialized', {
205
+ challengeCount: challengeList.length,
206
+ challengeActions: challengeList.map(c => c.action),
207
+ source: propChallenges && propChallenges.length > 0 ? 'propChallenges' :
208
+ onFetchChallenges ? 'onFetchChallenges' :
209
+ instructions && instructions.length > 0 ? 'instructions' : 'default'
210
+ });
203
211
  setChallenges(challengeList);
204
212
  setPhase('countdown');
205
213
  }
@@ -273,6 +281,7 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
273
281
  setFrames([]);
274
282
  framesRef.current = [];
275
283
  setCompletedChallenges([]);
284
+ completedChallengesRef.current = [];
276
285
  setCurrentChallengeIndex(0);
277
286
  setChallengeProgress(0);
278
287
  setOverallProgress(0);
@@ -320,15 +329,22 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
320
329
  return;
321
330
  }
322
331
  biometric_identity_sdk_core_1.logger.info('Using frames for completion:', finalFrames.length, 'frames');
332
+ const currentCompletedChallenges = completedChallengesRef.current.length > 0 ? completedChallengesRef.current : completedChallenges;
333
+ biometric_identity_sdk_core_1.logger.info('VideoRecorder: handleVideoComplete - completed challenges', {
334
+ refCount: completedChallengesRef.current.length,
335
+ stateCount: completedChallenges.length,
336
+ usingRef: completedChallengesRef.current.length > 0,
337
+ challengesCompleted: currentCompletedChallenges
338
+ });
323
339
  const result = {
324
340
  frames: finalFrames,
325
341
  duration: actualDuration,
326
- instructionsFollowed: completedChallenges.length === challenges.length,
342
+ instructionsFollowed: currentCompletedChallenges.length === challenges.length,
327
343
  qualityScore: finalFrames.length > 0 ? Math.min(100, (finalFrames.length / 30) * 100) : 85,
328
- challengesCompleted: completedChallenges,
344
+ challengesCompleted: currentCompletedChallenges,
329
345
  sessionId,
330
346
  };
331
- biometric_identity_sdk_core_1.logger.info('Video recording completed:', result.frames.length, 'frames,', (actualDuration / 1000).toFixed(1) + 's');
347
+ biometric_identity_sdk_core_1.logger.info('Video recording completed:', result.frames.length, 'frames,', (actualDuration / 1000).toFixed(1) + 's', 'challenges:', currentCompletedChallenges.length);
332
348
  isRecordingRef.current = false;
333
349
  onComplete(result);
334
350
  }
@@ -442,14 +458,15 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
442
458
  biometric_identity_sdk_core_1.logger.error('Error stopping video recording:', error);
443
459
  const actualDuration = Date.now() - recordingStartTime.current;
444
460
  const currentFrames = framesRef.current.length > 0 ? framesRef.current : frames;
461
+ const currentCompletedChallenges = completedChallengesRef.current.length > 0 ? completedChallengesRef.current : completedChallenges;
445
462
  if (actualDuration >= minDurationMs && currentFrames.length > 0) {
446
463
  biometric_identity_sdk_core_1.logger.info('Stopping recording with frames:', currentFrames.length);
447
464
  const result = {
448
465
  frames: currentFrames,
449
466
  duration: actualDuration,
450
- instructionsFollowed: completedChallenges.length === challenges.length,
467
+ instructionsFollowed: currentCompletedChallenges.length === challenges.length,
451
468
  qualityScore: Math.min(100, (currentFrames.length / 30) * 100),
452
- challengesCompleted: completedChallenges,
469
+ challengesCompleted: currentCompletedChallenges,
453
470
  sessionId,
454
471
  };
455
472
  onComplete(result);
@@ -459,9 +476,9 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
459
476
  const result = {
460
477
  frames: currentFrames.length > 0 ? currentFrames : [],
461
478
  duration: actualDuration,
462
- instructionsFollowed: completedChallenges.length === challenges.length,
479
+ instructionsFollowed: currentCompletedChallenges.length === challenges.length,
463
480
  qualityScore: currentFrames.length > 0 ? Math.min(100, (currentFrames.length / 30) * 100) : 0,
464
- challengesCompleted: completedChallenges,
481
+ challengesCompleted: currentCompletedChallenges,
465
482
  sessionId,
466
483
  };
467
484
  onComplete(result);
@@ -479,13 +496,20 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
479
496
  }
480
497
  setPhase('processing');
481
498
  setOverallProgress(100);
499
+ const currentCompletedChallenges = completedChallengesRef.current.length > 0 ? completedChallengesRef.current : completedChallenges;
500
+ biometric_identity_sdk_core_1.logger.info('VideoRecorder: stopRecording - completed challenges', {
501
+ refCount: completedChallengesRef.current.length,
502
+ stateCount: completedChallenges.length,
503
+ usingRef: completedChallengesRef.current.length > 0,
504
+ challengesCompleted: currentCompletedChallenges
505
+ });
482
506
  biometric_identity_sdk_core_1.logger.info('Completing recording with frames:', currentFrames.length);
483
507
  const result = {
484
508
  frames: currentFrames.length > 0 ? currentFrames : [],
485
509
  duration: actualDuration,
486
- instructionsFollowed: completedChallenges.length === challenges.length,
510
+ instructionsFollowed: currentCompletedChallenges.length === challenges.length,
487
511
  qualityScore: currentFrames.length > 0 ? Math.min(100, (currentFrames.length / 30) * 100) : 0,
488
- challengesCompleted: completedChallenges,
512
+ challengesCompleted: currentCompletedChallenges,
489
513
  sessionId,
490
514
  };
491
515
  setTimeout(() => {
@@ -494,6 +518,13 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
494
518
  }
495
519
  }, [frames, completedChallenges, challenges, sessionId, onComplete, minDurationMs, resetAndRetry, strings]);
496
520
  const runChallenge = (0, react_1.useCallback)((index) => {
521
+ const currentCompletedChallenges = completedChallengesRef.current.length > 0 ? completedChallengesRef.current : completedChallenges;
522
+ biometric_identity_sdk_core_1.logger.info('VideoRecorder: runChallenge called', {
523
+ index,
524
+ challengesLength: challenges.length,
525
+ challengeActions: challenges.map(c => c.action),
526
+ completedChallengesCount: currentCompletedChallenges.length
527
+ });
497
528
  if (index >= challenges.length) {
498
529
  setOverallProgress(100);
499
530
  if (recordingTimeoutRef.current) {
@@ -515,13 +546,14 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
515
546
  }
516
547
  else {
517
548
  const actualDuration = Date.now() - recordingStartTime.current;
549
+ const currentCompletedChallenges = completedChallengesRef.current.length > 0 ? completedChallengesRef.current : completedChallenges;
518
550
  if (actualDuration >= minDurationMs && frames.length > 0) {
519
551
  const result = {
520
552
  frames,
521
553
  duration: actualDuration,
522
- instructionsFollowed: completedChallenges.length === challenges.length,
554
+ instructionsFollowed: currentCompletedChallenges.length === challenges.length,
523
555
  qualityScore: Math.min(100, (frames.length / 30) * 100),
524
- challengesCompleted: completedChallenges,
556
+ challengesCompleted: currentCompletedChallenges,
525
557
  sessionId,
526
558
  };
527
559
  onComplete(result);
@@ -556,7 +588,17 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
556
588
  }, 100);
557
589
  setTimeout(() => {
558
590
  clearInterval(progressInterval);
559
- setCompletedChallenges(prev => [...prev, challenge.action]);
591
+ setCompletedChallenges(prev => {
592
+ const updated = [...prev, challenge.action];
593
+ completedChallengesRef.current = updated; // Keep ref in sync
594
+ biometric_identity_sdk_core_1.logger.info('VideoRecorder: Challenge completed', {
595
+ action: challenge.action,
596
+ index,
597
+ completedCount: updated.length,
598
+ completedActions: updated
599
+ });
600
+ return updated;
601
+ });
560
602
  react_native_1.Animated.timing(fadeAnim, {
561
603
  toValue: 0,
562
604
  duration: 200,
@@ -571,6 +613,9 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
571
613
  setPhase('recording');
572
614
  recordingStartTime.current = Date.now();
573
615
  isRecordingRef.current = true;
616
+ // Reset completed challenges ref when starting new recording
617
+ completedChallengesRef.current = [];
618
+ setCompletedChallenges([]);
574
619
  if (cameraRef.current && device) {
575
620
  try {
576
621
  videoRecordingRef.current = await cameraRef.current.startRecording({
@@ -593,7 +638,32 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
593
638
  else {
594
639
  startFrameCapture();
595
640
  }
596
- runChallenge(0);
641
+ /**
642
+ * If challenges are not available, set default challenges and run challenge
643
+ * This should not happen if ProfilePictureCapture properly waits for challenges
644
+ * But as a fallback, set defaults and retry after state update
645
+ * Schedule runChallenge to execute after state update completes
646
+ * This ensures the updated challenges are available when runChallenge executes
647
+ * Check again after state update - runChallenge callback should have latest challenges
648
+ */
649
+ if (challenges.length === 0) {
650
+ biometric_identity_sdk_core_1.logger.warn('VideoRecorder: No challenges available when starting recording - challenges should have been loaded by initChallenges');
651
+ const defaultChallenges = smartMode ? getDefaultChallenges((0, biometric_identity_sdk_core_1.getStrings)()) : [
652
+ {
653
+ action: 'stay_still',
654
+ instruction: (0, biometric_identity_sdk_core_1.getStrings)().liveness.instructions.stayStill || 'Look at the camera and stay still',
655
+ duration_ms: duration || 5000,
656
+ order: 1,
657
+ },
658
+ ];
659
+ setChallenges(defaultChallenges);
660
+ Promise.resolve().then(() => {
661
+ runChallenge(0);
662
+ });
663
+ }
664
+ else {
665
+ runChallenge(0);
666
+ }
597
667
  const maxDuration = Math.max(totalDuration, minDurationMs + 2000);
598
668
  recordingTimeoutRef.current = setTimeout(() => {
599
669
  if (isRecordingRef.current) {
@@ -608,7 +678,7 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
608
678
  recordingTimeoutRef.current = null;
609
679
  }
610
680
  };
611
- }, [device, totalDuration, minDurationMs, handleVideoComplete, handleRecordingError, runChallenge, stopRecording, startFrameCapture]);
681
+ }, [device, totalDuration, minDurationMs, handleVideoComplete, handleRecordingError, runChallenge, stopRecording, startFrameCapture, challenges, smartMode, duration]);
612
682
  const currentChallenge = challenges[currentChallengeIndex];
613
683
  const getDirectionIndicator = () => {
614
684
  if (!currentChallenge)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hexar/biometric-identity-sdk-react-native",
3
- "version": "1.1.17",
3
+ "version": "1.1.19",
4
4
  "description": "React Native wrapper for Biometric Identity SDK",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -129,6 +129,7 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
129
129
  const [challengeProgress, setChallengeProgress] = useState(0);
130
130
  const [overallProgress, setOverallProgress] = useState(0);
131
131
  const [completedChallenges, setCompletedChallenges] = useState<string[]>([]);
132
+ const completedChallengesRef = useRef<string[]>([]);
132
133
  const [frames, setFrames] = useState<string[]>([]);
133
134
  const framesRef = useRef<string[]>([]);
134
135
  const [hasPermission, setHasPermission] = useState(false);
@@ -238,6 +239,13 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
238
239
  ];
239
240
  }
240
241
 
242
+ logger.info('VideoRecorder: Challenges initialized', {
243
+ challengeCount: challengeList.length,
244
+ challengeActions: challengeList.map(c => c.action),
245
+ source: propChallenges && propChallenges.length > 0 ? 'propChallenges' :
246
+ onFetchChallenges ? 'onFetchChallenges' :
247
+ instructions && instructions.length > 0 ? 'instructions' : 'default'
248
+ });
241
249
  setChallenges(challengeList);
242
250
  setPhase('countdown');
243
251
  } catch (error) {
@@ -320,6 +328,7 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
320
328
  setFrames([]);
321
329
  framesRef.current = [];
322
330
  setCompletedChallenges([]);
331
+ completedChallengesRef.current = [];
323
332
  setCurrentChallengeIndex(0);
324
333
  setChallengeProgress(0);
325
334
  setOverallProgress(0);
@@ -385,16 +394,24 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
385
394
 
386
395
  logger.info('Using frames for completion:', finalFrames.length, 'frames');
387
396
 
397
+ const currentCompletedChallenges = completedChallengesRef.current.length > 0 ? completedChallengesRef.current : completedChallenges;
398
+ logger.info('VideoRecorder: handleVideoComplete - completed challenges', {
399
+ refCount: completedChallengesRef.current.length,
400
+ stateCount: completedChallenges.length,
401
+ usingRef: completedChallengesRef.current.length > 0,
402
+ challengesCompleted: currentCompletedChallenges
403
+ });
404
+
388
405
  const result: VideoRecordingResult = {
389
406
  frames: finalFrames,
390
407
  duration: actualDuration,
391
- instructionsFollowed: completedChallenges.length === challenges.length,
408
+ instructionsFollowed: currentCompletedChallenges.length === challenges.length,
392
409
  qualityScore: finalFrames.length > 0 ? Math.min(100, (finalFrames.length / 30) * 100) : 85,
393
- challengesCompleted: completedChallenges,
410
+ challengesCompleted: currentCompletedChallenges,
394
411
  sessionId,
395
412
  };
396
413
 
397
- logger.info('Video recording completed:', result.frames.length, 'frames,', (actualDuration / 1000).toFixed(1) + 's');
414
+ logger.info('Video recording completed:', result.frames.length, 'frames,', (actualDuration / 1000).toFixed(1) + 's', 'challenges:', currentCompletedChallenges.length);
398
415
 
399
416
  isRecordingRef.current = false;
400
417
  onComplete(result);
@@ -515,14 +532,15 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
515
532
  const actualDuration = Date.now() - recordingStartTime.current;
516
533
  const currentFrames = framesRef.current.length > 0 ? framesRef.current : frames;
517
534
 
535
+ const currentCompletedChallenges = completedChallengesRef.current.length > 0 ? completedChallengesRef.current : completedChallenges;
518
536
  if (actualDuration >= minDurationMs && currentFrames.length > 0) {
519
537
  logger.info('Stopping recording with frames:', currentFrames.length);
520
538
  const result: VideoRecordingResult = {
521
539
  frames: currentFrames,
522
540
  duration: actualDuration,
523
- instructionsFollowed: completedChallenges.length === challenges.length,
541
+ instructionsFollowed: currentCompletedChallenges.length === challenges.length,
524
542
  qualityScore: Math.min(100, (currentFrames.length / 30) * 100),
525
- challengesCompleted: completedChallenges,
543
+ challengesCompleted: currentCompletedChallenges,
526
544
  sessionId,
527
545
  };
528
546
  onComplete(result);
@@ -531,9 +549,9 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
531
549
  const result: VideoRecordingResult = {
532
550
  frames: currentFrames.length > 0 ? currentFrames : [],
533
551
  duration: actualDuration,
534
- instructionsFollowed: completedChallenges.length === challenges.length,
552
+ instructionsFollowed: currentCompletedChallenges.length === challenges.length,
535
553
  qualityScore: currentFrames.length > 0 ? Math.min(100, (currentFrames.length / 30) * 100) : 0,
536
- challengesCompleted: completedChallenges,
554
+ challengesCompleted: currentCompletedChallenges,
537
555
  sessionId,
538
556
  };
539
557
  onComplete(result);
@@ -557,13 +575,20 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
557
575
  setPhase('processing');
558
576
  setOverallProgress(100);
559
577
 
578
+ const currentCompletedChallenges = completedChallengesRef.current.length > 0 ? completedChallengesRef.current : completedChallenges;
579
+ logger.info('VideoRecorder: stopRecording - completed challenges', {
580
+ refCount: completedChallengesRef.current.length,
581
+ stateCount: completedChallenges.length,
582
+ usingRef: completedChallengesRef.current.length > 0,
583
+ challengesCompleted: currentCompletedChallenges
584
+ });
560
585
  logger.info('Completing recording with frames:', currentFrames.length);
561
586
  const result: VideoRecordingResult = {
562
587
  frames: currentFrames.length > 0 ? currentFrames : [],
563
588
  duration: actualDuration,
564
- instructionsFollowed: completedChallenges.length === challenges.length,
589
+ instructionsFollowed: currentCompletedChallenges.length === challenges.length,
565
590
  qualityScore: currentFrames.length > 0 ? Math.min(100, (currentFrames.length / 30) * 100) : 0,
566
- challengesCompleted: completedChallenges,
591
+ challengesCompleted: currentCompletedChallenges,
567
592
  sessionId,
568
593
  };
569
594
 
@@ -574,6 +599,14 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
574
599
  }, [frames, completedChallenges, challenges, sessionId, onComplete, minDurationMs, resetAndRetry, strings]);
575
600
 
576
601
  const runChallenge = useCallback((index: number) => {
602
+ const currentCompletedChallenges = completedChallengesRef.current.length > 0 ? completedChallengesRef.current : completedChallenges;
603
+ logger.info('VideoRecorder: runChallenge called', {
604
+ index,
605
+ challengesLength: challenges.length,
606
+ challengeActions: challenges.map(c => c.action),
607
+ completedChallengesCount: currentCompletedChallenges.length
608
+ });
609
+
577
610
  if (index >= challenges.length) {
578
611
  setOverallProgress(100);
579
612
 
@@ -598,13 +631,14 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
598
631
  stopRecording();
599
632
  } else {
600
633
  const actualDuration = Date.now() - recordingStartTime.current;
634
+ const currentCompletedChallenges = completedChallengesRef.current.length > 0 ? completedChallengesRef.current : completedChallenges;
601
635
  if (actualDuration >= minDurationMs && frames.length > 0) {
602
636
  const result: VideoRecordingResult = {
603
637
  frames,
604
638
  duration: actualDuration,
605
- instructionsFollowed: completedChallenges.length === challenges.length,
639
+ instructionsFollowed: currentCompletedChallenges.length === challenges.length,
606
640
  qualityScore: Math.min(100, (frames.length / 30) * 100),
607
- challengesCompleted: completedChallenges,
641
+ challengesCompleted: currentCompletedChallenges,
608
642
  sessionId,
609
643
  };
610
644
  onComplete(result);
@@ -647,7 +681,17 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
647
681
  setTimeout(() => {
648
682
  clearInterval(progressInterval);
649
683
 
650
- setCompletedChallenges(prev => [...prev, challenge.action]);
684
+ setCompletedChallenges(prev => {
685
+ const updated = [...prev, challenge.action];
686
+ completedChallengesRef.current = updated; // Keep ref in sync
687
+ logger.info('VideoRecorder: Challenge completed', {
688
+ action: challenge.action,
689
+ index,
690
+ completedCount: updated.length,
691
+ completedActions: updated
692
+ });
693
+ return updated;
694
+ });
651
695
 
652
696
  Animated.timing(fadeAnim, {
653
697
  toValue: 0,
@@ -664,6 +708,9 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
664
708
  setPhase('recording');
665
709
  recordingStartTime.current = Date.now();
666
710
  isRecordingRef.current = true;
711
+ // Reset completed challenges ref when starting new recording
712
+ completedChallengesRef.current = [];
713
+ setCompletedChallenges([]);
667
714
 
668
715
  if (cameraRef.current && device) {
669
716
  try {
@@ -687,7 +734,31 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
687
734
  startFrameCapture();
688
735
  }
689
736
 
690
- runChallenge(0);
737
+ /**
738
+ * If challenges are not available, set default challenges and run challenge
739
+ * This should not happen if ProfilePictureCapture properly waits for challenges
740
+ * But as a fallback, set defaults and retry after state update
741
+ * Schedule runChallenge to execute after state update completes
742
+ * This ensures the updated challenges are available when runChallenge executes
743
+ * Check again after state update - runChallenge callback should have latest challenges
744
+ */
745
+ if (challenges.length === 0) {
746
+ logger.warn('VideoRecorder: No challenges available when starting recording - challenges should have been loaded by initChallenges');
747
+ const defaultChallenges = smartMode ? getDefaultChallenges(getStrings()) : [
748
+ {
749
+ action: 'stay_still',
750
+ instruction: getStrings().liveness.instructions.stayStill || 'Look at the camera and stay still',
751
+ duration_ms: duration || 5000,
752
+ order: 1,
753
+ },
754
+ ];
755
+ setChallenges(defaultChallenges);
756
+ Promise.resolve().then(() => {
757
+ runChallenge(0);
758
+ });
759
+ } else {
760
+ runChallenge(0);
761
+ }
691
762
 
692
763
  const maxDuration = Math.max(totalDuration, minDurationMs + 2000);
693
764
  recordingTimeoutRef.current = setTimeout(() => {
@@ -704,7 +775,7 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
704
775
  recordingTimeoutRef.current = null;
705
776
  }
706
777
  };
707
- }, [device, totalDuration, minDurationMs, handleVideoComplete, handleRecordingError, runChallenge, stopRecording, startFrameCapture]);
778
+ }, [device, totalDuration, minDurationMs, handleVideoComplete, handleRecordingError, runChallenge, stopRecording, startFrameCapture, challenges, smartMode, duration]);
708
779
 
709
780
  const currentChallenge = challenges[currentChallengeIndex];
710
781