Compare commits

...

1 Commits

Author SHA1 Message Date
Jiang Bohan
627d1b4fbc fix(desktop): prevent false onboarding trigger for users with existing workspaces
When logging in via email/OTP, verifyCode sets `user` in the auth store
before listWorkspaces + hydrateWorkspace complete. This causes AppContent
to render DesktopShell with workspace=null, and OnboardingGate freezes
that state — permanently showing the "create workspace" wizard.

Three-part fix:
- Add `workspaceHydrated` flag to workspace store (true once
  hydrateWorkspace has run, regardless of result)
- Reorder AuthInitializer to hydrate workspace before flipping isLoading
- Gate DesktopShell rendering on workspaceHydrated in AppContent
2026-04-15 20:55:10 +08:00
3 changed files with 27 additions and 6 deletions

View File

@@ -13,6 +13,7 @@ import { UpdateNotification } from "./components/update-notification";
function AppContent() {
const user = useAuthStore((s) => s.user);
const isLoading = useAuthStore((s) => s.isLoading);
const workspaceHydrated = useWorkspaceStore((s) => s.workspaceHydrated);
// Deep-link login runs loginWithToken → syncToken → listWorkspaces →
// hydrateWorkspace sequentially. loginWithToken sets user+isLoading=false
// as soon as getMe resolves, which would cause DesktopShell to mount
@@ -72,6 +73,20 @@ function AppContent() {
}
if (!user) return <DesktopLoginPage />;
// Wait for workspace hydration before mounting DesktopShell so that
// OnboardingGate gets a definitive hasWorkspace value on first render.
// Without this, OTP login sets `user` (triggering this branch) before
// listWorkspaces + hydrateWorkspace complete, causing OnboardingGate
// to freeze with hasWorkspace=false even when the user has a workspace.
if (!workspaceHydrated) {
return (
<div className="flex h-screen items-center justify-center">
<MulticaIcon className="size-6 animate-pulse" />
</div>
);
}
return <DesktopShell />;
}

View File

@@ -43,9 +43,11 @@ export function AuthInitializer({
Promise.all([api.getMe(), api.listWorkspaces()])
.then(([user, wsList]) => {
onLogin?.();
useAuthStore.setState({ user, isLoading: false });
// Hydrate workspace BEFORE flipping isLoading — AppContent gates
// on isLoading, so workspace must be ready when it re-renders.
qc.setQueryData(workspaceKeys.list(), wsList);
useWorkspaceStore.getState().hydrateWorkspace(wsList, wsId);
useAuthStore.setState({ user, isLoading: false });
})
.catch((err) => {
logger.error("cookie auth init failed", err);
@@ -68,10 +70,11 @@ export function AuthInitializer({
Promise.all([api.getMe(), api.listWorkspaces()])
.then(([user, wsList]) => {
onLogin?.();
useAuthStore.setState({ user, isLoading: false });
// Seed React Query cache so components don't need a second fetch
// Hydrate workspace BEFORE flipping isLoading — AppContent gates
// on isLoading, so workspace must be ready when it re-renders.
qc.setQueryData(workspaceKeys.list(), wsList);
useWorkspaceStore.getState().hydrateWorkspace(wsList, wsId);
useAuthStore.setState({ user, isLoading: false });
})
.catch((err) => {
logger.error("auth init failed", err);

View File

@@ -12,6 +12,8 @@ interface WorkspaceStoreOptions {
interface WorkspaceState {
workspace: Workspace | null;
/** True once `hydrateWorkspace` has run (even if no workspace was found). */
workspaceHydrated: boolean;
}
interface WorkspaceActions {
@@ -39,6 +41,7 @@ export function createWorkspaceStore(api: ApiClient, options?: WorkspaceStoreOpt
// Only the currently selected workspace (UI state).
// The workspace list is server state and lives in React Query.
workspace: null,
workspaceHydrated: false,
hydrateWorkspace: (wsList, preferredWorkspaceId) => {
const nextWorkspace =
@@ -53,7 +56,7 @@ export function createWorkspaceStore(api: ApiClient, options?: WorkspaceStoreOpt
setCurrentWorkspaceId(null);
rehydrateAllWorkspaceStores();
storage?.removeItem("multica_workspace_id");
set({ workspace: null });
set({ workspace: null, workspaceHydrated: true });
return null;
}
@@ -61,7 +64,7 @@ export function createWorkspaceStore(api: ApiClient, options?: WorkspaceStoreOpt
setCurrentWorkspaceId(nextWorkspace.id);
rehydrateAllWorkspaceStores();
storage?.setItem("multica_workspace_id", nextWorkspace.id);
set({ workspace: nextWorkspace });
set({ workspace: nextWorkspace, workspaceHydrated: true });
logger.debug("hydrate workspace", nextWorkspace.name, nextWorkspace.id);
return nextWorkspace;
@@ -86,7 +89,7 @@ export function createWorkspaceStore(api: ApiClient, options?: WorkspaceStoreOpt
api.setWorkspaceId(null);
setCurrentWorkspaceId(null);
rehydrateAllWorkspaceStores();
set({ workspace: null });
set({ workspace: null, workspaceHydrated: false });
},
}));
}