224 lines
5.5 KiB
TypeScript
224 lines
5.5 KiB
TypeScript
/**
|
|
* 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<boolean> {
|
|
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<QRScannerProps> = ({ visible, onScan, onClose }) => {
|
|
const [hasPermission, setHasPermission] = useState<boolean | null>(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 (
|
|
<Modal
|
|
visible={visible}
|
|
animationType="slide"
|
|
presentationStyle="fullScreen"
|
|
onRequestClose={onClose}
|
|
>
|
|
<View style={styles.container}>
|
|
{hasPermission ? (
|
|
<>
|
|
<CameraScreen
|
|
scanBarcode={true}
|
|
onReadCode={handleBarcodeScan}
|
|
showFrame={true}
|
|
frameColor="#0096FF"
|
|
laserColor="#0096FF"
|
|
colorForScannerFrame="#0096FF"
|
|
/>
|
|
|
|
{/* Overlay oben */}
|
|
<View style={styles.topOverlay}>
|
|
<Text style={styles.title}>QR-Code scannen</Text>
|
|
<Text style={styles.subtitle}>
|
|
Richte die Kamera auf den QR-Code vom RVS
|
|
</Text>
|
|
</View>
|
|
|
|
{/* Abbrechen-Button unten */}
|
|
<View style={styles.bottomOverlay}>
|
|
<TouchableOpacity style={styles.cancelButton} onPress={onClose}>
|
|
<Text style={styles.cancelText}>Abbrechen</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</>
|
|
) : (
|
|
<View style={styles.noPermission}>
|
|
<Text style={styles.noPermissionText}>Kamera-Zugriff wird benoetigt</Text>
|
|
<TouchableOpacity style={styles.cancelButton} onPress={onClose}>
|
|
<Text style={styles.cancelText}>Zurueck</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
)}
|
|
</View>
|
|
</Modal>
|
|
);
|
|
};
|
|
|
|
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;
|