import { useState, useRef, useCallback } from 'react' import { View, Text, StyleSheet, Pressable, ActivityIndicator } from 'react-native' import { useSafeAreaInsets } from 'react-native-safe-area-context' import { CameraView, useCameraPermissions } from 'expo-camera' import { useRouter } from 'expo-router' import { ChevronLeft } from 'lucide-react-native' import { decodePairingUrl } from '../src/transport/pairing' import { connect } from '../src/transport/rpc-client' import { saveHost, getNextHostName } from '../src/transport/host-store' import type { PairingOffer } from '../src/transport/types' import { colors, spacing, radii, typography } from '../src/theme/mobile-theme' function Step({ number, text }: { number: number; text: string }) { return ( {number} {text} ) } export default function PairScanScreen() { const router = useRouter() const insets = useSafeAreaInsets() const [permission, requestPermission] = useCameraPermissions() const [status, setStatus] = useState<'scanning' | 'connecting' | 'error'>('scanning') const [errorMessage, setErrorMessage] = useState('') const processingRef = useRef(false) const handleBarCodeScanned = useCallback( ({ data }: { data: string }) => { if (processingRef.current) return processingRef.current = true const offer = decodePairingUrl(data) if (!offer) { setStatus('error') setErrorMessage('Not a valid Orca QR code') processingRef.current = false return } void testAndSave(offer) }, [router] ) async function testAndSave(offer: PairingOffer) { setStatus('connecting') let client: ReturnType | null = null try { client = connect(offer.endpoint, offer.deviceToken, offer.publicKeyB64) const response = await client.sendRequest('status.get') client.close() client = null if (!response.ok) { if (response.error.code === 'unauthorized') { setStatus('error') setErrorMessage('Authentication failed — token may be expired') processingRef.current = false return } setStatus('error') setErrorMessage(`Server error: ${response.error.message}`) processingRef.current = false return } const hostId = `host-${Date.now()}` const hostName = await getNextHostName() await saveHost({ id: hostId, name: hostName, endpoint: offer.endpoint, deviceToken: offer.deviceToken, publicKeyB64: offer.publicKeyB64, lastConnected: Date.now() }) router.replace(`/h/${hostId}`) } catch { setStatus('error') setErrorMessage('Cannot connect — check that your computer is on the same network') processingRef.current = false } finally { client?.close() } } function retry() { setStatus('scanning') setErrorMessage('') processingRef.current = false } const containerPadding = { paddingTop: insets.top + spacing.sm } if (!permission) { return ( ) } if (!permission.granted) { return ( router.back()}> Camera Permission Orca needs camera access to scan the pairing QR code from your desktop. Grant Camera Access ) } return ( router.back()}> {status === 'scanning' && ( )} {status === 'connecting' && ( Connecting… )} {status === 'error' && ( {errorMessage} Try Again )} ) } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: colors.bgBase, padding: spacing.lg }, backButton: { width: 36, height: 36, borderRadius: 18, alignItems: 'center', justifyContent: 'center', marginBottom: spacing.sm }, steps: { gap: spacing.sm, marginBottom: spacing.lg, marginLeft: 7 }, step: { flexDirection: 'row', alignItems: 'center', gap: spacing.sm }, stepBadge: { width: 22, height: 22, borderRadius: 11, backgroundColor: colors.bgRaised, alignItems: 'center', justifyContent: 'center' }, stepNumber: { fontSize: 12, fontWeight: '700', color: colors.textSecondary }, stepText: { fontSize: typography.bodySize, color: colors.textSecondary }, cameraWrap: { flex: 1, borderRadius: radii.camera, overflow: 'hidden' }, camera: { ...StyleSheet.absoluteFillObject }, reticle: { ...StyleSheet.absoluteFillObject, alignItems: 'center', justifyContent: 'center' }, corner: { position: 'absolute', width: 28, height: 28, borderColor: 'rgba(255,255,255,0.7)' }, cornerTL: { top: '30%', left: '20%', borderTopWidth: 2.5, borderLeftWidth: 2.5, borderTopLeftRadius: 6 }, cornerTR: { top: '30%', right: '20%', borderTopWidth: 2.5, borderRightWidth: 2.5, borderTopRightRadius: 6 }, cornerBL: { bottom: '30%', left: '20%', borderBottomWidth: 2.5, borderLeftWidth: 2.5, borderBottomLeftRadius: 6 }, cornerBR: { bottom: '30%', right: '20%', borderBottomWidth: 2.5, borderRightWidth: 2.5, borderBottomRightRadius: 6 }, centered: { flex: 1, alignItems: 'center', justifyContent: 'center' }, title: { fontSize: typography.titleSize, fontWeight: '600', color: colors.textPrimary, marginBottom: spacing.sm }, subtitle: { fontSize: typography.bodySize, color: colors.textSecondary, textAlign: 'center', marginBottom: spacing.xl, lineHeight: 20 }, connectingText: { color: colors.textSecondary, fontSize: typography.bodySize, marginTop: spacing.lg }, errorText: { color: colors.statusRed, fontSize: typography.bodySize, textAlign: 'center', marginBottom: spacing.xl, lineHeight: 20 }, primaryButton: { backgroundColor: colors.textPrimary, paddingHorizontal: spacing.xl, paddingVertical: spacing.sm + 2, borderRadius: radii.button }, primaryButtonText: { color: colors.bgBase, fontSize: typography.bodySize, fontWeight: '600' } })