Files
boc/iom/quixzoom-app/src/components/CameraView.js
T
Bernt bae705aa97 ARCHITECTURE: NFC roadmap, edge AI, audit logging
- 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
2026-06-29 16:24:48 +00:00

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%',
},
});