// components/sync/QRScanner.tsx import React, { useState, useEffect, useRef } from 'react'; import { View, Text, StyleSheet, TouchableOpacity, Alert, } from 'react-native'; import { CameraView, Camera, BarcodeScanningResult } from 'expo-camera'; import { X } from 'lucide-react-native'; interface QRScannerProps { onScanned: (shareId: string) => void; onCancel: () => void; } export default function QRScanner({ onScanned, onCancel }: QRScannerProps) { const [hasPermission, setHasPermission] = useState(null); const [scanned, setScanned] = useState(false); const lastScannedRef = useRef(null); useEffect(() => { requestPermission(); }, []); const requestPermission = async () => { const { status } = await Camera.requestCameraPermissionsAsync(); setHasPermission(status === 'granted'); }; const handleBarCodeScanned = (result: BarcodeScanningResult) => { if (scanned) return; const { data } = result; // Prevent duplicate scans of the same code if (lastScannedRef.current === data) return; lastScannedRef.current = data; // Extract Share ID — expected format: "TL-XXXXXXXX" (8 alphanumeric chars) const match = data.match(/TL-[A-Z0-9]{8}/i); if (match) { setScanned(true); onScanned(match[0].toUpperCase()); } else { Alert.alert( 'Invalid QR Code', 'This QR code does not contain a valid TerritoryLog Share ID.', [ { text: 'Try Again', onPress: () => { lastScannedRef.current = null; }, }, { text: 'Cancel', onPress: onCancel }, ] ); } }; if (hasPermission === null) { return ( Requesting camera permission… ); } if (hasPermission === false) { return ( Camera access denied. Enable camera access in your device settings to scan QR codes. Go Back ); } return ( {/* Overlay */} {/* Top */} Scan Partner QR Code Point your camera at your partner's TerritoryLog QR code {/* Scan frame row */} {/* Corner markers */} {/* Bottom */} Share ID format: TL-XXXXXXXX ); } const FRAME_SIZE = 240; const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#000', }, centered: { flex: 1, backgroundColor: '#111827', justifyContent: 'center', alignItems: 'center', padding: 24, gap: 12, }, message: { color: '#F9FAFB', fontSize: 16, textAlign: 'center', }, submessage: { color: '#9CA3AF', fontSize: 14, textAlign: 'center', }, cancelBtn: { marginTop: 16, paddingVertical: 12, paddingHorizontal: 24, backgroundColor: '#1A6B72', borderRadius: 12, }, cancelText: { color: '#fff', fontSize: 15, fontWeight: '600', }, overlay: { flex: 1, flexDirection: 'column', }, overlayTop: { flex: 1, backgroundColor: 'rgba(0,0,0,0.6)', alignItems: 'center', paddingTop: 60, paddingHorizontal: 24, gap: 8, }, closeBtn: { position: 'absolute', top: 52, right: 24, width: 40, height: 40, borderRadius: 20, backgroundColor: 'rgba(255,255,255,0.2)', justifyContent: 'center', alignItems: 'center', }, overlayTitle: { color: '#fff', fontSize: 20, fontWeight: '700', marginTop: 8, }, overlaySubtitle: { color: '#D1D5DB', fontSize: 13, textAlign: 'center', }, frameRow: { flexDirection: 'row', height: FRAME_SIZE, }, frameShade: { flex: 1, backgroundColor: 'rgba(0,0,0,0.6)', }, frame: { width: FRAME_SIZE, height: FRAME_SIZE, backgroundColor: 'transparent', }, corner: { position: 'absolute', width: 28, height: 28, borderColor: '#1A6B72', borderWidth: 3, }, cornerTL: { top: 0, left: 0, borderBottomWidth: 0, borderRightWidth: 0 }, cornerTR: { top: 0, right: 0, borderBottomWidth: 0, borderLeftWidth: 0 }, cornerBL: { bottom: 0, left: 0, borderTopWidth: 0, borderRightWidth: 0 }, cornerBR: { bottom: 0, right: 0, borderTopWidth: 0, borderLeftWidth: 0 }, overlayBottom: { flex: 1, backgroundColor: 'rgba(0,0,0,0.6)', alignItems: 'center', paddingTop: 24, }, overlayHint: { color: '#9CA3AF', fontSize: 13, }, });