feat: production-ready UI with onboarding wizard and dashboard
- Onboarding flow (welcome -> Codex setup -> Claude setup -> done) with dark-themed screens and step indicators; completion stored in SecureStore - Smart root redirect checks onboarding state on launch - Dashboard tab shows both services at a glance via ServiceStatusCard; useFocusEffect + reload() keeps it fresh when credentials change in tabs - Settings tab manages credentials and exposes re-run setup wizard - Polished Codex and Claude detail screens with service headers and empty states - 4-tab layout (Home, Codex, Claude, Settings) with dark-mode tab bar - Both hooks gain reload() for cross-tab credential refresh Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
import { useState, useCallback, useEffect } from "react";
|
||||
import { View, Text, TouchableOpacity, ActivityIndicator } from "react-native";
|
||||
import { SafeAreaView } from "react-native-safe-area-context";
|
||||
import { router } from "expo-router";
|
||||
import { MaterialIcons } from "@expo/vector-icons";
|
||||
import { pickAndReadCodexAuth } from "@/lib/fileReader";
|
||||
import { saveCodexAuth, loadCodexAuth } from "@/lib/storage";
|
||||
import type { CodexAuth } from "@/types/codex";
|
||||
|
||||
export default function OnboardingCodexScreen() {
|
||||
const [auth, setAuth] = useState<CodexAuth | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadCodexAuth().then((stored) => {
|
||||
if (stored) setAuth(stored);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const importFile = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const parsed = await pickAndReadCodexAuth();
|
||||
await saveCodexAuth(parsed);
|
||||
setAuth(parsed);
|
||||
} catch (e: unknown) {
|
||||
const msg = e instanceof Error ? e.message : "UNKNOWN_ERROR";
|
||||
if (msg !== "PICKER_CANCELLED") setError(msg);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<SafeAreaView className="flex-1 bg-neutral-950">
|
||||
<View className="flex-1 px-8 justify-between py-8">
|
||||
{/* Top: back + step indicator */}
|
||||
<View className="flex-row items-center justify-between">
|
||||
<TouchableOpacity
|
||||
onPress={() => router.back()}
|
||||
className="w-9 h-9 rounded-xl bg-neutral-900 items-center justify-center"
|
||||
>
|
||||
<MaterialIcons name="arrow-back" size={18} color="#a3a3a3" />
|
||||
</TouchableOpacity>
|
||||
<View className="flex-row items-center gap-x-2">
|
||||
<View className="w-6 h-6 rounded-full items-center justify-center" style={{ backgroundColor: "#10a37f" }}>
|
||||
<Text className="text-white text-xs font-bold">1</Text>
|
||||
</View>
|
||||
<View className="w-12 h-0.5 bg-neutral-800" />
|
||||
<View className="w-6 h-6 rounded-full bg-neutral-800 items-center justify-center">
|
||||
<Text className="text-neutral-500 text-xs font-bold">2</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View className="w-9" />
|
||||
</View>
|
||||
|
||||
{/* Content */}
|
||||
<View>
|
||||
<View
|
||||
className="w-16 h-16 rounded-2xl items-center justify-center mb-6"
|
||||
style={{ backgroundColor: "#10a37f22" }}
|
||||
>
|
||||
<MaterialIcons name="auto-awesome" size={32} color="#10a37f" />
|
||||
</View>
|
||||
<Text className="text-3xl font-bold text-white mb-3">
|
||||
Connect Codex CLI
|
||||
</Text>
|
||||
<Text className="text-neutral-400 text-base leading-relaxed mb-8">
|
||||
Import your{" "}
|
||||
<Text className="text-neutral-200 font-mono text-sm">
|
||||
~/.codex/auth.json
|
||||
</Text>{" "}
|
||||
file to track your rate limits and credits.
|
||||
</Text>
|
||||
|
||||
{/* Status / action */}
|
||||
{auth ? (
|
||||
<View className="flex-row items-center gap-x-3 bg-neutral-900 rounded-2xl p-4 border border-neutral-800 mb-4">
|
||||
<View
|
||||
className="w-10 h-10 rounded-xl items-center justify-center"
|
||||
style={{ backgroundColor: "#10a37f22" }}
|
||||
>
|
||||
<MaterialIcons name="check-circle" size={22} color="#10a37f" />
|
||||
</View>
|
||||
<View className="flex-1">
|
||||
<Text className="text-white font-semibold">Connected</Text>
|
||||
{auth.accountId && (
|
||||
<Text className="text-neutral-500 text-sm font-mono">
|
||||
{auth.accountId.slice(0, 12)}…
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
<TouchableOpacity onPress={importFile}>
|
||||
<Text className="text-neutral-400 text-xs">Re-import</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
) : (
|
||||
<View>
|
||||
{error && (
|
||||
<View className="bg-red-950 border border-red-900 rounded-xl px-4 py-3 mb-4">
|
||||
<Text className="text-red-400 text-sm">{error}</Text>
|
||||
</View>
|
||||
)}
|
||||
<TouchableOpacity
|
||||
onPress={importFile}
|
||||
disabled={loading}
|
||||
className="rounded-2xl py-4 items-center mb-3"
|
||||
style={{ backgroundColor: loading ? "#1a1a1a" : "#10a37f" }}
|
||||
>
|
||||
{loading ? (
|
||||
<ActivityIndicator color="#10a37f" />
|
||||
) : (
|
||||
<View className="flex-row items-center gap-x-2">
|
||||
<MaterialIcons name="upload-file" size={18} color="white" />
|
||||
<Text className="text-white font-bold text-base">
|
||||
Import auth.json
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
<Text className="text-neutral-600 text-xs text-center">
|
||||
Located at ~/.codex/auth.json on your Mac
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Bottom actions */}
|
||||
<View className="gap-y-3">
|
||||
<TouchableOpacity
|
||||
onPress={() => router.push("/onboarding/claude")}
|
||||
className="rounded-2xl py-4 items-center"
|
||||
style={{ backgroundColor: auth ? "#10a37f" : "#1a1a1a" }}
|
||||
>
|
||||
<Text
|
||||
className="font-bold text-base"
|
||||
style={{ color: auth ? "white" : "#a3a3a3" }}
|
||||
>
|
||||
Continue
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
onPress={() => router.push("/onboarding/claude")}
|
||||
className="py-3 items-center"
|
||||
>
|
||||
<Text className="text-neutral-600 text-sm">Skip for now</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user