@hexar/biometric-identity-sdk-react-native 1.0.6 → 1.0.8

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.
@@ -42,6 +42,7 @@ const react_1 = __importStar(require("react"));
42
42
  const react_native_1 = require("react-native");
43
43
  const react_native_vision_camera_1 = require("react-native-vision-camera");
44
44
  const react_native_permissions_1 = require("react-native-permissions");
45
+ const biometric_identity_sdk_core_1 = require("@hexar/biometric-identity-sdk-core");
45
46
  // Default challenge set (used if backend not available)
46
47
  const DEFAULT_CHALLENGES = [
47
48
  {
@@ -73,20 +74,23 @@ const DEFAULT_CHALLENGES = [
73
74
  icon: '😊',
74
75
  },
75
76
  ];
76
- // Instruction text mapping
77
- const INSTRUCTION_MAP = {
78
- look_left: { text: 'Slowly turn your head LEFT', icon: '←' },
79
- look_right: { text: 'Slowly turn your head RIGHT', icon: '→' },
80
- look_up: { text: 'Look UP', icon: '↑' },
81
- look_down: { text: 'Look DOWN', icon: '↓' },
82
- turn_head_left: { text: 'Turn your head LEFT', icon: '←' },
83
- turn_head_right: { text: 'Turn your head RIGHT', icon: '→' },
84
- smile: { text: 'Smile 😊', icon: '😊' },
85
- blink: { text: 'Blink your eyes naturally', icon: 'šŸ‘' },
86
- open_mouth: { text: 'Open your mouth slightly', icon: '😮' },
87
- stay_still: { text: 'Look at the camera and stay still', icon: 'šŸ“·' },
88
- };
89
- const VideoRecorder = ({ theme, duration, instructions, challenges: propChallenges, sessionId, smartMode = true, onComplete, onCancel, onFetchChallenges, }) => {
77
+ const getInstructionMap = (strings) => ({
78
+ look_left: { text: strings.liveness.instructions.lookLeft || 'Slowly turn your head LEFT', icon: '←' },
79
+ look_right: { text: strings.liveness.instructions.lookRight || 'Slowly turn your head RIGHT', icon: '→' },
80
+ look_up: { text: strings.liveness.instructions.lookUp || 'Look UP', icon: '↑' },
81
+ look_down: { text: strings.liveness.instructions.lookDown || 'Look DOWN', icon: '↓' },
82
+ turn_head_left: { text: strings.liveness.instructions.turnHeadLeft || 'Turn your head LEFT', icon: '←' },
83
+ turn_head_right: { text: strings.liveness.instructions.turnHeadRight || 'Turn your head RIGHT', icon: '→' },
84
+ smile: { text: strings.liveness.instructions.smile || 'Smile 😊', icon: '😊' },
85
+ blink: { text: strings.liveness.instructions.blink || 'Blink your eyes naturally', icon: 'šŸ‘' },
86
+ open_mouth: { text: strings.liveness.instructions.openMouth || 'Open your mouth slightly', icon: '😮' },
87
+ stay_still: { text: strings.liveness.instructions.stayStill || 'Look at the camera and stay still', icon: 'šŸ“·' },
88
+ });
89
+ const VideoRecorder = ({ theme, language, duration, instructions, challenges: propChallenges, sessionId, smartMode = true, onComplete, onCancel, onFetchChallenges, }) => {
90
+ if (language) {
91
+ (0, biometric_identity_sdk_core_1.setLanguage)(language);
92
+ }
93
+ const strings = (0, biometric_identity_sdk_core_1.getStrings)();
90
94
  // State
91
95
  const [phase, setPhase] = (0, react_1.useState)('loading');
92
96
  const [countdown, setCountdown] = (0, react_1.useState)(3);
@@ -107,12 +111,13 @@ const VideoRecorder = ({ theme, duration, instructions, challenges: propChalleng
107
111
  const pulseAnim = (0, react_1.useRef)(new react_native_1.Animated.Value(1)).current;
108
112
  const arrowAnim = (0, react_1.useRef)(new react_native_1.Animated.Value(0)).current;
109
113
  const progressAnim = (0, react_1.useRef)(new react_native_1.Animated.Value(0)).current;
110
- // Refs
111
114
  const recordingStartTime = (0, react_1.useRef)(0);
112
115
  const frameInterval = (0, react_1.useRef)(null);
113
116
  const frameCaptureInterval = (0, react_1.useRef)(null);
114
- // Calculate total duration from challenges
115
- const totalDuration = duration || challenges.reduce((sum, c) => sum + c.duration_ms, 0) + 2000;
117
+ const videoRecordingRef = (0, react_1.useRef)(null);
118
+ const isRecordingRef = (0, react_1.useRef)(false);
119
+ const minDurationMs = 8000;
120
+ const totalDuration = duration || Math.max(minDurationMs, challenges.reduce((sum, c) => sum + c.duration_ms, 0) + 2000);
116
121
  // Check camera permissions
117
122
  (0, react_1.useEffect)(() => {
118
123
  const checkPermissions = async () => {
@@ -159,13 +164,13 @@ const VideoRecorder = ({ theme, duration, instructions, challenges: propChalleng
159
164
  challengeList = await onFetchChallenges();
160
165
  }
161
166
  else if (instructions && instructions.length > 0) {
162
- // Convert instructions to challenges
167
+ const instructionMap = getInstructionMap(strings);
163
168
  challengeList = instructions.map((inst, idx) => ({
164
169
  action: inst,
165
- instruction: INSTRUCTION_MAP[inst]?.text || inst,
170
+ instruction: instructionMap[inst]?.text || inst,
166
171
  duration_ms: 2000,
167
172
  order: idx + 1,
168
- icon: INSTRUCTION_MAP[inst]?.icon,
173
+ icon: instructionMap[inst]?.icon,
169
174
  }));
170
175
  }
171
176
  else {
@@ -254,124 +259,213 @@ const VideoRecorder = ({ theme, duration, instructions, challenges: propChalleng
254
259
  }),
255
260
  ])).start();
256
261
  }, [arrowAnim]);
257
- // Start recording
258
- const startRecording = (0, react_1.useCallback)(async () => {
259
- setPhase('recording');
260
- recordingStartTime.current = Date.now();
261
- // Start capturing frames from camera
262
+ const resetAndRetry = (0, react_1.useCallback)(() => {
263
+ setFrames([]);
264
+ setCompletedChallenges([]);
265
+ setCurrentChallengeIndex(0);
266
+ setChallengeProgress(0);
267
+ setOverallProgress(0);
268
+ recordingStartTime.current = 0;
269
+ setPhase('countdown');
270
+ setCountdown(3);
271
+ }, []);
272
+ const handleRecordingError = (0, react_1.useCallback)((error) => {
273
+ setPhase('loading');
274
+ react_native_1.Alert.alert('Recording Error', 'Failed to record video. Please try again.', [{ text: 'OK', onPress: onCancel }]);
275
+ }, [onCancel]);
276
+ const handleVideoComplete = (0, react_1.useCallback)(async (video) => {
277
+ console.log('handleVideoComplete called with video:', video?.path);
278
+ try {
279
+ setPhase('processing');
280
+ const actualDuration = Date.now() - recordingStartTime.current;
281
+ console.log('Video duration:', actualDuration, 'min required:', minDurationMs);
282
+ if (actualDuration < minDurationMs) {
283
+ setPhase('recording');
284
+ react_native_1.Alert.alert(strings.errors.videoTooShort?.title || 'Recording Too Short', strings.errors.videoTooShort?.message || `Video must be at least ${minDurationMs / 1000} seconds. Please try again.`, [{ text: strings.common.retry || 'OK', onPress: resetAndRetry }]);
285
+ return;
286
+ }
287
+ let videoBase64 = '';
288
+ if (video?.path) {
289
+ try {
290
+ const RNFS = require('react-native-fs');
291
+ videoBase64 = await RNFS.readFile(video.path, 'base64');
292
+ console.log('Video file read successfully, size:', videoBase64.length);
293
+ }
294
+ catch (fsError) {
295
+ console.warn('Could not read video file, using captured frames:', fsError);
296
+ }
297
+ }
298
+ const result = {
299
+ frames: frames.length > 0 ? frames : (videoBase64 ? [videoBase64] : []),
300
+ duration: actualDuration,
301
+ instructionsFollowed: completedChallenges.length === challenges.length,
302
+ qualityScore: frames.length > 0 ? Math.min(100, (frames.length / 30) * 100) : 85,
303
+ challengesCompleted: completedChallenges,
304
+ sessionId,
305
+ };
306
+ console.log('Video recording completed successfully:', {
307
+ duration: actualDuration,
308
+ frames: result.frames.length,
309
+ challengesCompleted: completedChallenges.length,
310
+ instructionsFollowed: result.instructionsFollowed
311
+ });
312
+ onComplete(result);
313
+ }
314
+ catch (error) {
315
+ console.error('Error processing video:', error);
316
+ setPhase('recording');
317
+ handleRecordingError(error);
318
+ }
319
+ }, [frames, completedChallenges, challenges, sessionId, onComplete, resetAndRetry, handleRecordingError, strings, minDurationMs]);
320
+ const startFrameCapture = (0, react_1.useCallback)(() => {
262
321
  if (cameraRef.current && device) {
263
- // Capture frames periodically (every 100ms = ~10 FPS)
264
322
  frameCaptureInterval.current = setInterval(async () => {
265
323
  try {
266
- // Take a photo frame (react-native-vision-camera doesn't have direct frame access in v4)
267
- // We'll use takeSnapshot or capture frames via photo
268
324
  const photo = await cameraRef.current?.takePhoto({
269
325
  flash: 'off',
270
326
  });
271
327
  if (photo) {
272
- // Convert to base64 if possible, otherwise use file path
273
328
  try {
274
329
  const RNFS = require('react-native-fs');
275
330
  const base64 = await RNFS.readFile(photo.path, 'base64');
276
- setFrames(prev => [...prev, base64]);
331
+ setFrames(prev => {
332
+ if (prev.length < 100) {
333
+ return [...prev, base64];
334
+ }
335
+ return prev;
336
+ });
277
337
  }
278
338
  catch (fsError) {
279
- // Fallback: use file path
280
- setFrames(prev => [...prev, photo.path]);
339
+ setFrames(prev => {
340
+ if (prev.length < 100) {
341
+ return [...prev, photo.path];
342
+ }
343
+ return prev;
344
+ });
281
345
  }
282
346
  }
283
347
  }
284
348
  catch (error) {
285
349
  console.warn('Frame capture error:', error);
286
- // Continue even if one frame fails
287
350
  }
288
351
  }, 100);
289
352
  }
290
- else {
291
- // Fallback: simulate frames if camera not available
292
- frameInterval.current = setInterval(() => {
293
- setFrames(prev => [...prev, `frame_${Date.now()}`]);
294
- }, 100);
295
- }
296
- // Start first challenge
297
- runChallenge(0);
298
353
  }, [device]);
299
- // Run a specific challenge
354
+ const stopRecording = (0, react_1.useCallback)(async () => {
355
+ console.log('Stopping recording...');
356
+ isRecordingRef.current = false;
357
+ if (videoRecordingRef.current) {
358
+ try {
359
+ console.log('Stopping video recording');
360
+ await videoRecordingRef.current.stop();
361
+ console.log('Video recording stopped');
362
+ }
363
+ catch (error) {
364
+ console.error('Error stopping video recording:', error);
365
+ }
366
+ videoRecordingRef.current = null;
367
+ }
368
+ if (frameCaptureInterval.current) {
369
+ clearInterval(frameCaptureInterval.current);
370
+ frameCaptureInterval.current = null;
371
+ }
372
+ }, []);
300
373
  const runChallenge = (0, react_1.useCallback)((index) => {
301
374
  if (index >= challenges.length) {
302
- completeRecording();
375
+ if (isRecordingRef.current) {
376
+ const elapsed = Date.now() - recordingStartTime.current;
377
+ if (elapsed < minDurationMs) {
378
+ setTimeout(() => {
379
+ if (isRecordingRef.current) {
380
+ stopRecording();
381
+ }
382
+ }, minDurationMs - elapsed);
383
+ return;
384
+ }
385
+ stopRecording();
386
+ }
303
387
  return;
304
388
  }
305
389
  const challenge = challenges[index];
306
390
  setCurrentChallengeIndex(index);
307
391
  setChallengeProgress(0);
308
- // Fade in instruction
309
392
  react_native_1.Animated.timing(fadeAnim, {
310
393
  toValue: 1,
311
394
  duration: 300,
312
395
  useNativeDriver: true,
313
396
  }).start();
314
- // Animate arrow for directional challenges
315
397
  if (challenge.action.includes('left') || challenge.action.includes('right')) {
316
398
  animateArrow(challenge.action);
317
399
  }
318
- // Progress animation for this challenge
319
400
  react_native_1.Animated.timing(progressAnim, {
320
401
  toValue: 100,
321
402
  duration: challenge.duration_ms,
322
403
  useNativeDriver: false,
323
404
  }).start();
324
- // Update progress in real-time
325
405
  const progressInterval = setInterval(() => {
326
406
  setChallengeProgress(prev => {
327
407
  const newProgress = prev + (100 / (challenge.duration_ms / 100));
328
408
  return Math.min(100, newProgress);
329
409
  });
330
- // Update overall progress
331
410
  const elapsed = Date.now() - recordingStartTime.current;
332
- const totalTime = challenges.reduce((sum, c) => sum + c.duration_ms, 0);
411
+ const totalTime = Math.max(totalDuration, challenges.reduce((sum, c) => sum + c.duration_ms, 0) + 2000);
333
412
  setOverallProgress(Math.min(100, (elapsed / totalTime) * 100));
334
413
  }, 100);
335
- // Move to next challenge after duration
336
414
  setTimeout(() => {
337
415
  clearInterval(progressInterval);
338
- // Mark challenge as completed
339
416
  setCompletedChallenges(prev => [...prev, challenge.action]);
340
- // Fade out current instruction
341
417
  react_native_1.Animated.timing(fadeAnim, {
342
418
  toValue: 0,
343
419
  duration: 200,
344
420
  useNativeDriver: true,
345
421
  }).start(() => {
346
- // Reset and start next challenge
347
422
  progressAnim.setValue(0);
348
423
  runChallenge(index + 1);
349
424
  });
350
425
  }, challenge.duration_ms);
351
- }, [challenges, fadeAnim, progressAnim, animateArrow]);
352
- // Complete recording
353
- const completeRecording = (0, react_1.useCallback)(() => {
354
- if (frameInterval.current) {
355
- clearInterval(frameInterval.current);
426
+ }, [challenges, fadeAnim, progressAnim, animateArrow, totalDuration, minDurationMs, stopRecording]);
427
+ const startRecording = (0, react_1.useCallback)(async () => {
428
+ setPhase('recording');
429
+ recordingStartTime.current = Date.now();
430
+ isRecordingRef.current = true;
431
+ console.log('Starting video recording, total duration:', totalDuration);
432
+ if (cameraRef.current && device) {
433
+ try {
434
+ videoRecordingRef.current = await cameraRef.current.startRecording({
435
+ flash: 'off',
436
+ onRecordingFinished: (video) => {
437
+ console.log('Video recording finished callback called', video);
438
+ if (isRecordingRef.current) {
439
+ handleVideoComplete(video);
440
+ }
441
+ },
442
+ onRecordingError: (error) => {
443
+ console.error('Recording error:', error);
444
+ handleRecordingError(error);
445
+ },
446
+ });
447
+ console.log('Video recording started successfully');
448
+ }
449
+ catch (error) {
450
+ console.warn('Video recording not available, falling back to frame capture:', error);
451
+ startFrameCapture();
452
+ }
356
453
  }
357
- if (frameCaptureInterval.current) {
358
- clearInterval(frameCaptureInterval.current);
454
+ else {
455
+ console.log('Camera not available, using frame capture');
456
+ startFrameCapture();
359
457
  }
360
- setPhase('processing');
361
- setOverallProgress(100);
362
- // Process and return result
363
- setTimeout(() => {
364
- const result = {
365
- frames,
366
- duration: Date.now() - recordingStartTime.current,
367
- instructionsFollowed: completedChallenges.length === challenges.length,
368
- qualityScore: frames.length > 0 ? 85 + Math.random() * 10 : 0, // Quality based on frames captured
369
- challengesCompleted: completedChallenges,
370
- sessionId,
371
- };
372
- onComplete(result);
373
- }, 500);
374
- }, [frames, completedChallenges, challenges, sessionId, onComplete]);
458
+ runChallenge(0);
459
+ const timeoutId = setTimeout(() => {
460
+ console.log('Recording timeout reached, stopping recording');
461
+ if (isRecordingRef.current) {
462
+ stopRecording().catch(err => {
463
+ console.error('Error stopping recording on timeout:', err);
464
+ });
465
+ }
466
+ }, totalDuration);
467
+ return () => clearTimeout(timeoutId);
468
+ }, [device, totalDuration, handleVideoComplete, handleRecordingError, runChallenge, stopRecording, startFrameCapture]);
375
469
  // Current challenge
376
470
  const currentChallenge = challenges[currentChallengeIndex];
377
471
  // Get direction arrow for the current challenge
@@ -408,20 +502,22 @@ const VideoRecorder = ({ theme, duration, instructions, challenges: propChalleng
408
502
  if (!hasPermission) {
409
503
  return (react_1.default.createElement(react_native_1.View, { style: styles.container },
410
504
  react_1.default.createElement(react_native_1.View, { style: styles.permissionContainer },
411
- react_1.default.createElement(react_native_1.Text, { style: styles.permissionText }, "Camera permission is required"),
505
+ react_1.default.createElement(react_native_1.Text, { style: styles.permissionText }, typeof strings.errors.cameraPermissionDenied === 'string'
506
+ ? strings.errors.cameraPermissionDenied
507
+ : strings.errors.cameraPermissionDenied?.message || 'Camera permission is required'),
412
508
  react_1.default.createElement(react_native_1.TouchableOpacity, { style: [styles.cancelButton, { borderColor: theme?.errorColor || '#EF4444' }], onPress: onCancel },
413
- react_1.default.createElement(react_native_1.Text, { style: [styles.cancelButtonText, { color: theme?.errorColor || '#EF4444' }] }, "Cancel")))));
509
+ react_1.default.createElement(react_native_1.Text, { style: [styles.cancelButtonText, { color: theme?.errorColor || '#EF4444' }] }, strings.common.cancel || 'Cancel')))));
414
510
  }
415
511
  if (!device) {
416
512
  return (react_1.default.createElement(react_native_1.View, { style: styles.container },
417
513
  react_1.default.createElement(react_native_1.View, { style: styles.permissionContainer },
418
- react_1.default.createElement(react_native_1.Text, { style: styles.permissionText }, "Camera not available"),
514
+ react_1.default.createElement(react_native_1.Text, { style: styles.permissionText }, strings.errors.cameraNotAvailable || 'Camera not available'),
419
515
  react_1.default.createElement(react_native_1.TouchableOpacity, { style: [styles.cancelButton, { borderColor: theme?.errorColor || '#EF4444' }], onPress: onCancel },
420
- react_1.default.createElement(react_native_1.Text, { style: [styles.cancelButtonText, { color: theme?.errorColor || '#EF4444' }] }, "Cancel")))));
516
+ react_1.default.createElement(react_native_1.Text, { style: [styles.cancelButtonText, { color: theme?.errorColor || '#EF4444' }] }, strings.common.cancel || 'Cancel')))));
421
517
  }
422
518
  return (react_1.default.createElement(react_native_1.View, { style: styles.container },
423
519
  react_1.default.createElement(react_native_1.View, { style: styles.cameraContainer },
424
- 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: false, photo: true }),
520
+ 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, audio: false }),
425
521
  react_1.default.createElement(react_native_1.View, { style: styles.overlay },
426
522
  react_1.default.createElement(react_native_1.View, { style: [
427
523
  styles.faceOval,
@@ -432,9 +528,9 @@ const VideoRecorder = ({ theme, duration, instructions, challenges: propChalleng
432
528
  ] }),
433
529
  phase === 'recording' && getDirectionIndicator()),
434
530
  phase === 'loading' && (react_1.default.createElement(react_native_1.View, { style: styles.centeredOverlay },
435
- react_1.default.createElement(react_native_1.Text, { style: styles.loadingText }, "Preparing challenges..."))),
531
+ react_1.default.createElement(react_native_1.Text, { style: styles.loadingText }, strings.liveness.preparing || 'Preparing challenges...'))),
436
532
  phase === 'countdown' && (react_1.default.createElement(react_native_1.View, { style: styles.countdownContainer },
437
- react_1.default.createElement(react_native_1.Text, { style: styles.getReadyText }, "Get Ready!"),
533
+ react_1.default.createElement(react_native_1.Text, { style: styles.getReadyText }, strings.liveness.getReady || 'Get Ready!'),
438
534
  react_1.default.createElement(react_native_1.Animated.Text, { style: [
439
535
  styles.countdownText,
440
536
  { transform: [{ scale: scaleAnim }] }
@@ -444,7 +540,7 @@ const VideoRecorder = ({ theme, duration, instructions, challenges: propChalleng
444
540
  styles.recordingDot,
445
541
  { transform: [{ scale: pulseAnim }] }
446
542
  ] }),
447
- react_1.default.createElement(react_native_1.Text, { style: styles.recordingText }, "Recording"))),
543
+ react_1.default.createElement(react_native_1.Text, { style: styles.recordingText }, strings.liveness.recording || 'Recording'))),
448
544
  (phase === 'recording' || phase === 'processing') && (react_1.default.createElement(react_native_1.View, { style: styles.progressContainer },
449
545
  react_1.default.createElement(react_native_1.View, { style: styles.progressBar },
450
546
  react_1.default.createElement(react_native_1.View, { style: [
@@ -478,21 +574,14 @@ const VideoRecorder = ({ theme, duration, instructions, challenges: propChalleng
478
574
  " / ",
479
575
  challenges.length))),
480
576
  phase === 'processing' && (react_1.default.createElement(react_native_1.View, { style: styles.processingOverlay },
481
- react_1.default.createElement(react_native_1.Text, { style: styles.processingText }, "Processing video...")))),
577
+ react_1.default.createElement(react_native_1.Text, { style: styles.processingText }, strings.liveness.processing || 'Processing video...')))),
482
578
  react_1.default.createElement(react_native_1.View, { style: styles.bottomContainer },
483
579
  phase === 'countdown' && (react_1.default.createElement(react_1.default.Fragment, null,
484
- react_1.default.createElement(react_native_1.Text, { style: styles.bottomText },
485
- "You'll perform ",
486
- challenges.length,
487
- " action",
488
- challenges.length > 1 ? 's' : '',
489
- ".",
490
- '\n',
491
- "Follow the on-screen instructions."),
580
+ react_1.default.createElement(react_native_1.Text, { style: styles.bottomText }, strings.liveness.countdownMessage || `You'll perform ${challenges.length} action${challenges.length > 1 ? 's' : ''}.\nFollow the on-screen instructions.`),
492
581
  react_1.default.createElement(react_native_1.TouchableOpacity, { style: [styles.cancelButton, { borderColor: theme?.errorColor || '#EF4444' }], onPress: onCancel },
493
- react_1.default.createElement(react_native_1.Text, { style: [styles.cancelButtonText, { color: theme?.errorColor || '#EF4444' }] }, "Cancel")))),
494
- phase === 'recording' && (react_1.default.createElement(react_native_1.Text, { style: styles.bottomText }, "Keep your face visible and follow the instructions")),
495
- phase === 'processing' && (react_1.default.createElement(react_native_1.Text, { style: styles.bottomText }, "Almost done...")))));
582
+ react_1.default.createElement(react_native_1.Text, { style: [styles.cancelButtonText, { color: theme?.errorColor || '#EF4444' }] }, strings.common.cancel || 'Cancel')))),
583
+ phase === 'recording' && (react_1.default.createElement(react_native_1.Text, { style: styles.bottomText }, strings.liveness.recordingInstructions || 'Keep your face visible and follow the instructions')),
584
+ phase === 'processing' && (react_1.default.createElement(react_native_1.Text, { style: styles.bottomText }, strings.validation.almostDone || 'Almost done...')))));
496
585
  };
