@hexar/biometric-identity-sdk-react-native 1.0.7 → 1.0.8
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.map +1 -1
- package/dist/components/BiometricIdentityFlow.js +7 -3
- 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 +18 -1
- package/dist/components/VideoRecorder.d.ts +2 -1
- package/dist/components/VideoRecorder.d.ts.map +1 -1
- package/dist/components/VideoRecorder.js +81 -49
- package/package.json +1 -1
- package/src/components/BiometricIdentityFlow.tsx +8 -0
- package/src/components/CameraCapture.tsx +26 -13
- package/src/components/InstructionsScreen.tsx +28 -1
- package/src/components/VideoRecorder.tsx +97 -49
|
@@ -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,MAAM,CAAC,EAAE;QACP,SAAS,CAAC,EAAE,SAAS,CAAC;QACtB,OAAO,CAAC,EAAE,SAAS,CAAC;KACrB,CAAC;CACH;AAED,eAAO,MAAM,qBAAqB,EAAE,KAAK,CAAC,EAAE,CAAC,0BAA0B,CAoTtE,CAAC;AAiOF,eAAe,qBAAqB,CAAC"}
|
|
@@ -158,17 +158,21 @@ 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),
|
|
161
|
+
return (react_1.default.createElement(InstructionsScreen_1.InstructionsScreen, { theme: theme, language: language, onStart: () => setShowInstructions(false), onCancel: onError ? () => onError({
|
|
162
|
+
name: 'BiometricError',
|
|
163
|
+
message: 'User cancelled',
|
|
164
|
+
code: biometric_identity_sdk_core_1.BiometricErrorCode.USER_CANCELLED,
|
|
165
|
+
}) : undefined, styles: customStyles }));
|
|
162
166
|
}
|
|
163
167
|
// Show camera/video recorder
|
|
164
168
|
if (showCamera) {
|
|
165
169
|
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 () => {
|
|
170
|
+
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
171
|
const challenges = await fetchChallenges('active');
|
|
168
172
|
return challenges;
|
|
169
173
|
} }));
|
|
170
174
|
}
|
|
171
|
-
return (react_1.default.createElement(CameraCapture_1.CameraCapture, { mode: cameraMode, theme: theme, onCapture: handleCaptureComplete, onCancel: () => setShowCamera(false) }));
|
|
175
|
+
return (react_1.default.createElement(CameraCapture_1.CameraCapture, { mode: cameraMode, theme: theme, language: language, onCapture: handleCaptureComplete, onCancel: () => setShowCamera(false) }));
|
|
172
176
|
}
|
|
173
177
|
// Show validation progress
|
|
174
178
|
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,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,MAAM,CAAC,EAAE;QACP,SAAS,CAAC,EAAE,SAAS,CAAC;QACtB,OAAO,CAAC,EAAE,SAAS,CAAC;KACrB,CAAC;CACH;AAED,eAAO,MAAM,kBAAkB,EAAE,KAAK,CAAC,EAAE,CAAC,uBAAuB,CA4GhE,CAAC;AAiMF,eAAe,kBAAkB,CAAC"}
|
|
@@ -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, onCancel, styles: customStyles, }) => {
|
|
45
45
|
const [strings, setStrings] = (0, react_1.useState)(() => {
|
|
46
46
|
// Set initial language
|
|
47
47
|
if (language) {
|
|
@@ -76,9 +76,16 @@ const InstructionsScreen = ({ theme, language = 'en', onStart, styles: customSty
|
|
|
76
76
|
react_1.default.createElement(react_native_1.View, { style: styles.privacyContainer },
|
|
77
77
|
react_1.default.createElement(react_native_1.Text, { style: styles.privacyText }, privacyContent))),
|
|
78
78
|
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'))),
|
|
79
85
|
react_1.default.createElement(react_native_1.TouchableOpacity, { style: [
|
|
80
86
|
styles.button,
|
|
81
87
|
{ backgroundColor: theme?.primaryColor || '#6366F1' },
|
|
88
|
+
onCancel && styles.startButton,
|
|
82
89
|
], onPress: onStart },
|
|
83
90
|
react_1.default.createElement(react_native_1.Text, { style: styles.buttonText }, strings.instructions.startButton)))));
|
|
84
91
|
};
|
|
@@ -175,12 +182,22 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
175
182
|
padding: 24,
|
|
176
183
|
borderTopWidth: 1,
|
|
177
184
|
borderTopColor: '#E5E7EB',
|
|
185
|
+
flexDirection: 'row',
|
|
186
|
+
gap: 12,
|
|
178
187
|
},
|
|
179
188
|
button: {
|
|
180
189
|
paddingVertical: 16,
|
|
181
190
|
paddingHorizontal: 32,
|
|
182
191
|
borderRadius: 8,
|
|
183
192
|
alignItems: 'center',
|
|
193
|
+
flex: 1,
|
|
194
|
+
},
|
|
195
|
+
cancelButton: {
|
|
196
|
+
borderWidth: 2,
|
|
197
|
+
backgroundColor: 'transparent',
|
|
198
|
+
},
|
|
199
|
+
startButton: {
|
|
200
|
+
flex: 1,
|
|
184
201
|
},
|
|
185
202
|
buttonText: {
|
|
186
203
|
color: '#FFFFFF',
|
|
@@ -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,CAmsBtD,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,29 +274,49 @@ 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 (actualDuration < minDurationMs) {
|
|
275
|
-
react_native_1.Alert.alert('Recording Too Short', `Video must be at least ${minDurationMs / 1000} seconds. Please try again.`, [{ text: 'OK', onPress: resetAndRetry }]);
|
|
276
|
-
return;
|
|
277
|
-
}
|
|
277
|
+
console.log('handleVideoComplete called with video:', video?.path);
|
|
278
278
|
try {
|
|
279
|
-
|
|
280
|
-
const
|
|
279
|
+
setPhase('processing');
|
|
280
|
+
const actualDuration = Date.now() - recordingStartTime.current;
|
|
281
|
+
console.log('Video duration:', actualDuration, 'min required:', minDurationMs);
|
|
282
|
+
if (actualDuration < minDurationMs) {
|
|
283
|
+
setPhase('recording');
|
|
284
|
+
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
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
let videoBase64 = '';
|
|
288
|
+
if (video?.path) {
|
|
289
|
+
try {
|
|
290
|
+
const RNFS = require('react-native-fs');
|
|
291
|
+
videoBase64 = await RNFS.readFile(video.path, 'base64');
|
|
292
|
+
console.log('Video file read successfully, size:', videoBase64.length);
|
|
293
|
+
}
|
|
294
|
+
catch (fsError) {
|
|
295
|
+
console.warn('Could not read video file, using captured frames:', fsError);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
281
298
|
const result = {
|
|
282
|
-
frames: frames.length > 0 ? frames : [videoBase64],
|
|
299
|
+
frames: frames.length > 0 ? frames : (videoBase64 ? [videoBase64] : []),
|
|
283
300
|
duration: actualDuration,
|
|
284
301
|
instructionsFollowed: completedChallenges.length === challenges.length,
|
|
285
302
|
qualityScore: frames.length > 0 ? Math.min(100, (frames.length / 30) * 100) : 85,
|
|
286
303
|
challengesCompleted: completedChallenges,
|
|
287
304
|
sessionId,
|
|
288
305
|
};
|
|
306
|
+
console.log('Video recording completed successfully:', {
|
|
307
|
+
duration: actualDuration,
|
|
308
|
+
frames: result.frames.length,
|
|
309
|
+
challengesCompleted: completedChallenges.length,
|
|
310
|
+
instructionsFollowed: result.instructionsFollowed
|
|
311
|
+
});
|
|
289
312
|
onComplete(result);
|
|
290
313
|
}
|
|
291
314
|
catch (error) {
|
|
292
315
|
console.error('Error processing video:', error);
|
|
316
|
+
setPhase('recording');
|
|
293
317
|
handleRecordingError(error);
|
|
294
318
|
}
|
|
295
|
-
}, [frames, completedChallenges, challenges, sessionId, onComplete, resetAndRetry, handleRecordingError]);
|
|
319
|
+
}, [frames, completedChallenges, challenges, sessionId, onComplete, resetAndRetry, handleRecordingError, strings, minDurationMs]);
|
|
296
320
|
const startFrameCapture = (0, react_1.useCallback)(() => {
|
|
297
321
|
if (cameraRef.current && device) {
|
|
298
322
|
frameCaptureInterval.current = setInterval(async () => {
|
|
@@ -328,9 +352,13 @@ const VideoRecorder = ({ theme, duration, instructions, challenges: propChalleng
|
|
|
328
352
|
}
|
|
329
353
|
}, [device]);
|
|
330
354
|
const stopRecording = (0, react_1.useCallback)(async () => {
|
|
355
|
+
console.log('Stopping recording...');
|
|
356
|
+
isRecordingRef.current = false;
|
|
331
357
|
if (videoRecordingRef.current) {
|
|
332
358
|
try {
|
|
359
|
+
console.log('Stopping video recording');
|
|
333
360
|
await videoRecordingRef.current.stop();
|
|
361
|
+
console.log('Video recording stopped');
|
|
334
362
|
}
|
|
335
363
|
catch (error) {
|
|
336
364
|
console.error('Error stopping video recording:', error);
|
|
@@ -341,7 +369,6 @@ const VideoRecorder = ({ theme, duration, instructions, challenges: propChalleng
|
|
|
341
369
|
clearInterval(frameCaptureInterval.current);
|
|
342
370
|
frameCaptureInterval.current = null;
|
|
343
371
|
}
|
|
344
|
-
isRecordingRef.current = false;
|
|
345
372
|
}, []);
|
|
346
373
|
const runChallenge = (0, react_1.useCallback)((index) => {
|
|
347
374
|
if (index >= challenges.length) {
|
|
@@ -401,18 +428,23 @@ const VideoRecorder = ({ theme, duration, instructions, challenges: propChalleng
|
|
|
401
428
|
setPhase('recording');
|
|
402
429
|
recordingStartTime.current = Date.now();
|
|
403
430
|
isRecordingRef.current = true;
|
|
431
|
+
console.log('Starting video recording, total duration:', totalDuration);
|
|
404
432
|
if (cameraRef.current && device) {
|
|
405
433
|
try {
|
|
406
434
|
videoRecordingRef.current = await cameraRef.current.startRecording({
|
|
407
435
|
flash: 'off',
|
|
408
436
|
onRecordingFinished: (video) => {
|
|
409
|
-
|
|
437
|
+
console.log('Video recording finished callback called', video);
|
|
438
|
+
if (isRecordingRef.current) {
|
|
439
|
+
handleVideoComplete(video);
|
|
440
|
+
}
|
|
410
441
|
},
|
|
411
442
|
onRecordingError: (error) => {
|
|
412
443
|
console.error('Recording error:', error);
|
|
413
444
|
handleRecordingError(error);
|
|
414
445
|
},
|
|
415
446
|
});
|
|
447
|
+
console.log('Video recording started successfully');
|
|
416
448
|
}
|
|
417
449
|
catch (error) {
|
|
418
450
|
console.warn('Video recording not available, falling back to frame capture:', error);
|
|
@@ -420,14 +452,19 @@ const VideoRecorder = ({ theme, duration, instructions, challenges: propChalleng
|
|
|
420
452
|
}
|
|
421
453
|
}
|
|
422
454
|
else {
|
|
455
|
+
console.log('Camera not available, using frame capture');
|
|
423
456
|
startFrameCapture();
|
|
424
457
|
}
|
|
425
458
|
runChallenge(0);
|
|
426
|
-
setTimeout(() => {
|
|
459
|
+
const timeoutId = setTimeout(() => {
|
|
460
|
+
console.log('Recording timeout reached, stopping recording');
|
|
427
461
|
if (isRecordingRef.current) {
|
|
428
|
-
stopRecording()
|
|
462
|
+
stopRecording().catch(err => {
|
|
463
|
+
console.error('Error stopping recording on timeout:', err);
|
|
464
|
+
});
|
|
429
465
|
}
|
|
430
466
|
}, totalDuration);
|
|
467
|
+
return () => clearTimeout(timeoutId);
|
|
431
468
|
}, [device, totalDuration, handleVideoComplete, handleRecordingError, runChallenge, stopRecording, startFrameCapture]);
|
|
432
469
|
// Current challenge
|
|
433
470
|
const currentChallenge = challenges[currentChallengeIndex];
|
|
@@ -465,16 +502,18 @@ const VideoRecorder = ({ theme, duration, instructions, challenges: propChalleng
|
|
|
465
502
|
if (!hasPermission) {
|
|
466
503
|
return (react_1.default.createElement(react_native_1.View, { style: styles.container },
|
|
467
504
|
react_1.default.createElement(react_native_1.View, { style: styles.permissionContainer },
|
|
468
|
-
react_1.default.createElement(react_native_1.Text, { style: styles.permissionText },
|
|
505
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.permissionText }, typeof strings.errors.cameraPermissionDenied === 'string'
|
|
506
|
+
? strings.errors.cameraPermissionDenied
|
|
507
|
+
: strings.errors.cameraPermissionDenied?.message || 'Camera permission is required'),
|
|
469
508
|
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' }] },
|
|
509
|
+
react_1.default.createElement(react_native_1.Text, { style: [styles.cancelButtonText, { color: theme?.errorColor || '#EF4444' }] }, strings.common.cancel || 'Cancel')))));
|
|
471
510
|
}
|
|
472
511
|
if (!device) {
|
|
473
512
|
return (react_1.default.createElement(react_native_1.View, { style: styles.container },
|
|
474
513
|
react_1.default.createElement(react_native_1.View, { style: styles.permissionContainer },
|
|
475
|
-
react_1.default.createElement(react_native_1.Text, { style: styles.permissionText },
|
|
514
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.permissionText }, strings.errors.cameraNotAvailable || 'Camera not available'),
|
|
476
515
|
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' }] },
|
|
516
|
+
react_1.default.createElement(react_native_1.Text, { style: [styles.cancelButtonText, { color: theme?.errorColor || '#EF4444' }] }, strings.common.cancel || 'Cancel')))));
|
|
478
517
|
}
|
|
479
518
|
return (react_1.default.createElement(react_native_1.View, { style: styles.container },
|
|
480
519
|
react_1.default.createElement(react_native_1.View, { style: styles.cameraContainer },
|
|
@@ -489,9 +528,9 @@ const VideoRecorder = ({ theme, duration, instructions, challenges: propChalleng
|
|
|
489
528
|
] }),
|
|
490
529
|
phase === 'recording' && getDirectionIndicator()),
|
|
491
530
|
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 },
|
|
531
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.loadingText }, strings.liveness.preparing || 'Preparing challenges...'))),
|
|
493
532
|
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 },
|
|
533
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.getReadyText }, strings.liveness.getReady || 'Get Ready!'),
|
|
495
534
|
react_1.default.createElement(react_native_1.Animated.Text, { style: [
|
|
496
535
|
styles.countdownText,
|
|
497
536
|
{ transform: [{ scale: scaleAnim }] }
|
|
@@ -501,7 +540,7 @@ const VideoRecorder = ({ theme, duration, instructions, challenges: propChalleng
|
|
|
501
540
|
styles.recordingDot,
|
|
502
541
|
{ transform: [{ scale: pulseAnim }] }
|
|
503
542
|
] }),
|
|
504
|
-
react_1.default.createElement(react_native_1.Text, { style: styles.recordingText },
|
|
543
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.recordingText }, strings.liveness.recording || 'Recording'))),
|
|
505
544
|
(phase === 'recording' || phase === 'processing') && (react_1.default.createElement(react_native_1.View, { style: styles.progressContainer },
|
|
506
545
|
react_1.default.createElement(react_native_1.View, { style: styles.progressBar },
|
|
507
546
|
react_1.default.createElement(react_native_1.View, { style: [
|
|
@@ -535,21 +574,14 @@ const VideoRecorder = ({ theme, duration, instructions, challenges: propChalleng
|
|
|
535
574
|
" / ",
|
|
536
575
|
challenges.length))),
|
|
537
576
|
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 },
|
|
577
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.processingText }, strings.liveness.processing || 'Processing video...')))),
|
|
539
578
|
react_1.default.createElement(react_native_1.View, { style: styles.bottomContainer },
|
|
540
579
|
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."),
|
|
580
|
+
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
581
|
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 },
|
|
582
|
+
react_1.default.createElement(react_native_1.Text, { style: [styles.cancelButtonText, { color: theme?.errorColor || '#EF4444' }] }, strings.common.cancel || 'Cancel')))),
|
|
583
|
+
phase === 'recording' && (react_1.default.createElement(react_native_1.Text, { style: styles.bottomText }, strings.liveness.recordingInstructions || 'Keep your face visible and follow the instructions')),
|
|
584
|
+
phase === 'processing' && (react_1.default.createElement(react_native_1.Text, { style: styles.bottomText }, strings.validation.almostDone || 'Almost done...')))));
|
|
553
585
|
};
|
|
554
586
|
exports.VideoRecorder = VideoRecorder;
|
|
555
587
|
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,
|
|
@@ -193,6 +194,11 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
|
|
|
193
194
|
theme={theme}
|
|
194
195
|
language={language}
|
|
195
196
|
onStart={() => setShowInstructions(false)}
|
|
197
|
+
onCancel={onError ? () => onError({
|
|
198
|
+
name: 'BiometricError',
|
|
199
|
+
message: 'User cancelled',
|
|
200
|
+
code: BiometricErrorCode.USER_CANCELLED,
|
|
201
|
+
} as BiometricError) : undefined}
|
|
196
202
|
styles={customStyles}
|
|
197
203
|
/>
|
|
198
204
|
);
|
|
@@ -204,6 +210,7 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
|
|
|
204
210
|
return (
|
|
205
211
|
<VideoRecorder
|
|
206
212
|
theme={theme}
|
|
213
|
+
language={language}
|
|
207
214
|
challenges={currentChallenges}
|
|
208
215
|
smartMode={smartLivenessMode}
|
|
209
216
|
sessionId={sdk.getSessionId() || undefined}
|
|
@@ -221,6 +228,7 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
|
|
|
221
228
|
<CameraCapture
|
|
222
229
|
mode={cameraMode}
|
|
223
230
|
theme={theme}
|
|
231
|
+
language={language}
|
|
224
232
|
onCapture={handleCaptureComplete}
|
|
225
233
|
onCancel={() => setShowCamera(false)}
|
|
226
234
|
/>
|
|
@@ -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
|
+
onCancel?: () => 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
|
+
onCancel,
|
|
31
33
|
styles: customStyles,
|
|
32
34
|
}) => {
|
|
33
35
|
const [strings, setStrings] = useState(() => {
|
|
@@ -102,12 +104,27 @@ export const InstructionsScreen: React.FC<InstructionsScreenProps> = ({
|
|
|
102
104
|
</View>
|
|
103
105
|
</ScrollView>
|
|
104
106
|
|
|
105
|
-
{/*
|
|
107
|
+
{/* Footer Buttons */}
|
|
106
108
|
<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
|
+
)}
|
|
107
123
|
<TouchableOpacity
|
|
108
124
|
style={[
|
|
109
125
|
styles.button,
|
|
110
126
|
{ backgroundColor: theme?.primaryColor || '#6366F1' },
|
|
127
|
+
onCancel && styles.startButton,
|
|
111
128
|
]}
|
|
112
129
|
onPress={onStart}
|
|
113
130
|
>
|
|
@@ -235,12 +252,22 @@ const styles = StyleSheet.create({
|
|
|
235
252
|
padding: 24,
|
|
236
253
|
borderTopWidth: 1,
|
|
237
254
|
borderTopColor: '#E5E7EB',
|
|
255
|
+
flexDirection: 'row',
|
|
256
|
+
gap: 12,
|
|
238
257
|
},
|
|
239
258
|
button: {
|
|
240
259
|
paddingVertical: 16,
|
|
241
260
|
paddingHorizontal: 32,
|
|
242
261
|
borderRadius: 8,
|
|
243
262
|
alignItems: 'center',
|
|
263
|
+
flex: 1,
|
|
264
|
+
},
|
|
265
|
+
cancelButton: {
|
|
266
|
+
borderWidth: 2,
|
|
267
|
+
backgroundColor: 'transparent',
|
|
268
|
+
},
|
|
269
|
+
startButton: {
|
|
270
|
+
flex: 1,
|
|
244
271
|
},
|
|
245
272
|
buttonText: {
|
|
246
273
|
color: '#FFFFFF',
|
|
@@ -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,23 +328,37 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
322
328
|
}, [onCancel]);
|
|
323
329
|
|
|
324
330
|
const handleVideoComplete = useCallback(async (video: any) => {
|
|
325
|
-
|
|
331
|
+
console.log('handleVideoComplete called with video:', video?.path);
|
|
326
332
|
|
|
327
|
-
if (actualDuration < minDurationMs) {
|
|
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
|
-
return;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
333
|
try {
|
|
337
|
-
|
|
338
|
-
const
|
|
334
|
+
setPhase('processing');
|
|
335
|
+
const actualDuration = Date.now() - recordingStartTime.current;
|
|
336
|
+
|
|
337
|
+
console.log('Video duration:', actualDuration, 'min required:', minDurationMs);
|
|
338
|
+
|
|
339
|
+
if (actualDuration < minDurationMs) {
|
|
340
|
+
setPhase('recording');
|
|
341
|
+
Alert.alert(
|
|
342
|
+
strings.errors.videoTooShort?.title || 'Recording Too Short',
|
|
343
|
+
strings.errors.videoTooShort?.message || `Video must be at least ${minDurationMs / 1000} seconds. Please try again.`,
|
|
344
|
+
[{ text: strings.common.retry || 'OK', onPress: resetAndRetry }]
|
|
345
|
+
);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
let videoBase64 = '';
|
|
350
|
+
if (video?.path) {
|
|
351
|
+
try {
|
|
352
|
+
const RNFS = require('react-native-fs');
|
|
353
|
+
videoBase64 = await RNFS.readFile(video.path, 'base64');
|
|
354
|
+
console.log('Video file read successfully, size:', videoBase64.length);
|
|
355
|
+
} catch (fsError) {
|
|
356
|
+
console.warn('Could not read video file, using captured frames:', fsError);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
339
359
|
|
|
340
360
|
const result: VideoRecordingResult = {
|
|
341
|
-
frames: frames.length > 0 ? frames : [videoBase64],
|
|
361
|
+
frames: frames.length > 0 ? frames : (videoBase64 ? [videoBase64] : []),
|
|
342
362
|
duration: actualDuration,
|
|
343
363
|
instructionsFollowed: completedChallenges.length === challenges.length,
|
|
344
364
|
qualityScore: frames.length > 0 ? Math.min(100, (frames.length / 30) * 100) : 85,
|
|
@@ -346,12 +366,20 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
346
366
|
sessionId,
|
|
347
367
|
};
|
|
348
368
|
|
|
369
|
+
console.log('Video recording completed successfully:', {
|
|
370
|
+
duration: actualDuration,
|
|
371
|
+
frames: result.frames.length,
|
|
372
|
+
challengesCompleted: completedChallenges.length,
|
|
373
|
+
instructionsFollowed: result.instructionsFollowed
|
|
374
|
+
});
|
|
375
|
+
|
|
349
376
|
onComplete(result);
|
|
350
377
|
} catch (error) {
|
|
351
378
|
console.error('Error processing video:', error);
|
|
379
|
+
setPhase('recording');
|
|
352
380
|
handleRecordingError(error);
|
|
353
381
|
}
|
|
354
|
-
}, [frames, completedChallenges, challenges, sessionId, onComplete, resetAndRetry, handleRecordingError]);
|
|
382
|
+
}, [frames, completedChallenges, challenges, sessionId, onComplete, resetAndRetry, handleRecordingError, strings, minDurationMs]);
|
|
355
383
|
|
|
356
384
|
const startFrameCapture = useCallback(() => {
|
|
357
385
|
if (cameraRef.current && device) {
|
|
@@ -388,9 +416,14 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
388
416
|
}, [device]);
|
|
389
417
|
|
|
390
418
|
const stopRecording = useCallback(async () => {
|
|
419
|
+
console.log('Stopping recording...');
|
|
420
|
+
isRecordingRef.current = false;
|
|
421
|
+
|
|
391
422
|
if (videoRecordingRef.current) {
|
|
392
423
|
try {
|
|
424
|
+
console.log('Stopping video recording');
|
|
393
425
|
await videoRecordingRef.current.stop();
|
|
426
|
+
console.log('Video recording stopped');
|
|
394
427
|
} catch (error) {
|
|
395
428
|
console.error('Error stopping video recording:', error);
|
|
396
429
|
}
|
|
@@ -401,8 +434,6 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
401
434
|
clearInterval(frameCaptureInterval.current);
|
|
402
435
|
frameCaptureInterval.current = null;
|
|
403
436
|
}
|
|
404
|
-
|
|
405
|
-
isRecordingRef.current = false;
|
|
406
437
|
}, []);
|
|
407
438
|
|
|
408
439
|
const runChallenge = useCallback((index: number) => {
|
|
@@ -474,33 +505,45 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
474
505
|
recordingStartTime.current = Date.now();
|
|
475
506
|
isRecordingRef.current = true;
|
|
476
507
|
|
|
508
|
+
console.log('Starting video recording, total duration:', totalDuration);
|
|
509
|
+
|
|
477
510
|
if (cameraRef.current && device) {
|
|
478
511
|
try {
|
|
479
512
|
videoRecordingRef.current = await cameraRef.current.startRecording({
|
|
480
513
|
flash: 'off',
|
|
481
514
|
onRecordingFinished: (video: any) => {
|
|
482
|
-
|
|
515
|
+
console.log('Video recording finished callback called', video);
|
|
516
|
+
if (isRecordingRef.current) {
|
|
517
|
+
handleVideoComplete(video);
|
|
518
|
+
}
|
|
483
519
|
},
|
|
484
520
|
onRecordingError: (error: any) => {
|
|
485
521
|
console.error('Recording error:', error);
|
|
486
522
|
handleRecordingError(error);
|
|
487
523
|
},
|
|
488
524
|
});
|
|
525
|
+
console.log('Video recording started successfully');
|
|
489
526
|
} catch (error) {
|
|
490
527
|
console.warn('Video recording not available, falling back to frame capture:', error);
|
|
491
528
|
startFrameCapture();
|
|
492
529
|
}
|
|
493
530
|
} else {
|
|
531
|
+
console.log('Camera not available, using frame capture');
|
|
494
532
|
startFrameCapture();
|
|
495
533
|
}
|
|
496
534
|
|
|
497
535
|
runChallenge(0);
|
|
498
536
|
|
|
499
|
-
setTimeout(() => {
|
|
537
|
+
const timeoutId = setTimeout(() => {
|
|
538
|
+
console.log('Recording timeout reached, stopping recording');
|
|
500
539
|
if (isRecordingRef.current) {
|
|
501
|
-
stopRecording()
|
|
540
|
+
stopRecording().catch(err => {
|
|
541
|
+
console.error('Error stopping recording on timeout:', err);
|
|
542
|
+
});
|
|
502
543
|
}
|
|
503
544
|
}, totalDuration);
|
|
545
|
+
|
|
546
|
+
return () => clearTimeout(timeoutId);
|
|
504
547
|
}, [device, totalDuration, handleVideoComplete, handleRecordingError, runChallenge, stopRecording, startFrameCapture]);
|
|
505
548
|
|
|
506
549
|
// Current challenge
|
|
@@ -563,13 +606,17 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
563
606
|
return (
|
|
564
607
|
<View style={styles.container}>
|
|
565
608
|
<View style={styles.permissionContainer}>
|
|
566
|
-
<Text style={styles.permissionText}>
|
|
609
|
+
<Text style={styles.permissionText}>
|
|
610
|
+
{typeof strings.errors.cameraPermissionDenied === 'string'
|
|
611
|
+
? strings.errors.cameraPermissionDenied
|
|
612
|
+
: strings.errors.cameraPermissionDenied?.message || 'Camera permission is required'}
|
|
613
|
+
</Text>
|
|
567
614
|
<TouchableOpacity
|
|
568
615
|
style={[styles.cancelButton, { borderColor: theme?.errorColor || '#EF4444' }]}
|
|
569
616
|
onPress={onCancel}
|
|
570
617
|
>
|
|
571
618
|
<Text style={[styles.cancelButtonText, { color: theme?.errorColor || '#EF4444' }]}>
|
|
572
|
-
Cancel
|
|
619
|
+
{strings.common.cancel || 'Cancel'}
|
|
573
620
|
</Text>
|
|
574
621
|
</TouchableOpacity>
|
|
575
622
|
</View>
|
|
@@ -581,13 +628,15 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
581
628
|
return (
|
|
582
629
|
<View style={styles.container}>
|
|
583
630
|
<View style={styles.permissionContainer}>
|
|
584
|
-
<Text style={styles.permissionText}>
|
|
631
|
+
<Text style={styles.permissionText}>
|
|
632
|
+
{strings.errors.cameraNotAvailable || 'Camera not available'}
|
|
633
|
+
</Text>
|
|
585
634
|
<TouchableOpacity
|
|
586
635
|
style={[styles.cancelButton, { borderColor: theme?.errorColor || '#EF4444' }]}
|
|
587
636
|
onPress={onCancel}
|
|
588
637
|
>
|
|
589
638
|
<Text style={[styles.cancelButtonText, { color: theme?.errorColor || '#EF4444' }]}>
|
|
590
|
-
Cancel
|
|
639
|
+
{strings.common.cancel || 'Cancel'}
|
|
591
640
|
</Text>
|
|
592
641
|
</TouchableOpacity>
|
|
593
642
|
</View>
|
|
@@ -627,14 +676,14 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
627
676
|
{/* Loading Phase */}
|
|
628
677
|
{phase === 'loading' && (
|
|
629
678
|
<View style={styles.centeredOverlay}>
|
|
630
|
-
<Text style={styles.loadingText}>Preparing challenges
|
|
679
|
+
<Text style={styles.loadingText}>{strings.liveness.preparing || 'Preparing challenges...'}</Text>
|
|
631
680
|
</View>
|
|
632
681
|
)}
|
|
633
682
|
|
|
634
683
|
{/* Countdown */}
|
|
635
684
|
{phase === 'countdown' && (
|
|
636
685
|
<View style={styles.countdownContainer}>
|
|
637
|
-
<Text style={styles.getReadyText}>Get Ready
|
|
686
|
+
<Text style={styles.getReadyText}>{strings.liveness.getReady || 'Get Ready!'}</Text>
|
|
638
687
|
<Animated.Text
|
|
639
688
|
style={[
|
|
640
689
|
styles.countdownText,
|
|
@@ -655,7 +704,7 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
655
704
|
{ transform: [{ scale: pulseAnim }] }
|
|
656
705
|
]}
|
|
657
706
|
/>
|
|
658
|
-
<Text style={styles.recordingText}>Recording</Text>
|
|
707
|
+
<Text style={styles.recordingText}>{strings.liveness.recording || 'Recording'}</Text>
|
|
659
708
|
</View>
|
|
660
709
|
)}
|
|
661
710
|
|
|
@@ -723,7 +772,7 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
723
772
|
{/* Processing Overlay */}
|
|
724
773
|
{phase === 'processing' && (
|
|
725
774
|
<View style={styles.processingOverlay}>
|
|
726
|
-
<Text style={styles.processingText}>Processing video
|
|
775
|
+
<Text style={styles.processingText}>{strings.liveness.processing || 'Processing video...'}</Text>
|
|
727
776
|
</View>
|
|
728
777
|
)}
|
|
729
778
|
</View>
|
|
@@ -733,15 +782,14 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
733
782
|
{phase === 'countdown' && (
|
|
734
783
|
<>
|
|
735
784
|
<Text style={styles.bottomText}>
|
|
736
|
-
You'll perform {challenges.length} action{challenges.length > 1 ? 's' : ''}
|
|
737
|
-
Follow the on-screen instructions.
|
|
785
|
+
{strings.liveness.countdownMessage || `You'll perform ${challenges.length} action${challenges.length > 1 ? 's' : ''}.\nFollow the on-screen instructions.`}
|
|
738
786
|
</Text>
|
|
739
787
|
<TouchableOpacity
|
|
740
788
|
style={[styles.cancelButton, { borderColor: theme?.errorColor || '#EF4444' }]}
|
|
741
789
|
onPress={onCancel}
|
|
742
790
|
>
|
|
743
791
|
<Text style={[styles.cancelButtonText, { color: theme?.errorColor || '#EF4444' }]}>
|
|
744
|
-
Cancel
|
|
792
|
+
{strings.common.cancel || 'Cancel'}
|
|
745
793
|
</Text>
|
|
746
794
|
</TouchableOpacity>
|
|
747
795
|
</>
|
|
@@ -749,13 +797,13 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
749
797
|
|
|
750
798
|
{phase === 'recording' && (
|
|
751
799
|
<Text style={styles.bottomText}>
|
|
752
|
-
Keep your face visible and follow the instructions
|
|
800
|
+
{strings.liveness.recordingInstructions || 'Keep your face visible and follow the instructions'}
|
|
753
801
|
</Text>
|
|
754
802
|
)}
|
|
755
803
|
|
|
756
804
|
{phase === 'processing' && (
|
|
757
805
|
<Text style={styles.bottomText}>
|
|
758
|
-
Almost done...
|
|
806
|
+
{strings.validation.almostDone || 'Almost done...'}
|
|
759
807
|
</Text>
|
|
760
808
|
)}
|
|
761
809
|
</View>
|