diff --git a/app/settings.tsx b/app/settings.tsx index f755a4b..128f0d1 100644 --- a/app/settings.tsx +++ b/app/settings.tsx @@ -1,3 +1,4 @@ +import { useCallback } from 'react'; import { useRouter } from 'expo-router'; import { SafeAreaView } from 'react-native-safe-area-context'; @@ -16,11 +17,11 @@ const SettingsScreen = () => { const router = useRouter(); const { logout } = useAppStore(); - const handleSignOut = async () => { + const handleSignOut = useCallback(async () => { await mobileAuthService.logout(); logout(); router.replace('/'); - }; + }, [logout, router]); return ( diff --git a/src/components/mobile/MobileSettings.tsx b/src/components/mobile/MobileSettings.tsx index 9cf2ec3..e26cf1b 100644 --- a/src/components/mobile/MobileSettings.tsx +++ b/src/components/mobile/MobileSettings.tsx @@ -87,6 +87,30 @@ const SettingRow = memo(function SettingRow({ ); }); +// ───────────────────────────────────────────────────────────── +// Stable icon elements (created once to preserve referential +// identity across renders — enables React.memo on SettingRow) +// ───────────────────────────────────────────────────────────── + +const ICON_EYE = ; +const ICON_LOCK = ; +const ICON_FINGERPRINT = ; +const ICON_USER = ; +const ICON_CREDIT_CARD_YELLOW = ; +const ICON_CREDIT_CARD_GREEN = ; +const ICON_SUN = ; +const ICON_DATABASE = ; +const ICON_BAR_CHART = ; +const ICON_TRASH_RED = ; +const ICON_DOWNLOAD_INDIGO = ; +const ICON_WIFI = ; +const ICON_DOWNLOAD = ; +const ICON_REFRESH = ; +const ICON_ZAP = ; +const ICON_SHIELD = ; +const ICON_LOGOUT_RED = ; +const ICON_ALERT = ; + // ───────────────────────────────────────────────────────────── // Options // ───────────────────────────────────────────────────────────── @@ -344,58 +368,125 @@ export const MobileSettings = ({ onSignOut, onChangePassword, onLinkedAccounts } ); }, [performReauthCheck]); + // Wrap parent-provided callbacks so they are stable references + const handleChangePassword = useCallback(() => { + onChangePassword?.(); + }, [onChangePassword]); + + const handleLinkedAccounts = useCallback(() => { + onLinkedAccounts?.(); + }, [onLinkedAccounts]); + + // ── Memoised right elements (stable references) ──────── + const profileVisibilityRight = useMemo( + () => ( + + ), + [profileVisibility, setProfileVisibility] + ); + + const twoFactorRight = useMemo( + () => , + [twoFactorEnabled, setTwoFactorEnabled] + ); + + const biometricIcon = useMemo( + () => (biometricLoading ? : ICON_FINGERPRINT), + [biometricLoading] + ); + + const biometricRight = useMemo( + () => ( + + ), + [biometricEnabled, handleBiometricToggle, biometricLoading] + ); + + const themeRight = useMemo( + () => ( + + ), + [theme, setTheme] + ); + + const dataSaverRight = useMemo( + () => ( + + ), + [dataSaverEnabled, setDataSaverEnabled] + ); + + const analyticsRight = useMemo( + () => , + [analyticsEnabled, setAnalyticsEnabled] + ); + + const wifiOnlyRight = useMemo( + () => ( + + ), + [downloadOverWifiOnly, setDownloadOverWifiOnly] + ); + + const qualityRight = useMemo( + () => ( + + ), + [downloadQuality, setDownloadQuality] + ); + return ( {/* ── ESSENTIAL: ACCOUNT ─────────────────────────────── */} } + icon={ICON_EYE} label="Profile Visibility" - right={ - - } + right={profileVisibilityRight} /> } + icon={ICON_LOCK} label="Two-Factor Auth" - right={} + right={twoFactorRight} /> {biometricAvailable && ( - ) : ( - - ) - } + icon={biometricIcon} label="Biometric Login" description={biometricEnabled ? 'Enabled' : 'Disabled'} - right={ - - } + right={biometricRight} /> )} - } label="Change Password" onPress={onChangePassword} /> + } + icon={ICON_CREDIT_CARD_YELLOW} label="Change Payment Method" onPress={handleChangePaymentMethod} /> } + icon={ICON_CREDIT_CARD_GREEN} label="View Full Card Number" onPress={handleViewFullCardNumber} /> @@ -404,23 +495,16 @@ export const MobileSettings = ({ onSignOut, onChangePassword, onLinkedAccounts } {/* ── ESSENTIAL: APP ─────────────────────────────────── */} } + icon={ICON_SUN} label="Theme" - right={ - - } + right={themeRight} /> } + icon={ICON_DATABASE} label="Data Saver" description="Reduces bandwidth by disabling prefetch and lowering image quality" - right={} + right={dataSaverRight} /> @@ -432,13 +516,13 @@ export const MobileSettings = ({ onSignOut, onChangePassword, onLinkedAccounts } {/* PRIVACY */} } + icon={ICON_BAR_CHART} label="Analytics" - right={} + right={analyticsRight} /> } + icon={ICON_TRASH_RED} label="Clear Cached Form Data" description="Remove saved autofill values from this device" onPress={handleClearFormCache} @@ -446,7 +530,7 @@ export const MobileSettings = ({ onSignOut, onChangePassword, onLinkedAccounts } /> } + icon={ICON_DOWNLOAD_INDIGO} label="Export Personal Data" description="Export your account details and learning progress" onPress={handleExportData} @@ -456,31 +540,19 @@ export const MobileSettings = ({ onSignOut, onChangePassword, onLinkedAccounts } {/* DOWNLOADS */} } + icon={ICON_WIFI} label="WiFi Only" - right={ - - } + right={wifiOnlyRight} /> } + icon={ICON_DOWNLOAD} label="Quality" - right={ - - } + right={qualityRight} /> } + icon={ICON_TRASH_RED} label="Clear Downloads" onPress={handleClearDownloads} destructive @@ -490,7 +562,7 @@ export const MobileSettings = ({ onSignOut, onChangePassword, onLinkedAccounts } {/* SYNC */} } + icon={ICON_REFRESH} label="Manual Sync" onPress={handleManualSync} /> @@ -499,13 +571,13 @@ export const MobileSettings = ({ onSignOut, onChangePassword, onLinkedAccounts } {/* PERFORMANCE & UTILITIES */} } + icon={ICON_ZAP} label="Clipboard Optimizer" description="Test & profile asynchronous clipboard operations" /> } + icon={ICON_SHIELD} label="Admin Dashboard" description="Access systems health & performance diagnostics" onPress={handleAdminDashboard} @@ -517,13 +589,13 @@ export const MobileSettings = ({ onSignOut, onChangePassword, onLinkedAccounts } {/* ── ESSENTIAL: ACCOUNT ACTIONS ─────────────────────── */} } + icon={ICON_LOGOUT_RED} label="Sign Out" onPress={handleSignOut} destructive /> } + icon={ICON_ALERT} label="Delete Account" description="Permanently delete your account and all data" onPress={handleDeleteAccount} diff --git a/src/components/mobile/NotificationSettings.tsx b/src/components/mobile/NotificationSettings.tsx index d365373..1971eea 100644 --- a/src/components/mobile/NotificationSettings.tsx +++ b/src/components/mobile/NotificationSettings.tsx @@ -1,5 +1,5 @@ import { ChevronDown, ChevronUp } from 'lucide-react-native'; -import React, { memo, useState } from 'react'; +import React, { memo, useCallback, useState } from 'react'; import { ScrollView, Switch, @@ -62,28 +62,58 @@ export const NotificationSettings = () => { const isEnabled = permissionStatus === 'granted' && pushToken !== null; - const handlePreferenceChange = async (key: keyof NotificationPreferences, value: boolean) => { - try { - setSavingKey(key); - // Update local preferences (automatically persisted by Zustand) - setPreference(key, value); - - // TODO: Sync with backend - // try { - // await api.updateNotificationPreferences({ [key]: value }); - // } catch (error) { - // console.error('Failed to sync notification preferences:', error); - // // Preferences are still saved locally even if sync fails - // } - } finally { - setSavingKey(null); - } - }; - - const handleToggleAdvancedNotifications = () => { + const handlePreferenceChange = useCallback( + async (key: keyof NotificationPreferences, value: boolean) => { + try { + setSavingKey(key); + // Update local preferences (automatically persisted by Zustand) + setPreference(key, value); + + // TODO: Sync with backend + // try { + // await api.updateNotificationPreferences({ [key]: value }); + // } catch (error) { + // console.error('Failed to sync notification preferences:', error); + // // Preferences are still saved locally even if sync fails + // } + } finally { + setSavingKey(null); + } + }, + [setPreference] + ); + + const handleToggleAdvancedNotifications = useCallback(() => { configureNext(); setShowAdvancedNotifications(prev => !prev); - }; + }, []); + + // Stable callback refs for each notification preference — prevents + // breaking React.memo on SettingRow when NotificationSettings re-renders. + const handleCourseUpdatesChange = useCallback( + (value: boolean) => handlePreferenceChange('courseUpdates', value), + [handlePreferenceChange] + ); + + const handleMessagesChange = useCallback( + (value: boolean) => handlePreferenceChange('messages', value), + [handlePreferenceChange] + ); + + const handleLearningRemindersChange = useCallback( + (value: boolean) => handlePreferenceChange('learningReminders', value), + [handlePreferenceChange] + ); + + const handleAchievementUnlocksChange = useCallback( + (value: boolean) => handlePreferenceChange('achievementUnlocks', value), + [handlePreferenceChange] + ); + + const handleCommunityActivityChange = useCallback( + (value: boolean) => handlePreferenceChange('communityActivity', value), + [handlePreferenceChange] + ); return ( @@ -127,7 +157,7 @@ export const NotificationSettings = () => { title="Course Updates" description="New lessons, content updates, and announcements" value={preferences.courseUpdates} - onValueChange={value => handlePreferenceChange('courseUpdates', value)} + onValueChange={handleCourseUpdatesChange} disabled={!isEnabled} /> @@ -137,7 +167,7 @@ export const NotificationSettings = () => { title="Messages" description="Direct messages and chat notifications" value={preferences.messages} - onValueChange={value => handlePreferenceChange('messages', value)} + onValueChange={handleMessagesChange} disabled={!isEnabled} /> @@ -179,7 +209,7 @@ export const NotificationSettings = () => { title="Learning Reminders" description="Daily reminders to keep your streak" value={preferences.learningReminders} - onValueChange={value => handlePreferenceChange('learningReminders', value)} + onValueChange={handleLearningRemindersChange} disabled={!isEnabled} /> @@ -189,7 +219,7 @@ export const NotificationSettings = () => { title="Achievement Unlocks" description="Celebrate when you unlock achievements" value={preferences.achievementUnlocks} - onValueChange={value => handlePreferenceChange('achievementUnlocks', value)} + onValueChange={handleAchievementUnlocksChange} disabled={!isEnabled} /> @@ -199,7 +229,7 @@ export const NotificationSettings = () => { title="Community Activity" description="Posts, comments, and community updates" value={preferences.communityActivity} - onValueChange={value => handlePreferenceChange('communityActivity', value)} + onValueChange={handleCommunityActivityChange} disabled={!isEnabled} />