import { Alert, ActivityIndicator, Linking, Platform, ScrollView, Switch, Text, TouchableOpacity, View, } from 'react-native'; import { useRef, useState } from 'react'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useRouter } from 'expo-router'; import { Ionicons } from '@expo/vector-icons'; import { MenuView, type MenuAction } from '@react-native-menu/menu'; import { TrueSheet } from '@lodev09/react-native-true-sheet'; import { useTranslation } from 'react-i18next'; import { LanguageIcon } from '../components/icons/LanguageIcon'; import { useColors } from '../lib/theme'; import { useAuthStore } from '../stores/auth'; import { useAppLockStore } from '../stores/appLock'; import { useThemeStore, type ThemeMode } from '../stores/theme'; import { useLanguageStore, type AppLanguage } from '../stores/language'; import { useUserPlan } from '../hooks/useUserPlan'; import { useMe, invalidateMe, type Plan } from '../hooks/useMe'; import { apiFetch } from '../lib/api'; import { AppHeader } from '../components/AppHeader'; // ─── Subscription Sheet ──────────────────────────────────────────────────── type SubscriptionSheetProps = { plan: 'free' | 'pro' | 'legend'; colors: import('../lib/theme').ColorScheme; t: (key: string) => string; }; const PLAN_ACCENT: Record = { free: '#737373', pro: '#007AFF', legend: '#f59e0b', }; function SubscriptionSheet({ plan, colors, t }: SubscriptionSheetProps) { const accentColor = PLAN_ACCENT[plan] ?? '#737373'; const planLabel = plan === 'legend' ? t('settings.subscription_plan_legend') : plan === 'pro' ? t('settings.subscription_plan_pro') : t('settings.subscription_plan_free'); return ( {t('settings.subscription_sheet_title')} {planLabel} {t('settings.subscription_sheet_body')} { // TODO: für iOS-Submission ggf. zu nicht-tippbarem Text degradieren // (Apple Guideline 3.1.1: externe Abo-Links können Review-Ablehnung triggern, // wenn sie als Kauf-Umgehung gewertet werden. Standalone-URL ohne Preis-Info // sollte ok sein, ist aber ungeprüft — bei Submission erneut prüfen.) Linking.openURL('https://rebreak.org/account'); }} activeOpacity={0.8} style={{ backgroundColor: accentColor, borderRadius: 14, paddingVertical: 14, alignItems: 'center', }} > {t('settings.subscription_sheet_cta')} ); } // ─── Settings Screen ─────────────────────────────────────────────────────── type PickerOption = { value: T; label: string }; type SectionRow = { /** Ionicons-name ODER eigenes SVG-Component (für custom icons wie LanguageIcon) */ icon: React.ComponentProps['name'] | React.ComponentType<{ size?: number; color?: string }>; label: string; sublabel: string; soon?: boolean; destructive?: boolean; value?: string; onPress?: () => void; /** Wenn gesetzt, wrappt UIMenu (anchored Pull-Down) statt onPress-trigger */ menu?: { title: string; actions: MenuAction[]; onSelect: (id: string) => void; }; /** Wenn gesetzt, rendert ein native UISwitch am End-Anchor statt Chevron/Value */ toggle?: { value: boolean; onValueChange: (next: boolean) => void; disabled?: boolean; }; }; type Section = { key: string; title: string; rows: SectionRow[]; }; export default function SettingsScreen() { const router = useRouter(); const insets = useSafeAreaInsets(); const { t } = useTranslation(); const { signOut } = useAuthStore(); const appLockEnabled = useAppLockStore((s) => s.enabled); const appLockAvailable = useAppLockStore((s) => s.available); const setAppLockEnabled = useAppLockStore((s) => s.setEnabled); const appLockAuthenticate = useAppLockStore((s) => s.authenticate); const { mode: themeMode, setMode: setThemeMode } = useThemeStore(); const { language, setLanguage } = useLanguageStore(); const { plan } = useUserPlan(); const colors = useColors(); // Lyra Voice: hardcoded ElevenLabs voice IDs (expandable by user later) // Backend endpoint PATCH /api/profile/me/demographics does NOT accept lyraVoiceId. // A dedicated PATCH /api/profile/me/lyra-voice endpoint is needed from backend-agent. // For now: picker is wired to local state only, changes are NOT persisted. const [selectedVoice, setSelectedVoice] = useState('EXAVITQu4vr4xnSDxMaL'); const { me } = useMe(); // TrueSheet ref for Lyra-Voice picker (UISheetPresentationController bottom-sheet) const voiceSheetRef = useRef(null); const subscriptionSheetRef = useRef(null); const planSheetRef = useRef(null); async function handleToggleAppLock(next: boolean) { if (next) { // Erst verifizieren, dass Face ID / Touch ID / Passcode klappt — sonst nicht aktivieren. // (Switch ist controlled über appLockEnabled → springt von selbst zurück wenn wir nicht persistieren.) const ok = await appLockAuthenticate(t('applock.prompt')); if (!ok) return; await setAppLockEnabled(true); } else { await setAppLockEnabled(false); } } async function handleSignOut() { Alert.alert(t('auth.signOut'), '', [ { text: t('common.cancel'), style: 'cancel' }, { text: t('auth.signOut'), style: 'cancel', onPress: async () => { await signOut(); router.replace('/'); }, }, ]); } const themeOptions: PickerOption[] = [ { value: 'system', label: t('settings.theme_system') }, { value: 'light', label: t('settings.theme_light') }, { value: 'dark', label: t('settings.theme_dark') }, ]; const themeLabel = themeMode === 'system' ? t('settings.theme_system') : themeMode === 'light' ? t('settings.theme_light') : t('settings.theme_dark'); const langOptions: PickerOption[] = [ { value: 'de', label: t('settings.language_de') }, { value: 'en', label: t('settings.language_en') }, ]; const voiceOptions: PickerOption[] = [ { value: 'EXAVITQu4vr4xnSDxMaL', label: t('settings.lyra_voice_sarah') }, { value: 'ThT5KcBeYPX3keUQqHPh', label: t('settings.lyra_voice_aria') }, { value: 'XB0fDUnXU5powFXDhCwa', label: t('settings.lyra_voice_charlotte') }, { value: 'Xb7hH8MSUJpSbSDYk0k2', label: t('settings.lyra_voice_alice') }, { value: 'pqHfZKP75CvOlQylNhV4', label: t('settings.lyra_voice_bill') }, ]; const selectedVoiceName = voiceOptions.find((v) => v.value === selectedVoice)?.label ?? t('settings.lyra_voice_sarah'); const sections: Section[] = [ // Profile-Section entfernt — Profile-Edits sind in /profile-Page direkt { key: 'security', title: t('settings.section_security'), rows: [ { icon: 'lock-closed-outline', label: t('settings.app_lock'), sublabel: !appLockAvailable ? t('settings.app_lock_unavailable') : Platform.OS === 'ios' ? t('settings.app_lock_desc') : t('settings.app_lock_desc_android'), toggle: { value: appLockEnabled, onValueChange: handleToggleAppLock, disabled: !appLockAvailable, }, }, ], }, { key: 'theme', title: t('settings.section_theme'), rows: [ { icon: 'color-palette-outline', label: t('settings.theme'), sublabel: t('settings.theme_desc'), value: themeLabel, menu: { title: t('settings.theme'), // Bewusst KEINE `image`-Props (SF-Symbols) — sonst rendert UIMenu mit // Icon-Slot reserviert und das Menu wird breiter/höher als bei Sprache. actions: themeOptions.map((opt) => ({ id: opt.value, title: opt.label, state: opt.value === themeMode ? 'on' : 'off', })), onSelect: (id) => setThemeMode(id as ThemeMode), }, }, { icon: LanguageIcon, label: t('settings.language'), sublabel: t('settings.language_desc'), value: language === 'de' ? t('settings.language_de') : t('settings.language_en'), menu: { title: t('settings.language'), actions: langOptions.map((opt) => ({ id: opt.value, title: opt.label, state: opt.value === language ? 'on' : 'off', })), onSelect: (id) => setLanguage(id as AppLanguage), }, }, ], }, { key: 'notifications', title: t('settings.section_notifications'), rows: [ { icon: 'notifications-outline', label: t('settings.notifications_push'), sublabel: t('settings.notifications_push_desc'), soon: true, }, { icon: 'flame-outline', label: t('settings.notifications_streak'), sublabel: t('settings.notifications_streak_desc'), soon: true, }, ], }, { key: 'devices', title: t('settings.section_devices'), rows: [ { icon: 'phone-portrait-outline', label: t('settings.devices'), sublabel: t('settings.devices_desc'), onPress: () => router.push('/devices'), }, { icon: 'star-outline', label: t('settings.subscription'), sublabel: t('settings.subscription_desc'), value: plan === 'legend' ? t('settings.subscription_plan_legend') : plan === 'pro' ? t('settings.subscription_plan_pro') : t('settings.subscription_plan_free'), onPress: () => subscriptionSheetRef.current?.present(), }, ], }, { key: 'lyra', title: t('settings.section_lyra'), rows: [ { icon: 'mic-outline', label: t('settings.lyra_voice'), sublabel: plan === 'legend' ? t('settings.lyra_voice_desc') : t('settings.lyra_voice_only_legend'), value: plan === 'legend' ? selectedVoiceName : undefined, // Voice picker is wired but changes are local-only until // PATCH /api/profile/me/lyra-voice endpoint is added by backend-agent. onPress: plan === 'legend' ? () => voiceSheetRef.current?.present() : undefined, soon: plan !== 'legend', }, ], }, { key: 'account', title: t('settings.danger_section'), rows: [ { icon: 'log-out-outline', label: t('settings.sign_out'), sublabel: '', onPress: handleSignOut, }, { icon: 'trash-outline', label: t('settings.delete_account'), sublabel: t('settings.delete_desc'), destructive: true, soon: true, }, ], }, ]; if (__DEV__) { sections.push({ key: 'debug', title: t('settings.section_debug'), rows: [ { icon: 'bug-outline', label: t('settings.debug_llm'), sublabel: t('settings.debug_llm_desc'), soon: true, }, { icon: 'volume-high-outline', label: t('settings.debug_tts'), sublabel: t('settings.debug_tts_desc'), soon: true, }, { icon: 'star-outline', label: t('settings.debug_plan'), sublabel: t('settings.debug_plan_desc'), value: me?.plan ?? '…', onPress: () => planSheetRef.current?.present(), }, ], }); } return ( {sections.map((section) => ( {section.title} {section.rows.map((row, i) => { // Visual content of the row (icon + label + sublabel) const iconColor = row.destructive ? colors.error : colors.textMuted; const IconComponent = typeof row.icon === 'string' ? null : row.icon; const rowLeft = ( <> {IconComponent ? ( ) : ( ['name']} size={18} color={iconColor} /> )} {row.label} {row.sublabel ? ( {row.sublabel} ) : null} ); const containerStyle = { flexDirection: 'row' as const, alignItems: 'center' as const, gap: 12, paddingHorizontal: 14, paddingVertical: 12, minHeight: 56, borderBottomWidth: i < section.rows.length - 1 ? 1 : 0, borderBottomColor: colors.border, opacity: row.soon ? 0.5 : 1, }; // Row mit Toggle: native UISwitch am End-Anchor, Label-Bereich nicht tappable if (row.toggle) { return ( {rowLeft} ); } // Row mit Menu: Label-Bereich nicht tappable, MenuView nur am End-Anchor if (row.menu) { return ( {rowLeft} row.menu!.onSelect(event) } shouldOpenOnLongPress={false} > {row.value ? ( {row.value} ) : null} ); } return ( {rowLeft} {row.soon ? ( {t('settings.soon_badge')} ) : row.value ? ( {row.value} ) : ( )} ); })} ))} {t('settings.skeleton_footer')} {Platform.OS} {__DEV__ && ( planSheetRef.current?.dismiss()} /> )} {t('settings.lyra_voice')} {t('settings.lyra_voice_desc')} {voiceOptions.map((opt, idx) => { const isSelected = opt.value === selectedVoice; return ( { setSelectedVoice(opt.value); voiceSheetRef.current?.dismiss(); }} activeOpacity={0.6} > {opt.label} {isSelected ? ( ) : null} ); })} ); } // ─── Plan-Override Sheet (DEV only) ─────────────────────────────────────── const DEV_PLANS: Plan[] = ['free', 'pro', 'legend']; const DEV_PLAN_ACCENT: Record = { free: '#737373', pro: '#007AFF', legend: '#f59e0b', }; function PlanPickerSheetContent({ currentPlan, colors, t, onDone, }: { currentPlan: Plan; colors: import('../lib/theme').ColorScheme; t: (key: string) => string; onDone: () => void; }) { const [loading, setLoading] = useState(false); async function pick(plan: Plan) { if (plan === currentPlan || loading) return; setLoading(true); try { await apiFetch('/api/dev/set-plan', { method: 'POST', body: { plan } }); invalidateMe(); onDone(); } catch (e: unknown) { Alert.alert(t('common.error'), e instanceof Error ? e.message : String(e)); } finally { setLoading(false); } } return ( {t('settings.debug_plan')} {t('settings.debug_plan_desc')} {DEV_PLANS.map((plan, idx) => { const isActive = plan === currentPlan; const accent = DEV_PLAN_ACCENT[plan]; return ( pick(plan)} disabled={loading || isActive} activeOpacity={0.6} > {plan} {isActive ? ( loading ? ( ) : ( ) ) : null} ); })} ); }