From 3a57de2ed60db041124ef00478b4e55206eea75b Mon Sep 17 00:00:00 2001 From: Jake Jarvis Date: Fri, 13 Mar 2026 10:32:38 -0400 Subject: [PATCH] Improve server URL input UX on native app launch (#9) --- apps/native/src/app/(auth)/server-url.tsx | 34 +++++++---- apps/native/src/components/auth-screen.tsx | 65 ++++++++++++---------- 2 files changed, 58 insertions(+), 41 deletions(-) diff --git a/apps/native/src/app/(auth)/server-url.tsx b/apps/native/src/app/(auth)/server-url.tsx index 64e67c4..eb06d48 100644 --- a/apps/native/src/app/(auth)/server-url.tsx +++ b/apps/native/src/app/(auth)/server-url.tsx @@ -63,8 +63,6 @@ export default function ServerUrlScreen() { const destructiveColor = useCSSVariable("--color-destructive") as string; const mutedFgColor = useCSSVariable("--color-muted-foreground") as string; - const [isFirstLaunch] = useState(() => !hasStoredServerUrl()); - // Icon pulse animation const iconOpacity = useSharedValue(1); const iconAnimatedStyle = useAnimatedStyle(() => ({ @@ -94,14 +92,6 @@ export default function ServerUrlScreen() { }; }, []); - // Auto-focus on first launch - useEffect(() => { - if (isFirstLaunch) { - const timer = setTimeout(() => inputRef.current?.focus(), 500); - return () => clearTimeout(timer); - } - }, [isFirstLaunch]); - const handleChangeText = (text: string) => { setUrl(text); if (connection.phase === "error") { @@ -114,6 +104,14 @@ export default function ServerUrlScreen() { if (!trimmed) return; const fullUrl = normalizeUrl(trimmed); + + try { + new URL(fullUrl); + } catch { + setConnection({ phase: "error", error: "invalid_url" }); + Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error); + return; + } setConnection({ phase: "connecting" }); const result = await validateServerUrl(fullUrl); @@ -133,6 +131,16 @@ export default function ServerUrlScreen() { const isConnecting = connection.phase === "connecting"; const isSuccess = connection.phase === "success"; + const trimmedUrl = url.trim().replace(/\/+$/, ""); + const isValidUrl = (() => { + if (!trimmedUrl) return false; + try { + new URL(normalizeUrl(trimmedUrl)); + return true; + } catch { + return false; + } + })(); const isDisabled = isConnecting || isSuccess; return ( @@ -187,7 +195,11 @@ export default function ServerUrlScreen() { ) : ( - )} diff --git a/apps/native/src/components/auth-screen.tsx b/apps/native/src/components/auth-screen.tsx index ffefec5..4a2b6ff 100644 --- a/apps/native/src/components/auth-screen.tsx +++ b/apps/native/src/components/auth-screen.tsx @@ -1,6 +1,7 @@ import type { ReactNode } from "react"; import type { StyleProp, ViewStyle } from "react-native"; import { ScrollView } from "react-native"; +import { KeyboardAvoidingView } from "react-native-keyboard-controller"; import Animated, { FadeIn } from "react-native-reanimated"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { SofaLogo } from "@/components/ui/sofa-logo"; @@ -22,37 +23,41 @@ export function AuthScreen({ const insets = useSafeAreaInsets(); return ( - - + - {logoStyle ? ( - + + {logoStyle ? ( + + + + ) : ( - - ) : ( - - )} - - {title} - - {subtitle && ( - {subtitle} - )} - - {children} - + )} + + {title} + + {subtitle && ( + + {subtitle} + + )} + + {children} + + ); }