Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions app/setup/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { redirect } from "next/navigation";
import SetupEntry from "@/components/Onboarding/SetupEntry";

/**
* `/setup` — the welcome email's "Confirm your roster" CTA target. Sends the
* user straight into the interactive setup flow at the first step (roster).
* `/setup` — the welcome email's "Confirm your roster" CTA target, and the
* route the authenticated home forwards an incomplete account to. Opens the
* sequence at the account's DERIVED step rather than assuming step 1
* (chat#1889); the derived-step resolution needs client state, so the page
* mounts `SetupEntry`.
*/
export default function SetupPage() {
redirect("/setup/artists");
return <SetupEntry />;
}
32 changes: 16 additions & 16 deletions components/Home/HomePage.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
"use client";

import { useMiniKit } from "@coinbase/onchainkit/minikit";
import { useRouter } from "next/navigation";
import NewChatBootstrap from "../VercelChat/NewChatBootstrap";
import OnboardingChecklist from "@/components/Onboarding/OnboardingChecklist";
import OnboardingSequence from "@/components/Onboarding/OnboardingSequence";
import { useOnboardingGate } from "@/hooks/useOnboardingGate";
import { useEffect } from "react";
import { UIMessage } from "ai";

const HomePage = ({ initialMessages }: { initialMessages?: UIMessage[] }) => {
const { setFrameReady, isFrameReady } = useMiniKit();
const router = useRouter();
const onboarding = useOnboardingGate();

useEffect(() => {
Expand All @@ -18,21 +19,20 @@ const HomePage = ({ initialMessages }: { initialMessages?: UIMessage[] }) => {
}
}, [setFrameReady, isFrameReady]);

// Incomplete accounts resume the sequence at their derived step on every
// landing; skip drops to the app with the checklist pinned as a persistent
// reminder; activated accounts only ever see the normal home
// (recoupable/chat#1867).
if (onboarding.view === "sequence" && onboarding.step !== "complete") {
return (
<div className="flex flex-col size-full items-center">
<OnboardingSequence
step={onboarding.step}
checkpoints={onboarding.checkpoints}
onSkip={onboarding.skip}
/>
</div>
);
}
// One canonical onboarding surface (chat#1889): an incomplete account is
// forwarded into `/setup`, which opens at its derived step. Home used to host
// a second, card-based sequence that only linked out to the generic app pages,
// so a direct signup never saw the interactive steps the welcome email's
// `/setup/*` links reach. Skip still drops to the app with the checklist
// pinned (the gate stays soft — see SetupSkipLink).
const shouldEnterSetup =
onboarding.view === "sequence" && onboarding.step !== "complete";

useEffect(() => {
if (shouldEnterSetup) {
router.replace("/setup");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Incomplete accounts still mount the home chat while this redirect is pending, causing a visible home/chat flash and an unnecessary chat-session provisioning request before the canonical setup flow loads. A loading/empty handoff should be rendered instead of NewChatBootstrap whenever the account is being redirected to /setup.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At components/Home/HomePage.tsx, line 33:

<comment>Incomplete accounts still mount the home chat while this redirect is pending, causing a visible home/chat flash and an unnecessary chat-session provisioning request before the canonical setup flow loads. A loading/empty handoff should be rendered instead of `NewChatBootstrap` whenever the account is being redirected to `/setup`.</comment>

<file context>
@@ -18,21 +19,20 @@ const HomePage = ({ initialMessages }: { initialMessages?: UIMessage[] }) => {
+
+  useEffect(() => {
+    if (shouldEnterSetup) {
+      router.replace("/setup");
+    }
+  }, [shouldEnterSetup, router]);
</file context>

}
}, [shouldEnterSetup, router]);

return (
<div className="relative flex flex-col size-full items-center">
Expand Down
55 changes: 28 additions & 27 deletions components/Home/__tests__/HomePage.onboarding.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import { fireEvent, render, screen } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import HomePage from "@/components/Home/HomePage";

const replace = vi.fn();
const SKIP_KEY = "recoup-onboarding-skipped:acct-test";

vi.mock("@coinbase/onchainkit/minikit", () => ({
useMiniKit: () => ({ setFrameReady: vi.fn(), isFrameReady: true }),
}));
Expand All @@ -12,16 +15,8 @@ vi.mock("@/components/VercelChat/NewChatBootstrap", () => ({
default: () => <div data-testid="chat-bootstrap" />,
}));

vi.mock("next/link", () => ({
default: ({
href,
children,
...rest
}: React.ComponentProps<"a"> & { href: string }) => (
<a href={href} {...rest}>
{children}
</a>
),
vi.mock("next/navigation", () => ({
useRouter: () => ({ replace, push: vi.fn() }),
}));

vi.mock("@/providers/UserProvder", () => ({
Expand All @@ -41,37 +36,45 @@ vi.mock("@/hooks/useOnboardingState", () => ({
}),
}));

describe("HomePage onboarding gate (single source of truth)", () => {
describe("HomePage onboarding gate — one canonical surface", () => {
beforeEach(() => {
window.sessionStorage.clear();
replace.mockClear();
});

it("forwards an incomplete account into /setup instead of hosting its own sequence", () => {
render(<HomePage />);
expect(replace).toHaveBeenCalledWith("/setup");
});

it("renders the sequence with the step card as an h3 under the container h2", () => {
it("renders no competing step surface of its own", () => {
render(<HomePage />);
// The card-based sequence (OnboardingSequence + OnboardingStepCard) is gone:
// it only linked out to the generic app pages, so a direct signup never
// reached the interactive steps the welcome email's /setup/* links do.
expect(
screen.getByRole("heading", {
level: 2,
screen.queryByRole("heading", {
name: /finish setting up your account/i,
}),
).toBeDefined();
).toBeNull();
expect(
screen.getByRole("heading", { level: 3, name: /confirm your artists/i }),
).toBeDefined();
expect(screen.queryAllByRole("heading", { level: 1 })).toHaveLength(0);
screen.queryByRole("heading", { name: /confirm your artists/i }),
).toBeNull();
});

it("skip drops to the app with the pinned checklist", () => {
it("skip drops to the app with the pinned checklist and does not forward", () => {
window.sessionStorage.setItem(SKIP_KEY, "1");
render(<HomePage />);
fireEvent.click(screen.getByRole("button", { name: /skip for now/i }));
expect(replace).not.toHaveBeenCalled();
expect(screen.getByTestId("chat-bootstrap")).toBeDefined();
expect(
screen.getByRole("complementary", { name: /onboarding checklist/i }),
).toBeDefined();
});

it("positions the checklist inside the content area, not fixed to the viewport under the right rail", () => {
window.sessionStorage.setItem(SKIP_KEY, "1");
render(<HomePage />);
fireEvent.click(screen.getByRole("button", { name: /skip for now/i }));
const checklist = screen.getByRole("complementary", {
name: /onboarding checklist/i,
});
Expand All @@ -83,17 +86,15 @@ describe("HomePage onboarding gate (single source of truth)", () => {
});

it("has no dismiss control — the checklist is a persistent reminder", () => {
window.sessionStorage.setItem(SKIP_KEY, "1");
render(<HomePage />);
fireEvent.click(screen.getByRole("button", { name: /skip for now/i }));
expect(screen.queryByRole("button", { name: /dismiss/i })).toBeNull();
});

it("the Continue button re-opens the sequence", () => {
it("the Continue button re-arms the gate, forwarding back into /setup", () => {
window.sessionStorage.setItem(SKIP_KEY, "1");
render(<HomePage />);
fireEvent.click(screen.getByRole("button", { name: /skip for now/i }));
fireEvent.click(screen.getByRole("button", { name: /continue/i }));
expect(
screen.getByRole("heading", { level: 3, name: /confirm your artists/i }),
).toBeDefined();
expect(replace).toHaveBeenCalledWith("/setup");
});
});
14 changes: 9 additions & 5 deletions components/Onboarding/FirstTaskStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@ import useCatalogs from "@/hooks/useCatalogs";
import { useNewChatBootstrap } from "@/hooks/useNewChatBootstrap";
import { buildFirstTaskPrompt } from "@/lib/onboarding/buildFirstTaskPrompt";
import FirstTaskReportRun from "./FirstTaskReportRun";
import SetupSkipLink from "./SetupSkipLink";

/**
* Onboarding sequence final step (chat#1867): pre-run the first weekly
* catalog report, show it finished, then ask "get this every Monday?".
* Self-contained — provisions its own chat session (same infrastructure
* a normal send uses) so it can slot into the OnboardingSequence
* container later without depending on it.
* Final step of the canonical `/setup/*` sequence (chat#1867, chat#1889),
* mounted by `/setup/tasks`: pre-run the first weekly catalog report, show it
* finished, then ask "get this every Monday?". Self-contained — provisions its
* own chat session (the same infrastructure a normal send uses).
*
* Carries the gate's "skip for now" escape hatch, since home forwards
* incomplete accounts into the sequence.
*/
const FirstTaskStep = () => {
const { selectedArtist, isLoading: isArtistsLoading } = useArtistProvider();
Expand Down Expand Up @@ -84,6 +87,7 @@ const FirstTaskStep = () => {
catalogName={catalogName}
/>
)}
<SetupSkipLink />
</div>
);
};
Expand Down
10 changes: 6 additions & 4 deletions components/Onboarding/OnboardingCheckpointList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@

import { Check } from "lucide-react";
import { cn } from "@/lib/utils";
import { getOnboardingStepContent } from "@/lib/onboarding/getOnboardingStepContent";
import { getOnboardingStepTitle } from "@/lib/onboarding/getOnboardingStepTitle";
import type { OnboardingCheckpoint } from "@/lib/onboarding/types";

/**
* Read-only list of activation checkpoints with completion state. Shared
* by the onboarding sequence (progress) and the pinned checklist.
* Read-only list of activation checkpoints with completion state, used by the
* pinned checklist. Titles come from `getOnboardingStepTitle` — the placeholder
* step-card content it used to read is gone (chat#1889), since each step now
* renders its own real heading inside `/setup/*`.
*/
const OnboardingCheckpointList = ({
checkpoints,
Expand Down Expand Up @@ -35,7 +37,7 @@ const OnboardingCheckpointList = ({
: "text-foreground",
)}
>
{getOnboardingStepContent(checkpoint.id).title}
{getOnboardingStepTitle(checkpoint.id)}
</span>
<span className="sr-only">
{checkpoint.complete ? "(complete)" : "(incomplete)"}
Expand Down
51 changes: 0 additions & 51 deletions components/Onboarding/OnboardingSequence.tsx

This file was deleted.

39 changes: 0 additions & 39 deletions components/Onboarding/OnboardingStepCard.tsx

This file was deleted.

18 changes: 10 additions & 8 deletions components/Onboarding/RosterSocialsFlow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,20 @@
import { useState } from "react";
import ConfirmRosterStep from "./ConfirmRosterStep";
import RosterVerifiedPanel from "./RosterVerifiedPanel";
import SetupSkipLink from "./SetupSkipLink";
import VerifySocialsStep from "./VerifySocialsStep";

type FlowStep = "roster" | "socials" | "done";

/**
* Standalone container for the roster + socials onboarding steps, mounted at
* `/setup/artists` and `/setup/socials`. The sibling onboarding-sequence
* router (chat#1867) mounts `ConfirmRosterStep` and `VerifySocialsStep`
* directly with its own state-derived stepping.
* Container for the roster + socials steps of the canonical `/setup/*`
* sequence (chat#1889), mounted by `/setup/artists` and `/setup/socials`.
*
* `initialStep` lets a deep link open the flow directly at a given step (e.g.
* the welcome email's "verify socials" link → `/setup/socials`); it defaults to
* "roster" so existing mounts are unchanged.
* the welcome email's "verify socials" link → `/setup/socials`).
*
* Carries the gate's "skip for now" escape hatch: home forwards incomplete
* accounts here, so without it the soft gate would become a wall.
*/
const RosterSocialsFlow = ({
initialStep = "roster",
Expand All @@ -25,9 +26,9 @@ const RosterSocialsFlow = ({
const [step, setStep] = useState<FlowStep>(initialStep);

return (
<div className="w-full max-w-xl mx-auto grow py-8 px-6">
<div className="w-full max-w-xl mx-auto grow py-8 px-6 flex flex-col gap-6">
{step !== "done" && (
<p className="text-xs text-muted-foreground mb-6">
<p className="text-xs text-muted-foreground">
Step {step === "roster" ? "1" : "2"} of 2
</p>
)}
Expand All @@ -38,6 +39,7 @@ const RosterSocialsFlow = ({
<VerifySocialsStep onConfirmed={() => setStep("done")} />
)}
{step === "done" && <RosterVerifiedPanel />}
{step !== "done" && <SetupSkipLink />}
</div>
);
};
Expand Down
Loading
Loading