import { Ionicons } from "@expo/vector-icons"; import { useEffect, useState } from "react"; import { Pressable, StyleSheet, Text, View } from "react-native"; import { Input } from "@/components/ui/Input"; import { fonts, spacing } from "@/constants/theme"; import { useAccounts } from "@/contexts/AccountsContext"; import { useAppTheme } from "@/contexts/ThemeContext"; import { DEFAULT_API_URL } from "@/lib/config"; import { formatServerHost, isServerConfigValid, resolveServerMode, resolveServerUrl, SERVER_MODE_OPTIONS, type ServerMode, } from "@/lib/server-mode"; type AuthServerPickerProps = { onReadyChange?: (ready: boolean) => void; /** When true, picker sits inside the auth card with no outer margin. */ embedded?: boolean; }; function modeSummary(mode: ServerMode, selfHostedUrl: string) { if (mode === "official") return "Official"; const host = formatServerHost(selfHostedUrl); return host || "Self-hosted"; } export function AuthServerPicker({ onReadyChange, embedded = false }: AuthServerPickerProps) { const { colors } = useAppTheme(); const { apiUrl, setInstanceUrl } = useAccounts(); const [expanded, setExpanded] = useState(false); const [mode, setMode] = useState(() => resolveServerMode(apiUrl)); const [selfHostedUrl, setSelfHostedUrl] = useState(() => resolveServerMode(apiUrl) === "self-hosted" ? apiUrl : "", ); const [urlError, setUrlError] = useState(null); const ready = isServerConfigValid(mode, selfHostedUrl); useEffect(() => { onReadyChange?.(ready); }, [ready, onReadyChange]); useEffect(() => { const nextMode = resolveServerMode(apiUrl); setMode(nextMode); if (nextMode === "self-hosted") { setSelfHostedUrl(apiUrl); } }, [apiUrl]); async function applyMode(nextMode: ServerMode) { setMode(nextMode); setUrlError(null); if (nextMode === "official") { try { await setInstanceUrl(DEFAULT_API_URL); setExpanded(false); } catch (err) { setUrlError(err instanceof Error ? err.message : "Could not set server"); } return; } setExpanded(true); const resolved = resolveServerUrl("self-hosted", selfHostedUrl); if (!resolved) return; try { await setInstanceUrl(resolved); } catch (err) { setUrlError(err instanceof Error ? err.message : "Could not set server"); } } async function commitSelfHostedUrl() { const resolved = resolveServerUrl("self-hosted", selfHostedUrl); if (!resolved) { setUrlError("Enter a valid server URL (e.g. beenvoice.app or localhost:3000)"); return; } try { const saved = await setInstanceUrl(resolved); setSelfHostedUrl(saved); setUrlError(null); setExpanded(false); } catch (err) { setUrlError(err instanceof Error ? err.message : "Could not save server URL"); } } return ( setExpanded((open) => !open)} hitSlop={8} style={({ pressed }) => [styles.trigger, pressed && styles.pressed]} > Server ยท{" "} {modeSummary(mode, selfHostedUrl)} {expanded ? ( {SERVER_MODE_OPTIONS.map((option) => { const selected = option.value === mode; return ( void applyMode(option.value)} style={({ pressed }) => [ styles.option, { borderColor: colors.border, backgroundColor: selected ? colors.muted : "transparent", }, pressed && styles.pressed, ]} > {option.label} {selected ? ( ) : null} ); })} {mode === "self-hosted" ? ( <> { setSelfHostedUrl(value); setUrlError(null); }} onBlur={() => void commitSelfHostedUrl()} onSubmitEditing={() => void commitSelfHostedUrl()} autoCapitalize="none" autoCorrect={false} keyboardType="url" placeholder="beenvoice.app or localhost:3000" required error={urlError ?? undefined} /> Use your Mac's LAN IP on a physical device. ) : null} ) : null} ); } const styles = StyleSheet.create({ wrapper: { gap: spacing.sm, marginBottom: spacing.md, }, wrapperEmbedded: { marginBottom: 0, }, trigger: { flexDirection: "row", alignItems: "center", justifyContent: "center", gap: spacing.xs, minHeight: 36, }, pressed: { opacity: 0.7, }, triggerText: { fontSize: 13, fontFamily: fonts.body, }, summary: { fontFamily: fonts.bodyMedium, fontSize: 13, }, panel: { borderWidth: 1, borderRadius: 14, padding: spacing.md, gap: spacing.sm, }, option: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", minHeight: 44, paddingHorizontal: spacing.md, borderRadius: 10, borderWidth: 1, }, optionLabel: { fontFamily: fonts.bodyMedium, fontSize: 15, }, hint: { fontSize: 12, fontFamily: fonts.body, lineHeight: 16, }, });