@hexar/biometric-identity-sdk-react-native 1.0.7 → 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.
@@ -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,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,CAoTtE,CAAC;AAiOF,eAAe,qBAAqB,CAAC"}
@@ -158,17 +158,21 @@ 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), onCancel: onError ? () => onError({
162
+ name: 'BiometricError',
163
+ message: 'User cancelled',
164
+ code: biometric_identity_sdk_core_1.BiometricErrorCode.USER_CANCELLED,
165
+ }) : undefined, styles: customStyles }));
162
166
  }
163
167
  // Show camera/video recorder
164
168
  if (showCamera) {
165
169
  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 () => {
170
+ 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
171
  const challenges = await fetchChallenges('active');
168
172
  return challenges;
169
173
  } }));
170
174
  }
171
- return (react_1.default.createElement(CameraCapture_1.CameraCapture, { mode: cameraMode, theme: theme, onCapture: handleCaptureComplete, onCancel: () => setShowCamera(false) }));
175
+ return (react_1.default.createElement(CameraCapture_1.CameraCapture, { mode: cameraMode, theme: theme, language: language, onCapture: handleCaptureComplete, onCancel: () => setShowCamera(false) }));
172
176
  }
173
177
  // Show validation progress
174
178
  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
+ onCancel?: () => 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,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,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,CA4GhE,CAAC;AAiMF,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, onCancel, styles: customStyles, }) => {
45
45
  const [strings, setStrings] = (0, react_1.useState)(() => {
46
46
  // Set initial language
47
47
  if (language) {
@@ -76,9 +76,16 @@ const InstructionsScreen = ({ theme, language = 'en', onStart, styles: customSty
76
76
  react_1.default.createElement(react_native_1.View, { style: styles.privacyContainer },
77
77
  react_1.default.createElement(react_native_1.Text, { style: styles.privacyText }, privacyContent))),
78
78
  react_1.default.createElement(react_native_1.View, { style: styles.footer },
79
+ onCancel && (react_1.default.createElement(react_native_1.TouchableOpacity, { style: [
80
+ styles.button,
81
+ styles.cancelButton,
82
+ { borderColor: theme?.errorColor || '#EF4444' },
83
+ ], onPress: onCancel },
84
+ react_1.default.createElement(react_native_1.Text, { style: [styles.buttonText, { color: theme?.errorColor || '#EF4444' }] }, strings.common.cancel || 'Cancel'))),
79
85
  react_1.default.createElement(react_native_1.TouchableOpacity, { style: [
80
86
  styles.button,
81
87
  { backgroundColor: theme?.primaryColor || '#6366F1' },
88
+ onCancel && styles.startButton,
82
89
  ], onPress: onStart },
83
90
  react_1.default.createElement(react_native_1.Text, { style: styles.buttonText }, strings.instructions.startButton)))));
84
91
  };
@@ -175,12 +182,22 @@ const styles = react_native_1.StyleSheet.create({
175
182
  padding: 24,
176
183
  borderTopWidth: 1,
177
184
  borderTopColor: '#E5E7EB',
185
+ flexDirection: 'row',
186
+ gap: 12,
178
187
  },
179
188
  button: {
180
189
  paddingVertical: 16,
181
190
  paddingHorizontal: 32,
182
191
  borderRadius: 8,
183
192
  alignItems: 'center',
193
+ flex: 1,
194
+ },
195
+ cancelButton: {
196
+ borderWidth: 2,
197
+ backgroundColor: 'transparent',
198
+ },
199
+ startButton: {
200
+ flex: 1,
184
201
  },
185
202
  buttonText: {
186
203
  color: '#FFFFFF',
@@ -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,CAmsBtD,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,29 +274,49 @@ 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 }]);
276
- return;
277
- }
277
+ console.log('handleVideoComplete called with video:', video?.path);
278
278
  try {
279
- const RNFS = require('react-native-fs');
280
- const videoBase64 = await RNFS.readFile(video.path, 'base64');
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
+ }
281
298
  const result = {
282
- frames: frames.length > 0 ? frames : [videoBase64],
299
+ frames: frames.length > 0 ? frames : (videoBase64 ? [videoBase64] : []),
283
300
  duration: actualDuration,
284
301
  instructionsFollowed: completedChallenges.length === challenges.length,
285
302
  qualityScore: frames.length > 0 ? Math.min(100, (frames.length / 30) * 100) : 85,
286
303
  challengesCompleted: completedChallenges,
287
304
  sessionId,
288
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
+ });
289
312
  onComplete(result);
