mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
Integrate Redux for state management and refactor authentication flow
- Added Redux Toolkit for managing authentication state, replacing Zustand for token handling. - Implemented authSlice for token management, including setting and clearing tokens with localStorage synchronization. - Refactored components (Login, Home, TelegramLoginButton) to utilize Redux for token management, enhancing state consistency. - Updated App component to include Redux Provider and PersistGate for state persistence. - Removed Zustand-based authStore and socketStore, streamlining the state management approach. These changes improve the application's architecture by centralizing state management with Redux, enhancing maintainability and scalability.
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"@reduxjs/toolkit": "^2.11.2",
|
||||
"@tauri-apps/api": "^2",
|
||||
"@tauri-apps/plugin-deep-link": "^2",
|
||||
"@tauri-apps/plugin-opener": "^2",
|
||||
@@ -17,7 +18,10 @@
|
||||
"lottie-react": "^2.4.1",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-router-dom": "^7.13.0",
|
||||
"redux-logger": "^3.0.6",
|
||||
"redux-persist": "^6.0.0",
|
||||
"socket.io-client": "^4.8.3",
|
||||
"zustand": "^5.0.10"
|
||||
},
|
||||
@@ -27,6 +31,7 @@
|
||||
"@tauri-apps/cli": "^2",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@types/redux-logger": "^3.0.13",
|
||||
"@vitejs/plugin-react": "^4.6.0",
|
||||
"autoprefixer": "^10.4.23",
|
||||
"postcss": "^8.5.6",
|
||||
|
||||
+16
-9
@@ -1,4 +1,7 @@
|
||||
import { BrowserRouter as Router, Routes, Route, Navigate } 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';
|
||||
@@ -6,15 +9,19 @@ import Home from './pages/Home';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/" element={<Welcome />} />
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/onboarding" element={<Onboarding />} />
|
||||
<Route path="/home" element={<Home />} />
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
<Provider store={store}>
|
||||
<PersistGate loading={null} persistor={persistor}>
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/" element={<Welcome />} />
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/onboarding" element={<Onboarding />} />
|
||||
<Route path="/home" element={<Home />} />
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
</PersistGate>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useSocketStore } from '../store/socketStore';
|
||||
import { useAppSelector } from '../store/hooks';
|
||||
|
||||
interface ConnectionIndicatorProps {
|
||||
status?: 'connected' | 'disconnected' | 'connecting';
|
||||
@@ -12,7 +12,7 @@ const ConnectionIndicator = ({
|
||||
className = '',
|
||||
}: ConnectionIndicatorProps) => {
|
||||
// Use socket store status, but allow override via props
|
||||
const storeStatus = useSocketStore((state) => state.status);
|
||||
const storeStatus = useAppSelector((state) => state.socket.status);
|
||||
const status = overrideStatus || storeStatus;
|
||||
const statusConfig = {
|
||||
connected: {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { BACKEND_URL, TELEGRAM_BOT_ID } from '../utils/config';
|
||||
import { useAuthStore } from '../store/authStore';
|
||||
import { useAppDispatch } from '../store/hooks';
|
||||
import { setToken } from '../store/authSlice';
|
||||
|
||||
interface TelegramAuthData {
|
||||
id: number;
|
||||
@@ -27,8 +28,8 @@ const TelegramLoginButton = ({
|
||||
disabled: externalDisabled = false,
|
||||
}: TelegramLoginButtonProps) => {
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useAppDispatch();
|
||||
const [isAuthenticating, setIsAuthenticating] = useState(false);
|
||||
const setToken = useAuthStore((state) => state.setToken);
|
||||
|
||||
const handleTelegramLogin = async () => {
|
||||
if (isAuthenticating || externalDisabled) return;
|
||||
@@ -80,7 +81,7 @@ const TelegramLoginButton = ({
|
||||
const jwtToken = event.data.token;
|
||||
|
||||
// Store JWT token in store (received from backend server)
|
||||
setToken(jwtToken);
|
||||
dispatch(setToken(jwtToken));
|
||||
|
||||
// Call onSuccess callback if provided, otherwise navigate to onboarding
|
||||
if (onSuccess) {
|
||||
@@ -181,7 +182,7 @@ const TelegramLoginButton = ({
|
||||
}
|
||||
|
||||
// Store JWT token in store (this is the JWT from the backend)
|
||||
setToken(sessionToken);
|
||||
dispatch(setToken(sessionToken));
|
||||
|
||||
// Store user data in localStorage for backward compatibility
|
||||
if (user) {
|
||||
|
||||
+4
-3
@@ -3,11 +3,12 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { openUrl } from '../utils/openUrl';
|
||||
import { TELEGRAM_BOT_USERNAME } from '../utils/config';
|
||||
import ConnectionIndicator from '../components/ConnectionIndicator';
|
||||
import { useAuthStore } from '../store/authStore';
|
||||
import { useAppDispatch } from '../store/hooks';
|
||||
import { clearToken } from '../store/authSlice';
|
||||
|
||||
const Home = () => {
|
||||
const navigate = useNavigate();
|
||||
const clearToken = useAuthStore((state) => state.clearToken);
|
||||
const dispatch = useAppDispatch();
|
||||
const [userName] = useState('Cyrus');
|
||||
|
||||
// Get current date
|
||||
@@ -47,7 +48,7 @@ const Home = () => {
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
clearToken();
|
||||
dispatch(clearToken());
|
||||
navigate('/');
|
||||
};
|
||||
|
||||
|
||||
+5
-4
@@ -1,11 +1,12 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { useAuthStore } from '../store/authStore';
|
||||
import { useAppDispatch } from '../store/hooks';
|
||||
import { setToken } from '../store/authSlice';
|
||||
|
||||
const Login = () => {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const setToken = useAuthStore((state) => state.setToken);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
// Handle JWT token from URL query parameter (backend redirect)
|
||||
useEffect(() => {
|
||||
@@ -14,7 +15,7 @@ const Login = () => {
|
||||
if (token) {
|
||||
|
||||
// Store the JWT token from the backend
|
||||
setToken(token);
|
||||
dispatch(setToken(token));
|
||||
|
||||
// Clear the token from URL for security
|
||||
const newSearchParams = new URLSearchParams(searchParams);
|
||||
@@ -28,7 +29,7 @@ const Login = () => {
|
||||
navigate('/onboarding/');
|
||||
}, 100);
|
||||
}
|
||||
}, [searchParams, setToken, navigate]);
|
||||
}, [searchParams, dispatch, navigate]);
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,220 +0,0 @@
|
||||
import { io, Socket } from 'socket.io-client';
|
||||
import { BACKEND_URL } from '../utils/config';
|
||||
import { useAuthStore } from '../store/authStore';
|
||||
import { useSocketStore } from '../store/socketStore';
|
||||
|
||||
class SocketService {
|
||||
private socket: Socket | null = null;
|
||||
private reconnectAttempts = 0;
|
||||
private maxReconnectAttempts = 5;
|
||||
private reconnectDelay = 1000; // Start with 1 second
|
||||
|
||||
/**
|
||||
* Connect to the Socket.IO server using the JWT token from the auth store
|
||||
*/
|
||||
connect(): void {
|
||||
const token = useAuthStore.getState().token;
|
||||
|
||||
if (!token) {
|
||||
console.warn('[SocketService] No token available, cannot connect');
|
||||
useSocketStore.getState().setStatus('disconnected');
|
||||
return;
|
||||
}
|
||||
|
||||
// Disconnect existing connection if any
|
||||
if (this.socket?.connected) {
|
||||
this.disconnect();
|
||||
}
|
||||
|
||||
console.log('[SocketService] Connecting to socket server...');
|
||||
useSocketStore.getState().setStatus('connecting');
|
||||
|
||||
this.socket = io(BACKEND_URL, {
|
||||
auth: {
|
||||
token,
|
||||
},
|
||||
path: '/socket.io/',
|
||||
transports: ['polling', 'websocket'],
|
||||
reconnection: true,
|
||||
reconnectionAttempts: this.maxReconnectAttempts,
|
||||
reconnectionDelay: this.reconnectDelay,
|
||||
reconnectionDelayMax: 5000,
|
||||
});
|
||||
|
||||
this.setupEventHandlers();
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect from the Socket.IO server
|
||||
*/
|
||||
disconnect(): void {
|
||||
if (this.socket) {
|
||||
console.log('[SocketService] Disconnecting from socket server...');
|
||||
this.socket.disconnect();
|
||||
this.socket = null;
|
||||
this.reconnectAttempts = 0;
|
||||
useSocketStore.getState().setStatus('disconnected');
|
||||
useSocketStore.getState().setSocketId(null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the socket is connected
|
||||
*/
|
||||
isConnected(): boolean {
|
||||
return this.socket?.connected ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the socket instance (use with caution)
|
||||
*/
|
||||
getSocket(): Socket | null {
|
||||
return this.socket;
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit an event to the server
|
||||
*/
|
||||
emit(event: string, data?: unknown): void {
|
||||
if (!this.socket?.connected) {
|
||||
console.warn(`[SocketService] Cannot emit '${event}': socket not connected`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.socket.emit(event, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen to an event from the server
|
||||
*/
|
||||
on(event: string, callback: (...args: unknown[]) => void): void {
|
||||
if (!this.socket) {
|
||||
console.warn(`[SocketService] Cannot listen to '${event}': socket not initialized`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.socket.on(event, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an event listener
|
||||
*/
|
||||
off(event: string, callback?: (...args: unknown[]) => void): void {
|
||||
if (!this.socket) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.socket.off(event, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen to an event once
|
||||
*/
|
||||
once(event: string, callback: (...args: unknown[]) => void): void {
|
||||
if (!this.socket) {
|
||||
console.warn(`[SocketService] Cannot listen to '${event}': socket not initialized`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.socket.once(event, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup event handlers for connection, disconnection, and errors
|
||||
*/
|
||||
private setupEventHandlers(): void {
|
||||
if (!this.socket) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.socket.on('connect', () => {
|
||||
console.log('[SocketService] Connected to socket server:', this.socket?.id);
|
||||
this.reconnectAttempts = 0;
|
||||
useSocketStore.getState().setStatus('connected');
|
||||
useSocketStore.getState().setSocketId(this.socket?.id || null);
|
||||
});
|
||||
|
||||
this.socket.on('disconnect', (reason) => {
|
||||
console.log('[SocketService] Disconnected from socket server:', reason);
|
||||
useSocketStore.getState().setStatus('disconnected');
|
||||
useSocketStore.getState().setSocketId(null);
|
||||
|
||||
// If disconnected due to error, set status to connecting if we're trying to reconnect
|
||||
if (reason === 'io server disconnect' || reason === 'io client disconnect') {
|
||||
// Manual disconnect, keep as disconnected
|
||||
} else {
|
||||
// Network error or other, might be reconnecting
|
||||
if (this.socket && !this.socket.disconnected) {
|
||||
useSocketStore.getState().setStatus('connecting');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.socket.on('connect_error', (error) => {
|
||||
console.error('[SocketService] Connection error:', error.message);
|
||||
this.reconnectAttempts++;
|
||||
useSocketStore.getState().setStatus('connecting');
|
||||
|
||||
// If max attempts reached, try reconnecting with fresh token
|
||||
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
||||
console.log('[SocketService] Max reconnection attempts reached, reconnecting with fresh token...');
|
||||
useSocketStore.getState().setStatus('disconnected');
|
||||
setTimeout(() => {
|
||||
this.connect();
|
||||
}, this.reconnectDelay * 2);
|
||||
}
|
||||
});
|
||||
|
||||
this.socket.on('ready', () => {
|
||||
console.log('[SocketService] Server ready');
|
||||
useSocketStore.getState().setStatus('connected');
|
||||
});
|
||||
|
||||
this.socket.on('error', (error: { message?: string; status?: number; requestId?: string }) => {
|
||||
console.error('[SocketService] Server error:', error);
|
||||
// Don't change status on server errors, connection might still be active
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Export a singleton instance
|
||||
export const socketService = new SocketService();
|
||||
|
||||
// Auto-connect when token is available
|
||||
// Listen to auth store changes
|
||||
if (typeof window !== 'undefined') {
|
||||
let previousToken: string | null = null;
|
||||
|
||||
const checkAndUpdateConnection = () => {
|
||||
const currentToken = useAuthStore.getState().token;
|
||||
|
||||
// Only update connection if token actually changed
|
||||
if (currentToken !== previousToken) {
|
||||
previousToken = currentToken;
|
||||
|
||||
if (currentToken && !socketService.isConnected()) {
|
||||
socketService.connect();
|
||||
} else if (!currentToken && socketService.isConnected()) {
|
||||
socketService.disconnect();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Initial connection check
|
||||
checkAndUpdateConnection();
|
||||
|
||||
// Subscribe to auth store changes
|
||||
useAuthStore.subscribe((state) => {
|
||||
const currentToken = state.token;
|
||||
|
||||
if (currentToken !== previousToken) {
|
||||
previousToken = currentToken;
|
||||
|
||||
if (currentToken && !socketService.isConnected()) {
|
||||
socketService.connect();
|
||||
} else if (!currentToken && socketService.isConnected()) {
|
||||
socketService.disconnect();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
|
||||
interface AuthState {
|
||||
token: string | null;
|
||||
}
|
||||
|
||||
const initialState: AuthState = {
|
||||
token: null,
|
||||
};
|
||||
|
||||
// Initialize from localStorage for backward compatibility
|
||||
if (typeof window !== 'undefined') {
|
||||
const legacyToken = localStorage.getItem('sessionToken');
|
||||
if (legacyToken) {
|
||||
initialState.token = legacyToken;
|
||||
}
|
||||
}
|
||||
|
||||
const authSlice = createSlice({
|
||||
name: 'auth',
|
||||
initialState,
|
||||
reducers: {
|
||||
setToken: (state, action: PayloadAction<string>) => {
|
||||
state.token = action.payload;
|
||||
// Also sync to localStorage for backward compatibility
|
||||
localStorage.setItem('sessionToken', action.payload);
|
||||
},
|
||||
clearToken: (state) => {
|
||||
state.token = null;
|
||||
// Also clear from localStorage for backward compatibility
|
||||
localStorage.removeItem('sessionToken');
|
||||
localStorage.removeItem('user');
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { setToken, clearToken } = authSlice.actions;
|
||||
export default authSlice.reducer;
|
||||
@@ -1,81 +0,0 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist, createJSONStorage } from 'zustand/middleware';
|
||||
|
||||
interface AuthState {
|
||||
token: string | null;
|
||||
setToken: (token: string) => void;
|
||||
clearToken: () => void;
|
||||
isAuthenticated: () => boolean;
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'auth-storage';
|
||||
|
||||
// Custom storage that syncs with localStorage 'sessionToken' for backward compatibility
|
||||
const customStorage = {
|
||||
getItem: (name: string): string | null => {
|
||||
// First check Zustand's persisted storage
|
||||
const zustandData = localStorage.getItem(name);
|
||||
if (zustandData) {
|
||||
try {
|
||||
const parsed = JSON.parse(zustandData);
|
||||
if (parsed.state?.token) {
|
||||
return zustandData;
|
||||
}
|
||||
} catch {
|
||||
// Ignore parse errors
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to legacy sessionToken for backward compatibility
|
||||
const legacyToken = localStorage.getItem('sessionToken');
|
||||
if (legacyToken) {
|
||||
return JSON.stringify({ state: { token: legacyToken }, version: 0 });
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
setItem: (name: string, value: string): void => {
|
||||
localStorage.setItem(name, value);
|
||||
// Also sync to sessionToken for backward compatibility
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
if (parsed.state?.token) {
|
||||
localStorage.setItem('sessionToken', parsed.state.token);
|
||||
}
|
||||
} catch {
|
||||
// Ignore parse errors
|
||||
}
|
||||
},
|
||||
removeItem: (name: string): void => {
|
||||
localStorage.removeItem(name);
|
||||
localStorage.removeItem('sessionToken');
|
||||
},
|
||||
};
|
||||
|
||||
export const useAuthStore = create<AuthState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
token: null,
|
||||
|
||||
setToken: (token: string) => {
|
||||
set({ token });
|
||||
},
|
||||
|
||||
clearToken: () => {
|
||||
set({ token: null });
|
||||
// Also clear user data
|
||||
localStorage.removeItem('user');
|
||||
},
|
||||
|
||||
isAuthenticated: () => {
|
||||
return get().token !== null;
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: STORAGE_KEY,
|
||||
storage: createJSONStorage(() => customStorage),
|
||||
// Only persist the token
|
||||
partialize: (state) => ({ token: state.token }),
|
||||
}
|
||||
)
|
||||
);
|
||||
@@ -0,0 +1,5 @@
|
||||
import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux';
|
||||
import type { RootState, AppDispatch } from './index';
|
||||
|
||||
export const useAppDispatch = () => useDispatch<AppDispatch>();
|
||||
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
|
||||
@@ -0,0 +1,122 @@
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import { persistStore, persistReducer, FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER } from 'redux-persist';
|
||||
import storage from 'redux-persist/lib/storage';
|
||||
import authReducer from './authSlice';
|
||||
import socketReducer from './socketSlice';
|
||||
|
||||
// Persist config for auth only
|
||||
const authPersistConfig = {
|
||||
key: 'auth',
|
||||
storage,
|
||||
// Only persist the token
|
||||
whitelist: ['token'],
|
||||
};
|
||||
|
||||
// Custom storage that syncs with localStorage 'sessionToken' for backward compatibility
|
||||
// redux-persist stores data as JSON string with the state object
|
||||
const customStorage = {
|
||||
getItem: (key: string): Promise<string | null> => {
|
||||
return new Promise((resolve) => {
|
||||
// First check redux-persist storage (redux-persist adds 'persist:' prefix)
|
||||
const persistKey = `persist:${key}`;
|
||||
const persistData = localStorage.getItem(persistKey);
|
||||
|
||||
if (persistData) {
|
||||
try {
|
||||
const parsed = JSON.parse(persistData);
|
||||
// redux-persist format: { _persist: {...}, token: "..." }
|
||||
if (parsed && typeof parsed === 'object') {
|
||||
resolve(persistData);
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// Ignore parse errors
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to legacy sessionToken for backward compatibility
|
||||
const legacyToken = localStorage.getItem('sessionToken');
|
||||
if (legacyToken) {
|
||||
// Create redux-persist compatible format
|
||||
const fallbackData = JSON.stringify({
|
||||
token: legacyToken,
|
||||
_persist: { version: -1, rehydrated: true },
|
||||
});
|
||||
localStorage.setItem(persistKey, fallbackData);
|
||||
resolve(fallbackData);
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(null);
|
||||
});
|
||||
},
|
||||
setItem: (key: string, value: string): Promise<void> => {
|
||||
return new Promise((resolve) => {
|
||||
const persistKey = `persist:${key}`;
|
||||
localStorage.setItem(persistKey, value);
|
||||
// Also sync to sessionToken for backward compatibility
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
if (parsed && typeof parsed === 'object' && parsed.token) {
|
||||
localStorage.setItem('sessionToken', parsed.token);
|
||||
}
|
||||
} catch {
|
||||
// Ignore parse errors
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
},
|
||||
removeItem: (key: string): Promise<void> => {
|
||||
return new Promise((resolve) => {
|
||||
const persistKey = `persist:${key}`;
|
||||
localStorage.removeItem(persistKey);
|
||||
localStorage.removeItem('sessionToken');
|
||||
resolve();
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
const authPersistConfigWithCustomStorage = {
|
||||
...authPersistConfig,
|
||||
storage: customStorage,
|
||||
};
|
||||
|
||||
const persistedAuthReducer = persistReducer(authPersistConfigWithCustomStorage, authReducer);
|
||||
|
||||
// Get logger only in dev mode
|
||||
let loggerMiddleware: unknown = undefined;
|
||||
if (import.meta.env.DEV) {
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const createLogger = require('redux-logger');
|
||||
loggerMiddleware = createLogger.createLogger();
|
||||
} catch {
|
||||
// Logger not available, continue without it
|
||||
}
|
||||
}
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
auth: persistedAuthReducer,
|
||||
socket: socketReducer,
|
||||
},
|
||||
middleware: (getDefaultMiddleware) => {
|
||||
const middleware = getDefaultMiddleware({
|
||||
serializableCheck: {
|
||||
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
|
||||
},
|
||||
});
|
||||
|
||||
// Add redux-logger in development
|
||||
if (loggerMiddleware) {
|
||||
return middleware.concat(loggerMiddleware);
|
||||
}
|
||||
|
||||
return middleware;
|
||||
},
|
||||
});
|
||||
|
||||
export const persistor = persistStore(store);
|
||||
|
||||
export type RootState = ReturnType<typeof store.getState>;
|
||||
export type AppDispatch = typeof store.dispatch;
|
||||
@@ -0,0 +1,33 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
|
||||
export type SocketConnectionStatus = 'connected' | 'disconnected' | 'connecting';
|
||||
|
||||
interface SocketState {
|
||||
status: SocketConnectionStatus;
|
||||
socketId: string | null;
|
||||
}
|
||||
|
||||
const initialState: SocketState = {
|
||||
status: 'disconnected',
|
||||
socketId: null,
|
||||
};
|
||||
|
||||
const socketSlice = createSlice({
|
||||
name: 'socket',
|
||||
initialState,
|
||||
reducers: {
|
||||
setStatus: (state, action: PayloadAction<SocketConnectionStatus>) => {
|
||||
state.status = action.payload;
|
||||
},
|
||||
setSocketId: (state, action: PayloadAction<string | null>) => {
|
||||
state.socketId = action.payload;
|
||||
},
|
||||
reset: (state) => {
|
||||
state.status = 'disconnected';
|
||||
state.socketId = null;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { setStatus, setSocketId, reset } = socketSlice.actions;
|
||||
export default socketSlice.reducer;
|
||||
@@ -1,28 +0,0 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
export type SocketConnectionStatus = 'connected' | 'disconnected' | 'connecting';
|
||||
|
||||
interface SocketState {
|
||||
status: SocketConnectionStatus;
|
||||
socketId: string | null;
|
||||
setStatus: (status: SocketConnectionStatus) => void;
|
||||
setSocketId: (socketId: string | null) => void;
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
export const useSocketStore = create<SocketState>((set) => ({
|
||||
status: 'disconnected',
|
||||
socketId: null,
|
||||
|
||||
setStatus: (status: SocketConnectionStatus) => {
|
||||
set({ status });
|
||||
},
|
||||
|
||||
setSocketId: (socketId: string | null) => {
|
||||
set({ socketId });
|
||||
},
|
||||
|
||||
reset: () => {
|
||||
set({ status: 'disconnected', socketId: null });
|
||||
},
|
||||
}));
|
||||
@@ -1,7 +1,8 @@
|
||||
import { getCurrent, onOpenUrl } from '@tauri-apps/plugin-deep-link';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { BACKEND_URL } from './config';
|
||||
import { useAuthStore } from '../store/authStore';
|
||||
import { store } from '../store';
|
||||
import { setToken } from '../store/authSlice';
|
||||
|
||||
/**
|
||||
* Handle a list of deep link URLs delivered by the Tauri deep-link plugin.
|
||||
@@ -53,7 +54,7 @@ const handleDeepLinkUrls = async (urls: string[] | null | undefined) => {
|
||||
}
|
||||
|
||||
// Store session token in store
|
||||
useAuthStore.getState().setToken(sessionToken);
|
||||
store.dispatch(setToken(sessionToken));
|
||||
localStorage.setItem('deepLinkHandled', 'true');
|
||||
if (user) {
|
||||
localStorage.setItem('user', JSON.stringify(user));
|
||||
|
||||
@@ -350,6 +350,18 @@
|
||||
"@nodelib/fs.scandir" "2.1.5"
|
||||
fastq "^1.6.0"
|
||||
|
||||
"@reduxjs/toolkit@^2.11.2":
|
||||
version "2.11.2"
|
||||
resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-2.11.2.tgz#582225acea567329ca6848583e7dd72580d38e82"
|
||||
integrity sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==
|
||||
dependencies:
|
||||
"@standard-schema/spec" "^1.0.0"
|
||||
"@standard-schema/utils" "^0.3.0"
|
||||
immer "^11.0.0"
|
||||
redux "^5.0.1"
|
||||
redux-thunk "^3.1.0"
|
||||
reselect "^5.1.0"
|
||||
|
||||
"@rolldown/pluginutils@1.0.0-beta.27":
|
||||
version "1.0.0-beta.27"
|
||||
resolved "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz"
|
||||
@@ -485,6 +497,16 @@
|
||||
resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz#821f8442f4175d8f0467b9daf26e3a18e2d02af2"
|
||||
integrity sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==
|
||||
|
||||
"@standard-schema/spec@^1.0.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@standard-schema/spec/-/spec-1.1.0.tgz#a79b55dbaf8604812f52d140b2c9ab41bc150bb8"
|
||||
integrity sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==
|
||||
|
||||
"@standard-schema/utils@^0.3.0":
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@standard-schema/utils/-/utils-0.3.0.tgz#3d5e608f16c2390c10528e98e59aef6bf73cae7b"
|
||||
integrity sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==
|
||||
|
||||
"@tailwindcss/forms@^0.5.11":
|
||||
version "0.5.11"
|
||||
resolved "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.11.tgz"
|
||||
@@ -662,6 +684,18 @@
|
||||
dependencies:
|
||||
csstype "^3.2.2"
|
||||
|
||||
"@types/redux-logger@^3.0.13":
|
||||
version "3.0.13"
|
||||
resolved "https://registry.yarnpkg.com/@types/redux-logger/-/redux-logger-3.0.13.tgz#473e98428cdcc6dc93c908de66732bf932e36bc8"
|
||||
integrity sha512-jylqZXQfMxahkuPcO8J12AKSSCQngdEWQrw7UiLUJzMBcv1r4Qg77P6mjGLjM27e5gFQDPD8vwUMJ9AyVxFSsg==
|
||||
dependencies:
|
||||
redux "^5.0.0"
|
||||
|
||||
"@types/use-sync-external-store@^0.0.6":
|
||||
version "0.0.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz#60be8d21baab8c305132eb9cb912ed497852aadc"
|
||||
integrity sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==
|
||||
|
||||
"@vitejs/plugin-react@^4.6.0":
|
||||
version "4.7.0"
|
||||
resolved "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz"
|
||||
@@ -788,6 +822,11 @@ debug@^4.1.0, debug@^4.3.1, debug@~4.4.1:
|
||||
dependencies:
|
||||
ms "^2.1.3"
|
||||
|
||||
deep-diff@^0.3.5:
|
||||
version "0.3.8"
|
||||
resolved "https://registry.yarnpkg.com/deep-diff/-/deep-diff-0.3.8.tgz#c01de63efb0eec9798801d40c7e0dae25b582c84"
|
||||
integrity sha512-yVn6RZmHiGnxRKR9sJb3iVV2XTF1Ghh2DiWRZ3dMnGc43yUdWWF/kX6lQyk3+P84iprfWKU/8zFTrlkvtFm1ug==
|
||||
|
||||
didyoumean@^1.2.2:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz"
|
||||
@@ -927,6 +966,11 @@ hasown@^2.0.2:
|
||||
dependencies:
|
||||
function-bind "^1.1.2"
|
||||
|
||||
immer@^11.0.0:
|
||||
version "11.1.3"
|
||||
resolved "https://registry.yarnpkg.com/immer/-/immer-11.1.3.tgz#78681e1deb6cec39753acf04eb16d7576c04f4d6"
|
||||
integrity sha512-6jQTc5z0KJFtr1UgFpIL3N9XSC3saRaI9PwWtzM2pSqkNGtiNkYY2OSwkOGDK2XcTRcLb1pi/aNkKZz0nxVH4Q==
|
||||
|
||||
is-binary-path@~2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz"
|
||||
@@ -1166,6 +1210,14 @@ react-dom@^19.1.0:
|
||||
dependencies:
|
||||
scheduler "^0.27.0"
|
||||
|
||||
react-redux@^9.2.0:
|
||||
version "9.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-9.2.0.tgz#96c3ab23fb9a3af2cb4654be4b51c989e32366f5"
|
||||
integrity sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==
|
||||
dependencies:
|
||||
"@types/use-sync-external-store" "^0.0.6"
|
||||
use-sync-external-store "^1.4.0"
|
||||
|
||||
react-refresh@^0.17.0:
|
||||
version "0.17.0"
|
||||
resolved "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz"
|
||||
@@ -1205,6 +1257,33 @@ readdirp@~3.6.0:
|
||||
dependencies:
|
||||
picomatch "^2.2.1"
|
||||
|
||||
redux-logger@^3.0.6:
|
||||
version "3.0.6"
|
||||
resolved "https://registry.yarnpkg.com/redux-logger/-/redux-logger-3.0.6.tgz#f7555966f3098f3c88604c449cf0baf5778274bf"
|
||||
integrity sha512-JoCIok7bg/XpqA1JqCqXFypuqBbQzGQySrhFzewB7ThcnysTO30l4VCst86AuB9T9tuT03MAA56Jw2PNhRSNCg==
|
||||
dependencies:
|
||||
deep-diff "^0.3.5"
|
||||
|
||||
redux-persist@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/redux-persist/-/redux-persist-6.0.0.tgz#b4d2972f9859597c130d40d4b146fecdab51b3a8"
|
||||
integrity sha512-71LLMbUq2r02ng2We9S215LtPu3fY0KgaGE0k8WRgl6RkqxtGfl7HUozz1Dftwsb0D/5mZ8dwAaPbtnzfvbEwQ==
|
||||
|
||||
redux-thunk@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-3.1.0.tgz#94aa6e04977c30e14e892eae84978c1af6058ff3"
|
||||
integrity sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==
|
||||
|
||||
redux@^5.0.0, redux@^5.0.1:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/redux/-/redux-5.0.1.tgz#97fa26881ce5746500125585d5642c77b6e9447b"
|
||||
integrity sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==
|
||||
|
||||
reselect@^5.1.0:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/reselect/-/reselect-5.1.1.tgz#c766b1eb5d558291e5e550298adb0becc24bb72e"
|
||||
integrity sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==
|
||||
|
||||
resolve@^1.1.7, resolve@^1.22.8:
|
||||
version "1.22.11"
|
||||
resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz"
|
||||
@@ -1391,6 +1470,11 @@ update-browserslist-db@^1.2.0:
|
||||
escalade "^3.2.0"
|
||||
picocolors "^1.1.1"
|
||||
|
||||
use-sync-external-store@^1.4.0:
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz#b174bfa65cb2b526732d9f2ac0a408027876f32d"
|
||||
integrity sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==
|
||||
|
||||
util-deprecate@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz"
|
||||
|
||||
Reference in New Issue
Block a user