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:
Steven Enamakel
2026-01-28 04:29:06 +05:30
parent c58d5fd625
commit 4323c4812c
15 changed files with 322 additions and 353 deletions
+5
View File
@@ -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
View File
@@ -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>
);
}
+2 -2
View File
@@ -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: {
+5 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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]);
-220
View File
@@ -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();
}
}
});
}
+38
View File
@@ -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;
-81
View File
@@ -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 }),
}
)
);
+5
View File
@@ -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;
+122
View File
@@ -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;
+33
View File
@@ -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;
-28
View File
@@ -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 });
},
}));
+3 -2
View File
@@ -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));
+84
View File
@@ -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"