import * as SecureStore from "expo-secure-store"; import React, { createContext, useCallback, useContext, useEffect, useState, } from "react"; import { ActivityIndicator, KeyboardAvoidingView, Platform, StyleSheet, Text, TextInput, TouchableOpacity, View, } from "react-native"; import { colors } from "./styles"; const STORE_KEY = "instance_url"; // ─── Context ────────────────────────────────────────────────────────────────── interface InstanceUrlContextValue { baseUrl: string; /** Call to clear the stored URL and re-show the setup screen. */ resetUrl: () => void; } const InstanceUrlContext = createContext(null); export function useBaseUrl(): InstanceUrlContextValue { const ctx = useContext(InstanceUrlContext); if (!ctx) throw new Error("useBaseUrl must be used inside "); return ctx; } // ─── Validation helper ──────────────────────────────────────────────────────── async function validateUrl(url: string): Promise { const trimmed = url.replace(/\/+$/, ""); const res = await fetch(`${trimmed}/pull_full`, { method: "POST" }); if (!res.ok) throw new Error(`Server returned ${res.status}`); const data = await res.json(); if (typeof data !== "object" || data === null || !("state" in data)) { throw new Error("Unexpected response format"); } } // ─── Setup screen ───────────────────────────────────────────────────────────── function SetupScreen({ onSaved }: { onSaved: (url: string) => void }) { const [input, setInput] = useState(""); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const handleSave = async () => { const trimmed = input.trim().replace(/\/+$/, ""); if (!trimmed) { setError("Please enter a URL."); return; } setError(null); setLoading(true); try { await validateUrl(trimmed); await SecureStore.setItemAsync(STORE_KEY, trimmed); onSaved(trimmed); } catch (e: unknown) { const msg = e instanceof Error ? e.message : String(e); setError(`Could not connect: ${msg}`); } finally { setLoading(false); } }; return ( Server Setup Enter your instance URL to get started. The app will verify it before saving. { setInput(t); setError(null); }} editable={!loading} /> {error && {error}} {loading ? ( ) : ( Validate & Save )} ); } // ─── Provider ───────────────────────────────────────────────────────────────── export function InstanceUrlProvider({ children }: { children: React.ReactNode }) { const [baseUrl, setBaseUrl] = useState(null); // null = loading const [configured, setConfigured] = useState(false); // Load from storage on mount useEffect(() => { SecureStore.getItemAsync(STORE_KEY) .then((stored) => { if (stored) { setBaseUrl(stored); setConfigured(true); } else { setBaseUrl(""); // not configured setConfigured(false); } }) .catch(() => { setBaseUrl(""); setConfigured(false); }); }, []); const resetUrl = useCallback(async () => { await SecureStore.deleteItemAsync(STORE_KEY); setBaseUrl(""); setConfigured(false); }, []); const handleSaved = useCallback((url: string) => { setBaseUrl(url); setConfigured(true); }, []); // Still loading from storage if (baseUrl === null) { return ( ); } if (!configured) { return ; } return ( {children} ); } // ─── Styles ─────────────────────────────────────────────────────────────────── const styles = StyleSheet.create({ screen: { flex: 1, backgroundColor: colors.bg, alignItems: "center", justifyContent: "center", padding: 24, }, card: { backgroundColor: colors.surface, borderRadius: 16, padding: 24, width: "100%", maxWidth: 480, }, title: { fontSize: 32, fontWeight: "800", color: colors.textPrimary, marginTop: 8, marginBottom: 8, }, body: { color: colors.textSecondary, fontSize: 14, lineHeight: 20, marginBottom: 20, }, input: { backgroundColor: colors.bg, borderWidth: 1, borderColor: colors.border, borderRadius: 8, color: colors.textPrimary, fontSize: 14, paddingHorizontal: 12, paddingVertical: 10, marginBottom: 12, }, inputError: { borderColor: "#e55", }, errorText: { color: "#e55", fontSize: 13, marginBottom: 12, }, button: { backgroundColor: colors.accent, borderRadius: 8, paddingVertical: 12, alignItems: "center", }, buttonDisabled: { opacity: 0.5, }, buttonText: { color: "#fff", fontWeight: "600", fontSize: 15, }, });