@hexar/biometric-identity-sdk-react-native 1.0.8 → 1.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/BiometricIdentityFlow.d.ts +1 -0
- package/dist/components/BiometricIdentityFlow.d.ts.map +1 -1
- package/dist/components/BiometricIdentityFlow.js +2 -6
- package/dist/components/InstructionsScreen.d.ts +1 -1
- package/dist/components/InstructionsScreen.d.ts.map +1 -1
- package/dist/components/InstructionsScreen.js +20 -18
- package/dist/components/VideoRecorder.d.ts.map +1 -1
- package/dist/components/VideoRecorder.js +85 -38
- package/package.json +1 -1
- package/src/components/BiometricIdentityFlow.tsx +3 -5
- package/src/components/InstructionsScreen.tsx +31 -28
- package/src/components/VideoRecorder.tsx +103 -41
|
@@ -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,
|
|
1
|
+
{"version":3,"file":"BiometricIdentityFlow.d.ts","sourceRoot":"","sources":["../../src/components/BiometricIdentityFlow.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAA2C,MAAM,OAAO,CAAC;AAChE,OAAO,EAOL,SAAS,EACV,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,gBAAgB,EAChB,WAAW,EACX,cAAc,EAKd,iBAAiB,EAClB,MAAM,oCAAoC,CAAC;AAU5C,MAAM,WAAW,0BAA0B;IACzC,oBAAoB,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,IAAI,CAAC;IACzD,OAAO,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC;IACzC,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,kBAAkB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5C,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,MAAM,CAAC,EAAE;QACP,SAAS,CAAC,EAAE,SAAS,CAAC;QACtB,OAAO,CAAC,EAAE,SAAS,CAAC;KACrB,CAAC;CACH;AAED,eAAO,MAAM,qBAAqB,EAAE,KAAK,CAAC,EAAE,CAAC,0BAA0B,CAiTtE,CAAC;AAiOF,eAAe,qBAAqB,CAAC"}
|
|
@@ -48,7 +48,7 @@ const ValidationProgress_1 = require("./ValidationProgress");
|
|
|
48
48
|
const ResultScreen_1 = require("./ResultScreen");
|
|
49
49
|
const ErrorScreen_1 = require("./ErrorScreen");
|
|
50
50
|
const InstructionsScreen_1 = require("./InstructionsScreen");
|
|
51
|
-
const BiometricIdentityFlow = ({ onValidationComplete, onError, theme, language, customTranslations, smartLivenessMode = true, styles: customStyles, }) => {
|
|
51
|
+
const BiometricIdentityFlow = ({ onValidationComplete, onError, theme, language, customTranslations, smartLivenessMode = true, routeBack, styles: customStyles, }) => {
|
|
52
52
|
const { sdk, state, isInitialized, isUsingBackend, challenges, uploadFrontID, uploadBackID, storeVideoRecording, fetchChallenges, validateIdentity, reset, } = (0, useBiometricSDK_1.useBiometricSDK)();
|
|
53
53
|
const [showCamera, setShowCamera] = (0, react_1.useState)(false);
|
|
54
54
|
const [cameraMode, setCameraMode] = (0, react_1.useState)('front');
|
|
@@ -158,11 +158,7 @@ const BiometricIdentityFlow = ({ onValidationComplete, onError, theme, language,
|
|
|
158
158
|
}
|
|
159
159
|
// Show instructions on first load
|
|
160
160
|
if (showInstructions) {
|
|
161
|
-
return (react_1.default.createElement(InstructionsScreen_1.InstructionsScreen, { theme: theme, language: language, onStart: () => setShowInstructions(false),
|
|
162
|
-
name: 'BiometricError',
|
|
163
|
-
message: 'User cancelled',
|
|
164
|
-
code: biometric_identity_sdk_core_1.BiometricErrorCode.USER_CANCELLED,
|
|
165
|
-
}) : undefined, styles: customStyles }));
|
|
161
|
+
return (react_1.default.createElement(InstructionsScreen_1.InstructionsScreen, { theme: theme, language: language, onStart: () => setShowInstructions(false), routeBack: routeBack, styles: customStyles }));
|
|
166
162
|
}
|
|
167
163
|
// Show camera/video recorder
|
|
168
164
|
if (showCamera) {
|
|
@@ -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,
|
|
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,
|
|
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,
|
|
1
|
+
{"version":3,"file":"VideoRecorder.d.ts","sourceRoot":"","sources":["../../src/components/VideoRecorder.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAmD,MAAM,OAAO,CAAC;AAaxE,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAE,iBAAiB,EAA2B,MAAM,oCAAoC,CAAC;AAGlI,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,sEAAsE;IACtE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,sCAAsC;IACtC,YAAY,CAAC,EAAE,mBAAmB,EAAE,CAAC;IACrC,0CAA0C;IAC1C,UAAU,CAAC,EAAE,eAAe,EAAE,CAAC;IAC/B,wCAAwC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kDAAkD;IAClD,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,wCAAwC;IACxC,UAAU,EAAE,CAAC,SAAS,EAAE,oBAAoB,KAAK,IAAI,CAAC;IACtD,iCAAiC;IACjC,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,gDAAgD;IAChD,iBAAiB,CAAC,EAAE,MAAM,OAAO,CAAC,eAAe,EAAE,CAAC,CAAC;CACtD;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,oBAAoB,EAAE,OAAO,CAAC;IAC9B,YAAY,EAAE,MAAM,CAAC;IACrB,mBAAmB,EAAE,MAAM,EAAE,CAAC;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AA+CD,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CAiwBtD,CAAC;AA4OF,eAAe,aAAa,CAAC"}
|
|
@@ -274,13 +274,19 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
|
|
|
274
274
|
react_native_1.Alert.alert('Recording Error', 'Failed to record video. Please try again.', [{ text: 'OK', onPress: onCancel }]);
|
|
275
275
|
}, [onCancel]);
|
|
276
276
|
const handleVideoComplete = (0, react_1.useCallback)(async (video) => {
|
|
277
|
-
|
|
277
|
+
react_native_1.Alert.alert('Video Complete', `handleVideoComplete called\nVideo path: ${video?.path || 'N/A'}`);
|
|
278
|
+
if (!isRecordingRef.current && phase !== 'processing') {
|
|
279
|
+
react_native_1.Alert.alert('Info', 'Video already processed, ignoring duplicate callback');
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
278
282
|
try {
|
|
279
283
|
setPhase('processing');
|
|
284
|
+
setOverallProgress(100);
|
|
280
285
|
const actualDuration = Date.now() - recordingStartTime.current;
|
|
281
|
-
|
|
286
|
+
react_native_1.Alert.alert('Video Processing', `Duration: ${(actualDuration / 1000).toFixed(1)}s\nMin required: ${(minDurationMs / 1000).toFixed(1)}s\nFrames: ${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,41 @@ 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
|
-
|
|
298
|
+
react_native_1.Alert.alert('Video File Read', `Successfully read video file\nSize: ${videoBase64.length} bytes`);
|
|
293
299
|
}
|
|
294
300
|
catch (fsError) {
|
|
295
|
-
|
|
301
|
+
react_native_1.Alert.alert('Video File Warning', `Could not read video file, using captured frames\nError: ${fsError}`);
|
|
296
302
|
}
|
|
297
303
|
}
|
|
304
|
+
const finalFrames = frames.length > 0 ? frames : (videoBase64 ? [videoBase64] : []);
|
|
305
|
+
if (finalFrames.length === 0) {
|
|
306
|
+
react_native_1.Alert.alert('Error', 'No frames available, cannot complete');
|
|
307
|
+
setPhase('recording');
|
|
308
|
+
handleRecordingError(new Error('No video frames captured'));
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
298
311
|
const result = {
|
|
299
|
-
frames:
|
|
312
|
+
frames: finalFrames,
|
|
300
313
|
duration: actualDuration,
|
|
301
314
|
instructionsFollowed: completedChallenges.length === challenges.length,
|
|
302
|
-
qualityScore:
|
|
315
|
+
qualityScore: finalFrames.length > 0 ? Math.min(100, (finalFrames.length / 30) * 100) : 85,
|
|
303
316
|
challengesCompleted: completedChallenges,
|
|
304
317
|
sessionId,
|
|
305
318
|
};
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
frames: result.frames.length,
|
|
309
|
-
challengesCompleted: completedChallenges.length,
|
|
310
|
-
instructionsFollowed: result.instructionsFollowed
|
|
311
|
-
});
|
|
319
|
+
react_native_1.Alert.alert('Video Completed Successfully', `Duration: ${(actualDuration / 1000).toFixed(1)}s\nFrames: ${result.frames.length}\nChallenges: ${completedChallenges.length}/${challenges.length}\nInstructions followed: ${result.instructionsFollowed ? 'Yes' : 'No'}\nQuality: ${result.qualityScore.toFixed(0)}%`);
|
|
320
|
+
isRecordingRef.current = false;
|
|
312
321
|
onComplete(result);
|
|
313
322
|
}
|
|
314
323
|
catch (error) {
|
|
315
|
-
|
|
324
|
+
react_native_1.Alert.alert('Error Processing Video', `Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
316
325
|
setPhase('recording');
|
|
326
|
+
setOverallProgress(0);
|
|
317
327
|
handleRecordingError(error);
|
|
318
328
|
}
|
|
319
|
-
}, [frames, completedChallenges, challenges, sessionId, onComplete, resetAndRetry, handleRecordingError, strings, minDurationMs]);
|
|
329
|
+
}, [frames, completedChallenges, challenges, sessionId, onComplete, resetAndRetry, handleRecordingError, strings, minDurationMs, phase]);
|
|
320
330
|
const startFrameCapture = (0, react_1.useCallback)(() => {
|
|
321
331
|
if (cameraRef.current && device) {
|
|
332
|
+
react_native_1.Alert.alert('Frame Capture', 'Starting frame capture mode');
|
|
322
333
|
frameCaptureInterval.current = setInterval(async () => {
|
|
323
334
|
try {
|
|
324
335
|
const photo = await cameraRef.current?.takePhoto({
|
|
@@ -346,44 +357,82 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
|
|
|
346
357
|
}
|
|
347
358
|
}
|
|
348
359
|
catch (error) {
|
|
349
|
-
|
|
360
|
+
react_native_1.Alert.alert('Frame Capture Error', `Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
350
361
|
}
|
|
351
362
|
}, 100);
|
|
352
363
|
}
|
|
353
364
|
}, [device]);
|
|
354
365
|
const stopRecording = (0, react_1.useCallback)(async () => {
|
|
355
|
-
|
|
356
|
-
|
|
366
|
+
react_native_1.Alert.alert('Stop Recording', 'Stopping recording...');
|
|
367
|
+
if (frameCaptureInterval.current) {
|
|
368
|
+
clearInterval(frameCaptureInterval.current);
|
|
369
|
+
frameCaptureInterval.current = null;
|
|
370
|
+
}
|
|
357
371
|
if (videoRecordingRef.current) {
|
|
358
372
|
try {
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
373
|
+
react_native_1.Alert.alert('Stop Recording', 'Stopping video recording...');
|
|
374
|
+
const recording = videoRecordingRef.current;
|
|
375
|
+
videoRecordingRef.current = null;
|
|
376
|
+
isRecordingRef.current = false;
|
|
377
|
+
await recording.stop();
|
|
378
|
+
react_native_1.Alert.alert('Recording Stopped', 'Video recording stopped\nWaiting for onRecordingFinished callback');
|
|
362
379
|
}
|
|
363
380
|
catch (error) {
|
|
364
|
-
|
|
381
|
+
react_native_1.Alert.alert('Error Stopping Recording', `Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
382
|
+
isRecordingRef.current = false;
|
|
383
|
+
const actualDuration = Date.now() - recordingStartTime.current;
|
|
384
|
+
if (actualDuration >= minDurationMs && frames.length > 0) {
|
|
385
|
+
react_native_1.Alert.alert('Using Captured Frames', 'Video stopped with error, using captured frames');
|
|
386
|
+
const result = {
|
|
387
|
+
frames,
|
|
388
|
+
duration: actualDuration,
|
|
389
|
+
instructionsFollowed: completedChallenges.length === challenges.length,
|
|
390
|
+
qualityScore: Math.min(100, (frames.length / 30) * 100),
|
|
391
|
+
challengesCompleted: completedChallenges,
|
|
392
|
+
sessionId,
|
|
393
|
+
};
|
|
394
|
+
onComplete(result);
|
|
395
|
+
}
|
|
365
396
|
}
|
|
366
|
-
videoRecordingRef.current = null;
|
|
367
397
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
frameCaptureInterval.current = null;
|
|
398
|
+
else {
|
|
399
|
+
isRecordingRef.current = false;
|
|
371
400
|
}
|
|
372
|
-
}, []);
|
|
401
|
+
}, [frames, completedChallenges, challenges, sessionId, onComplete, minDurationMs]);
|
|
373
402
|
const runChallenge = (0, react_1.useCallback)((index) => {
|
|
374
403
|
if (index >= challenges.length) {
|
|
404
|
+
react_native_1.Alert.alert('Challenges Complete', 'All challenges completed, stopping recording');
|
|
405
|
+
setOverallProgress(100);
|
|
375
406
|
if (isRecordingRef.current) {
|
|
376
407
|
const elapsed = Date.now() - recordingStartTime.current;
|
|
408
|
+
react_native_1.Alert.alert('Checking Duration', `Elapsed: ${(elapsed / 1000).toFixed(1)}s\nMin required: ${(minDurationMs / 1000).toFixed(1)}s`);
|
|
377
409
|
if (elapsed < minDurationMs) {
|
|
410
|
+
const remaining = minDurationMs - elapsed;
|
|
411
|
+
react_native_1.Alert.alert('Waiting', `Waiting additional ${(remaining / 1000).toFixed(1)}s to meet minimum duration`);
|
|
378
412
|
setTimeout(() => {
|
|
379
413
|
if (isRecordingRef.current) {
|
|
380
414
|
stopRecording();
|
|
381
415
|
}
|
|
382
|
-
},
|
|
416
|
+
}, remaining);
|
|
383
417
|
return;
|
|
384
418
|
}
|
|
385
419
|
stopRecording();
|
|
386
420
|
}
|
|
421
|
+
else {
|
|
422
|
+
const actualDuration = Date.now() - recordingStartTime.current;
|
|
423
|
+
if (actualDuration >= minDurationMs && frames.length > 0) {
|
|
424
|
+
react_native_1.Alert.alert('Completing', 'Recording already stopped, completing with frames');
|
|
425
|
+
const result = {
|
|
426
|
+
frames,
|
|
427
|
+
duration: actualDuration,
|
|
428
|
+
instructionsFollowed: completedChallenges.length === challenges.length,
|
|
429
|
+
qualityScore: Math.min(100, (frames.length / 30) * 100),
|
|
430
|
+
challengesCompleted: completedChallenges,
|
|
431
|
+
sessionId,
|
|
432
|
+
};
|
|
433
|
+
onComplete(result);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
387
436
|
return;
|
|
388
437
|
}
|
|
389
438
|
const challenge = challenges[index];
|
|
@@ -428,39 +477,37 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
|
|
|
428
477
|
setPhase('recording');
|
|
429
478
|
recordingStartTime.current = Date.now();
|
|
430
479
|
isRecordingRef.current = true;
|
|
431
|
-
|
|
480
|
+
react_native_1.Alert.alert('Start Recording', `Starting video recording\nTotal duration: ${(totalDuration / 1000).toFixed(1)}s`);
|
|
432
481
|
if (cameraRef.current && device) {
|
|
433
482
|
try {
|
|
434
483
|
videoRecordingRef.current = await cameraRef.current.startRecording({
|
|
435
484
|
flash: 'off',
|
|
436
485
|
onRecordingFinished: (video) => {
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
handleVideoComplete(video);
|
|
440
|
-
}
|
|
486
|
+
react_native_1.Alert.alert('Recording Finished', `Video recording finished callback called\nPath: ${video?.path || 'N/A'}`);
|
|
487
|
+
handleVideoComplete(video);
|
|
441
488
|
},
|
|
442
489
|
onRecordingError: (error) => {
|
|
443
|
-
|
|
490
|
+
react_native_1.Alert.alert('Recording Error', `Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
444
491
|
handleRecordingError(error);
|
|
445
492
|
},
|
|
446
493
|
});
|
|
447
|
-
|
|
494
|
+
react_native_1.Alert.alert('Recording Started', 'Video recording started successfully');
|
|
448
495
|
}
|
|
449
496
|
catch (error) {
|
|
450
|
-
|
|
497
|
+
react_native_1.Alert.alert('Recording Fallback', `Video recording not available, falling back to frame capture\nError: ${error}`);
|
|
451
498
|
startFrameCapture();
|
|
452
499
|
}
|
|
453
500
|
}
|
|
454
501
|
else {
|
|
455
|
-
|
|
502
|
+
react_native_1.Alert.alert('Camera Not Available', 'Camera not available, using frame capture');
|
|
456
503
|
startFrameCapture();
|
|
457
504
|
}
|
|
458
505
|
runChallenge(0);
|
|
459
506
|
const timeoutId = setTimeout(() => {
|
|
460
|
-
|
|
507
|
+
react_native_1.Alert.alert('Timeout', 'Recording timeout reached, stopping recording');
|
|
461
508
|
if (isRecordingRef.current) {
|
|
462
509
|
stopRecording().catch(err => {
|
|
463
|
-
|
|
510
|
+
react_native_1.Alert.alert('Timeout Error', `Error stopping recording on timeout: ${err}`);
|
|
464
511
|
});
|
|
465
512
|
}
|
|
466
513
|
}, totalDuration);
|
package/package.json
CHANGED
|
@@ -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 {
|
|
@@ -194,11 +196,7 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
|
|
|
194
196
|
theme={theme}
|
|
195
197
|
language={language}
|
|
196
198
|
onStart={() => setShowInstructions(false)}
|
|
197
|
-
|
|
198
|
-
name: 'BiometricError',
|
|
199
|
-
message: 'User cancelled',
|
|
200
|
-
code: BiometricErrorCode.USER_CANCELLED,
|
|
201
|
-
} as BiometricError) : undefined}
|
|
199
|
+
routeBack={routeBack}
|
|
202
200
|
styles={customStyles}
|
|
203
201
|
/>
|
|
204
202
|
);
|
|
@@ -18,7 +18,7 @@ export interface InstructionsScreenProps {
|
|
|
18
18
|
theme?: ThemeConfig;
|
|
19
19
|
language?: SupportedLanguage;
|
|
20
20
|
onStart: () => void;
|
|
21
|
-
|
|
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
|
-
|
|
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
|
-
{/*
|
|
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',
|
|
@@ -328,16 +328,26 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
328
328
|
}, [onCancel]);
|
|
329
329
|
|
|
330
330
|
const handleVideoComplete = useCallback(async (video: any) => {
|
|
331
|
-
|
|
331
|
+
Alert.alert('Video Complete', `handleVideoComplete called\nVideo path: ${video?.path || 'N/A'}`);
|
|
332
|
+
|
|
333
|
+
if (!isRecordingRef.current && phase !== 'processing') {
|
|
334
|
+
Alert.alert('Info', 'Video already processed, ignoring duplicate callback');
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
332
337
|
|
|
333
338
|
try {
|
|
334
339
|
setPhase('processing');
|
|
335
|
-
|
|
340
|
+
setOverallProgress(100);
|
|
336
341
|
|
|
337
|
-
|
|
342
|
+
const actualDuration = Date.now() - recordingStartTime.current;
|
|
343
|
+
Alert.alert(
|
|
344
|
+
'Video Processing',
|
|
345
|
+
`Duration: ${(actualDuration / 1000).toFixed(1)}s\nMin required: ${(minDurationMs / 1000).toFixed(1)}s\nFrames: ${frames.length}`
|
|
346
|
+
);
|
|
338
347
|
|
|
339
348
|
if (actualDuration < minDurationMs) {
|
|
340
349
|
setPhase('recording');
|
|
350
|
+
setOverallProgress(0);
|
|
341
351
|
Alert.alert(
|
|
342
352
|
strings.errors.videoTooShort?.title || 'Recording Too Short',
|
|
343
353
|
strings.errors.videoTooShort?.message || `Video must be at least ${minDurationMs / 1000} seconds. Please try again.`,
|
|
@@ -351,38 +361,48 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
351
361
|
try {
|
|
352
362
|
const RNFS = require('react-native-fs');
|
|
353
363
|
videoBase64 = await RNFS.readFile(video.path, 'base64');
|
|
354
|
-
|
|
364
|
+
Alert.alert('Video File Read', `Successfully read video file\nSize: ${videoBase64.length} bytes`);
|
|
355
365
|
} catch (fsError) {
|
|
356
|
-
|
|
366
|
+
Alert.alert('Video File Warning', `Could not read video file, using captured frames\nError: ${fsError}`);
|
|
357
367
|
}
|
|
358
368
|
}
|
|
359
369
|
|
|
370
|
+
const finalFrames = frames.length > 0 ? frames : (videoBase64 ? [videoBase64] : []);
|
|
371
|
+
|
|
372
|
+
if (finalFrames.length === 0) {
|
|
373
|
+
Alert.alert('Error', 'No frames available, cannot complete');
|
|
374
|
+
setPhase('recording');
|
|
375
|
+
handleRecordingError(new Error('No video frames captured'));
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
|
|
360
379
|
const result: VideoRecordingResult = {
|
|
361
|
-
frames:
|
|
380
|
+
frames: finalFrames,
|
|
362
381
|
duration: actualDuration,
|
|
363
382
|
instructionsFollowed: completedChallenges.length === challenges.length,
|
|
364
|
-
qualityScore:
|
|
383
|
+
qualityScore: finalFrames.length > 0 ? Math.min(100, (finalFrames.length / 30) * 100) : 85,
|
|
365
384
|
challengesCompleted: completedChallenges,
|
|
366
385
|
sessionId,
|
|
367
386
|
};
|
|
368
387
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
instructionsFollowed: result.instructionsFollowed
|
|
374
|
-
});
|
|
388
|
+
Alert.alert(
|
|
389
|
+
'Video Completed Successfully',
|
|
390
|
+
`Duration: ${(actualDuration / 1000).toFixed(1)}s\nFrames: ${result.frames.length}\nChallenges: ${completedChallenges.length}/${challenges.length}\nInstructions followed: ${result.instructionsFollowed ? 'Yes' : 'No'}\nQuality: ${result.qualityScore.toFixed(0)}%`
|
|
391
|
+
);
|
|
375
392
|
|
|
393
|
+
isRecordingRef.current = false;
|
|
376
394
|
onComplete(result);
|
|
377
395
|
} catch (error) {
|
|
378
|
-
|
|
396
|
+
Alert.alert('Error Processing Video', `Error: ${error instanceof Error ? error.message : String(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
|
+
Alert.alert('Frame Capture', 'Starting frame capture mode');
|
|
386
406
|
frameCaptureInterval.current = setInterval(async () => {
|
|
387
407
|
try {
|
|
388
408
|
const photo = await cameraRef.current?.takePhoto({
|
|
@@ -409,46 +429,90 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
409
429
|
}
|
|
410
430
|
}
|
|
411
431
|
} catch (error) {
|
|
412
|
-
|
|
432
|
+
Alert.alert('Frame Capture Error', `Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
413
433
|
}
|
|
414
434
|
}, 100);
|
|
415
435
|
}
|
|
416
436
|
}, [device]);
|
|
417
437
|
|
|
418
438
|
const stopRecording = useCallback(async () => {
|
|
419
|
-
|
|
420
|
-
|
|
439
|
+
Alert.alert('Stop Recording', 'Stopping recording...');
|
|
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
|
-
|
|
425
|
-
|
|
426
|
-
|
|
448
|
+
Alert.alert('Stop Recording', 'Stopping video recording...');
|
|
449
|
+
const recording = videoRecordingRef.current;
|
|
450
|
+
videoRecordingRef.current = null;
|
|
451
|
+
isRecordingRef.current = false;
|
|
452
|
+
|
|
453
|
+
await recording.stop();
|
|
454
|
+
Alert.alert('Recording Stopped', 'Video recording stopped\nWaiting for onRecordingFinished callback');
|
|
427
455
|
} catch (error) {
|
|
428
|
-
|
|
456
|
+
Alert.alert('Error Stopping Recording', `Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
457
|
+
isRecordingRef.current = false;
|
|
458
|
+
|
|
459
|
+
const actualDuration = Date.now() - recordingStartTime.current;
|
|
460
|
+
if (actualDuration >= minDurationMs && frames.length > 0) {
|
|
461
|
+
Alert.alert('Using Captured Frames', 'Video stopped with error, using captured frames');
|
|
462
|
+
const result: VideoRecordingResult = {
|
|
463
|
+
frames,
|
|
464
|
+
duration: actualDuration,
|
|
465
|
+
instructionsFollowed: completedChallenges.length === challenges.length,
|
|
466
|
+
qualityScore: Math.min(100, (frames.length / 30) * 100),
|
|
467
|
+
challengesCompleted: completedChallenges,
|
|
468
|
+
sessionId,
|
|
469
|
+
};
|
|
470
|
+
onComplete(result);
|
|
471
|
+
}
|
|
429
472
|
}
|
|
430
|
-
|
|
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
|
+
Alert.alert('Challenges Complete', 'All challenges completed, stopping recording');
|
|
481
|
+
setOverallProgress(100);
|
|
482
|
+
|
|
441
483
|
if (isRecordingRef.current) {
|
|
442
484
|
const elapsed = Date.now() - recordingStartTime.current;
|
|
485
|
+
Alert.alert(
|
|
486
|
+
'Checking Duration',
|
|
487
|
+
`Elapsed: ${(elapsed / 1000).toFixed(1)}s\nMin required: ${(minDurationMs / 1000).toFixed(1)}s`
|
|
488
|
+
);
|
|
489
|
+
|
|
443
490
|
if (elapsed < minDurationMs) {
|
|
491
|
+
const remaining = minDurationMs - elapsed;
|
|
492
|
+
Alert.alert('Waiting', `Waiting additional ${(remaining / 1000).toFixed(1)}s to meet minimum duration`);
|
|
444
493
|
setTimeout(() => {
|
|
445
494
|
if (isRecordingRef.current) {
|
|
446
495
|
stopRecording();
|
|
447
496
|
}
|
|
448
|
-
},
|
|
497
|
+
}, remaining);
|
|
449
498
|
return;
|
|
450
499
|
}
|
|
500
|
+
|
|
451
501
|
stopRecording();
|
|
502
|
+
} else {
|
|
503
|
+
const actualDuration = Date.now() - recordingStartTime.current;
|
|
504
|
+
if (actualDuration >= minDurationMs && frames.length > 0) {
|
|
505
|
+
Alert.alert('Completing', 'Recording already stopped, completing with frames');
|
|
506
|
+
const result: VideoRecordingResult = {
|
|
507
|
+
frames,
|
|
508
|
+
duration: actualDuration,
|
|
509
|
+
instructionsFollowed: completedChallenges.length === challenges.length,
|
|
510
|
+
qualityScore: Math.min(100, (frames.length / 30) * 100),
|
|
511
|
+
challengesCompleted: completedChallenges,
|
|
512
|
+
sessionId,
|
|
513
|
+
};
|
|
514
|
+
onComplete(result);
|
|
515
|
+
}
|
|
452
516
|
}
|
|
453
517
|
return;
|
|
454
518
|
}
|
|
@@ -505,40 +569,38 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
505
569
|
recordingStartTime.current = Date.now();
|
|
506
570
|
isRecordingRef.current = true;
|
|
507
571
|
|
|
508
|
-
|
|
572
|
+
Alert.alert('Start Recording', `Starting video recording\nTotal duration: ${(totalDuration / 1000).toFixed(1)}s`);
|
|
509
573
|
|
|
510
574
|
if (cameraRef.current && device) {
|
|
511
575
|
try {
|
|
512
576
|
videoRecordingRef.current = await cameraRef.current.startRecording({
|
|
513
577
|
flash: 'off',
|
|
514
578
|
onRecordingFinished: (video: any) => {
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
handleVideoComplete(video);
|
|
518
|
-
}
|
|
579
|
+
Alert.alert('Recording Finished', `Video recording finished callback called\nPath: ${video?.path || 'N/A'}`);
|
|
580
|
+
handleVideoComplete(video);
|
|
519
581
|
},
|
|
520
582
|
onRecordingError: (error: any) => {
|
|
521
|
-
|
|
583
|
+
Alert.alert('Recording Error', `Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
522
584
|
handleRecordingError(error);
|
|
523
585
|
},
|
|
524
586
|
});
|
|
525
|
-
|
|
587
|
+
Alert.alert('Recording Started', 'Video recording started successfully');
|
|
526
588
|
} catch (error) {
|
|
527
|
-
|
|
589
|
+
Alert.alert('Recording Fallback', `Video recording not available, falling back to frame capture\nError: ${error}`);
|
|
528
590
|
startFrameCapture();
|
|
529
591
|
}
|
|
530
592
|
} else {
|
|
531
|
-
|
|
593
|
+
Alert.alert('Camera Not Available', 'Camera not available, using frame capture');
|
|
532
594
|
startFrameCapture();
|
|
533
595
|
}
|
|
534
596
|
|
|
535
597
|
runChallenge(0);
|
|
536
598
|
|
|
537
599
|
const timeoutId = setTimeout(() => {
|
|
538
|
-
|
|
600
|
+
Alert.alert('Timeout', 'Recording timeout reached, stopping recording');
|
|
539
601
|
if (isRecordingRef.current) {
|
|
540
602
|
stopRecording().catch(err => {
|
|
541
|
-
|
|
603
|
+
Alert.alert('Timeout Error', `Error stopping recording on timeout: ${err}`);
|
|
542
604
|
});
|
|
543
605
|
}
|
|
544
606
|
}, totalDuration);
|