497
586
  exports.VideoRecorder = VideoRecorder;
498
587
  const styles = react_native_1.StyleSheet.create({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hexar/biometric-identity-sdk-react-native",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "React Native wrapper for Biometric Identity SDK",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -17,6 +17,7 @@ import {
17
17
  ValidationResult,
18
18
  ThemeConfig,
19
19
  BiometricError,
20
+ BiometricErrorCode,
20
21
  SDKStep,
21
22
  getStrings,
22
23
  setLanguage,
@@ -74,11 +75,16 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
74
75
  const [isLoadingChallenges, setIsLoadingChallenges] = useState(false);
75
76
 
76
77
  // Set language early, before any components render
78
+ // Priority: language prop > SDK config language > default 'en'
77
79
  // Run on mount and whenever language prop changes
78
80
  useEffect(() => {
79
81
  if (language) {
82
+ // If language prop is provided, override the config
80
83
  setLanguage(language);
81
84
  }
85
+ // If no language prop, the language should already be set by BiometricIdentitySDK.configure()
86
+ // The global language state is set when configure() is called, so getStrings() will
87
+ // automatically return strings for the configured language (or 'en' as default)
82
88
  }, [language]);
83
89
 
84
90
  const strings = getStrings();
@@ -188,6 +194,11 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
188
194
  theme={theme}
189
195
  language={language}
190
196
  onStart={() => setShowInstructions(false)}
197
+ onCancel={onError ? () => onError({
198
+ name: 'BiometricError',
199
+ message: 'User cancelled',
200
+ code: BiometricErrorCode.USER_CANCELLED,
201
+ } as BiometricError) : undefined}
191
202
  styles={customStyles}
192
203
  />
193
204
  );
@@ -199,6 +210,7 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
199
210
  return (
200
211
  <VideoRecorder
201
212
  theme={theme}
213
+ language={language}
202
214
  challenges={currentChallenges}
203
215
  smartMode={smartLivenessMode}
204
216
  sessionId={sdk.getSessionId() || undefined}
@@ -216,6 +228,7 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
216
228
  <CameraCapture
217
229
  mode={cameraMode}
218
230
  theme={theme}
231
+ language={language}
219
232
  onCapture={handleCaptureComplete}
220
233
  onCancel={() => setShowCamera(false)}
221
234
  />
@@ -15,13 +15,14 @@ import {
15
15
  } from 'react-native';
16
16
  import { Camera, useCameraDevice, useCameraPermission } from 'react-native-vision-camera';
17
17
  import { request, PERMISSIONS, RESULTS } from 'react-native-permissions';
18
- import { ThemeConfig } from '@hexar/biometric-identity-sdk-core';
18
+ import { ThemeConfig, SupportedLanguage, getStrings, setLanguage } from '@hexar/biometric-identity-sdk-core';
19
19
 
20
20
  const { width, height } = Dimensions.get('window');
21
21
 
22
22
  export interface CameraCaptureProps {
23
23
  mode: 'front' | 'back';
24
24
  theme?: ThemeConfig;
25
+ language?: SupportedLanguage;
25
26
  onCapture: (imageData: string) => void;
26
27
  onCancel: () => void;
27
28
  }
@@ -29,6 +30,7 @@ export interface CameraCaptureProps {
29
30
  export const CameraCapture: React.FC<CameraCaptureProps> = ({
30
31
  mode,
31
32
  theme,
33
+ language,
32
34
  onCapture,
33
35
  onCancel,
34
36
  }) => {
@@ -37,6 +39,11 @@ export const CameraCapture: React.FC<CameraCaptureProps> = ({
37
39
  const cameraRef = useRef<Camera>(null);
38
40
  const { hasPermission: cameraPermission, requestPermission } = useCameraPermission();
39
41
 
42
+ if (language) {
43
+ setLanguage(language);
44
+ }
45
+ const strings = getStrings();
46
+
40
47
  // Get camera device (back camera for document capture)
41
48
  const device = useCameraDevice('back');
42
49
 
@@ -69,9 +76,12 @@ export const CameraCapture: React.FC<CameraCaptureProps> = ({
69
76
  } catch (error) {
70
77
  console.error('Permission check error:', error);
71
78
  setHasPermission(false);
79
+ const errorMsg = typeof strings.errors.cameraPermissionDenied === 'string'
80
+ ? strings.errors.cameraPermissionDenied
81
+ : strings.errors.cameraPermissionDenied?.message || 'Please enable camera access in your device settings to capture your ID document.';
72
82
  Alert.alert(
73
83
  'Camera Permission Required',
74
- 'Please enable camera access in your device settings to capture your ID document.'
84
+ errorMsg
75
85
  );
76
86
  }
77
87
  };
@@ -133,25 +143,29 @@ export const CameraCapture: React.FC<CameraCaptureProps> = ({
133
143
  };
134
144
 
135
145
  const instructions = mode === 'front'
136
- ? 'Position the front of your ID within the frame'
137
- : 'Position the back of your ID within the frame';
146
+ ? strings.capture.frontId.instruction
147
+ : strings.capture.backId.instruction;
138
148
 
139
149
  if (!hasPermission) {
140
150
  return (
141
151
  <View style={styles.container}>
142
152
  <View style={styles.permissionContainer}>
143
- <Text style={styles.permissionText}>Camera permission is required</Text>
153
+ <Text style={styles.permissionText}>
154
+ {typeof strings.errors.cameraPermissionDenied === 'string'
155
+ ? strings.errors.cameraPermissionDenied
156
+ : strings.errors.cameraPermissionDenied?.message || 'Camera permission is required'}
157
+ </Text>
144
158
  <TouchableOpacity
145
159
  style={[styles.button, { backgroundColor: theme?.primaryColor || '#6366F1' }]}
146
160
  onPress={checkPermissions}
147
161
  >
148
- <Text style={styles.buttonText}>Grant Permission</Text>
162
+ <Text style={styles.buttonText}>{strings.common.grantPermission || 'Grant Permission'}</Text>
149
163
  </TouchableOpacity>
150
164
  <TouchableOpacity
151
165
  style={[styles.button, styles.cancelButton]}
152
166
  onPress={onCancel}
153
167
  >
154
- <Text style={styles.buttonText}>Cancel</Text>
168
+ <Text style={styles.buttonText}>{strings.common.cancel || 'Cancel'}</Text>
155
169
  </TouchableOpacity>
156
170
  </View>
157
171
  </View>
@@ -162,12 +176,12 @@ export const CameraCapture: React.FC<CameraCaptureProps> = ({
162
176
  return (
163
177
  <View style={styles.container}>
164
178
  <View style={styles.permissionContainer}>
165
- <Text style={styles.permissionText}>Camera not available</Text>
179
+ <Text style={styles.permissionText}>{strings.errors.cameraNotAvailable || 'Camera not available'}</Text>
166
180
  <TouchableOpacity
167
181
  style={[styles.button, styles.cancelButton]}
168
182
  onPress={onCancel}
169
183
  >
170
- <Text style={styles.buttonText}>Cancel</Text>
184
+ <Text style={styles.buttonText}>{strings.common.cancel || 'Cancel'}</Text>
171
185
  </TouchableOpacity>
172
186
  </View>
173
187
  </View>
@@ -203,9 +217,8 @@ export const CameraCapture: React.FC<CameraCaptureProps> = ({
203
217
  <View style={styles.instructionsContainer}>
204
218
  <Text style={styles.instructionsText}>{instructions}</Text>
205
219
  <Text style={styles.tipsText}>
206
- • Ensure good lighting{'\n'}
207
- • Avoid glare and shadows{'\n'}
208
- • Keep document flat and complete
220
+ {mode === 'front' ? strings.capture.frontId.tips : strings.capture.backId.tips ||
221
+ '• Ensure good lighting\n• Avoid glare and shadows\n• Keep document flat and complete'}
209
222
  </Text>
210
223
  </View>
211
224
 
@@ -215,7 +228,7 @@ export const CameraCapture: React.FC<CameraCaptureProps> = ({
215
228
  style={[styles.button, styles.cancelButton]}
216
229
  onPress={onCancel}
217
230
  >
218
- <Text style={styles.buttonText}>Cancel</Text>
231
+ <Text style={styles.buttonText}>{strings.common.cancel || 'Cancel'}</Text>
219
232
  </TouchableOpacity>
220
233
 
221
234
  <TouchableOpacity