From bb1fd52e36dee0481b8ec642042bd6a124f52569 Mon Sep 17 00:00:00 2001 From: Gozirimdev Date: Wed, 27 May 2026 01:38:01 +0100 Subject: [PATCH] feat(#399): implement progressive disclosure UI for complex screens - MobileProfile: hide Location & Website behind expandable 'Advanced Details' toggle in edit mode - MobileSettings: group Privacy, Downloads & Sync sections under collapsible 'Advanced Settings' pill - NotificationSettings: show only Course Updates & Messages by default; Learning Reminders, Achievement Unlocks & Community Activity expandable under 'Advanced Notifications' - All toggles use LayoutAnimation (easeInEaseOut) for smooth expand/collapse - Proper accessibilityRole, accessibilityLabel and accessibilityState (expanded) on every disclosure trigger Closes #399 --- src/components/mobile/MobileProfile.tsx | 109 ++- src/components/mobile/MobileSettings.tsx | 781 ++++++++++-------- .../mobile/NotificationSettings.tsx | 128 ++- 3 files changed, 603 insertions(+), 415 deletions(-) diff --git a/src/components/mobile/MobileProfile.tsx b/src/components/mobile/MobileProfile.tsx index 305b58b..70d75f9 100644 --- a/src/components/mobile/MobileProfile.tsx +++ b/src/components/mobile/MobileProfile.tsx @@ -2,6 +2,8 @@ import { LinearGradient } from 'expo-linear-gradient'; import { BookOpen, Camera, + ChevronDown, + ChevronUp, Clock, Edit3, Globe, @@ -18,12 +20,21 @@ import { import React, { useState } from 'react'; import { ActivityIndicator, + Animated, + LayoutAnimation, + Platform, SafeAreaView, ScrollView, StyleSheet, TouchableOpacity, + UIManager, View, } from 'react-native'; + +// Enable LayoutAnimation on Android +if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) { + UIManager.setLayoutAnimationEnabledExperimental(true); +} import { AppText as Text } from '../common/AppText'; import { CachedImage } from '../ui/CachedImage'; import { Skeleton } from '../ui/Skeleton'; @@ -290,6 +301,8 @@ export const MobileProfile: React.FC = ({ const [isEditing, setIsEditing] = useState(false); const [isCameraVisible, setIsCameraVisible] = useState(false); const [isSaving, setIsSaving] = useState(false); + // Progressive disclosure: advanced profile fields collapsed by default + const [showAdvancedFields, setShowAdvancedFields] = useState(false); // Edit form state const [editName, setEditName] = useState(''); @@ -297,7 +310,7 @@ export const MobileProfile: React.FC = ({ const [editEmail, setEditEmail] = useState(''); const [editLocation, setEditLocation] = useState(''); const [editWebsite, setEditWebsite] = useState(''); - const [formErrors, setFormErrors] = useState>({}); + const [formErrors, setFormErrors] = useState>(); // Theme tokens const bg = isDark ? '#0f172a' : '#f8fafc'; @@ -321,9 +334,15 @@ export const MobileProfile: React.FC = ({ setEditLocation(profile.location); setEditWebsite(profile.website); setFormErrors({}); + setShowAdvancedFields(false); // reset disclosure state on each edit session setIsEditing(true); }; + const handleToggleAdvancedFields = () => { + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); + setShowAdvancedFields(prev => !prev); + }; + const validateForm = (): Record => { const errors: Record = {}; if (!editName.trim()) errors.name = 'Name is required'; @@ -595,13 +614,15 @@ export const MobileProfile: React.FC = ({ {isEditing ? ( <> Edit Profile + + {/* ── Basic Fields (always visible) ── */} } /> @@ -613,7 +634,7 @@ export const MobileProfile: React.FC = ({ keyboardType="email-address" autoCapitalize="none" required - error={formErrors.email} + error={formErrors?.email} isDark={isDark} leftIcon={} /> @@ -625,24 +646,50 @@ export const MobileProfile: React.FC = ({ multiline isDark={isDark} /> - } - /> - } - /> + + {/* ── Progressive Disclosure: Advanced Details ── */} + + + {showAdvancedFields ? 'Hide Advanced Details' : 'Advanced Details'} + + {showAdvancedFields + ? + : } + + + {/* ── Advanced Fields (expandable) ── */} + {showAdvancedFields && ( + + } + /> + } + /> + + )} ) : ( <> @@ -1123,4 +1170,24 @@ const styles = StyleSheet.create({ fontSize: 12, fontWeight: '700', }, + // Progressive disclosure + disclosureToggle: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingVertical: 12, + paddingHorizontal: 4, + marginTop: 4, + marginBottom: 2, + borderTopWidth: 1, + borderBottomWidth: 1, + }, + disclosureToggleText: { + fontSize: 14, + fontWeight: '600', + }, + disclosureContent: { + marginTop: 4, + gap: 0, + }, }); diff --git a/src/components/mobile/MobileSettings.tsx b/src/components/mobile/MobileSettings.tsx index b526681..baef1cd 100644 --- a/src/components/mobile/MobileSettings.tsx +++ b/src/components/mobile/MobileSettings.tsx @@ -1,361 +1,420 @@ -import React from 'react'; -import { - View, - Text, - ScrollView, - TouchableOpacity, - Alert, - ActivityIndicator, -} from 'react-native'; - -import { - BarChart2, - Bell, - ChevronRight, - Download, - Eye, - Globe, - HardDrive, - Lock, - LogOut, - MapPin, - Play, - 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'; - -// ───────────────────────────────────────────────────────────── -// 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' }, -]; - -// ───────────────────────────────────────────────────────────── -// Component -// ───────────────────────────────────────────────────────────── - -export function MobileSettings({ - onSignOut, - onChangePassword, - onLinkedAccounts, -}: any) { - const { theme, setTheme } = useAppStore(); - const { preferences, setPreference } = useNotificationStore(); - - 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' }, - ]); - }; - - return ( - - {/* ACCOUNT */} - - } - label="Profile Visibility" - right={ - - } - /> - - } - label="Two-Factor Auth" - right={} - /> - - {biometricAvailable && ( - - ) : ( - - ) - } - label="Biometric Login" - description={biometricEnabled ? 'Enabled' : 'Disabled'} - right={ - - } - /> - )} - - } label="Change Password" onPress={onChangePassword} /> - - - {/* PRIVACY */} - - } - label="Analytics" - right={} - /> - - - {/* DOWNLOADS */} - - } - label="WiFi Only" - right={ - - } - /> - - } - label="Quality" - right={ - - } - /> - - } - label="Clear Downloads" - onPress={handleClearDownloads} - destructive - /> - - - {/* APP */} - - } - label="Theme" - right={ - - } - /> - - - {/* SYNC */} - - } - label="Manual Sync" - onPress={handleManualSync} - /> - - - {/* SIGN OUT */} - - } - label="Sign Out" - onPress={handleSignOut} - destructive - /> - - - ); -} - -export default MobileSettings; \ No newline at end of file +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; \ No newline at end of file diff --git a/src/components/mobile/NotificationSettings.tsx b/src/components/mobile/NotificationSettings.tsx index 236eb1c..0b16df3 100644 --- a/src/components/mobile/NotificationSettings.tsx +++ b/src/components/mobile/NotificationSettings.tsx @@ -1,9 +1,24 @@ -import React from 'react'; -import { View, Text, Switch, ScrollView, TouchableOpacity } from 'react-native'; +import React, { useState } from 'react'; +import { + LayoutAnimation, + Platform, + ScrollView, + Switch, + Text, + TouchableOpacity, + UIManager, + View, +} from 'react-native'; +import { ChevronDown, ChevronUp } from 'lucide-react-native'; import { useNotificationPermission } from '../../hooks'; import { useNotificationStore } from '../../store/notificationStore'; import { NotificationPreferences } from '../../types/notifications'; +// Enable LayoutAnimation on Android +if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) { + UIManager.setLayoutAnimationEnabledExperimental(true); +} + interface SettingRowProps { icon: string; title: string; @@ -48,6 +63,9 @@ export function NotificationSettings() { const { preferences, setPreference, pushToken } = useNotificationStore(); const [savingKey, setSavingKey] = useState(null); + // Progressive disclosure: advanced notifications collapsed by default + const [showAdvancedNotifications, setShowAdvancedNotifications] = useState(false); + const isEnabled = permissionStatus === 'granted' && pushToken !== null; const handlePreferenceChange = async ( @@ -58,7 +76,7 @@ export function NotificationSettings() { setSavingKey(key); // Update local preferences (automatically persisted by Zustand) setPreference(key, value); - + // TODO: Sync with backend // try { // await api.updateNotificationPreferences({ [key]: value }); @@ -71,6 +89,11 @@ export function NotificationSettings() { } }; + const handleToggleAdvancedNotifications = () => { + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); + setShowAdvancedNotifications(prev => !prev); + }; + return ( {/* Permission Status Banner */} @@ -103,10 +126,10 @@ export function NotificationSettings() { )} - {/* Notification Categories */} + {/* ── ESSENTIAL: Primary Notification Types ───────────── */} - Notification Types + Notifications handlePreferenceChange('messages', value)} disabled={!isEnabled} /> - + + - handlePreferenceChange('learningReminders', value)} - disabled={!isEnabled} - /> - + {/* ── PROGRESSIVE DISCLOSURE: Advanced Notifications ─── */} + + + 🔔 + + {showAdvancedNotifications + ? 'Hide Advanced Notifications' + : 'Advanced Notifications'} + + + {showAdvancedNotifications ? ( + + ) : ( + + )} + - handlePreferenceChange('achievementUnlocks', value)} - disabled={!isEnabled} - /> - + {/* Advanced notification categories (expandable) */} + {showAdvancedNotifications && ( + + + More Notification Types + + + handlePreferenceChange('learningReminders', value)} + disabled={!isEnabled} + /> + - handlePreferenceChange('communityActivity', value)} - disabled={!isEnabled} - /> + handlePreferenceChange('achievementUnlocks', value)} + disabled={!isEnabled} + /> + + + handlePreferenceChange('communityActivity', value)} + disabled={!isEnabled} + /> + - + )} {/* Debug Info (remove in production) */} {__DEV__ && ( @@ -180,4 +240,6 @@ export function NotificationSettings() { ); } + export default NotificationSettings; +