@hexar/biometric-identity-sdk-react-native 1.0.6 → 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 +12 -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 +184 -95
- package/package.json +1 -1
- package/src/components/BiometricIdentityFlow.tsx +13 -0
- package/src/components/CameraCapture.tsx +26 -13
- package/src/components/InstructionsScreen.tsx +28 -1
- package/src/components/VideoRecorder.tsx +215 -95
|
@@ -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',
|
|
@@ -14,9 +14,9 @@ import {
|
|
|
14
14
|
Platform,
|
|
15
15
|
Alert,
|
|
16
16
|
} from 'react-native';
|
|
17
|
-
import { Camera, useCameraDevice, useCameraPermission
|
|
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);
|
|
@@ -136,13 +142,16 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
136
142
|
const arrowAnim = useRef(new Animated.Value(0)).current;
|
|
137
143
|
const progressAnim = useRef(new Animated.Value(0)).current;
|
|
138
144
|
|
|
139
|
-
// Refs
|
|
140
145
|
const recordingStartTime = useRef<number>(0);
|
|
141
146
|
const frameInterval = useRef<NodeJS.Timeout | null>(null);
|
|
142
147
|
const frameCaptureInterval = useRef<NodeJS.Timeout | null>(null);
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const
|
|
148
|
+
const videoRecordingRef = useRef<any>(null);
|
|
149
|
+
const isRecordingRef = useRef<boolean>(false);
|
|
150
|
+
const minDurationMs = 8000;
|
|
151
|
+
const totalDuration = duration || Math.max(
|
|
152
|
+
minDurationMs,
|
|
153
|
+
challenges.reduce((sum, c) => sum + c.duration_ms, 0) + 2000
|
|
154
|
+
);
|
|
146
155
|
|
|
147
156
|
// Check camera permissions
|
|
148
157
|
useEffect(() => {
|
|
@@ -194,13 +203,13 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
194
203
|
// Fetch from backend
|
|
195
204
|
challengeList = await onFetchChallenges();
|
|
196
205
|
} else if (instructions && instructions.length > 0) {
|
|
197
|
-
|
|
206
|
+
const instructionMap = getInstructionMap(strings);
|
|
198
207
|
challengeList = instructions.map((inst, idx) => ({
|
|
199
208
|
action: inst,
|
|
200
|
-
instruction:
|
|
209
|
+
instruction: instructionMap[inst]?.text || inst,
|
|
201
210
|
duration_ms: 2000,
|
|
202
211
|
order: idx + 1,
|
|
203
|
-
icon:
|
|
212
|
+
icon: instructionMap[inst]?.icon,
|
|
204
213
|
}));
|
|
205
214
|
} else {
|
|
206
215
|
// Use default challenges
|
|
@@ -298,53 +307,149 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
298
307
|
).start();
|
|
299
308
|
}, [arrowAnim]);
|
|
300
309
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
310
|
+
const resetAndRetry = useCallback(() => {
|
|
311
|
+
setFrames([]);
|
|
312
|
+
setCompletedChallenges([]);
|
|
313
|
+
setCurrentChallengeIndex(0);
|
|
314
|
+
setChallengeProgress(0);
|
|
315
|
+
setOverallProgress(0);
|
|
316
|
+
recordingStartTime.current = 0;
|
|
317
|
+
setPhase('countdown');
|
|
318
|
+
setCountdown(3);
|
|
319
|
+
}, []);
|
|
320
|
+
|
|
321
|
+
const handleRecordingError = useCallback((error: any) => {
|
|
322
|
+
setPhase('loading');
|
|
323
|
+
Alert.alert(
|
|
324
|
+
'Recording Error',
|
|
325
|
+
'Failed to record video. Please try again.',
|
|
326
|
+
[{ text: 'OK', onPress: onCancel }]
|
|
327
|
+
);
|
|
328
|
+
}, [onCancel]);
|
|
329
|
+
|
|
330
|
+
const handleVideoComplete = useCallback(async (video: any) => {
|
|
331
|
+
console.log('handleVideoComplete called with video:', video?.path);
|
|
305
332
|
|
|
306
|
-
|
|
333
|
+
try {
|
|
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
|
+
}
|
|
359
|
+
|
|
360
|
+
const result: VideoRecordingResult = {
|
|
361
|
+
frames: frames.length > 0 ? frames : (videoBase64 ? [videoBase64] : []),
|
|
362
|
+
duration: actualDuration,
|
|
363
|
+
instructionsFollowed: completedChallenges.length === challenges.length,
|
|
364
|
+
qualityScore: frames.length > 0 ? Math.min(100, (frames.length / 30) * 100) : 85,
|
|
365
|
+
challengesCompleted: completedChallenges,
|
|
366
|
+
sessionId,
|
|
367
|
+
};
|
|
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
|
+
|
|
376
|
+
onComplete(result);
|
|
377
|
+
} catch (error) {
|
|
378
|
+
console.error('Error processing video:', error);
|
|
379
|
+
setPhase('recording');
|
|
380
|
+
handleRecordingError(error);
|
|
381
|
+
}
|
|
382
|
+
}, [frames, completedChallenges, challenges, sessionId, onComplete, resetAndRetry, handleRecordingError, strings, minDurationMs]);
|
|
383
|
+
|
|
384
|
+
const startFrameCapture = useCallback(() => {
|
|
307
385
|
if (cameraRef.current && device) {
|
|
308
|
-
// Capture frames periodically (every 100ms = ~10 FPS)
|
|
309
386
|
frameCaptureInterval.current = setInterval(async () => {
|
|
310
387
|
try {
|
|
311
|
-
// Take a photo frame (react-native-vision-camera doesn't have direct frame access in v4)
|
|
312
|
-
// We'll use takeSnapshot or capture frames via photo
|
|
313
388
|
const photo = await cameraRef.current?.takePhoto({
|
|
314
389
|
flash: 'off',
|
|
315
390
|
});
|
|
316
391
|
|
|
317
392
|
if (photo) {
|
|
318
|
-
// Convert to base64 if possible, otherwise use file path
|
|
319
393
|
try {
|
|
320
394
|
const RNFS = require('react-native-fs');
|
|
321
395
|
const base64 = await RNFS.readFile(photo.path, 'base64');
|
|
322
|
-
setFrames(prev =>
|
|
396
|
+
setFrames(prev => {
|
|
397
|
+
if (prev.length < 100) {
|
|
398
|
+
return [...prev, base64];
|
|
399
|
+
}
|
|
400
|
+
return prev;
|
|
401
|
+
});
|
|
323
402
|
} catch (fsError) {
|
|
324
|
-
|
|
325
|
-
|
|
403
|
+
setFrames(prev => {
|
|
404
|
+
if (prev.length < 100) {
|
|
405
|
+
return [...prev, photo.path];
|
|
406
|
+
}
|
|
407
|
+
return prev;
|
|
408
|
+
});
|
|
326
409
|
}
|
|
327
410
|
}
|
|
328
411
|
} catch (error) {
|
|
329
412
|
console.warn('Frame capture error:', error);
|
|
330
|
-
// Continue even if one frame fails
|
|
331
413
|
}
|
|
332
414
|
}, 100);
|
|
333
|
-
} else {
|
|
334
|
-
// Fallback: simulate frames if camera not available
|
|
335
|
-
frameInterval.current = setInterval(() => {
|
|
336
|
-
setFrames(prev => [...prev, `frame_${Date.now()}`]);
|
|
337
|
-
}, 100);
|
|
338
415
|
}
|
|
339
|
-
|
|
340
|
-
// Start first challenge
|
|
341
|
-
runChallenge(0);
|
|
342
416
|
}, [device]);
|
|
343
417
|
|
|
344
|
-
|
|
418
|
+
const stopRecording = useCallback(async () => {
|
|
419
|
+
console.log('Stopping recording...');
|
|
420
|
+
isRecordingRef.current = false;
|
|
421
|
+
|
|
422
|
+
if (videoRecordingRef.current) {
|
|
423
|
+
try {
|
|
424
|
+
console.log('Stopping video recording');
|
|
425
|
+
await videoRecordingRef.current.stop();
|
|
426
|
+
console.log('Video recording stopped');
|
|
427
|
+
} catch (error) {
|
|
428
|
+
console.error('Error stopping video recording:', error);
|
|
429
|
+
}
|
|
430
|
+
videoRecordingRef.current = null;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (frameCaptureInterval.current) {
|
|
434
|
+
clearInterval(frameCaptureInterval.current);
|
|
435
|
+
frameCaptureInterval.current = null;
|
|
436
|
+
}
|
|
437
|
+
}, []);
|
|
438
|
+
|
|
345
439
|
const runChallenge = useCallback((index: number) => {
|
|
346
440
|
if (index >= challenges.length) {
|
|
347
|
-
|
|
441
|
+
if (isRecordingRef.current) {
|
|
442
|
+
const elapsed = Date.now() - recordingStartTime.current;
|
|
443
|
+
if (elapsed < minDurationMs) {
|
|
444
|
+
setTimeout(() => {
|
|
445
|
+
if (isRecordingRef.current) {
|
|
446
|
+
stopRecording();
|
|
447
|
+
}
|
|
448
|
+
}, minDurationMs - elapsed);
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
stopRecording();
|
|
452
|
+
}
|
|
348
453
|
return;
|
|
349
454
|
}
|
|
350
455
|
|
|
@@ -352,84 +457,94 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
352
457
|
setCurrentChallengeIndex(index);
|
|
353
458
|
setChallengeProgress(0);
|
|
354
459
|
|
|
355
|
-
// Fade in instruction
|
|
356
460
|
Animated.timing(fadeAnim, {
|
|
357
461
|
toValue: 1,
|
|
358
462
|
duration: 300,
|
|
359
463
|
useNativeDriver: true,
|
|
360
464
|
}).start();
|
|
361
465
|
|
|
362
|
-
// Animate arrow for directional challenges
|
|
363
466
|
if (challenge.action.includes('left') || challenge.action.includes('right')) {
|
|
364
467
|
animateArrow(challenge.action);
|
|
365
468
|
}
|
|
366
469
|
|
|
367
|
-
// Progress animation for this challenge
|
|
368
470
|
Animated.timing(progressAnim, {
|
|
369
471
|
toValue: 100,
|
|
370
472
|
duration: challenge.duration_ms,
|
|
371
473
|
useNativeDriver: false,
|
|
372
474
|
}).start();
|
|
373
475
|
|
|
374
|
-
// Update progress in real-time
|
|
375
476
|
const progressInterval = setInterval(() => {
|
|
376
477
|
setChallengeProgress(prev => {
|
|
377
478
|
const newProgress = prev + (100 / (challenge.duration_ms / 100));
|
|
378
479
|
return Math.min(100, newProgress);
|
|
379
480
|
});
|
|
380
481
|
|
|
381
|
-
// Update overall progress
|
|
382
482
|
const elapsed = Date.now() - recordingStartTime.current;
|
|
383
|
-
const totalTime = challenges.reduce((sum, c) => sum + c.duration_ms, 0);
|
|
483
|
+
const totalTime = Math.max(totalDuration, challenges.reduce((sum, c) => sum + c.duration_ms, 0) + 2000);
|
|
384
484
|
setOverallProgress(Math.min(100, (elapsed / totalTime) * 100));
|
|
385
485
|
}, 100);
|
|
386
486
|
|
|
387
|
-
// Move to next challenge after duration
|
|
388
487
|
setTimeout(() => {
|
|
389
488
|
clearInterval(progressInterval);
|
|
390
489
|
|
|
391
|
-
// Mark challenge as completed
|
|
392
490
|
setCompletedChallenges(prev => [...prev, challenge.action]);
|
|
393
491
|
|
|
394
|
-
// Fade out current instruction
|
|
395
492
|
Animated.timing(fadeAnim, {
|
|
396
493
|
toValue: 0,
|
|
397
494
|
duration: 200,
|
|
398
495
|
useNativeDriver: true,
|
|
399
496
|
}).start(() => {
|
|
400
|
-
// Reset and start next challenge
|
|
401
497
|
progressAnim.setValue(0);
|
|
402
498
|
runChallenge(index + 1);
|
|
403
499
|
});
|
|
404
500
|
}, challenge.duration_ms);
|
|
405
|
-
}, [challenges, fadeAnim, progressAnim, animateArrow]);
|
|
501
|
+
}, [challenges, fadeAnim, progressAnim, animateArrow, totalDuration, minDurationMs, stopRecording]);
|
|
406
502
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
503
|
+
const startRecording = useCallback(async () => {
|
|
504
|
+
setPhase('recording');
|
|
505
|
+
recordingStartTime.current = Date.now();
|
|
506
|
+
isRecordingRef.current = true;
|
|
507
|
+
|
|
508
|
+
console.log('Starting video recording, total duration:', totalDuration);
|
|
509
|
+
|
|
510
|
+
if (cameraRef.current && device) {
|
|
511
|
+
try {
|
|
512
|
+
videoRecordingRef.current = await cameraRef.current.startRecording({
|
|
513
|
+
flash: 'off',
|
|
514
|
+
onRecordingFinished: (video: any) => {
|
|
515
|
+
console.log('Video recording finished callback called', video);
|
|
516
|
+
if (isRecordingRef.current) {
|
|
517
|
+
handleVideoComplete(video);
|
|
518
|
+
}
|
|
519
|
+
},
|
|
520
|
+
onRecordingError: (error: any) => {
|
|
521
|
+
console.error('Recording error:', error);
|
|
522
|
+
handleRecordingError(error);
|
|
523
|
+
},
|
|
524
|
+
});
|
|
525
|
+
console.log('Video recording started successfully');
|
|
526
|
+
} catch (error) {
|
|
527
|
+
console.warn('Video recording not available, falling back to frame capture:', error);
|
|
528
|
+
startFrameCapture();
|
|
529
|
+
}
|
|
530
|
+
} else {
|
|
531
|
+
console.log('Camera not available, using frame capture');
|
|
532
|
+
startFrameCapture();
|
|
414
533
|
}
|
|
415
534
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
onComplete(result);
|
|
431
|
-
}, 500);
|
|
432
|
-
}, [frames, completedChallenges, challenges, sessionId, onComplete]);
|
|
535
|
+
runChallenge(0);
|
|
536
|
+
|
|
537
|
+
const timeoutId = setTimeout(() => {
|
|
538
|
+
console.log('Recording timeout reached, stopping recording');
|
|
539
|
+
if (isRecordingRef.current) {
|
|
540
|
+
stopRecording().catch(err => {
|
|
541
|
+
console.error('Error stopping recording on timeout:', err);
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
}, totalDuration);
|
|
545
|
+
|
|
546
|
+
return () => clearTimeout(timeoutId);
|
|
547
|
+
}, [device, totalDuration, handleVideoComplete, handleRecordingError, runChallenge, stopRecording, startFrameCapture]);
|
|
433
548
|
|
|
434
549
|
// Current challenge
|
|
435
550
|
const currentChallenge = challenges[currentChallengeIndex];
|
|
@@ -491,13 +606,17 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
491
606
|
return (
|
|
492
607
|
<View style={styles.container}>
|
|
493
608
|
<View style={styles.permissionContainer}>
|
|
494
|
-
<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>
|
|
495
614
|
<TouchableOpacity
|
|
496
615
|
style={[styles.cancelButton, { borderColor: theme?.errorColor || '#EF4444' }]}
|
|
497
616
|
onPress={onCancel}
|
|
498
617
|
>
|
|
499
618
|
<Text style={[styles.cancelButtonText, { color: theme?.errorColor || '#EF4444' }]}>
|
|
500
|
-
Cancel
|
|
619
|
+
{strings.common.cancel || 'Cancel'}
|
|
501
620
|
</Text>
|
|
502
621
|
</TouchableOpacity>
|
|
503
622
|
</View>
|
|
@@ -509,13 +628,15 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
509
628
|
return (
|
|
510
629
|
<View style={styles.container}>
|
|
511
630
|
<View style={styles.permissionContainer}>
|
|
512
|
-
<Text style={styles.permissionText}>
|
|
631
|
+
<Text style={styles.permissionText}>
|
|
632
|
+
{strings.errors.cameraNotAvailable || 'Camera not available'}
|
|
633
|
+
</Text>
|
|
513
634
|
<TouchableOpacity
|
|
514
635
|
style={[styles.cancelButton, { borderColor: theme?.errorColor || '#EF4444' }]}
|
|
515
636
|
onPress={onCancel}
|
|
516
637
|
>
|
|
517
638
|
<Text style={[styles.cancelButtonText, { color: theme?.errorColor || '#EF4444' }]}>
|
|
518
|
-
Cancel
|
|
639
|
+
{strings.common.cancel || 'Cancel'}
|
|
519
640
|
</Text>
|
|
520
641
|
</TouchableOpacity>
|
|
521
642
|
</View>
|
|
@@ -532,8 +653,8 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
532
653
|
style={StyleSheet.absoluteFill}
|
|
533
654
|
device={device}
|
|
534
655
|
isActive={phase === 'recording' || phase === 'countdown'}
|
|
535
|
-
video={
|
|
536
|
-
|
|
656
|
+
video={true}
|
|
657
|
+
audio={false}
|
|
537
658
|
/>
|
|
538
659
|
|
|
539
660
|
{/* Face Oval Overlay */}
|
|
@@ -555,14 +676,14 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
555
676
|
{/* Loading Phase */}
|
|
556
677
|
{phase === 'loading' && (
|
|
557
678
|
<View style={styles.centeredOverlay}>
|
|
558
|
-
<Text style={styles.loadingText}>Preparing challenges
|
|
679
|
+
<Text style={styles.loadingText}>{strings.liveness.preparing || 'Preparing challenges...'}</Text>
|
|
559
680
|
</View>
|
|
560
681
|
)}
|
|
561
682
|
|
|
562
683
|
{/* Countdown */}
|
|
563
684
|
{phase === 'countdown' && (
|
|
564
685
|
<View style={styles.countdownContainer}>
|
|
565
|
-
<Text style={styles.getReadyText}>Get Ready
|
|
686
|
+
<Text style={styles.getReadyText}>{strings.liveness.getReady || 'Get Ready!'}</Text>
|
|
566
687
|
<Animated.Text
|
|
567
688
|
style={[
|
|
568
689
|
styles.countdownText,
|
|
@@ -583,7 +704,7 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
583
704
|
{ transform: [{ scale: pulseAnim }] }
|
|
584
705
|
]}
|
|
585
706
|
/>
|
|
586
|
-
<Text style={styles.recordingText}>Recording</Text>
|
|
707
|
+
<Text style={styles.recordingText}>{strings.liveness.recording || 'Recording'}</Text>
|
|
587
708
|
</View>
|
|
588
709
|
)}
|
|
589
710
|
|
|
@@ -651,7 +772,7 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
651
772
|
{/* Processing Overlay */}
|
|
652
773
|
{phase === 'processing' && (
|
|
653
774
|
<View style={styles.processingOverlay}>
|
|
654
|
-
<Text style={styles.processingText}>Processing video
|
|
775
|
+
<Text style={styles.processingText}>{strings.liveness.processing || 'Processing video...'}</Text>
|
|
655
776
|
</View>
|
|
656
777
|
)}
|
|
657
778
|
</View>
|
|
@@ -661,15 +782,14 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
661
782
|
{phase === 'countdown' && (
|
|
662
783
|
<>
|
|
663
784
|
<Text style={styles.bottomText}>
|
|
664
|
-
You'll perform {challenges.length} action{challenges.length > 1 ? 's' : ''}
|
|
665
|
-
Follow the on-screen instructions.
|
|
785
|
+
{strings.liveness.countdownMessage || `You'll perform ${challenges.length} action${challenges.length > 1 ? 's' : ''}.\nFollow the on-screen instructions.`}
|
|
666
786
|
</Text>
|
|
667
787
|
<TouchableOpacity
|
|
668
788
|
style={[styles.cancelButton, { borderColor: theme?.errorColor || '#EF4444' }]}
|
|
669
789
|
onPress={onCancel}
|
|
670
790
|
>
|
|
671
791
|
<Text style={[styles.cancelButtonText, { color: theme?.errorColor || '#EF4444' }]}>
|
|
672
|
-
Cancel
|
|
792
|
+
{strings.common.cancel || 'Cancel'}
|
|
673
793
|
</Text>
|
|
674
794
|
</TouchableOpacity>
|
|
675
795
|
</>
|
|
@@ -677,13 +797,13 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
677
797
|
|
|
678
798
|
{phase === 'recording' && (
|
|
679
799
|
<Text style={styles.bottomText}>
|
|
680
|
-
Keep your face visible and follow the instructions
|
|
800
|
+
{strings.liveness.recordingInstructions || 'Keep your face visible and follow the instructions'}
|
|
681
801
|
</Text>
|
|
682
802
|
)}
|
|
683
803
|
|
|
684
804
|
{phase === 'processing' && (
|
|
685
805
|
<Text style={styles.bottomText}>
|
|
686
|
-
Almost done...
|
|
806
|
+
{strings.validation.almostDone || 'Almost done...'}
|
|
687
807
|
</Text>
|
|
688
808
|
)}
|
|
689
809
|
</View>
|