@hexar/biometric-identity-sdk-react-native 1.0.16 → 1.0.18

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,CA8VtE,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,49 +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)(() => {
73
+ console.log('🔵 [BiometricIdentityFlow] useEffect triggered - currentStep:', state.currentStep, 'hasResult:', !!state.validationResult, 'hasCalled:', hasCalledValidationComplete.current);
83
74
  if (state.currentStep === biometric_identity_sdk_core_1.SDKStep.RESULT && state.validationResult && !hasCalledValidationComplete.current) {
75
+ console.log('🟢 [BiometricIdentityFlow] Calling onValidationComplete callback');
84
76
  biometric_identity_sdk_core_1.logger.info('Validation completed, calling onValidationComplete callback');
85
77
  hasCalledValidationComplete.current = true;
86
- // Use ref to avoid dependency issues
87
78
  onValidationCompleteRef.current(state.validationResult);
88
79
  }
89
80
  }, [state.currentStep, state.validationResult]);
90
- // Reset the flag when validation result changes or component resets
91
81
  (0, react_1.useEffect)(() => {
92
82
  if (state.currentStep !== biometric_identity_sdk_core_1.SDKStep.RESULT) {
93
83
  hasCalledValidationComplete.current = false;
94
84
  }
95
85
  }, [state.currentStep]);
96
- // Handle error
97
86
  (0, react_1.useEffect)(() => {
98
87
  if (state.error) {
99
88
  biometric_identity_sdk_core_1.logger.error('SDK error detected:', state.error);
100
- // Use ref to avoid dependency issues
101
89
  onErrorRef.current(state.error);
102
90
  }
103
91
  }, [state.error]);
@@ -106,7 +94,6 @@ const BiometricIdentityFlow = ({ onValidationComplete, onError, theme, language,
106
94
  */
107
95
  const handleCaptureStart = (0, react_1.useCallback)(async (mode) => {
108
96
  setCameraMode(mode);
109
- // If video mode, fetch challenges first
110
97
  if (mode === 'video' && smartLivenessMode && isUsingBackend) {
111
98
  setIsLoadingChallenges(true);
112
99
  try {
@@ -120,7 +107,6 @@ const BiometricIdentityFlow = ({ onValidationComplete, onError, theme, language,
120
107
  setIsLoadingChallenges(false);
121
108
  }
122
109
  else if (mode === 'video' && smartLivenessMode) {
123
- // Use default challenges for offline mode
124
110
  setCurrentChallenges(sdk.getDefaultChallenges());
125
111
  }
126
112
  setShowCamera(true);
@@ -147,29 +133,55 @@ const BiometricIdentityFlow = ({ onValidationComplete, onError, theme, language,
147
133
  challengesCompleted: videoResult.challengesCompleted,
148
134
  sessionId: videoResult.sessionId,
149
135
  });
136
+ console.log('🔵 [BiometricIdentityFlow] Video stored, starting validation...');
150
137
  biometric_identity_sdk_core_1.logger.info('Starting validation...');
151
- const result = await validateIdentity();
152
- biometric_identity_sdk_core_1.logger.info('Validation completed successfully:', result);
153
- // Ensure state is synced after validation
154
- // 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
+ }
155
149
  const finalState = sdk.getState();
150
+ console.log('🔵 [BiometricIdentityFlow] Final state after validation:', JSON.stringify({
151
+ currentStep: finalState.currentStep,
152
+ hasResult: !!finalState.validationResult,
153
+ progress: finalState.progress,
154
+ isLoading: finalState.isLoading
155
+ }));
156
156
  biometric_identity_sdk_core_1.logger.info('Final state after validation:', finalState);
157
- // 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
+ }
158
163
  if (finalState.currentStep === biometric_identity_sdk_core_1.SDKStep.RESULT && finalState.validationResult && !hasCalledValidationComplete.current) {
159
- 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');
160
166
  hasCalledValidationComplete.current = true;
161
167
  onValidationCompleteRef.current(finalState.validationResult);
162
168
  }
163
169
  }
164
170
  }
165
171
  catch (error) {
172
+ console.error('❌ [BiometricIdentityFlow] Capture/validation error:', error);
166
173
  biometric_identity_sdk_core_1.logger.error('Capture/validation error:', error);
167
- const biometricError = {
168
- name: 'BiometricError',
169
- message: error instanceof Error ? error.message : 'Unknown error during capture',
170
- code: 'LIVENESS_CHECK_FAILED',
171
- };
172
- 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
+ }
173
185
  }
174
186
  }, [cameraMode, uploadFrontID, uploadBackID, storeVideoRecording, validateIdentity, sdk]);
