@hexar/biometric-identity-sdk-react-native 1.0.7 → 1.0.9

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.
@@ -12,6 +12,7 @@ export interface BiometricIdentityFlowProps {
12
12
  language?: SupportedLanguage;
13
13
  customTranslations?: Record<string, string>;
14
14
  smartLivenessMode?: boolean;
15
+ routeBack?: () => void;
15
16
  styles?: {
16
17
  container?: ViewStyle;
17
18
  content?: ViewStyle;
@@ -1 +1 @@
1
- {"version":3,"file":"BiometricIdentityFlow.d.ts","sourceRoot":"","sources":["../../src/components/BiometricIdentityFlow.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAA2C,MAAM,OAAO,CAAC;AAChE,OAAO,EAOL,SAAS,EACV,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,gBAAgB,EAChB,WAAW,EACX,cAAc,EAId,iBAAiB,EAClB,MAAM,oCAAoC,CAAC;AAU5C,MAAM,WAAW,0BAA0B;IACzC,oBAAoB,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,IAAI,CAAC;IACzD,OAAO,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC;IACzC,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,kBAAkB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5C,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,MAAM,CAAC,EAAE;QACP,SAAS,CAAC,EAAE,SAAS,CAAC;QACtB,OAAO,CAAC,EAAE,SAAS,CAAC;KACrB,CAAC;CACH;AAED,eAAO,MAAM,qBAAqB,EAAE,KAAK,CAAC,EAAE,CAAC,0BAA0B,CA6StE,CAAC;AAiOF,eAAe,qBAAqB,CAAC"}
1
+ {"version":3,"file":"BiometricIdentityFlow.d.ts","sourceRoot":"","sources":["../../src/components/BiometricIdentityFlow.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAA2C,MAAM,OAAO,CAAC;AAChE,OAAO,EAOL,SAAS,EACV,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,gBAAgB,EAChB,WAAW,EACX,cAAc,EAKd,iBAAiB,EAClB,MAAM,oCAAoC,CAAC;AAU5C,MAAM,WAAW,0BAA0B;IACzC,oBAAoB,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,IAAI,CAAC;IACzD,OAAO,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC;IACzC,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,kBAAkB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5C,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,MAAM,CAAC,EAAE;QACP,SAAS,CAAC,EAAE,SAAS,CAAC;QACtB,OAAO,CAAC,EAAE,SAAS,CAAC;KACrB,CAAC;CACH;AAED,eAAO,MAAM,qBAAqB,EAAE,KAAK,CAAC,EAAE,CAAC,0BAA0B,CAiTtE,CAAC;AAiOF,eAAe,qBAAqB,CAAC"}
@@ -48,7 +48,7 @@ const ValidationProgress_1 = require("./ValidationProgress");
48
48
  const ResultScreen_1 = require("./ResultScreen");
49
49
  const ErrorScreen_1 = require("./ErrorScreen");
50
50
  const InstructionsScreen_1 = require("./InstructionsScreen");
51
- const BiometricIdentityFlow = ({ onValidationComplete, onError, theme, language, customTranslations, smartLivenessMode = true, styles: customStyles, }) => {
51
+ const BiometricIdentityFlow = ({ onValidationComplete, onError, theme, language, customTranslations, smartLivenessMode = true, routeBack, styles: customStyles, }) => {
52
52
  const { sdk, state, isInitialized, isUsingBackend, challenges, uploadFrontID, uploadBackID, storeVideoRecording, fetchChallenges, validateIdentity, reset, } = (0, useBiometricSDK_1.useBiometricSDK)();
53
53
  const [showCamera, setShowCamera] = (0, react_1.useState)(false);
54
54
  const [cameraMode, setCameraMode] = (0, react_1.useState)('front');
@@ -158,17 +158,17 @@ const BiometricIdentityFlow = ({ onValidationComplete, onError, theme, language,
158
158
  }
159
159
  // Show instructions on first load
160
160
  if (showInstructions) {
161
- return (react_1.default.createElement(InstructionsScreen_1.InstructionsScreen, { theme: theme, language: language, onStart: () => setShowInstructions(false), styles: customStyles }));
161
+ return (react_1.default.createElement(InstructionsScreen_1.InstructionsScreen, { theme: theme, language: language, onStart: () => setShowInstructions(false), routeBack: routeBack, styles: customStyles }));
162
162
  }
163
163
  // Show camera/video recorder
164
164
  if (showCamera) {
165
165
  if (cameraMode === 'video') {
166
- return (react_1.default.createElement(VideoRecorder_1.VideoRecorder, { theme: theme, challenges: currentChallenges, smartMode: smartLivenessMode, sessionId: sdk.getSessionId() || undefined, onComplete: handleCaptureComplete, onCancel: () => setShowCamera(false), onFetchChallenges: async () => {
166
+ return (react_1.default.createElement(VideoRecorder_1.VideoRecorder, { theme: theme, language: language, challenges: currentChallenges, smartMode: smartLivenessMode, sessionId: sdk.getSessionId() || undefined, onComplete: handleCaptureComplete, onCancel: () => setShowCamera(false), onFetchChallenges: async () => {
167
167
  const challenges = await fetchChallenges('active');
168
168
  return challenges;
169
169
  } }));
170
170
  }
171
- return (react_1.default.createElement(CameraCapture_1.CameraCapture, { mode: cameraMode, theme: theme, onCapture: handleCaptureComplete, onCancel: () => setShowCamera(false) }));
171
+ return (react_1.default.createElement(CameraCapture_1.CameraCapture, { mode: cameraMode, theme: theme, language: language, onCapture: handleCaptureComplete, onCancel: () => setShowCamera(false) }));
172
172
  }
173
173
  // Show validation progress
174
174
  if (state.currentStep === biometric_identity_sdk_core_1.SDKStep.VALIDATING) {
@@ -3,10 +3,11 @@
3
3
  * Handles ID document photo capture
4
4
  */
5
5
  import React from 'react';
6
- import { ThemeConfig } from '@hexar/biometric-identity-sdk-core';
6
+ import { ThemeConfig, SupportedLanguage } from '@hexar/biometric-identity-sdk-core';
7
7
  export interface CameraCaptureProps {
8
8
  mode: 'front' | 'back';
9
9
  theme?: ThemeConfig;
10
+ language?: SupportedLanguage;
10
11
  onCapture: (imageData: string) => void;
11
12
  onCancel: () => void;
12
13
  }
@@ -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;AAY3D,OAAO,EAAE,WAAW,EAAE,MAAM,oCAAoC,CAAC;AAIjE,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,OAAO,GAAG,MAAM,CAAC;IACvB,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,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,CAiNtD,CAAC;AA0JF,eAAe,aAAa,CAAC"}
1
+ {"version":3,"file":"CameraCapture.d.ts","sourceRoot":"","sources":["../../src/components/CameraCapture.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAsC,MAAM,OAAO,CAAC;AAY3D,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAA2B,MAAM,oCAAoC,CAAC;AAI7G,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,CA6NtD,CAAC;AA0JF,eAAe,aAAa,CAAC"}
@@ -42,12 +42,17 @@ 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
  const { width, height } = react_native_1.Dimensions.get('window');
46
- const CameraCapture = ({ mode, theme, onCapture, onCancel, }) => {
47
+ const CameraCapture = ({ mode, theme, language, onCapture, onCancel, }) => {
47
48
  const [isCapturing, setIsCapturing] = (0, react_1.useState)(false);
48
49
  const [hasPermission, setHasPermission] = (0, react_1.useState)(false);
49
50
  const cameraRef = (0, react_1.useRef)(null);
50
51
  const { hasPermission: cameraPermission, requestPermission } = (0, react_native_vision_camera_1.useCameraPermission)();
52
+ if (language) {
53
+ (0, biometric_identity_sdk_core_1.setLanguage)(language);
54
+ }
55
+ const strings = (0, biometric_identity_sdk_core_1.getStrings)();
51
56
  // Get camera device (back camera for document capture)
52
57
  const device = (0, react_native_vision_camera_1.useCameraDevice)('back');
53
58
  (0, react_1.useEffect)(() => {
@@ -78,7 +83,10 @@ const CameraCapture = ({ mode, theme, onCapture, onCancel, }) => {
78
83
  catch (error) {
79
84
  console.error('Permission check error:', error);
80
85
  setHasPermission(false);
81
- react_native_1.Alert.alert('Camera Permission Required', 'Please enable camera access in your device settings to capture your ID document.');
86
+ const errorMsg = typeof strings.errors.cameraPermissionDenied === 'string'
87
+ ? strings.errors.cameraPermissionDenied
88
+ : strings.errors.cameraPermissionDenied?.message || 'Please enable camera access in your device settings to capture your ID document.';
89
+ react_native_1.Alert.alert('Camera Permission Required', errorMsg);
82
90
  }
83
91
  };
84
92
  const handleCapture = async () => {
@@ -136,23 +144,25 @@ const CameraCapture = ({ mode, theme, onCapture, onCancel, }) => {
136
144
  }
137
145
  };
138
146
  const instructions = mode === 'front'
139
- ? 'Position the front of your ID within the frame'
140
- : 'Position the back of your ID within the frame';
147
+ ? strings.capture.frontId.instruction
148
+ : strings.capture.backId.instruction;
141
149
  if (!hasPermission) {
142
150
  return (react_1.default.createElement(react_native_1.View, { style: styles.container },
143
151
  react_1.default.createElement(react_native_1.View, { style: styles.permissionContainer },
144
- react_1.default.createElement(react_native_1.Text, { style: styles.permissionText }, "Camera permission is required"),
152
+ react_1.default.createElement(react_native_1.Text, { style: styles.permissionText }, typeof strings.errors.cameraPermissionDenied === 'string'
153
+ ? strings.errors.cameraPermissionDenied
154
+ : strings.errors.cameraPermissionDenied?.message || 'Camera permission is required'),
145
155
  react_1.default.createElement(react_native_1.TouchableOpacity, { style: [styles.button, { backgroundColor: theme?.primaryColor || '#6366F1' }], onPress: checkPermissions },
146
- react_1.default.createElement(react_native_1.Text, { style: styles.buttonText }, "Grant Permission")),
156
+ react_1.default.createElement(react_native_1.Text, { style: styles.buttonText }, strings.common.grantPermission || 'Grant Permission')),
147
157
  react_1.default.createElement(react_native_1.TouchableOpacity, { style: [styles.button, styles.cancelButton], onPress: onCancel },
148
- react_1.default.createElement(react_native_1.Text, { style: styles.buttonText }, "Cancel")))));
158
+ react_1.default.createElement(react_native_1.Text, { style: styles.buttonText }, strings.common.cancel || 'Cancel')))));
149
159
  }
150
160
  if (!device) {
151
161
  return (react_1.default.createElement(react_native_1.View, { style: styles.container },
152
162
  react_1.default.createElement(react_native_1.View, { style: styles.permissionContainer },
153
- react_1.default.createElement(react_native_1.Text, { style: styles.permissionText }, "Camera not available"),
163
+ react_1.default.createElement(react_native_1.Text, { style: styles.permissionText }, strings.errors.cameraNotAvailable || 'Camera not available'),
154
164
  react_1.default.createElement(react_native_1.TouchableOpacity, { style: [styles.button, styles.cancelButton], onPress: onCancel },
155
- react_1.default.createElement(react_native_1.Text, { style: styles.buttonText }, "Cancel")))));
165
+ react_1.default.createElement(react_native_1.Text, { style: styles.buttonText }, strings.common.cancel || 'Cancel')))));
156
166
  }
157
167
  return (react_1.default.createElement(react_native_1.View, { style: styles.container },
158
168
  react_1.default.createElement(react_native_1.View, { style: styles.cameraContainer },
@@ -166,15 +176,11 @@ const CameraCapture = ({ mode, theme, onCapture, onCancel, }) => {
166
176
  react_1.default.createElement(react_native_1.View, { style: [styles.corner, styles.cornerBottomRight] }))))),
167
177
  react_1.default.createElement(react_native_1.View, { style: styles.instructionsContainer },
168
178
  react_1.default.createElement(react_native_1.Text, { style: styles.instructionsText }, instructions),
169
- react_1.default.createElement(react_native_1.Text, { style: styles.tipsText },
170
- "\u2022 Ensure good lighting",
171
- '\n',
172
- "\u2022 Avoid glare and shadows",
173
- '\n',
174
- "\u2022 Keep document flat and complete")),
179
+ react_1.default.createElement(react_native_1.Text, { style: styles.tipsText }, mode === 'front' ? strings.capture.frontId.tips : strings.capture.backId.tips ||
180
+ '• Ensure good lighting\n• Avoid glare and shadows\n• Keep document flat and complete')),
175
181
  react_1.default.createElement(react_native_1.View, { style: styles.controls },
176
182
  react_1.default.createElement(react_native_1.TouchableOpacity, { style: [styles.button, styles.cancelButton], onPress: onCancel },
177
- react_1.default.createElement(react_native_1.Text, { style: styles.buttonText }, "Cancel")),
183
+ react_1.default.createElement(react_native_1.Text, { style: styles.buttonText }, strings.common.cancel || 'Cancel')),
178
184
  react_1.default.createElement(react_native_1.TouchableOpacity, { style: [
179
185
  styles.button,
180
186
  styles.captureButton,
@@ -9,6 +9,7 @@ export interface InstructionsScreenProps {
9
9
  theme?: ThemeConfig;
10
10
  language?: SupportedLanguage;
11
11
  onStart: () => void;
12
+ routeBack?: () => void;
12
13
  styles?: {
13
14
  container?: ViewStyle;
14
15
  content?: ViewStyle;
@@ -1 +1 @@
1
- {"version":3,"file":"InstructionsScreen.d.ts","sourceRoot":"","sources":["../../src/components/InstructionsScreen.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAA8B,MAAM,OAAO,CAAC;AACnD,OAAO,EAML,SAAS,EACV,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAA2B,MAAM,oCAAoC,CAAC;AAE7G,MAAM,WAAW,uBAAuB;IACtC,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,MAAM,CAAC,EAAE;QACP,SAAS,CAAC,EAAE,SAAS,CAAC;QACtB,OAAO,CAAC,EAAE,SAAS,CAAC;KACrB,CAAC;CACH;AAED,eAAO,MAAM,kBAAkB,EAAE,KAAK,CAAC,EAAE,CAAC,uBAAuB,CA4FhE,CAAC;AAuLF,eAAe,kBAAkB,CAAC"}
1
+ {"version":3,"file":"InstructionsScreen.d.ts","sourceRoot":"","sources":["../../src/components/InstructionsScreen.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAA8B,MAAM,OAAO,CAAC;AACnD,OAAO,EAML,SAAS,EACV,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAA2B,MAAM,oCAAoC,CAAC;AAE7G,MAAM,WAAW,uBAAuB;IACtC,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,MAAM,CAAC,EAAE;QACP,SAAS,CAAC,EAAE,SAAS,CAAC;QACtB,OAAO,CAAC,EAAE,SAAS,CAAC;KACrB,CAAC;CACH;AAED,eAAO,MAAM,kBAAkB,EAAE,KAAK,CAAC,EAAE,CAAC,uBAAuB,CAyGhE,CAAC;AAuMF,eAAe,kBAAkB,CAAC"}
@@ -41,7 +41,7 @@ exports.InstructionsScreen = void 0;
41
41
  const react_1 = __importStar(require("react"));
42
42
  const react_native_1 = require("react-native");
43
43
  const biometric_identity_sdk_core_1 = require("@hexar/biometric-identity-sdk-core");
44
- const InstructionsScreen = ({ theme, language = 'en', onStart, styles: customStyles, }) => {
44
+ const InstructionsScreen = ({ theme, language = 'en', onStart, routeBack, styles: customStyles, }) => {
45
45
  const [strings, setStrings] = (0, react_1.useState)(() => {
46
46
  // Set initial language
47
47
  if (language) {
@@ -61,6 +61,9 @@ const InstructionsScreen = ({ theme, language = 'en', onStart, styles: customSty
61
61
  const tipsContent = getTipsContent(language);
62
62
  const privacyContent = getPrivacyContent(language);
63
63
  return (react_1.default.createElement(react_native_1.View, { style: [styles.container, customStyles?.container] },
64
+ routeBack && (react_1.default.createElement(react_native_1.View, { style: styles.header },
65
+ react_1.default.createElement(react_native_1.TouchableOpacity, { style: styles.backButton, onPress: routeBack },
66
+ react_1.default.createElement(react_native_1.Text, { style: [styles.backButtonText, { color: theme?.textColor || '#000000' }] }, strings.common.back || 'Volver')))),
64
67
  react_1.default.createElement(react_native_1.ScrollView, { contentContainerStyle: [styles.content, customStyles?.content] },
65
68
  react_1.default.createElement(react_native_1.Text, { style: [styles.title, { color: theme?.textColor || '#000000' }] }, strings.instructions.title),
66
69
  react_1.default.createElement(react_native_1.Text, { style: [styles.subtitle, { color: theme?.secondaryTextColor || '#6B7280' }] }, strings.instructions.subtitle),
@@ -128,6 +131,22 @@ const styles = react_native_1.StyleSheet.create({
128
131
  flex: 1,
129
132
  backgroundColor: '#FFFFFF',
130
133
  },
134
+ header: {
135
+ paddingTop: 16,
136
+ paddingHorizontal: 16,
137
+ paddingBottom: 8,
138
+ borderBottomWidth: 1,
139
+ borderBottomColor: '#E5E7EB',
140
+ },
141
+ backButton: {
142
+ paddingVertical: 8,
143
+ paddingHorizontal: 12,
144
+ alignSelf: 'flex-start',
145
+ },
146
+ backButtonText: {
147
+ fontSize: 16,
148
+ fontWeight: '600',
149
+ },
131
150
  content: {
132
151
  padding: 24,
133
152
  },
@@ -3,7 +3,7 @@
3
3
  * Features challenge-response flow with guided head movements
4
4
  */
5
5
  import React from 'react';
6
- import { ThemeConfig, LivenessInstruction } from '@hexar/biometric-identity-sdk-core';
6
+ import { ThemeConfig, LivenessInstruction, SupportedLanguage } from '@hexar/biometric-identity-sdk-core';
7
7
  export interface ChallengeAction {
8
8
  action: string;
9
9
  instruction: string;
@@ -13,6 +13,7 @@ export interface ChallengeAction {
13
13
  }
14
14
  export interface VideoRecorderProps {
15
15
  theme?: ThemeConfig;
16
+ language?: SupportedLanguage;
16
17
  /** Total recording duration in ms (default: 8000ms for smart mode) */
17
18
  duration?: number;
18
19
  /** Instructions for user to follow */
@@ -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,MAAM,oCAAoC,CAAC;AAGtF,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,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;AAgDD,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CAmpBtD,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,EAA2B,MAAM,oCAAoC,CAAC;AAGlI,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;AA+CD,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CAiwBtD,CAAC;AA4OF,eAAe,aAAa,CAAC"}
@@ -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);
@@ -160,13 +164,13 @@ const VideoRecorder = ({ theme, duration, instructions, challenges: propChalleng
160
164
  challengeList = await onFetchChallenges();
161
165
  }
162
166
  else if (instructions && instructions.length > 0) {
163
- // Convert instructions to challenges
167
+ const instructionMap = getInstructionMap(strings);
164
168
  challengeList = instructions.map((inst, idx) => ({
165
169
  action: inst,
166
- instruction: INSTRUCTION_MAP[inst]?.text || inst,
170
+ instruction: instructionMap[inst]?.text || inst,
167
171
  duration_ms: 2000,
168
172
  order: idx + 1,
169
- icon: INSTRUCTION_MAP[inst]?.icon,
173
+ icon: instructionMap[inst]?.icon,
170
174
  }));
171
175
  }
172
176
  else {
@@ -270,31 +274,62 @@ const VideoRecorder = ({ theme, duration, instructions, challenges: propChalleng
270
274
  react_native_1.Alert.alert('Recording Error', 'Failed to record video. Please try again.', [{ text: 'OK', onPress: onCancel }]);
271
275
  }, [onCancel]);
272
276
  const handleVideoComplete = (0, react_1.useCallback)(async (video) => {
273
- const actualDuration = Date.now() - recordingStartTime.current;
274
- if (actualDuration < minDurationMs) {
275
- react_native_1.Alert.alert('Recording Too Short', `Video must be at least ${minDurationMs / 1000} seconds. Please try again.`, [{ text: 'OK', onPress: resetAndRetry }]);
277
+ react_native_1.Alert.alert('Video Complete', `handleVideoComplete called\nVideo path: ${video?.path || 'N/A'}`);
278
+ if (!isRecordingRef.current && phase !== 'processing') {
279
+ react_native_1.Alert.alert('Info', 'Video already processed, ignoring duplicate callback');
276
280
  return;
277
281
  }
278
282
  try {
279
- const RNFS = require('react-native-fs');
280
- const videoBase64 = await RNFS.readFile(video.path, 'base64');
283
+ setPhase('processing');
284
+ setOverallProgress(100);
285
+ const actualDuration = Date.now() - recordingStartTime.current;
286
+ react_native_1.Alert.alert('Video Processing', `Duration: ${(actualDuration / 1000).toFixed(1)}s\nMin required: ${(minDurationMs / 1000).toFixed(1)}s\nFrames: ${frames.length}`);
287
+ if (actualDuration < minDurationMs) {
288
+ setPhase('recording');
289
+ setOverallProgress(0);
290
+ 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 }]);
291
+ return;
292
+ }
293
+ let videoBase64 = '';
294
+ if (video?.path) {
295
+ try {
296
+ const RNFS = require('react-native-fs');
297
+ videoBase64 = await RNFS.readFile(video.path, 'base64');
298
+ react_native_1.Alert.alert('Video File Read', `Successfully read video file\nSize: ${videoBase64.length} bytes`);
299
+ }
300
+ catch (fsError) {
301
+ react_native_1.Alert.alert('Video File Warning', `Could not read video file, using captured frames\nError: ${fsError}`);
302
+ }
303
+ }
304
+ const finalFrames = frames.length > 0 ? frames : (videoBase64 ? [videoBase64] : []);
305
+ if (finalFrames.length === 0) {
306
+ react_native_1.Alert.alert('Error', 'No frames available, cannot complete');
307
+ setPhase('recording');
308
+ handleRecordingError(new Error('No video frames captured'));
309
+ return;
310
+ }
281
311
  const result = {
282
- frames: frames.length > 0 ? frames : [videoBase64],
312
+ frames: finalFrames,
283
313
  duration: actualDuration,
284
314
  instructionsFollowed: completedChallenges.length === challenges.length,
285
- qualityScore: frames.length > 0 ? Math.min(100, (frames.length / 30) * 100) : 85,
315
+ qualityScore: finalFrames.length > 0 ? Math.min(100, (finalFrames.length / 30) * 100) : 85,
286
316
  challengesCompleted: completedChallenges,
287
317
  sessionId,
288
318
  };
319
+ react_native_1.Alert.alert('Video Completed Successfully', `Duration: ${(actualDuration / 1000).toFixed(1)}s\nFrames: ${result.frames.length}\nChallenges: ${completedChallenges.length}/${challenges.length}\nInstructions followed: ${result.instructionsFollowed ? 'Yes' : 'No'}\nQuality: ${result.qualityScore.toFixed(0)}%`);
320
+ isRecordingRef.current = false;
289
321
  onComplete(result);
290
322
  }
291
323
  catch (error) {
292
- console.error('Error processing video:', error);
324
+ react_native_1.Alert.alert('Error Processing Video', `Error: ${error instanceof Error ? error.message : String(error)}`);
325
+ setPhase('recording');
326
+ setOverallProgress(0);
293
327
  handleRecordingError(error);
294
328
  }
295
- }, [frames, completedChallenges, challenges, sessionId, onComplete, resetAndRetry, handleRecordingError]);
329
+ }, [frames, completedChallenges, challenges, sessionId, onComplete, resetAndRetry, handleRecordingError, strings, minDurationMs, phase]);
296
330
  const startFrameCapture = (0, react_1.useCallback)(() => {
297
331
  if (cameraRef.current && device) {
332
+ react_native_1.Alert.alert('Frame Capture', 'Starting frame capture mode');
298
333
  frameCaptureInterval.current = setInterval(async () => {
299
334
  try {
300
335
  const photo = await cameraRef.current?.takePhoto({
@@ -322,41 +357,82 @@ const VideoRecorder = ({ theme, duration, instructions, challenges: propChalleng
322
357
  }
323
358
  }
324
359
  catch (error) {
325
- console.warn('Frame capture error:', error);
360
+ react_native_1.Alert.alert('Frame Capture Error', `Error: ${error instanceof Error ? error.message : String(error)}`);
326
361
  }
327
362
  }, 100);
328
363
  }
329
364
  }, [device]);
330
365
  const stopRecording = (0, react_1.useCallback)(async () => {
366
+ react_native_1.Alert.alert('Stop Recording', 'Stopping recording...');
367
+ if (frameCaptureInterval.current) {
368
+ clearInterval(frameCaptureInterval.current);
369
+ frameCaptureInterval.current = null;
370
+ }
331
371
  if (videoRecordingRef.current) {
332
372
  try {
333
- await videoRecordingRef.current.stop();
373
+ react_native_1.Alert.alert('Stop Recording', 'Stopping video recording...');
374
+ const recording = videoRecordingRef.current;
375
+ videoRecordingRef.current = null;
376
+ isRecordingRef.current = false;
377
+ await recording.stop();
378
+ react_native_1.Alert.alert('Recording Stopped', 'Video recording stopped\nWaiting for onRecordingFinished callback');
334
379
  }
335
380
  catch (error) {
336
- console.error('Error stopping video recording:', error);
381
+ react_native_1.Alert.alert('Error Stopping Recording', `Error: ${error instanceof Error ? error.message : String(error)}`);
382
+ isRecordingRef.current = false;
383
+ const actualDuration = Date.now() - recordingStartTime.current;
384
+ if (actualDuration >= minDurationMs && frames.length > 0) {
385
+ react_native_1.Alert.alert('Using Captured Frames', 'Video stopped with error, using captured frames');
386
+ const result = {
387
+ frames,
388
+ duration: actualDuration,
389
+ instructionsFollowed: completedChallenges.length === challenges.length,
390
+ qualityScore: Math.min(100, (frames.length / 30) * 100),
391
+ challengesCompleted: completedChallenges,
392
+ sessionId,
393
+ };
394
+ onComplete(result);
395
+ }
337
396
  }
338
- videoRecordingRef.current = null;
339
397
  }
340
- if (frameCaptureInterval.current) {
341
- clearInterval(frameCaptureInterval.current);
342
- frameCaptureInterval.current = null;
398
+ else {
399
+ isRecordingRef.current = false;
343
400
  }
344
- isRecordingRef.current = false;
345
- }, []);
401
+ }, [frames, completedChallenges, challenges, sessionId, onComplete, minDurationMs]);
346
402
  const runChallenge = (0, react_1.useCallback)((index) => {
347
403
  if (index >= challenges.length) {
404
+ react_native_1.Alert.alert('Challenges Complete', 'All challenges completed, stopping recording');
405
+ setOverallProgress(100);
348
406
  if (isRecordingRef.current) {
349
407
  const elapsed = Date.now() - recordingStartTime.current;
408
+ react_native_1.Alert.alert('Checking Duration', `Elapsed: ${(elapsed / 1000).toFixed(1)}s\nMin required: ${(minDurationMs / 1000).toFixed(1)}s`);
350
409
  if (elapsed < minDurationMs) {
410
+ const remaining = minDurationMs - elapsed;
411
+ react_native_1.Alert.alert('Waiting', `Waiting additional ${(remaining / 1000).toFixed(1)}s to meet minimum duration`);
351
412
  setTimeout(() => {
352
413
  if (isRecordingRef.current) {
353
414
  stopRecording();
354
415
  }
355
- }, minDurationMs - elapsed);
416
+ }, remaining);
356
417
  return;
357
418
  }
358
419
  stopRecording();
359
420
  }
421
+ else {
422
+ const actualDuration = Date.now() - recordingStartTime.current;
423
+ if (actualDuration >= minDurationMs && frames.length > 0) {
424
+ react_native_1.Alert.alert('Completing', 'Recording already stopped, completing with frames');
425
+ const result = {
426
+ frames,
427
+ duration: actualDuration,
428
+ instructionsFollowed: completedChallenges.length === challenges.length,
429
+ qualityScore: Math.min(100, (frames.length / 30) * 100),
430
+ challengesCompleted: completedChallenges,
431
+ sessionId,
432
+ };
433
+ onComplete(result);
434
+ }
435
+ }
360
436
  return;
361
437
  }
362
438
  const challenge = challenges[index];
@@ -401,33 +477,41 @@ const VideoRecorder = ({ theme, duration, instructions, challenges: propChalleng
401
477
  setPhase('recording');
402
478
  recordingStartTime.current = Date.now();
403
479
  isRecordingRef.current = true;
480
+ react_native_1.Alert.alert('Start Recording', `Starting video recording\nTotal duration: ${(totalDuration / 1000).toFixed(1)}s`);
404
481
  if (cameraRef.current && device) {
405
482
  try {
406
483
  videoRecordingRef.current = await cameraRef.current.startRecording({
407
484
  flash: 'off',
408
485
  onRecordingFinished: (video) => {
486
+ react_native_1.Alert.alert('Recording Finished', `Video recording finished callback called\nPath: ${video?.path || 'N/A'}`);
409
487
  handleVideoComplete(video);
410
488
  },
411
489
  onRecordingError: (error) => {
412
- console.error('Recording error:', error);
490
+ react_native_1.Alert.alert('Recording Error', `Error: ${error instanceof Error ? error.message : String(error)}`);
413
491
  handleRecordingError(error);
414
492
  },
415
493
  });
494
+ react_native_1.Alert.alert('Recording Started', 'Video recording started successfully');
416
495
  }
417
496
  catch (error) {
418
- console.warn('Video recording not available, falling back to frame capture:', error);
497
+ react_native_1.Alert.alert('Recording Fallback', `Video recording not available, falling back to frame capture\nError: ${error}`);
419
498
  startFrameCapture();
420
499
  }
421
500
  }
422
501
  else {
502
+ react_native_1.Alert.alert('Camera Not Available', 'Camera not available, using frame capture');
423
503
  startFrameCapture();
424
504
  }
425
505
  runChallenge(0);
426
- setTimeout(() => {
506
+ const timeoutId = setTimeout(() => {
507
+ react_native_1.Alert.alert('Timeout', 'Recording timeout reached, stopping recording');
427
508
  if (isRecordingRef.current) {
428
- stopRecording();
509
+ stopRecording().catch(err => {
510
+ react_native_1.Alert.alert('Timeout Error', `Error stopping recording on timeout: ${err}`);
511
+ });
429
512
  }
430
513
  }, totalDuration);
514
+ return () => clearTimeout(timeoutId);
431
515
  }, [device, totalDuration, handleVideoComplete, handleRecordingError, runChallenge, stopRecording, startFrameCapture]);
432
516
  // Current challenge
433
517
  const currentChallenge = challenges[currentChallengeIndex];
@@ -465,16 +549,18 @@ const VideoRecorder = ({ theme, duration, instructions, challenges: propChalleng
465
549
  if (!hasPermission) {
466
550
  return (react_1.default.createElement(react_native_1.View, { style: styles.container },
467
551
  react_1.default.createElement(react_native_1.View, { style: styles.permissionContainer },
468
- react_1.default.createElement(react_native_1.Text, { style: styles.permissionText }, "Camera permission is required"),
552
+ react_1.default.createElement(react_native_1.Text, { style: styles.permissionText }, typeof strings.errors.cameraPermissionDenied === 'string'
553
+ ? strings.errors.cameraPermissionDenied
554
+ : strings.errors.cameraPermissionDenied?.message || 'Camera permission is required'),
469
555
  react_1.default.createElement(react_native_1.TouchableOpacity, { style: [styles.cancelButton, { borderColor: theme?.errorColor || '#EF4444' }], onPress: onCancel },
470
- react_1.default.createElement(react_native_1.Text, { style: [styles.cancelButtonText, { color: theme?.errorColor || '#EF4444' }] }, "Cancel")))));
556
+ react_1.default.createElement(react_native_1.Text, { style: [styles.cancelButtonText, { color: theme?.errorColor || '#EF4444' }] }, strings.common.cancel || 'Cancel')))));
471
557
  }
472
558
  if (!device) {
473
559
  return (react_1.default.createElement(react_native_1.View, { style: styles.container },
474
560
  react_1.default.createElement(react_native_1.View, { style: styles.permissionContainer },
475
- react_1.default.createElement(react_native_1.Text, { style: styles.permissionText }, "Camera not available"),
561
+ react_1.default.createElement(react_native_1.Text, { style: styles.permissionText }, strings.errors.cameraNotAvailable || 'Camera not available'),
476
562
  react_1.default.createElement(react_native_1.TouchableOpacity, { style: [styles.cancelButton, { borderColor: theme?.errorColor || '#EF4444' }], onPress: onCancel },
477
- react_1.default.createElement(react_native_1.Text, { style: [styles.cancelButtonText, { color: theme?.errorColor || '#EF4444' }] }, "Cancel")))));
563
+ react_1.default.createElement(react_native_1.Text, { style: [styles.cancelButtonText, { color: theme?.errorColor || '#EF4444' }] }, strings.common.cancel || 'Cancel')))));
478
564
  }
479
565
  return (react_1.default.createElement(react_native_1.View, { style: styles.container },
480
566
  react_1.default.createElement(react_native_1.View, { style: styles.cameraContainer },
@@ -489,9 +575,9 @@ const VideoRecorder = ({ theme, duration, instructions, challenges: propChalleng
489
575
  ] }),
490
576
  phase === 'recording' && getDirectionIndicator()),
491
577
  phase === 'loading' && (react_1.default.createElement(react_native_1.View, { style: styles.centeredOverlay },
492
- react_1.default.createElement(react_native_1.Text, { style: styles.loadingText }, "Preparing challenges..."))),
578
+ react_1.default.createElement(react_native_1.Text, { style: styles.loadingText }, strings.liveness.preparing || 'Preparing challenges...'))),
493
579
  phase === 'countdown' && (react_1.default.createElement(react_native_1.View, { style: styles.countdownContainer },
494
- react_1.default.createElement(react_native_1.Text, { style: styles.getReadyText }, "Get Ready!"),
580
+ react_1.default.createElement(react_native_1.Text, { style: styles.getReadyText }, strings.liveness.getReady || 'Get Ready!'),
495
581
  react_1.default.createElement(react_native_1.Animated.Text, { style: [
496
582
  styles.countdownText,
497
583
  { transform: [{ scale: scaleAnim }] }
@@ -501,7 +587,7 @@ const VideoRecorder = ({ theme, duration, instructions, challenges: propChalleng
501
587
  styles.recordingDot,
502
588
  { transform: [{ scale: pulseAnim }] }
503
589
  ] }),
504
- react_1.default.createElement(react_native_1.Text, { style: styles.recordingText }, "Recording"))),
590
+ react_1.default.createElement(react_native_1.Text, { style: styles.recordingText }, strings.liveness.recording || 'Recording'))),
505
591
  (phase === 'recording' || phase === 'processing') && (react_1.default.createElement(react_native_1.View, { style: styles.progressContainer },
506
592
  react_1.default.createElement(react_native_1.View, { style: styles.progressBar },
507
593
  react_1.default.createElement(react_native_1.View, { style: [
@@ -535,21 +621,14 @@ const VideoRecorder = ({ theme, duration, instructions, challenges: propChalleng
535
621
  " / ",
536
622
  challenges.length))),
537
623
  phase === 'processing' && (react_1.default.createElement(react_native_1.View, { style: styles.processingOverlay },
538
- react_1.default.createElement(react_native_1.Text, { style: styles.processingText }, "Processing video...")))),
624
+ react_1.default.createElement(react_native_1.Text, { style: styles.processingText }, strings.liveness.processing || 'Processing video...')))),
539
625
  react_1.default.createElement(react_native_1.View, { style: styles.bottomContainer },
540
626
  phase === 'countdown' && (react_1.default.createElement(react_1.default.Fragment, null,
541
- react_1.default.createElement(react_native_1.Text, { style: styles.bottomText },
542
- "You'll perform ",
543
- challenges.length,
544
- " action",
545
- challenges.length > 1 ? 's' : '',
546
- ".",
547
- '\n',
548
- "Follow the on-screen instructions."),
627
+ 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.`),
549
628
  react_1.default.createElement(react_native_1.TouchableOpacity, { style: [styles.cancelButton, { borderColor: theme?.errorColor || '#EF4444' }], onPress: onCancel },
550
- react_1.default.createElement(react_native_1.Text, { style: [styles.cancelButtonText, { color: theme?.errorColor || '#EF4444' }] }, "Cancel")))),
551
- phase === 'recording' && (react_1.default.createElement(react_native_1.Text, { style: styles.bottomText }, "Keep your face visible and follow the instructions")),
552
- phase === 'processing' && (react_1.default.createElement(react_native_1.Text, { style: styles.bottomText }, "Almost done...")))));
629
+ react_1.default.createElement(react_native_1.Text, { style: [styles.cancelButtonText, { color: theme?.errorColor || '#EF4444' }] }, strings.common.cancel || 'Cancel')))),
630
+ phase === 'recording' && (react_1.default.createElement(react_native_1.Text, { style: styles.bottomText }, strings.liveness.recordingInstructions || 'Keep your face visible and follow the instructions')),
631
+ phase === 'processing' && (react_1.default.createElement(react_native_1.Text, { style: styles.bottomText }, strings.validation.almostDone || 'Almost done...')))));
553
632
  };
554
633
  exports.VideoRecorder = VideoRecorder;
555
634
  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.7",
3
+ "version": "1.0.9",
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,
@@ -38,6 +39,7 @@ export interface BiometricIdentityFlowProps {
38
39
  language?: SupportedLanguage;
39
40
  customTranslations?: Record<string, string>;
40
41
  smartLivenessMode?: boolean;
42
+ routeBack?: () => void;
41
43
  styles?: {
42
44
  container?: ViewStyle;
43
45
  content?: ViewStyle;
@@ -51,6 +53,7 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
51
53
  language,
52
54
  customTranslations,
53
55
  smartLivenessMode = true,
56
+ routeBack,
54
57
  styles: customStyles,
55
58
  }) => {
56
59
  const {
@@ -193,6 +196,7 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
193
196
  theme={theme}
194
197
  language={language}
195
198
  onStart={() => setShowInstructions(false)}
199
+ routeBack={routeBack}
196
200
  styles={customStyles}
197
201
  />
198
202
  );
@@ -204,6 +208,7 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
204
208
  return (
205
209
  <VideoRecorder
206
210
  theme={theme}
211
+ language={language}
207
212
  challenges={currentChallenges}
208
213
  smartMode={smartLivenessMode}
209
214
  sessionId={sdk.getSessionId() || undefined}
@@ -221,6 +226,7 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
221
226
  <CameraCapture
222
227
  mode={cameraMode}
223
228
  theme={theme}
229
+ language={language}
224
230
  onCapture={handleCaptureComplete}
225
231
  onCancel={() => setShowCamera(false)}
226
232
  />
@@ -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
@@ -18,6 +18,7 @@ export interface InstructionsScreenProps {
18
18
  theme?: ThemeConfig;
19
19
  language?: SupportedLanguage;
20
20
  onStart: () => void;
21
+ routeBack?: () => void;
21
22
  styles?: {
22
23
  container?: ViewStyle;
23
24
  content?: ViewStyle;
@@ -28,6 +29,7 @@ export const InstructionsScreen: React.FC<InstructionsScreenProps> = ({
28
29
  theme,
29
30
  language = 'en',
30
31
  onStart,
32
+ routeBack,
31
33
  styles: customStyles,
32
34
  }) => {
33
35
  const [strings, setStrings] = useState(() => {
@@ -53,6 +55,18 @@ export const InstructionsScreen: React.FC<InstructionsScreenProps> = ({
53
55
 
54
56
  return (
55
57
  <View style={[styles.container, customStyles?.container]}>
58
+ {routeBack && (
59
+ <View style={styles.header}>
60
+ <TouchableOpacity
61
+ style={styles.backButton}
62
+ onPress={routeBack}
63
+ >
64
+ <Text style={[styles.backButtonText, { color: theme?.textColor || '#000000' }]}>
65
+ {strings.common.back || 'Volver'}
66
+ </Text>
67
+ </TouchableOpacity>
68
+ </View>
69
+ )}
56
70
  <ScrollView contentContainerStyle={[styles.content, customStyles?.content]}>
57
71
  <Text style={[styles.title, { color: theme?.textColor || '#000000' }]}>
58
72
  {strings.instructions.title}
@@ -188,6 +202,22 @@ const styles = StyleSheet.create({
188
202
  flex: 1,
189
203
  backgroundColor: '#FFFFFF',
190
204
  },
205
+ header: {
206
+ paddingTop: 16,
207
+ paddingHorizontal: 16,
208
+ paddingBottom: 8,
209
+ borderBottomWidth: 1,
210
+ borderBottomColor: '#E5E7EB',
211
+ },
212
+ backButton: {
213
+ paddingVertical: 8,
214
+ paddingHorizontal: 12,
215
+ alignSelf: 'flex-start',
216
+ },
217
+ backButtonText: {
218
+ fontSize: 16,
219
+ fontWeight: '600',
220
+ },
191
221
  content: {
192
222
  padding: 24,
193
223
  },
@@ -16,7 +16,7 @@ import {
16
16
  } from 'react-native';
17
17
  import { Camera, useCameraDevice, useCameraPermission } from 'react-native-vision-camera';
18
18
  import { request, PERMISSIONS, RESULTS } from 'react-native-permissions';
19
- import { ThemeConfig, LivenessInstruction } from '@hexar/biometric-identity-sdk-core';
19
+ import { ThemeConfig, LivenessInstruction, SupportedLanguage, getStrings, setLanguage } from '@hexar/biometric-identity-sdk-core';
20
20
 
21
21
  // Challenge action configuration (matches backend response)
22
22
  export interface ChallengeAction {
@@ -29,6 +29,7 @@ export interface ChallengeAction {
29
29
 
30
30
  export interface VideoRecorderProps {
31
31
  theme?: ThemeConfig;
32
+ language?: SupportedLanguage;
32
33
  /** Total recording duration in ms (default: 8000ms for smart mode) */
33
34
  duration?: number;
34
35
  /** Instructions for user to follow */
@@ -88,22 +89,22 @@ const DEFAULT_CHALLENGES: ChallengeAction[] = [
88
89
  },
89
90
  ];
90
91
 
91
- // Instruction text mapping
92
- const INSTRUCTION_MAP: Record<string, { text: string; icon: string }> = {
93
- look_left: { text: 'Slowly turn your head LEFT', icon: '' },
94
- look_right: { text: 'Slowly turn your head RIGHT', icon: '' },
95
- look_up: { text: 'Look UP', icon: '' },
96
- look_down: { text: 'Look DOWN', icon: '' },
97
- turn_head_left: { text: 'Turn your head LEFT', icon: '' },
98
- turn_head_right: { text: 'Turn your head RIGHT', icon: '' },
99
- smile: { text: 'Smile 😊', icon: '😊' },
100
- blink: { text: 'Blink your eyes naturally', icon: '👁' },
101
- open_mouth: { text: 'Open your mouth slightly', icon: '😮' },
102
- stay_still: { text: 'Look at the camera and stay still', icon: '📷' },
103
- };
92
+ const getInstructionMap = (strings: any): Record<string, { text: string; icon: string }> => ({
93
+ look_left: { text: strings.liveness.instructions.lookLeft || 'Slowly turn your head LEFT', icon: '←' },
94
+ look_right: { text: strings.liveness.instructions.lookRight || 'Slowly turn your head RIGHT', icon: '' },
95
+ look_up: { text: strings.liveness.instructions.lookUp || 'Look UP', icon: '' },
96
+ look_down: { text: strings.liveness.instructions.lookDown || 'Look DOWN', icon: '' },
97
+ turn_head_left: { text: strings.liveness.instructions.turnHeadLeft || 'Turn your head LEFT', icon: '' },
98
+ turn_head_right: { text: strings.liveness.instructions.turnHeadRight || 'Turn your head RIGHT', icon: '' },
99
+ smile: { text: strings.liveness.instructions.smile || 'Smile 😊', icon: '😊' },
100
+ blink: { text: strings.liveness.instructions.blink || 'Blink your eyes naturally', icon: '👁' },
101
+ open_mouth: { text: strings.liveness.instructions.openMouth || 'Open your mouth slightly', icon: '😮' },
102
+ stay_still: { text: strings.liveness.instructions.stayStill || 'Look at the camera and stay still', icon: '📷' },
103
+ });
104
104
 
105
105
  export const VideoRecorder: React.FC<VideoRecorderProps> = ({
106
106
  theme,
107
+ language,
107
108
  duration,
108
109
  instructions,
109
110
  challenges: propChallenges,
@@ -113,6 +114,11 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
113
114
  onCancel,
114
115
  onFetchChallenges,
115
116
  }) => {
117
+ if (language) {
118
+ setLanguage(language);
119
+ }
120
+ const strings = getStrings();
121
+
116
122
  // State
117
123
  const [phase, setPhase] = useState<'loading' | 'countdown' | 'recording' | 'processing'>('loading');
118
124
  const [countdown, setCountdown] = useState(3);
@@ -197,13 +203,13 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
197
203
  // Fetch from backend
198
204
  challengeList = await onFetchChallenges();
199
205
  } else if (instructions && instructions.length > 0) {
200
- // Convert instructions to challenges
206
+ const instructionMap = getInstructionMap(strings);
201
207
  challengeList = instructions.map((inst, idx) => ({
202
208
  action: inst,
203
- instruction: INSTRUCTION_MAP[inst]?.text || inst,
209
+ instruction: instructionMap[inst]?.text || inst,
204
210
  duration_ms: 2000,
205
211
  order: idx + 1,
206
- icon: INSTRUCTION_MAP[inst]?.icon,
212
+ icon: instructionMap[inst]?.icon,
207
213
  }));
208
214
  } else {
209
215
  // Use default challenges
@@ -322,39 +328,81 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
322
328
  }, [onCancel]);
323
329
 
324
330
  const handleVideoComplete = useCallback(async (video: any) => {
325
- const actualDuration = Date.now() - recordingStartTime.current;
331
+ Alert.alert('Video Complete', `handleVideoComplete called\nVideo path: ${video?.path || 'N/A'}`);
326
332
 
327
- if (actualDuration < minDurationMs) {
328
- Alert.alert(
329
- 'Recording Too Short',
330
- `Video must be at least ${minDurationMs / 1000} seconds. Please try again.`,
331
- [{ text: 'OK', onPress: resetAndRetry }]
332
- );
333
+ if (!isRecordingRef.current && phase !== 'processing') {
334
+ Alert.alert('Info', 'Video already processed, ignoring duplicate callback');
333
335
  return;
334
336
  }
335
-
337
+
336
338
  try {
337
- const RNFS = require('react-native-fs');
338
- const videoBase64 = await RNFS.readFile(video.path, 'base64');
339
+ setPhase('processing');
340
+ setOverallProgress(100);
341
+
342
+ const actualDuration = Date.now() - recordingStartTime.current;
343
+ Alert.alert(
344
+ 'Video Processing',
345
+ `Duration: ${(actualDuration / 1000).toFixed(1)}s\nMin required: ${(minDurationMs / 1000).toFixed(1)}s\nFrames: ${frames.length}`
346
+ );
347
+
348
+ if (actualDuration < minDurationMs) {
349
+ setPhase('recording');
350
+ setOverallProgress(0);
351
+ Alert.alert(
352
+ strings.errors.videoTooShort?.title || 'Recording Too Short',
353
+ strings.errors.videoTooShort?.message || `Video must be at least ${minDurationMs / 1000} seconds. Please try again.`,
354
+ [{ text: strings.common.retry || 'OK', onPress: resetAndRetry }]
355
+ );
356
+ return;
357
+ }
358
+
359
+ let videoBase64 = '';
360
+ if (video?.path) {
361
+ try {
362
+ const RNFS = require('react-native-fs');
363
+ videoBase64 = await RNFS.readFile(video.path, 'base64');
364
+ Alert.alert('Video File Read', `Successfully read video file\nSize: ${videoBase64.length} bytes`);
365
+ } catch (fsError) {
366
+ Alert.alert('Video File Warning', `Could not read video file, using captured frames\nError: ${fsError}`);
367
+ }
368
+ }
369
+
370
+ const finalFrames = frames.length > 0 ? frames : (videoBase64 ? [videoBase64] : []);
371
+
372
+ if (finalFrames.length === 0) {
373
+ Alert.alert('Error', 'No frames available, cannot complete');
374
+ setPhase('recording');
375
+ handleRecordingError(new Error('No video frames captured'));
376
+ return;
377
+ }
339
378
 
340
379
  const result: VideoRecordingResult = {
341
- frames: frames.length > 0 ? frames : [videoBase64],
380
+ frames: finalFrames,
342
381
  duration: actualDuration,
343
382
  instructionsFollowed: completedChallenges.length === challenges.length,
344
- qualityScore: frames.length > 0 ? Math.min(100, (frames.length / 30) * 100) : 85,
383
+ qualityScore: finalFrames.length > 0 ? Math.min(100, (finalFrames.length / 30) * 100) : 85,
345
384
  challengesCompleted: completedChallenges,
346
385
  sessionId,
347
386
  };
348
387
 
388
+ Alert.alert(
389
+ 'Video Completed Successfully',
390
+ `Duration: ${(actualDuration / 1000).toFixed(1)}s\nFrames: ${result.frames.length}\nChallenges: ${completedChallenges.length}/${challenges.length}\nInstructions followed: ${result.instructionsFollowed ? 'Yes' : 'No'}\nQuality: ${result.qualityScore.toFixed(0)}%`
391
+ );
392
+
393
+ isRecordingRef.current = false;
349
394
  onComplete(result);
350
395
  } catch (error) {
351
- console.error('Error processing video:', error);
396
+ Alert.alert('Error Processing Video', `Error: ${error instanceof Error ? error.message : String(error)}`);
397
+ setPhase('recording');
398
+ setOverallProgress(0);
352
399
  handleRecordingError(error);
353
400
  }
354
- }, [frames, completedChallenges, challenges, sessionId, onComplete, resetAndRetry, handleRecordingError]);
401
+ }, [frames, completedChallenges, challenges, sessionId, onComplete, resetAndRetry, handleRecordingError, strings, minDurationMs, phase]);
355
402
 
356
403
  const startFrameCapture = useCallback(() => {
357
404
  if (cameraRef.current && device) {
405
+ Alert.alert('Frame Capture', 'Starting frame capture mode');
358
406
  frameCaptureInterval.current = setInterval(async () => {
359
407
  try {
360
408
  const photo = await cameraRef.current?.takePhoto({
@@ -381,43 +429,90 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
381
429
  }
382
430
  }
383
431
  } catch (error) {
384
- console.warn('Frame capture error:', error);
432
+ Alert.alert('Frame Capture Error', `Error: ${error instanceof Error ? error.message : String(error)}`);
385
433
  }
386
434
  }, 100);
387
435
  }
388
436
  }, [device]);
389
437
 
390
438
  const stopRecording = useCallback(async () => {
391
- if (videoRecordingRef.current) {
392
- try {
393
- await videoRecordingRef.current.stop();
394
- } catch (error) {
395
- console.error('Error stopping video recording:', error);
396
- }
397
- videoRecordingRef.current = null;
398
- }
439
+ Alert.alert('Stop Recording', 'Stopping recording...');
399
440
 
400
441
  if (frameCaptureInterval.current) {
401
442
  clearInterval(frameCaptureInterval.current);
402
443
  frameCaptureInterval.current = null;
403
444
  }
404
445
 
405
- isRecordingRef.current = false;
406
- }, []);
446
+ if (videoRecordingRef.current) {
447
+ try {
448
+ Alert.alert('Stop Recording', 'Stopping video recording...');
449
+ const recording = videoRecordingRef.current;
450
+ videoRecordingRef.current = null;
451
+ isRecordingRef.current = false;
452
+
453
+ await recording.stop();
454
+ Alert.alert('Recording Stopped', 'Video recording stopped\nWaiting for onRecordingFinished callback');
455
+ } catch (error) {
456
+ Alert.alert('Error Stopping Recording', `Error: ${error instanceof Error ? error.message : String(error)}`);
457
+ isRecordingRef.current = false;
458
+
459
+ const actualDuration = Date.now() - recordingStartTime.current;
460
+ if (actualDuration >= minDurationMs && frames.length > 0) {
461
+ Alert.alert('Using Captured Frames', 'Video stopped with error, using captured frames');
462
+ const result: VideoRecordingResult = {
463
+ frames,
464
+ duration: actualDuration,
465
+ instructionsFollowed: completedChallenges.length === challenges.length,
466
+ qualityScore: Math.min(100, (frames.length / 30) * 100),
467
+ challengesCompleted: completedChallenges,
468
+ sessionId,
469
+ };
470
+ onComplete(result);
471
+ }
472
+ }
473
+ } else {
474
+ isRecordingRef.current = false;
475
+ }
476
+ }, [frames, completedChallenges, challenges, sessionId, onComplete, minDurationMs]);
407
477
 
408
478
  const runChallenge = useCallback((index: number) => {
409
479
  if (index >= challenges.length) {
480
+ Alert.alert('Challenges Complete', 'All challenges completed, stopping recording');
481
+ setOverallProgress(100);
482
+
410
483
  if (isRecordingRef.current) {
411
484
  const elapsed = Date.now() - recordingStartTime.current;
485
+ Alert.alert(
486
+ 'Checking Duration',
487
+ `Elapsed: ${(elapsed / 1000).toFixed(1)}s\nMin required: ${(minDurationMs / 1000).toFixed(1)}s`
488
+ );
489
+
412
490
  if (elapsed < minDurationMs) {
491
+ const remaining = minDurationMs - elapsed;
492
+ Alert.alert('Waiting', `Waiting additional ${(remaining / 1000).toFixed(1)}s to meet minimum duration`);
413
493
  setTimeout(() => {
414
494
  if (isRecordingRef.current) {
415
495
  stopRecording();
416
496
  }
417
- }, minDurationMs - elapsed);
497
+ }, remaining);
418
498
  return;
419
499
  }
500
+
420
501
  stopRecording();
502
+ } else {
503
+ const actualDuration = Date.now() - recordingStartTime.current;
504
+ if (actualDuration >= minDurationMs && frames.length > 0) {
505
+ Alert.alert('Completing', 'Recording already stopped, completing with frames');
506
+ const result: VideoRecordingResult = {
507
+ frames,
508
+ duration: actualDuration,
509
+ instructionsFollowed: completedChallenges.length === challenges.length,
510
+ qualityScore: Math.min(100, (frames.length / 30) * 100),
511
+ challengesCompleted: completedChallenges,
512
+ sessionId,
513
+ };
514
+ onComplete(result);
515
+ }
421
516
  }
422
517
  return;
423
518
  }
@@ -474,33 +569,43 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
474
569
  recordingStartTime.current = Date.now();
475
570
  isRecordingRef.current = true;
476
571
 
572
+ Alert.alert('Start Recording', `Starting video recording\nTotal duration: ${(totalDuration / 1000).toFixed(1)}s`);
573
+
477
574
  if (cameraRef.current && device) {
478
575
  try {
479
576
  videoRecordingRef.current = await cameraRef.current.startRecording({
480
577
  flash: 'off',
481
578
  onRecordingFinished: (video: any) => {
579
+ Alert.alert('Recording Finished', `Video recording finished callback called\nPath: ${video?.path || 'N/A'}`);
482
580
  handleVideoComplete(video);
483
581
  },
484
582
  onRecordingError: (error: any) => {
485
- console.error('Recording error:', error);
583
+ Alert.alert('Recording Error', `Error: ${error instanceof Error ? error.message : String(error)}`);
486
584
  handleRecordingError(error);
487
585
  },
488
586
  });
587
+ Alert.alert('Recording Started', 'Video recording started successfully');
489
588
  } catch (error) {
490
- console.warn('Video recording not available, falling back to frame capture:', error);
589
+ Alert.alert('Recording Fallback', `Video recording not available, falling back to frame capture\nError: ${error}`);
491
590
  startFrameCapture();
492
591
  }
493
592
  } else {
593
+ Alert.alert('Camera Not Available', 'Camera not available, using frame capture');
494
594
  startFrameCapture();
495
595
  }
496
596
 
497
597
  runChallenge(0);
498
598
 
499
- setTimeout(() => {
599
+ const timeoutId = setTimeout(() => {
600
+ Alert.alert('Timeout', 'Recording timeout reached, stopping recording');
500
601
  if (isRecordingRef.current) {
501
- stopRecording();
602
+ stopRecording().catch(err => {
603
+ Alert.alert('Timeout Error', `Error stopping recording on timeout: ${err}`);
604
+ });
502
605
  }
503
606
  }, totalDuration);
607
+
608
+ return () => clearTimeout(timeoutId);
504
609
  }, [device, totalDuration, handleVideoComplete, handleRecordingError, runChallenge, stopRecording, startFrameCapture]);
505
610
 
506
611
  // Current challenge
@@ -563,13 +668,17 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
563
668
  return (
564
669
  <View style={styles.container}>
565
670
  <View style={styles.permissionContainer}>
566
- <Text style={styles.permissionText}>Camera permission is required</Text>
671
+ <Text style={styles.permissionText}>
672
+ {typeof strings.errors.cameraPermissionDenied === 'string'
673
+ ? strings.errors.cameraPermissionDenied
674
+ : strings.errors.cameraPermissionDenied?.message || 'Camera permission is required'}
675
+ </Text>
567
676
  <TouchableOpacity
568
677
  style={[styles.cancelButton, { borderColor: theme?.errorColor || '#EF4444' }]}
569
678
  onPress={onCancel}
570
679
  >
571
680
  <Text style={[styles.cancelButtonText, { color: theme?.errorColor || '#EF4444' }]}>
572
- Cancel
681
+ {strings.common.cancel || 'Cancel'}
573
682
  </Text>
574
683
  </TouchableOpacity>
575
684
  </View>
@@ -581,13 +690,15 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
581
690
  return (
582
691
  <View style={styles.container}>
583
692
  <View style={styles.permissionContainer}>
584
- <Text style={styles.permissionText}>Camera not available</Text>
693
+ <Text style={styles.permissionText}>
694
+ {strings.errors.cameraNotAvailable || 'Camera not available'}
695
+ </Text>
585
696
  <TouchableOpacity
586
697
  style={[styles.cancelButton, { borderColor: theme?.errorColor || '#EF4444' }]}
587
698
  onPress={onCancel}
588
699
  >
589
700
  <Text style={[styles.cancelButtonText, { color: theme?.errorColor || '#EF4444' }]}>
590
- Cancel
701
+ {strings.common.cancel || 'Cancel'}
591
702
  </Text>
592
703
  </TouchableOpacity>
593
704
  </View>
@@ -627,14 +738,14 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
627
738
  {/* Loading Phase */}
628
739
  {phase === 'loading' && (
629
740
  <View style={styles.centeredOverlay}>
630
- <Text style={styles.loadingText}>Preparing challenges...</Text>
741
+ <Text style={styles.loadingText}>{strings.liveness.preparing || 'Preparing challenges...'}</Text>
631
742
  </View>
632
743
  )}
633
744
 
634
745
  {/* Countdown */}
635
746
  {phase === 'countdown' && (
636
747
  <View style={styles.countdownContainer}>
637
- <Text style={styles.getReadyText}>Get Ready!</Text>
748
+ <Text style={styles.getReadyText}>{strings.liveness.getReady || 'Get Ready!'}</Text>
638
749
  <Animated.Text
639
750
  style={[
640
751
  styles.countdownText,
@@ -655,7 +766,7 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
655
766
  { transform: [{ scale: pulseAnim }] }
656
767
  ]}
657
768
  />
658
- <Text style={styles.recordingText}>Recording</Text>
769
+ <Text style={styles.recordingText}>{strings.liveness.recording || 'Recording'}</Text>
659
770
  </View>
660
771
  )}
661
772
 
@@ -723,7 +834,7 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
723
834
  {/* Processing Overlay */}
724
835
  {phase === 'processing' && (
725
836
  <View style={styles.processingOverlay}>
726
- <Text style={styles.processingText}>Processing video...</Text>
837
+ <Text style={styles.processingText}>{strings.liveness.processing || 'Processing video...'}</Text>
727
838
  </View>
728
839
  )}
729
840
  </View>
@@ -733,15 +844,14 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
733
844
  {phase === 'countdown' && (
734
845
  <>
735
846
  <Text style={styles.bottomText}>
736
- You'll perform {challenges.length} action{challenges.length > 1 ? 's' : ''}.{'\n'}
737
- Follow the on-screen instructions.
847
+ {strings.liveness.countdownMessage || `You'll perform ${challenges.length} action${challenges.length > 1 ? 's' : ''}.\nFollow the on-screen instructions.`}
738
848
  </Text>
739
849
  <TouchableOpacity
740
850
  style={[styles.cancelButton, { borderColor: theme?.errorColor || '#EF4444' }]}
741
851
  onPress={onCancel}
742
852
  >
743
853
  <Text style={[styles.cancelButtonText, { color: theme?.errorColor || '#EF4444' }]}>
744
- Cancel
854
+ {strings.common.cancel || 'Cancel'}
745
855
  </Text>
746
856
  </TouchableOpacity>
747
857
  </>
@@ -749,13 +859,13 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
749
859
 
750
860
  {phase === 'recording' && (
751
861
  <Text style={styles.bottomText}>
752
- Keep your face visible and follow the instructions
862
+ {strings.liveness.recordingInstructions || 'Keep your face visible and follow the instructions'}
753
863
  </Text>
754
864
  )}
755
865
 
756
866
  {phase === 'processing' && (
757
867
  <Text style={styles.bottomText}>
758
- Almost done...
868
+ {strings.validation.almostDone || 'Almost done...'}
759
869
  </Text>
760
870
  )}
761
871
  </View>