diff --git a/app/index.tsx b/app/index.tsx index fe4378a..4eed4e3 100644 --- a/app/index.tsx +++ b/app/index.tsx @@ -1,5 +1,5 @@ import { Redirect, Stack, useRouter } from 'expo-router'; -import { useMemo } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { Alert, ScrollView, StyleSheet, Text, View } from 'react-native'; import { useEntitlement } from '@/billing'; @@ -21,23 +21,75 @@ export default function DevicesScreen() { const devices = useDevicesStore((s) => s.devices); const setActiveDevice = useDevicesStore((s) => s.setActiveDevice); const removeDevice = useDevicesStore((s) => s.removeDevice); + const connectionPhase = useDevicesStore((s) => s.connectionPhase); + const connectionError = useDevicesStore((s) => s.connectionError); const entitlement = useEntitlement(); + const [pendingId, setPendingId] = useState(null); + const [errorByDevice, setErrorByDevice] = useState>({}); + const visibleDevices = useMemo( () => (demoMode ? devices.filter((d) => d.id === DEMO_DEVICE_ID) : devices.filter((d) => d.id !== DEMO_DEVICE_ID)), [demoMode, devices], ); + useEffect(() => { + if (!pendingId) return; + if (connectionPhase === 'connected') { + const id = pendingId; + setPendingId(null); + setErrorByDevice((prev) => { + if (!(id in prev)) return prev; + const { [id]: _removed, ...rest } = prev; + return rest; + }); + router.push('/projects'); + return; + } + if (connectionPhase === 'unauthorized' || connectionPhase === 'disconnected') { + const id = pendingId; + const message = connectionError ?? (connectionPhase === 'unauthorized' ? 'Pairing revoked' : 'Couldn’t connect'); + setPendingId(null); + setActiveDevice(null); + setErrorByDevice((prev) => ({ ...prev, [id]: message })); + } + }, [pendingId, connectionPhase, connectionError, router, setActiveDevice]); + + const handleRepair = useCallback( + (entry: DeviceEntry) => { + router.push({ + pathname: '/add-device', + params: { + entryId: entry.id, + host: entry.host, + port: String(entry.port), + label: entry.label, + }, + }); + }, + [router], + ); + if (!hasHydrated || !settingsHydrated) return null; if (!hasOnboarded) return ; const handleSelect = (id: string) => { + const entry = devices.find((d) => d.id === id); + if (entry?.needsRepair) { + handleRepair(entry); + return; + } if (entitlement.kind === 'expired') { router.push('/paywall'); return; } + setErrorByDevice((prev) => { + if (!(id in prev)) return prev; + const { [id]: _removed, ...rest } = prev; + return rest; + }); + setPendingId(id); setActiveDevice(id); - router.push('/projects'); }; const handleLongPress = (entry: DeviceEntry) => { @@ -53,18 +105,6 @@ export default function DevicesScreen() { ]); }; - const handleRepair = (entry: DeviceEntry) => { - router.push({ - pathname: '/add-device', - params: { - entryId: entry.id, - host: entry.host, - port: String(entry.port), - label: entry.label, - }, - }); - }; - return ( handleSelect(d.id)} onLongPress={demoMode && d.id === DEMO_DEVICE_ID ? () => {} : () => handleLongPress(d)} onRepair={() => handleRepair(d)} diff --git a/src/components/DeviceRow.tsx b/src/components/DeviceRow.tsx index 8a04902..2ea9399 100644 --- a/src/components/DeviceRow.tsx +++ b/src/components/DeviceRow.tsx @@ -1,5 +1,5 @@ import { Ionicons } from '@expo/vector-icons'; -import { Pressable, StyleSheet, Text, View } from 'react-native'; +import { ActivityIndicator, Pressable, StyleSheet, Text, View } from 'react-native'; import { useTokens } from '@/theme'; @@ -8,15 +8,34 @@ type Props = { host: string; port: number; needsRepair: boolean; + connecting?: boolean; + errorMessage?: string | null; onPress: () => void; onLongPress: () => void; onRepair?: () => void; }; -export function DeviceRow({ label, host, port, needsRepair, onPress, onLongPress, onRepair }: Props) { +export function DeviceRow({ + label, + host, + port, + needsRepair, + connecting, + errorMessage, + onPress, + onLongPress, + onRepair, +}: Props) { const tokens = useTokens(); - const subtitle = needsRepair ? 'Pairing revoked — re-pair to reconnect' : `${host}:${port}`; + const subtitle = needsRepair + ? 'Pairing revoked — re-pair to reconnect' + : connecting + ? 'Connecting…' + : errorMessage + ? errorMessage + : `${host}:${port}`; + const subtitleColor = needsRepair || errorMessage ? tokens.status.danger : tokens.text.muted; return ( {label} - + {subtitle} @@ -52,6 +66,8 @@ export function DeviceRow({ label, host, port, needsRepair, onPress, onLongPress style={[styles.repair, { backgroundColor: tokens.accent.primary }]}> Re-pair + ) : connecting ? ( + ) : ( )}