mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-31 00:40:46 +02:00
PR #1868 conflated "has workspace" with "completed onboarding" — restore `onboarded_at` as the single signal, and route invited users through a dedicated /invitations page before they ever see onboarding. - Backend: CreateWorkspace + AcceptInvitation atomically set onboarded_at alongside the member insert, establishing the invariant "member row exists ↔ onboarded_at != null" at the DB layer. - Migration 065: one-shot backfill closes the dirty rows produced by PR #1868 (users with a workspace but onboarded_at == null). - Entry points (web callback, login, desktop App): if onboarded_at is null, look up pending invitations by email and route to the new batch /invitations page; otherwise the resolver picks workspace / new-workspace as before. - OnboardingPage: stops bouncing on hasWorkspaces; only hasOnboarded bounces. Unblocks the user from completing Step 3 (workspace creation) → Steps 4 / 5. - StarterContentPrompt: only shows when the user is the solo member of the workspace, so invited users never get prompted to import starter content into someone else's workspace. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
68 lines
2.3 KiB
TypeScript
68 lines
2.3 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect } from "react";
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import { useNavigationStore } from "@multica/core/navigation";
|
|
import { useAuthStore } from "@multica/core/auth";
|
|
import {
|
|
paths,
|
|
resolvePostAuthDestination,
|
|
useCurrentWorkspace,
|
|
useHasOnboarded,
|
|
} from "@multica/core/paths";
|
|
import { workspaceListOptions } from "@multica/core/workspace";
|
|
import { useNavigation } from "../navigation";
|
|
|
|
/**
|
|
* Auth + workspace gate for the dashboard.
|
|
*
|
|
* Redirect logic:
|
|
* - Auth still loading → wait
|
|
* - Not logged in → /login
|
|
* - Logged in but workspace list not yet loaded → wait (don't bounce prematurely)
|
|
* - Logged in but URL slug doesn't resolve to any workspace →
|
|
* `resolvePostAuthDestination(list, hasOnboarded)`:
|
|
* • un-onboarded → /onboarding
|
|
* • onboarded with workspaces → first workspace
|
|
* • onboarded with zero workspaces → /workspaces/new
|
|
*
|
|
* The "un-onboarded but in workspace" state is now physically impossible:
|
|
* CreateWorkspace and AcceptInvitation both atomically set `onboarded_at`
|
|
* inside the same transaction that inserts the `member` row.
|
|
* Existing dirty rows from PR #1868 are cleaned by migration 065.
|
|
*
|
|
* We read the workspace list query state directly (rather than relying on
|
|
* useCurrentWorkspace's null return) so we can distinguish "list loading"
|
|
* from "slug not found". Otherwise users could see a transient redirect
|
|
* before their workspace list arrives.
|
|
*/
|
|
export function useDashboardGuard() {
|
|
const { pathname, replace } = useNavigation();
|
|
const user = useAuthStore((s) => s.user);
|
|
const isLoading = useAuthStore((s) => s.isLoading);
|
|
const workspace = useCurrentWorkspace();
|
|
const hasOnboarded = useHasOnboarded();
|
|
const { data: workspaces = [], isFetched: workspaceListFetched } = useQuery({
|
|
...workspaceListOptions(),
|
|
enabled: !!user,
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (isLoading) return;
|
|
if (!user) {
|
|
replace(paths.login());
|
|
return;
|
|
}
|
|
if (!workspaceListFetched) return;
|
|
if (!workspace) {
|
|
replace(resolvePostAuthDestination(workspaces, hasOnboarded));
|
|
}
|
|
}, [user, isLoading, workspaceListFetched, workspace, workspaces, hasOnboarded, replace]);
|
|
|
|
useEffect(() => {
|
|
useNavigationStore.getState().onPathChange(pathname);
|
|
}, [pathname]);
|
|
|
|
return { user, isLoading, workspace };
|
|
}
|