import { useEffect, useState, useCallback } from "react"; import { View, Text, TouchableOpacity, Alert, ScrollView, TextInput, Switch, KeyboardAvoidingView, Platform, } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; import { router, useFocusEffect } from "expo-router"; import { MaterialIcons } from "@expo/vector-icons"; import { loadCodexAuth, clearCodexAuth, saveCodexAuth, loadClaudeSessionKey, clearClaudeSessionKey, saveClaudeSessionKey, saveClaudeLastActiveOrg, clearClaudeLastActiveOrg, } from "@/lib/storage"; import { pickAndReadCodexAuth } from "@/lib/fileReader"; import { resetOnboarding } from "@/lib/setupState"; import { useNotificationSettings } from "@/hooks/useNotificationSettings"; 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, value, onPress, destructive, disabled, }: { icon: React.ComponentProps["name"]; label: string; value?: string; onPress?: () => void; destructive?: boolean; disabled?: boolean; }) { return ( {label} {value && ( {value} )} {onPress && !disabled && ( )} ); } function SectionHeader({ title }: { title: string }) { return ( {title} ); } function SectionCard({ children }: { children: React.ReactNode }) { return ( {children} ); } function Divider() { return ; } export default function SettingsTab() { const [codexAuth, setCodexAuth] = useState(null); const [claudeKey, setClaudeKey] = useState(null); const [pendingKey, setPendingKey] = useState(""); const [pendingOrg, setPendingOrg] = useState(""); const [claudeError, setClaudeError] = useState(null); const notif = useNotificationSettings(); const [timeInput, setTimeInput] = useState(""); const [thresholdInput, setThresholdInput] = useState(""); // Sync local text inputs when settings load useEffect(() => { if (notif.loaded) { setTimeInput(formatTime(notif.settings.dailyHour, notif.settings.dailyMinute)); setThresholdInput(String(notif.settings.thresholdPct)); } }, [notif.loaded]); const reload = useCallback(() => { Promise.all([loadCodexAuth(), loadClaudeSessionKey()]).then( ([codex, claude]) => { setCodexAuth(codex); setClaudeKey(claude); } ); }, []); useEffect(() => { reload(); }, [reload]); useFocusEffect( useCallback(() => { reload(); }, [reload]) ); const handleImportCodex = async () => { try { const auth = await pickAndReadCodexAuth(); await saveCodexAuth(auth); setCodexAuth(auth); } catch (e: unknown) { const msg = e instanceof Error ? e.message : "UNKNOWN_ERROR"; if (msg !== "PICKER_CANCELLED") { Alert.alert("Import failed", msg); } } }; const handleSaveClaude = async () => { const trimmed = pendingKey.trim(); if (!trimmed.startsWith("sk-ant-")) { setClaudeError("Key must start with sk-ant-"); return; } const orgTrimmed = pendingOrg.trim() || undefined; await saveClaudeSessionKey(trimmed); if (orgTrimmed) await saveClaudeLastActiveOrg(orgTrimmed); else await clearClaudeLastActiveOrg(); setClaudeKey(trimmed); setPendingKey(""); setPendingOrg(""); setClaudeError(null); }; const handleClearCodex = () => { Alert.alert( "Clear Codex credentials", "This will remove your stored auth.json data. You can re-import it anytime.", [ { text: "Cancel", style: "cancel" }, { text: "Clear", style: "destructive", onPress: async () => { await clearCodexAuth(); setCodexAuth(null); }, }, ] ); }; const handleClearClaude = () => { Alert.alert( "Clear Claude session key", "Your session key will be removed. You can re-enter it anytime.", [ { text: "Cancel", style: "cancel" }, { text: "Clear", style: "destructive", onPress: async () => { await clearClaudeSessionKey(); setClaudeKey(null); }, }, ] ); }; const handleResetSetup = () => { Alert.alert( "Re-run Setup Wizard", "This will clear all credentials and restart the setup flow.", [ { text: "Cancel", style: "cancel" }, { text: "Reset & Re-run", style: "destructive", onPress: async () => { await Promise.all([ clearCodexAuth(), clearClaudeSessionKey(), resetOnboarding(), ]); router.replace("/onboarding"); }, }, ] ); }; const handleTimeBlur = useCallback(() => { const parsed = parseTimeInput(timeInput); if (parsed) { void notif.update({ dailyHour: parsed.hour, dailyMinute: parsed.minute }); } else { // Reset to last valid value setTimeInput(formatTime(notif.settings.dailyHour, notif.settings.dailyMinute)); } }, [timeInput, notif]); const handleThresholdBlur = useCallback(() => { const n = parseInt(thresholdInput, 10); if (!isNaN(n) && n >= 1 && n <= 99) { void notif.update({ thresholdPct: n }); } else { setThresholdInput(String(notif.settings.thresholdPct)); } }, [thresholdInput, notif]); const canSaveClaude = pendingKey.trim().startsWith("sk-ant-"); const appVersion = Constants.expoConfig?.version ?? "1.0.0"; return ( {/* Header */} Settings {/* Codex CLI section */} {codexAuth ? ( <> ) : ( )} {/* Claude.ai section */} {claudeKey ? ( <> ) : ( {claudeError && ( {claudeError} )} { setPendingKey(t); setClaudeError(null); }} placeholder="sk-ant-… (required)" placeholderTextColor="#a3a3a3" autoCapitalize="none" autoCorrect={false} 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-neutral-50 dark:bg-neutral-800 mb-2" /> Save Session Key Chrome DevTools → Application → Cookies → claude.ai →{" "} sessionKey )} {/* Notifications section */} {notif.permissionStatus !== "granted" ? ( { const granted = await notif.askPermissions(); if (!granted && notif.permissionStatus === "denied") { Alert.alert( "Notifications blocked", "Go to Settings → CodexBar → Notifications to enable them." ); } }} className="flex-row items-center gap-x-3 py-3.5 px-4" > Enable notifications {notif.permissionStatus === "denied" ? "Blocked — open system Settings to allow" : "Required to receive alerts"} {notif.permissionStatus !== "denied" && ( Allow )} ) : ( <> {/* Daily digest */} Daily digest Daily reminder with last-known usage void notif.update({ dailyEnabled: v })} trackColor={{ false: "#e5e5e5", true: "#10a37f" }} thumbColor="white" /> {notif.settings.dailyEnabled && ( <> Time )} {/* Threshold alert */} Low quota alert Alert when weekly quota runs low void notif.update({ thresholdEnabled: v })} trackColor={{ false: "#e5e5e5", true: "#d97706" }} thumbColor="white" /> {notif.settings.thresholdEnabled && ( <> Alert below % remaining )} )} {/* App section */} ); }