@hexar/biometric-identity-sdk-react-native 1.0.7 → 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 +4 -4
- package/dist/components/CameraCapture.d.ts +2 -1
- package/dist/components/CameraCapture.d.ts.map +1 -1
- package/dist/components/CameraCapture.js +22 -16
- package/dist/components/InstructionsScreen.d.ts +1 -0
- package/dist/components/InstructionsScreen.d.ts.map +1 -1
- package/dist/components/InstructionsScreen.js +20 -1
- package/dist/components/VideoRecorder.d.ts +2 -1
- package/dist/components/VideoRecorder.d.ts.map +1 -1
- package/dist/components/VideoRecorder.js +138 -59
- package/package.json +1 -1
- package/src/components/BiometricIdentityFlow.tsx +6 -0
- package/src/components/CameraCapture.tsx +26 -13
- package/src/components/InstructionsScreen.tsx +30 -0
- package/src/components/VideoRecorder.tsx +170 -60
|
@@ -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,
|
|
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,17 +158,17 @@ const BiometricIdentityFlow = ({ onValidationComplete, onError, theme, language,
|
|
|
158
158
|
}
|
|
159
159
|
// Show instructions on first load
|
|
160
160
|
if (showInstructions) {
|
|
161
|
-
return (react_1.default.createElement(InstructionsScreen_1.InstructionsScreen, { theme: theme, language: language, onStart: () => setShowInstructions(false), styles: customStyles }));
|
|
161
|
+
return (react_1.default.createElement(InstructionsScreen_1.InstructionsScreen, { theme: theme, language: language, onStart: () => setShowInstructions(false), routeBack: routeBack, styles: customStyles }));
|
|
162
162
|
}
|
|
163
163
|
// Show camera/video recorder
|
|
164
164
|
if (showCamera) {
|
|
165
165
|
if (cameraMode === 'video') {
|
|
166
|
-
return (react_1.default.createElement(VideoRecorder_1.VideoRecorder, { theme: theme, challenges: currentChallenges, smartMode: smartLivenessMode, sessionId: sdk.getSessionId() || undefined, onComplete: handleCaptureComplete, onCancel: () => setShowCamera(false), onFetchChallenges: async () => {
|
|
166
|
+
return (react_1.default.createElement(VideoRecorder_1.VideoRecorder, { theme: theme, language: language, challenges: currentChallenges, smartMode: smartLivenessMode, sessionId: sdk.getSessionId() || undefined, onComplete: handleCaptureComplete, onCancel: () => setShowCamera(false), onFetchChallenges: async () => {
|
|
167
167
|
const challenges = await fetchChallenges('active');
|
|
168
168
|
return challenges;
|
|
169
169
|
} }));
|
|
170
170
|
}
|
|
171
|
-
return (react_1.default.createElement(CameraCapture_1.CameraCapture, { mode: cameraMode, theme: theme, onCapture: handleCaptureComplete, onCancel: () => setShowCamera(false) }));
|
|
171
|
+
return (react_1.default.createElement(CameraCapture_1.CameraCapture, { mode: cameraMode, theme: theme, language: language, onCapture: handleCaptureComplete, onCancel: () => setShowCamera(false) }));
|
|
172
172
|
}
|
|
173
173
|
// Show validation progress
|
|
174
174
|
if (state.currentStep === biometric_identity_sdk_core_1.SDKStep.VALIDATING) {
|
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
* Handles ID document photo capture
|
|
4
4
|
*/
|
|
5
5
|
import React from 'react';
|
|
6
|
-
import { ThemeConfig } from '@hexar/biometric-identity-sdk-core';
|
|
6
|
+
import { ThemeConfig, SupportedLanguage } from '@hexar/biometric-identity-sdk-core';
|
|
7
7
|
export interface CameraCaptureProps {
|
|
8
8
|
mode: 'front' | 'back';
|
|
9
9
|
theme?: ThemeConfig;
|
|
10
|
+
language?: SupportedLanguage;
|
|
10
11
|
onCapture: (imageData: string) => void;
|
|
11
12
|
onCancel: () => void;
|
|
12
13
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CameraCapture.d.ts","sourceRoot":"","sources":["../../src/components/CameraCapture.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAsC,MAAM,OAAO,CAAC;AAY3D,OAAO,EAAE,WAAW,EAAE,MAAM,oCAAoC,CAAC;
|
|
1
|
+
{"version":3,"file":"CameraCapture.d.ts","sourceRoot":"","sources":["../../src/components/CameraCapture.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAsC,MAAM,OAAO,CAAC;AAY3D,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAA2B,MAAM,oCAAoC,CAAC;AAI7G,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,OAAO,GAAG,MAAM,CAAC;IACvB,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,SAAS,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,QAAQ,EAAE,MAAM,IAAI,CAAC;CACtB;AAED,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CA6NtD,CAAC;AA0JF,eAAe,aAAa,CAAC"}
|
|
@@ -42,12 +42,17 @@ const react_1 = __importStar(require("react"));
|
|
|
42
42
|
const react_native_1 = require("react-native");
|
|
43
43
|
const react_native_vision_camera_1 = require("react-native-vision-camera");
|
|
44
44
|
const react_native_permissions_1 = require("react-native-permissions");
|
|
45
|
+
const biometric_identity_sdk_core_1 = require("@hexar/biometric-identity-sdk-core");
|
|
45
46
|
const { width, height } = react_native_1.Dimensions.get('window');
|
|
46
|
-
const CameraCapture = ({ mode, theme, onCapture, onCancel, }) => {
|
|
47
|
+
const CameraCapture = ({ mode, theme, language, onCapture, onCancel, }) => {
|
|
47
48
|
const [isCapturing, setIsCapturing] = (0, react_1.useState)(false);
|
|
48
49
|
const [hasPermission, setHasPermission] = (0, react_1.useState)(false);
|
|
49
50
|
const cameraRef = (0, react_1.useRef)(null);
|
|
50
51
|
const { hasPermission: cameraPermission, requestPermission } = (0, react_native_vision_camera_1.useCameraPermission)();
|
|
52
|
+
if (language) {
|
|
53
|
+
(0, biometric_identity_sdk_core_1.setLanguage)(language);
|
|
54
|
+
}
|
|
55
|
+
const strings = (0, biometric_identity_sdk_core_1.getStrings)();
|
|
51
56
|
// Get camera device (back camera for document capture)
|
|
52
57
|
const device = (0, react_native_vision_camera_1.useCameraDevice)('back');
|
|
53
58
|
(0, react_1.useEffect)(() => {
|
|
@@ -78,7 +83,10 @@ const CameraCapture = ({ mode, theme, onCapture, onCancel, }) => {
|
|
|
78
83
|
catch (error) {
|
|
79
84
|
console.error('Permission check error:', error);
|
|
80
85
|
setHasPermission(false);
|
|
81
|
-
|
|
86
|
+
const errorMsg = typeof strings.errors.cameraPermissionDenied === 'string'
|
|
87
|
+
? strings.errors.cameraPermissionDenied
|
|
88
|
+
: strings.errors.cameraPermissionDenied?.message || 'Please enable camera access in your device settings to capture your ID document.';
|
|
89
|
+
react_native_1.Alert.alert('Camera Permission Required', errorMsg);
|
|
82
90
|
}
|
|
83
91
|
};
|
|
84
92
|
const handleCapture = async () => {
|
|
@@ -136,23 +144,25 @@ const CameraCapture = ({ mode, theme, onCapture, onCancel, }) => {
|
|
|
136
144
|
}
|
|
137
145
|
};
|
|
138
146
|
const instructions = mode === 'front'
|
|
139
|
-
?
|
|
140
|
-
:
|
|
147
|
+
? strings.capture.frontId.instruction
|
|
148
|
+
: strings.capture.backId.instruction;
|
|
141
149
|
if (!hasPermission) {
|
|
142
150
|
return (react_1.default.createElement(react_native_1.View, { style: styles.container },
|
|
143
151
|
react_1.default.createElement(react_native_1.View, { style: styles.permissionContainer },
|
|
144
|
-
react_1.default.createElement(react_native_1.Text, { style: styles.permissionText },
|
|
152
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.permissionText }, typeof strings.errors.cameraPermissionDenied === 'string'
|
|
153
|
+
? strings.errors.cameraPermissionDenied
|
|
154
|
+
: strings.errors.cameraPermissionDenied?.message || 'Camera permission is required'),
|
|
145
155
|
react_1.default.createElement(react_native_1.TouchableOpacity, { style: [styles.button, { backgroundColor: theme?.primaryColor || '#6366F1' }], onPress: checkPermissions },
|
|
146
|
-
react_1.default.createElement(react_native_1.Text, { style: styles.buttonText },
|
|
156
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.buttonText }, strings.common.grantPermission || 'Grant Permission')),
|
|
147
157
|
react_1.default.createElement(react_native_1.TouchableOpacity, { style: [styles.button, styles.cancelButton], onPress: onCancel },
|
|
148
|
-
react_1.default.createElement(react_native_1.Text, { style: styles.buttonText },
|
|
158
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.buttonText }, strings.common.cancel || 'Cancel')))));
|
|
149
159
|
}
|
|
150
160
|
if (!device) {
|
|
151
161
|
return (react_1.default.createElement(react_native_1.View, { style: styles.container },
|
|
152
162
|
react_1.default.createElement(react_native_1.View, { style: styles.permissionContainer },
|
|
153
|
-
react_1.default.createElement(react_native_1.Text, { style: styles.permissionText },
|
|
163
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.permissionText }, strings.errors.cameraNotAvailable || 'Camera not available'),
|
|
154
164
|
react_1.default.createElement(react_native_1.TouchableOpacity, { style: [styles.button, styles.cancelButton], onPress: onCancel },
|
|
155
|
-
react_1.default.createElement(react_native_1.Text, { style: styles.buttonText },
|
|
165
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.buttonText }, strings.common.cancel || 'Cancel')))));
|
|
156
166
|
}
|
|
157
167
|
return (react_1.default.createElement(react_native_1.View, { style: styles.container },
|
|
158
168
|
react_1.default.createElement(react_native_1.View, { style: styles.cameraContainer },
|
|
@@ -166,15 +176,11 @@ const CameraCapture = ({ mode, theme, onCapture, onCancel, }) => {
|
|
|
166
176
|
react_1.default.createElement(react_native_1.View, { style: [styles.corner, styles.cornerBottomRight] }))))),
|
|
167
177
|
react_1.default.createElement(react_native_1.View, { style: styles.instructionsContainer },
|
|
168
178
|
react_1.default.createElement(react_native_1.Text, { style: styles.instructionsText }, instructions),
|
|
169
|
-
react_1.default.createElement(react_native_1.Text, { style: styles.tipsText },
|
|
170
|
-
|
|
171
|
-
'\n',
|
|
172
|
-
"\u2022 Avoid glare and shadows",
|
|
173
|
-
'\n',
|
|
174
|
-
"\u2022 Keep document flat and complete")),
|
|
179
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.tipsText }, mode === 'front' ? strings.capture.frontId.tips : strings.capture.backId.tips ||
|
|
180
|
+
'• Ensure good lighting\n• Avoid glare and shadows\n• Keep document flat and complete')),
|
|
175
181
|
react_1.default.createElement(react_native_1.View, { style: styles.controls },
|
|
176
182
|
react_1.default.createElement(react_native_1.TouchableOpacity, { style: [styles.button, styles.cancelButton], onPress: onCancel },
|
|
177
|
-
react_1.default.createElement(react_native_1.Text, { style: styles.buttonText },
|
|
183
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.buttonText }, strings.common.cancel || 'Cancel')),
|
|
178
184
|
react_1.default.createElement(react_native_1.TouchableOpacity, { style: [
|
|
179
185
|
styles.button,
|
|
180
186
|
styles.captureButton,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"InstructionsScreen.d.ts","sourceRoot":"","sources":["../../src/components/InstructionsScreen.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAA8B,MAAM,OAAO,CAAC;AACnD,OAAO,EAML,SAAS,EACV,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAA2B,MAAM,oCAAoC,CAAC;AAE7G,MAAM,WAAW,uBAAuB;IACtC,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,MAAM,CAAC,EAAE;QACP,SAAS,CAAC,EAAE,SAAS,CAAC;QACtB,OAAO,CAAC,EAAE,SAAS,CAAC;KACrB,CAAC;CACH;AAED,eAAO,MAAM,kBAAkB,EAAE,KAAK,CAAC,EAAE,CAAC,uBAAuB,
|
|
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, 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, styles: customSty
|
|
|
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),
|
|
@@ -128,6 +131,22 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
128
131
|
flex: 1,
|
|
129
132
|
backgroundColor: '#FFFFFF',
|
|
130
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
|
+
},
|
|
131
150
|
content: {
|
|
132
151
|
padding: 24,
|
|
133
152
|
},
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Features challenge-response flow with guided head movements
|
|
4
4
|
*/
|
|
5
5
|
import React from 'react';
|
|
6
|
-
import { ThemeConfig, LivenessInstruction } from '@hexar/biometric-identity-sdk-core';
|
|
6
|
+
import { ThemeConfig, LivenessInstruction, SupportedLanguage } from '@hexar/biometric-identity-sdk-core';
|
|
7
7
|
export interface ChallengeAction {
|
|
8
8
|
action: string;
|
|
9
9
|
instruction: string;
|
|
@@ -13,6 +13,7 @@ export interface ChallengeAction {
|
|
|
13
13
|
}
|
|
14
14
|
export interface VideoRecorderProps {
|
|
15
15
|
theme?: ThemeConfig;
|
|
16
|
+
language?: SupportedLanguage;
|
|
16
17
|
/** Total recording duration in ms (default: 8000ms for smart mode) */
|
|
17
18
|
duration?: number;
|
|
18
19
|
/** Instructions for user to follow */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VideoRecorder.d.ts","sourceRoot":"","sources":["../../src/components/VideoRecorder.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAmD,MAAM,OAAO,CAAC;AAaxE,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,oCAAoC,CAAC;
|
|
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"}
|
|
@@ -42,6 +42,7 @@ const react_1 = __importStar(require("react"));
|
|
|
42
42
|
const react_native_1 = require("react-native");
|
|
43
43
|
const react_native_vision_camera_1 = require("react-native-vision-camera");
|
|
44
44
|
const react_native_permissions_1 = require("react-native-permissions");
|
|
45
|
+
const biometric_identity_sdk_core_1 = require("@hexar/biometric-identity-sdk-core");
|
|
45
46
|
// Default challenge set (used if backend not available)
|
|
46
47
|
const DEFAULT_CHALLENGES = [
|
|
47
48
|
{
|
|
@@ -73,20 +74,23 @@ const DEFAULT_CHALLENGES = [
|
|
|
73
74
|
icon: '😊',
|
|
74
75
|
},
|
|
75
76
|
];
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
|
|
77
|
+
const getInstructionMap = (strings) => ({
|
|
78
|
+
look_left: { text: strings.liveness.instructions.lookLeft || 'Slowly turn your head LEFT', icon: '←' },
|
|
79
|
+
look_right: { text: strings.liveness.instructions.lookRight || 'Slowly turn your head RIGHT', icon: '→' },
|
|
80
|
+
look_up: { text: strings.liveness.instructions.lookUp || 'Look UP', icon: '↑' },
|
|
81
|
+
look_down: { text: strings.liveness.instructions.lookDown || 'Look DOWN', icon: '↓' },
|
|
82
|
+
turn_head_left: { text: strings.liveness.instructions.turnHeadLeft || 'Turn your head LEFT', icon: '←' },
|
|
83
|
+
turn_head_right: { text: strings.liveness.instructions.turnHeadRight || 'Turn your head RIGHT', icon: '→' },
|
|
84
|
+
smile: { text: strings.liveness.instructions.smile || 'Smile 😊', icon: '😊' },
|
|
85
|
+
blink: { text: strings.liveness.instructions.blink || 'Blink your eyes naturally', icon: '👁' },
|
|
86
|
+
open_mouth: { text: strings.liveness.instructions.openMouth || 'Open your mouth slightly', icon: '😮' },
|
|
87
|
+
stay_still: { text: strings.liveness.instructions.stayStill || 'Look at the camera and stay still', icon: '📷' },
|
|
88
|
+
});
|
|
89
|
+
const VideoRecorder = ({ theme, language, duration, instructions, challenges: propChallenges, sessionId, smartMode = true, onComplete, onCancel, onFetchChallenges, }) => {
|
|
90
|
+
if (language) {
|
|
91
|
+
(0, biometric_identity_sdk_core_1.setLanguage)(language);
|
|
92
|
+
}
|
|
93
|
+
const strings = (0, biometric_identity_sdk_core_1.getStrings)();
|
|
90
94
|
// State
|
|
91
95
|
const [phase, setPhase] = (0, react_1.useState)('loading');
|
|
92
96
|
const [countdown, setCountdown] = (0, react_1.useState)(3);
|
|
@@ -160,13 +164,13 @@ const VideoRecorder = ({ theme, duration, instructions, challenges: propChalleng
|
|
|
160
164
|
challengeList = await onFetchChallenges();
|
|
161
165
|
}
|
|
162
166
|
else if (instructions && instructions.length > 0) {
|
|
163
|
-
|
|
167
|
+
const instructionMap = getInstructionMap(strings);
|
|
164
168
|
challengeList = instructions.map((inst, idx) => ({
|
|
165
169
|
action: inst,
|
|
166
|
-
instruction:
|
|
170
|
+
instruction: instructionMap[inst]?.text || inst,
|
|
167
171
|
duration_ms: 2000,
|
|
168
172
|
order: idx + 1,
|
|
169
|
-
icon:
|
|
173
|
+
icon: instructionMap[inst]?.icon,
|
|
170
174
|
}));
|
|
171
175
|
}
|
|
172
176
|
else {
|
|
@@ -270,31 +274,62 @@ const VideoRecorder = ({ theme, duration, instructions, challenges: propChalleng
|
|
|
270
274
|
react_native_1.Alert.alert('Recording Error', 'Failed to record video. Please try again.', [{ text: 'OK', onPress: onCancel }]);
|
|
271
275
|
}, [onCancel]);
|
|
272
276
|
const handleVideoComplete = (0, react_1.useCallback)(async (video) => {
|
|
273
|
-
|
|
274
|
-
if (
|
|
275
|
-
react_native_1.Alert.alert('
|
|
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');
|
|
276
280
|
return;
|
|
277
281
|
}
|
|
278
282
|
try {
|
|
279
|
-
|
|
280
|
-
|
|
283
|
+
setPhase('processing');
|
|
284
|
+
setOverallProgress(100);
|
|
285
|
+
const actualDuration = Date.now() - recordingStartTime.current;
|
|
286
|
+
react_native_1.Alert.alert('Video Processing', `Duration: ${(actualDuration / 1000).toFixed(1)}s\nMin required: ${(minDurationMs / 1000).toFixed(1)}s\nFrames: ${frames.length}`);
|
|
287
|
+
if (actualDuration < minDurationMs) {
|
|
288
|
+
setPhase('recording');
|
|
289
|
+
setOverallProgress(0);
|
|
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 }]);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
let videoBase64 = '';
|
|
294
|
+
if (video?.path) {
|
|
295
|
+
try {
|
|
296
|
+
const RNFS = require('react-native-fs');
|
|
297
|
+
videoBase64 = await RNFS.readFile(video.path, 'base64');
|
|
298
|
+
react_native_1.Alert.alert('Video File Read', `Successfully read video file\nSize: ${videoBase64.length} bytes`);
|
|
299
|
+
}
|
|
300
|
+
catch (fsError) {
|
|
301
|
+
react_native_1.Alert.alert('Video File Warning', `Could not read video file, using captured frames\nError: ${fsError}`);
|
|
302
|
+
}
|
|
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
|
+
}
|
|
281
311
|
const result = {
|
|
282
|
-
frames:
|
|
312
|
+
frames: finalFrames,
|
|
283
313
|
duration: actualDuration,
|
|
284
314
|
instructionsFollowed: completedChallenges.length === challenges.length,
|
|
285
|
-
qualityScore:
|
|
315
|
+
qualityScore: finalFrames.length > 0 ? Math.min(100, (finalFrames.length / 30) * 100) : 85,
|
|
286
316
|
challengesCompleted: completedChallenges,
|
|
287
317
|
sessionId,
|
|
288
318
|
};
|
|
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;
|
|
289
321
|
onComplete(result);
|
|
290
322
|
}
|
|
291
323
|
catch (error) {
|
|
292
|
-
|
|
324
|
+
react_native_1.Alert.alert('Error Processing Video', `Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
325
|
+
setPhase('recording');
|
|
326
|
+
setOverallProgress(0);
|
|
293
327
|
handleRecordingError(error);
|
|
294
328
|
}
|
|
295
|
-
}, [frames, completedChallenges, challenges, sessionId, onComplete, resetAndRetry, handleRecordingError]);
|
|
329
|
+
}, [frames, completedChallenges, challenges, sessionId, onComplete, resetAndRetry, handleRecordingError, strings, minDurationMs, phase]);
|
|
296
330
|
const startFrameCapture = (0, react_1.useCallback)(() => {
|
|
297
331
|
if (cameraRef.current && device) {
|
|
332
|
+
react_native_1.Alert.alert('Frame Capture', 'Starting frame capture mode');
|
|
298
333
|
frameCaptureInterval.current = setInterval(async () => {
|
|
299
334
|
try {
|
|
300
335
|
const photo = await cameraRef.current?.takePhoto({
|
|
@@ -322,41 +357,82 @@ const VideoRecorder = ({ theme, duration, instructions, challenges: propChalleng
|
|
|
322
357
|
}
|
|
323
358
|
}
|
|
324
359
|
catch (error) {
|
|
325
|
-
|
|
360
|
+
react_native_1.Alert.alert('Frame Capture Error', `Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
326
361
|
}
|
|
327
362
|
}, 100);
|
|
328
363
|
}
|
|
329
364
|
}, [device]);
|
|
330
365
|
const stopRecording = (0, react_1.useCallback)(async () => {
|
|
366
|
+
react_native_1.Alert.alert('Stop Recording', 'Stopping recording...');
|
|
367
|
+
if (frameCaptureInterval.current) {
|
|
368
|
+
clearInterval(frameCaptureInterval.current);
|
|
369
|
+
frameCaptureInterval.current = null;
|
|
370
|
+
}
|
|
331
371
|
if (videoRecordingRef.current) {
|
|
332
372
|
try {
|
|
333
|
-
|
|
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');
|
|
334
379
|
}
|
|
335
380
|
catch (error) {
|
|
336
|
-
|
|
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
|
+
}
|
|
337
396
|
}
|
|
338
|
-
videoRecordingRef.current = null;
|
|
339
397
|
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
frameCaptureInterval.current = null;
|
|
398
|
+
else {
|
|
399
|
+
isRecordingRef.current = false;
|
|
343
400
|
}
|
|
344
|
-
|
|
345
|
-
}, []);
|
|
401
|
+
}, [frames, completedChallenges, challenges, sessionId, onComplete, minDurationMs]);
|
|
346
402
|
const runChallenge = (0, react_1.useCallback)((index) => {
|
|
347
403
|
if (index >= challenges.length) {
|
|
404
|
+
react_native_1.Alert.alert('Challenges Complete', 'All challenges completed, stopping recording');
|
|
405
|
+
setOverallProgress(100);
|
|
348
406
|
if (isRecordingRef.current) {
|
|
349
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`);
|
|
350
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`);
|
|
351
412
|
setTimeout(() => {
|
|
352
413
|
if (isRecordingRef.current) {
|
|
353
414
|
stopRecording();
|
|
354
415
|
}
|
|
355
|
-
},
|
|
416
|
+
}, remaining);
|
|
356
417
|
return;
|
|
357
418
|
}
|
|
358
419
|
stopRecording();
|
|
359
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
|
+
}
|
|
360
436
|
return;
|
|
361
437
|
}
|
|
362
438
|
const challenge = challenges[index];
|
|
@@ -401,33 +477,41 @@ const VideoRecorder = ({ theme, duration, instructions, challenges: propChalleng
|
|
|
401
477
|
setPhase('recording');
|
|
402
478
|
recordingStartTime.current = Date.now();
|
|
403
479
|
isRecordingRef.current = true;
|
|
480
|
+
react_native_1.Alert.alert('Start Recording', `Starting video recording\nTotal duration: ${(totalDuration / 1000).toFixed(1)}s`);
|
|
404
481
|
if (cameraRef.current && device) {
|
|
405
482
|
try {
|
|
406
483
|
videoRecordingRef.current = await cameraRef.current.startRecording({
|
|
407
484
|
flash: 'off',
|
|
408
485
|
onRecordingFinished: (video) => {
|
|
486
|
+
react_native_1.Alert.alert('Recording Finished', `Video recording finished callback called\nPath: ${video?.path || 'N/A'}`);
|
|
409
487
|
handleVideoComplete(video);
|
|
410
488
|
},
|
|
411
489
|
onRecordingError: (error) => {
|
|
412
|
-
|
|
490
|
+
react_native_1.Alert.alert('Recording Error', `Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
413
491
|
handleRecordingError(error);
|
|
414
492
|
},
|
|
415
493
|
});
|
|
494
|
+
react_native_1.Alert.alert('Recording Started', 'Video recording started successfully');
|
|
416
495
|
}
|
|
417
496
|
catch (error) {
|
|
418
|
-
|
|
497
|
+
react_native_1.Alert.alert('Recording Fallback', `Video recording not available, falling back to frame capture\nError: ${error}`);
|
|
419
498
|
startFrameCapture();
|
|
420
499
|
}
|
|
421
500
|
}
|
|
422
501
|
else {
|
|
502
|
+
react_native_1.Alert.alert('Camera Not Available', 'Camera not available, using frame capture');
|
|
423
503
|
startFrameCapture();
|
|
424
504
|
}
|
|
425
505
|
runChallenge(0);
|
|
426
|
-
setTimeout(() => {
|
|
506
|
+
const timeoutId = setTimeout(() => {
|
|
507
|
+
react_native_1.Alert.alert('Timeout', 'Recording timeout reached, stopping recording');
|
|
427
508
|
if (isRecordingRef.current) {
|
|
428
|
-
stopRecording()
|
|
509
|
+
stopRecording().catch(err => {
|
|
510
|
+
react_native_1.Alert.alert('Timeout Error', `Error stopping recording on timeout: ${err}`);
|
|
511
|
+
});
|
|
429
512
|
}
|
|
430
513
|
}, totalDuration);
|
|
514
|
+
return () => clearTimeout(timeoutId);
|
|
431
515
|
}, [device, totalDuration, handleVideoComplete, handleRecordingError, runChallenge, stopRecording, startFrameCapture]);
|
|
432
516
|
// Current challenge
|
|
433
517
|
const currentChallenge = challenges[currentChallengeIndex];
|
|
@@ -465,16 +549,18 @@ const VideoRecorder = ({ theme, duration, instructions, challenges: propChalleng
|
|
|
465
549
|
if (!hasPermission) {
|
|
466
550
|
return (react_1.default.createElement(react_native_1.View, { style: styles.container },
|
|
467
551
|
react_1.default.createElement(react_native_1.View, { style: styles.permissionContainer },
|
|
468
|
-
react_1.default.createElement(react_native_1.Text, { style: styles.permissionText },
|
|
552
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.permissionText }, typeof strings.errors.cameraPermissionDenied === 'string'
|
|
553
|
+
? strings.errors.cameraPermissionDenied
|
|
554
|
+
: strings.errors.cameraPermissionDenied?.message || 'Camera permission is required'),
|
|
469
555
|
react_1.default.createElement(react_native_1.TouchableOpacity, { style: [styles.cancelButton, { borderColor: theme?.errorColor || '#EF4444' }], onPress: onCancel },
|
|
470
|
-
react_1.default.createElement(react_native_1.Text, { style: [styles.cancelButtonText, { color: theme?.errorColor || '#EF4444' }] },
|
|
556
|
+
react_1.default.createElement(react_native_1.Text, { style: [styles.cancelButtonText, { color: theme?.errorColor || '#EF4444' }] }, strings.common.cancel || 'Cancel')))));
|
|
471
557
|
}
|
|
472
558
|
if (!device) {
|
|
473
559
|
return (react_1.default.createElement(react_native_1.View, { style: styles.container },
|
|
474
560
|
react_1.default.createElement(react_native_1.View, { style: styles.permissionContainer },
|
|
475
|
-
react_1.default.createElement(react_native_1.Text, { style: styles.permissionText },
|
|
561
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.permissionText }, strings.errors.cameraNotAvailable || 'Camera not available'),
|
|
476
562
|
react_1.default.createElement(react_native_1.TouchableOpacity, { style: [styles.cancelButton, { borderColor: theme?.errorColor || '#EF4444' }], onPress: onCancel },
|
|
477
|
-
react_1.default.createElement(react_native_1.Text, { style: [styles.cancelButtonText, { color: theme?.errorColor || '#EF4444' }] },
|
|
563
|
+
react_1.default.createElement(react_native_1.Text, { style: [styles.cancelButtonText, { color: theme?.errorColor || '#EF4444' }] }, strings.common.cancel || 'Cancel')))));
|
|
478
564
|
}
|
|
479
565
|
return (react_1.default.createElement(react_native_1.View, { style: styles.container },
|
|
480
566
|
react_1.default.createElement(react_native_1.View, { style: styles.cameraContainer },
|
|
@@ -489,9 +575,9 @@ const VideoRecorder = ({ theme, duration, instructions, challenges: propChalleng
|
|
|
489
575
|
] }),
|
|
490
576
|
phase === 'recording' && getDirectionIndicator()),
|
|
491
577
|
phase === 'loading' && (react_1.default.createElement(react_native_1.View, { style: styles.centeredOverlay },
|
|
492
|
-
react_1.default.createElement(react_native_1.Text, { style: styles.loadingText },
|
|
578
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.loadingText }, strings.liveness.preparing || 'Preparing challenges...'))),
|
|
493
579
|
phase === 'countdown' && (react_1.default.createElement(react_native_1.View, { style: styles.countdownContainer },
|
|
494
|
-
react_1.default.createElement(react_native_1.Text, { style: styles.getReadyText },
|
|
580
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.getReadyText }, strings.liveness.getReady || 'Get Ready!'),
|
|
495
581
|
react_1.default.createElement(react_native_1.Animated.Text, { style: [
|
|
496
582
|
styles.countdownText,
|
|
497
583
|
{ transform: [{ scale: scaleAnim }] }
|
|
@@ -501,7 +587,7 @@ const VideoRecorder = ({ theme, duration, instructions, challenges: propChalleng
|
|
|
501
587
|
styles.recordingDot,
|
|
502
588
|
{ transform: [{ scale: pulseAnim }] }
|
|
503
589
|
] }),
|
|
504
|
-
react_1.default.createElement(react_native_1.Text, { style: styles.recordingText },
|
|
590
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.recordingText }, strings.liveness.recording || 'Recording'))),
|
|
505
591
|
(phase === 'recording' || phase === 'processing') && (react_1.default.createElement(react_native_1.View, { style: styles.progressContainer },
|
|
506
592
|
react_1.default.createElement(react_native_1.View, { style: styles.progressBar },
|
|
507
593
|
react_1.default.createElement(react_native_1.View, { style: [
|
|
@@ -535,21 +621,14 @@ const VideoRecorder = ({ theme, duration, instructions, challenges: propChalleng
|
|
|
535
621
|
" / ",
|
|
536
622
|
challenges.length))),
|
|
537
623
|
phase === 'processing' && (react_1.default.createElement(react_native_1.View, { style: styles.processingOverlay },
|
|
538
|
-
react_1.default.createElement(react_native_1.Text, { style: styles.processingText },
|
|
624
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.processingText }, strings.liveness.processing || 'Processing video...')))),
|
|
539
625
|
react_1.default.createElement(react_native_1.View, { style: styles.bottomContainer },
|
|
540
626
|
phase === 'countdown' && (react_1.default.createElement(react_1.default.Fragment, null,
|
|
541
|
-
react_1.default.createElement(react_native_1.Text, { style: styles.bottomText },
|
|
542
|
-
"You'll perform ",
|
|
543
|
-
challenges.length,
|
|
544
|
-
" action",
|
|
545
|
-
challenges.length > 1 ? 's' : '',
|
|
546
|
-
".",
|
|
547
|
-
'\n',
|
|
548
|
-
"Follow the on-screen instructions."),
|
|
627
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.bottomText }, strings.liveness.countdownMessage || `You'll perform ${challenges.length} action${challenges.length > 1 ? 's' : ''}.\nFollow the on-screen instructions.`),
|
|
549
628
|
react_1.default.createElement(react_native_1.TouchableOpacity, { style: [styles.cancelButton, { borderColor: theme?.errorColor || '#EF4444' }], onPress: onCancel },
|
|
550
|
-
react_1.default.createElement(react_native_1.Text, { style: [styles.cancelButtonText, { color: theme?.errorColor || '#EF4444' }] },
|
|
551
|
-
phase === 'recording' && (react_1.default.createElement(react_native_1.Text, { style: styles.bottomText },
|
|
552
|
-
phase === 'processing' && (react_1.default.createElement(react_native_1.Text, { style: styles.bottomText },
|
|
629
|
+
react_1.default.createElement(react_native_1.Text, { style: [styles.cancelButtonText, { color: theme?.errorColor || '#EF4444' }] }, strings.common.cancel || 'Cancel')))),
|
|
630
|
+
phase === 'recording' && (react_1.default.createElement(react_native_1.Text, { style: styles.bottomText }, strings.liveness.recordingInstructions || 'Keep your face visible and follow the instructions')),
|
|
631
|
+
phase === 'processing' && (react_1.default.createElement(react_native_1.Text, { style: styles.bottomText }, strings.validation.almostDone || 'Almost done...')))));
|
|
553
632
|
};
|
|
554
633
|
exports.VideoRecorder = VideoRecorder;
|
|
555
634
|
const styles = react_native_1.StyleSheet.create({
|
package/package.json
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
ValidationResult,
|
|
18
18
|
ThemeConfig,
|
|
19
19
|
BiometricError,
|
|
20
|
+
BiometricErrorCode,
|
|
20
21
|
SDKStep,
|
|
21
22
|
getStrings,
|
|
22
23
|
setLanguage,
|
|
@@ -38,6 +39,7 @@ export interface BiometricIdentityFlowProps {
|
|
|
38
39
|
language?: SupportedLanguage;
|
|
39
40
|
customTranslations?: Record<string, string>;
|
|
40
41
|
smartLivenessMode?: boolean;
|
|
42
|
+
routeBack?: () => void;
|
|
41
43
|
styles?: {
|
|
42
44
|
container?: ViewStyle;
|
|
43
45
|
content?: ViewStyle;
|
|
@@ -51,6 +53,7 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
|
|
|
51
53
|
language,
|
|
52
54
|
customTranslations,
|
|
53
55
|
smartLivenessMode = true,
|
|
56
|
+
routeBack,
|
|
54
57
|
styles: customStyles,
|
|
55
58
|
}) => {
|
|
56
59
|
const {
|
|
@@ -193,6 +196,7 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
|
|
|
193
196
|
theme={theme}
|
|
194
197
|
language={language}
|
|
195
198
|
onStart={() => setShowInstructions(false)}
|
|
199
|
+
routeBack={routeBack}
|
|
196
200
|
styles={customStyles}
|
|
197
201
|
/>
|
|
198
202
|
);
|
|
@@ -204,6 +208,7 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
|
|
|
204
208
|
return (
|
|
205
209
|
<VideoRecorder
|
|
206
210
|
theme={theme}
|
|
211
|
+
language={language}
|
|
207
212
|
challenges={currentChallenges}
|
|
208
213
|
smartMode={smartLivenessMode}
|
|
209
214
|
sessionId={sdk.getSessionId() || undefined}
|
|
@@ -221,6 +226,7 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
|
|
|
221
226
|
<CameraCapture
|
|
222
227
|
mode={cameraMode}
|
|
223
228
|
theme={theme}
|
|
229
|
+
language={language}
|
|
224
230
|
onCapture={handleCaptureComplete}
|
|
225
231
|
onCancel={() => setShowCamera(false)}
|
|
226
232
|
/>
|
|
@@ -15,13 +15,14 @@ import {
|
|
|
15
15
|
} from 'react-native';
|
|
16
16
|
import { Camera, useCameraDevice, useCameraPermission } from 'react-native-vision-camera';
|
|
17
17
|
import { request, PERMISSIONS, RESULTS } from 'react-native-permissions';
|
|
18
|
-
import { ThemeConfig } from '@hexar/biometric-identity-sdk-core';
|
|
18
|
+
import { ThemeConfig, SupportedLanguage, getStrings, setLanguage } from '@hexar/biometric-identity-sdk-core';
|
|
19
19
|
|
|
20
20
|
const { width, height } = Dimensions.get('window');
|
|
21
21
|
|
|
22
22
|
export interface CameraCaptureProps {
|
|
23
23
|
mode: 'front' | 'back';
|
|
24
24
|
theme?: ThemeConfig;
|
|
25
|
+
language?: SupportedLanguage;
|
|
25
26
|
onCapture: (imageData: string) => void;
|
|
26
27
|
onCancel: () => void;
|
|
27
28
|
}
|
|
@@ -29,6 +30,7 @@ export interface CameraCaptureProps {
|
|
|
29
30
|
export const CameraCapture: React.FC<CameraCaptureProps> = ({
|
|
30
31
|
mode,
|
|
31
32
|
theme,
|
|
33
|
+
language,
|
|
32
34
|
onCapture,
|
|
33
35
|
onCancel,
|
|
34
36
|
}) => {
|
|
@@ -37,6 +39,11 @@ export const CameraCapture: React.FC<CameraCaptureProps> = ({
|
|
|
37
39
|
const cameraRef = useRef<Camera>(null);
|
|
38
40
|
const { hasPermission: cameraPermission, requestPermission } = useCameraPermission();
|
|
39
41
|
|
|
42
|
+
if (language) {
|
|
43
|
+
setLanguage(language);
|
|
44
|
+
}
|
|
45
|
+
const strings = getStrings();
|
|
46
|
+
|
|
40
47
|
// Get camera device (back camera for document capture)
|
|
41
48
|
const device = useCameraDevice('back');
|
|
42
49
|
|
|
@@ -69,9 +76,12 @@ export const CameraCapture: React.FC<CameraCaptureProps> = ({
|
|
|
69
76
|
} catch (error) {
|
|
70
77
|
console.error('Permission check error:', error);
|
|
71
78
|
setHasPermission(false);
|
|
79
|
+
const errorMsg = typeof strings.errors.cameraPermissionDenied === 'string'
|
|
80
|
+
? strings.errors.cameraPermissionDenied
|
|
81
|
+
: strings.errors.cameraPermissionDenied?.message || 'Please enable camera access in your device settings to capture your ID document.';
|
|
72
82
|
Alert.alert(
|
|
73
83
|
'Camera Permission Required',
|
|
74
|
-
|
|
84
|
+
errorMsg
|
|
75
85
|
);
|
|
76
86
|
}
|
|
77
87
|
};
|
|
@@ -133,25 +143,29 @@ export const CameraCapture: React.FC<CameraCaptureProps> = ({
|
|
|
133
143
|
};
|
|
134
144
|
|
|
135
145
|
const instructions = mode === 'front'
|
|
136
|
-
?
|
|
137
|
-
:
|
|
146
|
+
? strings.capture.frontId.instruction
|
|
147
|
+
: strings.capture.backId.instruction;
|
|
138
148
|
|
|
139
149
|
if (!hasPermission) {
|
|
140
150
|
return (
|
|
141
151
|
<View style={styles.container}>
|
|
142
152
|
<View style={styles.permissionContainer}>
|
|
143
|
-
<Text style={styles.permissionText}>
|
|
153
|
+
<Text style={styles.permissionText}>
|
|
154
|
+
{typeof strings.errors.cameraPermissionDenied === 'string'
|
|
155
|
+
? strings.errors.cameraPermissionDenied
|
|
156
|
+
: strings.errors.cameraPermissionDenied?.message || 'Camera permission is required'}
|
|
157
|
+
</Text>
|
|
144
158
|
<TouchableOpacity
|
|
145
159
|
style={[styles.button, { backgroundColor: theme?.primaryColor || '#6366F1' }]}
|
|
146
160
|
onPress={checkPermissions}
|
|
147
161
|
>
|
|
148
|
-
<Text style={styles.buttonText}>Grant Permission</Text>
|
|
162
|
+
<Text style={styles.buttonText}>{strings.common.grantPermission || 'Grant Permission'}</Text>
|
|
149
163
|
</TouchableOpacity>
|
|
150
164
|
<TouchableOpacity
|
|
151
165
|
style={[styles.button, styles.cancelButton]}
|
|
152
166
|
onPress={onCancel}
|
|
153
167
|
>
|
|
154
|
-
<Text style={styles.buttonText}>Cancel</Text>
|
|
168
|
+
<Text style={styles.buttonText}>{strings.common.cancel || 'Cancel'}</Text>
|
|
155
169
|
</TouchableOpacity>
|
|
156
170
|
</View>
|
|
157
171
|
</View>
|
|
@@ -162,12 +176,12 @@ export const CameraCapture: React.FC<CameraCaptureProps> = ({
|
|
|
162
176
|
return (
|
|
163
177
|
<View style={styles.container}>
|
|
164
178
|
<View style={styles.permissionContainer}>
|
|
165
|
-
<Text style={styles.permissionText}>Camera not available</Text>
|
|
179
|
+
<Text style={styles.permissionText}>{strings.errors.cameraNotAvailable || 'Camera not available'}</Text>
|
|
166
180
|
<TouchableOpacity
|
|
167
181
|
style={[styles.button, styles.cancelButton]}
|
|
168
182
|
onPress={onCancel}
|
|
169
183
|
>
|
|
170
|
-
<Text style={styles.buttonText}>Cancel</Text>
|
|
184
|
+
<Text style={styles.buttonText}>{strings.common.cancel || 'Cancel'}</Text>
|
|
171
185
|
</TouchableOpacity>
|
|
172
186
|
</View>
|
|
173
187
|
</View>
|
|
@@ -203,9 +217,8 @@ export const CameraCapture: React.FC<CameraCaptureProps> = ({
|
|
|
203
217
|
<View style={styles.instructionsContainer}>
|
|
204
218
|
<Text style={styles.instructionsText}>{instructions}</Text>
|
|
205
219
|
<Text style={styles.tipsText}>
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
• Keep document flat and complete
|
|
220
|
+
{mode === 'front' ? strings.capture.frontId.tips : strings.capture.backId.tips ||
|
|
221
|
+
'• Ensure good lighting\n• Avoid glare and shadows\n• Keep document flat and complete'}
|
|
209
222
|
</Text>
|
|
210
223
|
</View>
|
|
211
224
|
|
|
@@ -215,7 +228,7 @@ export const CameraCapture: React.FC<CameraCaptureProps> = ({
|
|
|
215
228
|
style={[styles.button, styles.cancelButton]}
|
|
216
229
|
onPress={onCancel}
|
|
217
230
|
>
|
|
218
|
-
<Text style={styles.buttonText}>Cancel</Text>
|
|
231
|
+
<Text style={styles.buttonText}>{strings.common.cancel || 'Cancel'}</Text>
|
|
219
232
|
</TouchableOpacity>
|
|
220
233
|
|
|
221
234
|
<TouchableOpacity
|
|
@@ -18,6 +18,7 @@ export interface InstructionsScreenProps {
|
|
|
18
18
|
theme?: ThemeConfig;
|
|
19
19
|
language?: SupportedLanguage;
|
|
20
20
|
onStart: () => void;
|
|
21
|
+
routeBack?: () => void;
|
|
21
22
|
styles?: {
|
|
22
23
|
container?: ViewStyle;
|
|
23
24
|
content?: ViewStyle;
|
|
@@ -28,6 +29,7 @@ export const InstructionsScreen: React.FC<InstructionsScreenProps> = ({
|
|
|
28
29
|
theme,
|
|
29
30
|
language = 'en',
|
|
30
31
|
onStart,
|
|
32
|
+
routeBack,
|
|
31
33
|
styles: customStyles,
|
|
32
34
|
}) => {
|
|
33
35
|
const [strings, setStrings] = useState(() => {
|
|
@@ -53,6 +55,18 @@ export const InstructionsScreen: React.FC<InstructionsScreenProps> = ({
|
|
|
53
55
|
|
|
54
56
|
return (
|
|
55
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
|
+
)}
|
|
56
70
|
<ScrollView contentContainerStyle={[styles.content, customStyles?.content]}>
|
|
57
71
|
<Text style={[styles.title, { color: theme?.textColor || '#000000' }]}>
|
|
58
72
|
{strings.instructions.title}
|
|
@@ -188,6 +202,22 @@ const styles = StyleSheet.create({
|
|
|
188
202
|
flex: 1,
|
|
189
203
|
backgroundColor: '#FFFFFF',
|
|
190
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
|
+
},
|
|
191
221
|
content: {
|
|
192
222
|
padding: 24,
|
|
193
223
|
},
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
} from 'react-native';
|
|
17
17
|
import { Camera, useCameraDevice, useCameraPermission } from 'react-native-vision-camera';
|
|
18
18
|
import { request, PERMISSIONS, RESULTS } from 'react-native-permissions';
|
|
19
|
-
import { ThemeConfig, LivenessInstruction } from '@hexar/biometric-identity-sdk-core';
|
|
19
|
+
import { ThemeConfig, LivenessInstruction, SupportedLanguage, getStrings, setLanguage } from '@hexar/biometric-identity-sdk-core';
|
|
20
20
|
|
|
21
21
|
// Challenge action configuration (matches backend response)
|
|
22
22
|
export interface ChallengeAction {
|
|
@@ -29,6 +29,7 @@ export interface ChallengeAction {
|
|
|
29
29
|
|
|
30
30
|
export interface VideoRecorderProps {
|
|
31
31
|
theme?: ThemeConfig;
|
|
32
|
+
language?: SupportedLanguage;
|
|
32
33
|
/** Total recording duration in ms (default: 8000ms for smart mode) */
|
|
33
34
|
duration?: number;
|
|
34
35
|
/** Instructions for user to follow */
|
|
@@ -88,22 +89,22 @@ const DEFAULT_CHALLENGES: ChallengeAction[] = [
|
|
|
88
89
|
},
|
|
89
90
|
];
|
|
90
91
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
};
|
|
92
|
+
const getInstructionMap = (strings: any): Record<string, { text: string; icon: string }> => ({
|
|
93
|
+
look_left: { text: strings.liveness.instructions.lookLeft || 'Slowly turn your head LEFT', icon: '←' },
|
|
94
|
+
look_right: { text: strings.liveness.instructions.lookRight || 'Slowly turn your head RIGHT', icon: '→' },
|
|
95
|
+
look_up: { text: strings.liveness.instructions.lookUp || 'Look UP', icon: '↑' },
|
|
96
|
+
look_down: { text: strings.liveness.instructions.lookDown || 'Look DOWN', icon: '↓' },
|
|
97
|
+
turn_head_left: { text: strings.liveness.instructions.turnHeadLeft || 'Turn your head LEFT', icon: '←' },
|
|
98
|
+
turn_head_right: { text: strings.liveness.instructions.turnHeadRight || 'Turn your head RIGHT', icon: '→' },
|
|
99
|
+
smile: { text: strings.liveness.instructions.smile || 'Smile 😊', icon: '😊' },
|
|
100
|
+
blink: { text: strings.liveness.instructions.blink || 'Blink your eyes naturally', icon: '👁' },
|
|
101
|
+
open_mouth: { text: strings.liveness.instructions.openMouth || 'Open your mouth slightly', icon: '😮' },
|
|
102
|
+
stay_still: { text: strings.liveness.instructions.stayStill || 'Look at the camera and stay still', icon: '📷' },
|
|
103
|
+
});
|
|
104
104
|
|
|
105
105
|
export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
106
106
|
theme,
|
|
107
|
+
language,
|
|
107
108
|
duration,
|
|
108
109
|
instructions,
|
|
109
110
|
challenges: propChallenges,
|
|
@@ -113,6 +114,11 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
113
114
|
onCancel,
|
|
114
115
|
onFetchChallenges,
|
|
115
116
|
}) => {
|
|
117
|
+
if (language) {
|
|
118
|
+
setLanguage(language);
|
|
119
|
+
}
|
|
120
|
+
const strings = getStrings();
|
|
121
|
+
|
|
116
122
|
// State
|
|
117
123
|
const [phase, setPhase] = useState<'loading' | 'countdown' | 'recording' | 'processing'>('loading');
|
|
118
124
|
const [countdown, setCountdown] = useState(3);
|
|
@@ -197,13 +203,13 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
197
203
|
// Fetch from backend
|
|
198
204
|
challengeList = await onFetchChallenges();
|
|
199
205
|
} else if (instructions && instructions.length > 0) {
|
|
200
|
-
|
|
206
|
+
const instructionMap = getInstructionMap(strings);
|
|
201
207
|
challengeList = instructions.map((inst, idx) => ({
|
|
202
208
|
action: inst,
|
|
203
|
-
instruction:
|
|
209
|
+
instruction: instructionMap[inst]?.text || inst,
|
|
204
210
|
duration_ms: 2000,
|
|
205
211
|
order: idx + 1,
|
|
206
|
-
icon:
|
|
212
|
+
icon: instructionMap[inst]?.icon,
|
|
207
213
|
}));
|
|
208
214
|
} else {
|
|
209
215
|
// Use default challenges
|
|
@@ -322,39 +328,81 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
322
328
|
}, [onCancel]);
|
|
323
329
|
|
|
324
330
|
const handleVideoComplete = useCallback(async (video: any) => {
|
|
325
|
-
|
|
331
|
+
Alert.alert('Video Complete', `handleVideoComplete called\nVideo path: ${video?.path || 'N/A'}`);
|
|
326
332
|
|
|
327
|
-
if (
|
|
328
|
-
Alert.alert(
|
|
329
|
-
'Recording Too Short',
|
|
330
|
-
`Video must be at least ${minDurationMs / 1000} seconds. Please try again.`,
|
|
331
|
-
[{ text: 'OK', onPress: resetAndRetry }]
|
|
332
|
-
);
|
|
333
|
+
if (!isRecordingRef.current && phase !== 'processing') {
|
|
334
|
+
Alert.alert('Info', 'Video already processed, ignoring duplicate callback');
|
|
333
335
|
return;
|
|
334
336
|
}
|
|
335
|
-
|
|
337
|
+
|
|
336
338
|
try {
|
|
337
|
-
|
|
338
|
-
|
|
339
|
+
setPhase('processing');
|
|
340
|
+
setOverallProgress(100);
|
|
341
|
+
|
|
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
|
+
);
|
|
347
|
+
|
|
348
|
+
if (actualDuration < minDurationMs) {
|
|
349
|
+
setPhase('recording');
|
|
350
|
+
setOverallProgress(0);
|
|
351
|
+
Alert.alert(
|
|
352
|
+
strings.errors.videoTooShort?.title || 'Recording Too Short',
|
|
353
|
+
strings.errors.videoTooShort?.message || `Video must be at least ${minDurationMs / 1000} seconds. Please try again.`,
|
|
354
|
+
[{ text: strings.common.retry || 'OK', onPress: resetAndRetry }]
|
|
355
|
+
);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
let videoBase64 = '';
|
|
360
|
+
if (video?.path) {
|
|
361
|
+
try {
|
|
362
|
+
const RNFS = require('react-native-fs');
|
|
363
|
+
videoBase64 = await RNFS.readFile(video.path, 'base64');
|
|
364
|
+
Alert.alert('Video File Read', `Successfully read video file\nSize: ${videoBase64.length} bytes`);
|
|
365
|
+
} catch (fsError) {
|
|
366
|
+
Alert.alert('Video File Warning', `Could not read video file, using captured frames\nError: ${fsError}`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
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
|
+
}
|
|
339
378
|
|
|
340
379
|
const result: VideoRecordingResult = {
|
|
341
|
-
frames:
|
|
380
|
+
frames: finalFrames,
|
|
342
381
|
duration: actualDuration,
|
|
343
382
|
instructionsFollowed: completedChallenges.length === challenges.length,
|
|
344
|
-
qualityScore:
|
|
383
|
+
qualityScore: finalFrames.length > 0 ? Math.min(100, (finalFrames.length / 30) * 100) : 85,
|
|
345
384
|
challengesCompleted: completedChallenges,
|
|
346
385
|
sessionId,
|
|
347
386
|
};
|
|
348
387
|
|
|
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
|
+
);
|
|
392
|
+
|
|
393
|
+
isRecordingRef.current = false;
|
|
349
394
|
onComplete(result);
|
|
350
395
|
} catch (error) {
|
|
351
|
-
|
|
396
|
+
Alert.alert('Error Processing Video', `Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
397
|
+
setPhase('recording');
|
|
398
|
+
setOverallProgress(0);
|
|
352
399
|
handleRecordingError(error);
|
|
353
400
|
}
|
|
354
|
-
}, [frames, completedChallenges, challenges, sessionId, onComplete, resetAndRetry, handleRecordingError]);
|
|
401
|
+
}, [frames, completedChallenges, challenges, sessionId, onComplete, resetAndRetry, handleRecordingError, strings, minDurationMs, phase]);
|
|
355
402
|
|
|
356
403
|
const startFrameCapture = useCallback(() => {
|
|
357
404
|
if (cameraRef.current && device) {
|
|
405
|
+
Alert.alert('Frame Capture', 'Starting frame capture mode');
|
|
358
406
|
frameCaptureInterval.current = setInterval(async () => {
|
|
359
407
|
try {
|
|
360
408
|
const photo = await cameraRef.current?.takePhoto({
|
|
@@ -381,43 +429,90 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
381
429
|
}
|
|
382
430
|
}
|
|
383
431
|
} catch (error) {
|
|
384
|
-
|
|
432
|
+
Alert.alert('Frame Capture Error', `Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
385
433
|
}
|
|
386
434
|
}, 100);
|
|
387
435
|
}
|
|
388
436
|
}, [device]);
|
|
389
437
|
|
|
390
438
|
const stopRecording = useCallback(async () => {
|
|
391
|
-
|
|
392
|
-
try {
|
|
393
|
-
await videoRecordingRef.current.stop();
|
|
394
|
-
} catch (error) {
|
|
395
|
-
console.error('Error stopping video recording:', error);
|
|
396
|
-
}
|
|
397
|
-
videoRecordingRef.current = null;
|
|
398
|
-
}
|
|
439
|
+
Alert.alert('Stop Recording', 'Stopping recording...');
|
|
399
440
|
|
|
400
441
|
if (frameCaptureInterval.current) {
|
|
401
442
|
clearInterval(frameCaptureInterval.current);
|
|
402
443
|
frameCaptureInterval.current = null;
|
|
403
444
|
}
|
|
404
445
|
|
|
405
|
-
|
|
406
|
-
|
|
446
|
+
if (videoRecordingRef.current) {
|
|
447
|
+
try {
|
|
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');
|
|
455
|
+
} catch (error) {
|
|
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
|
+
}
|
|
472
|
+
}
|
|
473
|
+
} else {
|
|
474
|
+
isRecordingRef.current = false;
|
|
475
|
+
}
|
|
476
|
+
}, [frames, completedChallenges, challenges, sessionId, onComplete, minDurationMs]);
|
|
407
477
|
|
|
408
478
|
const runChallenge = useCallback((index: number) => {
|
|
409
479
|
if (index >= challenges.length) {
|
|
480
|
+
Alert.alert('Challenges Complete', 'All challenges completed, stopping recording');
|
|
481
|
+
setOverallProgress(100);
|
|
482
|
+
|
|
410
483
|
if (isRecordingRef.current) {
|
|
411
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
|
+
|
|
412
490
|
if (elapsed < minDurationMs) {
|
|
491
|
+
const remaining = minDurationMs - elapsed;
|
|
492
|
+
Alert.alert('Waiting', `Waiting additional ${(remaining / 1000).toFixed(1)}s to meet minimum duration`);
|
|
413
493
|
setTimeout(() => {
|
|
414
494
|
if (isRecordingRef.current) {
|
|
415
495
|
stopRecording();
|
|
416
496
|
}
|
|
417
|
-
},
|
|
497
|
+
}, remaining);
|
|
418
498
|
return;
|
|
419
499
|
}
|
|
500
|
+
|
|
420
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
|
+
}
|
|
421
516
|
}
|
|
422
517
|
return;
|
|
423
518
|
}
|
|
@@ -474,33 +569,43 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
474
569
|
recordingStartTime.current = Date.now();
|
|
475
570
|
isRecordingRef.current = true;
|
|
476
571
|
|
|
572
|
+
Alert.alert('Start Recording', `Starting video recording\nTotal duration: ${(totalDuration / 1000).toFixed(1)}s`);
|
|
573
|
+
|
|
477
574
|
if (cameraRef.current && device) {
|
|
478
575
|
try {
|
|
479
576
|
videoRecordingRef.current = await cameraRef.current.startRecording({
|
|
480
577
|
flash: 'off',
|
|
481
578
|
onRecordingFinished: (video: any) => {
|
|
579
|
+
Alert.alert('Recording Finished', `Video recording finished callback called\nPath: ${video?.path || 'N/A'}`);
|
|
482
580
|
handleVideoComplete(video);
|
|
483
581
|
},
|
|
484
582
|
onRecordingError: (error: any) => {
|
|
485
|
-
|
|
583
|
+
Alert.alert('Recording Error', `Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
486
584
|
handleRecordingError(error);
|
|
487
585
|
},
|
|
488
586
|
});
|
|
587
|
+
Alert.alert('Recording Started', 'Video recording started successfully');
|
|
489
588
|
} catch (error) {
|
|
490
|
-
|
|
589
|
+
Alert.alert('Recording Fallback', `Video recording not available, falling back to frame capture\nError: ${error}`);
|
|
491
590
|
startFrameCapture();
|
|
492
591
|
}
|
|
493
592
|
} else {
|
|
593
|
+
Alert.alert('Camera Not Available', 'Camera not available, using frame capture');
|
|
494
594
|
startFrameCapture();
|
|
495
595
|
}
|
|
496
596
|
|
|
497
597
|
runChallenge(0);
|
|
498
598
|
|
|
499
|
-
setTimeout(() => {
|
|
599
|
+
const timeoutId = setTimeout(() => {
|
|
600
|
+
Alert.alert('Timeout', 'Recording timeout reached, stopping recording');
|
|
500
601
|
if (isRecordingRef.current) {
|
|
501
|
-
stopRecording()
|
|
602
|
+
stopRecording().catch(err => {
|
|
603
|
+
Alert.alert('Timeout Error', `Error stopping recording on timeout: ${err}`);
|
|
604
|
+
});
|
|
502
605
|
}
|
|
503
606
|
}, totalDuration);
|
|
607
|
+
|
|
608
|
+
return () => clearTimeout(timeoutId);
|
|
504
609
|
}, [device, totalDuration, handleVideoComplete, handleRecordingError, runChallenge, stopRecording, startFrameCapture]);
|
|
505
610
|
|
|
506
611
|
// Current challenge
|
|
@@ -563,13 +668,17 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
563
668
|
return (
|
|
564
669
|
<View style={styles.container}>
|
|
565
670
|
<View style={styles.permissionContainer}>
|
|
566
|
-
<Text style={styles.permissionText}>
|
|
671
|
+
<Text style={styles.permissionText}>
|
|
672
|
+
{typeof strings.errors.cameraPermissionDenied === 'string'
|
|
673
|
+
? strings.errors.cameraPermissionDenied
|
|
674
|
+
: strings.errors.cameraPermissionDenied?.message || 'Camera permission is required'}
|
|
675
|
+
</Text>
|
|
567
676
|
<TouchableOpacity
|
|
568
677
|
style={[styles.cancelButton, { borderColor: theme?.errorColor || '#EF4444' }]}
|
|
569
678
|
onPress={onCancel}
|
|
570
679
|
>
|
|
571
680
|
<Text style={[styles.cancelButtonText, { color: theme?.errorColor || '#EF4444' }]}>
|
|
572
|
-
Cancel
|
|
681
|
+
{strings.common.cancel || 'Cancel'}
|
|
573
682
|
</Text>
|
|
574
683
|
</TouchableOpacity>
|
|
575
684
|
</View>
|
|
@@ -581,13 +690,15 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
581
690
|
return (
|
|
582
691
|
<View style={styles.container}>
|
|
583
692
|
<View style={styles.permissionContainer}>
|
|
584
|
-
<Text style={styles.permissionText}>
|
|
693
|
+
<Text style={styles.permissionText}>
|
|
694
|
+
{strings.errors.cameraNotAvailable || 'Camera not available'}
|
|
695
|
+
</Text>
|
|
585
696
|
<TouchableOpacity
|
|
586
697
|
style={[styles.cancelButton, { borderColor: theme?.errorColor || '#EF4444' }]}
|
|
587
698
|
onPress={onCancel}
|
|
588
699
|
>
|
|
589
700
|
<Text style={[styles.cancelButtonText, { color: theme?.errorColor || '#EF4444' }]}>
|
|
590
|
-
Cancel
|
|
701
|
+
{strings.common.cancel || 'Cancel'}
|
|
591
702
|
</Text>
|
|
592
703
|
</TouchableOpacity>
|
|
593
704
|
</View>
|
|
@@ -627,14 +738,14 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
627
738
|
{/* Loading Phase */}
|
|
628
739
|
{phase === 'loading' && (
|
|
629
740
|
<View style={styles.centeredOverlay}>
|
|
630
|
-
<Text style={styles.loadingText}>Preparing challenges
|
|
741
|
+
<Text style={styles.loadingText}>{strings.liveness.preparing || 'Preparing challenges...'}</Text>
|
|
631
742
|
</View>
|
|
632
743
|
)}
|
|
633
744
|
|
|
634
745
|
{/* Countdown */}
|
|
635
746
|
{phase === 'countdown' && (
|
|
636
747
|
<View style={styles.countdownContainer}>
|
|
637
|
-
<Text style={styles.getReadyText}>Get Ready
|
|
748
|
+
<Text style={styles.getReadyText}>{strings.liveness.getReady || 'Get Ready!'}</Text>
|
|
638
749
|
<Animated.Text
|
|
639
750
|
style={[
|
|
640
751
|
styles.countdownText,
|
|
@@ -655,7 +766,7 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
655
766
|
{ transform: [{ scale: pulseAnim }] }
|
|
656
767
|
]}
|
|
657
768
|
/>
|
|
658
|
-
<Text style={styles.recordingText}>Recording</Text>
|
|
769
|
+
<Text style={styles.recordingText}>{strings.liveness.recording || 'Recording'}</Text>
|
|
659
770
|
</View>
|
|
660
771
|
)}
|
|
661
772
|
|
|
@@ -723,7 +834,7 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
723
834
|
{/* Processing Overlay */}
|
|
724
835
|
{phase === 'processing' && (
|
|
725
836
|
<View style={styles.processingOverlay}>
|
|
726
|
-
<Text style={styles.processingText}>Processing video
|
|
837
|
+
<Text style={styles.processingText}>{strings.liveness.processing || 'Processing video...'}</Text>
|
|
727
838
|
</View>
|
|
728
839
|
)}
|
|
729
840
|
</View>
|
|
@@ -733,15 +844,14 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
733
844
|
{phase === 'countdown' && (
|
|
734
845
|
<>
|
|
735
846
|
<Text style={styles.bottomText}>
|
|
736
|
-
You'll perform {challenges.length} action{challenges.length > 1 ? 's' : ''}
|
|
737
|
-
Follow the on-screen instructions.
|
|
847
|
+
{strings.liveness.countdownMessage || `You'll perform ${challenges.length} action${challenges.length > 1 ? 's' : ''}.\nFollow the on-screen instructions.`}
|
|
738
848
|
</Text>
|
|
739
849
|
<TouchableOpacity
|
|
740
850
|
style={[styles.cancelButton, { borderColor: theme?.errorColor || '#EF4444' }]}
|
|
741
851
|
onPress={onCancel}
|
|
742
852
|
>
|
|
743
853
|
<Text style={[styles.cancelButtonText, { color: theme?.errorColor || '#EF4444' }]}>
|
|
744
|
-
Cancel
|
|
854
|
+
{strings.common.cancel || 'Cancel'}
|
|
745
855
|
</Text>
|
|
746
856
|
</TouchableOpacity>
|
|
747
857
|
</>
|
|
@@ -749,13 +859,13 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
749
859
|
|
|
750
860
|
{phase === 'recording' && (
|
|
751
861
|
<Text style={styles.bottomText}>
|
|
752
|
-
Keep your face visible and follow the instructions
|
|
862
|
+
{strings.liveness.recordingInstructions || 'Keep your face visible and follow the instructions'}
|
|
753
863
|
</Text>
|
|
754
864
|
)}
|
|
755
865
|
|
|
756
866
|
{phase === 'processing' && (
|
|
757
867
|
<Text style={styles.bottomText}>
|
|
758
|
-
Almost done...
|
|
868
|
+
{strings.validation.almostDone || 'Almost done...'}
|
|
759
869
|
</Text>
|
|
760
870
|
)}
|
|
761
871
|
</View>
|