import { useEffect, useState, useCallback } from "react"; import { View, Text, TouchableOpacity, Alert, ScrollView, TextInput, Switch, KeyboardAvoidingView, } 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 { 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 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 ; } function SettingsTabContent() { 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 [timeError, setTimeError] = useState(null); 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(); const validationError = validateClaudeSessionKey(trimmed); if (validationError) { setClaudeError(validationError); 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 }); setTimeInput(formatTime(parsed.hour, parsed.minute)); setTimeError(null); } else { setTimeError("Use HH:MM format, from 00:00 to 23:59."); } }, [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 = isClaudeSessionKeyValid(pendingKey); const inlineClaudeError = claudeError ?? (pendingKey ? validateClaudeSessionKey(pendingKey) : null); const appVersion = Constants.expoConfig?.version ?? "1.0.0"; return ( {/* Header */} Settings {/* Codex CLI section */} {codexAuth ? ( <> ) : ( )} {/* Claude.ai section */} {claudeKey ? ( <> ) : ( {inlineClaudeError && ( {inlineClaudeError} )} { 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: COLORS.disabled, true: COLORS.codex }} thumbColor="white" /> {notif.settings.dailyEnabled && ( <> Time { 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 }} /> {timeError && ( {timeError} )} )} {/* Threshold alert */} Low quota alert Alert when weekly quota runs low void notif.update({ thresholdEnabled: v })} trackColor={{ false: COLORS.disabled, true: COLORS.claude }} thumbColor="white" /> {notif.settings.thresholdEnabled && ( <> Alert below % remaining )} )} {/* App section */} ); } export default function SettingsTab() { return ( ); }