import { useCallback, useRef, useState } from 'react' import { View, Text, StyleSheet, Pressable, Linking, ActivityIndicator, ScrollView } from 'react-native' import { useSafeAreaInsets } from 'react-native-safe-area-context' import { useFocusEffect, useRouter } from 'expo-router' import { ChevronLeft, ChevronRight, Info, Bell, Wrench, Shield, LifeBuoy, Mic, Globe, Terminal as TerminalIcon, KeyRound } from 'lucide-react-native' import { colors, radii, spacing, typography } from '../src/theme/mobile-theme' import { loadPendingHostCredentialCleanup, subscribePendingHostCredentialCleanup } from '../src/transport/host-credential-cleanup' import { retryPendingHostCredentialCleanup } from '../src/transport/host-store' export default function SettingsScreen() { const router = useRouter() const insets = useSafeAreaInsets() const [pendingCredentialIds, setPendingCredentialIds] = useState([]) const [credentialStorageUnreadable, setCredentialStorageUnreadable] = useState(false) const [retryingCredentialCleanup, setRetryingCredentialCleanup] = useState(false) const [credentialRetryFailed, setCredentialRetryFailed] = useState(false) const credentialRefreshGenerationRef = useRef(0) useFocusEffect( useCallback(() => { let active = true setCredentialRetryFailed(false) const refresh = () => { const generation = ++credentialRefreshGenerationRef.current void loadPendingHostCredentialCleanup().then((state) => { if (active && generation === credentialRefreshGenerationRef.current) { setPendingCredentialIds(state.ids) setCredentialStorageUnreadable(state.storageUnreadable) // Why: neutral copy once the queue is confirmed empty so a later // pending set does not inherit a previous Retry failure message. if (state.ids.length === 0 && !state.storageUnreadable) { setCredentialRetryFailed(false) } } }) } const unsubscribe = subscribePendingHostCredentialCleanup(refresh) refresh() return () => { active = false credentialRefreshGenerationRef.current += 1 unsubscribe() } }, []) ) const retryCredentialCleanup = useCallback(async () => { if (retryingCredentialCleanup) { return } setCredentialRetryFailed(false) setRetryingCredentialCleanup(true) try { const result = await retryPendingHostCredentialCleanup() setPendingCredentialIds(result.remainingIds) setCredentialStorageUnreadable(result.storageUnreadable) setCredentialRetryFailed(result.remainingIds.length > 0 || result.storageUnreadable) } catch { setCredentialRetryFailed(true) } finally { setRetryingCredentialCleanup(false) } }, [retryingCredentialCleanup]) const pendingCredentialCount = pendingCredentialIds.length // Why: show the cleanup card whenever cleanup is pending OR the durable queue // is unreadable — an unreadable queue can hide an orphaned token, so keep a // retry affordance rather than a silently-empty (hidden) section. const showCredentialCleanup = pendingCredentialCount > 0 || credentialStorageUnreadable return ( router.back()}> Settings [styles.row, pressed && styles.rowPressed]} onPress={() => router.push('/terminal-settings')} > Terminal [styles.row, pressed && styles.rowPressed]} onPress={() => router.push('/browser-settings')} > Browser [styles.row, pressed && styles.rowPressed]} onPress={() => router.push('/voice-settings')} > Voice [styles.row, pressed && styles.rowPressed]} onPress={() => router.push('/notifications')} > Notifications [styles.row, pressed && styles.rowPressed]} onPress={() => router.push('/troubleshoot')} > Troubleshooting [styles.row, pressed && styles.rowPressed]} onPress={() => router.push('/about')} > About {showCredentialCleanup ? ( Pairing credential cleanup {credentialRetryFailed ? "Cleanup still couldn't be confirmed. Try again later." : pendingCredentialCount > 0 ? `Couldn't confirm cleanup for ${pendingCredentialCount} credential${pendingCredentialCount === 1 ? '' : 's'} on this device.` : "Couldn't check cleanup status on this device. Retry to be safe."} [ styles.retryButton, pressed && !retryingCredentialCleanup && styles.rowPressed ]} onPress={() => void retryCredentialCleanup()} > {retryingCredentialCleanup ? ( ) : ( Retry )} ) : null} [styles.row, pressed && styles.rowPressed]} onPress={() => void Linking.openURL('https://www.onorca.dev/privacy')} > Privacy Policy [styles.row, pressed && styles.rowPressed]} onPress={() => void Linking.openURL('https://github.com/stablyai/orca/issues')} > Support ) } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: colors.bgBase, paddingHorizontal: spacing.lg }, topRow: { flexDirection: 'row', alignItems: 'center', marginBottom: spacing.xl }, backButton: { width: 36, height: 36, borderRadius: 18, alignItems: 'center', justifyContent: 'center', marginRight: spacing.sm }, heading: { fontSize: 20, fontWeight: '700', color: colors.textPrimary }, section: { backgroundColor: colors.bgPanel, borderRadius: 12, overflow: 'hidden' }, sectionSpacer: { marginTop: spacing.md }, row: { flexDirection: 'row', alignItems: 'center', gap: spacing.sm + 2, paddingVertical: spacing.md, paddingHorizontal: spacing.md + 2 }, rowPressed: { backgroundColor: colors.bgRaised }, rowLabel: { flex: 1, fontSize: typography.bodySize, fontWeight: '500', color: colors.textPrimary }, credentialCleanupRow: { flexDirection: 'row', alignItems: 'center', gap: spacing.sm + 2, paddingVertical: spacing.md, paddingHorizontal: spacing.md + 2 }, credentialCleanupCopy: { flex: 1, gap: spacing.xs }, credentialCleanupTitle: { fontSize: typography.bodySize, fontWeight: '500', color: colors.textPrimary }, rowHint: { fontSize: typography.metaSize, color: colors.textSecondary, lineHeight: 17 }, retryButton: { width: 72, height: 32, borderRadius: radii.button, backgroundColor: colors.bgRaised, alignItems: 'center', justifyContent: 'center' }, retryButtonText: { fontSize: typography.metaSize, fontWeight: '600', color: colors.textPrimary }, separator: { height: StyleSheet.hairlineWidth, backgroundColor: colors.borderSubtle, marginHorizontal: spacing.md } })