175
187
  /**
@@ -181,18 +193,15 @@ const BiometricIdentityFlow = ({ onValidationComplete, onError, theme, language,
181
193
  setCurrentChallenges([]);
182
194
  setShowInstructions(true);
183
195
  }, [reset]);
184
- // Show loading while initializing
185
196
  if (!isInitialized) {
186
197
  return (react_1.default.createElement(react_native_1.SafeAreaView, { style: [styles.container, customStyles?.container] },
187
198
  react_1.default.createElement(react_native_1.View, { style: styles.loadingFullScreen },
188
199
  react_1.default.createElement(react_native_1.ActivityIndicator, { size: "large", color: theme?.primaryColor || '#6366F1' }),
189
200
  react_1.default.createElement(react_native_1.Text, { style: styles.loadingText }, strings.initialization.initializing))));
190
201
  }
191
- // Show instructions on first load
192
202
  if (showInstructions) {
193
203
  return (react_1.default.createElement(InstructionsScreen_1.InstructionsScreen, { theme: theme, language: language, onStart: () => setShowInstructions(false), routeBack: routeBack, styles: customStyles }));
194
204
  }
195
- // Show camera/video recorder
196
205
  if (showCamera) {
197
206
  if (cameraMode === 'video') {
198
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 () => {
@@ -202,23 +211,16 @@ const BiometricIdentityFlow = ({ onValidationComplete, onError, theme, language,
202
211
  }
203
212
  return (react_1.default.createElement(CameraCapture_1.CameraCapture, { mode: cameraMode, theme: theme, language: language, onCapture: handleCaptureComplete, onCancel: () => setShowCamera(false) }));
204
213
  }
205
- // Show result (check this first, even if step is still VALIDATING but result exists)
206
- // This handles edge cases where state transition might be delayed
207
214
  if (state.validationResult && (state.currentStep === biometric_identity_sdk_core_1.SDKStep.RESULT || state.progress >= 100)) {
208
- // If we have a result but step is still VALIDATING, force transition to RESULT
209
215
  if (state.currentStep === biometric_identity_sdk_core_1.SDKStep.VALIDATING && state.progress >= 100) {
210
216
  biometric_identity_sdk_core_1.logger.info('Result available but step still VALIDATING, showing ResultScreen');
211
217
  }
212
218
  return (react_1.default.createElement(ResultScreen_1.ResultScreen, { result: state.validationResult, theme: theme, language: language, onClose: () => {
213
- // Callback already called automatically when RESULT step was reached
214
- // This onClose is just for UI cleanup/navigation
215
219
  } }));
216
220
  }
217
- // Show validation progress
218
221
  if (state.currentStep === biometric_identity_sdk_core_1.SDKStep.VALIDATING) {
219
222
  return (react_1.default.createElement(ValidationProgress_1.ValidationProgress, { progress: state.progress, theme: theme, language: language }));
220
223
  }
221
- // Show error
222
224
  if (state.currentStep === biometric_identity_sdk_core_1.SDKStep.ERROR && state.error) {
223
225
  return (react_1.default.createElement(ErrorScreen_1.ErrorScreen, { error: state.error, theme: theme, language: language, onRetry: handleRetry, onClose: () => onError(state.error) }));
224
226
  }
@@ -323,7 +325,6 @@ const buttonStyles = react_native_1.StyleSheet.create({
323
325
  marginTop: 4,
324
326
  },
325
327
  });
326
- // Create styles based on theme
327
328
  const createStyles = (theme) => {
328
329
  const backgroundColor = theme?.backgroundColor || '#FFFFFF';
329
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,qBAgQlC,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"}
@@ -137,6 +137,7 @@ const useBiometricSDK = () => {
137
137
  * Validate identity with all collected data
138
138
  */
