diff --git a/src/components/Layout/index.tsx b/src/components/Layout/index.tsx index 0eef1dce1..5632acf5c 100644 --- a/src/components/Layout/index.tsx +++ b/src/components/Layout/index.tsx @@ -16,6 +16,7 @@ import animationData from '@/assets/animation/onboarding_success.json'; import { AnimationJson } from '@/components/AnimationJson'; import { InstallDependencies } from '@/components/InstallStep/InstallDependencies'; import TopBar from '@/components/TopBar'; +import { useAuthHydration } from '@/hooks/useAuthHydration'; import useChatStoreAdapter from '@/hooks/useChatStoreAdapter'; import { useInstallationSetup } from '@/hooks/useInstallationSetup'; import { useAuthStore } from '@/store/authStore'; @@ -27,6 +28,7 @@ import HistorySidebar from '../HistorySidebar'; import InstallationErrorDialog from '../InstallStep/InstallationErrorDialog/InstallationErrorDialog'; const Layout = () => { + const hasHydrated = useAuthHydration(); const { initState, isFirstLaunch, @@ -52,6 +54,8 @@ const Layout = () => { useInstallationSetup(); useEffect(() => { + if (!chatStore) return; + const handleBeforeClose = () => { const currentStatus = chatStore.tasks[chatStore.activeTaskId as string]?.status; @@ -67,7 +71,7 @@ const Layout = () => { return () => { window.ipcRenderer.removeAllListeners('before-close'); }; - }, [chatStore.tasks, chatStore.activeTaskId]); + }, [chatStore, chatStore?.tasks, chatStore?.activeTaskId]); // Determine what to show based on states const shouldShowOnboarding = @@ -79,9 +83,10 @@ const Layout = () => { installationState === 'waiting-backend'; const shouldShowMainContent = !actualShouldShowInstallScreen; - if (!chatStore) { - console.log(chatStore); - + // Wait for auth store hydration so initState/isFirstLaunch are correct + // and we don't briefly show install/onboarding then switch to main content + if (!hasHydrated || !chatStore) { + if (!chatStore) console.log(chatStore); return
Loading...
; } diff --git a/src/hooks/useAuthHydration.ts b/src/hooks/useAuthHydration.ts new file mode 100644 index 000000000..888d9d89a --- /dev/null +++ b/src/hooks/useAuthHydration.ts @@ -0,0 +1,43 @@ +// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= + +import { useAuthStore } from '@/store/authStore'; +import { useEffect, useState } from 'react'; + +/** + * Waits for the persisted auth store to finish rehydrating from storage. + * Use this before reading auth state (token, initState, etc.) to avoid + * temporary redirects or UI flicker when persisted state loads after first render. + * + * @returns true once the auth store has been hydrated (sync or async) + */ +export function useAuthHydration(): boolean { + const [hydrated, setHydrated] = useState(false); + + useEffect(() => { + // Subscribe first so we don't miss hydration completing between check and subscribe + const unsubFinish = useAuthStore.persist.onFinishHydration(() => { + setHydrated(true); + }); + // Sync check: hydration may already be done (e.g. sync localStorage) + if (useAuthStore.persist.hasHydrated()) { + setHydrated(true); + } + return () => { + unsubFinish(); + }; + }, []); + + return hydrated; +} diff --git a/src/routers/index.tsx b/src/routers/index.tsx index 301a86188..8bb49f5c5 100644 --- a/src/routers/index.tsx +++ b/src/routers/index.tsx @@ -12,6 +12,7 @@ // limitations under the License. // ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= +import { useAuthHydration } from '@/hooks/useAuthHydration'; import { useAuthStore } from '@/store/authStore'; import { lazy, useEffect, useReducer } from 'react'; import { Navigate, Outlet, Route, Routes } from 'react-router-dom'; @@ -53,8 +54,11 @@ const authReducer = (state: AuthState, action: AuthAction): AuthState => { } }; -// Route guard: Check if user is logged in +// Route guard: Check if user is logged in. +// Waits for persisted auth store to hydrate before reading token to avoid +// temporary redirect to /login or flicker when the user is actually logged in. const ProtectedRoute = () => { + const hasHydrated = useAuthHydration(); const [state, dispatch] = useReducer(authReducer, { loading: false, isAuthenticated: false, @@ -62,7 +66,10 @@ const ProtectedRoute = () => { }); const { token, localProxyValue, logout } = useAuthStore(); + useEffect(() => { + if (!hasHydrated) return; + // Check VITE_USE_LOCAL_PROXY value on app startup if (token) { const currentProxyValue = import.meta.env.VITE_USE_LOCAL_PROXY || null; @@ -78,9 +85,11 @@ const ProtectedRoute = () => { } dispatch({ type: 'INITIALIZE', payload: { isAuthenticated: !!token } }); - }, [token, localProxyValue, logout]); + }, [hasHydrated, token, localProxyValue, logout]); - if (state.loading || !state.initialized) { + // Show loading until persisted auth is rehydrated so we don't redirect + // logged-in users to /login briefly + if (!hasHydrated || state.loading || !state.initialized) { return (