by codex: feat. General Improvements
This commit is contained in:
@@ -2,6 +2,7 @@ import { Tabs } from "expo-router";
|
||||
import { useColorScheme } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { MaterialIcons } from "@expo/vector-icons";
|
||||
import { COLORS } from "@/lib/constants";
|
||||
|
||||
export default function TabLayout() {
|
||||
const colorScheme = useColorScheme();
|
||||
@@ -12,7 +13,7 @@ export default function TabLayout() {
|
||||
<Tabs
|
||||
screenOptions={{
|
||||
headerShown: false,
|
||||
tabBarActiveTintColor: "#10a37f",
|
||||
tabBarActiveTintColor: COLORS.codex,
|
||||
tabBarInactiveTintColor: isDark ? "#52525b" : "#a1a1aa",
|
||||
tabBarStyle: {
|
||||
backgroundColor: isDark ? "#09090b" : "#ffffff",
|
||||
|
||||
+34
-12
@@ -14,8 +14,11 @@ import { MaterialIcons } from "@expo/vector-icons";
|
||||
import { useClaudeUsage } from "@/hooks/useClaudeUsage";
|
||||
import { UsageStat } from "@/components/UsageStat";
|
||||
import { ErrorMessage } from "@/components/ErrorMessage";
|
||||
import { LastUpdated } from "@/components/LastUpdated";
|
||||
import { ScreenErrorBoundary } from "@/components/ScreenErrorBoundary";
|
||||
import { COLORS } from "@/lib/constants";
|
||||
|
||||
export default function ClaudeTab() {
|
||||
function ClaudeTabContent() {
|
||||
const {
|
||||
auth,
|
||||
pendingKey,
|
||||
@@ -23,15 +26,16 @@ export default function ClaudeTab() {
|
||||
pendingOrg,
|
||||
setPendingOrg,
|
||||
usage,
|
||||
lastFetchedAt,
|
||||
status,
|
||||
error,
|
||||
keyValidationError,
|
||||
canSaveKey,
|
||||
saveKey,
|
||||
refresh,
|
||||
clearKey,
|
||||
} = useClaudeUsage();
|
||||
|
||||
const canSave = pendingKey.trim().startsWith("sk-ant-");
|
||||
|
||||
return (
|
||||
<SafeAreaView
|
||||
className="flex-1 bg-neutral-50 dark:bg-neutral-950"
|
||||
@@ -49,7 +53,7 @@ export default function ClaudeTab() {
|
||||
<RefreshControl
|
||||
refreshing={status === "loading"}
|
||||
onRefresh={refresh}
|
||||
tintColor="#d97706"
|
||||
tintColor={COLORS.claude}
|
||||
/>
|
||||
}
|
||||
>
|
||||
@@ -57,9 +61,9 @@ export default function ClaudeTab() {
|
||||
<View className="flex-row items-center gap-x-3 py-5">
|
||||
<View
|
||||
className="w-9 h-9 rounded-xl items-center justify-center"
|
||||
style={{ backgroundColor: "#d9770622" }}
|
||||
style={{ backgroundColor: `${COLORS.claude}22` }}
|
||||
>
|
||||
<MaterialIcons name="psychology" size={18} color="#d97706" />
|
||||
<MaterialIcons name="psychology" size={18} color={COLORS.claude} />
|
||||
</View>
|
||||
<View>
|
||||
<Text className="text-xl font-bold text-neutral-900 dark:text-white tracking-tight">
|
||||
@@ -111,6 +115,11 @@ export default function ClaudeTab() {
|
||||
secureTextEntry
|
||||
className="border border-neutral-200 dark:border-neutral-700 rounded-xl px-3 py-3 text-sm text-neutral-900 dark:text-white bg-white dark:bg-neutral-800 mb-2"
|
||||
/>
|
||||
{keyValidationError && (
|
||||
<Text selectable className="mb-2 text-xs text-red-500">
|
||||
{keyValidationError}
|
||||
</Text>
|
||||
)}
|
||||
<TextInput
|
||||
value={pendingOrg}
|
||||
onChangeText={setPendingOrg}
|
||||
@@ -122,13 +131,15 @@ export default function ClaudeTab() {
|
||||
/>
|
||||
<TouchableOpacity
|
||||
onPress={saveKey}
|
||||
disabled={!canSave}
|
||||
disabled={!canSaveKey}
|
||||
className="py-3.5 px-4 rounded-xl items-center"
|
||||
style={{ backgroundColor: canSave ? "#d97706" : "#e5e5e5" }}
|
||||
style={{
|
||||
backgroundColor: canSaveKey ? COLORS.claude : COLORS.disabled,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
className="font-semibold text-sm"
|
||||
style={{ color: canSave ? "white" : "#a3a3a3" }}
|
||||
style={{ color: canSaveKey ? "white" : COLORS.muted }}
|
||||
>
|
||||
Save Session Key
|
||||
</Text>
|
||||
@@ -162,11 +173,11 @@ export default function ClaudeTab() {
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
{status === "loading" && (
|
||||
<ActivityIndicator color="#d97706" style={{ marginVertical: 24 }} />
|
||||
{status === "loading" && !usage && (
|
||||
<ActivityIndicator color={COLORS.claude} style={{ marginVertical: 24 }} />
|
||||
)}
|
||||
{status === "error" && error && <ErrorMessage message={error} />}
|
||||
{status === "success" && usage && (
|
||||
{usage && (
|
||||
<View>
|
||||
<UsageStat
|
||||
label="5-hour window"
|
||||
@@ -198,6 +209,9 @@ export default function ClaudeTab() {
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
<View className="mt-4 border-t border-neutral-100 pt-3 dark:border-neutral-800">
|
||||
<LastUpdated timestamp={lastFetchedAt} />
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
@@ -206,3 +220,11 @@ export default function ClaudeTab() {
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ClaudeTab() {
|
||||
return (
|
||||
<ScreenErrorBoundary screenName="Claude">
|
||||
<ClaudeTabContent />
|
||||
</ScreenErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
+33
-10
@@ -11,10 +11,22 @@ import { MaterialIcons } from "@expo/vector-icons";
|
||||
import { useCodexUsage } from "@/hooks/useCodexUsage";
|
||||
import { UsageStat } from "@/components/UsageStat";
|
||||
import { ErrorMessage } from "@/components/ErrorMessage";
|
||||
import { LastUpdated } from "@/components/LastUpdated";
|
||||
import { ScreenErrorBoundary } from "@/components/ScreenErrorBoundary";
|
||||
import { COLORS } from "@/lib/constants";
|
||||
import { windowLabel } from "@/lib/timeUtils";
|
||||
|
||||
export default function CodexTab() {
|
||||
const { auth, usage, status, error, importAuthFile, refresh, clearAuth } =
|
||||
function CodexTabContent() {
|
||||
const {
|
||||
auth,
|
||||
usage,
|
||||
lastFetchedAt,
|
||||
status,
|
||||
error,
|
||||
importAuthFile,
|
||||
refresh,
|
||||
clearAuth,
|
||||
} =
|
||||
useCodexUsage();
|
||||
|
||||
return (
|
||||
@@ -29,7 +41,7 @@ export default function CodexTab() {
|
||||
<RefreshControl
|
||||
refreshing={status === "loading"}
|
||||
onRefresh={refresh}
|
||||
tintColor="#10a37f"
|
||||
tintColor={COLORS.codex}
|
||||
/>
|
||||
}
|
||||
>
|
||||
@@ -37,9 +49,9 @@ export default function CodexTab() {
|
||||
<View className="flex-row items-center gap-x-3 py-5">
|
||||
<View
|
||||
className="w-9 h-9 rounded-xl items-center justify-center"
|
||||
style={{ backgroundColor: "#10a37f22" }}
|
||||
style={{ backgroundColor: `${COLORS.codex}22` }}
|
||||
>
|
||||
<MaterialIcons name="auto-awesome" size={18} color="#10a37f" />
|
||||
<MaterialIcons name="auto-awesome" size={18} color={COLORS.codex} />
|
||||
</View>
|
||||
<View>
|
||||
<Text className="text-xl font-bold text-neutral-900 dark:text-white tracking-tight">
|
||||
@@ -84,7 +96,7 @@ export default function CodexTab() {
|
||||
<TouchableOpacity
|
||||
onPress={importAuthFile}
|
||||
className="flex-row items-center justify-center gap-x-2 py-3.5 px-4 rounded-xl"
|
||||
style={{ backgroundColor: "#10a37f" }}
|
||||
style={{ backgroundColor: COLORS.codex }}
|
||||
>
|
||||
<MaterialIcons name="upload-file" size={16} color="white" />
|
||||
<Text className="text-white font-semibold text-sm">
|
||||
@@ -112,11 +124,11 @@ export default function CodexTab() {
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
{status === "loading" && (
|
||||
<ActivityIndicator color="#10a37f" style={{ marginVertical: 24 }} />
|
||||
{status === "loading" && !usage && (
|
||||
<ActivityIndicator color={COLORS.codex} style={{ marginVertical: 24 }} />
|
||||
)}
|
||||
{status === "error" && error && <ErrorMessage message={error} />}
|
||||
{status === "success" && usage && (
|
||||
{usage && (
|
||||
<View>
|
||||
{/* Plan badge */}
|
||||
<View className="flex-row items-center mb-5">
|
||||
@@ -145,7 +157,7 @@ export default function CodexTab() {
|
||||
</Text>
|
||||
{usage.credits.unlimited ? (
|
||||
<View className="flex-row items-center gap-x-2">
|
||||
<MaterialIcons name="all-inclusive" size={16} color="#10a37f" />
|
||||
<MaterialIcons name="all-inclusive" size={16} color={COLORS.codex} />
|
||||
<Text className="text-sm font-semibold text-green-600 dark:text-green-400">
|
||||
Unlimited
|
||||
</Text>
|
||||
@@ -158,6 +170,9 @@ export default function CodexTab() {
|
||||
<Text className="text-sm text-neutral-400">No credits</Text>
|
||||
)}
|
||||
</View>
|
||||
<View className="mt-4 border-t border-neutral-100 pt-3 dark:border-neutral-800">
|
||||
<LastUpdated timestamp={lastFetchedAt} />
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
@@ -165,3 +180,11 @@ export default function CodexTab() {
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
export default function CodexTab() {
|
||||
return (
|
||||
<ScreenErrorBoundary screenName="Codex">
|
||||
<CodexTabContent />
|
||||
</ScreenErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
+43
-27
@@ -15,6 +15,9 @@ import { useClaudeUsage } from "@/hooks/useClaudeUsage";
|
||||
import { onUsageDataLoaded } from "@/lib/notifications";
|
||||
import { ProgressBar } from "@/components/ProgressBar";
|
||||
import { ResetCountdown } from "@/components/ResetCountdown";
|
||||
import { LastUpdated } from "@/components/LastUpdated";
|
||||
import { ScreenErrorBoundary } from "@/components/ScreenErrorBoundary";
|
||||
import { COLORS, getUsageColor } from "@/lib/constants";
|
||||
import { windowLabel } from "@/lib/timeUtils";
|
||||
|
||||
function UsageRow({
|
||||
@@ -28,8 +31,7 @@ function UsageRow({
|
||||
resetAtSeconds?: number;
|
||||
resetAtISO?: string;
|
||||
}) {
|
||||
const color =
|
||||
percent >= 90 ? "#ef4444" : percent >= 70 ? "#f59e0b" : "#10a37f";
|
||||
const color = getUsageColor(percent);
|
||||
return (
|
||||
<View className="mb-4">
|
||||
<View className="flex-row justify-between items-baseline mb-1.5">
|
||||
@@ -65,33 +67,33 @@ function SetupPrompt({ label }: { label: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
export default function DashboardTab() {
|
||||
function DashboardTabContent() {
|
||||
const codex = useCodexUsage();
|
||||
const claude = useClaudeUsage();
|
||||
|
||||
// Fire daily digest reschedule + threshold check whenever fresh data arrives
|
||||
useEffect(() => {
|
||||
if (codex.status === "success" || claude.status === "success") {
|
||||
if (codex.usage || claude.usage) {
|
||||
void onUsageDataLoaded(
|
||||
claude.status === "success" ? claude.usage : null,
|
||||
codex.status === "success" ? codex.usage : null
|
||||
claude.usage,
|
||||
codex.usage
|
||||
);
|
||||
}
|
||||
}, [codex.status, claude.status]);
|
||||
}, [codex.lastFetchedAt, claude.lastFetchedAt]);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
codex.reload();
|
||||
claude.reload();
|
||||
}, [])
|
||||
void codex.reloadCredentials();
|
||||
void claude.reloadCredentials();
|
||||
}, [codex.reloadCredentials, claude.reloadCredentials])
|
||||
);
|
||||
|
||||
const isRefreshing =
|
||||
codex.status === "loading" || claude.status === "loading";
|
||||
|
||||
const handleRefresh = () => {
|
||||
codex.refresh();
|
||||
claude.refresh();
|
||||
void codex.refresh();
|
||||
void claude.refresh();
|
||||
};
|
||||
|
||||
const connectedCount = [codex.auth, claude.auth].filter(Boolean).length;
|
||||
@@ -108,7 +110,7 @@ export default function DashboardTab() {
|
||||
<RefreshControl
|
||||
refreshing={isRefreshing}
|
||||
onRefresh={handleRefresh}
|
||||
tintColor="#10a37f"
|
||||
tintColor={COLORS.codex}
|
||||
/>
|
||||
}
|
||||
>
|
||||
@@ -135,21 +137,21 @@ export default function DashboardTab() {
|
||||
onPress={handleRefresh}
|
||||
className="w-9 h-9 rounded-xl bg-neutral-100 dark:bg-neutral-800 items-center justify-center"
|
||||
>
|
||||
<MaterialIcons name="refresh" size={18} color="#10a37f" />
|
||||
<MaterialIcons name="refresh" size={18} color={COLORS.codex} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Codex card */}
|
||||
<View className="rounded-2xl bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 mb-4 overflow-hidden">
|
||||
<View style={{ height: 3, backgroundColor: "#10a37f" }} />
|
||||
<View style={{ height: 3, backgroundColor: COLORS.codex }} />
|
||||
<View className="p-4">
|
||||
<View className="flex-row items-center justify-between mb-4">
|
||||
<View className="flex-row items-center gap-x-2.5">
|
||||
<View
|
||||
className="w-8 h-8 rounded-xl items-center justify-center"
|
||||
style={{ backgroundColor: "#10a37f18" }}
|
||||
style={{ backgroundColor: `${COLORS.codex}18` }}
|
||||
>
|
||||
<MaterialIcons name="auto-awesome" size={16} color="#10a37f" />
|
||||
<MaterialIcons name="auto-awesome" size={16} color={COLORS.codex} />
|
||||
</View>
|
||||
<Text className="text-sm font-semibold text-neutral-900 dark:text-white">
|
||||
Codex CLI
|
||||
@@ -175,9 +177,9 @@ export default function DashboardTab() {
|
||||
{!codex.auth && (
|
||||
<SetupPrompt label="Import auth.json to see your Codex rate limits." />
|
||||
)}
|
||||
{codex.auth && codex.status === "loading" && (
|
||||
{codex.auth && codex.status === "loading" && !codex.usage && (
|
||||
<ActivityIndicator
|
||||
color="#10a37f"
|
||||
color={COLORS.codex}
|
||||
style={{ marginVertical: 16 }}
|
||||
/>
|
||||
)}
|
||||
@@ -189,7 +191,7 @@ export default function DashboardTab() {
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
{codex.status === "success" && codex.usage && (
|
||||
{codex.usage && (
|
||||
<View>
|
||||
<UsageRow
|
||||
label={`${windowLabel(codex.usage.rate_limit.primary_window.limit_window_seconds)} window`}
|
||||
@@ -220,13 +222,16 @@ export default function DashboardTab() {
|
||||
<MaterialIcons
|
||||
name="all-inclusive"
|
||||
size={14}
|
||||
color="#10a37f"
|
||||
color={COLORS.codex}
|
||||
/>
|
||||
<Text className="text-xs text-green-600 dark:text-green-400">
|
||||
Unlimited credits
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
<View className="mt-3 border-t border-neutral-100 pt-3 dark:border-neutral-800">
|
||||
<LastUpdated timestamp={codex.lastFetchedAt} />
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
@@ -234,15 +239,15 @@ export default function DashboardTab() {
|
||||
|
||||
{/* Claude card */}
|
||||
<View className="rounded-2xl bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 mb-4 overflow-hidden">
|
||||
<View style={{ height: 3, backgroundColor: "#d97706" }} />
|
||||
<View style={{ height: 3, backgroundColor: COLORS.claude }} />
|
||||
<View className="p-4">
|
||||
<View className="flex-row items-center justify-between mb-4">
|
||||
<View className="flex-row items-center gap-x-2.5">
|
||||
<View
|
||||
className="w-8 h-8 rounded-xl items-center justify-center"
|
||||
style={{ backgroundColor: "#d9770618" }}
|
||||
style={{ backgroundColor: `${COLORS.claude}18` }}
|
||||
>
|
||||
<MaterialIcons name="psychology" size={16} color="#d97706" />
|
||||
<MaterialIcons name="psychology" size={16} color={COLORS.claude} />
|
||||
</View>
|
||||
<Text className="text-sm font-semibold text-neutral-900 dark:text-white">
|
||||
Claude.ai
|
||||
@@ -259,9 +264,9 @@ export default function DashboardTab() {
|
||||
{!claude.auth && (
|
||||
<SetupPrompt label="Add your session key to see Claude usage windows." />
|
||||
)}
|
||||
{claude.auth && claude.status === "loading" && (
|
||||
{claude.auth && claude.status === "loading" && !claude.usage && (
|
||||
<ActivityIndicator
|
||||
color="#d97706"
|
||||
color={COLORS.claude}
|
||||
style={{ marginVertical: 16 }}
|
||||
/>
|
||||
)}
|
||||
@@ -273,7 +278,7 @@ export default function DashboardTab() {
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
{claude.status === "success" && claude.usage && (
|
||||
{claude.usage && (
|
||||
<View>
|
||||
<UsageRow
|
||||
label="5-hour window"
|
||||
@@ -305,6 +310,9 @@ export default function DashboardTab() {
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
<View className="mt-3 border-t border-neutral-100 pt-3 dark:border-neutral-800">
|
||||
<LastUpdated timestamp={claude.lastFetchedAt} />
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
@@ -313,3 +321,11 @@ export default function DashboardTab() {
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
export default function DashboardTab() {
|
||||
return (
|
||||
<ScreenErrorBoundary screenName="Dashboard">
|
||||
<DashboardTabContent />
|
||||
</ScreenErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
+64
-46
@@ -8,7 +8,6 @@ import {
|
||||
TextInput,
|
||||
Switch,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
} from "react-native";
|
||||
import { SafeAreaView } from "react-native-safe-area-context";
|
||||
import { router, useFocusEffect } from "expo-router";
|
||||
@@ -26,22 +25,16 @@ import {
|
||||
import { pickAndReadCodexAuth } from "@/lib/fileReader";
|
||||
import { resetOnboarding } from "@/lib/setupState";
|
||||
import { useNotificationSettings } from "@/hooks/useNotificationSettings";
|
||||
import {
|
||||
isClaudeSessionKeyValid,
|
||||
validateClaudeSessionKey,
|
||||
} from "@/lib/claudeCredentials";
|
||||
import { COLORS } from "@/lib/constants";
|
||||
import { formatTime, parseTimeInput } from "@/lib/timeUtils";
|
||||
import { ScreenErrorBoundary } from "@/components/ScreenErrorBoundary";
|
||||
import type { CodexAuth } from "@/types/codex";
|
||||
import Constants from "expo-constants";
|
||||
|
||||
function parseTimeInput(s: string): { hour: number; minute: number } | null {
|
||||
const m = s.trim().match(/^(\d{1,2}):(\d{2})$/);
|
||||
if (!m) return null;
|
||||
const h = parseInt(m[1], 10);
|
||||
const min = parseInt(m[2], 10);
|
||||
if (h < 0 || h > 23 || min < 0 || min > 59) return null;
|
||||
return { hour: h, minute: min };
|
||||
}
|
||||
|
||||
function formatTime(hour: number, minute: number): string {
|
||||
return `${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
function SettingRow({
|
||||
icon,
|
||||
label,
|
||||
@@ -120,7 +113,7 @@ function Divider() {
|
||||
return <View className="ml-16 mr-0 h-px bg-neutral-100 dark:bg-neutral-800" />;
|
||||
}
|
||||
|
||||
export default function SettingsTab() {
|
||||
function SettingsTabContent() {
|
||||
const [codexAuth, setCodexAuth] = useState<CodexAuth | null>(null);
|
||||
const [claudeKey, setClaudeKey] = useState<string | null>(null);
|
||||
const [pendingKey, setPendingKey] = useState("");
|
||||
@@ -129,6 +122,7 @@ export default function SettingsTab() {
|
||||
|
||||
const notif = useNotificationSettings();
|
||||
const [timeInput, setTimeInput] = useState("");
|
||||
const [timeError, setTimeError] = useState<string | null>(null);
|
||||
const [thresholdInput, setThresholdInput] = useState("");
|
||||
|
||||
// Sync local text inputs when settings load
|
||||
@@ -173,8 +167,9 @@ export default function SettingsTab() {
|
||||
|
||||
const handleSaveClaude = async () => {
|
||||
const trimmed = pendingKey.trim();
|
||||
if (!trimmed.startsWith("sk-ant-")) {
|
||||
setClaudeError("Key must start with sk-ant-");
|
||||
const validationError = validateClaudeSessionKey(trimmed);
|
||||
if (validationError) {
|
||||
setClaudeError(validationError);
|
||||
return;
|
||||
}
|
||||
const orgTrimmed = pendingOrg.trim() || undefined;
|
||||
@@ -249,9 +244,10 @@ export default function SettingsTab() {
|
||||
const parsed = parseTimeInput(timeInput);
|
||||
if (parsed) {
|
||||
void notif.update({ dailyHour: parsed.hour, dailyMinute: parsed.minute });
|
||||
setTimeInput(formatTime(parsed.hour, parsed.minute));
|
||||
setTimeError(null);
|
||||
} else {
|
||||
// Reset to last valid value
|
||||
setTimeInput(formatTime(notif.settings.dailyHour, notif.settings.dailyMinute));
|
||||
setTimeError("Use HH:MM format, from 00:00 to 23:59.");
|
||||
}
|
||||
}, [timeInput, notif]);
|
||||
|
||||
@@ -264,7 +260,9 @@ export default function SettingsTab() {
|
||||
}
|
||||
}, [thresholdInput, notif]);
|
||||
|
||||
const canSaveClaude = pendingKey.trim().startsWith("sk-ant-");
|
||||
const canSaveClaude = isClaudeSessionKeyValid(pendingKey);
|
||||
const inlineClaudeError =
|
||||
claudeError ?? (pendingKey ? validateClaudeSessionKey(pendingKey) : null);
|
||||
const appVersion = Constants.expoConfig?.version ?? "1.0.0";
|
||||
|
||||
return (
|
||||
@@ -274,7 +272,7 @@ export default function SettingsTab() {
|
||||
>
|
||||
<KeyboardAvoidingView
|
||||
className="flex-1"
|
||||
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||||
behavior={process.env.EXPO_OS === "ios" ? "padding" : "height"}
|
||||
>
|
||||
<ScrollView
|
||||
className="flex-1"
|
||||
@@ -341,9 +339,9 @@ export default function SettingsTab() {
|
||||
</>
|
||||
) : (
|
||||
<View className="p-4">
|
||||
{claudeError && (
|
||||
<Text className="text-xs text-red-500 mb-2">
|
||||
{claudeError}
|
||||
{inlineClaudeError && (
|
||||
<Text selectable className="text-xs text-red-500 mb-2">
|
||||
{inlineClaudeError}
|
||||
</Text>
|
||||
)}
|
||||
<TextInput
|
||||
@@ -373,12 +371,14 @@ export default function SettingsTab() {
|
||||
disabled={!canSaveClaude}
|
||||
className="py-3 px-4 rounded-xl items-center"
|
||||
style={{
|
||||
backgroundColor: canSaveClaude ? "#d97706" : "#e5e5e5",
|
||||
backgroundColor: canSaveClaude
|
||||
? COLORS.claude
|
||||
: COLORS.disabled,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
className="font-semibold text-sm"
|
||||
style={{ color: canSaveClaude ? "white" : "#a3a3a3" }}
|
||||
style={{ color: canSaveClaude ? "white" : COLORS.muted }}
|
||||
>
|
||||
Save Session Key
|
||||
</Text>
|
||||
@@ -446,7 +446,7 @@ export default function SettingsTab() {
|
||||
<Switch
|
||||
value={notif.settings.dailyEnabled}
|
||||
onValueChange={(v) => void notif.update({ dailyEnabled: v })}
|
||||
trackColor={{ false: "#e5e5e5", true: "#10a37f" }}
|
||||
trackColor={{ false: COLORS.disabled, true: COLORS.codex }}
|
||||
thumbColor="white"
|
||||
/>
|
||||
</View>
|
||||
@@ -454,25 +454,35 @@ export default function SettingsTab() {
|
||||
{notif.settings.dailyEnabled && (
|
||||
<>
|
||||
<Divider />
|
||||
<View className="flex-row items-center gap-x-3 py-3 px-4">
|
||||
<View className="w-8 items-center">
|
||||
<MaterialIcons name="schedule" size={20} color="#737373" />
|
||||
<View className="px-4 py-3">
|
||||
<View className="flex-row items-center gap-x-3">
|
||||
<View className="w-8 items-center">
|
||||
<MaterialIcons name="schedule" size={20} color="#737373" />
|
||||
</View>
|
||||
<Text className="flex-1 text-sm font-medium text-neutral-800 dark:text-neutral-100">
|
||||
Time
|
||||
</Text>
|
||||
<TextInput
|
||||
value={timeInput}
|
||||
onChangeText={(value) => {
|
||||
setTimeInput(value);
|
||||
setTimeError(null);
|
||||
}}
|
||||
onBlur={handleTimeBlur}
|
||||
placeholder="09:00"
|
||||
placeholderTextColor={COLORS.muted}
|
||||
keyboardType="numbers-and-punctuation"
|
||||
returnKeyType="done"
|
||||
maxLength={5}
|
||||
className="text-sm font-mono text-neutral-600 dark:text-neutral-300 text-right"
|
||||
style={{ minWidth: 52 }}
|
||||
/>
|
||||
</View>
|
||||
<Text className="flex-1 text-sm font-medium text-neutral-800 dark:text-neutral-100">
|
||||
Time
|
||||
</Text>
|
||||
<TextInput
|
||||
value={timeInput}
|
||||
onChangeText={setTimeInput}
|
||||
onBlur={handleTimeBlur}
|
||||
placeholder="09:00"
|
||||
placeholderTextColor="#a3a3a3"
|
||||
keyboardType="numbers-and-punctuation"
|
||||
returnKeyType="done"
|
||||
maxLength={5}
|
||||
className="text-sm font-mono text-neutral-600 dark:text-neutral-300 text-right"
|
||||
style={{ minWidth: 52 }}
|
||||
/>
|
||||
{timeError && (
|
||||
<Text selectable className="ml-11 mt-1.5 text-xs text-red-500">
|
||||
{timeError}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
@@ -495,7 +505,7 @@ export default function SettingsTab() {
|
||||
<Switch
|
||||
value={notif.settings.thresholdEnabled}
|
||||
onValueChange={(v) => void notif.update({ thresholdEnabled: v })}
|
||||
trackColor={{ false: "#e5e5e5", true: "#d97706" }}
|
||||
trackColor={{ false: COLORS.disabled, true: COLORS.claude }}
|
||||
thumbColor="white"
|
||||
/>
|
||||
</View>
|
||||
@@ -554,3 +564,11 @@ export default function SettingsTab() {
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
export default function SettingsTab() {
|
||||
return (
|
||||
<ScreenErrorBoundary screenName="Settings">
|
||||
<SettingsTabContent />
|
||||
</ScreenErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
+2
-1
@@ -2,6 +2,7 @@ import { useEffect } from "react";
|
||||
import { View, ActivityIndicator } from "react-native";
|
||||
import { router } from "expo-router";
|
||||
import { hasCompletedOnboarding } from "@/lib/setupState";
|
||||
import { COLORS } from "@/lib/constants";
|
||||
|
||||
export default function Index() {
|
||||
useEffect(() => {
|
||||
@@ -12,7 +13,7 @@ export default function Index() {
|
||||
|
||||
return (
|
||||
<View className="flex-1 bg-neutral-950 items-center justify-center">
|
||||
<ActivityIndicator color="#10a37f" size="large" />
|
||||
<ActivityIndicator color={COLORS.codex} size="large" />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
+27
-16
@@ -5,7 +5,6 @@ import {
|
||||
TouchableOpacity,
|
||||
TextInput,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
ScrollView,
|
||||
} from "react-native";
|
||||
import { SafeAreaView } from "react-native-safe-area-context";
|
||||
@@ -18,6 +17,11 @@ import {
|
||||
loadClaudeSessionKey,
|
||||
loadClaudeLastActiveOrg,
|
||||
} from "@/lib/storage";
|
||||
import {
|
||||
isClaudeSessionKeyValid,
|
||||
validateClaudeSessionKey,
|
||||
} from "@/lib/claudeCredentials";
|
||||
import { COLORS } from "@/lib/constants";
|
||||
|
||||
export default function OnboardingClaudeScreen() {
|
||||
const [savedKey, setSavedKey] = useState<string | null>(null);
|
||||
@@ -35,12 +39,15 @@ export default function OnboardingClaudeScreen() {
|
||||
);
|
||||
}, []);
|
||||
|
||||
const canSave = pendingKey.trim().startsWith("sk-ant-");
|
||||
const canSave = isClaudeSessionKeyValid(pendingKey);
|
||||
const inlineError =
|
||||
error ?? (pendingKey ? validateClaudeSessionKey(pendingKey) : null);
|
||||
|
||||
const handleSave = useCallback(async () => {
|
||||
const trimmed = pendingKey.trim();
|
||||
if (!trimmed.startsWith("sk-ant-")) {
|
||||
setError("Session key must start with sk-ant-");
|
||||
const validationError = validateClaudeSessionKey(trimmed);
|
||||
if (validationError) {
|
||||
setError(validationError);
|
||||
return;
|
||||
}
|
||||
const orgTrimmed = pendingOrg.trim() || undefined;
|
||||
@@ -58,7 +65,7 @@ export default function OnboardingClaudeScreen() {
|
||||
<SafeAreaView className="flex-1 bg-neutral-950">
|
||||
<KeyboardAvoidingView
|
||||
className="flex-1"
|
||||
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||||
behavior={process.env.EXPO_OS === "ios" ? "padding" : "height"}
|
||||
>
|
||||
<ScrollView
|
||||
className="flex-1"
|
||||
@@ -77,14 +84,14 @@ export default function OnboardingClaudeScreen() {
|
||||
<View className="flex-row items-center gap-x-2">
|
||||
<View
|
||||
className="w-6 h-6 rounded-full items-center justify-center"
|
||||
style={{ backgroundColor: "#10a37f" }}
|
||||
style={{ backgroundColor: COLORS.codex }}
|
||||
>
|
||||
<MaterialIcons name="check" size={14} color="white" />
|
||||
</View>
|
||||
<View className="w-12 h-0.5" style={{ backgroundColor: "#d97706" }} />
|
||||
<View className="w-12 h-0.5" style={{ backgroundColor: COLORS.claude }} />
|
||||
<View
|
||||
className="w-6 h-6 rounded-full items-center justify-center"
|
||||
style={{ backgroundColor: "#d97706" }}
|
||||
style={{ backgroundColor: COLORS.claude }}
|
||||
>
|
||||
<Text className="text-white text-xs font-bold">2</Text>
|
||||
</View>
|
||||
@@ -96,9 +103,9 @@ export default function OnboardingClaudeScreen() {
|
||||
<View>
|
||||
<View
|
||||
className="w-16 h-16 rounded-2xl items-center justify-center mb-6"
|
||||
style={{ backgroundColor: "#d9770622" }}
|
||||
style={{ backgroundColor: `${COLORS.claude}22` }}
|
||||
>
|
||||
<MaterialIcons name="psychology" size={32} color="#d97706" />
|
||||
<MaterialIcons name="psychology" size={32} color={COLORS.claude} />
|
||||
</View>
|
||||
<Text className="text-3xl font-bold text-white mb-3">
|
||||
Connect Claude.ai
|
||||
@@ -112,9 +119,9 @@ export default function OnboardingClaudeScreen() {
|
||||
<View className="flex-row items-center gap-x-3 p-4">
|
||||
<View
|
||||
className="w-10 h-10 rounded-xl items-center justify-center"
|
||||
style={{ backgroundColor: "#d9770622" }}
|
||||
style={{ backgroundColor: `${COLORS.claude}22` }}
|
||||
>
|
||||
<MaterialIcons name="check-circle" size={22} color="#d97706" />
|
||||
<MaterialIcons name="check-circle" size={22} color={COLORS.claude} />
|
||||
</View>
|
||||
<View className="flex-1">
|
||||
<Text className="text-white font-semibold">Connected</Text>
|
||||
@@ -143,9 +150,9 @@ export default function OnboardingClaudeScreen() {
|
||||
</View>
|
||||
) : (
|
||||
<View>
|
||||
{error && (
|
||||
{inlineError && (
|
||||
<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>
|
||||
<Text selectable className="text-red-400 text-sm">{inlineError}</Text>
|
||||
</View>
|
||||
)}
|
||||
<TextInput
|
||||
@@ -174,7 +181,9 @@ export default function OnboardingClaudeScreen() {
|
||||
onPress={handleSave}
|
||||
disabled={!canSave}
|
||||
className="rounded-2xl py-4 items-center mb-4"
|
||||
style={{ backgroundColor: canSave ? "#d97706" : "#1a1a1a" }}
|
||||
style={{
|
||||
backgroundColor: canSave ? COLORS.claude : "#1a1a1a",
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
className="font-bold text-base"
|
||||
@@ -201,7 +210,9 @@ export default function OnboardingClaudeScreen() {
|
||||
<TouchableOpacity
|
||||
onPress={() => router.push("/onboarding/done")}
|
||||
className="rounded-2xl py-4 items-center"
|
||||
style={{ backgroundColor: savedKey ? "#d97706" : "#1a1a1a" }}
|
||||
style={{
|
||||
backgroundColor: savedKey ? COLORS.claude : "#1a1a1a",
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
className="font-bold text-base"
|
||||
|
||||
@@ -6,6 +6,7 @@ import { MaterialIcons } from "@expo/vector-icons";
|
||||
import { pickAndReadCodexAuth } from "@/lib/fileReader";
|
||||
import { saveCodexAuth, loadCodexAuth } from "@/lib/storage";
|
||||
import type { CodexAuth } from "@/types/codex";
|
||||
import { COLORS } from "@/lib/constants";
|
||||
|
||||
export default function OnboardingCodexScreen() {
|
||||
const [auth, setAuth] = useState<CodexAuth | null>(null);
|
||||
@@ -45,7 +46,7 @@ export default function OnboardingCodexScreen() {
|
||||
<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" }}>
|
||||
<View className="w-6 h-6 rounded-full items-center justify-center" style={{ backgroundColor: COLORS.codex }}>
|
||||
<Text className="text-white text-xs font-bold">1</Text>
|
||||
</View>
|
||||
<View className="w-12 h-0.5 bg-neutral-800" />
|
||||
@@ -60,9 +61,9 @@ export default function OnboardingCodexScreen() {
|
||||
<View>
|
||||
<View
|
||||
className="w-16 h-16 rounded-2xl items-center justify-center mb-6"
|
||||
style={{ backgroundColor: "#10a37f22" }}
|
||||
style={{ backgroundColor: `${COLORS.codex}22` }}
|
||||
>
|
||||
<MaterialIcons name="auto-awesome" size={32} color="#10a37f" />
|
||||
<MaterialIcons name="auto-awesome" size={32} color={COLORS.codex} />
|
||||
</View>
|
||||
<Text className="text-3xl font-bold text-white mb-3">
|
||||
Connect Codex CLI
|
||||
@@ -80,9 +81,9 @@ export default function OnboardingCodexScreen() {
|
||||
<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" }}
|
||||
style={{ backgroundColor: `${COLORS.codex}22` }}
|
||||
>
|
||||
<MaterialIcons name="check-circle" size={22} color="#10a37f" />
|
||||
<MaterialIcons name="check-circle" size={22} color={COLORS.codex} />
|
||||
</View>
|
||||
<View className="flex-1">
|
||||
<Text className="text-white font-semibold">Connected</Text>
|
||||
@@ -107,10 +108,10 @@ export default function OnboardingCodexScreen() {
|
||||
onPress={importFile}
|
||||
disabled={loading}
|
||||
className="rounded-2xl py-4 items-center mb-3"
|
||||
style={{ backgroundColor: loading ? "#1a1a1a" : "#10a37f" }}
|
||||
style={{ backgroundColor: loading ? "#1a1a1a" : COLORS.codex }}
|
||||
>
|
||||
{loading ? (
|
||||
<ActivityIndicator color="#10a37f" />
|
||||
<ActivityIndicator color={COLORS.codex} />
|
||||
) : (
|
||||
<View className="flex-row items-center gap-x-2">
|
||||
<MaterialIcons name="upload-file" size={18} color="white" />
|
||||
@@ -132,7 +133,7 @@ export default function OnboardingCodexScreen() {
|
||||
<TouchableOpacity
|
||||
onPress={() => router.push("/onboarding/claude")}
|
||||
className="rounded-2xl py-4 items-center"
|
||||
style={{ backgroundColor: auth ? "#10a37f" : "#1a1a1a" }}
|
||||
style={{ backgroundColor: auth ? COLORS.codex : "#1a1a1a" }}
|
||||
>
|
||||
<Text
|
||||
className="font-bold text-base"
|
||||
|
||||
+12
-7
@@ -5,6 +5,7 @@ import { router } from "expo-router";
|
||||
import { MaterialIcons } from "@expo/vector-icons";
|
||||
import { markOnboardingComplete } from "@/lib/setupState";
|
||||
import { loadCodexAuth, loadClaudeSessionKey } from "@/lib/storage";
|
||||
import { COLORS } from "@/lib/constants";
|
||||
|
||||
export default function OnboardingDoneScreen() {
|
||||
const [codexConfigured, setCodexConfigured] = useState(false);
|
||||
@@ -32,12 +33,12 @@ export default function OnboardingDoneScreen() {
|
||||
{/* Icon */}
|
||||
<View
|
||||
className="w-20 h-20 rounded-3xl items-center justify-center mb-8"
|
||||
style={{ backgroundColor: "#10a37f22" }}
|
||||
style={{ backgroundColor: `${COLORS.codex}22` }}
|
||||
>
|
||||
<MaterialIcons
|
||||
name={noneConfigured ? "info-outline" : "check-circle"}
|
||||
size={40}
|
||||
color={noneConfigured ? "#a3a3a3" : "#10a37f"}
|
||||
color={noneConfigured ? COLORS.muted : COLORS.codex}
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -57,13 +58,15 @@ export default function OnboardingDoneScreen() {
|
||||
<View
|
||||
className="w-10 h-10 rounded-xl items-center justify-center"
|
||||
style={{
|
||||
backgroundColor: codexConfigured ? "#10a37f22" : "#27272a",
|
||||
backgroundColor: codexConfigured
|
||||
? `${COLORS.codex}22`
|
||||
: "#27272a",
|
||||
}}
|
||||
>
|
||||
<MaterialIcons
|
||||
name={codexConfigured ? "check-circle" : "radio-button-unchecked"}
|
||||
size={22}
|
||||
color={codexConfigured ? "#10a37f" : "#525252"}
|
||||
color={codexConfigured ? COLORS.codex : "#525252"}
|
||||
/>
|
||||
</View>
|
||||
<View className="flex-1">
|
||||
@@ -83,13 +86,15 @@ export default function OnboardingDoneScreen() {
|
||||
<View
|
||||
className="w-10 h-10 rounded-xl items-center justify-center"
|
||||
style={{
|
||||
backgroundColor: claudeConfigured ? "#d9770622" : "#27272a",
|
||||
backgroundColor: claudeConfigured
|
||||
? `${COLORS.claude}22`
|
||||
: "#27272a",
|
||||
}}
|
||||
>
|
||||
<MaterialIcons
|
||||
name={claudeConfigured ? "check-circle" : "radio-button-unchecked"}
|
||||
size={22}
|
||||
color={claudeConfigured ? "#d97706" : "#525252"}
|
||||
color={claudeConfigured ? COLORS.claude : "#525252"}
|
||||
/>
|
||||
</View>
|
||||
<View className="flex-1">
|
||||
@@ -110,7 +115,7 @@ export default function OnboardingDoneScreen() {
|
||||
<TouchableOpacity
|
||||
onPress={handleEnterApp}
|
||||
className="w-full rounded-2xl py-4 items-center"
|
||||
style={{ backgroundColor: "#10a37f" }}
|
||||
style={{ backgroundColor: COLORS.codex }}
|
||||
>
|
||||
<Text className="text-white font-bold text-base">Open App</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
@@ -2,6 +2,7 @@ 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 { COLORS } from "@/lib/constants";
|
||||
|
||||
export default function WelcomeScreen() {
|
||||
return (
|
||||
@@ -12,13 +13,13 @@ export default function WelcomeScreen() {
|
||||
<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" }}
|
||||
style={{ backgroundColor: COLORS.codex }}
|
||||
>
|
||||
<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" }}
|
||||
style={{ backgroundColor: COLORS.claude }}
|
||||
>
|
||||
<MaterialIcons name="psychology" size={28} color="white" />
|
||||
</View>
|
||||
@@ -36,9 +37,9 @@ export default function WelcomeScreen() {
|
||||
<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" }}
|
||||
style={{ backgroundColor: `${COLORS.codex}22` }}
|
||||
>
|
||||
<MaterialIcons name="auto-awesome" size={20} color="#10a37f" />
|
||||
<MaterialIcons name="auto-awesome" size={20} color={COLORS.codex} />
|
||||
</View>
|
||||
<View className="flex-1">
|
||||
<Text className="text-white font-semibold mb-0.5">Codex CLI</Text>
|
||||
@@ -50,9 +51,9 @@ export default function WelcomeScreen() {
|
||||
<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" }}
|
||||
style={{ backgroundColor: `${COLORS.claude}22` }}
|
||||
>
|
||||
<MaterialIcons name="psychology" size={20} color="#d97706" />
|
||||
<MaterialIcons name="psychology" size={20} color={COLORS.claude} />
|
||||
</View>
|
||||
<View className="flex-1">
|
||||
<Text className="text-white font-semibold mb-0.5">Claude.ai</Text>
|
||||
@@ -68,7 +69,7 @@ export default function WelcomeScreen() {
|
||||
<TouchableOpacity
|
||||
onPress={() => router.push("/onboarding/codex")}
|
||||
className="rounded-2xl py-4 items-center"
|
||||
style={{ backgroundColor: "#10a37f" }}
|
||||
style={{ backgroundColor: COLORS.codex }}
|
||||
>
|
||||
<Text className="text-white font-bold text-base">Get Started</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
Reference in New Issue
Block a user