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);