diff --git a/app/settings.tsx b/app/settings.tsx index c929676..968aa09 100644 --- a/app/settings.tsx +++ b/app/settings.tsx @@ -1,26 +1,23 @@ -import { useAppStore } from '@/src/store'; +import { useRouter } from 'expo-router'; import React from 'react'; -import { Switch, Text, View } from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import { MobileSettings } from '@/src/components/mobile/MobileSettings'; +import { useAppStore } from '@/src/store'; +import mobileAuthService from '@/src/services/mobileAuth'; export default function SettingsScreen() { - const { theme, setTheme } = useAppStore(); - const isDark = theme === 'dark'; + const router = useRouter(); + const { logout } = useAppStore(); - return ( - - - Settings - + const handleSignOut = async () => { + await mobileAuthService.logout(); + logout(); + router.replace('/'); + }; - - - Dark Mode - - setTheme(value ? 'dark' : 'light')} - /> - - - ); + return ( + + + + ); } diff --git a/src/components/mobile/MobileSettings.tsx b/src/components/mobile/MobileSettings.tsx index 9f16842..7c35d99 100644 --- a/src/components/mobile/MobileSettings.tsx +++ b/src/components/mobile/MobileSettings.tsx @@ -5,6 +5,7 @@ import { ScrollView, TouchableOpacity, Alert, + ActivityIndicator, } from 'react-native'; import { User, @@ -26,10 +27,12 @@ import { Play, Vibrate, LogOut, + FingerprintPattern, } from 'lucide-react-native'; import { useAppStore } from '../../store'; import { useSettingsStore } from '../../store/settingsStore'; import { useNotificationStore } from '../../store/notificationStore'; +import { useBiometricAuth } from '../../hooks/useBiometricAuth'; import { NativeToggle } from './NativeToggle'; import { SettingsPicker, PickerOption } from './SettingsPicker'; import { SettingsSection } from './SettingsSection'; @@ -154,6 +157,29 @@ export function MobileSettings({ }: MobileSettingsProps) { const { theme, setTheme } = useAppStore(); + const { + isAvailable: biometricAvailable, + isEnabled: biometricEnabled, + biometricType, + enable: enableBiometric, + disable: disableBiometric, + isLoading: biometricLoading, + } = useBiometricAuth(); + + const handleBiometricToggle = async (value: boolean) => { + if (value) { + const success = await enableBiometric(); + if (!success) { + Alert.alert( + 'Biometric Login', + 'Could not enable biometric login. Please check your device settings.', + ); + } + } else { + await disableBiometric(); + } + }; + const { profileVisibility, setProfileVisibility, twoFactorEnabled, setTwoFactorEnabled, @@ -240,6 +266,31 @@ export function MobileSettings({ /> } /> + {biometricAvailable && ( + + : + } + label={ + biometricType === 'face' + ? 'Face ID Login' + : biometricType === 'iris' + ? 'Iris Login' + : 'Fingerprint Login' + } + description={biometricEnabled ? 'Enabled — sign in without a password' : 'Disabled'} + right={ + + } + /> + )} } diff --git a/src/services/socket/index.ts b/src/services/socket/index.ts index 2bbd149..4582493 100644 --- a/src/services/socket/index.ts +++ b/src/services/socket/index.ts @@ -2,30 +2,92 @@ import { io, Socket } from "socket.io-client"; import logger from "../../utils/logger"; import { getEnv } from "../../config"; +// ─── Reconnection config ────────────────────────────────────────────────────── + +const RECONNECTION_ATTEMPTS = 10; +const RECONNECTION_DELAY_MS = 1_000; // initial delay +const RECONNECTION_DELAY_MAX_MS = 30_000; // cap at 30 s + class SocketService { private socket: Socket | null = null; connect() { + if (this.socket?.connected) return this.socket; + if (!this.socket) { const socketUrl = getEnv("EXPO_PUBLIC_SOCKET_URL"); this.socket = io(socketUrl, { transports: ["websocket"], autoConnect: true, + // ── Reconnection ────────────────────────────────────────────────── + reconnection: true, + reconnectionAttempts: RECONNECTION_ATTEMPTS, + reconnectionDelay: RECONNECTION_DELAY_MS, + reconnectionDelayMax: RECONNECTION_DELAY_MAX_MS, + randomizationFactor: 0.5, // jitter to avoid thundering herd }); + // ── Connection lifecycle ────────────────────────────────────────── + this.socket.on("connect", () => { logger.info("Socket connected:", this.socket?.id); }); - this.socket.on("disconnect", () => { - logger.info("Socket disconnected"); + this.socket.on("disconnect", (reason: string) => { + logger.warn("Socket disconnected:", reason); + // socket.io auto-reconnects unless the server explicitly closed it + if (reason === "io server disconnect") { + // Server forced disconnect — reconnect manually + this.socket?.connect(); + } }); - this.socket.on("error", (error) => { + this.socket.on("error", (error: unknown) => { logger.error("Socket error:", error); }); + + // ── Reconnection listeners ──────────────────────────────────────── + + this.socket.on("reconnect_attempt", (attempt: number) => { + logger.info(`Socket reconnection attempt #${attempt}`); + }); + + this.socket.on("reconnect", (attempt: number) => { + logger.info(`Socket reconnected after ${attempt} attempt(s)`); + }); + + this.socket.on("reconnect_error", (error: unknown) => { + logger.warn("Socket reconnection error:", error); + }); + + this.socket.on("reconnect_failed", () => { + logger.error( + `Socket failed to reconnect after ${RECONNECTION_ATTEMPTS} attempts` + ); + }); + + // ── Real-time event handlers ────────────────────────────────────── + + this.socket.on("notification_created", (notification: any) => { + logger.info("New notification received:", notification); + // TODO: Handle notification display/storage + // This could trigger a notification banner, update notification count, etc. + }); + + this.socket.on("course_updated", (courseData: any) => { + logger.info("Course updated:", courseData); + // TODO: Handle course data refresh + // This could update cached course data, refresh UI components, etc. + }); + + this.socket.on("message_received", (message: any) => { + logger.info("New message received:", message); + // TODO: Handle new message + // This could update chat UI, show message notification, etc. + }); } + return this.socket; } @@ -53,6 +115,11 @@ class SocketService { this.socket.off(event); } } + + /** Returns true when the underlying socket is currently connected. */ + get isConnected(): boolean { + return this.socket?.connected ?? false; + } } -export default new SocketService(); \ No newline at end of file +export default new SocketService();