Files
boc/iom/quixzoom-app/App.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

692 lines
17 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* quiXzoom Native App
* iOS/Android field data collection
* Mobile-first design
*/
import React, { useState, useEffect, useRef } from 'react';
import {
View,
Text,
StyleSheet,
TouchableOpacity,
Image,
ScrollView,
Alert,
ActivityIndicator,
SafeAreaView,
StatusBar,
} from 'react-native';
import { Camera, useCameraDevices } from 'react-native-vision-camera';
import Geolocation from '@react-native-community/geolocation';
import AsyncStorage from '@react-native-async-storage/async-storage';
// API Configuration
const API_URL = 'https://api.quixzoom.com';
const API_KEY = 'YOUR_API_KEY';
/**
* quiXzoom App Component
*/
export default function quiXzoomApp() {
// State
const [hasPermission, setHasPermission] = useState(false);
const [isCapturing, setIsCapturing] = useState(false);
const [capturedImage, setCapturedImage] = useState(null);
const [location, setLocation] = useState(null);
const [evidence, setEvidence] = useState(null);
const [missions, setMissions] = useState([]);
const [activeMission, setActiveMission] = useState(null);
const [isOffline, setIsOffline] = useState(false);
const [queueCount, setQueueCount] = useState(0);
// Camera ref
const camera = useRef(null);
const devices = useCameraDevices();
const device = devices.back;
// Initialize
useEffect(() => {
checkPermissions();
loadMissions();
loadQueue();
}, []);
// Check camera and location permissions
const checkPermissions = async () => {
const cameraStatus = await Camera.requestCameraPermission();
const locationStatus = await Geolocation.requestAuthorization();
setHasPermission(cameraStatus === 'authorized');
};
// Load missions from API
const loadMissions = async () => {
try {
const response = await fetch(`${API_URL}/missions`, {
headers: { 'Authorization': `Bearer ${API_KEY}` }
});
const data = await response.json();
setMissions(data.missions || []);
} catch (error) {
console.log('Offline mode - using cached missions');
setIsOffline(true);
}
};
// Load offline queue
const loadQueue = async () => {
const queue = await AsyncStorage.getItem('observationQueue');
if (queue) {
setQueueCount(JSON.parse(queue).length);
}
};
// Capture photo
const capturePhoto = async () => {
if (!camera.current) return;
setIsCapturing(true);
try {
// Take photo
const photo = await camera.current.takePhoto({
qualityPrioritization: 'quality',
flash: 'auto',
});
// Get GPS location
Geolocation.getCurrentPosition(
async (position) => {
const loc = {
latitude: position.coords.latitude,
longitude: position.coords.longitude,
altitude: position.coords.altitude,
accuracy: position.coords.accuracy,
heading: position.coords.heading,
timestamp: new Date().toISOString(),
};
setLocation(loc);
setCapturedImage(photo.path);
// Extract evidence
await extractEvidence(photo.path, loc);
},
(error) => {
Alert.alert('GPS Error', 'Could not get location');
setIsCapturing(false);
},
{ enableHighAccuracy: true, timeout: 15000, maximumAge: 10000 }
);
} catch (error) {
Alert.alert('Error', 'Failed to capture photo');
setIsCapturing(false);
}
};
// Extract evidence from image
const extractEvidence = async (imagePath, loc) => {
try {
// Create form data
const formData = new FormData();
formData.append('image', {
uri: imagePath,
type: 'image/jpeg',
name: 'observation.jpg',
});
formData.append('latitude', loc.latitude.toString());
formData.append('longitude', loc.longitude.toString());
formData.append('altitude', loc.altitude?.toString() || '0');
// Send to API
const response = await fetch(`${API_URL}/visual-geolocation/analyze`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'multipart/form-data',
},
body: formData,
});
const data = await response.json();
setEvidence(data);
setIsCapturing(false);
// Save to queue if offline
if (isOffline) {
await saveToQueue({
imagePath,
location: loc,
evidence: data,
timestamp: new Date().toISOString(),
});
}
} catch (error) {
console.log('Evidence extraction failed, saving to queue');
await saveToQueue({
imagePath,
location: loc,
evidence: null,
timestamp: new Date().toISOString(),
});
setIsCapturing(false);
}
};
// Save to offline queue
const saveToQueue = async (observation) => {
const queue = await AsyncStorage.getItem('observationQueue');
const observations = queue ? JSON.parse(queue) : [];
observations.push(observation);
await AsyncStorage.setItem('observationQueue', JSON.stringify(observations));
setQueueCount(observations.length);
};
// Sync offline queue
const syncQueue = async () => {
const queue = await AsyncStorage.getItem('observationQueue');
if (!queue) return;
const observations = JSON.parse(queue);
let synced = 0;
for (const obs of observations) {
try {
// Re-send to API
const formData = new FormData();
formData.append('image', {
uri: obs.imagePath,
type: 'image/jpeg',
name: 'observation.jpg',
});
formData.append('latitude', obs.location.latitude.toString());
formData.append('longitude', obs.location.longitude.toString());
await fetch(`${API_URL}/visual-geolocation/analyze`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'multipart/form-data',
},
body: formData,
});
synced++;
} catch (error) {
console.log('Sync failed for observation');
}
}
// Clear synced observations
const remaining = observations.slice(synced);
await AsyncStorage.setItem('observationQueue', JSON.stringify(remaining));
setQueueCount(remaining.length);
Alert.alert('Sync Complete', `${synced} observations synced`);
};
// Submit observation
const submitObservation = async () => {
if (!capturedImage || !location) return;
try {
const response = await fetch(`${API_URL}/observations`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
image_url: capturedImage,
latitude: location.latitude,
longitude: location.longitude,
altitude: location.altitude,
evidence: evidence,
mission_id: activeMission?.id,
timestamp: new Date().toISOString(),
}),
});
if (response.ok) {
Alert.alert('Success', 'Observation submitted!');
setCapturedImage(null);
setEvidence(null);
setLocation(null);
} else {
throw new Error('Submit failed');
}
} catch (error) {
Alert.alert('Error', 'Saved to queue for later sync');
await saveToQueue({
imagePath: capturedImage,
location,
evidence,
timestamp: new Date().toISOString(),
});
}
};
// Render camera view
const renderCamera = () => {
if (device == null) return <ActivityIndicator size="large" />;
return (
<View style={styles.cameraContainer}>
<Camera
ref={camera}
style={styles.camera}
device={device}
isActive={true}
photo={true}
/>
{/* Overlay */}
<View style={styles.overlay}>
{/* GPS Status */}
<View style={styles.gpsBadge}>
<Text style={styles.gpsText}>
{location ? '📍 GPS Ready' : '📍 Getting GPS...'}
</Text>
</View>
{/* Mission Info */}
{activeMission && (
<View style={styles.missionBadge}>
<Text style={styles.missionText}>
🎯 {activeMission.title}
</Text>
<Text style={styles.missionReward}>
${activeMission.reward_usd}
</Text>
</View>
)}
{/* Queue Status */}
{queueCount > 0 && (
<View style={styles.queueBadge}>
<Text style={styles.queueText}>
📤 {queueCount} in queue
</Text>
</View>
)}
</View>
{/* Capture Button */}
<TouchableOpacity
style={styles.captureButton}
onPress={capturePhoto}
disabled={isCapturing}
>
{isCapturing ? (
<ActivityIndicator size="large" color="#fff" />
) : (
<View style={styles.captureInner} />
)}
</TouchableOpacity>
</View>
);
};
// Render preview
const renderPreview = () => {
return (
<ScrollView style={styles.previewContainer}>
<Image source={{ uri: capturedImage }} style={styles.previewImage} />
{/* Location Info */}
{location && (
<View style={styles.infoCard}>
<Text style={styles.infoTitle}>📍 Location</Text>
<Text style={styles.infoText}>
Lat: {location.latitude.toFixed(6)}
</Text>
<Text style={styles.infoText}>
Lng: {location.longitude.toFixed(6)}
</Text>
<Text style={styles.infoText}>
Accuracy: ±{location.accuracy?.toFixed(0)}m
</Text>
</View>
)}
{/* Evidence */}
{evidence && (
<View style={styles.infoCard}>
<Text style={styles.infoTitle}>🔍 Evidence</Text>
<Text style={styles.infoText}>
Visual Objects: {evidence.evidence_summary?.visual_objects || 0}
</Text>
<Text style={styles.infoText}>
Text Detections: {evidence.evidence_summary?.text_detections || 0}
</Text>
<Text style={styles.infoText}>
Confidence: {(evidence.confidence_report?.overall * 100).toFixed(0)}%
</Text>
</View>
)}
{/* Actions */}
<View style={styles.actionButtons}>
<TouchableOpacity
style={styles.submitButton}
onPress={submitObservation}
>
<Text style={styles.submitText}> Submit Observation</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.retakeButton}
onPress={() => {
setCapturedImage(null);
setEvidence(null);
}}
>
<Text style={styles.retakeText}> Retake</Text>
</TouchableOpacity>
</View>
</ScrollView>
);
};
// Render missions list
const renderMissions = () => {
return (
<ScrollView style={styles.missionsContainer}>
<Text style={styles.sectionTitle}>🎯 Available Missions</Text>
{missions.map((mission) => (
<TouchableOpacity
key={mission.id}
style={[
styles.missionCard,
activeMission?.id === mission.id && styles.activeMission,
]}
onPress={() => setActiveMission(mission)}
>
<Text style={styles.missionTitle}>{mission.title}</Text>
<Text style={styles.missionDescription}>
{mission.description}
</Text>
<View style={styles.missionFooter}>
<Text style={styles.missionReward}>
${mission.reward_usd}
</Text>
<Text style={styles.missionDeadline}>
{new Date(mission.deadline).toLocaleDateString()}
</Text>
</View>
</TouchableOpacity>
))}
{missions.length === 0 && (
<Text style={styles.emptyText}>No missions available</Text>
)}
</ScrollView>
);
};
// Main render
return (
<SafeAreaView style={styles.container}>
<StatusBar barStyle="light-content" backgroundColor="#000" />
{/* Header */}
<View style={styles.header}>
<Text style={styles.headerTitle}>quiXzoom</Text>
<View style={styles.headerRight}>
{isOffline && (
<Text style={styles.offlineBadge}>OFFLINE</Text>
)}
{queueCount > 0 && (
<TouchableOpacity onPress={syncQueue}>
<Text style={styles.queueBadge}>
📤 {queueCount}
</Text>
</TouchableOpacity>
)}
</View>
</View>
{/* Content */}
{capturedImage ? renderPreview() : renderCamera()}
{/* Bottom Navigation */}
<View style={styles.bottomNav}>
<TouchableOpacity style={styles.navButton}>
<Text style={styles.navText}>📷 Capture</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.navButton}>
<Text style={styles.navText}>🎯 Missions</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.navButton}>
<Text style={styles.navText}>📊 Stats</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.navButton}>
<Text style={styles.navText}> Settings</Text>
</TouchableOpacity>
</View>
</SafeAreaView>
);
}
// Styles
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#000',
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
padding: 16,
backgroundColor: '#1a1a1a',
},
headerTitle: {
fontSize: 20,
fontWeight: 'bold',
color: '#fff',
},
headerRight: {
flexDirection: 'row',
alignItems: 'center',
},
offlineBadge: {
color: '#ff9500',
fontSize: 12,
fontWeight: 'bold',
marginRight: 8,
},
cameraContainer: {
flex: 1,
position: 'relative',
},
camera: {
flex: 1,
},
overlay: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
padding: 16,
},
gpsBadge: {
backgroundColor: 'rgba(0,0,0,0.7)',
padding: 8,
borderRadius: 8,
alignSelf: 'flex-start',
marginBottom: 8,
},
gpsText: {
color: '#fff',
fontSize: 14,
},
missionBadge: {
backgroundColor: 'rgba(0,122,255,0.8)',
padding: 8,
borderRadius: 8,
alignSelf: 'flex-start',
marginBottom: 8,
},
missionText: {
color: '#fff',
fontSize: 14,
fontWeight: 'bold',
},
missionReward: {
color: '#fff',
fontSize: 12,
},
queueBadge: {
backgroundColor: 'rgba(255,149,0,0.8)',
padding: 8,
borderRadius: 8,
alignSelf: 'flex-start',
},
queueText: {
color: '#fff',
fontSize: 14,
},
captureButton: {
position: 'absolute',
bottom: 100,
alignSelf: 'center',
width: 80,
height: 80,
borderRadius: 40,
backgroundColor: 'rgba(255,255,255,0.3)',
justifyContent: 'center',
alignItems: 'center',
},
captureInner: {
width: 60,
height: 60,
borderRadius: 30,
backgroundColor: '#fff',
},
previewContainer: {
flex: 1,
backgroundColor: '#000',
},
previewImage: {
width: '100%',
height: 300,
resizeMode: 'cover',
},
infoCard: {
backgroundColor: '#1a1a1a',
margin: 16,
padding: 16,
borderRadius: 12,
},
infoTitle: {
color: '#fff',
fontSize: 16,
fontWeight: 'bold',
marginBottom: 8,
},
infoText: {
color: '#ccc',
fontSize: 14,
marginBottom: 4,
},
actionButtons: {
flexDirection: 'row',
justifyContent: 'space-around',
margin: 16,
},
submitButton: {
backgroundColor: '#34c759',
padding: 16,
borderRadius: 12,
flex: 1,
marginRight: 8,
},
submitText: {
color: '#fff',
fontSize: 16,
fontWeight: 'bold',
textAlign: 'center',
},
retakeButton: {
backgroundColor: '#ff3b30',
padding: 16,
borderRadius: 12,
flex: 1,
marginLeft: 8,
},
retakeText: {
color: '#fff',
fontSize: 16,
fontWeight: 'bold',
textAlign: 'center',
},
missionsContainer: {
flex: 1,
backgroundColor: '#000',
padding: 16,
},
sectionTitle: {
color: '#fff',
fontSize: 20,
fontWeight: 'bold',
marginBottom: 16,
},
missionCard: {
backgroundColor: '#1a1a1a',
padding: 16,
borderRadius: 12,
marginBottom: 12,
},
activeMission: {
borderColor: '#007aff',
borderWidth: 2,
},
missionTitle: {
color: '#fff',
fontSize: 16,
fontWeight: 'bold',
marginBottom: 4,
},
missionDescription: {
color: '#ccc',
fontSize: 14,
marginBottom: 8,
},
missionFooter: {
flexDirection: 'row',
justifyContent: 'space-between',
},
missionReward: {
color: '#34c759',
fontSize: 16,
fontWeight: 'bold',
},
missionDeadline: {
color: '#999',
fontSize: 14,
},
emptyText: {
color: '#666',
fontSize: 16,
textAlign: 'center',
marginTop: 32,
},
bottomNav: {
flexDirection: 'row',
justifyContent: 'space-around',
padding: 16,
backgroundColor: '#1a1a1a',
borderTopWidth: 1,
borderTopColor: '#333',
},
navButton: {
alignItems: 'center',
},
navText: {
color: '#fff',
fontSize: 12,
},
});