import { CameraView, useCameraPermissions } from "expo-camera"; import { Stack, useLocalSearchParams, useRouter } from "expo-router"; import { SymbolView } from "expo-symbols"; import { useCallback, useEffect, useState } from "react"; import { Alert, Pressable, ScrollView, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useThemeColor } from "../../lib/useThemeColor"; import { AppText as Text, AppTextInput as TextInput } from "../../components/AppText"; import { ErrorBanner } from "../../components/ErrorBanner"; import { dismissRoute } from "../../lib/routes"; import { ConnectionSheetButton } from "./ConnectionSheetButton"; import { extractPairingUrlFromQrPayload } from "./pairing"; import { useRemoteConnections } from "../../state/use-remote-environment-registry"; import { buildPairingUrl, parsePairingUrl } from "./pairing"; export function NewConnectionRouteScreen() { const { connectionError, connectionPairingUrl, connectionState, onChangeConnectionPairingUrl, onConnectPress, } = useRemoteConnections(); const router = useRouter(); const params = useLocalSearchParams<{ mode?: string }>(); const insets = useSafeAreaInsets(); const [hostInput, setHostInput] = useState(""); const [codeInput, setCodeInput] = useState(""); const [isSubmitting, setIsSubmitting] = useState(false); const [showScanner, setShowScanner] = useState(params.mode === "scan_qr"); const [cameraPermission, requestCameraPermission] = useCameraPermissions(); const [scannerLocked, setScannerLocked] = useState(false); const textColor = useThemeColor("--color-icon"); const placeholderColor = useThemeColor("--color-placeholder"); const connectDisabled = isSubmitting || connectionState === "connecting" || hostInput.trim().length === 0; useEffect(() => { const { host, code } = parsePairingUrl(connectionPairingUrl); setHostInput(host); setCodeInput(code); }, [connectionPairingUrl]); useEffect(() => { if (connectionError) { setIsSubmitting(false); } }, [connectionError]); const handleHostChange = useCallback((value: string) => { setHostInput(value); }, []); const handleCodeChange = useCallback((value: string) => { setCodeInput(value); }, []); const openScanner = useCallback(async () => { if (cameraPermission?.granted) { setScannerLocked(false); setShowScanner(true); return; } const permission = await requestCameraPermission(); if (permission.granted) { setScannerLocked(false); setShowScanner(true); return; } Alert.alert("Camera access needed", "Allow camera access to scan a backend pairing QR code."); }, [cameraPermission?.granted, requestCameraPermission]); const closeScanner = useCallback(() => { setShowScanner(false); setScannerLocked(false); }, []); const handleQrScan = useCallback( ({ data }: { readonly data: string }) => { if (scannerLocked) { return; } setScannerLocked(true); try { const pairingUrl = extractPairingUrlFromQrPayload(data); const { host, code } = parsePairingUrl(pairingUrl); setHostInput(host); setCodeInput(code); onChangeConnectionPairingUrl(pairingUrl); setShowScanner(false); } catch (error) { Alert.alert( "Invalid QR code", error instanceof Error ? error.message : "Scanned QR code was not recognized.", ); } finally { setTimeout(() => { setScannerLocked(false); }, 600); } }, [onChangeConnectionPairingUrl, scannerLocked], ); const handleSubmit = useCallback(async () => { setIsSubmitting(true); try { const pairingUrl = buildPairingUrl(hostInput, codeInput); onChangeConnectionPairingUrl(pairingUrl); await onConnectPress(pairingUrl); dismissRoute(router); } catch { setIsSubmitting(false); } }, [codeInput, hostInput, onChangeConnectionPairingUrl, onConnectPress, router]); return ( ( { if (showScanner) { closeScanner(); } else { void openScanner(); } }} > ), }} /> {showScanner ? ( cameraPermission?.granted ? ( ) : ( Camera permission is required to scan a QR code. { void openScanner(); }} /> ) ) : ( Host Pairing code {connectionError ? : null} { void handleSubmit(); }} /> )} ); }