From 8ff94dc03ed25db3ecf64b7a1bcc1605041093d6 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Wed, 8 Apr 2026 23:36:07 +0700 Subject: [PATCH] fix(app): improve pair device screen with read-only input for link Replace truncating text with a read-only TextInput + copy button pattern so the pairing URL doesn't get cut off. Increase QR code size from 200x200 to 320x320. --- .../components/pair-device-section.tsx | 187 ++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 packages/app/src/desktop/components/pair-device-section.tsx diff --git a/packages/app/src/desktop/components/pair-device-section.tsx b/packages/app/src/desktop/components/pair-device-section.tsx new file mode 100644 index 000000000..ee63a78c6 --- /dev/null +++ b/packages/app/src/desktop/components/pair-device-section.tsx @@ -0,0 +1,187 @@ +import { useCallback } from "react"; +import { ActivityIndicator, Image, Text, TextInput, View } from "react-native"; +import * as Clipboard from "expo-clipboard"; +import * as QRCode from "qrcode"; +import { useQuery } from "@tanstack/react-query"; +import { StyleSheet, useUnistyles } from "react-native-unistyles"; +import { RotateCw, Copy, Check } from "lucide-react-native"; +import { settingsStyles } from "@/styles/settings"; +import { Button } from "@/components/ui/button"; +import { getDesktopDaemonPairing, shouldUseDesktopDaemon } from "@/desktop/daemon/desktop-daemon"; +import { useState } from "react"; + +export function PairDeviceSection() { + const { theme } = useUnistyles(); + const showSection = shouldUseDesktopDaemon(); + const [copied, setCopied] = useState(false); + + const pairingQuery = useQuery({ + queryKey: ["desktop-daemon-pairing"], + queryFn: getDesktopDaemonPairing, + enabled: showSection, + staleTime: 5 * 60 * 1000, + retry: 1, + }); + + const qrQuery = useQuery({ + queryKey: ["desktop-daemon-pairing-qr", pairingQuery.data?.url], + queryFn: () => + QRCode.toDataURL(pairingQuery.data!.url!, { + errorCorrectionLevel: "M", + margin: 1, + width: 480, + }), + enabled: !!pairingQuery.data?.url, + staleTime: Infinity, + }); + + const handleCopyLink = useCallback(async () => { + if (!pairingQuery.data?.url) return; + await Clipboard.setStringAsync(pairingQuery.data.url); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }, [pairingQuery.data?.url]); + + if (!showSection) return null; + + return ( + + Pair device + + {pairingQuery.isPending ? ( + + + Loading pairing offer… + + ) : pairingQuery.isError ? ( + + + {pairingQuery.error instanceof Error + ? pairingQuery.error.message + : "Failed to load pairing offer."} + + + + ) : !pairingQuery.data?.url ? ( + + + {pairingQuery.data?.relayEnabled === false + ? "Relay is not enabled. Enable relay to pair a device." + : "Pairing offer unavailable."} + + + + ) : ( + + + Scan this QR code with Paseo on your phone, or copy the link below. + + + {qrQuery.data ? ( + + ) : qrQuery.isError ? ( + QR code unavailable. + ) : ( + + )} + + + + + + + + + )} + + + ); +} + +const styles = StyleSheet.create((theme) => ({ + centered: { + alignItems: "center", + justifyContent: "center", + gap: theme.spacing[3], + paddingVertical: theme.spacing[6], + paddingHorizontal: theme.spacing[4], + }, + content: { + gap: theme.spacing[3], + padding: theme.spacing[4], + }, + hint: { + color: theme.colors.foregroundMuted, + fontSize: theme.fontSize.xs, + textAlign: "center", + }, + qrContainer: { + alignItems: "center", + justifyContent: "center", + alignSelf: "center", + width: 320, + height: 320, + borderRadius: theme.borderRadius.lg, + borderWidth: 1, + borderColor: theme.colors.border, + backgroundColor: theme.colors.surface0, + padding: theme.spacing[2], + }, + qrImage: { + width: "100%", + height: "100%", + }, + linkRow: { + flexDirection: "row", + alignItems: "center", + gap: theme.spacing[2], + }, + inputWrapper: { + flex: 1, + borderRadius: theme.borderRadius.md, + borderWidth: 1, + borderColor: theme.colors.border, + backgroundColor: theme.colors.surface0, + overflow: "hidden", + }, + linkInput: { + color: theme.colors.foregroundMuted, + fontSize: theme.fontSize.xs, + paddingVertical: theme.spacing[2], + paddingHorizontal: theme.spacing[3], + outlineStyle: "none", + } as any, +}));