import { useCallback, useState } from "react"; import { Alert, Text, View } from "react-native"; import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { useIsCompactFormFactor } from "@/constants/layout"; import { Link } from "lucide-react-native"; import type { HostProfile } from "@/types/host-connection"; import { useHosts, useHostMutations } from "@/runtime/host-runtime"; import { decodeOfferFragmentPayload, normalizeHostPort } from "@/utils/daemon-endpoints"; import { connectToDaemon } from "@/utils/test-daemon-connection"; import { ConnectionOfferSchema } from "@server/shared/connection-offer"; import { AdaptiveModalSheet, AdaptiveTextInput } from "./adaptive-modal-sheet"; import { Button } from "@/components/ui/button"; const styles = StyleSheet.create((theme) => ({ helper: { color: theme.colors.foregroundMuted, fontSize: theme.fontSize.sm, }, field: { gap: theme.spacing[2], }, label: { color: theme.colors.foregroundMuted, fontSize: theme.fontSize.sm, fontWeight: theme.fontWeight.medium, }, input: { backgroundColor: theme.colors.surface2, borderRadius: theme.borderRadius.lg, paddingHorizontal: theme.spacing[4], paddingVertical: theme.spacing[3], color: theme.colors.foreground, borderWidth: 1, borderColor: theme.colors.border, }, error: { color: theme.colors.destructive, fontSize: theme.fontSize.sm, }, actions: { flexDirection: "row", gap: theme.spacing[3], marginTop: theme.spacing[2], }, })); export interface PairLinkModalProps { visible: boolean; onClose: () => void; targetServerId?: string; onCancel?: () => void; onSaved?: (result: { profile: HostProfile; serverId: string; hostname: string | null; isNewHost: boolean; }) => void; } export function PairLinkModal({ visible, onClose, onCancel, onSaved, targetServerId, }: PairLinkModalProps) { const { theme } = useUnistyles(); const daemons = useHosts(); const { upsertConnectionFromOfferUrl: upsertDaemonFromOfferUrl } = useHostMutations(); const isMobile = useIsCompactFormFactor(); const [offerUrl, setOfferUrl] = useState(""); const [isSaving, setIsSaving] = useState(false); const [errorMessage, setErrorMessage] = useState(""); const handleClose = useCallback(() => { if (isSaving) return; setOfferUrl(""); setErrorMessage(""); onClose(); }, [isSaving, onClose]); const handleCancel = useCallback(() => { if (isSaving) return; setOfferUrl(""); setErrorMessage(""); (onCancel ?? onClose)(); }, [isSaving, onCancel, onClose]); const handleSave = useCallback(async () => { if (isSaving) return; const raw = offerUrl.trim(); if (!raw) { setErrorMessage("Paste a pairing link (…/#offer=...)"); return; } if (!raw.includes("#offer=")) { setErrorMessage("Link must include #offer=..."); return; } const parsedOffer = (() => { try { const idx = raw.indexOf("#offer="); const encoded = raw.slice(idx + "#offer=".length).trim(); if (!encoded) { throw new Error("Offer payload is empty"); } const payload = decodeOfferFragmentPayload(encoded); return ConnectionOfferSchema.parse(payload); } catch (error) { const message = error instanceof Error ? error.message : "Invalid pairing link"; setErrorMessage(message); if (!isMobile) { Alert.alert("Pairing failed", message); } return null; } })(); if (!parsedOffer) { return; } if (targetServerId && parsedOffer.serverId !== targetServerId) { const message = `That pairing link belongs to ${parsedOffer.serverId}, not ${targetServerId}.`; setErrorMessage(message); if (!isMobile) { Alert.alert("Wrong daemon", message); } return; } try { setIsSaving(true); setErrorMessage(""); const { client, hostname } = await connectToDaemon( { id: "probe", type: "relay", relayEndpoint: normalizeHostPort(parsedOffer.relay.endpoint), daemonPublicKeyB64: parsedOffer.daemonPublicKeyB64, }, { serverId: parsedOffer.serverId }, ); await client.close().catch(() => undefined); const isNewHost = !daemons.some((daemon) => daemon.serverId === parsedOffer.serverId); const profile = await upsertDaemonFromOfferUrl(raw, hostname ?? undefined); onSaved?.({ profile, serverId: parsedOffer.serverId, hostname, isNewHost }); handleClose(); } catch (error) { const message = error instanceof Error ? error.message : "Unable to pair host"; setErrorMessage(message); if (!isMobile) { Alert.alert("Pairing failed", message); } } finally { setIsSaving(false); } }, [ daemons, handleClose, isMobile, isSaving, offerUrl, onSaved, targetServerId, upsertDaemonFromOfferUrl, ]); return ( Paste the pairing link from your server. Pairing link {errorMessage ? {errorMessage} : null} ); }