bae705aa97
- Add NFC ePassport roadmap (ICAO 9303, eIDAS) - Add TensorFlow.js edge face detection (BlazeFace) - Add structured audit logger (GDPR-compliant) - Risk scoring support Part of KYC Apple Native UX v1.1.0
309 lines
7.1 KiB
JavaScript
309 lines
7.1 KiB
JavaScript
/**
|
|
* CameraView Component
|
|
* Advanced camera with real-time evidence overlay
|
|
*/
|
|
|
|
import React, { useState, useEffect, useRef } from 'react';
|
|
import {
|
|
View,
|
|
Text,
|
|
StyleSheet,
|
|
TouchableOpacity,
|
|
Animated,
|
|
Dimensions,
|
|
} from 'react-native';
|
|
import { Camera, useCameraDevices } from 'react-native-vision-camera';
|
|
import Icon from 'react-native-vector-icons/MaterialIcons';
|
|
|
|
const { width, height } = Dimensions.get('window');
|
|
|
|
export default function CameraView({ onCapture, mission, evidence }) {
|
|
const [hasPermission, setHasPermission] = useState(false);
|
|
const [isCapturing, setIsCapturing] = useState(false);
|
|
const [flashMode, setFlashMode] = useState('auto');
|
|
const [zoom, setZoom] = useState(0);
|
|
|
|
const camera = useRef(null);
|
|
const devices = useCameraDevices();
|
|
const device = devices.back;
|
|
|
|
const pulseAnim = useRef(new Animated.Value(1)).current;
|
|
|
|
// Pulse animation for capture button
|
|
useEffect(() => {
|
|
const pulse = Animated.loop(
|
|
Animated.sequence([
|
|
Animated.timing(pulseAnim, {
|
|
toValue: 1.1,
|
|
duration: 1000,
|
|
useNativeDriver: true,
|
|
}),
|
|
Animated.timing(pulseAnim, {
|
|
toValue: 1,
|
|
duration: 1000,
|
|
useNativeDriver: true,
|
|
}),
|
|
])
|
|
);
|
|
pulse.start();
|
|
return () => pulse.stop();
|
|
}, []);
|
|
|
|
// Check permissions
|
|
useEffect(() => {
|
|
checkPermission();
|
|
}, []);
|
|
|
|
const checkPermission = async () => {
|
|
const status = await Camera.requestCameraPermission();
|
|
setHasPermission(status === 'authorized');
|
|
};
|
|
|
|
// Capture photo
|
|
const capturePhoto = async () => {
|
|
if (!camera.current || isCapturing) return;
|
|
|
|
setIsCapturing(true);
|
|
|
|
try {
|
|
const photo = await camera.current.takePhoto({
|
|
qualityPrioritization: 'quality',
|
|
flash: flashMode,
|
|
enableShutterSound: true,
|
|
});
|
|
|
|
onCapture(photo);
|
|
} catch (error) {
|
|
console.error('Capture failed:', error);
|
|
} finally {
|
|
setIsCapturing(false);
|
|
}
|
|
};
|
|
|
|
// Toggle flash
|
|
const toggleFlash = () => {
|
|
const modes = ['off', 'auto', 'on'];
|
|
const currentIndex = modes.indexOf(flashMode);
|
|
setFlashMode(modes[(currentIndex + 1) % modes.length]);
|
|
};
|
|
|
|
// Get flash icon
|
|
const getFlashIcon = () => {
|
|
switch (flashMode) {
|
|
case 'on': return 'flash-on';
|
|
case 'off': return 'flash-off';
|
|
default: return 'flash-auto';
|
|
}
|
|
};
|
|
|
|
if (!device) {
|
|
return (
|
|
<View style={styles.loadingContainer}>
|
|
<Text style={styles.loadingText}>Loading camera...</Text>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<View style={styles.container}>
|
|
{/* Camera */}
|
|
<Camera
|
|
ref={camera}
|
|
style={styles.camera}
|
|
device={device}
|
|
isActive={true}
|
|
photo={true}
|
|
zoom={zoom}
|
|
/>
|
|
|
|
{/* Evidence Overlay */}
|
|
{evidence && (
|
|
<View style={styles.evidenceOverlay}>
|
|
<View style={styles.evidenceBadge}>
|
|
<Icon name="search" size={16} color="#fff" />
|
|
<Text style={styles.evidenceText}>
|
|
{evidence.visual_objects || 0} objects detected
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
)}
|
|
|
|
{/* Mission Overlay */}
|
|
{mission && (
|
|
<View style={styles.missionOverlay}>
|
|
<View style={styles.missionCard}>
|
|
<Text style={styles.missionTitle}>{mission.title}</Text>
|
|
<Text style={styles.missionReward}>${mission.reward_usd}</Text>
|
|
</View>
|
|
</View>
|
|
)}
|
|
|
|
{/* Top Controls */}
|
|
<View style={styles.topControls}>
|
|
<TouchableOpacity onPress={toggleFlash} style={styles.controlButton}>
|
|
<Icon name={getFlashIcon()} size={24} color="#fff" />
|
|
</TouchableOpacity>
|
|
|
|
<TouchableOpacity
|
|
onPress={() => setZoom(zoom === 0 ? 0.5 : 0)}
|
|
style={styles.controlButton}
|
|
>
|
|
<Icon name={zoom > 0 ? 'zoom-out' : 'zoom-in'} size={24} color="#fff" />
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
{/* Bottom Controls */}
|
|
<View style={styles.bottomControls}>
|
|
{/* Capture Button */}
|
|
<Animated.View style={[styles.captureButton, { transform: [{ scale: pulseAnim }] }]}>
|
|
<TouchableOpacity
|
|
onPress={capturePhoto}
|
|
disabled={isCapturing}
|
|
style={styles.captureInner}
|
|
>
|
|
{isCapturing ? (
|
|
<Icon name="hourglass-empty" size={32} color="#fff" />
|
|
) : (
|
|
<View style={styles.captureCircle} />
|
|
)}
|
|
</TouchableOpacity>
|
|
</Animated.View>
|
|
</View>
|
|
|
|
{/* Grid Overlay */}
|
|
<View style={styles.gridOverlay} pointerEvents="none">
|
|
<View style={styles.gridLine} />
|
|
<View style={[styles.gridLine, styles.gridLineVertical]} />
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
backgroundColor: '#000',
|
|
},
|
|
loadingContainer: {
|
|
flex: 1,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
backgroundColor: '#000',
|
|
},
|
|
loadingText: {
|
|
color: '#fff',
|
|
fontSize: 16,
|
|
},
|
|
camera: {
|
|
flex: 1,
|
|
},
|
|
evidenceOverlay: {
|
|
position: 'absolute',
|
|
top: 100,
|
|
left: 16,
|
|
},
|
|
evidenceBadge: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
backgroundColor: 'rgba(0,122,255,0.8)',
|
|
paddingHorizontal: 12,
|
|
paddingVertical: 6,
|
|
borderRadius: 16,
|
|
},
|
|
evidenceText: {
|
|
color: '#fff',
|
|
fontSize: 12,
|
|
fontWeight: '600',
|
|
marginLeft: 4,
|
|
},
|
|
missionOverlay: {
|
|
position: 'absolute',
|
|
top: 100,
|
|
right: 16,
|
|
},
|
|
missionCard: {
|
|
backgroundColor: 'rgba(52,199,89,0.9)',
|
|
padding: 12,
|
|
borderRadius: 12,
|
|
minWidth: 120,
|
|
},
|
|
missionTitle: {
|
|
color: '#fff',
|
|
fontSize: 12,
|
|
fontWeight: '600',
|
|
},
|
|
missionReward: {
|
|
color: '#fff',
|
|
fontSize: 16,
|
|
fontWeight: '700',
|
|
marginTop: 4,
|
|
},
|
|
topControls: {
|
|
position: 'absolute',
|
|
top: 50,
|
|
left: 0,
|
|
right: 0,
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
paddingHorizontal: 16,
|
|
},
|
|
controlButton: {
|
|
width: 44,
|
|
height: 44,
|
|
borderRadius: 22,
|
|
backgroundColor: 'rgba(0,0,0,0.5)',
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
},
|
|
bottomControls: {
|
|
position: 'absolute',
|
|
bottom: 100,
|
|
left: 0,
|
|
right: 0,
|
|
alignItems: 'center',
|
|
},
|
|
captureButton: {
|
|
width: 80,
|
|
height: 80,
|
|
borderRadius: 40,
|
|
backgroundColor: 'rgba(255,255,255,0.3)',
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
},
|
|
captureInner: {
|
|
width: 70,
|
|
height: 70,
|
|
borderRadius: 35,
|
|
backgroundColor: '#fff',
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
},
|
|
captureCircle: {
|
|
width: 60,
|
|
height: 60,
|
|
borderRadius: 30,
|
|
backgroundColor: '#fff',
|
|
},
|
|
gridOverlay: {
|
|
position: 'absolute',
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
},
|
|
gridLine: {
|
|
position: 'absolute',
|
|
top: '33%',
|
|
left: 0,
|
|
right: 0,
|
|
height: 1,
|
|
backgroundColor: 'rgba(255,255,255,0.2)',
|
|
},
|
|
gridLineVertical: {
|
|
top: 0,
|
|
left: '33%',
|
|
width: 1,
|
|
height: '100%',
|
|
},
|
|
});
|