import { CameraView, useCameraPermissions } from "expo-camera"; import { Stack, useLocalSearchParams, useRouter } from "expo-router"; import { AsyncResult } from "effect/unstable/reactivity"; import { useCallback, useEffect, useState } from "react"; import { Alert, 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 "../../features/connection/ConnectionSheetButton"; import { extractPairingUrlFromQrPayload } from "../../features/connection/pairing"; import { useRemoteConnections } from "../../state/use-remote-environment-registry"; import { buildPairingUrl, parsePairingUrl } from "../../features/connection/pairing"; export default function ConnectionsNewRouteScreen() { const { connectionPairingUrl, onChangeConnectionPairingUrl, onConnectPress, pairingConnectionError, } = 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 placeholderColor = useThemeColor("--color-placeholder"); const connectDisabled = isSubmitting || hostInput.trim().length === 0; useEffect(() => { const { host, code } = parsePairingUrl(connectionPairingUrl); setHostInput(host); setCodeInput(code); }, [connectionPairingUrl]); useEffect(() => { if (pairingConnectionError) { setIsSubmitting(false); } }, [pairingConnectionError]); 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 an environment 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); const pairingUrl = buildPairingUrl(hostInput, codeInput); onChangeConnectionPairingUrl(pairingUrl); const result = await onConnectPress(pairingUrl); if (AsyncResult.isSuccess(result)) { dismissRoute(router); } else { setIsSubmitting(false); } }, [codeInput, hostInput, onChangeConnectionPairingUrl, onConnectPress, router]); return ( { if (showScanner) { closeScanner(); } else { void openScanner(); } }} separateBackground /> {showScanner ? ( cameraPermission?.granted ? ( ) : ( Camera permission is required to scan a QR code. { void openScanner(); }} /> ) ) : ( Host Pairing code {pairingConnectionError ? : null} { void handleSubmit(); }} /> )} ); }