"use client"; import { Suspense, useEffect, useState } from "react"; import { useSearchParams, useRouter } from "next/navigation"; import { useQueryClient } from "@tanstack/react-query"; import { sanitizeNextUrl, useAuthStore } from "@multica/core/auth"; import { workspaceKeys } from "@multica/core/workspace/queries"; import { paths, resolvePostAuthDestination } from "@multica/core/paths"; 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 [error, setError] = useState(""); const [desktopToken, setDesktopToken] = useState(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 stateParts = state.split(","); const isDesktop = stateParts.includes("platform:desktop"); const nextPart = stateParts.find((p) => p.startsWith("next:")); // Strip "next:" prefix, then drop anything that isn't a safe relative path // so an attacker-controlled `state=next:https://evil` cannot redirect here. const nextUrl = sanitizeNextUrl(nextPart ? nextPart.slice(5) : null); 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 (loggedInUser) => { const wsList = await api.listWorkspaces(); qc.setQueryData(workspaceKeys.list(), wsList); const onboarded = loggedInUser.onboarded_at != null; // 1. nextUrl wins: a `next=/invite/` always survives the OAuth // round-trip — the user clicked a specific link and we should // honor exactly that destination. if (nextUrl) { router.push(nextUrl); return; } // 2. Un-onboarded users may have pending invitations on their // email even when no `next=` was carried (came from a fresh // login on app.multica.ai instead of clicking the email link, // or `state` was lost across the round-trip). Look them up by // email and route to the batch /invitations page if any. // Already-onboarded users skip this lookup — their new invites // surface in the sidebar dropdown, not as a forced wall. if (!onboarded) { try { const invites = await api.listMyInvitations(); if (invites.length > 0) { qc.setQueryData(workspaceKeys.myInvitations(), invites); router.push(paths.invitations()); return; } } catch { // Network blip on the invite lookup is non-fatal — fall through // to the normal post-auth destination so the user isn't stuck // on a blank callback screen. Worst case they land on // /onboarding and the sidebar will surface invites later. } } // 3. Default: hand off to the resolver (onboarding for first-timers, // first workspace for returning users, /workspaces/new for // onboarded users with zero workspaces). Source-attribution // backfill for onboarded users with no recorded source is // handled by `` inside the dashboard // shell — not a route detour, so we route straight to dest. router.push(resolvePostAuthDestination(wsList, onboarded)); }) .catch((err) => { setError(err instanceof Error ? err.message : "Login failed"); }); } }, [searchParams, loginWithGoogle, router, qc]); if (desktopToken) { return (
Opening Multica You should see a prompt to open the Multica desktop app. If nothing happens, click the button below.
); } if (error) { return (
Login Failed {error} Back to login
); } return (
Signing in... Please wait while we complete your login
); } export default function CallbackPage() { return ( ); }