Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 17 additions & 20 deletions app/settings.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<View className="flex-1 bg-white dark:bg-gray-900 p-4">
<Text className="text-2xl font-bold text-gray-900 dark:text-white mb-6">
Settings
</Text>
const handleSignOut = async () => {
await mobileAuthService.logout();
logout();
router.replace('/');
};

<View className="flex-row items-center justify-between mb-4">
<Text className="text-gray-900 dark:text-white text-lg">
Dark Mode
</Text>
<Switch
value={isDark}
onValueChange={(value) => setTheme(value ? 'dark' : 'light')}
/>
</View>
</View>
);
return (
<SafeAreaView style={{ flex: 1 }} edges={['top']}>
<MobileSettings onSignOut={handleSignOut} />
</SafeAreaView>
);
}
51 changes: 51 additions & 0 deletions src/components/mobile/MobileSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
ScrollView,
TouchableOpacity,
Alert,
ActivityIndicator,
} from 'react-native';
import {
User,
Expand All @@ -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';
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -240,6 +266,31 @@ export function MobileSettings({
/>
}
/>
{biometricAvailable && (
<SettingRow
iconBg="bg-cyan-100 dark:bg-cyan-900/50"
icon={
biometricLoading
? <ActivityIndicator size="small" color="#06b6d4" />
: <FingerprintPattern size={18} color="#06b6d4" />
}
label={
biometricType === 'face'
? 'Face ID Login'
: biometricType === 'iris'
? 'Iris Login'
: 'Fingerprint Login'
}
description={biometricEnabled ? 'Enabled — sign in without a password' : 'Disabled'}
right={
<NativeToggle
value={biometricEnabled}
onValueChange={handleBiometricToggle}
disabled={biometricLoading}
/>
}
/>
)}
<SettingRow
iconBg="bg-blue-100 dark:bg-blue-900/50"
icon={<User size={18} color="#3b82f6" />}
Expand Down
75 changes: 71 additions & 4 deletions src/services/socket/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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();
export default new SocketService();
Loading