/** * QRScanner - Vollbild QR-Code Scanner fuer ARIA Pairing * * Scannt QR-Codes im Format: * {"host": "rvs.hackersoft.de", "port": 443, "token": "a3f8b2c9..."} */ import React, { useState, useCallback } from 'react'; import { View, Text, TouchableOpacity, StyleSheet, Modal, Alert, Platform, PermissionsAndroid, } from 'react-native'; import { CameraScreen } from 'react-native-camera-kit'; import { ConnectionConfig } from '../services/rvs'; interface QRScannerProps { visible: boolean; onScan: (config: ConnectionConfig) => void; onClose: () => void; } /** QR-Daten parsen und validieren */ function parseQRData(data: string): ConnectionConfig | null { try { const parsed = JSON.parse(data); if (!parsed.host || !parsed.token) { return null; } return { host: String(parsed.host), port: Number(parsed.port) || 443, token: String(parsed.token), useTLS: parsed.tls !== false, // Standard: TLS an }; } catch { return null; } } /** Kamera-Berechtigung anfordern (Android) */ async function requestCameraPermission(): Promise { if (Platform.OS !== 'android') return true; try { const granted = await PermissionsAndroid.request( PermissionsAndroid.PERMISSIONS.CAMERA, { title: 'Kamera-Zugriff', message: 'ARIA Cockpit braucht Kamera-Zugriff um den QR-Code zu scannen.', buttonPositive: 'Erlauben', buttonNegative: 'Ablehnen', }, ); return granted === PermissionsAndroid.RESULTS.GRANTED; } catch { return false; } } const QRScanner: React.FC = ({ visible, onScan, onClose }) => { const [hasPermission, setHasPermission] = useState(null); const [scanned, setScanned] = useState(false); // Berechtigung pruefen beim Oeffnen React.useEffect(() => { if (visible) { setScanned(false); requestCameraPermission().then(granted => { setHasPermission(granted); if (!granted) { Alert.alert( 'Kamera blockiert', 'Bitte erlaube den Kamera-Zugriff in den Einstellungen.', [{ text: 'OK', onPress: onClose }], ); } }); } }, [visible, onClose]); const handleBarcodeScan = useCallback( (event: { nativeEvent: { codeStringValue: string } }) => { if (scanned) return; const data = event.nativeEvent.codeStringValue; const config = parseQRData(data); if (config) { setScanned(true); onScan(config); } else { // Ungueltig — einmal warnen, dann weiter scannen lassen setScanned(true); Alert.alert( 'Ungueltiger QR-Code', 'Der QR-Code hat nicht das erwartete ARIA-Format.\n\nErwartet: {"host": "...", "port": 443, "token": "..."}', [{ text: 'Nochmal', onPress: () => setScanned(false) }], ); } }, [scanned, onScan], ); if (!visible) return null; return ( {hasPermission ? ( <> {/* Overlay oben */} QR-Code scannen Richte die Kamera auf den QR-Code vom RVS {/* Abbrechen-Button unten */} Abbrechen ) : ( Kamera-Zugriff wird benoetigt Zurueck )} ); }; const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#000000', }, topOverlay: { position: 'absolute', top: 0, left: 0, right: 0, paddingTop: 60, paddingBottom: 20, paddingHorizontal: 20, backgroundColor: 'rgba(0, 0, 0, 0.6)', alignItems: 'center', }, title: { color: '#FFFFFF', fontSize: 20, fontWeight: '700', }, subtitle: { color: '#AAAACC', fontSize: 14, marginTop: 6, textAlign: 'center', }, bottomOverlay: { position: 'absolute', bottom: 0, left: 0, right: 0, paddingBottom: 40, paddingTop: 20, backgroundColor: 'rgba(0, 0, 0, 0.6)', alignItems: 'center', }, cancelButton: { backgroundColor: 'rgba(255, 255, 255, 0.15)', paddingHorizontal: 40, paddingVertical: 14, borderRadius: 12, }, cancelText: { color: '#FFFFFF', fontSize: 16, fontWeight: '600', }, noPermission: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 20, }, noPermissionText: { color: '#AAAACC', fontSize: 16, marginBottom: 20, textAlign: 'center', }, }); export default QRScanner;