Files
multica/packages/views/onboarding/steps/step-first-issue.tsx
Jiayuan Zhang 591e47842d refactor(onboarding): remove starter-content kit; unify install-runtime issue across mark-onboarded paths (MUL-2438) (#2884)
* refactor(onboarding): remove starter-content kit, unify install-runtime issue across mark-onboarded paths (MUL-2438)

Drops the post-onboarding ImportStarterContent / DismissStarterContent
flow (handler + routes + StarterContentPrompt + templates + locale
strings + analytics event). The bug — web onboarding seeding 6+ starter
issues without a runtime — only existed through that path; with it gone
the source disappears.

The "install a runtime" issue from BootstrapOnboardingNoRuntime is now
the canonical no-runtime onboarding seed. The title/description and a
LockAndFindActiveDuplicate-deduped seeder move to
handler/no_runtime_issue.go, and CompleteOnboarding / CreateWorkspace /
AcceptInvitation seed it whenever the workspace has no runtime yet, so
every mark-onboarded entry point lands the user on a concrete next
step.

starter_content_state column is kept and continues to be claimed as
'imported' in all five entry points so older desktop builds (which
still render the legacy dialog on NULL) don't surface it to accounts
created after this change.

Co-authored-by: multica-agent <github@multica.ai>

* fix(onboarding): backfill starter_content_state for in-window NULL users (MUL-2438)

054 only covered pre-feature users. Anyone onboarded between then and the
starter-content kit removal could still sit at NULL, and old desktop
clients gate the legacy StarterContentPrompt on `starter_content_state
IS NULL`. The import/dismiss routes are gone, so leaving these rows NULL
would surface a dialog whose buttons 404. Mark them 'imported' to match
the new helper's claim semantics.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-05-19 18:37:48 +02:00

127 lines
4.2 KiB
TypeScript

"use client";
import { useEffect, useRef, useState } from "react";
import { Loader2, AlertCircle } from "lucide-react";
import { toast } from "sonner";
import { Button } from "@multica/ui/components/ui/button";
import {
completeOnboarding,
type OnboardingCompletionPath,
} from "@multica/core/onboarding";
import { useT } from "../../i18n";
/**
* Step 5 — the final onboarding beat.
*
* Runtime-skipped finalizer. The runtime-connected path now bootstraps one
* default assistant plus one onboarding issue server-side and routes there
* directly. This step remains for users who skip runtime connection: it only
* flips `onboarded_at` and lands them in the workspace.
* Two consequences of that move:
*
* 1. This step can't fail in user-visible ways any more. `completeOnboarding`
* is one PATCH to `/api/me`; the only failure mode is a network error,
* which we surface as a toast + Retry, not a full error screen.
* 2. The sub-issue "Unknown" assignee race is gone for free — by the time
* the import runs, the user has already landed in the workspace, so
* `listMembers` has resolved and the current user's member_id is in
* the query cache.
*/
export function StepFirstIssue({
onFinished,
completionPath,
workspaceId,
}: {
/** Called after `onboarded_at` is set server-side. Parent handles
* navigation to the workspace landing page. */
onFinished: () => void;
/** Which exit label the server should record on `onboarding_completed`.
* Computed in the parent shell where runtime + waitlist state are
* both in scope. */
completionPath: OnboardingCompletionPath;
workspaceId?: string;
}) {
const { t } = useT("onboarding");
const [error, setError] = useState<string | null>(null);
const [retrying, setRetrying] = useState(false);
const started = useRef(false);
const onFinishedRef = useRef(onFinished);
onFinishedRef.current = onFinished;
const completionPathRef = useRef(completionPath);
completionPathRef.current = completionPath;
const workspaceIdRef = useRef(workspaceId);
workspaceIdRef.current = workspaceId;
useEffect(() => {
if (started.current) return;
started.current = true;
(async () => {
try {
await completeOnboarding(
completionPathRef.current,
workspaceIdRef.current,
);
onFinishedRef.current();
} catch (err) {
setError(
err instanceof Error ? err.message : t(($) => $.errors.skip_failed),
);
}
})();
}, [t]);
const retry = async () => {
if (retrying) return;
setRetrying(true);
setError(null);
try {
await completeOnboarding(
completionPathRef.current,
workspaceIdRef.current,
);
onFinishedRef.current();
} catch (err) {
const msg =
err instanceof Error ? err.message : t(($) => $.first_issue.retry_failed);
setError(msg);
toast.error(msg);
} finally {
setRetrying(false);
}
};
if (error) {
return (
<div className="animate-onboarding-enter flex w-full flex-col items-center gap-6 text-center">
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-destructive/10 text-destructive">
<AlertCircle className="h-6 w-6" />
</div>
<div className="flex flex-col gap-2">
<h1 className="text-2xl font-semibold tracking-tight">
{t(($) => $.first_issue.error_title)}
</h1>
<p className="text-sm text-muted-foreground">{error}</p>
</div>
<Button onClick={retry} disabled={retrying}>
{retrying && <Loader2 className="h-4 w-4 animate-spin" />}
{t(($) => $.first_issue.retry)}
</Button>
</div>
);
}
return (
<div className="animate-onboarding-enter flex w-full flex-col items-center gap-6 text-center">
<Loader2 className="h-10 w-10 animate-spin text-primary" />
<div className="flex flex-col gap-2">
<h1 className="text-2xl font-semibold tracking-tight">
{t(($) => $.first_issue.finishing)}
</h1>
<p className="text-sm text-muted-foreground">
{t(($) => $.first_issue.opening)}
</p>
</div>
</div>
);
}