diff --git a/src/App.tsx b/src/App.tsx
index 47d2e20d8..0a06a9648 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,61 +1,19 @@
-import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
+import { BrowserRouter as Router } from 'react-router-dom';
import { Provider } from 'react-redux';
import { PersistGate } from 'redux-persist/integration/react';
import { store, persistor } from './store';
-import Welcome from './pages/Welcome';
-import Login from './pages/Login';
-import Onboarding from './pages/onboarding/Onboarding';
-import Home from './pages/Home';
-import PublicRoute from './components/PublicRoute';
-import ProtectedRoute from './components/ProtectedRoute';
-import DefaultRedirect from './components/DefaultRedirect';
+import SocketProvider from './providers/SocketProvider';
+import AppRoutes from './AppRoutes';
function App() {
return (
-
-
- {/* Public routes - redirect to /home or /onboarding if logged in */}
-
-
-
- }
- />
-
-
-
- }
- />
-
- {/* Protected routes */}
-
-
-
- }
- />
-
-
-
- }
- />
-
- {/* Default redirect based on auth status */}
- } />
-
-
+
+
+
+
+
);
diff --git a/src/AppRoutes.tsx b/src/AppRoutes.tsx
new file mode 100644
index 000000000..f6199d629
--- /dev/null
+++ b/src/AppRoutes.tsx
@@ -0,0 +1,55 @@
+import { Routes, Route } from 'react-router-dom';
+import Welcome from './pages/Welcome';
+import Login from './pages/Login';
+import Onboarding from './pages/onboarding/Onboarding';
+import Home from './pages/Home';
+import PublicRoute from './components/PublicRoute';
+import ProtectedRoute from './components/ProtectedRoute';
+import DefaultRedirect from './components/DefaultRedirect';
+
+const AppRoutes = () => {
+ return (
+
+ {/* Public routes - redirect to /home or /onboarding if logged in */}
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+ {/* Protected routes */}
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+ {/* Default redirect based on auth status */}
+ } />
+
+ );
+};
+
+export default AppRoutes;
diff --git a/src/hooks/useSocket.ts b/src/hooks/useSocket.ts
index be234726c..cafbc333f 100644
--- a/src/hooks/useSocket.ts
+++ b/src/hooks/useSocket.ts
@@ -1,13 +1,12 @@
import { useEffect, useRef } from 'react';
import { socketService } from '../services/socketService';
+import { useAppSelector } from '../store/hooks';
import type { Socket } from 'socket.io-client';
-interface UseSocketOptions {
- autoConnect?: boolean;
-}
-
/**
* React hook for using the Socket.IO connection
+ * Note: The socket connection is managed by SocketProvider based on JWT token.
+ * This hook provides access to the socket instance and methods.
*
* @example
* ```tsx
@@ -24,15 +23,11 @@ interface UseSocketOptions {
* }, [on, off]);
* ```
*/
-export const useSocket = (options: UseSocketOptions = {}) => {
- const { autoConnect = true } = options;
+export const useSocket = () => {
const listenersRef = useRef void }>>([]);
+ const socketStatus = useAppSelector((state) => state.socket.status);
useEffect(() => {
- if (autoConnect) {
- socketService.connect();
- }
-
return () => {
// Cleanup: remove all listeners registered through this hook
listenersRef.current.forEach(({ event, callback }) => {
@@ -40,7 +35,7 @@ export const useSocket = (options: UseSocketOptions = {}) => {
});
listenersRef.current = [];
};
- }, [autoConnect]);
+ }, []);
const emit = (event: string, data?: unknown) => {
socketService.emit(event, data);
@@ -68,12 +63,11 @@ export const useSocket = (options: UseSocketOptions = {}) => {
return {
socket: socketService.getSocket() as Socket | null,
- isConnected: socketService.isConnected(),
+ isConnected: socketStatus === 'connected',
+ status: socketStatus,
emit,
on,
off,
once,
- connect: () => socketService.connect(),
- disconnect: () => socketService.disconnect(),
};
};
diff --git a/src/providers/SocketProvider.tsx b/src/providers/SocketProvider.tsx
new file mode 100644
index 000000000..d8844cc62
--- /dev/null
+++ b/src/providers/SocketProvider.tsx
@@ -0,0 +1,40 @@
+import { useEffect, useRef } from 'react';
+import { useAppSelector } from '../store/hooks';
+import { socketService } from '../services/socketService';
+
+/**
+ * SocketProvider manages the socket connection based on JWT token
+ * - Connects when token is set
+ * - Disconnects when token is unset
+ */
+const SocketProvider = ({ children }: { children: React.ReactNode }) => {
+ const token = useAppSelector((state) => state.auth.token);
+ const previousTokenRef = useRef(null);
+
+ useEffect(() => {
+ const previousToken = previousTokenRef.current;
+
+ // Token was set - connect
+ if (token && token !== previousToken) {
+ socketService.connect(token);
+ previousTokenRef.current = token;
+ }
+
+ // Token was unset - disconnect
+ if (!token && previousToken) {
+ socketService.disconnect();
+ previousTokenRef.current = null;
+ }
+ }, [token]);
+
+ // Cleanup on unmount
+ useEffect(() => {
+ return () => {
+ socketService.disconnect();
+ };
+ }, []);
+
+ return <>{children}>;
+};
+
+export default SocketProvider;
diff --git a/src/services/socketService.ts b/src/services/socketService.ts
new file mode 100644
index 000000000..730263284
--- /dev/null
+++ b/src/services/socketService.ts
@@ -0,0 +1,132 @@
+import { io, Socket } from 'socket.io-client';
+import { BACKEND_URL } from '../utils/config';
+import { store } from '../store';
+import { setStatus, setSocketId, reset } from '../store/socketSlice';
+
+class SocketService {
+ private socket: Socket | null = null;
+ private token: string | null = null;
+
+ /**
+ * Connect to the socket server with authentication
+ */
+ connect(token: string): void {
+ // Don't connect if already connected with the same token
+ if (this.socket?.connected && this.token === token) {
+ return;
+ }
+
+ // Disconnect existing connection if token changed
+ if (this.socket && this.token !== token) {
+ this.disconnect();
+ }
+
+ // Don't connect if socket exists and is not disconnected (already connecting or connected)
+ if (this.socket && !this.socket.disconnected) {
+ return;
+ }
+
+ this.token = token;
+
+ // Update status to connecting
+ store.dispatch(setStatus('connecting'));
+
+ // Create socket connection with auth token
+ this.socket = io(BACKEND_URL, {
+ auth: {
+ token,
+ },
+ transports: ['websocket', 'polling'],
+ reconnection: true,
+ reconnectionDelay: 1000,
+ reconnectionAttempts: 5,
+ });
+
+ // Connection event handlers
+ this.socket.on('connect', () => {
+ const socketId = this.socket?.id || null;
+ store.dispatch(setStatus('connected'));
+ store.dispatch(setSocketId(socketId));
+ });
+
+ this.socket.on('disconnect', () => {
+ store.dispatch(setStatus('disconnected'));
+ store.dispatch(setSocketId(null));
+ });
+
+ this.socket.on('connect_error', (error) => {
+ console.error('[Socket] Connection error:', error);
+ store.dispatch(setStatus('disconnected'));
+ });
+ }
+
+ /**
+ * Disconnect from the socket server
+ */
+ disconnect(): void {
+ if (this.socket) {
+ this.socket.disconnect();
+ this.socket = null;
+ this.token = null;
+ store.dispatch(reset());
+ }
+ }
+
+ /**
+ * Get the current socket instance
+ */
+ getSocket(): Socket | null {
+ return this.socket;
+ }
+
+ /**
+ * Check if socket is connected
+ */
+ isConnected(): boolean {
+ return this.socket?.connected || false;
+ }
+
+ /**
+ * Emit an event to the server
+ */
+ emit(event: string, data?: unknown): void {
+ if (this.socket?.connected) {
+ this.socket.emit(event, data);
+ } else {
+ console.warn(`[Socket] Cannot emit '${event}': socket not connected`);
+ }
+ }
+
+ /**
+ * Listen to an event from the server
+ */
+ on(event: string, callback: (...args: unknown[]) => void): void {
+ if (this.socket) {
+ this.socket.on(event, callback);
+ }
+ }
+
+ /**
+ * Remove an event listener
+ */
+ off(event: string, callback?: (...args: unknown[]) => void): void {
+ if (this.socket) {
+ if (callback) {
+ this.socket.off(event, callback);
+ } else {
+ this.socket.off(event);
+ }
+ }
+ }
+
+ /**
+ * Listen to an event once
+ */
+ once(event: string, callback: (...args: unknown[]) => void): void {
+ if (this.socket) {
+ this.socket.once(event, callback);
+ }
+ }
+}
+
+export const socketService = new SocketService();