139
139
  const validateIdentity = (0, react_1.useCallback)(async () => {
140
+ console.log('🔵 [useBiometricSDK] validateIdentity called');
140
141
  biometric_identity_sdk_core_1.logger.info('validateIdentity called, current state:', sdk.getState());
141
142
  setState(sdk.getState());
142
143
  const pollInterval = setInterval(() => {
@@ -145,13 +146,16 @@ const useBiometricSDK = () => {
145
146
  setState(currentState);
146
147
  }
147
148
  }, 200);
149
+ let finalPollInterval = null;
148
150
  try {
151
+ console.log('🔵 [useBiometricSDK] Calling sdk.validateIdentity()...');
149
152
  const result = await sdk.validateIdentity();
153
+ console.log('🟢 [useBiometricSDK] sdk.validateIdentity() completed, result:', result);
150
154
  // Keep polling for a bit to ensure we capture the RESULT state transition
151
155
  // The SDK updates state synchronously, but we want to make sure React sees it
152
156
  let pollCount = 0;
153
157
  const maxPolls = 10; // Poll for up to 2 seconds (10 * 200ms)
154
- const finalPollInterval = setInterval(() => {
158
+ finalPollInterval = setInterval(() => {
155
159
  pollCount++;
156
160
  if (isMounted.current) {
157
161
  const currentState = sdk.getState();
@@ -159,13 +163,16 @@ const useBiometricSDK = () => {
159
163
  setState(currentState);
160
164
  // Stop polling once we reach RESULT or ERROR state
161
165
  if (currentState.currentStep === biometric_identity_sdk_core_1.SDKStep.RESULT || currentState.currentStep === biometric_identity_sdk_core_1.SDKStep.ERROR) {
162
- clearInterval(finalPollInterval);
166
+ console.log('🟢 [useBiometricSDK] Reached final state:', currentState.currentStep);
167
+ if (finalPollInterval)
168
+ clearInterval(finalPollInterval);
163
169
  clearInterval(pollInterval);
164
170
  biometric_identity_sdk_core_1.logger.info('Reached final state, stopped polling:', currentState);
165
171
  }
166
172
  else if (pollCount >= maxPolls) {
167
173
  // Force stop after max polls
168
- clearInterval(finalPollInterval);
174
+ if (finalPollInterval)
175
+ clearInterval(finalPollInterval);
169
176
  clearInterval(pollInterval);
170
177
  const finalState = sdk.getState();
171
178
  biometric_identity_sdk_core_1.logger.warn('Max polls reached, forcing final state:', finalState);
@@ -180,17 +187,23 @@ const useBiometricSDK = () => {
180
187
  setState(immediateState);
181
188
  // If we're already at RESULT, clear intervals immediately
182
189
  if (immediateState.currentStep === biometric_identity_sdk_core_1.SDKStep.RESULT || immediateState.currentStep === biometric_identity_sdk_core_1.SDKStep.ERROR) {
183
- clearInterval(finalPollInterval);
190
+ if (finalPollInterval)
191
+ clearInterval(finalPollInterval);
184
192
  clearInterval(pollInterval);
185
193
  }
186
194
  }
187
195
  return result;
188
196
  }
189
197
  catch (error) {
198
+ // Clear all intervals on error
190
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);
191
205
  if (isMounted.current) {
192
206
  const errorState = sdk.getState();
193
- biometric_identity_sdk_core_1.logger.error('Validation error, state:', errorState, error);
194
207
  setState(errorState);
195
208
  }
196
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.16",
3
+ "version": "1.0.18",
4
4
  "description": "React Native wrapper for Biometric Identity SDK",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -77,55 +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(() => {
99
+ console.log('🔵 [BiometricIdentityFlow] useEffect triggered - currentStep:', state.currentStep, 'hasResult:', !!state.validationResult, 'hasCalled:', hasCalledValidationComplete.current);
109
100
  if (state.currentStep === SDKStep.RESULT && state.validationResult && !hasCalledValidationComplete.current) {
101
+ console.log('🟢 [BiometricIdentityFlow] Calling onValidationComplete callback');
110
102
  logger.info('Validation completed, calling onValidationComplete callback');
111
103
  hasCalledValidationComplete.current = true;
112
- // Use ref to avoid dependency issues
113
104
  onValidationCompleteRef.current(state.validationResult);
114
105
  }
115
106
  }, [state.currentStep, state.validationResult]);
116
107
 
117
- // Reset the flag when validation result changes or component resets
118
108
  useEffect(() => {
119
109
  if (state.currentStep !== SDKStep.RESULT) {
120
110
  hasCalledValidationComplete.current = false;
121
111
  }
122
112
  }, [state.currentStep]);
123
113
 
124
- // Handle error
125
114
  useEffect(() => {
126
115
  if (state.error) {
127
116
  logger.error('SDK error detected:', state.error);
128
- // Use ref to avoid dependency issues
129
117
  onErrorRef.current(state.error);
130
118
  }
131
119
  }, [state.error]);
@@ -136,7 +124,6 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
136
124
  const handleCaptureStart = useCallback(async (mode: 'front' | 'back' | 'video') => {
137
125
  setCameraMode(mode);
138
126
 
139
- // If video mode, fetch challenges first
140
127
  if (mode === 'video' && smartLivenessMode && isUsingBackend) {
141
128
  setIsLoadingChallenges(true);
142
129
  try {
@@ -148,7 +135,6 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
148
135
  }
149
136
  setIsLoadingChallenges(false);
150
137
  } else if (mode === 'video' && smartLivenessMode) {
151
- // Use default challenges for offline mode
152
138
  setCurrentChallenges(sdk.getDefaultChallenges());
153
139
  }
154
140
 
@@ -178,30 +164,57 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
178
164
  sessionId: videoResult.sessionId,
179
165
  });
180
166
 
167
+ console.log('🔵 [BiometricIdentityFlow] Video stored, starting validation...');
181
168
  logger.info('Starting validation...');
182
- const result = await validateIdentity();
183
- logger.info('Validation completed successfully:', result);
184
169
 
185
- // Ensure state is synced after validation
186
- // 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
+
187
181
  const finalState = sdk.getState();
182
+ console.log('🔵 [BiometricIdentityFlow] Final state after validation:', JSON.stringify({
183
+ currentStep: finalState.currentStep,
184
+ hasResult: !!finalState.validationResult,
185
+ progress: finalState.progress,
186
+ isLoading: finalState.isLoading
187
+ }));
188
188
  logger.info('Final state after validation:', finalState);
189
189
 
190
- // 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
+
191
197
  if (finalState.currentStep === SDKStep.RESULT && finalState.validationResult && !hasCalledValidationComplete.current) {
192
- 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');
193
200
  hasCalledValidationComplete.current = true;
194
201
  onValidationCompleteRef.current(finalState.validationResult);
195
202
  }
196
203
  }
197
204
  } catch (error) {
205
+ console.error('❌ [BiometricIdentityFlow] Capture/validation error:', error);
198
206
  logger.error('Capture/validation error:', error);
199
- const biometricError: BiometricError = {
200
- name: 'BiometricError',
201
- message: error instanceof Error ? error.message : 'Unknown error during capture',
202
- code: 'LIVENESS_CHECK_FAILED',
203
- } as BiometricError;
204
- 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
+ }
205
218
  }
206
219
  }, [cameraMode, uploadFrontID, uploadBackID, storeVideoRecording, validateIdentity, sdk]);
207
220
 
@@ -215,7 +228,6 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
215
228
  setShowInstructions(true);
216
229
  }, [reset]);
217
230
 
218
- // Show loading while initializing
219
231
  if (!isInitialized) {
220
232
  return (
221
233
  <SafeAreaView style={[styles.container, customStyles?.container]}>
@@ -227,7 +239,6 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
227
239
  );
228
240
  }
229
241
 
230
- // Show instructions on first load
231
242
  if (showInstructions) {
232
243
  return (
233
244
  <InstructionsScreen
@@ -240,7 +251,6 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
240
251
  );
241
252
  }
242
253
 
243
- // Show camera/video recorder
244
254
  if (showCamera) {
245
255
  if (cameraMode === 'video') {
246
256
  return (
@@ -271,10 +281,7 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
271
281
  );
272
282
  }
273
283
 
274
- // Show result (check this first, even if step is still VALIDATING but result exists)
275
- // This handles edge cases where state transition might be delayed
276
284
  if (state.validationResult && (state.currentStep === SDKStep.RESULT || state.progress >= 100)) {
277
- // If we have a result but step is still VALIDATING, force transition to RESULT
278
285
  if (state.currentStep === SDKStep.VALIDATING && state.progress >= 100) {
279
286
  logger.info('Result available but step still VALIDATING, showing ResultScreen');
280
287
  }
@@ -284,14 +291,11 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
284
291
  theme={theme}
285
292
  language={language}
286
293
  onClose={() => {
287
- // Callback already called automatically when RESULT step was reached
288
- // This onClose is just for UI cleanup/navigation
289
294
  }}
290
295
  />
291
296
  );
292
297
  }
293
298
 
294
- // Show validation progress
295
299
  if (state.currentStep === SDKStep.VALIDATING) {
296
300
  return (
297
301
  <ValidationProgress
@@ -302,7 +306,6 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
302
306
  );
303
307
  }
304
308
 
305
- // Show error
306
309
  if (state.currentStep === SDKStep.ERROR && state.error) {
307
310
  return (
308
311
  <ErrorScreen
@@ -399,7 +402,6 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
399
402
  );
400
403
  };
401
404
 
402
- // Step Indicator Component
403
405
  interface StepIndicatorProps {
404
406
  step: number;
405
407
  active: boolean;
@@ -467,7 +469,6 @@ const stepStyles = StyleSheet.create({
467
469
  },
468
470
  });
469
471
 
470
- // Action Button Component
471
472
  interface ActionButtonProps {
472
473
  title: string;
473
474
  subtitle?: string;
@@ -531,7 +532,6 @@ const buttonStyles = StyleSheet.create({
531
532
  },
532
533
  });
533
534
 
534
- // Create styles based on theme
535
535
  const createStyles = (theme?: ThemeConfig) => {
536
536
  const backgroundColor = theme?.backgroundColor || '#FFFFFF';
537
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
 
@@ -194,6 +194,7 @@ export const useBiometricSDK = (): UseBiometricSDKResult => {
194
194
  * Validate identity with all collected data
195
195
  */
196
196
  const validateIdentity = useCallback(async () => {
197
+ console.log('🔵 [useBiometricSDK] validateIdentity called');
197
198
  logger.info('validateIdentity called, current state:', sdk.getState());
198
199
  setState(sdk.getState());
199
200
 
@@ -204,15 +205,19 @@ export const useBiometricSDK = (): UseBiometricSDKResult => {
204
205
  }
205
206
  }, 200);
206
207
 
208
+ let finalPollInterval: NodeJS.Timeout | null = null;
209
+
207
210
  try {
211
+ console.log('🔵 [useBiometricSDK] Calling sdk.validateIdentity()...');
208
212
  const result = await sdk.validateIdentity();
213
+ console.log('🟢 [useBiometricSDK] sdk.validateIdentity() completed, result:', result);
209
214
 
210
215
  // Keep polling for a bit to ensure we capture the RESULT state transition
211
216
  // The SDK updates state synchronously, but we want to make sure React sees it
212
217
  let pollCount = 0;
213
218
  const maxPolls = 10; // Poll for up to 2 seconds (10 * 200ms)
214
219
 
215
- const finalPollInterval = setInterval(() => {
220
+ finalPollInterval = setInterval(() => {
216
221
  pollCount++;
217
222
  if (isMounted.current) {
218
223
  const currentState = sdk.getState();
@@ -221,12 +226,13 @@ export const useBiometricSDK = (): UseBiometricSDKResult => {
221
226
 
222
227
  // Stop polling once we reach RESULT or ERROR state
223
228
  if (currentState.currentStep === SDKStep.RESULT || currentState.currentStep === SDKStep.ERROR) {
224
- clearInterval(finalPollInterval);
229
+ console.log('🟢 [useBiometricSDK] Reached final state:', currentState.currentStep);
230
+ if (finalPollInterval) clearInterval(finalPollInterval);
225
231
  clearInterval(pollInterval);
226
232
  logger.info('Reached final state, stopped polling:', currentState);
227
233
  } else if (pollCount >= maxPolls) {
228
234
  // Force stop after max polls
229
- clearInterval(finalPollInterval);
235
+ if (finalPollInterval) clearInterval(finalPollInterval);
230
236
  clearInterval(pollInterval);
231
237
  const finalState = sdk.getState();
232
238
  logger.warn('Max polls reached, forcing final state:', finalState);
@@ -243,17 +249,23 @@ export const useBiometricSDK = (): UseBiometricSDKResult => {
243
249
 
244
250
  // If we're already at RESULT, clear intervals immediately
245
251
  if (immediateState.currentStep === SDKStep.RESULT || immediateState.currentStep === SDKStep.ERROR) {
246
- clearInterval(finalPollInterval);
252
+ if (finalPollInterval) clearInterval(finalPollInterval);
247
253
  clearInterval(pollInterval);
248
254
  }
249
255
  }
250
256
 
251
257
  return result;
252
258
  } catch (error) {
259
+ // Clear all intervals on error
253
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
+
254
267
  if (isMounted.current) {
255
268
  const errorState = sdk.getState();
256
- logger.error('Validation error, state:', errorState, error);
257
269
  setState(errorState);
258
270
  }
259
271
  throw error;