@hexar/biometric-identity-sdk-react-native 1.0.17 → 1.0.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"BiometricIdentityFlow.d.ts","sourceRoot":"","sources":["../../src/components/BiometricIdentityFlow.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAmD,MAAM,OAAO,CAAC;AACxE,OAAO,EAOL,SAAS,EACV,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,gBAAgB,EAChB,WAAW,EACX,cAAc,EAKd,iBAAiB,EAElB,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,CAyWtE,CAAC;AAiOF,eAAe,qBAAqB,CAAC"}
1
+ {"version":3,"file":"BiometricIdentityFlow.d.ts","sourceRoot":"","sources":["../../src/components/BiometricIdentityFlow.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAmD,MAAM,OAAO,CAAC;AACxE,OAAO,EAOL,SAAS,EACV,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,gBAAgB,EAChB,WAAW,EACX,cAAc,EAKd,iBAAiB,EAElB,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,CAiWtE,CAAC;AA8NF,eAAe,qBAAqB,CAAC"}
@@ -55,51 +55,37 @@ const BiometricIdentityFlow = ({ onValidationComplete, onError, theme, language,
55
55
  const [showInstructions, setShowInstructions] = (0, react_1.useState)(true);
56
56
  const [currentChallenges, setCurrentChallenges] = (0, react_1.useState)([]);
57
57
  const [isLoadingChallenges, setIsLoadingChallenges] = (0, react_1.useState)(false);
58
- // Use refs to store callbacks to avoid dependency issues in useEffect
59
58
  const onValidationCompleteRef = (0, react_1.useRef)(onValidationComplete);
60
59
  const onErrorRef = (0, react_1.useRef)(onError);
61
60
  const hasCalledValidationComplete = (0, react_1.useRef)(false);
62
- // Update refs when callbacks change
63
61
  (0, react_1.useEffect)(() => {
64
62
  onValidationCompleteRef.current = onValidationComplete;
65
63
  onErrorRef.current = onError;
66
64
  }, [onValidationComplete, onError]);
67
- // Set language early, before any components render
68
- // Priority: language prop > SDK config language > default 'en'
69
- // Run on mount and whenever language prop changes
70
65
  (0, react_1.useEffect)(() => {
71
66
  if (language) {
72
- // If language prop is provided, override the config
73
67
  (0, biometric_identity_sdk_core_1.setLanguage)(language);
74
68
  }
75
- // If no language prop, the language should already be set by BiometricIdentitySDK.configure()
76
- // The global language state is set when configure() is called, so getStrings() will
77
- // automatically return strings for the configured language (or 'en' as default)
78
69
  }, [language]);
79
70
  const strings = (0, biometric_identity_sdk_core_1.getStrings)();
80
71
  const styles = createStyles(theme);
81
- // Handle validation result - call callback when RESULT step is reached
82
72
  (0, react_1.useEffect)(() => {
83
73
  console.log('🔵 [BiometricIdentityFlow] useEffect triggered - currentStep:', state.currentStep, 'hasResult:', !!state.validationResult, 'hasCalled:', hasCalledValidationComplete.current);
84
74
  if (state.currentStep === biometric_identity_sdk_core_1.SDKStep.RESULT && state.validationResult && !hasCalledValidationComplete.current) {
85
75
  console.log('🟢 [BiometricIdentityFlow] Calling onValidationComplete callback');
86
76
  biometric_identity_sdk_core_1.logger.info('Validation completed, calling onValidationComplete callback');
87
77
  hasCalledValidationComplete.current = true;
88
- // Use ref to avoid dependency issues
89
78
  onValidationCompleteRef.current(state.validationResult);
90
79
  }
91
80
  }, [state.currentStep, state.validationResult]);
92
- // Reset the flag when validation result changes or component resets
93
81
  (0, react_1.useEffect)(() => {
94
82
  if (state.currentStep !== biometric_identity_sdk_core_1.SDKStep.RESULT) {
95
83
  hasCalledValidationComplete.current = false;
96
84
  }
97
85
  }, [state.currentStep]);
98
- // Handle error
99
86
  (0, react_1.useEffect)(() => {
100
87
  if (state.error) {
101
88
  biometric_identity_sdk_core_1.logger.error('SDK error detected:', state.error);
102
- // Use ref to avoid dependency issues
103
89
  onErrorRef.current(state.error);
104
90
  }
105
91
  }, [state.error]);
@@ -108,7 +94,6 @@ const BiometricIdentityFlow = ({ onValidationComplete, onError, theme, language,
108
94
  */
109
95
  const handleCaptureStart = (0, react_1.useCallback)(async (mode) => {
110
96
  setCameraMode(mode);
111
- // If video mode, fetch challenges first
112
97
  if (mode === 'video' && smartLivenessMode && isUsingBackend) {
113
98
  setIsLoadingChallenges(true);
114
99
  try {
@@ -122,7 +107,6 @@ const BiometricIdentityFlow = ({ onValidationComplete, onError, theme, language,
122
107
  setIsLoadingChallenges(false);
123
108
  }
124
109
  else if (mode === 'video' && smartLivenessMode) {
125
- // Use default challenges for offline mode
126
110
  setCurrentChallenges(sdk.getDefaultChallenges());
127
111
  }
128
112
  setShowCamera(true);
@@ -151,11 +135,17 @@ const BiometricIdentityFlow = ({ onValidationComplete, onError, theme, language,
151
135
  });
152
136
  console.log('🔵 [BiometricIdentityFlow] Video stored, starting validation...');
153
137
  biometric_identity_sdk_core_1.logger.info('Starting validation...');
154
- const result = await validateIdentity();
155
- console.log('🟢 [BiometricIdentityFlow] Validation completed:', result);
156
- biometric_identity_sdk_core_1.logger.info('Validation completed successfully:', result);
157
- // Ensure state is synced after validation
158
- // The state should be updated by validateIdentity, but we'll force a sync
138
+ let result = null;
139
+ try {
140
+ result = await validateIdentity();
141
+ console.log('🟢 [BiometricIdentityFlow] Validation completed:', result);
142
+ biometric_identity_sdk_core_1.logger.info('Validation completed successfully:', result);
143
+ }
144
+ catch (validationError) {
145
+ console.error('❌ [BiometricIdentityFlow] Validation error:', validationError);
146
+ biometric_identity_sdk_core_1.logger.error('Validation error:', validationError);
147
+ throw validationError; // Re-throw to be caught by outer catch
148
+ }
159
149
  const finalState = sdk.getState();
160
150
  console.log('🔵 [BiometricIdentityFlow] Final state after validation:', JSON.stringify({
161
151
  currentStep: finalState.currentStep,
@@ -164,23 +154,34 @@ const BiometricIdentityFlow = ({ onValidationComplete, onError, theme, language,
164
154
  isLoading: finalState.isLoading
165
155
  }));
166
156
  biometric_identity_sdk_core_1.logger.info('Final state after validation:', finalState);
167
- // Backup: If we have a result but the useEffect didn't fire, call the callback directly
157
+ if (result && !hasCalledValidationComplete.current) {
158
+ console.log('🟢 [BiometricIdentityFlow] Calling onValidationComplete directly with result');
159
+ biometric_identity_sdk_core_1.logger.info('Calling onValidationComplete directly with validation result');
160
+ hasCalledValidationComplete.current = true;
161
+ onValidationCompleteRef.current(result);
162
+ }
168
163
  if (finalState.currentStep === biometric_identity_sdk_core_1.SDKStep.RESULT && finalState.validationResult && !hasCalledValidationComplete.current) {
169
- console.log('🟢 [BiometricIdentityFlow] Backup: Calling onValidationComplete directly');
170
- biometric_identity_sdk_core_1.logger.info('Backup: Calling onValidationComplete directly after validation');
164
+ console.log('🟢 [BiometricIdentityFlow] Backup: Calling onValidationComplete from state');
165
+ biometric_identity_sdk_core_1.logger.info('Backup: Calling onValidationComplete from state');
171
166
  hasCalledValidationComplete.current = true;
172
167
  onValidationCompleteRef.current(finalState.validationResult);
173
168
  }
174
169
  }
175
170
  }
176
171
  catch (error) {
172
+ console.error('❌ [BiometricIdentityFlow] Capture/validation error:', error);
177
173
  biometric_identity_sdk_core_1.logger.error('Capture/validation error:', error);
178
- const biometricError = {
179
- name: 'BiometricError',
180
- message: error instanceof Error ? error.message : 'Unknown error during capture',
181
- code: 'LIVENESS_CHECK_FAILED',
182
- };
183
- onErrorRef.current(biometricError);
174
+ if (error && typeof error === 'object' && 'code' in error) {
175
+ onErrorRef.current(error);
176
+ }
177
+ else {
178
+ const biometricError = {
179
+ name: 'BiometricError',
180
+ message: error instanceof Error ? error.message : 'Unknown error during capture',
181
+ code: 'LIVENESS_CHECK_FAILED',
182
+ };
183
+ onErrorRef.current(biometricError);
184
+ }
184
185
  }
185
186
  }, [cameraMode, uploadFrontID, uploadBackID, storeVideoRecording, validateIdentity, sdk]);
186
187
  /**
@@ -192,18 +193,15 @@ const BiometricIdentityFlow = ({ onValidationComplete, onError, theme, language,
192
193
  setCurrentChallenges([]);
193
194
  setShowInstructions(true);
194
195
  }, [reset]);
195
- // Show loading while initializing
196
196
  if (!isInitialized) {
197
197
  return (react_1.default.createElement(react_native_1.SafeAreaView, { style: [styles.container, customStyles?.container] },
198
198
  react_1.default.createElement(react_native_1.View, { style: styles.loadingFullScreen },
199
199
  react_1.default.createElement(react_native_1.ActivityIndicator, { size: "large", color: theme?.primaryColor || '#6366F1' }),
200
200
  react_1.default.createElement(react_native_1.Text, { style: styles.loadingText }, strings.initialization.initializing))));
201
201
  }
202
- // Show instructions on first load
203
202
  if (showInstructions) {
204
203
  return (react_1.default.createElement(InstructionsScreen_1.InstructionsScreen, { theme: theme, language: language, onStart: () => setShowInstructions(false), routeBack: routeBack, styles: customStyles }));
205
204
  }
206
- // Show camera/video recorder
207
205
  if (showCamera) {
208
206
  if (cameraMode === 'video') {
209
207
  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 () => {
@@ -213,23 +211,16 @@ const BiometricIdentityFlow = ({ onValidationComplete, onError, theme, language,
213
211
  }
214
212
  return (react_1.default.createElement(CameraCapture_1.CameraCapture, { mode: cameraMode, theme: theme, language: language, onCapture: handleCaptureComplete, onCancel: () => setShowCamera(false) }));
215
213
  }
216
- // Show result (check this first, even if step is still VALIDATING but result exists)
217
- // This handles edge cases where state transition might be delayed
218
214
  if (state.validationResult && (state.currentStep === biometric_identity_sdk_core_1.SDKStep.RESULT || state.progress >= 100)) {
219
- // If we have a result but step is still VALIDATING, force transition to RESULT
220
215
  if (state.currentStep === biometric_identity_sdk_core_1.SDKStep.VALIDATING && state.progress >= 100) {
221
216
  biometric_identity_sdk_core_1.logger.info('Result available but step still VALIDATING, showing ResultScreen');
222
217
  }
223
218
  return (react_1.default.createElement(ResultScreen_1.ResultScreen, { result: state.validationResult, theme: theme, language: language, onClose: () => {
224
- // Callback already called automatically when RESULT step was reached
225
- // This onClose is just for UI cleanup/navigation
226
219
  } }));
227
220
  }
228
- // Show validation progress
229
221
  if (state.currentStep === biometric_identity_sdk_core_1.SDKStep.VALIDATING) {
230
222
  return (react_1.default.createElement(ValidationProgress_1.ValidationProgress, { progress: state.progress, theme: theme, language: language }));
231
223
  }
232
- // Show error
233
224
  if (state.currentStep === biometric_identity_sdk_core_1.SDKStep.ERROR && state.error) {
234
225
  return (react_1.default.createElement(ErrorScreen_1.ErrorScreen, { error: state.error, theme: theme, language: language, onRetry: handleRetry, onClose: () => onError(state.error) }));
235
226
  }
@@ -334,7 +325,6 @@ const buttonStyles = react_native_1.StyleSheet.create({
334
325
  marginTop: 4,
335
326
  },
336
327
  });
337
- // Create styles based on theme
338
328
  const createStyles = (theme) => {
339
329
  const backgroundColor = theme?.backgroundColor || '#FFFFFF';
340
330
  const textColor = theme?.textColor || '#000000';
@@ -1 +1 @@
1
- {"version":3,"file":"VideoRecorder.d.ts","sourceRoot":"","sources":["../../src/components/VideoRecorder.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAmD,MAAM,OAAO,CAAC;AAaxE,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAE,iBAAiB,EAAmC,MAAM,oCAAoC,CAAC;AAG1I,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,CAkyBtD,CAAC;AA4OF,eAAe,aAAa,CAAC"}
1
+ {"version":3,"file":"VideoRecorder.d.ts","sourceRoot":"","sources":["../../src/components/VideoRecorder.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAmD,MAAM,OAAO,CAAC;AAaxE,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAE,iBAAiB,EAAmC,MAAM,oCAAoC,CAAC;AAE1I,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,sEAAsE;IACtE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,sCAAsC;IACtC,YAAY,CAAC,EAAE,mBAAmB,EAAE,CAAC;IACrC,0CAA0C;IAC1C,UAAU,CAAC,EAAE,eAAe,EAAE,CAAC;IAC/B,wCAAwC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kDAAkD;IAClD,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,wCAAwC;IACxC,UAAU,EAAE,CAAC,SAAS,EAAE,oBAAoB,KAAK,IAAI,CAAC;IACtD,iCAAiC;IACjC,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,gDAAgD;IAChD,iBAAiB,CAAC,EAAE,MAAM,OAAO,CAAC,eAAe,EAAE,CAAC,CAAC;CACtD;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,oBAAoB,EAAE,OAAO,CAAC;IAC9B,YAAY,EAAE,MAAM,CAAC;IACrB,mBAAmB,EAAE,MAAM,EAAE,CAAC;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AA8CD,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CA2xBtD,CAAC;AA4OF,eAAe,aAAa,CAAC"}
@@ -43,32 +43,31 @@ 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
45
  const biometric_identity_sdk_core_1 = require("@hexar/biometric-identity-sdk-core");
46
- // Default challenge set (used if backend not available)
47
- const DEFAULT_CHALLENGES = [
46
+ const getDefaultChallenges = (strings) => [
48
47
  {
49
48
  action: 'look_left',
50
- instruction: 'Slowly turn your head to the LEFT',
49
+ instruction: strings.liveness.instructions.lookLeft || 'Slowly turn your head to the LEFT',
51
50
  duration_ms: 2500,
52
51
  order: 1,
53
52
  icon: '←',
54
53
  },
55
54
  {
56
55
  action: 'look_right',
57
- instruction: 'Slowly turn your head to the RIGHT',
56
+ instruction: strings.liveness.instructions.lookRight || 'Slowly turn your head to the RIGHT',
58
57
  duration_ms: 2500,
59
58
  order: 2,
60
59
  icon: '→',
61
60
  },
62
61
  {
63
62
  action: 'blink',
64
- instruction: 'Blink your eyes naturally',
63
+ instruction: strings.liveness.instructions.blink || 'Blink your eyes naturally',
65
64
  duration_ms: 2000,
66
65
  order: 3,
67
66
  icon: '👁',
68
67
  },
69
68
  {
70
69
  action: 'smile',
71
- instruction: 'Smile 😊',
70
+ instruction: strings.liveness.instructions.smile || 'Smile 😊',
72
71
  duration_ms: 2000,
73
72
  order: 4,
74
73
  icon: '😊',
@@ -87,11 +86,12 @@ const getInstructionMap = (strings) => ({
87
86
  stay_still: { text: strings.liveness.instructions.stayStill || 'Look at the camera and follow the instructions', icon: '📷' },
88
87
  });
89
88
  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
- }
89
+ (0, react_1.useEffect)(() => {
90
+ if (language) {
91
+ (0, biometric_identity_sdk_core_1.setLanguage)(language);
92
+ }
93
+ }, [language]);
93
94
  const strings = (0, biometric_identity_sdk_core_1.getStrings)();
94
- // State
95
95
  const [phase, setPhase] = (0, react_1.useState)('loading');
96
96
  const [countdown, setCountdown] = (0, react_1.useState)(3);
97
97
  const [challenges, setChallenges] = (0, react_1.useState)([]);
@@ -101,11 +101,9 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
101
101
  const [completedChallenges, setCompletedChallenges] = (0, react_1.useState)([]);
102
102
  const [frames, setFrames] = (0, react_1.useState)([]);
103
103
  const [hasPermission, setHasPermission] = (0, react_1.useState)(false);
104
- // Camera
105
104
  const cameraRef = (0, react_1.useRef)(null);
106
105
  const { hasPermission: cameraPermission, requestPermission } = (0, react_native_vision_camera_1.useCameraPermission)();
107
106
  const device = (0, react_native_vision_camera_1.useCameraDevice)('front');
108
- // Animations
109
107
  const fadeAnim = (0, react_1.useRef)(new react_native_1.Animated.Value(0)).current;
110
108
  const scaleAnim = (0, react_1.useRef)(new react_native_1.Animated.Value(1)).current;
111
109
  const pulseAnim = (0, react_1.useRef)(new react_native_1.Animated.Value(1)).current;
@@ -119,19 +117,15 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
119
117
  const recordingTimeoutRef = (0, react_1.useRef)(null);
120
118
  const minDurationMs = 8000;
121
119
  const totalDuration = duration || Math.max(minDurationMs, challenges.reduce((sum, c) => sum + c.duration_ms, 0) + 2000);
122
- // Check camera permissions
123
120
  (0, react_1.useEffect)(() => {
124
121
  const checkPermissions = async () => {
125
122
  try {
126
- // First check if we already have permission from the hook
127
123
  if (cameraPermission) {
128
124
  setHasPermission(true);
129
125
  return;
130
126
  }
131
- // Otherwise, request permission using the hook
132
127
  const granted = await requestPermission();
133
128
  setHasPermission(granted);
134
- // If hook method didn't work, try platform-specific request
135
129
  if (!granted) {
136
130
  if (react_native_1.Platform.OS === 'ios') {
137
131
  const result = await (0, react_native_permissions_1.request)(react_native_permissions_1.PERMISSIONS.IOS.CAMERA);
@@ -151,21 +145,28 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
151
145
  };
152
146
  checkPermissions();
153
147
  }, [cameraPermission, requestPermission]);
154
- // Initialize challenges
155
148
  (0, react_1.useEffect)(() => {
156
149
  const initChallenges = async () => {
157
150
  try {
158
151
  let challengeList;
152
+ const currentStrings = (0, biometric_identity_sdk_core_1.getStrings)();
153
+ const instructionMap = getInstructionMap(currentStrings);
159
154
  if (propChallenges && propChallenges.length > 0) {
160
- // Use provided challenges
161
- challengeList = propChallenges;
155
+ challengeList = propChallenges.map(challenge => ({
156
+ ...challenge,
157
+ instruction: instructionMap[challenge.action]?.text || challenge.instruction,
158
+ icon: instructionMap[challenge.action]?.icon || challenge.icon,
159
+ }));
162
160
  }
163
161
  else if (onFetchChallenges) {
164
- // Fetch from backend
165
- challengeList = await onFetchChallenges();
162
+ const fetchedChallenges = await onFetchChallenges();
163
+ challengeList = fetchedChallenges.map(challenge => ({
164
+ ...challenge,
165
+ instruction: instructionMap[challenge.action]?.text || challenge.instruction,
166
+ icon: instructionMap[challenge.action]?.icon || challenge.icon,
167
+ }));
166
168
  }
167
169
  else if (instructions && instructions.length > 0) {
168
- const instructionMap = getInstructionMap(strings);
169
170
  challengeList = instructions.map((inst, idx) => ({
170
171
  action: inst,
171
172
  instruction: instructionMap[inst]?.text || inst,
@@ -175,11 +176,10 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
175
176
  }));
176
177
  }
177
178
  else {
178
- // Use default challenges
179
- challengeList = smartMode ? DEFAULT_CHALLENGES : [
179
+ challengeList = smartMode ? getDefaultChallenges(currentStrings) : [
180
180
  {
181
181
  action: 'stay_still',
182
- instruction: 'Look at the camera and stay still',
182
+ instruction: currentStrings.liveness.instructions.stayStill || 'Look at the camera and stay still',
183
183
  duration_ms: duration || 5000,
184
184
  order: 1,
185
185
  icon: '📷',
@@ -191,19 +191,16 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
191
191
  }
192
192
  catch (error) {
193
193
  biometric_identity_sdk_core_1.logger.error('Failed to fetch challenges:', error);
194
- // Fallback to default
195
- setChallenges(DEFAULT_CHALLENGES);
194
+ setChallenges(getDefaultChallenges((0, biometric_identity_sdk_core_1.getStrings)()));
196
195
  setPhase('countdown');
197
196
  }
198
197
  };
199
198
  initChallenges();
200
- }, [propChallenges, instructions, onFetchChallenges, smartMode, duration]);
201
- // Countdown phase
199
+ }, [propChallenges, instructions, onFetchChallenges, smartMode, duration, language]);
202
200
  (0, react_1.useEffect)(() => {
203
201
  if (phase !== 'countdown')
204
202
  return;
205
203
  if (countdown > 0) {
206
- // Animate countdown number
207
204
  react_native_1.Animated.sequence([
208
205
  react_native_1.Animated.timing(scaleAnim, {
209
206
  toValue: 1.3,
@@ -223,7 +220,6 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
223
220
  startRecording();
224
221
  }
225
222
  }, [countdown, phase]);
226
- // Start pulse animation for recording indicator
227
223
  (0, react_1.useEffect)(() => {
228
224
  if (phase === 'recording') {
229
225
  react_native_1.Animated.loop(react_native_1.Animated.sequence([
@@ -242,7 +238,6 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
242
238
  ])).start();
243
239
  }
244
240
  }, [phase]);
245
- // Animate arrow for directional challenges
246
241
  const animateArrow = (0, react_1.useCallback)((direction) => {
247
242
  const toValue = direction.includes('left') ? -20 : direction.includes('right') ? 20 : 0;
248
243
  react_native_1.Animated.loop(react_native_1.Animated.sequence([
@@ -356,7 +351,6 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
356
351
  }
357
352
  }
358
353
  catch (error) {
359
- // Silent frame capture errors
360
354
  }
361
355
  }, 100);
362
356
  }
@@ -546,9 +540,7 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
546
540
  }
547
541
  };
548
542
  }, [device, totalDuration, minDurationMs, handleVideoComplete, handleRecordingError, runChallenge, stopRecording, startFrameCapture]);
549
- // Current challenge
550
543
  const currentChallenge = challenges[currentChallengeIndex];
551
- // Get direction arrow for the current challenge
552
544
  const getDirectionIndicator = () => {
553
545
  if (!currentChallenge)
554
546
  return null;
@@ -1 +1 @@
1
- {"version":3,"file":"useBiometricSDK.d.ts","sourceRoot":"","sources":["../../src/hooks/useBiometricSDK.ts"],"names":[],"mappings":"AACA,OAAO,EACL,oBAAoB,EACpB,QAAQ,EACR,WAAW,EAGZ,MAAM,oCAAoC,CAAC;AAE5C,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,qBAAqB;IACpC,GAAG,EAAE,oBAAoB,CAAC;IAC1B,KAAK,EAAE,QAAQ,CAAC;IAChB,aAAa,EAAE,OAAO,CAAC;IACvB,cAAc,EAAE,OAAO,CAAC;IACxB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,UAAU,EAAE,eAAe,EAAE,CAAC;IAC9B,aAAa,EAAE,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAClE,YAAY,EAAE,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACjE,mBAAmB,EAAE,CAAC,SAAS,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/D,eAAe,EAAE,CAAC,IAAI,CAAC,EAAE,QAAQ,GAAG,SAAS,KAAK,OAAO,CAAC,eAAe,EAAE,CAAC,CAAC;IAC7E,gBAAgB,EAAE,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC;IACrC,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,WAAW,EAAE,CAAC,SAAS,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACzD;AAED,eAAO,MAAM,eAAe,QAAO,qBAoQlC,CAAC;AAEF,eAAe,eAAe,CAAC"}
1
+ {"version":3,"file":"useBiometricSDK.d.ts","sourceRoot":"","sources":["../../src/hooks/useBiometricSDK.ts"],"names":[],"mappings":"AACA,OAAO,EACL,oBAAoB,EACpB,QAAQ,EACR,WAAW,EAGZ,MAAM,oCAAoC,CAAC;AAE5C,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,qBAAqB;IACpC,GAAG,EAAE,oBAAoB,CAAC;IAC1B,KAAK,EAAE,QAAQ,CAAC;IAChB,aAAa,EAAE,OAAO,CAAC;IACvB,cAAc,EAAE,OAAO,CAAC;IACxB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,UAAU,EAAE,eAAe,EAAE,CAAC;IAC9B,aAAa,EAAE,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAClE,YAAY,EAAE,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACjE,mBAAmB,EAAE,CAAC,SAAS,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/D,eAAe,EAAE,CAAC,IAAI,CAAC,EAAE,QAAQ,GAAG,SAAS,KAAK,OAAO,CAAC,eAAe,EAAE,CAAC,CAAC;IAC7E,gBAAgB,EAAE,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC;IACrC,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,WAAW,EAAE,CAAC,SAAS,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACzD;AAED,eAAO,MAAM,eAAe,QAAO,qBA4QlC,CAAC;AAEF,eAAe,eAAe,CAAC"}
@@ -146,6 +146,7 @@ const useBiometricSDK = () => {
146
146
  setState(currentState);
147
147
  }
148
148
  }, 200);
149
+ let finalPollInterval = null;
149
150
  try {
150
151
  console.log('🔵 [useBiometricSDK] Calling sdk.validateIdentity()...');
151
152
  const result = await sdk.validateIdentity();
@@ -154,7 +155,7 @@ const useBiometricSDK = () => {
154
155
  // The SDK updates state synchronously, but we want to make sure React sees it
155
156
  let pollCount = 0;
156
157
  const maxPolls = 10; // Poll for up to 2 seconds (10 * 200ms)
157
- const finalPollInterval = setInterval(() => {
158
+ finalPollInterval = setInterval(() => {
158
159
  pollCount++;
159
160
  if (isMounted.current) {
160
161
  const currentState = sdk.getState();
@@ -163,13 +164,15 @@ const useBiometricSDK = () => {
163
164
  // Stop polling once we reach RESULT or ERROR state
164
165
  if (currentState.currentStep === biometric_identity_sdk_core_1.SDKStep.RESULT || currentState.currentStep === biometric_identity_sdk_core_1.SDKStep.ERROR) {
165
166
  console.log('🟢 [useBiometricSDK] Reached final state:', currentState.currentStep);
166
- clearInterval(finalPollInterval);
167
+ if (finalPollInterval)
168
+ clearInterval(finalPollInterval);
167
169
  clearInterval(pollInterval);
168
170
  biometric_identity_sdk_core_1.logger.info('Reached final state, stopped polling:', currentState);
169
171
  }
170
172
  else if (pollCount >= maxPolls) {
171
173
  // Force stop after max polls
172
- clearInterval(finalPollInterval);
174
+ if (finalPollInterval)
175
+ clearInterval(finalPollInterval);
173
176
  clearInterval(pollInterval);
174
177
  const finalState = sdk.getState();
175
178
  biometric_identity_sdk_core_1.logger.warn('Max polls reached, forcing final state:', finalState);
@@ -184,17 +187,23 @@ const useBiometricSDK = () => {
184
187
  setState(immediateState);
185
188
  // If we're already at RESULT, clear intervals immediately
186
189
  if (immediateState.currentStep === biometric_identity_sdk_core_1.SDKStep.RESULT || immediateState.currentStep === biometric_identity_sdk_core_1.SDKStep.ERROR) {
187
- clearInterval(finalPollInterval);
190
+ if (finalPollInterval)
191
+ clearInterval(finalPollInterval);
188
192
  clearInterval(pollInterval);
189
193
  }
190
194
  }
191
195
  return result;
192
196
  }
193
197
  catch (error) {
198
+ // Clear all intervals on error
194
199
  clearInterval(pollInterval);
200
+ if (finalPollInterval) {
201
+ clearInterval(finalPollInterval);
202
+ }
203
+ console.error('❌ [useBiometricSDK] Validation error:', error);
204
+ biometric_identity_sdk_core_1.logger.error('Validation error, state:', sdk.getState(), error);
195
205
  if (isMounted.current) {
196
206
  const errorState = sdk.getState();
197
- biometric_identity_sdk_core_1.logger.error('Validation error, state:', errorState, error);
198
207
  setState(errorState);
199
208
  }
200
209
  throw error;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hexar/biometric-identity-sdk-react-native",
3
- "version": "1.0.17",
3
+ "version": "1.0.19",
4
4
  "description": "React Native wrapper for Biometric Identity SDK",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -11,7 +11,7 @@
11
11
  "clean": "rm -rf dist"
12
12
  },
13
13
  "peerDependencies": {
14
- "@hexar/biometric-identity-sdk-core": ">=1.0.0",
14
+ "@hexar/biometric-identity-sdk-core": ">=1.0.11",
15
15
  "react": ">=18.0.0",
16
16
  "react-native": ">=0.70.0",
17
17
  "react-native-permissions": ">=4.0.0",
@@ -77,57 +77,43 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
77
77
  const [currentChallenges, setCurrentChallenges] = useState<ChallengeAction[]>([]);
78
78
  const [isLoadingChallenges, setIsLoadingChallenges] = useState(false);
79
79
 
80
- // Use refs to store callbacks to avoid dependency issues in useEffect
81
80
  const onValidationCompleteRef = useRef(onValidationComplete);
82
81
  const onErrorRef = useRef(onError);
83
82
  const hasCalledValidationComplete = useRef(false);
84
83
 
85
- // Update refs when callbacks change
86
84
  useEffect(() => {
87
85
  onValidationCompleteRef.current = onValidationComplete;
88
86
  onErrorRef.current = onError;
89
87
  }, [onValidationComplete, onError]);
90
88
 
91
- // Set language early, before any components render
92
- // Priority: language prop > SDK config language > default 'en'
93
- // Run on mount and whenever language prop changes
94
89
  useEffect(() => {
95
90
  if (language) {
96
- // If language prop is provided, override the config
97
91
  setLanguage(language);
98
92
  }
99
- // If no language prop, the language should already be set by BiometricIdentitySDK.configure()
100
- // The global language state is set when configure() is called, so getStrings() will
101
- // automatically return strings for the configured language (or 'en' as default)
102
93
  }, [language]);
103
94
 
104
95
  const strings = getStrings();
105
96
  const styles = createStyles(theme);
106
97
 
107
- // Handle validation result - call callback when RESULT step is reached
108
98
  useEffect(() => {
109
99
  console.log('🔵 [BiometricIdentityFlow] useEffect triggered - currentStep:', state.currentStep, 'hasResult:', !!state.validationResult, 'hasCalled:', hasCalledValidationComplete.current);
110
100
  if (state.currentStep === SDKStep.RESULT && state.validationResult && !hasCalledValidationComplete.current) {
111
101
  console.log('🟢 [BiometricIdentityFlow] Calling onValidationComplete callback');
112
102
  logger.info('Validation completed, calling onValidationComplete callback');
113
103
  hasCalledValidationComplete.current = true;
114
- // Use ref to avoid dependency issues
115
104
  onValidationCompleteRef.current(state.validationResult);
116
105
  }
117
106
  }, [state.currentStep, state.validationResult]);
118
107
 
119
- // Reset the flag when validation result changes or component resets
120
108
  useEffect(() => {
121
109
  if (state.currentStep !== SDKStep.RESULT) {
122
110
  hasCalledValidationComplete.current = false;
123
111
  }
124
112
  }, [state.currentStep]);
125
113
 
126
- // Handle error
127
114
  useEffect(() => {
128
115
  if (state.error) {
129
116
  logger.error('SDK error detected:', state.error);
130
- // Use ref to avoid dependency issues
131
117
  onErrorRef.current(state.error);
132
118
  }
133
119
  }, [state.error]);
@@ -138,7 +124,6 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
138
124
  const handleCaptureStart = useCallback(async (mode: 'front' | 'back' | 'video') => {
139
125
  setCameraMode(mode);
140
126
 
141
- // If video mode, fetch challenges first
142
127
  if (mode === 'video' && smartLivenessMode && isUsingBackend) {
143
128
  setIsLoadingChallenges(true);
144
129
  try {
@@ -150,7 +135,6 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
150
135
  }
151
136
  setIsLoadingChallenges(false);
152
137
  } else if (mode === 'video' && smartLivenessMode) {
153
- // Use default challenges for offline mode
154
138
  setCurrentChallenges(sdk.getDefaultChallenges());
155
139
  }
156
140
 
@@ -182,12 +166,18 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
182
166
 
183
167
  console.log('🔵 [BiometricIdentityFlow] Video stored, starting validation...');
184
168
  logger.info('Starting validation...');
185
- const result = await validateIdentity();
186
- console.log('🟢 [BiometricIdentityFlow] Validation completed:', result);
187
- logger.info('Validation completed successfully:', result);
188
169
 
189
- // Ensure state is synced after validation
190
- // The state should be updated by validateIdentity, but we'll force a sync
170
+ let result: ValidationResult | null = null;
171
+ try {
172
+ result = await validateIdentity();
173
+ console.log('🟢 [BiometricIdentityFlow] Validation completed:', result);
174
+ logger.info('Validation completed successfully:', result);
175
+ } catch (validationError) {
176
+ console.error('❌ [BiometricIdentityFlow] Validation error:', validationError);
177
+ logger.error('Validation error:', validationError);
178
+ throw validationError; // Re-throw to be caught by outer catch
179
+ }
180
+
191
181
  const finalState = sdk.getState();
192
182
  console.log('🔵 [BiometricIdentityFlow] Final state after validation:', JSON.stringify({
193
183
  currentStep: finalState.currentStep,
@@ -197,22 +187,34 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
197
187
  }));
198
188
  logger.info('Final state after validation:', finalState);
199
189
 
200
- // Backup: If we have a result but the useEffect didn't fire, call the callback directly
190
+ if (result && !hasCalledValidationComplete.current) {
191
+ console.log('🟢 [BiometricIdentityFlow] Calling onValidationComplete directly with result');
192
+ logger.info('Calling onValidationComplete directly with validation result');
193
+ hasCalledValidationComplete.current = true;
194
+ onValidationCompleteRef.current(result);
195
+ }
196
+
201
197
  if (finalState.currentStep === SDKStep.RESULT && finalState.validationResult && !hasCalledValidationComplete.current) {
202
- console.log('🟢 [BiometricIdentityFlow] Backup: Calling onValidationComplete directly');
203
- logger.info('Backup: Calling onValidationComplete directly after validation');
198
+ console.log('🟢 [BiometricIdentityFlow] Backup: Calling onValidationComplete from state');
199
+ logger.info('Backup: Calling onValidationComplete from state');
204
200
  hasCalledValidationComplete.current = true;
205
201
  onValidationCompleteRef.current(finalState.validationResult);
206
202
  }
207
203
  }
208
204
  } catch (error) {
205
+ console.error('❌ [BiometricIdentityFlow] Capture/validation error:', error);
209
206
  logger.error('Capture/validation error:', error);
210
- const biometricError: BiometricError = {
211
- name: 'BiometricError',
212
- message: error instanceof Error ? error.message : 'Unknown error during capture',
213
- code: 'LIVENESS_CHECK_FAILED',
214
- } as BiometricError;
215
- onErrorRef.current(biometricError);
207
+
208
+ if (error && typeof error === 'object' && 'code' in error) {
209
+ onErrorRef.current(error as BiometricError);
210
+ } else {
211
+ const biometricError: BiometricError = {
212
+ name: 'BiometricError',
213
+ message: error instanceof Error ? error.message : 'Unknown error during capture',
214
+ code: 'LIVENESS_CHECK_FAILED',
215
+ } as BiometricError;
216
+ onErrorRef.current(biometricError);
217
+ }
216
218
  }
217
219
  }, [cameraMode, uploadFrontID, uploadBackID, storeVideoRecording, validateIdentity, sdk]);
218
220
 
@@ -226,7 +228,6 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
226
228
  setShowInstructions(true);
227
229
  }, [reset]);
228
230
 
229
- // Show loading while initializing
230
231
  if (!isInitialized) {
231
232
  return (
232
233
  <SafeAreaView style={[styles.container, customStyles?.container]}>
@@ -238,7 +239,6 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
238
239
  );
239
240
  }
240
241
 
241
- // Show instructions on first load
242
242
  if (showInstructions) {
243
243
  return (
244
244
  <InstructionsScreen
@@ -251,7 +251,6 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
251
251
  );
252
252
  }
253
253
 
254
- // Show camera/video recorder
255
254
  if (showCamera) {
256
255
  if (cameraMode === 'video') {
257
256
  return (
@@ -282,10 +281,7 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
282
281
  );
283
282
  }
284
283
 
285
- // Show result (check this first, even if step is still VALIDATING but result exists)
286
- // This handles edge cases where state transition might be delayed
287
284
  if (state.validationResult && (state.currentStep === SDKStep.RESULT || state.progress >= 100)) {
288
- // If we have a result but step is still VALIDATING, force transition to RESULT
289
285
  if (state.currentStep === SDKStep.VALIDATING && state.progress >= 100) {
290
286
  logger.info('Result available but step still VALIDATING, showing ResultScreen');
291
287
  }
@@ -295,14 +291,11 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
295
291
  theme={theme}
296
292
  language={language}
297
293
  onClose={() => {
298
- // Callback already called automatically when RESULT step was reached
299
- // This onClose is just for UI cleanup/navigation
300
294
  }}
301
295
  />
302
296
  );
303
297
  }
304
298
 
305
- // Show validation progress
306
299
  if (state.currentStep === SDKStep.VALIDATING) {
307
300
  return (
308
301
  <ValidationProgress
@@ -313,7 +306,6 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
313
306
  );
314
307
  }
315
308
 
316
- // Show error
317
309
  if (state.currentStep === SDKStep.ERROR && state.error) {
318
310
  return (
319
311
  <ErrorScreen
@@ -410,7 +402,6 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
410
402
  );
411
403
  };
412
404
 
413
- // Step Indicator Component
414
405
  interface StepIndicatorProps {
415
406
  step: number;
416
407
  active: boolean;
@@ -478,7 +469,6 @@ const stepStyles = StyleSheet.create({
478
469
  },
479
470
  });
480
471
 
481
- // Action Button Component
482
472
  interface ActionButtonProps {
483
473
  title: string;
484
474
  subtitle?: string;
@@ -542,7 +532,6 @@ const buttonStyles = StyleSheet.create({
542
532
  },
543
533
  });
544
534
 
545
- // Create styles based on theme
546
535
  const createStyles = (theme?: ThemeConfig) => {
547
536
  const backgroundColor = theme?.backgroundColor || '#FFFFFF';
548
537
  const textColor = theme?.textColor || '#000000';
@@ -18,7 +18,6 @@ import { Camera, useCameraDevice, useCameraPermission } from 'react-native-visio
18
18
  import { request, PERMISSIONS, RESULTS } from 'react-native-permissions';
19
19
  import { ThemeConfig, LivenessInstruction, SupportedLanguage, getStrings, setLanguage, logger } from '@hexar/biometric-identity-sdk-core';
20
20
 
21
- // Challenge action configuration (matches backend response)
22
21
  export interface ChallengeAction {
23
22
  action: string;
24
23
  instruction: string;
@@ -57,32 +56,31 @@ export interface VideoRecordingResult {
57
56
  sessionId?: string;
58
57
  }
59
58
 
60
- // Default challenge set (used if backend not available)
61
- const DEFAULT_CHALLENGES: ChallengeAction[] = [
59
+ const getDefaultChallenges = (strings: any): ChallengeAction[] => [
62
60
  {
63
61
  action: 'look_left',
64
- instruction: 'Slowly turn your head to the LEFT',
62
+ instruction: strings.liveness.instructions.lookLeft || 'Slowly turn your head to the LEFT',
65
63
  duration_ms: 2500,
66
64
  order: 1,
67
65
  icon: '←',
68
66
  },
69
67
  {
70
68
  action: 'look_right',
71
- instruction: 'Slowly turn your head to the RIGHT',
69
+ instruction: strings.liveness.instructions.lookRight || 'Slowly turn your head to the RIGHT',
72
70
  duration_ms: 2500,
73
71
  order: 2,
74
72
  icon: '→',
75
73
  },
76
74
  {
77
75
  action: 'blink',
78
- instruction: 'Blink your eyes naturally',
76
+ instruction: strings.liveness.instructions.blink || 'Blink your eyes naturally',
79
77
  duration_ms: 2000,
80
78
  order: 3,
81
79
  icon: '👁',
82
80
  },
83
81
  {
84
82
  action: 'smile',
85
- instruction: 'Smile 😊',
83
+ instruction: strings.liveness.instructions.smile || 'Smile 😊',
86
84
  duration_ms: 2000,
87
85
  order: 4,
88
86
  icon: '😊',
@@ -114,12 +112,13 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
114
112
  onCancel,
115
113
  onFetchChallenges,
116
114
  }) => {
117
- if (language) {
118
- setLanguage(language);
119
- }
120
- const strings = getStrings();
115
+ useEffect(() => {
116
+ if (language) {
117
+ setLanguage(language);
118
+ }
119
+ }, [language]);
121
120
 
122
- // State
121
+ const strings = getStrings();
123
122
  const [phase, setPhase] = useState<'loading' | 'countdown' | 'recording' | 'processing'>('loading');
124
123
  const [countdown, setCountdown] = useState(3);
125
124
  const [challenges, setChallenges] = useState<ChallengeAction[]>([]);
@@ -130,12 +129,10 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
130
129
  const [frames, setFrames] = useState<string[]>([]);
131
130
  const [hasPermission, setHasPermission] = useState(false);
132
131
 
133
- // Camera
134
132
  const cameraRef = useRef<Camera>(null);
135
133
  const { hasPermission: cameraPermission, requestPermission } = useCameraPermission();
136
134
  const device = useCameraDevice('front');
137
135
 
138
- // Animations
139
136
  const fadeAnim = useRef(new Animated.Value(0)).current;
140
137
  const scaleAnim = useRef(new Animated.Value(1)).current;
141
138
  const pulseAnim = useRef(new Animated.Value(1)).current;
@@ -154,21 +151,17 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
154
151
  challenges.reduce((sum, c) => sum + c.duration_ms, 0) + 2000
155
152
  );
156
153
 
157
- // Check camera permissions
158
154
  useEffect(() => {
159
155
  const checkPermissions = async () => {
160
156
  try {
161
- // First check if we already have permission from the hook
162
157
  if (cameraPermission) {
163
158
  setHasPermission(true);
164
159
  return;
165
160
  }
166
161
 
167
- // Otherwise, request permission using the hook
168
162
  const granted = await requestPermission();
169
163
  setHasPermission(granted);
170
164
 
171
- // If hook method didn't work, try platform-specific request
172
165
  if (!granted) {
173
166
  if (Platform.OS === 'ios') {
174
167
  const result = await request(PERMISSIONS.IOS.CAMERA);
@@ -191,20 +184,27 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
191
184
  checkPermissions();
192
185
  }, [cameraPermission, requestPermission]);
193
186
 
194
- // Initialize challenges
195
187
  useEffect(() => {
196
188
  const initChallenges = async () => {
197
189
  try {
198
190
  let challengeList: ChallengeAction[];
191
+ const currentStrings = getStrings();
192
+ const instructionMap = getInstructionMap(currentStrings);
199
193
 
200
194
  if (propChallenges && propChallenges.length > 0) {
201
- // Use provided challenges
202
- challengeList = propChallenges;
195
+ challengeList = propChallenges.map(challenge => ({
196
+ ...challenge,
197
+ instruction: instructionMap[challenge.action]?.text || challenge.instruction,
198
+ icon: instructionMap[challenge.action]?.icon || challenge.icon,
199
+ }));
203
200
  } else if (onFetchChallenges) {
204
- // Fetch from backend
205
- challengeList = await onFetchChallenges();
201
+ const fetchedChallenges = await onFetchChallenges();
202
+ challengeList = fetchedChallenges.map(challenge => ({
203
+ ...challenge,
204
+ instruction: instructionMap[challenge.action]?.text || challenge.instruction,
205
+ icon: instructionMap[challenge.action]?.icon || challenge.icon,
206
+ }));
206
207
  } else if (instructions && instructions.length > 0) {
207
- const instructionMap = getInstructionMap(strings);
208
208
  challengeList = instructions.map((inst, idx) => ({
209
209
  action: inst,
210
210
  instruction: instructionMap[inst]?.text || inst,
@@ -213,11 +213,10 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
213
213
  icon: instructionMap[inst]?.icon,
214
214
  }));
215
215
  } else {
216
- // Use default challenges
217
- challengeList = smartMode ? DEFAULT_CHALLENGES : [
216
+ challengeList = smartMode ? getDefaultChallenges(currentStrings) : [
218
217
  {
219
218
  action: 'stay_still',
220
- instruction: 'Look at the camera and stay still',
219
+ instruction: currentStrings.liveness.instructions.stayStill || 'Look at the camera and stay still',
221
220
  duration_ms: duration || 5000,
222
221
  order: 1,
223
222
  icon: '📷',
@@ -229,21 +228,18 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
229
228
  setPhase('countdown');
230
229
  } catch (error) {
231
230
  logger.error('Failed to fetch challenges:', error);
232
- // Fallback to default
233
- setChallenges(DEFAULT_CHALLENGES);
231
+ setChallenges(getDefaultChallenges(getStrings()));
234
232
  setPhase('countdown');
235
233
  }
236
234
  };
237
235
 
238
236
  initChallenges();
239
- }, [propChallenges, instructions, onFetchChallenges, smartMode, duration]);
237
+ }, [propChallenges, instructions, onFetchChallenges, smartMode, duration, language]);
240
238
 
241
- // Countdown phase
242
239
  useEffect(() => {
243
240
  if (phase !== 'countdown') return;
244
241
 
245
242
  if (countdown > 0) {
246
- // Animate countdown number
247
243
  Animated.sequence([
248
244
  Animated.timing(scaleAnim, {
249
245
  toValue: 1.3,
@@ -264,7 +260,6 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
264
260
  }
265
261
  }, [countdown, phase]);
266
262
 
267
- // Start pulse animation for recording indicator
268
263
  useEffect(() => {
269
264
  if (phase === 'recording') {
270
265
  Animated.loop(
@@ -286,7 +281,6 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
286
281
  }
287
282
  }, [phase]);
288
283
 
289
- // Animate arrow for directional challenges
290
284
  const animateArrow = useCallback((direction: string) => {
291
285
  const toValue = direction.includes('left') ? -20 : direction.includes('right') ? 20 : 0;
292
286
 
@@ -422,7 +416,6 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
422
416
  }
423
417
  }
424
418
  } catch (error) {
425
- // Silent frame capture errors
426
419
  }
427
420
  }, 100);
428
421
  }
@@ -641,10 +634,8 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
641
634
  };
642
635
  }, [device, totalDuration, minDurationMs, handleVideoComplete, handleRecordingError, runChallenge, stopRecording, startFrameCapture]);
643
636
 
644
- // Current challenge
645
637
  const currentChallenge = challenges[currentChallengeIndex];
646
638
 
647
- // Get direction arrow for the current challenge
648
639
  const getDirectionIndicator = () => {
649
640
  if (!currentChallenge) return null;
650
641
 
@@ -205,6 +205,8 @@ export const useBiometricSDK = (): UseBiometricSDKResult => {
205
205
  }
206
206
  }, 200);
207
207
 
208
+ let finalPollInterval: NodeJS.Timeout | null = null;
209
+
208
210
  try {
209
211
  console.log('🔵 [useBiometricSDK] Calling sdk.validateIdentity()...');
210
212
  const result = await sdk.validateIdentity();
@@ -215,7 +217,7 @@ export const useBiometricSDK = (): UseBiometricSDKResult => {
215
217
  let pollCount = 0;
216
218
  const maxPolls = 10; // Poll for up to 2 seconds (10 * 200ms)
217
219
 
218
- const finalPollInterval = setInterval(() => {
220
+ finalPollInterval = setInterval(() => {
219
221
  pollCount++;
220
222
  if (isMounted.current) {
221
223
  const currentState = sdk.getState();
@@ -225,12 +227,12 @@ export const useBiometricSDK = (): UseBiometricSDKResult => {
225
227
  // Stop polling once we reach RESULT or ERROR state
226
228
  if (currentState.currentStep === SDKStep.RESULT || currentState.currentStep === SDKStep.ERROR) {
227
229
  console.log('🟢 [useBiometricSDK] Reached final state:', currentState.currentStep);
228
- clearInterval(finalPollInterval);
230
+ if (finalPollInterval) clearInterval(finalPollInterval);
229
231
  clearInterval(pollInterval);
230
232
  logger.info('Reached final state, stopped polling:', currentState);
231
233
  } else if (pollCount >= maxPolls) {
232
234
  // Force stop after max polls
233
- clearInterval(finalPollInterval);
235
+ if (finalPollInterval) clearInterval(finalPollInterval);
234
236
  clearInterval(pollInterval);
235
237
  const finalState = sdk.getState();
236
238
  logger.warn('Max polls reached, forcing final state:', finalState);
@@ -247,17 +249,23 @@ export const useBiometricSDK = (): UseBiometricSDKResult => {
247
249
 
248
250
  // If we're already at RESULT, clear intervals immediately
249
251
  if (immediateState.currentStep === SDKStep.RESULT || immediateState.currentStep === SDKStep.ERROR) {
250
- clearInterval(finalPollInterval);
252
+ if (finalPollInterval) clearInterval(finalPollInterval);
251
253
  clearInterval(pollInterval);
252
254
  }
253
255
  }
254
256
 
255
257
  return result;
256
258
  } catch (error) {
259
+ // Clear all intervals on error
257
260
  clearInterval(pollInterval);
261
+ if (finalPollInterval) {
262
+ clearInterval(finalPollInterval);
263
+ }
264
+ console.error('❌ [useBiometricSDK] Validation error:', error);
265
+ logger.error('Validation error, state:', sdk.getState(), error);
266
+
258
267
  if (isMounted.current) {
259
268
  const errorState = sdk.getState();
260
- logger.error('Validation error, state:', errorState, error);
261
269
  setState(errorState);
262
270
  }
263
271
  throw error;