290
313
  }
291
314
  catch (error) {
292
315
  console.error('Error processing video:', error);
316
+ setPhase('recording');
293
317
  handleRecordingError(error);
294
318
  }
295
- }, [frames, completedChallenges, challenges, sessionId, onComplete, resetAndRetry, handleRecordingError]);
319
+ }, [frames, completedChallenges, challenges, sessionId, onComplete, resetAndRetry, handleRecordingError, strings, minDurationMs]);
296
320
  const startFrameCapture = (0, react_1.useCallback)(() => {
297
321
  if (cameraRef.current && device) {
298
322
  frameCaptureInterval.current = setInterval(async () => {
@@ -328,9 +352,13 @@ const VideoRecorder = ({ theme, duration, instructions, challenges: propChalleng
328
352
  }
329
353
  }, [device]);
330
354
  const stopRecording = (0, react_1.useCallback)(async () => {
355
+ console.log('Stopping recording...');
356
+ isRecordingRef.current = false;
331
357
  if (videoRecordingRef.current) {
332
358
  try {
359
+ console.log('Stopping video recording');
333
360
  await videoRecordingRef.current.stop();
361
+ console.log('Video recording stopped');
334
362
  }
335
363
  catch (error) {
336
364
  console.error('Error stopping video recording:', error);
@@ -341,7 +369,6 @@ const VideoRecorder = ({ theme, duration, instructions, challenges: propChalleng
341
369
  clearInterval(frameCaptureInterval.current);
342
370
  frameCaptureInterval.current = null;
343
371
  }
344
- isRecordingRef.current = false;
345
372
  }, []);
346
373
  const runChallenge = (0, react_1.useCallback)((index) => {
347
374
  if (index >= challenges.length) {
@@ -401,18 +428,23 @@ const VideoRecorder = ({ theme, duration, instructions, challenges: propChalleng
401
428
  setPhase('recording');
402
429
  recordingStartTime.current = Date.now();
403
430
  isRecordingRef.current = true;
431
+ console.log('Starting video recording, total duration:', totalDuration);
404
432
  if (cameraRef.current && device) {
405
433
  try {
406
434
  videoRecordingRef.current = await cameraRef.current.startRecording({
407
435
  flash: 'off',
408
436
  onRecordingFinished: (video) => {
409
- handleVideoComplete(video);
437
+ console.log('Video recording finished callback called', video);
438
+ if (isRecordingRef.current) {
439
+ handleVideoComplete(video);
440
+ }
410
441
  },
411
442
  onRecordingError: (error) => {
412
443
  console.error('Recording error:', error);
413
444
  handleRecordingError(error);
414
445
  },
415
446
  });
447
+ console.log('Video recording started successfully');
416
448
  }
417
449
  catch (error) {
418
450
  console.warn('Video recording not available, falling back to frame capture:', error);
@@ -420,14 +452,19 @@ const VideoRecorder = ({ theme, duration, instructions, challenges: propChalleng
420
452
  }
421
453
  }
422
454
  else {
455
+ console.log('Camera not available, using frame capture');
423
456
  startFrameCapture();
424
457
  }
425
458
  runChallenge(0);
426
- setTimeout(() => {
459
+ const timeoutId = setTimeout(() => {
460
+ console.log('Recording timeout reached, stopping recording');
427
461
  if (isRecordingRef.current) {
428
- stopRecording();
462
+ stopRecording().catch(err => {
463
+ console.error('Error stopping recording on timeout:', err);
464
+ });
429
465
  }
430
466
  }, totalDuration);
467
+ return () => clearTimeout(timeoutId);
431
468
  }, [device, totalDuration, handleVideoComplete, handleRecordingError, runChallenge, stopRecording, startFrameCapture]);
432
469
  // Current challenge
433
470
  const currentChallenge = challenges[currentChallengeIndex];
@@ -465,16 +502,18 @@ const VideoRecorder = ({ theme, duration, instructions, challenges: propChalleng
465
502
  if (!hasPermission) {
466
503
  return (react_1.default.createElement(react_native_1.View, { style: styles.container },
467
504
  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"),
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'),
469
508
  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")))));
509
+ react_1.default.createElement(react_native_1.Text, { style: [styles.cancelButtonText, { color: theme?.errorColor || '#EF4444' }] }, strings.common.cancel || 'Cancel')))));
471
510
  }
472
511
  if (!device) {
473
512
  return (react_1.default.createElement(react_native_1.View, { style: styles.container },
474
513
  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"),
514
+ react_1.default.createElement(react_native_1.Text, { style: styles.permissionText }, strings.errors.cameraNotAvailable || 'Camera not available'),
476
515
  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")))));
516
+ react_1.default.createElement(react_native_1.Text, { style: [styles.cancelButtonText, { color: theme?.errorColor || '#EF4444' }] }, strings.common.cancel || 'Cancel')))));
478
517
  }
479
518
  return (react_1.default.createElement(react_native_1.View, { style: styles.container },
480
519
  react_1.default.createElement(react_native_1.View, { style: styles.cameraContainer },
@@ -489,9 +528,9 @@ const VideoRecorder = ({ theme, duration, instructions, challenges: propChalleng
489
528
  ] }),
490
529
  phase === 'recording' && getDirectionIndicator()),
491
530
  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..."))),
531
+ react_1.default.createElement(react_native_1.Text, { style: styles.loadingText }, strings.liveness.preparing || 'Preparing challenges...'))),
493
532
  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!"),
533
+ react_1.default.createElement(react_native_1.Text, { style: styles.getReadyText }, strings.liveness.getReady || 'Get Ready!'),
495
534
  react_1.default.createElement(react_native_1.Animated.Text, { style: [
496
535
  styles.countdownText,
497
536
  { transform: [{ scale: scaleAnim }] }
@@ -501,7 +540,7 @@ const VideoRecorder = ({ theme, duration, instructions, challenges: propChalleng
501
540
  styles.recordingDot,
502
541
  { transform: [{ scale: pulseAnim }] }
503
542
  ] }),
504
- 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'))),
505
544
  (phase === 'recording' || phase === 'processing') && (react_1.default.createElement(react_native_1.View, { style: styles.progressContainer },
506
545
  react_1.default.createElement(react_native_1.View, { style: styles.progressBar },
507
546
  react_1.default.createElement(react_native_1.View, { style: [
@@ -535,21 +574,14 @@ const VideoRecorder = ({ theme, duration, instructions, challenges: propChalleng
535
574
  " / ",
536
575
  challenges.length))),
537
576
  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...")))),
577
+ react_1.default.createElement(react_native_1.Text, { style: styles.processingText }, strings.liveness.processing || 'Processing video...')))),
539
578
  react_1.default.createElement(react_native_1.View, { style: styles.bottomContainer },
540
579
  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."),
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.`),
549
581
  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...")))));
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...')))));
553
585
  };
554
586
  exports.VideoRecorder = VideoRecorder;
555
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.7",
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,
@@ -193,6 +194,11 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
193
194
  theme={theme}
194
195
  language={language}
195
196
  onStart={() => setShowInstructions(false)}
197
+ onCancel={onError ? () => onError({
198
+ name: 'BiometricError',
199
+ message: 'User cancelled',
200
+ code: BiometricErrorCode.USER_CANCELLED,
201
+ } as BiometricError) : undefined}
196
202
  styles={customStyles}
197
203
  />
198
204
  );
@@ -204,6 +210,7 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
204
210
  return (
205
211
  <VideoRecorder
206
212
  theme={theme}
213
+ language={language}
207
214
  challenges={currentChallenges}
208
215
  smartMode={smartLivenessMode}
209
216
  sessionId={sdk.getSessionId() || undefined}
@@ -221,6 +228,7 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
221
228
  <CameraCapture
222
229
  mode={cameraMode}
223
230
  theme={theme}
231
+ language={language}
224
232
  onCapture={handleCaptureComplete}
225
233
  onCancel={() => setShowCamera(false)}
226
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
@@ -18,6 +18,7 @@ export interface InstructionsScreenProps {
18
18
  theme?: ThemeConfig;
19
19
  language?: SupportedLanguage;
20
20
  onStart: () => void;
21
+ onCancel?: () => 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
+ onCancel,
31
33
  styles: customStyles,
32
34
  }) => {
33
35
  const [strings, setStrings] = useState(() => {
@@ -102,12 +104,27 @@ export const InstructionsScreen: React.FC<InstructionsScreenProps> = ({
102
104
  </View>
103
105
  </ScrollView>
104
106
 
105
- {/* Start Button */}
107
+ {/* Footer Buttons */}
106
108
  <View style={styles.footer}>
109
+ {onCancel && (
110
+ <TouchableOpacity
111
+ style={[
112
+ styles.button,
113
+ styles.cancelButton,
114
+ { borderColor: theme?.errorColor || '#EF4444' },
115
+ ]}
116
+ onPress={onCancel}
117
+ >
118
+ <Text style={[styles.buttonText, { color: theme?.errorColor || '#EF4444' }]}>
119
+ {strings.common.cancel || 'Cancel'}
120
+ </Text>
121
+ </TouchableOpacity>
122
+ )}
107
123
  <TouchableOpacity
108
124
  style={[
109
125
  styles.button,
110
126
  { backgroundColor: theme?.primaryColor || '#6366F1' },
127
+ onCancel && styles.startButton,
111
128
  ]}
112
129
  onPress={onStart}
113
130
  >
@@ -235,12 +252,22 @@ const styles = StyleSheet.create({
235
252
  padding: 24,
236
253
  borderTopWidth: 1,
237
254
  borderTopColor: '#E5E7EB',
255
+ flexDirection: 'row',
256
+ gap: 12,
238
257
  },
239
258
  button: {
240
259
  paddingVertical: 16,
241
260
  paddingHorizontal: 32,
242
261
  borderRadius: 8,
243
262
  alignItems: 'center',
263
+ flex: 1,
264
+ },
265
+ cancelButton: {
266
+ borderWidth: 2,
267
+ backgroundColor: 'transparent',
268
+ },
269
+ startButton: {
270
+ flex: 1,
244
271
  },
245
272
  buttonText: {
246
273
  color: '#FFFFFF',
@@ -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,23 +328,37 @@ 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
+ console.log('handleVideoComplete called with video:', video?.path);
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
- return;
334
- }
335
-
336
333
  try {
337
- const RNFS = require('react-native-fs');
338
- const videoBase64 = await RNFS.readFile(video.path, 'base64');
334
+ setPhase('processing');
335
+ const actualDuration = Date.now() - recordingStartTime.current;
336
+
337
+ console.log('Video duration:', actualDuration, 'min required:', minDurationMs);
338
+
339
+ if (actualDuration < minDurationMs) {
340
+ setPhase('recording');
341
+ Alert.alert(
342
+ strings.errors.videoTooShort?.title || 'Recording Too Short',
343
+ strings.errors.videoTooShort?.message || `Video must be at least ${minDurationMs / 1000} seconds. Please try again.`,
344
+ [{ text: strings.common.retry || 'OK', onPress: resetAndRetry }]
345
+ );
346
+ return;
347
+ }
348
+
349
+ let videoBase64 = '';
350
+ if (video?.path) {
351
+ try {
352
+ const RNFS = require('react-native-fs');
353
+ videoBase64 = await RNFS.readFile(video.path, 'base64');
354
+ console.log('Video file read successfully, size:', videoBase64.length);
355
+ } catch (fsError) {
356
+ console.warn('Could not read video file, using captured frames:', fsError);
357
+ }
358
+ }
339
359
 
340
360
  const result: VideoRecordingResult = {
341
- frames: frames.length > 0 ? frames : [videoBase64],
361
+ frames: frames.length > 0 ? frames : (videoBase64 ? [videoBase64] : []),
342
362
  duration: actualDuration,
343
363
  instructionsFollowed: completedChallenges.length === challenges.length,
344
364
  qualityScore: frames.length > 0 ? Math.min(100, (frames.length / 30) * 100) : 85,
@@ -346,12 +366,20 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
346
366
  sessionId,
347
367
  };
348
368
 
369
+ console.log('Video recording completed successfully:', {
370
+ duration: actualDuration,
371
+ frames: result.frames.length,
372
+ challengesCompleted: completedChallenges.length,
373
+ instructionsFollowed: result.instructionsFollowed
374
+ });
375
+
349
376
  onComplete(result);
350
377
  } catch (error) {
351
378
  console.error('Error processing video:', error);
379
+ setPhase('recording');
352
380
  handleRecordingError(error);
353
381
  }
354
- }, [frames, completedChallenges, challenges, sessionId, onComplete, resetAndRetry, handleRecordingError]);
382
+ }, [frames, completedChallenges, challenges, sessionId, onComplete, resetAndRetry, handleRecordingError, strings, minDurationMs]);
355
383
 
356
384
  const startFrameCapture = useCallback(() => {
357
385
  if (cameraRef.current && device) {
@@ -388,9 +416,14 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
388
416
  }, [device]);
389
417
 
390
418
  const stopRecording = useCallback(async () => {
419
+ console.log('Stopping recording...');
420
+ isRecordingRef.current = false;
421
+
391
422
  if (videoRecordingRef.current) {
392
423
  try {
424
+ console.log('Stopping video recording');
393
425
  await videoRecordingRef.current.stop();
426
+ console.log('Video recording stopped');
394
427
  } catch (error) {
395
428
  console.error('Error stopping video recording:', error);
396
429
  }
@@ -401,8 +434,6 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
401
434
  clearInterval(frameCaptureInterval.current);
402
435
  frameCaptureInterval.current = null;
403
436
  }
404
-
405
- isRecordingRef.current = false;
406
437
  }, []);
407
438
 
408
439
  const runChallenge = useCallback((index: number) => {
@@ -474,33 +505,45 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
474
505
  recordingStartTime.current = Date.now();
475
506
  isRecordingRef.current = true;
476
507
 
508
+ console.log('Starting video recording, total duration:', totalDuration);
509
+
477
510
  if (cameraRef.current && device) {
478
511
  try {
479
512
  videoRecordingRef.current = await cameraRef.current.startRecording({
480
513
  flash: 'off',
481
514
  onRecordingFinished: (video: any) => {
482
- handleVideoComplete(video);
515
+ console.log('Video recording finished callback called', video);
516
+ if (isRecordingRef.current) {
517
+ handleVideoComplete(video);
518
+ }
483
519
  },
484
520
  onRecordingError: (error: any) => {
485
521
  console.error('Recording error:', error);
486
522
  handleRecordingError(error);
487
523
  },
488
524
  });
525
+ console.log('Video recording started successfully');
489
526
  } catch (error) {
490
527
  console.warn('Video recording not available, falling back to frame capture:', error);
491
528
  startFrameCapture();
492
529
  }
493
530
  } else {
531
+ console.log('Camera not available, using frame capture');
494
532
  startFrameCapture();
495
533
  }
496
534
 
497
535
  runChallenge(0);
498
536
 
499
- setTimeout(() => {
537
+ const timeoutId = setTimeout(() => {
538
+ console.log('Recording timeout reached, stopping recording');
500
539
  if (isRecordingRef.current) {
501
- stopRecording();
540
+ stopRecording().catch(err => {
541
+ console.error('Error stopping recording on timeout:', err);
542
+ });
502
543
  }
503
544
  }, totalDuration);
545
+
546
+ return () => clearTimeout(timeoutId);
504
547
  }, [device, totalDuration, handleVideoComplete, handleRecordingError, runChallenge, stopRecording, startFrameCapture]);
505
548
 
506
549
  // Current challenge
@@ -563,13 +606,17 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
563
606
  return (
564
607
  <View style={styles.container}>
565
608
  <View style={styles.permissionContainer}>
566
- <Text style={styles.permissionText}>Camera permission is required</Text>
609
+ <Text style={styles.permissionText}>
610
+ {typeof strings.errors.cameraPermissionDenied === 'string'
611
+ ? strings.errors.cameraPermissionDenied
612
+ : strings.errors.cameraPermissionDenied?.message || 'Camera permission is required'}
613
+ </Text>
567
614
  <TouchableOpacity
568
615
  style={[styles.cancelButton, { borderColor: theme?.errorColor || '#EF4444' }]}
569
616
  onPress={onCancel}
570
617
  >
571
618
  <Text style={[styles.cancelButtonText, { color: theme?.errorColor || '#EF4444' }]}>
572
- Cancel
619
+ {strings.common.cancel || 'Cancel'}
573
620
  </Text>
574
621
  </TouchableOpacity>
575
622
  </View>
@@ -581,13 +628,15 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
581
628
  return (
582
629
  <View style={styles.container}>
583
630
  <View style={styles.permissionContainer}>
584
- <Text style={styles.permissionText}>Camera not available</Text>
631
+ <Text style={styles.permissionText}>
632
+ {strings.errors.cameraNotAvailable || 'Camera not available'}
633
+ </Text>
585
634
  <TouchableOpacity
586
635
  style={[styles.cancelButton, { borderColor: theme?.errorColor || '#EF4444' }]}
587
636
  onPress={onCancel}
588
637
  >
589
638
  <Text style={[styles.cancelButtonText, { color: theme?.errorColor || '#EF4444' }]}>
590
- Cancel
639
+ {strings.common.cancel || 'Cancel'}
591
640
  </Text>
592
641
  </TouchableOpacity>
593
642
  </View>
@@ -627,14 +676,14 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
627
676
  {/* Loading Phase */}
628
677
  {phase === 'loading' && (
629
678
  <View style={styles.centeredOverlay}>
630
- <Text style={styles.loadingText}>Preparing challenges...</Text>
679
+ <Text style={styles.loadingText}>{strings.liveness.preparing || 'Preparing challenges...'}</Text>
631
680
  </View>
632
681
  )}
633
682
 
634
683
  {/* Countdown */}
635
684
  {phase === 'countdown' && (
636
685
  <View style={styles.countdownContainer}>
637
- <Text style={styles.getReadyText}>Get Ready!</Text>
686
+ <Text style={styles.getReadyText}>{strings.liveness.getReady || 'Get Ready!'}</Text>
638
687
  <Animated.Text
639
688
  style={[
640
689
  styles.countdownText,
@@ -655,7 +704,7 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
655
704
  { transform: [{ scale: pulseAnim }] }
656
705
  ]}
657
706
  />
658
- <Text style={styles.recordingText}>Recording</Text>
707
+ <Text style={styles.recordingText}>{strings.liveness.recording || 'Recording'}</Text>
659
708
  </View>
660
709
  )}
661
710
 
@@ -723,7 +772,7 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
723
772
  {/* Processing Overlay */}
724
773
  {phase === 'processing' && (
725
774
  <View style={styles.processingOverlay}>
726
- <Text style={styles.processingText}>Processing video...</Text>
775
+ <Text style={styles.processingText}>{strings.liveness.processing || 'Processing video...'}</Text>
727
776
  </View>
728
777
  )}
729
778
  </View>
@@ -733,15 +782,14 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
733
782
  {phase === 'countdown' && (
734
783
  <>
735
784
  <Text style={styles.bottomText}>
736
- You'll perform {challenges.length} action{challenges.length > 1 ? 's' : ''}.{'\n'}
737
- Follow the on-screen instructions.
785
+ {strings.liveness.countdownMessage || `You'll perform ${challenges.length} action${challenges.length > 1 ? 's' : ''}.\nFollow the on-screen instructions.`}
738
786
  </Text>
739
787
  <TouchableOpacity
740
788
  style={[styles.cancelButton, { borderColor: theme?.errorColor || '#EF4444' }]}
741
789
  onPress={onCancel}
742
790
  >
743
791
  <Text style={[styles.cancelButtonText, { color: theme?.errorColor || '#EF4444' }]}>
744
- Cancel
792
+ {strings.common.cancel || 'Cancel'}
745
793
  </Text>
746
794
  </TouchableOpacity>
747
795
  </>
@@ -749,13 +797,13 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
749
797
 
750
798
  {phase === 'recording' && (
751
799
  <Text style={styles.bottomText}>
752
- Keep your face visible and follow the instructions
800
+ {strings.liveness.recordingInstructions || 'Keep your face visible and follow the instructions'}
753
801
  </Text>
754
802
  )}
755
803
 
756
804
  {phase === 'processing' && (
757
805
  <Text style={styles.bottomText}>
758
- Almost done...
806
+ {strings.validation.almostDone || 'Almost done...'}
759
807
  </Text>
760
808
  )}
761
809
  </View>