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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,6 +12,7 @@ export interface BiometricIdentityFlowProps {
12
12
  language?: SupportedLanguage;
13
13
  customTranslations?: Record<string, string>;
14
14
  smartLivenessMode?: boolean;
15
+ routeBack?: () => void;
15
16
  styles?: {
16
17
  container?: ViewStyle;
17
18
  content?: ViewStyle;
@@ -1 +1 @@
1
- {"version":3,"file":"BiometricIdentityFlow.d.ts","sourceRoot":"","sources":["../../src/components/BiometricIdentityFlow.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAA2C,MAAM,OAAO,CAAC;AAChE,OAAO,EAOL,SAAS,EACV,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,gBAAgB,EAChB,WAAW,EACX,cAAc,EAKd,iBAAiB,EAClB,MAAM,oCAAoC,CAAC;AAU5C,MAAM,WAAW,0BAA0B;IACzC,oBAAoB,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,IAAI,CAAC;IACzD,OAAO,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC;IACzC,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,kBAAkB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5C,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,MAAM,CAAC,EAAE;QACP,SAAS,CAAC,EAAE,SAAS,CAAC;QACtB,OAAO,CAAC,EAAE,SAAS,CAAC;KACrB,CAAC;CACH;AAED,eAAO,MAAM,qBAAqB,EAAE,KAAK,CAAC,EAAE,CAAC,0BAA0B,CAoTtE,CAAC;AAiOF,eAAe,qBAAqB,CAAC"}
1
+ {"version":3,"file":"BiometricIdentityFlow.d.ts","sourceRoot":"","sources":["../../src/components/BiometricIdentityFlow.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAA2C,MAAM,OAAO,CAAC;AAChE,OAAO,EAOL,SAAS,EACV,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,gBAAgB,EAChB,WAAW,EACX,cAAc,EAKd,iBAAiB,EAClB,MAAM,oCAAoC,CAAC;AAU5C,MAAM,WAAW,0BAA0B;IACzC,oBAAoB,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,IAAI,CAAC;IACzD,OAAO,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC;IACzC,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,kBAAkB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5C,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,MAAM,CAAC,EAAE;QACP,SAAS,CAAC,EAAE,SAAS,CAAC;QACtB,OAAO,CAAC,EAAE,SAAS,CAAC;KACrB,CAAC;CACH;AAED,eAAO,MAAM,qBAAqB,EAAE,KAAK,CAAC,EAAE,CAAC,0BAA0B,CAiUtE,CAAC;AAiOF,eAAe,qBAAqB,CAAC"}
@@ -48,7 +48,7 @@ const ValidationProgress_1 = require("./ValidationProgress");
48
48
  const ResultScreen_1 = require("./ResultScreen");
49
49
  const ErrorScreen_1 = require("./ErrorScreen");
50
50
  const InstructionsScreen_1 = require("./InstructionsScreen");
51
- const BiometricIdentityFlow = ({ onValidationComplete, onError, theme, language, customTranslations, smartLivenessMode = true, styles: customStyles, }) => {
51
+ const BiometricIdentityFlow = ({ onValidationComplete, onError, theme, language, customTranslations, smartLivenessMode = true, routeBack, styles: customStyles, }) => {
52
52
  const { sdk, state, isInitialized, isUsingBackend, challenges, uploadFrontID, uploadBackID, storeVideoRecording, fetchChallenges, validateIdentity, reset, } = (0, useBiometricSDK_1.useBiometricSDK)();
53
53
  const [showCamera, setShowCamera] = (0, react_1.useState)(false);
54
54
  const [cameraMode, setCameraMode] = (0, react_1.useState)('front');
@@ -109,17 +109,28 @@ const BiometricIdentityFlow = ({ onValidationComplete, onError, theme, language,
109
109
  * Handle capture completion
110
110
  */
111
111
  const handleCaptureComplete = (0, react_1.useCallback)(async (data) => {
112
+ console.log('handleCaptureComplete called, cameraMode:', cameraMode);
112
113
  setShowCamera(false);
113
114
  try {
114
115
  if (cameraMode === 'front') {
116
+ console.log('Uploading front ID');
115
117
  await uploadFrontID(data);
118
+ console.log('Front ID uploaded successfully');
116
119
  }
117
120
  else if (cameraMode === 'back') {
121
+ console.log('Uploading back ID');
118
122
  await uploadBackID(data);
123
+ console.log('Back ID uploaded successfully');
119
124
  }
120
125
  else if (cameraMode === 'video') {
126
+ console.log('Processing video recording result');
121
127
  // Handle video recording result
122
128
  const videoResult = data;
129
+ console.log('Storing video recording:', {
130
+ frames: videoResult.frames.length,
131
+ duration: videoResult.duration,
132
+ challengesCompleted: videoResult.challengesCompleted.length
133
+ });
123
134
  await storeVideoRecording({
124
135
  frames: videoResult.frames,
125
136
  duration: videoResult.duration,
@@ -128,8 +139,10 @@ const BiometricIdentityFlow = ({ onValidationComplete, onError, theme, language,
128
139
  challengesCompleted: videoResult.challengesCompleted,
129
140
  sessionId: videoResult.sessionId,
130
141
  });
142
+ console.log('Video recording stored, starting validation...');
131
143
  // Automatically start validation after video
132
- await validateIdentity();
144
+ const result = await validateIdentity();
145
+ console.log('Validation complete, result:', result);
133
146
  }
134
147
  }
135
148
  catch (error) {
@@ -158,11 +171,7 @@ const BiometricIdentityFlow = ({ onValidationComplete, onError, theme, language,
158
171
  }
159
172
  // Show instructions on first load
160
173
  if (showInstructions) {
161
- return (react_1.default.createElement(InstructionsScreen_1.InstructionsScreen, { theme: theme, language: language, onStart: () => setShowInstructions(false), onCancel: onError ? () => onError({
162
- name: 'BiometricError',
163
- message: 'User cancelled',
164
- code: biometric_identity_sdk_core_1.BiometricErrorCode.USER_CANCELLED,
165
- }) : undefined, styles: customStyles }));
174
+ return (react_1.default.createElement(InstructionsScreen_1.InstructionsScreen, { theme: theme, language: language, onStart: () => setShowInstructions(false), routeBack: routeBack, styles: customStyles }));
166
175
  }
167
176
  // Show camera/video recorder
168
177
  if (showCamera) {
@@ -176,10 +185,12 @@ const BiometricIdentityFlow = ({ onValidationComplete, onError, theme, language,
176
185
  }
177
186
  // Show validation progress
178
187
  if (state.currentStep === biometric_identity_sdk_core_1.SDKStep.VALIDATING) {
188
+ console.log('Rendering ValidationProgress, progress:', state.progress);
179
189
  return (react_1.default.createElement(ValidationProgress_1.ValidationProgress, { progress: state.progress, theme: theme, language: language }));
180
190
  }
181
191
  // Show result
182
192
  if (state.currentStep === biometric_identity_sdk_core_1.SDKStep.RESULT && state.validationResult) {
193
+ console.log('Rendering ResultScreen, result:', state.validationResult);
183
194
  return (react_1.default.createElement(ResultScreen_1.ResultScreen, { result: state.validationResult, theme: theme, language: language, onClose: () => onValidationComplete(state.validationResult) }));
184
195
  }
185
196
  // Show error
@@ -9,7 +9,7 @@ export interface InstructionsScreenProps {
9
9
  theme?: ThemeConfig;
10
10
  language?: SupportedLanguage;
11
11
  onStart: () => void;
12
- onCancel?: () => void;
12
+ routeBack?: () => void;
13
13
  styles?: {
14
14
  container?: ViewStyle;
15
15
  content?: ViewStyle;
@@ -1 +1 @@
1
- {"version":3,"file":"InstructionsScreen.d.ts","sourceRoot":"","sources":["../../src/components/InstructionsScreen.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAA8B,MAAM,OAAO,CAAC;AACnD,OAAO,EAML,SAAS,EACV,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAA2B,MAAM,oCAAoC,CAAC;AAE7G,MAAM,WAAW,uBAAuB;IACtC,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,MAAM,CAAC,EAAE;QACP,SAAS,CAAC,EAAE,SAAS,CAAC;QACtB,OAAO,CAAC,EAAE,SAAS,CAAC;KACrB,CAAC;CACH;AAED,eAAO,MAAM,kBAAkB,EAAE,KAAK,CAAC,EAAE,CAAC,uBAAuB,CA4GhE,CAAC;AAiMF,eAAe,kBAAkB,CAAC"}
1
+ {"version":3,"file":"InstructionsScreen.d.ts","sourceRoot":"","sources":["../../src/components/InstructionsScreen.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAA8B,MAAM,OAAO,CAAC;AACnD,OAAO,EAML,SAAS,EACV,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAA2B,MAAM,oCAAoC,CAAC;AAE7G,MAAM,WAAW,uBAAuB;IACtC,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,MAAM,CAAC,EAAE;QACP,SAAS,CAAC,EAAE,SAAS,CAAC;QACtB,OAAO,CAAC,EAAE,SAAS,CAAC;KACrB,CAAC;CACH;AAED,eAAO,MAAM,kBAAkB,EAAE,KAAK,CAAC,EAAE,CAAC,uBAAuB,CAyGhE,CAAC;AAuMF,eAAe,kBAAkB,CAAC"}
@@ -41,7 +41,7 @@ exports.InstructionsScreen = void 0;
41
41
  const react_1 = __importStar(require("react"));
42
42
  const react_native_1 = require("react-native");
43
43
  const biometric_identity_sdk_core_1 = require("@hexar/biometric-identity-sdk-core");
44
- const InstructionsScreen = ({ theme, language = 'en', onStart, onCancel, styles: customStyles, }) => {
44
+ const InstructionsScreen = ({ theme, language = 'en', onStart, routeBack, styles: customStyles, }) => {
45
45
  const [strings, setStrings] = (0, react_1.useState)(() => {
46
46
  // Set initial language
47
47
  if (language) {
@@ -61,6 +61,9 @@ const InstructionsScreen = ({ theme, language = 'en', onStart, onCancel, styles:
61
61
  const tipsContent = getTipsContent(language);
62
62
  const privacyContent = getPrivacyContent(language);
63
63
  return (react_1.default.createElement(react_native_1.View, { style: [styles.container, customStyles?.container] },
64
+ routeBack && (react_1.default.createElement(react_native_1.View, { style: styles.header },
65
+ react_1.default.createElement(react_native_1.TouchableOpacity, { style: styles.backButton, onPress: routeBack },
66
+ react_1.default.createElement(react_native_1.Text, { style: [styles.backButtonText, { color: theme?.textColor || '#000000' }] }, strings.common.back || 'Volver')))),
64
67
  react_1.default.createElement(react_native_1.ScrollView, { contentContainerStyle: [styles.content, customStyles?.content] },
65
68
  react_1.default.createElement(react_native_1.Text, { style: [styles.title, { color: theme?.textColor || '#000000' }] }, strings.instructions.title),
66
69
  react_1.default.createElement(react_native_1.Text, { style: [styles.subtitle, { color: theme?.secondaryTextColor || '#6B7280' }] }, strings.instructions.subtitle),
@@ -76,16 +79,9 @@ const InstructionsScreen = ({ theme, language = 'en', onStart, onCancel, styles:
76
79
  react_1.default.createElement(react_native_1.View, { style: styles.privacyContainer },
77
80
  react_1.default.createElement(react_native_1.Text, { style: styles.privacyText }, privacyContent))),
78
81
  react_1.default.createElement(react_native_1.View, { style: styles.footer },
79
- onCancel && (react_1.default.createElement(react_native_1.TouchableOpacity, { style: [
80
- styles.button,
81
- styles.cancelButton,
82
- { borderColor: theme?.errorColor || '#EF4444' },
83
- ], onPress: onCancel },
84
- react_1.default.createElement(react_native_1.Text, { style: [styles.buttonText, { color: theme?.errorColor || '#EF4444' }] }, strings.common.cancel || 'Cancel'))),
85
82
  react_1.default.createElement(react_native_1.TouchableOpacity, { style: [
86
83
  styles.button,
87
84
  { backgroundColor: theme?.primaryColor || '#6366F1' },
88
- onCancel && styles.startButton,
89
85
  ], onPress: onStart },
90
86
  react_1.default.createElement(react_native_1.Text, { style: styles.buttonText }, strings.instructions.startButton)))));
91
87
  };
@@ -135,6 +131,22 @@ const styles = react_native_1.StyleSheet.create({
135
131
  flex: 1,
136
132
  backgroundColor: '#FFFFFF',
137
133
  },
134
+ header: {
135
+ paddingTop: 16,
136
+ paddingHorizontal: 16,
137
+ paddingBottom: 8,
138
+ borderBottomWidth: 1,
139
+ borderBottomColor: '#E5E7EB',
140
+ },
141
+ backButton: {
142
+ paddingVertical: 8,
143
+ paddingHorizontal: 12,
144
+ alignSelf: 'flex-start',
145
+ },
146
+ backButtonText: {
147
+ fontSize: 16,
148
+ fontWeight: '600',
149
+ },
138
150
  content: {
139
151
  padding: 24,
140
152
  },
@@ -182,22 +194,12 @@ const styles = react_native_1.StyleSheet.create({
182
194
  padding: 24,
183
195
  borderTopWidth: 1,
184
196
  borderTopColor: '#E5E7EB',
185
- flexDirection: 'row',
186
- gap: 12,
187
197
  },
188
198
  button: {
189
199
  paddingVertical: 16,
190
200
  paddingHorizontal: 32,
191
201
  borderRadius: 8,
192
202
  alignItems: 'center',
193
- flex: 1,
194
- },
195
- cancelButton: {
196
- borderWidth: 2,
197
- backgroundColor: 'transparent',
198
- },
199
- startButton: {
200
- flex: 1,
201
203
  },
202
204
  buttonText: {
203
205
  color: '#FFFFFF',
@@ -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,EAA2B,MAAM,oCAAoC,CAAC;AAGlI,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,sEAAsE;IACtE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,sCAAsC;IACtC,YAAY,CAAC,EAAE,mBAAmB,EAAE,CAAC;IACrC,0CAA0C;IAC1C,UAAU,CAAC,EAAE,eAAe,EAAE,CAAC;IAC/B,wCAAwC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kDAAkD;IAClD,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,wCAAwC;IACxC,UAAU,EAAE,CAAC,SAAS,EAAE,oBAAoB,KAAK,IAAI,CAAC;IACtD,iCAAiC;IACjC,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,gDAAgD;IAChD,iBAAiB,CAAC,EAAE,MAAM,OAAO,CAAC,eAAe,EAAE,CAAC,CAAC;CACtD;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,oBAAoB,EAAE,OAAO,CAAC;IAC9B,YAAY,EAAE,MAAM,CAAC;IACrB,mBAAmB,EAAE,MAAM,EAAE,CAAC;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AA+CD,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CAmsBtD,CAAC;AA4OF,eAAe,aAAa,CAAC"}
1
+ {"version":3,"file":"VideoRecorder.d.ts","sourceRoot":"","sources":["../../src/components/VideoRecorder.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAmD,MAAM,OAAO,CAAC;AAaxE,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAE,iBAAiB,EAA2B,MAAM,oCAAoC,CAAC;AAGlI,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,sEAAsE;IACtE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,sCAAsC;IACtC,YAAY,CAAC,EAAE,mBAAmB,EAAE,CAAC;IACrC,0CAA0C;IAC1C,UAAU,CAAC,EAAE,eAAe,EAAE,CAAC;IAC/B,wCAAwC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kDAAkD;IAClD,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,wCAAwC;IACxC,UAAU,EAAE,CAAC,SAAS,EAAE,oBAAoB,KAAK,IAAI,CAAC;IACtD,iCAAiC;IACjC,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,gDAAgD;IAChD,iBAAiB,CAAC,EAAE,MAAM,OAAO,CAAC,eAAe,EAAE,CAAC,CAAC;CACtD;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,oBAAoB,EAAE,OAAO,CAAC;IAC9B,YAAY,EAAE,MAAM,CAAC;IACrB,mBAAmB,EAAE,MAAM,EAAE,CAAC;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AA+CD,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CA8vBtD,CAAC;AA4OF,eAAe,aAAa,CAAC"}
@@ -275,12 +275,18 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
275
275
  }, [onCancel]);
276
276
  const handleVideoComplete = (0, react_1.useCallback)(async (video) => {
277
277
  console.log('handleVideoComplete called with video:', video?.path);
278
+ if (!isRecordingRef.current && phase !== 'processing') {
279
+ console.log('Video already processed, ignoring duplicate callback');
280
+ return;
281
+ }
278
282
  try {
279
283
  setPhase('processing');
284
+ setOverallProgress(100);
280
285
  const actualDuration = Date.now() - recordingStartTime.current;
281
- console.log('Video duration:', actualDuration, 'min required:', minDurationMs);
286
+ console.log('Video Processing - Duration:', (actualDuration / 1000).toFixed(1), 's, Min required:', (minDurationMs / 1000).toFixed(1), 's, Frames:', frames.length);
282
287
  if (actualDuration < minDurationMs) {
283
288
  setPhase('recording');
289
+ setOverallProgress(0);
284
290
  react_native_1.Alert.alert(strings.errors.videoTooShort?.title || 'Recording Too Short', strings.errors.videoTooShort?.message || `Video must be at least ${minDurationMs / 1000} seconds. Please try again.`, [{ text: strings.common.retry || 'OK', onPress: resetAndRetry }]);
285
291
  return;
286
292
  }
@@ -289,36 +295,47 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
289
295
  try {
290
296
  const RNFS = require('react-native-fs');
291
297
  videoBase64 = await RNFS.readFile(video.path, 'base64');
292
- console.log('Video file read successfully, size:', videoBase64.length);
298
+ console.log('Video file read successfully, size:', videoBase64.length, 'bytes');
293
299
  }
294
300
  catch (fsError) {
295
301
  console.warn('Could not read video file, using captured frames:', fsError);
296
302
  }
297
303
  }
304
+ const finalFrames = frames.length > 0 ? frames : (videoBase64 ? [videoBase64] : []);
305
+ if (finalFrames.length === 0) {
306
+ console.error('No frames available, cannot complete');
307
+ setPhase('recording');
308
+ handleRecordingError(new Error('No video frames captured'));
309
+ return;
310
+ }
298
311
  const result = {
299
- frames: frames.length > 0 ? frames : (videoBase64 ? [videoBase64] : []),
312
+ frames: finalFrames,
300
313
  duration: actualDuration,
301
314
  instructionsFollowed: completedChallenges.length === challenges.length,
302
- qualityScore: frames.length > 0 ? Math.min(100, (frames.length / 30) * 100) : 85,
315
+ qualityScore: finalFrames.length > 0 ? Math.min(100, (finalFrames.length / 30) * 100) : 85,
303
316
  challengesCompleted: completedChallenges,
304
317
  sessionId,
305
318
  };
306
319
  console.log('Video recording completed successfully:', {
307
- duration: actualDuration,
320
+ duration: (actualDuration / 1000).toFixed(1) + 's',
308
321
  frames: result.frames.length,
309
- challengesCompleted: completedChallenges.length,
310
- instructionsFollowed: result.instructionsFollowed
322
+ challenges: `${completedChallenges.length}/${challenges.length}`,
323
+ instructionsFollowed: result.instructionsFollowed,
324
+ quality: result.qualityScore.toFixed(0) + '%'
311
325
  });
326
+ isRecordingRef.current = false;
312
327
  onComplete(result);
313
328
  }
314
329
  catch (error) {
315
330
  console.error('Error processing video:', error);
316
331
  setPhase('recording');
332
+ setOverallProgress(0);
317
333
  handleRecordingError(error);
318
334
  }
319
- }, [frames, completedChallenges, challenges, sessionId, onComplete, resetAndRetry, handleRecordingError, strings, minDurationMs]);
335
+ }, [frames, completedChallenges, challenges, sessionId, onComplete, resetAndRetry, handleRecordingError, strings, minDurationMs, phase]);
320
336
  const startFrameCapture = (0, react_1.useCallback)(() => {
321
337
  if (cameraRef.current && device) {
338
+ console.log('Starting frame capture mode');
322
339
  frameCaptureInterval.current = setInterval(async () => {
323
340
  try {
324
341
  const photo = await cameraRef.current?.takePhoto({
@@ -353,37 +370,75 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
353
370
  }, [device]);
354
371
  const stopRecording = (0, react_1.useCallback)(async () => {
355
372
  console.log('Stopping recording...');
356
- isRecordingRef.current = false;
373
+ if (frameCaptureInterval.current) {
374
+ clearInterval(frameCaptureInterval.current);
375
+ frameCaptureInterval.current = null;
376
+ }
357
377
  if (videoRecordingRef.current) {
358
378
  try {
359
379
  console.log('Stopping video recording');
360
- await videoRecordingRef.current.stop();
361
- console.log('Video recording stopped');
380
+ const recording = videoRecordingRef.current;
381
+ videoRecordingRef.current = null;
382
+ isRecordingRef.current = false;
383
+ await recording.stop();
384
+ console.log('Video recording stopped - waiting for onRecordingFinished callback');
362
385
  }
363
386
  catch (error) {
364
387
  console.error('Error stopping video recording:', error);
388
+ isRecordingRef.current = false;
389
+ const actualDuration = Date.now() - recordingStartTime.current;
390
+ if (actualDuration >= minDurationMs && frames.length > 0) {
391
+ console.log('Video stopped with error, using captured frames');
392
+ const result = {
393
+ frames,
394
+ duration: actualDuration,
395
+ instructionsFollowed: completedChallenges.length === challenges.length,
396
+ qualityScore: Math.min(100, (frames.length / 30) * 100),
397
+ challengesCompleted: completedChallenges,
398
+ sessionId,
399
+ };
400
+ onComplete(result);
401
+ }
365
402
  }
366
- videoRecordingRef.current = null;
367
403
  }
368
- if (frameCaptureInterval.current) {
369
- clearInterval(frameCaptureInterval.current);
370
- frameCaptureInterval.current = null;
404
+ else {
405
+ isRecordingRef.current = false;
371
406
  }
372
- }, []);
407
+ }, [frames, completedChallenges, challenges, sessionId, onComplete, minDurationMs]);
373
408
  const runChallenge = (0, react_1.useCallback)((index) => {
374
409
  if (index >= challenges.length) {
410
+ console.log('All challenges completed, stopping recording');
411
+ setOverallProgress(100);
375
412
  if (isRecordingRef.current) {
376
413
  const elapsed = Date.now() - recordingStartTime.current;
414
+ console.log('Checking duration - Elapsed:', (elapsed / 1000).toFixed(1), 's, Min required:', (minDurationMs / 1000).toFixed(1), 's');
377
415
  if (elapsed < minDurationMs) {
416
+ const remaining = minDurationMs - elapsed;
417
+ console.log('Waiting additional', (remaining / 1000).toFixed(1), 's to meet minimum duration');
378
418
  setTimeout(() => {
379
419
  if (isRecordingRef.current) {
380
420
  stopRecording();
381
421
  }
382
- }, minDurationMs - elapsed);
422
+ }, remaining);
383
423
  return;
384
424
  }
385
425
  stopRecording();
386
426
  }
427
+ else {
428
+ const actualDuration = Date.now() - recordingStartTime.current;
429
+ if (actualDuration >= minDurationMs && frames.length > 0) {
430
+ console.log('Recording already stopped, completing with frames');
431
+ const result = {
432
+ frames,
433
+ duration: actualDuration,
434
+ instructionsFollowed: completedChallenges.length === challenges.length,
435
+ qualityScore: Math.min(100, (frames.length / 30) * 100),
436
+ challengesCompleted: completedChallenges,
437
+ sessionId,
438
+ };
439
+ onComplete(result);
440
+ }
441
+ }
387
442
  return;
388
443
  }
389
444
  const challenge = challenges[index];
@@ -428,16 +483,14 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
428
483
  setPhase('recording');
429
484
  recordingStartTime.current = Date.now();
430
485
  isRecordingRef.current = true;
431
- console.log('Starting video recording, total duration:', totalDuration);
486
+ console.log('Starting video recording, total duration:', (totalDuration / 1000).toFixed(1), 's');
432
487
  if (cameraRef.current && device) {
433
488
  try {
434
489
  videoRecordingRef.current = await cameraRef.current.startRecording({
435
490
  flash: 'off',
436
491
  onRecordingFinished: (video) => {
437
- console.log('Video recording finished callback called', video);
438
- if (isRecordingRef.current) {
439
- handleVideoComplete(video);
440
- }
492
+ console.log('Video recording finished callback called, path:', video?.path || 'N/A');
493
+ handleVideoComplete(video);
441
494
  },
442
495
  onRecordingError: (error) => {
443
496
  console.error('Recording error:', error);
@@ -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,EACZ,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,qBAqMlC,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,EACZ,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,qBA+NlC,CAAC;AAEF,eAAe,eAAe,CAAC"}
@@ -137,11 +137,34 @@ const useBiometricSDK = () => {
137
137
  * Validate identity with all collected data
138
138
  */
139
139
  const validateIdentity = (0, react_1.useCallback)(async () => {
140
- const result = await sdk.validateIdentity();
141
- if (isMounted.current) {
142
- setState(sdk.getState());
140
+ console.log('validateIdentity called, current state:', sdk.getState().currentStep);
141
+ // Update state immediately to show validation screen
142
+ setState(sdk.getState());
143
+ // Poll state during validation to catch intermediate updates
144
+ const pollInterval = setInterval(() => {
145
+ if (isMounted.current) {
146
+ const currentState = sdk.getState();
147
+ setState(currentState);
148
+ console.log('State polled - step:', currentState.currentStep, 'progress:', currentState.progress);
149
+ }
150
+ }, 200);
151
+ try {
152
+ const result = await sdk.validateIdentity();
153
+ clearInterval(pollInterval);
154
+ if (isMounted.current) {
155
+ const finalState = sdk.getState();
156
+ setState(finalState);
157
+ console.log('Validation complete, final state:', finalState.currentStep, 'result:', result);
158
+ }
159
+ return result;
160
+ }
161
+ catch (error) {
162
+ clearInterval(pollInterval);
163
+ if (isMounted.current) {
164
+ setState(sdk.getState());
165
+ }
166
+ throw error;
143
167
  }
144
- return result;
145
168
  }, [sdk]);
146
169
  /**
147
170
  * Reset SDK state
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hexar/biometric-identity-sdk-react-native",
3
- "version": "1.0.8",
3
+ "version": "1.0.10",
4
4
  "description": "React Native wrapper for Biometric Identity SDK",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -39,6 +39,7 @@ export interface BiometricIdentityFlowProps {
39
39
  language?: SupportedLanguage;
40
40
  customTranslations?: Record<string, string>;
41
41
  smartLivenessMode?: boolean;
42
+ routeBack?: () => void;
42
43
  styles?: {
43
44
  container?: ViewStyle;
44
45
  content?: ViewStyle;
@@ -52,6 +53,7 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
52
53
  language,
53
54
  customTranslations,
54
55
  smartLivenessMode = true,
56
+ routeBack,
55
57
  styles: customStyles,
56
58
  }) => {
57
59
  const {
@@ -133,17 +135,29 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
133
135
  * Handle capture completion
134
136
  */
135
137
  const handleCaptureComplete = useCallback(async (data: any) => {
138
+ console.log('handleCaptureComplete called, cameraMode:', cameraMode);
136
139
  setShowCamera(false);
137
140
 
138
141
  try {
139
142
  if (cameraMode === 'front') {
143
+ console.log('Uploading front ID');
140
144
  await uploadFrontID(data);
145
+ console.log('Front ID uploaded successfully');
141
146
  } else if (cameraMode === 'back') {
147
+ console.log('Uploading back ID');
142
148
  await uploadBackID(data);
149
+ console.log('Back ID uploaded successfully');
143
150
  } else if (cameraMode === 'video') {
151
+ console.log('Processing video recording result');
144
152
  // Handle video recording result
145
153
  const videoResult: VideoRecordingResult = data;
146
154
 
155
+ console.log('Storing video recording:', {
156
+ frames: videoResult.frames.length,
157
+ duration: videoResult.duration,
158
+ challengesCompleted: videoResult.challengesCompleted.length
159
+ });
160
+
147
161
  await storeVideoRecording({
148
162
  frames: videoResult.frames,
149
163
  duration: videoResult.duration,
@@ -153,8 +167,10 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
153
167
  sessionId: videoResult.sessionId,
154
168
  });
155
169
 
170
+ console.log('Video recording stored, starting validation...');
156
171
  // Automatically start validation after video
157
- await validateIdentity();
172
+ const result = await validateIdentity();
173
+ console.log('Validation complete, result:', result);
158
174
  }
159
175
  } catch (error) {
160
176
  console.error('Capture error:', error);
@@ -194,11 +210,7 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
194
210
  theme={theme}
195
211
  language={language}
196
212
  onStart={() => setShowInstructions(false)}
197
- onCancel={onError ? () => onError({
198
- name: 'BiometricError',
199
- message: 'User cancelled',
200
- code: BiometricErrorCode.USER_CANCELLED,
201
- } as BiometricError) : undefined}
213
+ routeBack={routeBack}
202
214
  styles={customStyles}
203
215
  />
204
216
  );
@@ -237,6 +249,7 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
237
249
 
238
250
  // Show validation progress
239
251
  if (state.currentStep === SDKStep.VALIDATING) {
252
+ console.log('Rendering ValidationProgress, progress:', state.progress);
240
253
  return (
241
254
  <ValidationProgress
242
255
  progress={state.progress}
@@ -248,6 +261,7 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
248
261
 
249
262
  // Show result
250
263
  if (state.currentStep === SDKStep.RESULT && state.validationResult) {
264
+ console.log('Rendering ResultScreen, result:', state.validationResult);
251
265
  return (
252
266
  <ResultScreen
253
267
  result={state.validationResult}
@@ -18,7 +18,7 @@ export interface InstructionsScreenProps {
18
18
  theme?: ThemeConfig;
19
19
  language?: SupportedLanguage;
20
20
  onStart: () => void;
21
- onCancel?: () => void;
21
+ routeBack?: () => void;
22
22
  styles?: {
23
23
  container?: ViewStyle;
24
24
  content?: ViewStyle;
@@ -29,7 +29,7 @@ export const InstructionsScreen: React.FC<InstructionsScreenProps> = ({
29
29
  theme,
30
30
  language = 'en',
31
31
  onStart,
32
- onCancel,
32
+ routeBack,
33
33
  styles: customStyles,
34
34
  }) => {
35
35
  const [strings, setStrings] = useState(() => {
@@ -55,6 +55,18 @@ export const InstructionsScreen: React.FC<InstructionsScreenProps> = ({
55
55
 
56
56
  return (
57
57
  <View style={[styles.container, customStyles?.container]}>
58
+ {routeBack && (
59
+ <View style={styles.header}>
60
+ <TouchableOpacity
61
+ style={styles.backButton}
62
+ onPress={routeBack}
63
+ >
64
+ <Text style={[styles.backButtonText, { color: theme?.textColor || '#000000' }]}>
65
+ {strings.common.back || 'Volver'}
66
+ </Text>
67
+ </TouchableOpacity>
68
+ </View>
69
+ )}
58
70
  <ScrollView contentContainerStyle={[styles.content, customStyles?.content]}>
59
71
  <Text style={[styles.title, { color: theme?.textColor || '#000000' }]}>
60
72
  {strings.instructions.title}
@@ -104,27 +116,12 @@ export const InstructionsScreen: React.FC<InstructionsScreenProps> = ({
104
116
  </View>
105
117
  </ScrollView>
106
118
 
107
- {/* Footer Buttons */}
119
+ {/* Start Button */}
108
120
  <View style={styles.footer}>
109
- {onCancel && (
110
- <TouchableOpacity
111
- style={[
112
- styles.button,
113
- styles.cancelButton,
114
- { borderColor: theme?.errorColor || '#EF4444' },
115
- ]}
116
- onPress={onCancel}
117
- >
118
- <Text style={[styles.buttonText, { color: theme?.errorColor || '#EF4444' }]}>
119
- {strings.common.cancel || 'Cancel'}
120
- </Text>
121
- </TouchableOpacity>
122
- )}
123
121
  <TouchableOpacity
124
122
  style={[
125
123
  styles.button,
126
124
  { backgroundColor: theme?.primaryColor || '#6366F1' },
127
- onCancel && styles.startButton,
128
125
  ]}
129
126
  onPress={onStart}
130
127
  >
@@ -205,6 +202,22 @@ const styles = StyleSheet.create({
205
202
  flex: 1,
206
203
  backgroundColor: '#FFFFFF',
207
204
  },
205
+ header: {
206
+ paddingTop: 16,
207
+ paddingHorizontal: 16,
208
+ paddingBottom: 8,
209
+ borderBottomWidth: 1,
210
+ borderBottomColor: '#E5E7EB',
211
+ },
212
+ backButton: {
213
+ paddingVertical: 8,
214
+ paddingHorizontal: 12,
215
+ alignSelf: 'flex-start',
216
+ },
217
+ backButtonText: {
218
+ fontSize: 16,
219
+ fontWeight: '600',
220
+ },
208
221
  content: {
209
222
  padding: 24,
210
223
  },
@@ -252,22 +265,12 @@ const styles = StyleSheet.create({
252
265
  padding: 24,
253
266
  borderTopWidth: 1,
254
267
  borderTopColor: '#E5E7EB',
255
- flexDirection: 'row',
256
- gap: 12,
257
268
  },
258
269
  button: {
259
270
  paddingVertical: 16,
260
271
  paddingHorizontal: 32,
261
272
  borderRadius: 8,
262
273
  alignItems: 'center',
263
- flex: 1,
264
- },
265
- cancelButton: {
266
- borderWidth: 2,
267
- backgroundColor: 'transparent',
268
- },
269
- startButton: {
270
- flex: 1,
271
274
  },
272
275
  buttonText: {
273
276
  color: '#FFFFFF',
@@ -330,14 +330,21 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
330
330
  const handleVideoComplete = useCallback(async (video: any) => {
331
331
  console.log('handleVideoComplete called with video:', video?.path);
332
332
 
333
+ if (!isRecordingRef.current && phase !== 'processing') {
334
+ console.log('Video already processed, ignoring duplicate callback');
335
+ return;
336
+ }
337
+
333
338
  try {
334
339
  setPhase('processing');
335
- const actualDuration = Date.now() - recordingStartTime.current;
340
+ setOverallProgress(100);
336
341
 
337
- console.log('Video duration:', actualDuration, 'min required:', minDurationMs);
342
+ const actualDuration = Date.now() - recordingStartTime.current;
343
+ console.log('Video Processing - Duration:', (actualDuration / 1000).toFixed(1), 's, Min required:', (minDurationMs / 1000).toFixed(1), 's, Frames:', frames.length);
338
344
 
339
345
  if (actualDuration < minDurationMs) {
340
346
  setPhase('recording');
347
+ setOverallProgress(0);
341
348
  Alert.alert(
342
349
  strings.errors.videoTooShort?.title || 'Recording Too Short',
343
350
  strings.errors.videoTooShort?.message || `Video must be at least ${minDurationMs / 1000} seconds. Please try again.`,
@@ -351,38 +358,51 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
351
358
  try {
352
359
  const RNFS = require('react-native-fs');
353
360
  videoBase64 = await RNFS.readFile(video.path, 'base64');
354
- console.log('Video file read successfully, size:', videoBase64.length);
361
+ console.log('Video file read successfully, size:', videoBase64.length, 'bytes');
355
362
  } catch (fsError) {
356
363
  console.warn('Could not read video file, using captured frames:', fsError);
357
364
  }
358
365
  }
359
366
 
367
+ const finalFrames = frames.length > 0 ? frames : (videoBase64 ? [videoBase64] : []);
368
+
369
+ if (finalFrames.length === 0) {
370
+ console.error('No frames available, cannot complete');
371
+ setPhase('recording');
372
+ handleRecordingError(new Error('No video frames captured'));
373
+ return;
374
+ }
375
+
360
376
  const result: VideoRecordingResult = {
361
- frames: frames.length > 0 ? frames : (videoBase64 ? [videoBase64] : []),
377
+ frames: finalFrames,
362
378
  duration: actualDuration,
363
379
  instructionsFollowed: completedChallenges.length === challenges.length,
364
- qualityScore: frames.length > 0 ? Math.min(100, (frames.length / 30) * 100) : 85,
380
+ qualityScore: finalFrames.length > 0 ? Math.min(100, (finalFrames.length / 30) * 100) : 85,
365
381
  challengesCompleted: completedChallenges,
366
382
  sessionId,
367
383
  };
368
384
 
369
385
  console.log('Video recording completed successfully:', {
370
- duration: actualDuration,
386
+ duration: (actualDuration / 1000).toFixed(1) + 's',
371
387
  frames: result.frames.length,
372
- challengesCompleted: completedChallenges.length,
373
- instructionsFollowed: result.instructionsFollowed
388
+ challenges: `${completedChallenges.length}/${challenges.length}`,
389
+ instructionsFollowed: result.instructionsFollowed,
390
+ quality: result.qualityScore.toFixed(0) + '%'
374
391
  });
375
392
 
393
+ isRecordingRef.current = false;
376
394
  onComplete(result);
377
395
  } catch (error) {
378
396
  console.error('Error processing video:', error);
379
397
  setPhase('recording');
398
+ setOverallProgress(0);
380
399
  handleRecordingError(error);
381
400
  }
382
- }, [frames, completedChallenges, challenges, sessionId, onComplete, resetAndRetry, handleRecordingError, strings, minDurationMs]);
401
+ }, [frames, completedChallenges, challenges, sessionId, onComplete, resetAndRetry, handleRecordingError, strings, minDurationMs, phase]);
383
402
 
384
403
  const startFrameCapture = useCallback(() => {
385
404
  if (cameraRef.current && device) {
405
+ console.log('Starting frame capture mode');
386
406
  frameCaptureInterval.current = setInterval(async () => {
387
407
  try {
388
408
  const photo = await cameraRef.current?.takePhoto({
@@ -417,38 +437,79 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
417
437
 
418
438
  const stopRecording = useCallback(async () => {
419
439
  console.log('Stopping recording...');
420
- isRecordingRef.current = false;
440
+
441
+ if (frameCaptureInterval.current) {
442
+ clearInterval(frameCaptureInterval.current);
443
+ frameCaptureInterval.current = null;
444
+ }
421
445
 
422
446
  if (videoRecordingRef.current) {
423
447
  try {
424
448
  console.log('Stopping video recording');
425
- await videoRecordingRef.current.stop();
426
- console.log('Video recording stopped');
449
+ const recording = videoRecordingRef.current;
450
+ videoRecordingRef.current = null;
451
+ isRecordingRef.current = false;
452
+
453
+ await recording.stop();
454
+ console.log('Video recording stopped - waiting for onRecordingFinished callback');
427
455
  } catch (error) {
428
456
  console.error('Error stopping video recording:', error);
457
+ isRecordingRef.current = false;
458
+
459
+ const actualDuration = Date.now() - recordingStartTime.current;
460
+ if (actualDuration >= minDurationMs && frames.length > 0) {
461
+ console.log('Video stopped with error, using captured frames');
462
+ const result: VideoRecordingResult = {
463
+ frames,
464
+ duration: actualDuration,
465
+ instructionsFollowed: completedChallenges.length === challenges.length,
466
+ qualityScore: Math.min(100, (frames.length / 30) * 100),
467
+ challengesCompleted: completedChallenges,
468
+ sessionId,
469
+ };
470
+ onComplete(result);
471
+ }
429
472
  }
430
- videoRecordingRef.current = null;
431
- }
432
-
433
- if (frameCaptureInterval.current) {
434
- clearInterval(frameCaptureInterval.current);
435
- frameCaptureInterval.current = null;
473
+ } else {
474
+ isRecordingRef.current = false;
436
475
  }
437
- }, []);
476
+ }, [frames, completedChallenges, challenges, sessionId, onComplete, minDurationMs]);
438
477
 
439
478
  const runChallenge = useCallback((index: number) => {
440
479
  if (index >= challenges.length) {
480
+ console.log('All challenges completed, stopping recording');
481
+ setOverallProgress(100);
482
+
441
483
  if (isRecordingRef.current) {
442
484
  const elapsed = Date.now() - recordingStartTime.current;
485
+ console.log('Checking duration - Elapsed:', (elapsed / 1000).toFixed(1), 's, Min required:', (minDurationMs / 1000).toFixed(1), 's');
486
+
443
487
  if (elapsed < minDurationMs) {
488
+ const remaining = minDurationMs - elapsed;
489
+ console.log('Waiting additional', (remaining / 1000).toFixed(1), 's to meet minimum duration');
444
490
  setTimeout(() => {
445
491
  if (isRecordingRef.current) {
446
492
  stopRecording();
447
493
  }
448
- }, minDurationMs - elapsed);
494
+ }, remaining);
449
495
  return;
450
496
  }
497
+
451
498
  stopRecording();
499
+ } else {
500
+ const actualDuration = Date.now() - recordingStartTime.current;
501
+ if (actualDuration >= minDurationMs && frames.length > 0) {
502
+ console.log('Recording already stopped, completing with frames');
503
+ const result: VideoRecordingResult = {
504
+ frames,
505
+ duration: actualDuration,
506
+ instructionsFollowed: completedChallenges.length === challenges.length,
507
+ qualityScore: Math.min(100, (frames.length / 30) * 100),
508
+ challengesCompleted: completedChallenges,
509
+ sessionId,
510
+ };
511
+ onComplete(result);
512
+ }
452
513
  }
453
514
  return;
454
515
  }
@@ -505,17 +566,15 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
505
566
  recordingStartTime.current = Date.now();
506
567
  isRecordingRef.current = true;
507
568
 
508
- console.log('Starting video recording, total duration:', totalDuration);
569
+ console.log('Starting video recording, total duration:', (totalDuration / 1000).toFixed(1), 's');
509
570
 
510
571
  if (cameraRef.current && device) {
511
572
  try {
512
573
  videoRecordingRef.current = await cameraRef.current.startRecording({
513
574
  flash: 'off',
514
575
  onRecordingFinished: (video: any) => {
515
- console.log('Video recording finished callback called', video);
516
- if (isRecordingRef.current) {
517
- handleVideoComplete(video);
518
- }
576
+ console.log('Video recording finished callback called, path:', video?.path || 'N/A');
577
+ handleVideoComplete(video);
519
578
  },
520
579
  onRecordingError: (error: any) => {
521
580
  console.error('Recording error:', error);
@@ -192,11 +192,37 @@ export const useBiometricSDK = (): UseBiometricSDKResult => {
192
192
  * Validate identity with all collected data
193
193
  */
194
194
  const validateIdentity = useCallback(async () => {
195
- const result = await sdk.validateIdentity();
196
- if (isMounted.current) {
197
- setState(sdk.getState());
195
+ console.log('validateIdentity called, current state:', sdk.getState().currentStep);
196
+
197
+ // Update state immediately to show validation screen
198
+ setState(sdk.getState());
199
+
200
+ // Poll state during validation to catch intermediate updates
201
+ const pollInterval = setInterval(() => {
202
+ if (isMounted.current) {
203
+ const currentState = sdk.getState();
204
+ setState(currentState);
205
+ console.log('State polled - step:', currentState.currentStep, 'progress:', currentState.progress);
206
+ }
207
+ }, 200);
208
+
209
+ try {
210
+ const result = await sdk.validateIdentity();
211
+ clearInterval(pollInterval);
212
+
213
+ if (isMounted.current) {
214
+ const finalState = sdk.getState();
215
+ setState(finalState);
216
+ console.log('Validation complete, final state:', finalState.currentStep, 'result:', result);
217
+ }
218
+ return result;
219
+ } catch (error) {
220
+ clearInterval(pollInterval);
221
+ if (isMounted.current) {
222
+ setState(sdk.getState());
223
+ }
224
+ throw error;
198
225
  }
199
- return result;
200
226
  }, [sdk]);
201
227
 
202
228
  /**