@hexar/biometric-identity-sdk-react-native 1.0.0

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.
Files changed (39) hide show
  1. package/README.md +68 -0
  2. package/dist/components/BiometricIdentityFlow.d.ts +17 -0
  3. package/dist/components/BiometricIdentityFlow.d.ts.map +1 -0
  4. package/dist/components/BiometricIdentityFlow.js +366 -0
  5. package/dist/components/CameraCapture.d.ts +15 -0
  6. package/dist/components/CameraCapture.d.ts.map +1 -0
  7. package/dist/components/CameraCapture.js +238 -0
  8. package/dist/components/ErrorScreen.d.ts +15 -0
  9. package/dist/components/ErrorScreen.d.ts.map +1 -0
  10. package/dist/components/ErrorScreen.js +142 -0
  11. package/dist/components/InstructionsScreen.d.ts +14 -0
  12. package/dist/components/InstructionsScreen.d.ts.map +1 -0
  13. package/dist/components/InstructionsScreen.js +181 -0
  14. package/dist/components/ResultScreen.d.ts +15 -0
  15. package/dist/components/ResultScreen.d.ts.map +1 -0
  16. package/dist/components/ResultScreen.js +182 -0
  17. package/dist/components/ValidationProgress.d.ts +14 -0
  18. package/dist/components/ValidationProgress.d.ts.map +1 -0
  19. package/dist/components/ValidationProgress.js +143 -0
  20. package/dist/components/VideoRecorder.d.ts +43 -0
  21. package/dist/components/VideoRecorder.d.ts.map +1 -0
  22. package/dist/components/VideoRecorder.js +631 -0
  23. package/dist/hooks/useBiometricSDK.d.ts +25 -0
  24. package/dist/hooks/useBiometricSDK.d.ts.map +1 -0
  25. package/dist/hooks/useBiometricSDK.js +173 -0
  26. package/dist/index.d.ts +15 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +47 -0
  29. package/package.json +27 -0
  30. package/src/components/BiometricIdentityFlow.tsx +557 -0
  31. package/src/components/CameraCapture.tsx +262 -0
  32. package/src/components/ErrorScreen.tsx +201 -0
  33. package/src/components/InstructionsScreen.tsx +269 -0
  34. package/src/components/ResultScreen.tsx +301 -0
  35. package/src/components/ValidationProgress.tsx +223 -0
  36. package/src/components/VideoRecorder.tsx +794 -0
  37. package/src/hooks/useBiometricSDK.ts +230 -0
  38. package/src/index.ts +24 -0
  39. package/tsconfig.json +20 -0
