diff --git a/src/components/mobile/MobileSettings.tsx b/src/components/mobile/MobileSettings.tsx index 9f1eaf9..8497597 100644 --- a/src/components/mobile/MobileSettings.tsx +++ b/src/components/mobile/MobileSettings.tsx @@ -1,420 +1,420 @@ -import React, { useState } from 'react'; -import { - Alert, - ActivityIndicator, - LayoutAnimation, - Platform, - ScrollView, - TouchableOpacity, - UIManager, - View, -} from 'react-native'; - -import { - BarChart2, - Bell, - ChevronDown, - ChevronUp, - Download, - Eye, - Globe, - HardDrive, - Lock, - LogOut, - MapPin, - Play, - Settings2, - Shield, - Sun, - Trash2, - Type, - User, - Vibrate, - Wifi, - RefreshCw, - Fingerprint as FingerprintPattern, -} from 'lucide-react-native'; - -import { useAppStore } from '../../store'; -import { useNotificationStore } from '../../store/notificationStore'; -import { useSettingsStore } from '../../store/settingsStore'; -import { useBiometricAuth } from '../../hooks/useBiometricAuth'; -import { useDynamicFontSize } from '../../hooks'; - -import { NativeToggle } from './NativeToggle'; -import { PickerOption, SettingsPicker } from './SettingsPicker'; -import { SettingsSection } from './SettingsSection'; -import { AppText } from '../common/AppText'; - -// Enable LayoutAnimation on Android -if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) { - UIManager.setLayoutAnimationEnabledExperimental(true); -} - -// ───────────────────────────────────────────────────────────── -// Shared Row -// ───────────────────────────────────────────────────────────── - -interface SettingRowProps { - icon: React.ReactNode; - iconBg?: string; - label: string; - description?: string; - right?: React.ReactNode; - onPress?: () => void; - destructive?: boolean; -} - -function SettingRow({ - icon, - iconBg = 'bg-gray-100 dark:bg-gray-700', - label, - description, - right, - onPress, - destructive = false, -}: SettingRowProps) { - const Row = onPress ? TouchableOpacity : View; - const { scale } = useDynamicFontSize(); - - return ( - - - {icon} - - - - - {label} - - - {description && ( - - {description} - - )} - - - {right ?? (onPress ? : null)} - - ); -} - -// ───────────────────────────────────────────────────────────── -// Options -// ───────────────────────────────────────────────────────────── - -const VISIBILITY_OPTIONS: PickerOption[] = [ - { label: 'Public', value: 'public' }, - { label: 'Friends Only', value: 'friends_only' }, - { label: 'Private', value: 'private' }, -]; - -const THEME_OPTIONS: PickerOption[] = [ - { label: 'Light', value: 'light' }, - { label: 'Dark', value: 'dark' }, -]; - -const QUALITY_OPTIONS: PickerOption[] = [ - { label: 'Low', value: 'low' }, - { label: 'Medium', value: 'medium' }, - { label: 'High', value: 'high' }, -]; - -const STORAGE_OPTIONS: PickerOption[] = [ - { label: '1 GB', value: '1GB' }, - { label: '2 GB', value: '2GB' }, - { label: '5 GB', value: '5GB' }, - { label: 'Unlimited', value: 'unlimited' }, -]; - -const LANGUAGE_OPTIONS: PickerOption[] = [ - { label: 'English', value: 'english' }, - { label: 'Spanish', value: 'spanish' }, - { label: 'French', value: 'french' }, -]; - -const FONT_SIZE_OPTIONS: PickerOption[] = [ - { label: 'Small', value: 'small' }, - { label: 'Medium', value: 'medium' }, - { label: 'Large', value: 'large' }, -]; - -// ───────────────────────────────────────────────────────────── -// AdvancedToggle – pill button for expanding advanced settings -// ───────────────────────────────────────────────────────────── - -interface AdvancedToggleProps { - expanded: boolean; - onToggle: () => void; -} - -function AdvancedToggle({ expanded, onToggle }: AdvancedToggleProps) { - return ( - - - - - {expanded ? 'Hide Advanced Settings' : 'Advanced Settings'} - - - {expanded ? ( - - ) : ( - - )} - - ); -} - -// ───────────────────────────────────────────────────────────── -// Component -// ───────────────────────────────────────────────────────────── - -export function MobileSettings({ - onSignOut, - onChangePassword, - onLinkedAccounts, -}: any) { - const { theme, setTheme } = useAppStore(); - const { preferences, setPreference } = useNotificationStore(); - - // Progressive disclosure: advanced settings collapsed by default - const [showAdvancedSettings, setShowAdvancedSettings] = useState(false); - - const { - profileVisibility, - setProfileVisibility, - twoFactorEnabled, - setTwoFactorEnabled, - dataSharing, - setDataSharing, - analyticsEnabled, - setAnalyticsEnabled, - locationServices, - setLocationServices, - downloadOverWifiOnly, - setDownloadOverWifiOnly, - autoDownload, - setAutoDownload, - downloadQuality, - setDownloadQuality, - storageLimit, - setStorageLimit, - language, - setLanguage, - fontSize, - setFontSize, - autoplay, - setAutoplay, - hapticFeedback, - setHapticFeedback, - } = useSettingsStore(); - - const { - isAvailable: biometricAvailable, - isEnabled: biometricEnabled, - biometricType, - enable: enableBiometric, - disable: disableBiometric, - isLoading: biometricLoading, - } = useBiometricAuth(); - - const { scale } = useDynamicFontSize(); - - const handleBiometricToggle = async (value: boolean) => { - if (value) { - const ok = await enableBiometric(); - if (!ok) { - Alert.alert('Biometric Login', 'Enable failed. Check device settings.'); - } - } else { - await disableBiometric(); - } - }; - - const handleSignOut = () => { - Alert.alert('Sign Out', 'Are you sure?', [ - { text: 'Cancel', style: 'cancel' }, - { text: 'Sign Out', style: 'destructive', onPress: onSignOut }, - ]); - }; - - const handleManualSync = async () => { - Alert.alert('Sync', 'Sync data with server?', [ - { text: 'Cancel', style: 'cancel' }, - { - text: 'Sync', - onPress: async () => { - try { - Alert.alert('Syncing...'); - // await syncService.manualSync(); - Alert.alert('Success'); - } catch { - Alert.alert('Failed to sync'); - } - }, - }, - ]); - }; - - const handleClearDownloads = () => { - Alert.alert('Clear Downloads', 'Remove all downloads?', [ - { text: 'Cancel', style: 'cancel' }, - { text: 'Clear', style: 'destructive' }, - ]); - }; - - const handleToggleAdvanced = () => { - LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); - setShowAdvancedSettings(prev => !prev); - }; - - return ( - - - {/* ── ESSENTIAL: ACCOUNT ─────────────────────────────── */} - - } - label="Profile Visibility" - right={ - - } - /> - - } - label="Two-Factor Auth" - right={} - /> - - {biometricAvailable && ( - - ) : ( - - ) - } - label="Biometric Login" - description={biometricEnabled ? 'Enabled' : 'Disabled'} - right={ - - } - /> - )} - - } label="Change Password" onPress={onChangePassword} /> - - - {/* ── ESSENTIAL: APP ─────────────────────────────────── */} - - } - label="Theme" - right={ - - } - /> - - - {/* ── PROGRESSIVE DISCLOSURE: ADVANCED SETTINGS ──────── */} - - - {showAdvancedSettings && ( - <> - {/* PRIVACY */} - - } - label="Analytics" - right={} - /> - - - {/* DOWNLOADS */} - - } - label="WiFi Only" - right={ - - } - /> - - } - label="Quality" - right={ - - } - /> - - } - label="Clear Downloads" - onPress={handleClearDownloads} - destructive - /> - - - {/* SYNC */} - - } - label="Manual Sync" - onPress={handleManualSync} - /> - - - )} - - {/* ── ESSENTIAL: ACCOUNT ACTIONS ─────────────────────── */} - - } - label="Sign Out" - onPress={handleSignOut} - destructive - /> - - - ); -} - -export default MobileSettings; +import React, { useState } from 'react'; +import { + Alert, + ActivityIndicator, + LayoutAnimation, + Platform, + ScrollView, + TouchableOpacity, + UIManager, + View, +} from 'react-native'; + +import { + BarChart2, + Bell, + ChevronDown, + ChevronUp, + Download, + Eye, + Globe, + HardDrive, + Lock, + LogOut, + MapPin, + Play, + Settings2, + Shield, + Sun, + Trash2, + Type, + User, + Vibrate, + Wifi, + RefreshCw, + Fingerprint as FingerprintPattern, +} from 'lucide-react-native'; + +import { useAppStore } from '../../store'; +import { useNotificationStore } from '../../store/notificationStore'; +import { useSettingsStore } from '../../store/settingsStore'; +import { useBiometricAuth } from '../../hooks/useBiometricAuth'; +import { useDynamicFontSize } from '../../hooks'; + +import { NativeToggle } from './NativeToggle'; +import { PickerOption, SettingsPicker } from './SettingsPicker'; +import { SettingsSection } from './SettingsSection'; +import { AppText } from '../common/AppText'; + +// Enable LayoutAnimation on Android +if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) { + UIManager.setLayoutAnimationEnabledExperimental(true); +} + +// ───────────────────────────────────────────────────────────── +// Shared Row +// ───────────────────────────────────────────────────────────── + +interface SettingRowProps { + icon: React.ReactNode; + iconBg?: string; + label: string; + description?: string; + right?: React.ReactNode; + onPress?: () => void; + destructive?: boolean; +} + +function SettingRow({ + icon, + iconBg = 'bg-gray-100 dark:bg-gray-700', + label, + description, + right, + onPress, + destructive = false, +}: SettingRowProps) { + const Row = onPress ? TouchableOpacity : View; + const { scale } = useDynamicFontSize(); + + return ( + + + {icon} + + + + + {label} + + + {description && ( + + {description} + + )} + + + {right ?? (onPress ? : null)} + + ); +} + +// ───────────────────────────────────────────────────────────── +// Options +// ───────────────────────────────────────────────────────────── + +const VISIBILITY_OPTIONS: PickerOption[] = [ + { label: 'Public', value: 'public' }, + { label: 'Friends Only', value: 'friends_only' }, + { label: 'Private', value: 'private' }, +]; + +const THEME_OPTIONS: PickerOption[] = [ + { label: 'Light', value: 'light' }, + { label: 'Dark', value: 'dark' }, +]; + +const QUALITY_OPTIONS: PickerOption[] = [ + { label: 'Low', value: 'low' }, + { label: 'Medium', value: 'medium' }, + { label: 'High', value: 'high' }, +]; + +const STORAGE_OPTIONS: PickerOption[] = [ + { label: '1 GB', value: '1GB' }, + { label: '2 GB', value: '2GB' }, + { label: '5 GB', value: '5GB' }, + { label: 'Unlimited', value: 'unlimited' }, +]; + +const LANGUAGE_OPTIONS: PickerOption[] = [ + { label: 'English', value: 'english' }, + { label: 'Spanish', value: 'spanish' }, + { label: 'French', value: 'french' }, +]; + +const FONT_SIZE_OPTIONS: PickerOption[] = [ + { label: 'Small', value: 'small' }, + { label: 'Medium', value: 'medium' }, + { label: 'Large', value: 'large' }, +]; + +// ───────────────────────────────────────────────────────────── +// AdvancedToggle – pill button for expanding advanced settings +// ───────────────────────────────────────────────────────────── + +interface AdvancedToggleProps { + expanded: boolean; + onToggle: () => void; +} + +function AdvancedToggle({ expanded, onToggle }: AdvancedToggleProps) { + return ( + + + + + {expanded ? 'Hide Advanced Settings' : 'Advanced Settings'} + + + {expanded ? ( + + ) : ( + + )} + + ); +} + +// ───────────────────────────────────────────────────────────── +// Component +// ───────────────────────────────────────────────────────────── + +export function MobileSettings({ + onSignOut, + onChangePassword, + onLinkedAccounts, +}: any) { + const { theme, setTheme } = useAppStore(); + const { preferences, setPreference } = useNotificationStore(); + + // Progressive disclosure: advanced settings collapsed by default + const [showAdvancedSettings, setShowAdvancedSettings] = useState(false); + + const { + profileVisibility, + setProfileVisibility, + twoFactorEnabled, + setTwoFactorEnabled, + dataSharing, + setDataSharing, + analyticsEnabled, + setAnalyticsEnabled, + locationServices, + setLocationServices, + downloadOverWifiOnly, + setDownloadOverWifiOnly, + autoDownload, + setAutoDownload, + downloadQuality, + setDownloadQuality, + storageLimit, + setStorageLimit, + language, + setLanguage, + fontSize, + setFontSize, + autoplay, + setAutoplay, + hapticFeedback, + setHapticFeedback, + } = useSettingsStore(); + + const { + isAvailable: biometricAvailable, + isEnabled: biometricEnabled, + biometricType, + enable: enableBiometric, + disable: disableBiometric, + isLoading: biometricLoading, + } = useBiometricAuth(); + + const { scale } = useDynamicFontSize(); + + const handleBiometricToggle = async (value: boolean) => { + if (value) { + const ok = await enableBiometric(); + if (!ok) { + Alert.alert('Biometric Login', 'Enable failed. Check device settings.'); + } + } else { + await disableBiometric(); + } + }; + + const handleSignOut = () => { + Alert.alert('Sign Out', 'Are you sure?', [ + { text: 'Cancel', style: 'cancel' }, + { text: 'Sign Out', style: 'destructive', onPress: onSignOut }, + ]); + }; + + const handleManualSync = async () => { + Alert.alert('Sync', 'Sync data with server?', [ + { text: 'Cancel', style: 'cancel' }, + { + text: 'Sync', + onPress: async () => { + try { + Alert.alert('Syncing...'); + // await syncService.manualSync(); + Alert.alert('Success'); + } catch { + Alert.alert('Failed to sync'); + } + }, + }, + ]); + }; + + const handleClearDownloads = () => { + Alert.alert('Clear Downloads', 'Remove all downloads?', [ + { text: 'Cancel', style: 'cancel' }, + { text: 'Clear', style: 'destructive' }, + ]); + }; + + const handleToggleAdvanced = () => { + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); + setShowAdvancedSettings(prev => !prev); + }; + + return ( + + + {/* ── ESSENTIAL: ACCOUNT ─────────────────────────────── */} + + } + label="Profile Visibility" + right={ + + } + /> + + } + label="Two-Factor Auth" + right={} + /> + + {biometricAvailable && ( + + ) : ( + + ) + } + label="Biometric Login" + description={biometricEnabled ? 'Enabled' : 'Disabled'} + right={ + + } + /> + )} + + } label="Change Password" onPress={onChangePassword} /> + + + {/* ── ESSENTIAL: APP ─────────────────────────────────── */} + + } + label="Theme" + right={ + + } + /> + + + {/* ── PROGRESSIVE DISCLOSURE: ADVANCED SETTINGS ──────── */} + + + {showAdvancedSettings && ( + <> + {/* PRIVACY */} + + } + label="Analytics" + right={} + /> + + + {/* DOWNLOADS */} + + } + label="WiFi Only" + right={ + + } + /> + + } + label="Quality" + right={ + + } + /> + + } + label="Clear Downloads" + onPress={handleClearDownloads} + destructive + /> + + + {/* SYNC */} + + } + label="Manual Sync" + onPress={handleManualSync} + /> + + + )} + + {/* ── ESSENTIAL: ACCOUNT ACTIONS ─────────────────────── */} + + } + label="Sign Out" + onPress={handleSignOut} + destructive + /> + + + ); +} + +export default MobileSettings; diff --git a/src/hooks/useAdaptiveTheme.ts b/src/hooks/useAdaptiveTheme.ts index f77908f..5441762 100644 --- a/src/hooks/useAdaptiveTheme.ts +++ b/src/hooks/useAdaptiveTheme.ts @@ -2,8 +2,8 @@ import { LightSensor } from 'expo-sensors'; import { useEffect, useRef } from 'react'; import { AppState, type AppStateStatus } from 'react-native'; -import { useAppStore } from '../store'; import { useSettingsStore } from '../store/settingsStore'; +import { useUiStore } from '../store/uiStore'; export const DARK_LUX_THRESHOLD = 25; export const LIGHT_LUX_THRESHOLD = 75; @@ -61,7 +61,7 @@ export function advanceDebounce( export function useAdaptiveTheme(): void { const adaptiveThemeEnabled = useSettingsStore((s) => s.adaptiveThemeEnabled); - const setTheme = useAppStore((s) => s.setTheme); + const setTheme = useUiStore((s) => s.setTheme); const debounceRef = useRef({ candidate: null, consecutiveCount: 0 }); const subscriptionRef = useRef<{ remove: () => void } | null>(null); @@ -77,7 +77,7 @@ export function useAdaptiveTheme(): void { }; const handleReading = (lux: number) => { - const currentTheme = useAppStore.getState().theme; + const currentTheme = useUiStore.getState().theme; const { state, confirmedTheme } = advanceDebounce(debounceRef.current, lux, currentTheme); debounceRef.current = state; if (confirmedTheme) { diff --git a/src/store/achievementStore.ts b/src/store/achievementStore.ts index 9625f2a..72de652 100644 --- a/src/store/achievementStore.ts +++ b/src/store/achievementStore.ts @@ -53,8 +53,12 @@ interface AchievementState { achievementProgress: Record; /** Number of unlocked achievements */ unlockedCount: number; - + /** Whether achievements have been loaded */ + isLoaded: boolean; + // Actions + /** Load achievements — initializes with defaults if not yet persisted */ + loadAchievements: () => void; /** Unlock an achievement by ID */ unlockAchievement: (id: string) => void; /** Update progress on an achievement */ @@ -280,6 +284,17 @@ export const useAchievementStore = create()( achievements: buildAchievementsFromProgress({}), achievementProgress: {}, unlockedCount: 0, + isLoaded: false, + + loadAchievements: () => { + const { isLoaded, achievements } = get(); + if (isLoaded) return; + // If persisted achievements exist, keep them; otherwise seed defaults + set({ + achievements: achievements.length > 0 ? achievements : DEFAULT_ACHIEVEMENTS, + isLoaded: true, + }); + }, unlockAchievement: (id: string) => set((state) => { @@ -369,6 +384,7 @@ export const useAchievementStore = create()( partialize: (state) => ({ achievementProgress: state.achievementProgress, unlockedCount: state.unlockedCount, + isLoaded: state.isLoaded, }), migrate: (persistedState) => normalizeAchievementState(persistedState), merge: (persistedState, currentState) => { diff --git a/src/store/index.ts b/src/store/index.ts index 59a548c..1b60d13 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -22,7 +22,6 @@ interface AppState { refreshToken: string | null; sessionExpiresAt: number | null; sessionExpiringSoon: boolean; - theme: 'light' | 'dark'; isLoading: boolean; error: string | null; setUser: (user: User | null) => void; @@ -56,7 +55,7 @@ const secureStorageAdapter: StateStorage = { export const useAppStore = create()( devtools( persist( - subscribeWithSelector((set) => ({ + subscribeWithSelector(set => ({ user: null, isAuthenticated: false, isAuthLoading: false, @@ -65,11 +64,9 @@ export const useAppStore = create()( refreshToken: null, sessionExpiresAt: null, sessionExpiringSoon: false, - theme: 'light', isLoading: false, error: null, - setUser: (user) => set({ user, isAuthenticated: !!user }, false, 'setUser'), - setTheme: (theme) => set({ theme }, false, 'setTheme'), + setUser: user => set({ user, isAuthenticated: !!user }, false, 'setUser'), setTokens: (accessToken, refreshToken, sessionExpiresAt) => set( { @@ -82,8 +79,8 @@ export const useAppStore = create()( ), setSessionExpiringSoon: (sessionExpiringSoon) => set({ sessionExpiringSoon }, false, 'setSessionExpiringSoon'), - setAuthLoading: (isAuthLoading) => set({ isAuthLoading }, false, 'setAuthLoading'), - setAuthError: (authError) => set({ authError }, false, 'setAuthError'), + setAuthLoading: isAuthLoading => set({ isAuthLoading }, false, 'setAuthLoading'), + setAuthError: authError => set({ authError }, false, 'setAuthError'), logout: () => set( { @@ -99,8 +96,8 @@ export const useAppStore = create()( false, 'logout' ), - setLoading: (isLoading) => set({ isLoading }, false, 'setLoading'), - setError: (error) => set({ error }, false, 'setError'), + setLoading: isLoading => set({ isLoading }, false, 'setLoading'), + setError: error => set({ error }, false, 'setError'), })), { name: 'app-auth-storage', @@ -110,7 +107,7 @@ export const useAppStore = create()( * Transient flags (isLoading, isAuthLoading, error, authError) * are intentionally excluded — they should always start fresh. */ - partialize: (state) => ({ + partialize: state => ({ user: state.user, isAuthenticated: state.isAuthenticated, accessToken: state.accessToken, @@ -132,6 +129,3 @@ export const useAppStore = create()( { name: 'AppStore' } ) ); - -export * from './notificationStore'; -export * from './courseProgressStore'; diff --git a/src/store/settingsStore.ts b/src/store/settingsStore.ts index e7c4209..ac72ba5 100644 --- a/src/store/settingsStore.ts +++ b/src/store/settingsStore.ts @@ -1,6 +1,6 @@ +import AsyncStorage from '@react-native-async-storage/async-storage'; import { create } from 'zustand'; import { persist, createJSONStorage } from 'zustand/middleware'; -import AsyncStorage from '@react-native-async-storage/async-storage'; export type ProfileVisibility = 'public' | 'private' | 'friends_only'; export type DownloadQuality = 'low' | 'medium' | 'high'; @@ -57,23 +57,6 @@ interface SettingsState { resetSettings: () => void; } -const DEFAULT_SETTINGS: Omit> = { - profileVisibility: 'public' as ProfileVisibility, - twoFactorEnabled: false, - dataSharing: true, - analyticsEnabled: true, - locationServices: false, - downloadOverWifiOnly: true, - autoDownload: false, - downloadQuality: 'medium' as DownloadQuality, - storageLimit: '2GB' as StorageLimit, - language: 'english' as AppLanguage, - fontSize: 'medium' as FontSize, - autoplay: true, - hapticFeedback: true, - adaptiveThemeEnabled: false, -}; - const INITIAL_STATE = { profileVisibility: 'public' as ProfileVisibility, twoFactorEnabled: false, @@ -93,30 +76,30 @@ const INITIAL_STATE = { export const useSettingsStore = create()( persist( - (set) => ({ + set => ({ ...INITIAL_STATE, // Account - setProfileVisibility: (v) => set({ profileVisibility: v }), - setTwoFactorEnabled: (v) => set({ twoFactorEnabled: v }), + setProfileVisibility: v => set({ profileVisibility: v }), + setTwoFactorEnabled: v => set({ twoFactorEnabled: v }), // Privacy - setDataSharing: (v) => set({ dataSharing: v }), - setAnalyticsEnabled: (v) => set({ analyticsEnabled: v }), - setLocationServices: (v) => set({ locationServices: v }), + setDataSharing: v => set({ dataSharing: v }), + setAnalyticsEnabled: v => set({ analyticsEnabled: v }), + setLocationServices: v => set({ locationServices: v }), // Downloads - setDownloadOverWifiOnly: (v) => set({ downloadOverWifiOnly: v }), - setAutoDownload: (v) => set({ autoDownload: v }), - setDownloadQuality: (v) => set({ downloadQuality: v }), - setStorageLimit: (v) => set({ storageLimit: v }), + setDownloadOverWifiOnly: v => set({ downloadOverWifiOnly: v }), + setAutoDownload: v => set({ autoDownload: v }), + setDownloadQuality: v => set({ downloadQuality: v }), + setStorageLimit: v => set({ storageLimit: v }), // App Preferences - setLanguage: (v) => set({ language: v }), - setFontSize: (v) => set({ fontSize: v }), - setAutoplay: (v) => set({ autoplay: v }), - setHapticFeedback: (v) => set({ hapticFeedback: v }), - setAdaptiveThemeEnabled: (v) => set({ adaptiveThemeEnabled: v }), + setLanguage: v => set({ language: v }), + setFontSize: v => set({ fontSize: v }), + setAutoplay: v => set({ autoplay: v }), + setHapticFeedback: v => set({ hapticFeedback: v }), + setAdaptiveThemeEnabled: v => set({ adaptiveThemeEnabled: v }), resetSettings: () => set(INITIAL_STATE), }), diff --git a/src/store/uiStore.ts b/src/store/uiStore.ts new file mode 100644 index 0000000..9a34ab1 --- /dev/null +++ b/src/store/uiStore.ts @@ -0,0 +1,21 @@ +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { create } from 'zustand'; +import { createJSONStorage, persist } from 'zustand/middleware'; + +interface UiState { + theme: 'light' | 'dark'; + setTheme: (theme: 'light' | 'dark') => void; +} + +export const useUiStore = create()( + persist( + (set) => ({ + theme: 'light', + setTheme: (theme) => set({ theme }), + }), + { + name: 'ui-storage', + storage: createJSONStorage(() => AsyncStorage), + } + ) +); diff --git a/tests/store.test.ts b/tests/store.test.ts index 1d6ab60..8a5bda8 100644 --- a/tests/store.test.ts +++ b/tests/store.test.ts @@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it } from '@jest/globals'; import { AuthUser } from '../src/services/mobileAuth'; import { useAppStore } from '../src/store'; +import { useUiStore } from '../src/store/uiStore'; const MOCK_USER: AuthUser = { id: 'user-001', @@ -25,7 +26,6 @@ const INITIAL_STATE = { accessToken: null, refreshToken: null, sessionExpiresAt: null, - theme: 'light' as const, }; describe('useAppStore', () => { @@ -218,20 +218,24 @@ describe('useAppStore', () => { // ── setTheme ──────────────────────────────────────────────────────────── describe('setTheme', () => { + beforeEach(() => { + useUiStore.setState({ theme: 'light' }); + }); + it('switches theme to dark', () => { - useAppStore.getState().setTheme('dark'); - expect(useAppStore.getState().theme).toBe('dark'); + useUiStore.getState().setTheme('dark'); + expect(useUiStore.getState().theme).toBe('dark'); }); it('switches theme back to light', () => { - useAppStore.setState({ theme: 'dark' }); - useAppStore.getState().setTheme('light'); - expect(useAppStore.getState().theme).toBe('light'); + useUiStore.setState({ theme: 'dark' }); + useUiStore.getState().setTheme('light'); + expect(useUiStore.getState().theme).toBe('light'); }); it('does not affect auth state', () => { useAppStore.setState({ user: MOCK_USER, isAuthenticated: true }); - useAppStore.getState().setTheme('dark'); + useUiStore.getState().setTheme('dark'); const state = useAppStore.getState(); expect(state.isAuthenticated).toBe(true); expect(state.user).toEqual(MOCK_USER);