import { useState } from 'react' import { Alert, Linking, Pressable, ScrollView, StyleSheet, Switch, View, } from 'react-native' import { useSafeAreaInsets } from 'react-native-safe-area-context' import { useNavigation } from '@react-navigation/native' import { useI18n, type TranslationKey } from '@d3ro/i18n' import { SITE_URLS } from '@d3ro/core/web-urls' import { useAuth } from '../lib/auth-context' import { useEntitlement } from '../lib/entitlement-context' import { useMobilePreferences, type MobilePreferencesPatch, type PreferencesErrorCode, type PreferencesSyncStatus, type SupportedMobileLocale, } from '../lib/preferences-context' import { MOBILE_THEME_MODES, type MobileThemeMode, } from '../theme/mobile-theme' import { ThemeButton, ThemeCard, ThemeText, } from '../theme/themed-components' import { useMobileAds } from '../lib/mobile-ads-context' import { useDevice } from '../lib/device-context' import { currentAdminRole } from '../features/admin/admin-service' import { performAccountLogout } from '../lib/account-exit' type SavingPreference = 'theme' | 'locale' | 'autoPolish' | 'haptic' | null const THEME_LABEL_KEYS: Record = { system: 'mobile.theme.system', light: 'mobile.theme.light', dark: 'mobile.theme.dark', } const SYNC_STATUS_KEYS: Record = { loading: 'mobile.preferences.statusLoading', local: 'mobile.preferences.statusLocal', saving: 'mobile.preferences.statusSaving', synced: 'mobile.preferences.statusSynced', offline: 'mobile.preferences.statusOffline', error: 'mobile.preferences.statusError', } const ERROR_KEYS: Record = { CACHE_READ_FAILED: 'mobile.preferences.cacheReadError', CACHE_WRITE_FAILED: 'mobile.preferences.cacheWriteError', SYNC_FAILED: 'mobile.preferences.syncError', SYNC_CONFLICT: 'mobile.preferences.conflictError', } export default function SettingsScreen(): React.ReactElement { const insets = useSafeAreaInsets() const { t, locale, setLocale, formatTime } = useI18n() const { user, purgeLocalSession } = useAuth() const navigation = useNavigation() const { preferences, palette, syncStatus, errorCode, lastSyncedAt, updatePreferences, retrySync, } = useMobilePreferences() const [savingPreference, setSavingPreference] = useState(null) const [loggingOut, setLoggingOut] = useState(false) const [retrying, setRetrying] = useState(false) const { snapshot: entitlement } = useEntitlement() const mobileAds = useMobileAds() const { currentDevice } = useDevice() const adminRole = currentAdminRole(user?.app_metadata) const initials = user?.email ? user.email.substring(0, 2).toUpperCase() : 'US' const syncLabel = t(SYNC_STATUS_KEYS[syncStatus]) const syncError = errorCode === null ? null : t(ERROR_KEYS[errorCode]) const syncedTime = lastSyncedAt === null ? null : formatTime(Date.parse(lastSyncedAt)) async function persistPreference( key: Exclude, patch: MobilePreferencesPatch, ): Promise { if (savingPreference !== null) return setSavingPreference(key) const result = await updatePreferences(patch) setSavingPreference(null) if (!result.localSaved) { Alert.alert( t('mobile.preferences.saveFailedTitle'), t('mobile.preferences.cacheWriteError'), ) } } function selectLocale(nextLocale: SupportedMobileLocale): void { if (locale === nextLocale || savingPreference !== null) return setLocale(nextLocale) void persistPreference('locale', { locale: nextLocale }) } async function performLogout(): Promise { if (loggingOut) return setLoggingOut(true) try { await performAccountLogout({ userId: user?.id ?? null, deviceId: currentDevice?.id ?? null, purgeLocalSession, }) } catch { Alert.alert(t('mobile.auth.errorTitle'), t('mobile.auth.logoutFailed')) } finally { setLoggingOut(false) } } function confirmLogout(): void { Alert.alert(t('mobile.set.logout'), t('mobile.set.logoutConfirm'), [ { text: t('common.cancel'), style: 'cancel' }, { text: t('mobile.set.logout'), style: 'destructive', onPress: () => void performLogout(), }, ]) } async function handleRetry(): Promise { if (retrying) return setRetrying(true) await retrySync() setRetrying(false) } async function openAdPrivacyOptions(): Promise { const opened = await mobileAds.showPrivacyOptions() if (!opened) { Alert.alert( t('mobile.set.adPrivacy'), t('mobile.set.adPrivacyUnavailable'), ) } } return ( {t('mobile.set.title')} navigation.navigate('Account')} style={({ pressed }) => [ styles.profileRow, { borderBottomColor: palette.border.subtle }, pressed && { backgroundColor: palette.bg.cardHover }, ]} > {initials} {user?.email?.split('@')[0] ?? t('mobile.set.userFallback')} {user?.email ?? '—'} {t('mobile.set.edit')} {t('mobile.dash.tier')} {entitlement.tier === 'pro_plus' ? 'PRO+' : entitlement.tier === 'pro' ? 'PRO' : t('mobile.dash.free')} navigation.navigate('ProPaywall')} style={({ pressed }) => [ styles.upgradeSettingRow, { borderTopColor: palette.accent.dim, backgroundColor: pressed ? palette.bg.cardHover : palette.accent.dim, }, ]} > {t('mobile.set.planAndRewards')} → navigation.navigate('Devices')} style={({ pressed }) => [ styles.upgradeSettingRow, { borderTopColor: palette.border.subtle, backgroundColor: pressed ? palette.bg.cardHover : palette.bg.card, }, ]} > {t('mobile.devices.title')} {t('mobile.devices.description')} → {adminRole !== null && ( navigation.navigate('Admin')} style={({ pressed }) => [ styles.upgradeSettingRow, { borderTopColor: palette.border.subtle, backgroundColor: pressed ? palette.bg.cardHover : palette.bg.card, }, ]} > {t('mobile.admin.title')} {t('mobile.admin.description', { role: t(`mobile.admin.role.${adminRole}`) })} → )} {MOBILE_THEME_MODES.map((mode) => ( void persistPreference('theme', { themeMode: mode })} /> ))} selectLocale('ko')} /> selectLocale('en')} /> {t('mobile.set.llmModel')} {t('mobile.set.llmDesc')} {(preferences.preferredLlmModel ?? t('mobile.set.automatic')).toUpperCase()} {t('mobile.set.cloudStt')} {t('mobile.set.cloudSttManagedDesc')} {t('mobile.set.serverManaged')} void persistPreference( 'autoPolish', { autoPolishEnabled: value }, )} /> void persistPreference( 'haptic', { hapticEnabled: value }, )} /> navigation.navigate('Onboarding', { replay: true })} style={({ pressed }) => [ styles.replayRow, { borderTopColor: palette.border.subtle, backgroundColor: pressed ? palette.bg.cardHover : 'transparent', }, ]} > {t('mobile.set.replayTutorial')} {t('mobile.set.replayTutorialDesc')} → {mobileAds.privacyOptionsRequired ? ( <> {t('mobile.set.adPrivacyRequired')} {t('mobile.set.adPrivacyDesc')} void openAdPrivacyOptions()} testID="settings-ad-privacy" /> ) : null} void Linking.openURL(SITE_URLS.privacy)} /> void Linking.openURL(SITE_URLS.terms)} /> void Linking.openURL(SITE_URLS.deleteAccount)} /> {syncLabel} {syncError ?? (syncedTime === null ? t('mobile.preferences.notSyncedYet') : t('mobile.preferences.lastSynced', { time: syncedTime }))} {(syncStatus === 'offline' || syncStatus === 'error') && ( void handleRetry()} testID="settings-sync-retry" style={styles.retryButton} /> )} {t('mobile.set.statusFooter')} ) } function SectionLabel({ label }: { label: string }): React.ReactElement { return ( {label} ) } function SettingHeader({ title, description, }: { title: string description: string }): React.ReactElement { return ( {title} {description} ) } function SegmentOption({ label, selected, disabled, testID, onPress, }: { label: string selected: boolean disabled: boolean testID: string onPress: () => void }): React.ReactElement { const { palette } = useMobilePreferences() return ( [ styles.segmentOption, { borderColor: selected ? palette.accent.main : palette.border.default, backgroundColor: selected ? palette.accent.dim : palette.bg.inset, opacity: disabled ? 0.5 : pressed ? 0.72 : 1, }, ]} > {label} ) } function PreferenceSwitch({ label, description, value, disabled, bordered = false, testID, onValueChange, }: { label: string description: string value: boolean disabled: boolean bordered?: boolean testID: string onValueChange: (value: boolean) => void }): React.ReactElement { const { palette } = useMobilePreferences() return ( {label} {description} ) } function LegalLinkRow({ label, hint, testID, bordered = false, onPress, }: { label: string hint: string testID: string bordered?: boolean onPress: () => void }): React.ReactElement { const { palette } = useMobilePreferences() return ( [ styles.legalRow, bordered && { borderTopColor: palette.border.subtle, borderTopWidth: 1, }, pressed && { backgroundColor: palette.bg.cardHover }, ]} > {label} {hint} ↗ ) } const styles = StyleSheet.create({ container: { flex: 1 }, content: { paddingBottom: 120 }, title: { paddingHorizontal: 20, paddingBottom: 12, borderBottomWidth: 1, letterSpacing: 1.2, }, sectionLabel: { paddingHorizontal: 24, paddingTop: 22, paddingBottom: 8, letterSpacing: 0.4, }, section: { marginHorizontal: 20, padding: 0, overflow: 'hidden' }, profileRow: { minHeight: 74, flexDirection: 'row', alignItems: 'center', padding: 15, gap: 12, borderBottomWidth: 1, }, avatar: { width: 44, height: 44, borderRadius: 22, borderWidth: 1, justifyContent: 'center', alignItems: 'center', }, profileInfo: { flex: 1, gap: 3 }, row: { minHeight: 64, flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', gap: 14, padding: 15, }, rowBorder: { borderTopWidth: 1 }, rowRight: { flexDirection: 'row', alignItems: 'center' }, rowValue: { marginLeft: 8 }, rowText: { flex: 1, gap: 4 }, statusDot: { width: 7, height: 7, borderRadius: 4 }, upgradeSettingRow: { minHeight: 58, flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', gap: 12, padding: 15, borderTopWidth: 1, }, upgradeText: { flex: 1 }, settingHeader: { gap: 4, padding: 15, paddingBottom: 10 }, settingBlock: { borderTopWidth: 1, marginTop: 15, paddingTop: 2 }, segmentRow: { flexDirection: 'row', gap: 8, paddingHorizontal: 15, paddingBottom: 15 }, segmentOption: { minHeight: 48, flex: 1, alignItems: 'center', justifyContent: 'center', borderWidth: 1, borderRadius: 9, paddingHorizontal: 8, }, preferenceRow: { minHeight: 72, flexDirection: 'row', alignItems: 'center', gap: 14, padding: 15, }, replayRow: { minHeight: 72, flexDirection: 'row', alignItems: 'center', gap: 12, padding: 15, borderTopWidth: 1, }, legalRow: { minHeight: 68, flexDirection: 'row', alignItems: 'center', gap: 12, padding: 15, }, syncHeader: { flexDirection: 'row', alignItems: 'center', gap: 12, padding: 15 }, syncDot: { width: 10, height: 10, borderRadius: 5 }, retryButton: { marginHorizontal: 15, marginBottom: 15 }, privacyBlock: { gap: 14, padding: 15 }, logoutWrap: { margin: 20 }, footer: { flexDirection: 'row', justifyContent: 'center', alignItems: 'center', gap: 8, paddingVertical: 8, }, footerText: { fontSize: 8, letterSpacing: 0.4 }, })