@@ -0,0 +1,794 @@
1
+ /**
2
+ * Smart Video Recorder Component for Liveness Detection
3
+ * Features challenge-response flow with guided head movements
4
+ */
5
+
6
+ import React, { useState, useEffect, useRef, useCallback } from 'react';
7
+ import {
8
+ View,
9
+ Text,
10
+ StyleSheet,
11
+ TouchableOpacity,
12
+ Animated,
13
+ Easing,
14
+ } from 'react-native';
15
+ import { ThemeConfig, LivenessInstruction } from '@hexar/biometric-identity-sdk-core';
16
+
17
+ // Challenge action configuration (matches backend response)
18
+ export interface ChallengeAction {
19
+ action: string;
20
+ instruction: string;
21
+ duration_ms: number;
22
+ order: number;
23
+ icon?: string;
24
+ }
25
+
26
+ export interface VideoRecorderProps {
27
+ theme?: ThemeConfig;
28
+ /** Total recording duration in ms (default: 8000ms for smart mode) */
29
+ duration?: number;
30
+ /** Instructions for user to follow */
31
+ instructions?: LivenessInstruction[];
32
+ /** Pre-fetched challenges from backend */
33
+ challenges?: ChallengeAction[];
34
+ /** Session ID from backend challenge */
35
+ sessionId?: string;
36
+ /** Enable smart challenge mode (default: true) */
37
+ smartMode?: boolean;
38
+ /** Callback when recording completes */
39
+ onComplete: (videoData: VideoRecordingResult) => void;
40
+ /** Callback when user cancels */
41
+ onCancel: () => void;
42
+ /** Callback to fetch challenges from backend */
43
+ onFetchChallenges?: () => Promise<ChallengeAction[]>;
44
+ }
45
+
46
+ export interface VideoRecordingResult {
47
+ frames: string[];
48
+ duration: number;
49
+ instructionsFollowed: boolean;
50
+ qualityScore: number;
51
+ challengesCompleted: string[];
52
+ sessionId?: string;
53
+ }
54
+
55
+ // Default challenge set (used if backend not available)
56
+ const DEFAULT_CHALLENGES: ChallengeAction[] = [
57
+ {
58
+ action: 'look_left',
59
+ instruction: 'Slowly turn your head to the LEFT',
60
+ duration_ms: 2500,
61
+ order: 1,
62
+ icon: '←',
63
+ },
64
+ {
65
+ action: 'look_right',
66
+ instruction: 'Slowly turn your head to the RIGHT',
67
+ duration_ms: 2500,
68
+ order: 2,
69
+ icon: '→',
70
+ },
71
+ {
72
+ action: 'blink',
73
+ instruction: 'Blink your eyes naturally',
74
+ duration_ms: 2000,
75
+ order: 3,
76
+ icon: '👁',
77
+ },
78
+ {
79
+ action: 'smile',
80
+ instruction: 'Smile 😊',
81
+ duration_ms: 2000,
82
+ order: 4,
83
+ icon: '😊',
84
+ },
85
+ ];
86
+
87
+ // Instruction text mapping
88
+ const INSTRUCTION_MAP: Record<string, { text: string; icon: string }> = {
89
+ look_left: { text: 'Slowly turn your head LEFT', icon: '←' },
90
+ look_right: { text: 'Slowly turn your head RIGHT', icon: '→' },
91
+ look_up: { text: 'Look UP', icon: '↑' },
92
+ look_down: { text: 'Look DOWN', icon: '↓' },
93
+ turn_head_left: { text: 'Turn your head LEFT', icon: '←' },
94
+ turn_head_right: { text: 'Turn your head RIGHT', icon: '→' },
95
+ smile: { text: 'Smile 😊', icon: '😊' },
96
+ blink: { text: 'Blink your eyes naturally', icon: '👁' },
97
+ open_mouth: { text: 'Open your mouth slightly', icon: '😮' },
98
+ stay_still: { text: 'Look at the camera and stay still', icon: '📷' },
99
+ };
100
+
101
+ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
102
+ theme,
103
+ duration,
104
+ instructions,
105
+ challenges: propChallenges,
106
+ sessionId,
107
+ smartMode = true,
108
+ onComplete,
109
+ onCancel,
110
+ onFetchChallenges,
111
+ }) => {
112
+ // State
113
+ const [phase, setPhase] = useState<'loading' | 'countdown' | 'recording' | 'processing'>('loading');
114
+ const [countdown, setCountdown] = useState(3);
115
+ const [challenges, setChallenges] = useState<ChallengeAction[]>([]);
116
+ const [currentChallengeIndex, setCurrentChallengeIndex] = useState(0);
117
+ const [challengeProgress, setChallengeProgress] = useState(0);
118
+ const [overallProgress, setOverallProgress] = useState(0);
119
+ const [completedChallenges, setCompletedChallenges] = useState<string[]>([]);
120
+ const [frames, setFrames] = useState<string[]>([]);
121
+
122
+ // Animations
123
+ const fadeAnim = useRef(new Animated.Value(0)).current;
124
+ const scaleAnim = useRef(new Animated.Value(1)).current;
125
+ const pulseAnim = useRef(new Animated.Value(1)).current;
126
+ const arrowAnim = useRef(new Animated.Value(0)).current;
127
+ const progressAnim = useRef(new Animated.Value(0)).current;
128
+
129
+ // Refs
130
+ const recordingStartTime = useRef<number>(0);
131
+ const frameInterval = useRef<NodeJS.Timeout | null>(null);
132
+
133
+ // Calculate total duration from challenges
134
+ const totalDuration = duration || challenges.reduce((sum, c) => sum + c.duration_ms, 0) + 2000;
135
+
136
+ // Initialize challenges
137
+ useEffect(() => {
138
+ const initChallenges = async () => {
139
+ try {
140
+ let challengeList: ChallengeAction[];
141
+
142
+ if (propChallenges && propChallenges.length > 0) {
143
+ // Use provided challenges
144
+ challengeList = propChallenges;
145
+ } else if (onFetchChallenges) {
146
+ // Fetch from backend
147
+ challengeList = await onFetchChallenges();
148
+ } else if (instructions && instructions.length > 0) {
149
+ // Convert instructions to challenges
150
+ challengeList = instructions.map((inst, idx) => ({
151
+ action: inst,
152
+ instruction: INSTRUCTION_MAP[inst]?.text || inst,
153
+ duration_ms: 2000,
154
+ order: idx + 1,
155
+ icon: INSTRUCTION_MAP[inst]?.icon,
156
+ }));
157
+ } else {
158
+ // Use default challenges
159
+ challengeList = smartMode ? DEFAULT_CHALLENGES : [
160
+ {
161
+ action: 'stay_still',
162
+ instruction: 'Look at the camera and stay still',
163
+ duration_ms: duration || 5000,
164
+ order: 1,
165
+ icon: '📷',
166
+ },
167
+ ];
168
+ }
169
+
170
+ setChallenges(challengeList);
171
+ setPhase('countdown');
172
+ } catch (error) {
173
+ console.error('Failed to fetch challenges:', error);
174
+ // Fallback to default
175
+ setChallenges(DEFAULT_CHALLENGES);
176
+ setPhase('countdown');
177
+ }
178
+ };
179
+
180
+ initChallenges();
181
+ }, [propChallenges, instructions, onFetchChallenges, smartMode, duration]);
182
+
183
+ // Countdown phase
184
+ useEffect(() => {
185
+ if (phase !== 'countdown') return;
186
+
187
+ if (countdown > 0) {
188
+ // Animate countdown number
189
+ Animated.sequence([
190
+ Animated.timing(scaleAnim, {
191
+ toValue: 1.3,
192
+ duration: 200,
193
+ useNativeDriver: true,
194
+ }),
195
+ Animated.timing(scaleAnim, {
196
+ toValue: 1,
197
+ duration: 200,
198
+ useNativeDriver: true,
199
+ }),
200
+ ]).start();
201
+
202
+ const timer = setTimeout(() => setCountdown(countdown - 1), 1000);
203
+ return () => clearTimeout(timer);
204
+ } else {
205
+ startRecording();
206
+ }
207
+ }, [countdown, phase]);
208
+
209
+ // Start pulse animation for recording indicator
210
+ useEffect(() => {
211
+ if (phase === 'recording') {
212
+ Animated.loop(
213
+ Animated.sequence([
214
+ Animated.timing(pulseAnim, {
215
+ toValue: 1.2,
216
+ duration: 500,
217
+ easing: Easing.inOut(Easing.ease),
218
+ useNativeDriver: true,
219
+ }),
220
+ Animated.timing(pulseAnim, {
221
+ toValue: 1,
222
+ duration: 500,
223
+ easing: Easing.inOut(Easing.ease),
224
+ useNativeDriver: true,
225
+ }),
226
+ ])
227
+ ).start();
228
+ }
229
+ }, [phase]);
230
+
231
+ // Animate arrow for directional challenges
232
+ const animateArrow = useCallback((direction: string) => {
233
+ const toValue = direction.includes('left') ? -20 : direction.includes('right') ? 20 : 0;
234
+
235
+ Animated.loop(
236
+ Animated.sequence([
237
+ Animated.timing(arrowAnim, {
238
+ toValue,
239
+ duration: 500,
240
+ easing: Easing.inOut(Easing.ease),
241
+ useNativeDriver: true,
242
+ }),
243
+ Animated.timing(arrowAnim, {
244
+ toValue: 0,
245
+ duration: 500,
246
+ easing: Easing.inOut(Easing.ease),
247
+ useNativeDriver: true,
248
+ }),
249
+ ])
250
+ ).start();
251
+ }, [arrowAnim]);
252
+
253
+ // Start recording
254
+ const startRecording = useCallback(() => {
255
+ setPhase('recording');
256
+ recordingStartTime.current = Date.now();
257
+
258
+ // Simulate frame capture (in real implementation, this would capture actual camera frames)
259
+ frameInterval.current = setInterval(() => {
260
+ setFrames(prev => [...prev, `frame_${Date.now()}`]);
261
+ }, 100); // Capture ~10 FPS
262
+
263
+ // Start first challenge
264
+ runChallenge(0);
265
+ }, []);
266
+
267
+ // Run a specific challenge
268
+ const runChallenge = useCallback((index: number) => {
269
+ if (index >= challenges.length) {
270
+ completeRecording();
271
+ return;
272
+ }
273
+
274
+ const challenge = challenges[index];
275
+ setCurrentChallengeIndex(index);
276
+ setChallengeProgress(0);
277
+
278
+ // Fade in instruction
279
+ Animated.timing(fadeAnim, {
280
+ toValue: 1,
281
+ duration: 300,
282
+ useNativeDriver: true,
283
+ }).start();
284
+
285
+ // Animate arrow for directional challenges
286
+ if (challenge.action.includes('left') || challenge.action.includes('right')) {
287
+ animateArrow(challenge.action);
288
+ }
289
+
290
+ // Progress animation for this challenge
291
+ Animated.timing(progressAnim, {
292
+ toValue: 100,
293
+ duration: challenge.duration_ms,
294
+ useNativeDriver: false,
295
+ }).start();
296
+
297
+ // Update progress in real-time
298
+ const progressInterval = setInterval(() => {
299
+ setChallengeProgress(prev => {
300
+ const newProgress = prev + (100 / (challenge.duration_ms / 100));
301
+ return Math.min(100, newProgress);
302
+ });
303
+
304
+ // Update overall progress
305
+ const elapsed = Date.now() - recordingStartTime.current;
306
+ const totalTime = challenges.reduce((sum, c) => sum + c.duration_ms, 0);
307
+ setOverallProgress(Math.min(100, (elapsed / totalTime) * 100));
308
+ }, 100);
309
+
310
+ // Move to next challenge after duration
311
+ setTimeout(() => {
312
+ clearInterval(progressInterval);
313
+
314
+ // Mark challenge as completed
315
+ setCompletedChallenges(prev => [...prev, challenge.action]);
316
+
317
+ // Fade out current instruction
318
+ Animated.timing(fadeAnim, {
319
+ toValue: 0,
320
+ duration: 200,
321
+ useNativeDriver: true,
322
+ }).start(() => {
323
+ // Reset and start next challenge
324
+ progressAnim.setValue(0);
325
+ runChallenge(index + 1);
326
+ });
327
+ }, challenge.duration_ms);
328
+ }, [challenges, fadeAnim, progressAnim, animateArrow]);
329
+
330
+ // Complete recording
331
+ const completeRecording = useCallback(() => {
332
+ if (frameInterval.current) {
333
+ clearInterval(frameInterval.current);
334
+ }
335
+
336
+ setPhase('processing');
337
+ setOverallProgress(100);
338
+
339
+ // Simulate processing delay
340
+ setTimeout(() => {
341
+ const result: VideoRecordingResult = {
342
+ frames,
343
+ duration: Date.now() - recordingStartTime.current,
344
+ instructionsFollowed: completedChallenges.length === challenges.length,
345
+ qualityScore: 85 + Math.random() * 10, // Simulated quality score
346
+ challengesCompleted: completedChallenges,
347
+ sessionId,
348
+ };
349
+
350
+ onComplete(result);
351
+ }, 500);
352
+ }, [frames, completedChallenges, challenges, sessionId, onComplete]);
353
+
354
+ // Current challenge
355
+ const currentChallenge = challenges[currentChallengeIndex];
356
+
357
+ // Get direction arrow for the current challenge
358
+ const getDirectionIndicator = () => {
359
+ if (!currentChallenge) return null;
360
+
361
+ const action = currentChallenge.action;
362
+
363
+ if (action.includes('left')) {
364
+ return (
365
+ <Animated.View
366
+ style={[
367
+ styles.directionArrow,
368
+ styles.leftArrow,
369
+ { transform: [{ translateX: arrowAnim }] }
370
+ ]}
371
+ >
372
+ <Text style={styles.arrowText}>◀</Text>
373
+ </Animated.View>
374
+ );
375
+ }
376
+
377
+ if (action.includes('right')) {
378
+ return (
379
+ <Animated.View
380
+ style={[
381
+ styles.directionArrow,
382
+ styles.rightArrow,
383
+ { transform: [{ translateX: arrowAnim }] }
384
+ ]}
385
+ >
386
+ <Text style={styles.arrowText}>▶</Text>
387
+ </Animated.View>
388
+ );
389
+ }
390
+
391
+ if (action === 'look_up') {
392
+ return (
393
+ <View style={[styles.directionArrow, styles.topArrow]}>
394
+ <Text style={styles.arrowText}>▲</Text>
395
+ </View>
396
+ );
397
+ }
398
+
399
+ if (action === 'look_down') {
400
+ return (
401
+ <View style={[styles.directionArrow, styles.bottomArrow]}>
402
+ <Text style={styles.arrowText}>▼</Text>
403
+ </View>
404
+ );
405
+ }
406
+
407
+ return null;
408
+ };
409
+
410
+ return (
411
+ <View style={styles.container}>
412
+ {/* Camera View */}
413
+ <View style={styles.cameraContainer}>
414
+ <View style={styles.mockCamera}>
415
+ <Text style={styles.mockCameraText}>Front Camera</Text>
416
+ </View>
417
+
418
+ {/* Face Oval Overlay */}
419
+ <View style={styles.overlay}>
420
+ <View
421
+ style={[
422
+ styles.faceOval,
423
+ phase === 'recording' && {
424
+ borderColor: theme?.successColor || '#10B981',
425
+ borderStyle: 'solid',
426
+ }
427
+ ]}
428
+ />
429
+
430
+ {/* Direction indicators */}
431
+ {phase === 'recording' && getDirectionIndicator()}
432
+ </View>
433
+
434
+ {/* Loading Phase */}
435
+ {phase === 'loading' && (
436
+ <View style={styles.centeredOverlay}>
437
+ <Text style={styles.loadingText}>Preparing challenges...</Text>
438
+ </View>
439
+ )}
440
+
441
+ {/* Countdown */}
442
+ {phase === 'countdown' && (
443
+ <View style={styles.countdownContainer}>
444
+ <Text style={styles.getReadyText}>Get Ready!</Text>
445
+ <Animated.Text
446
+ style={[
447
+ styles.countdownText,
448
+ { transform: [{ scale: scaleAnim }] }
449
+ ]}
450
+ >
451
+ {countdown}
452
+ </Animated.Text>
453
+ </View>
454
+ )}
455
+
456
+ {/* Recording Indicator */}
457
+ {phase === 'recording' && (
458
+ <View style={styles.recordingIndicator}>
459
+ <Animated.View
460
+ style={[
461
+ styles.recordingDot,
462
+ { transform: [{ scale: pulseAnim }] }
463
+ ]}
464
+ />
465
+ <Text style={styles.recordingText}>Recording</Text>
466
+ </View>
467
+ )}
468
+
469
+ {/* Overall Progress Bar */}
470
+ {(phase === 'recording' || phase === 'processing') && (
471
+ <View style={styles.progressContainer}>
472
+ <View style={styles.progressBar}>
473
+ <View
474
+ style={[
475
+ styles.progressFill,
476
+ {
477
+ width: `${overallProgress}%`,
478
+ backgroundColor: theme?.primaryColor || '#6366F1'
479
+ }
480
+ ]}
481
+ />
482
+ </View>
483
+ <Text style={styles.progressText}>
484
+ {Math.round(overallProgress)}%
485
+ </Text>
486
+ </View>
487
+ )}
488
+
489
+ {/* Challenge Instructions */}
490
+ {phase === 'recording' && currentChallenge && (
491
+ <Animated.View
492
+ style={[
493
+ styles.instructionContainer,
494
+ { opacity: fadeAnim }
495
+ ]}
496
+ >
497
+ <View style={styles.instructionBox}>
498
+ {currentChallenge.icon && (
499
+ <Text style={styles.instructionIcon}>{currentChallenge.icon}</Text>
500
+ )}
501
+ <Text style={styles.instructionText}>
502
+ {currentChallenge.instruction}
503
+ </Text>
504
+
505
+ {/* Challenge progress bar */}
506
+ <View style={styles.challengeProgressBar}>
507
+ <View
508
+ style={[
509
+ styles.challengeProgressFill,
510
+ {
511
+ width: `${challengeProgress}%`,
512
+ backgroundColor: theme?.successColor || '#10B981'
513
+ }
514
+ ]}
515
+ />
516
+ </View>
517
+ </View>
518
+ </Animated.View>
519
+ )}
520
+
521
+ {/* Challenge Counter */}
522
+ {phase === 'recording' && challenges.length > 1 && (
523
+ <View style={styles.challengeCounter}>
524
+ <Text style={styles.challengeCounterText}>
525
+ {currentChallengeIndex + 1} / {challenges.length}
526
+ </Text>
527
+ </View>
528
+ )}
529
+
530
+ {/* Processing Overlay */}
531
+ {phase === 'processing' && (
532
+ <View style={styles.processingOverlay}>
533
+ <Text style={styles.processingText}>Processing video...</Text>
534
+ </View>
535
+ )}
536
+ </View>
537
+
538
+ {/* Bottom Container */}
539
+ <View style={styles.bottomContainer}>
540
+ {phase === 'countdown' && (
541
+ <>
542
+ <Text style={styles.bottomText}>
543
+ You'll perform {challenges.length} action{challenges.length > 1 ? 's' : ''}.{'\n'}
544
+ Follow the on-screen instructions.
545
+ </Text>
546
+ <TouchableOpacity
547
+ style={[styles.cancelButton, { borderColor: theme?.errorColor || '#EF4444' }]}
548
+ onPress={onCancel}
549
+ >
550
+ <Text style={[styles.cancelButtonText, { color: theme?.errorColor || '#EF4444' }]}>
551
+ Cancel
552
+ </Text>
553
+ </TouchableOpacity>
554
+ </>
555
+ )}
556
+
557
+ {phase === 'recording' && (
558
+ <Text style={styles.bottomText}>
559
+ Keep your face visible and follow the instructions
560
+ </Text>
561
+ )}
562
+
563
+ {phase === 'processing' && (
564
+ <Text style={styles.bottomText}>
565
+ Almost done...
566
+ </Text>
567
+ )}
568
+ </View>
569
+ </View>
570
+ );
571
+ };
572
+
573
+ const styles = StyleSheet.create({
574
+ container: {
575
+ flex: 1,
576
+ backgroundColor: '#000000',
577
+ },
578
+ cameraContainer: {
579
+ flex: 1,
580
+ position: 'relative',
581
+ },
582
+ mockCamera: {
583
+ flex: 1,
584
+ backgroundColor: '#1F2937',
585
+ justifyContent: 'center',
586
+ alignItems: 'center',
587
+ },
588
+ mockCameraText: {
589
+ color: '#FFFFFF',
590
+ fontSize: 20,
591
+ fontWeight: 'bold',
592
+ },
593
+ overlay: {
594
+ ...StyleSheet.absoluteFillObject,
595
+ justifyContent: 'center',
596
+ alignItems: 'center',
597
+ },
598
+ faceOval: {
599
+ width: 250,
600
+ height: 320,
601
+ borderRadius: 125,
602
+ borderWidth: 4,
603
+ borderColor: '#FFFFFF',
604
+ borderStyle: 'dashed',
605
+ },
606
+ centeredOverlay: {
607
+ ...StyleSheet.absoluteFillObject,
608
+ justifyContent: 'center',
609
+ alignItems: 'center',
610
+ backgroundColor: 'rgba(0, 0, 0, 0.7)',
611
+ },
612
+ loadingText: {
613
+ color: '#FFFFFF',
614
+ fontSize: 18,
615
+ },
616
+ countdownContainer: {
617
+ ...StyleSheet.absoluteFillObject,
618
+ justifyContent: 'center',
619
+ alignItems: 'center',
620
+ backgroundColor: 'rgba(0, 0, 0, 0.6)',
621
+ },
622
+ getReadyText: {
623
+ color: '#FFFFFF',
624
+ fontSize: 24,
625
+ fontWeight: '600',
626
+ marginBottom: 20,
627
+ },
628
+ countdownText: {
629
+ fontSize: 96,
630
+ fontWeight: 'bold',
631
+ color: '#FFFFFF',
632
+ },
633
+ recordingIndicator: {
634
+ position: 'absolute',
635
+ top: 50,
636
+ right: 20,
637
+ flexDirection: 'row',
638
+ alignItems: 'center',
639
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
640
+ paddingHorizontal: 12,
641
+ paddingVertical: 6,
642
+ borderRadius: 20,
643
+ },
644
+ recordingDot: {
645
+ width: 12,
646
+ height: 12,
647
+ borderRadius: 6,
648
+ backgroundColor: '#EF4444',
649
+ marginRight: 8,
650
+ },
651
+ recordingText: {
652
+ color: '#FFFFFF',
653
+ fontSize: 14,
654
+ fontWeight: '600',
655
+ },
656
+ progressContainer: {
657
+ position: 'absolute',
658
+ top: 100,
659
+ left: 20,
660
+ right: 20,
661
+ flexDirection: 'row',
662
+ alignItems: 'center',
663
+ },
664
+ progressBar: {
665
+ flex: 1,
666
+ height: 8,
667
+ backgroundColor: 'rgba(255, 255, 255, 0.3)',
668
+ borderRadius: 4,
669
+ overflow: 'hidden',
670
+ marginRight: 10,
671
+ },
672
+ progressFill: {
673
+ height: '100%',
674
+ borderRadius: 4,
675
+ },
676
+ progressText: {
677
+ color: '#FFFFFF',
678
+ fontSize: 12,
679
+ fontWeight: '600',
680
+ width: 40,
681
+ textAlign: 'right',
682
+ },
683
+ instructionContainer: {
684
+ position: 'absolute',
685
+ top: 140,
686
+ left: 20,
687
+ right: 20,
688
+ alignItems: 'center',
689
+ },
690
+ instructionBox: {
691
+ backgroundColor: 'rgba(0, 0, 0, 0.8)',
692
+ paddingHorizontal: 24,
693
+ paddingVertical: 16,
694
+ borderRadius: 16,
695
+ alignItems: 'center',
696
+ minWidth: 280,
697
+ },
698
+ instructionIcon: {
699
+ fontSize: 36,
700
+ marginBottom: 8,
701
+ },
702
+ instructionText: {
703
+ fontSize: 20,
704
+ fontWeight: 'bold',
705
+ color: '#FFFFFF',
706
+ textAlign: 'center',
707
+ marginBottom: 12,
708
+ },
709
+ challengeProgressBar: {
710
+ width: '100%',
711
+ height: 4,
712
+ backgroundColor: 'rgba(255, 255, 255, 0.3)',
713
+ borderRadius: 2,
714
+ overflow: 'hidden',
715
+ },
716
+ challengeProgressFill: {
717
+ height: '100%',
718
+ borderRadius: 2,
719
+ },
720
+ challengeCounter: {
721
+ position: 'absolute',
722
+ top: 50,
723
+ left: 20,
724
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
725
+ paddingHorizontal: 12,
726
+ paddingVertical: 6,
727
+ borderRadius: 20,
728
+ },
729
+ challengeCounterText: {
730
+ color: '#FFFFFF',
731
+ fontSize: 14,
732
+ fontWeight: '600',
733
+ },
734
+ directionArrow: {
735
+ position: 'absolute',
736
+ justifyContent: 'center',
737
+ alignItems: 'center',
738
+ },
739
+ leftArrow: {
740
+ left: 20,
741
+ },
742
+ rightArrow: {
743
+ right: 20,
744
+ },
745
+ topArrow: {
746
+ top: 100,
747
+ },
748
+ bottomArrow: {
749
+ bottom: 150,
750
+ },
751
+ arrowText: {
752
+ fontSize: 48,
753
+ color: '#FFFFFF',
754
+ textShadowColor: 'rgba(0, 0, 0, 0.5)',
755
+ textShadowOffset: { width: 2, height: 2 },
756
+ textShadowRadius: 4,
757
+ },
758
+ processingOverlay: {
759
+ ...StyleSheet.absoluteFillObject,
760
+ justifyContent: 'center',
761
+ alignItems: 'center',
762
+ backgroundColor: 'rgba(0, 0, 0, 0.7)',
763
+ },
764
+ processingText: {
765
+ color: '#FFFFFF',
766
+ fontSize: 20,
767
+ fontWeight: '600',
768
+ },
769
+ bottomContainer: {
770
+ paddingVertical: 32,
771
+ paddingHorizontal: 24,
772
+ backgroundColor: 'rgba(0, 0, 0, 0.9)',
773
+ alignItems: 'center',
774
+ },
775
+ bottomText: {
776
+ color: '#FFFFFF',
777
+ fontSize: 16,
778
+ textAlign: 'center',
779
+ marginBottom: 16,
780
+ lineHeight: 24,
781
+ },
782
+ cancelButton: {
783
+ paddingVertical: 12,
784
+ paddingHorizontal: 32,
785
+ borderRadius: 8,
786
+ borderWidth: 2,
787
+ },
788
+ cancelButtonText: {
789
+ fontSize: 16,
790
+ fontWeight: '600',
791
+ },
792
+ });
793
+
794
+ export default VideoRecorder;