Files
multica/apps/web/app/auth/callback/page.tsx
Jiayuan Zhang 01232fc2f9 feat(onboarding): add full-screen onboarding wizard for new workspaces (#852)
* feat(onboarding): add full-screen onboarding wizard for new workspaces

Replace auto-provisioned workspace with an interactive 4-step onboarding
wizard: Create Workspace → Connect Runtime → Create Agent → Get Started.

- Remove server-side ensureUserWorkspace() so new users land in onboarding
- Add onboarding wizard in packages/views/onboarding/ (4 steps)
- Wire login/OAuth callbacks to redirect to /onboarding when no workspace
- Add DashboardGuard onboardingPath fallback for workspace-less users
- Sidebar "Create workspace" navigates to /onboarding instead of modal
- Remove CreateWorkspaceModal (replaced by wizard step 1)
- Auto-generate workspace slug from name (no user-facing URL field)
- Unified CLI install flow: install.sh + multica setup (auto-detects local)
- Create onboarding issues on completion with interactive "Say hello" task

* test(auth): update workspace tests to match onboarding flow

Login no longer auto-creates workspaces — new users start with zero
workspaces and create one through the onboarding wizard. Update both
integration and handler tests to assert 0 workspaces after verify-code.
2026-04-13 17:59:51 +08:00

140 lines
4.6 KiB
TypeScript

"use client";
import { Suspense, useEffect, useState } from "react";
import { useSearchParams, useRouter } from "next/navigation";
import { useQueryClient } from "@tanstack/react-query";
import { useAuthStore } from "@multica/core/auth";
import { useWorkspaceStore } from "@multica/core/workspace";
import { workspaceKeys } from "@multica/core/workspace/queries";
import { api } from "@multica/core/api";
import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
} from "@multica/ui/components/ui/card";
import { Button } from "@multica/ui/components/ui/button";
import { Loader2 } from "lucide-react";
function CallbackContent() {
const router = useRouter();
const searchParams = useSearchParams();
const qc = useQueryClient();
const loginWithGoogle = useAuthStore((s) => s.loginWithGoogle);
const hydrateWorkspace = useWorkspaceStore((s) => s.hydrateWorkspace);
const [error, setError] = useState("");
const [desktopToken, setDesktopToken] = useState<string | null>(null);
useEffect(() => {
const code = searchParams.get("code");
if (!code) {
setError("Missing authorization code");
return;
}
const errorParam = searchParams.get("error");
if (errorParam) {
setError(errorParam === "access_denied" ? "Access denied" : errorParam);
return;
}
const state = searchParams.get("state");
const isDesktop = state === "platform:desktop";
const redirectUri = `${window.location.origin}/auth/callback`;
if (isDesktop) {
// Desktop flow: exchange code for token, then redirect via deep link
api
.googleLogin(code, redirectUri)
.then(({ token }) => {
setDesktopToken(token);
window.location.href = `multica://auth/callback?token=${encodeURIComponent(token)}`;
})
.catch((err) => {
setError(err instanceof Error ? err.message : "Login failed");
});
} else {
// Normal web flow
loginWithGoogle(code, redirectUri)
.then(async () => {
const wsList = await api.listWorkspaces();
qc.setQueryData(workspaceKeys.list(), wsList);
const lastWsId = localStorage.getItem("multica_workspace_id");
const ws = await hydrateWorkspace(wsList, lastWsId);
router.push(ws ? "/issues" : "/onboarding");
})
.catch((err) => {
setError(err instanceof Error ? err.message : "Login failed");
});
}
}, [searchParams, loginWithGoogle, hydrateWorkspace, router, qc]);
if (desktopToken) {
return (
<div className="flex min-h-screen items-center justify-center">
<Card className="w-full max-w-sm">
<CardHeader className="text-center">
<CardTitle className="text-2xl">Opening Multica</CardTitle>
<CardDescription>
You should see a prompt to open the Multica desktop app. If
nothing happens, click the button below.
</CardDescription>
</CardHeader>
<CardContent className="flex justify-center">
<Button
variant="outline"
onClick={() => {
window.location.href = `multica://auth/callback?token=${encodeURIComponent(desktopToken)}`;
}}
>
Open Multica Desktop
</Button>
</CardContent>
</Card>
</div>
);
}
if (error) {
return (
<div className="flex min-h-screen items-center justify-center">
<Card className="w-full max-w-sm">
<CardHeader className="text-center">
<CardTitle className="text-2xl">Login Failed</CardTitle>
<CardDescription>{error}</CardDescription>
</CardHeader>
<CardContent className="flex justify-center">
<a href="/login" className="text-primary underline-offset-4 hover:underline">
Back to login
</a>
</CardContent>
</Card>
</div>
);
}
return (
<div className="flex min-h-screen items-center justify-center">
<Card className="w-full max-w-sm">
<CardHeader className="text-center">
<CardTitle className="text-2xl">Signing in...</CardTitle>
<CardDescription>Please wait while we complete your login</CardDescription>
</CardHeader>
<CardContent className="flex justify-center">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</CardContent>
</Card>
</div>
);
}
export default function CallbackPage() {
return (
<Suspense fallback={null}>
<CallbackContent />
</Suspense>
);
}