feat: production-ready UI with onboarding wizard and dashboard
CodexBar Mobile Build / build-android (push) Successful in 31m31s
CodexBar Mobile Build / release (push) Failing after 23s

- 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:
2026-06-24 17:52:06 +02:00
parent 6bcef323be
commit d5b0f9c833
15 changed files with 1398 additions and 107 deletions
+12
View File
@@ -0,0 +1,12 @@
import { Stack } from "expo-router";
export default function OnboardingLayout() {
return (
<Stack
screenOptions={{
headerShown: false,
animation: "slide_from_right",
}}
/>
);
}
+189
View File
@@ -0,0 +1,189 @@
import { useState, useCallback, useEffect } from "react";
import {
View,
Text,
TouchableOpacity,
TextInput,
KeyboardAvoidingView,
Platform,
ScrollView,
} from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import { router } from "expo-router";
import { MaterialIcons } from "@expo/vector-icons";
import { saveClaudeSessionKey, loadClaudeSessionKey } from "@/lib/storage";
export default function OnboardingClaudeScreen() {
const [savedKey, setSavedKey] = useState<string | null>(null);
const [pendingKey, setPendingKey] = useState("");
const [error, setError] = useState<string | null>(null);
useEffect(() => {
loadClaudeSessionKey().then((k) => {
if (k) setSavedKey(k);
});
}, []);
const canSave = pendingKey.trim().startsWith("sk-ant-");
const handleSave = useCallback(async () => {
const trimmed = pendingKey.trim();
if (!trimmed.startsWith("sk-ant-")) {
setError("Session key must start with sk-ant-");
return;
}
await saveClaudeSessionKey(trimmed);
setSavedKey(trimmed);
setPendingKey("");
setError(null);
}, [pendingKey]);
return (
<SafeAreaView className="flex-1 bg-neutral-950">
<KeyboardAvoidingView
className="flex-1"
behavior={Platform.OS === "ios" ? "padding" : "height"}
>
<ScrollView
className="flex-1"
contentContainerStyle={{ flexGrow: 1 }}
keyboardShouldPersistTaps="handled"
>
<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" }}
>
<MaterialIcons name="check" size={14} color="white" />
</View>
<View className="w-12 h-0.5" style={{ backgroundColor: "#d97706" }} />
<View
className="w-6 h-6 rounded-full items-center justify-center"
style={{ backgroundColor: "#d97706" }}
>
<Text className="text-white 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: "#d9770622" }}
>
<MaterialIcons name="psychology" size={32} color="#d97706" />
</View>
<Text className="text-3xl font-bold text-white mb-3">
Connect Claude.ai
</Text>
<Text className="text-neutral-400 text-base leading-relaxed mb-8">
Enter your session key to monitor your Claude.ai usage windows.
</Text>
{savedKey ? (
<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: "#d9770622" }}
>
<MaterialIcons name="check-circle" size={22} color="#d97706" />
</View>
<View className="flex-1">
<Text className="text-white font-semibold">Connected</Text>
<Text className="text-neutral-500 text-sm font-mono">
{savedKey.slice(0, 16)}
</Text>
</View>
<TouchableOpacity
onPress={() => {
setSavedKey(null);
setPendingKey("");
}}
>
<Text className="text-neutral-400 text-xs">Change</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>
)}
<TextInput
value={pendingKey}
onChangeText={(t) => {
setPendingKey(t);
setError(null);
}}
placeholder="sk-ant-..."
placeholderTextColor="#525252"
autoCapitalize="none"
autoCorrect={false}
secureTextEntry
className="border border-neutral-800 rounded-xl px-4 py-3.5 text-sm text-white bg-neutral-900 mb-3"
/>
<TouchableOpacity
onPress={handleSave}
disabled={!canSave}
className="rounded-2xl py-4 items-center mb-4"
style={{ backgroundColor: canSave ? "#d97706" : "#1a1a1a" }}
>
<Text
className="font-bold text-base"
style={{ color: canSave ? "white" : "#525252" }}
>
Save Session Key
</Text>
</TouchableOpacity>
<View className="bg-neutral-900 rounded-xl p-4 border border-neutral-800">
<Text className="text-neutral-400 text-xs font-semibold uppercase tracking-widest mb-2">
How to find your session key
</Text>
<Text className="text-neutral-500 text-sm leading-relaxed">
Open Claude.ai in Chrome DevTools (F12) Application Cookies claude.ai find{" "}
<Text className="text-neutral-300 font-mono">sessionKey</Text>
</Text>
</View>
</View>
)}
</View>
{/* Bottom actions */}
<View className="gap-y-3">
<TouchableOpacity
onPress={() => router.push("/onboarding/done")}
className="rounded-2xl py-4 items-center"
style={{ backgroundColor: savedKey ? "#d97706" : "#1a1a1a" }}
>
<Text
className="font-bold text-base"
style={{ color: savedKey ? "white" : "#a3a3a3" }}
>
Continue
</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={() => router.push("/onboarding/done")}
className="py-3 items-center"
>
<Text className="text-neutral-600 text-sm">Skip for now</Text>
</TouchableOpacity>
</View>
</View>
</ScrollView>
</KeyboardAvoidingView>
</SafeAreaView>
);
}
+154
View File
@@ -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>
);
}
+120
View File
@@ -0,0 +1,120 @@
import { useEffect, useState } from "react";
import { View, Text, TouchableOpacity } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import { router } from "expo-router";
import { MaterialIcons } from "@expo/vector-icons";
import { markOnboardingComplete } from "@/lib/setupState";
import { loadCodexAuth, loadClaudeSessionKey } from "@/lib/storage";
export default function OnboardingDoneScreen() {
const [codexConfigured, setCodexConfigured] = useState(false);
const [claudeConfigured, setClaudeConfigured] = useState(false);
useEffect(() => {
Promise.all([loadCodexAuth(), loadClaudeSessionKey()]).then(
([codex, claude]) => {
setCodexConfigured(!!codex);
setClaudeConfigured(!!claude);
}
);
}, []);
const handleEnterApp = async () => {
await markOnboardingComplete();
router.replace("/(tabs)");
};
const noneConfigured = !codexConfigured && !claudeConfigured;
return (
<SafeAreaView className="flex-1 bg-neutral-950">
<View className="flex-1 px-8 items-center justify-center">
{/* Icon */}
<View
className="w-20 h-20 rounded-3xl items-center justify-center mb-8"
style={{ backgroundColor: "#10a37f22" }}
>
<MaterialIcons
name={noneConfigured ? "info-outline" : "check-circle"}
size={40}
color={noneConfigured ? "#a3a3a3" : "#10a37f"}
/>
</View>
{/* Title */}
<Text className="text-4xl font-bold text-white mb-3 text-center">
{noneConfigured ? "Almost there" : "You're all set!"}
</Text>
<Text className="text-neutral-400 text-base text-center leading-relaxed mb-10">
{noneConfigured
? "No services configured yet. You can set them up anytime in Settings."
: "Your services are connected and ready to monitor."}
</Text>
{/* Summary cards */}
<View className="w-full gap-y-3 mb-10">
<View className="flex-row items-center gap-x-3 bg-neutral-900 rounded-2xl p-4 border border-neutral-800">
<View
className="w-10 h-10 rounded-xl items-center justify-center"
style={{
backgroundColor: codexConfigured ? "#10a37f22" : "#27272a",
}}
>
<MaterialIcons
name={codexConfigured ? "check-circle" : "radio-button-unchecked"}
size={22}
color={codexConfigured ? "#10a37f" : "#525252"}
/>
</View>
<View className="flex-1">
<Text
className="font-semibold"
style={{ color: codexConfigured ? "white" : "#525252" }}
>
Codex CLI
</Text>
<Text className="text-neutral-600 text-sm">
{codexConfigured ? "Connected" : "Not configured"}
</Text>
</View>
</View>
<View className="flex-row items-center gap-x-3 bg-neutral-900 rounded-2xl p-4 border border-neutral-800">
<View
className="w-10 h-10 rounded-xl items-center justify-center"
style={{
backgroundColor: claudeConfigured ? "#d9770622" : "#27272a",
}}
>
<MaterialIcons
name={claudeConfigured ? "check-circle" : "radio-button-unchecked"}
size={22}
color={claudeConfigured ? "#d97706" : "#525252"}
/>
</View>
<View className="flex-1">
<Text
className="font-semibold"
style={{ color: claudeConfigured ? "white" : "#525252" }}
>
Claude.ai
</Text>
<Text className="text-neutral-600 text-sm">
{claudeConfigured ? "Connected" : "Not configured"}
</Text>
</View>
</View>
</View>
{/* CTA */}
<TouchableOpacity
onPress={handleEnterApp}
className="w-full rounded-2xl py-4 items-center"
style={{ backgroundColor: "#10a37f" }}
>
<Text className="text-white font-bold text-base">Open App</Text>
</TouchableOpacity>
</View>
</SafeAreaView>
);
}
+82
View File
@@ -0,0 +1,82 @@
import { View, Text, TouchableOpacity } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import { router } from "expo-router";
import { MaterialIcons } from "@expo/vector-icons";
export default function WelcomeScreen() {
return (
<SafeAreaView className="flex-1 bg-neutral-950">
<View className="flex-1 px-8 justify-between py-12">
{/* Logo + title */}
<View className="items-center mt-8">
<View className="flex-row items-center justify-center mb-8">
<View
className="w-16 h-16 rounded-2xl items-center justify-center"
style={{ backgroundColor: "#10a37f" }}
>
<MaterialIcons name="auto-awesome" size={28} color="white" />
</View>
<View
className="w-16 h-16 rounded-2xl items-center justify-center -ml-5"
style={{ backgroundColor: "#d97706" }}
>
<MaterialIcons name="psychology" size={28} color="white" />
</View>
</View>
<Text className="text-5xl font-bold text-white mb-4 tracking-tight">
Codexbar
</Text>
<Text className="text-neutral-400 text-base text-center leading-relaxed">
Monitor your Codex CLI and Claude.ai{"\n"}usage limits at a glance.
</Text>
</View>
{/* Feature pills */}
<View className="gap-y-3">
<View className="flex-row items-center gap-x-3 bg-neutral-900 rounded-2xl p-4 border border-neutral-800">
<View
className="w-10 h-10 rounded-xl items-center justify-center"
style={{ backgroundColor: "#10a37f22" }}
>
<MaterialIcons name="auto-awesome" size={20} color="#10a37f" />
</View>
<View className="flex-1">
<Text className="text-white font-semibold mb-0.5">Codex CLI</Text>
<Text className="text-neutral-500 text-sm">
Rate limits · Credits · Windows
</Text>
</View>
</View>
<View className="flex-row items-center gap-x-3 bg-neutral-900 rounded-2xl p-4 border border-neutral-800">
<View
className="w-10 h-10 rounded-xl items-center justify-center"
style={{ backgroundColor: "#d9770622" }}
>
<MaterialIcons name="psychology" size={20} color="#d97706" />
</View>
<View className="flex-1">
<Text className="text-white font-semibold mb-0.5">Claude.ai</Text>
<Text className="text-neutral-500 text-sm">
5-hour · 7-day · Model usage
</Text>
</View>
</View>
</View>
{/* CTA */}
<View>
<TouchableOpacity
onPress={() => router.push("/onboarding/codex")}
className="rounded-2xl py-4 items-center"
style={{ backgroundColor: "#10a37f" }}
>
<Text className="text-white font-bold text-base">Get Started</Text>
</TouchableOpacity>
<Text className="text-neutral-600 text-xs text-center mt-4">
You can configure services at any time in Settings
</Text>
</View>
</View>
</SafeAreaView>
);
}