Retry signup tests with Stripe CLI

This commit is contained in:
Arthur Belleville 2026-03-15 19:09:35 +01:00
parent 3d4297f330
commit c481f2c577
No known key found for this signature in database
19 changed files with 1280 additions and 40 deletions

View file

@ -0,0 +1 @@
12949

View file

@ -0,0 +1 @@
{"reason":"idle timeout","timestamp":1773483820241}

View file

@ -0,0 +1 @@
14566

View file

@ -0,0 +1,56 @@
<h2>Tablo Details — No-Code Overview Builder (V1)</h2>
<p class="subtitle">
Proposed interaction model: same page, toggle edit mode, drag blocks inside fixed zones.
</p>
<div class="section">
<div class="options">
<div class="option selected" data-choice="ux-flow" onclick="toggleSelect(this)">
<div class="letter">A</div>
<div class="content">
<h3>View/Edit Toggle + Auto-Save</h3>
<p>
"Edit layout" appears for allowed users. Drag-and-drop saves instantly after each drop.
"Reset default" restores the initial layout.
</p>
</div>
</div>
</div>
</div>
<div class="split">
<div class="mockup">
<div class="mockup-header">Normal view (for all members)</div>
<div class="mockup-body">
<div class="mock-nav">Header + metadata + tabs</div>
<div style="display: grid; grid-template-columns: 2fr 1fr; gap: 12px; margin-top: 12px;">
<div class="placeholder" style="min-height: 50px;">Left Zone: Description</div>
<div class="placeholder" style="min-height: 50px;">Right Zone: Files</div>
<div class="placeholder" style="min-height: 50px;">Left Zone: Mes taches</div>
<div class="placeholder" style="min-height: 50px;">Right Zone: Informations</div>
</div>
</div>
</div>
<div class="mockup">
<div class="mockup-header">Edit mode (admins + org members)</div>
<div class="mockup-body">
<div class="mock-nav">Header + metadata + tabs + "Done editing"</div>
<div style="display: grid; grid-template-columns: 2fr 1fr; gap: 12px; margin-top: 12px;">
<div class="placeholder" style="min-height: 50px;">
Left Zone
<div style="font-size: 12px; margin-top: 6px;">[::] Description</div>
<div style="font-size: 12px; margin-top: 4px;">[::] Mes taches</div>
</div>
<div class="placeholder" style="min-height: 50px;">
Right Zone
<div style="font-size: 12px; margin-top: 6px;">[::] Fichiers</div>
<div style="font-size: 12px; margin-top: 4px;">[::] Informations</div>
</div>
</div>
<div style="margin-top: 10px;">
<button class="mock-button">Reset default</button>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,3 @@
<div style="display:flex;align-items:center;justify-content:center;min-height:60vh">
<p class="subtitle">Continuing in terminal...</p>
</div>

View file

@ -3,7 +3,7 @@ import { ThemeProvider } from "@xtablo/shared/contexts/ThemeContext";
import { Toaster } from "@xtablo/ui/components/sonner";
import { BrowserRouter as Router, useRoutes } from "react-router-dom";
import { CookieBanner } from "./components/CookieBanner";
import { PendingFounderCheckout } from "./components/PendingFounderCheckout";
import { PendingSignupCheckout } from "./components/PendingSignupCheckout";
import { TrialUpsellModal } from "./components/TrialUpsellModal";
import { UpgradePanel } from "./components/UpgradePanel";
import { UpgradeBlockProvider } from "./contexts/UpgradeBlockContext";
@ -30,7 +30,7 @@ const Routes = () => {
return (
<UserStoreProvider>
<UpgradeBlockProvider>
<PendingFounderCheckout />
<PendingSignupCheckout />
<UpgradePanel />
<TrialUpsellModal />
{appElement}

View file

@ -0,0 +1,118 @@
import { render, waitFor } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { PendingSignupCheckout } from "./PendingSignupCheckout";
const mockCreateCheckout = vi.fn();
vi.mock("../hooks/organization", () => ({
useOrganization: vi.fn(),
}));
vi.mock("../hooks/stripe", () => ({
useCreateCheckoutSession: () => ({
mutate: mockCreateCheckout,
}),
}));
vi.mock("../providers/UserStoreProvider", () => ({
useMaybeUser: vi.fn(),
}));
describe("PendingSignupCheckout", () => {
beforeEach(() => {
vi.clearAllMocks();
localStorage.clear();
});
it("creates checkout for pending solo", async () => {
const { useOrganization } = await import("../hooks/organization");
const { useMaybeUser } = await import("../providers/UserStoreProvider");
vi.mocked(useMaybeUser).mockReturnValue({ id: "user-1" } as never);
vi.mocked(useOrganization).mockReturnValue({
isLoading: false,
data: {
active_subscription_plan: null,
is_billing_owner: true,
},
} as never);
localStorage.setItem("pendingBillingCheckoutPlan", "solo");
render(<PendingSignupCheckout />);
await waitFor(() => {
expect(mockCreateCheckout).toHaveBeenCalledWith(
expect.objectContaining({ plan: "solo" }),
expect.any(Object)
);
});
});
it("creates checkout for pending team", async () => {
const { useOrganization } = await import("../hooks/organization");
const { useMaybeUser } = await import("../providers/UserStoreProvider");
vi.mocked(useMaybeUser).mockReturnValue({ id: "user-1" } as never);
vi.mocked(useOrganization).mockReturnValue({
isLoading: false,
data: {
active_subscription_plan: null,
is_billing_owner: true,
},
} as never);
localStorage.setItem("pendingBillingCheckoutPlan", "team");
render(<PendingSignupCheckout />);
await waitFor(() => {
expect(mockCreateCheckout).toHaveBeenCalledWith(
expect.objectContaining({ plan: "team" }),
expect.any(Object)
);
});
});
it("clears founder pending checkout when annual plan is already active", async () => {
const { useOrganization } = await import("../hooks/organization");
const { useMaybeUser } = await import("../providers/UserStoreProvider");
vi.mocked(useMaybeUser).mockReturnValue({ id: "user-1" } as never);
vi.mocked(useOrganization).mockReturnValue({
isLoading: false,
data: {
active_subscription_plan: "annual",
is_billing_owner: true,
},
} as never);
localStorage.setItem("pendingBillingCheckoutPlan", "founder");
render(<PendingSignupCheckout />);
await waitFor(() => {
expect(localStorage.getItem("pendingBillingCheckoutPlan")).toBeNull();
expect(mockCreateCheckout).not.toHaveBeenCalled();
});
});
it("clears invalid pending checkout value", async () => {
const { useOrganization } = await import("../hooks/organization");
const { useMaybeUser } = await import("../providers/UserStoreProvider");
vi.mocked(useMaybeUser).mockReturnValue({ id: "user-1" } as never);
vi.mocked(useOrganization).mockReturnValue({
isLoading: false,
data: {
active_subscription_plan: null,
is_billing_owner: true,
},
} as never);
localStorage.setItem("pendingBillingCheckoutPlan", "invalid");
render(<PendingSignupCheckout />);
await waitFor(() => {
expect(localStorage.getItem("pendingBillingCheckoutPlan")).toBeNull();
expect(mockCreateCheckout).not.toHaveBeenCalled();
});
});
});

View file

@ -1,10 +1,14 @@
import { useEffect, useRef } from "react";
import { useOrganization } from "../hooks/organization";
import { useCreateCheckoutSession } from "../hooks/stripe";
import { PENDING_BILLING_CHECKOUT_PLAN_KEY } from "../lib/billing";
import {
PENDING_BILLING_CHECKOUT_PLAN_KEY,
isSignupBillingIntent,
satisfiesPendingCheckoutPlan,
} from "../lib/billing";
import { useMaybeUser } from "../providers/UserStoreProvider";
export function PendingFounderCheckout() {
export function PendingSignupCheckout() {
const user = useMaybeUser();
const { data: organizationData, isLoading } = useOrganization();
const { mutate: createCheckout } = useCreateCheckoutSession();
@ -14,12 +18,17 @@ export function PendingFounderCheckout() {
if (hasTriggered.current) return;
if (!user || isLoading || !organizationData) return;
const pendingCheckout = localStorage.getItem(PENDING_BILLING_CHECKOUT_PLAN_KEY);
if (pendingCheckout !== "founder") {
const pendingCheckoutValue = localStorage.getItem(PENDING_BILLING_CHECKOUT_PLAN_KEY);
if (!isSignupBillingIntent(pendingCheckoutValue)) {
if (pendingCheckoutValue) {
localStorage.removeItem(PENDING_BILLING_CHECKOUT_PLAN_KEY);
}
return;
}
if (organizationData.active_subscription_plan === "annual") {
if (
satisfiesPendingCheckoutPlan(pendingCheckoutValue, organizationData.active_subscription_plan)
) {
localStorage.removeItem(PENDING_BILLING_CHECKOUT_PLAN_KEY);
return;
}
@ -32,13 +41,13 @@ export function PendingFounderCheckout() {
localStorage.removeItem(PENDING_BILLING_CHECKOUT_PLAN_KEY);
createCheckout(
{
plan: "founder",
plan: pendingCheckoutValue,
successUrl: `${window.location.origin}/settings?success=true`,
cancelUrl: `${window.location.origin}/settings?canceled=true`,
},
{
onError: () => {
localStorage.setItem(PENDING_BILLING_CHECKOUT_PLAN_KEY, "founder");
localStorage.setItem(PENDING_BILLING_CHECKOUT_PLAN_KEY, pendingCheckoutValue);
hasTriggered.current = false;
},
}

View file

@ -0,0 +1,16 @@
import { describe, expect, it } from "vitest";
import { resolveSignupBillingIntent } from "./auth";
describe("resolveSignupBillingIntent", () => {
it("defaults to solo when billing intent is missing", () => {
expect(resolveSignupBillingIntent()).toBe("solo");
});
it("returns team when selected", () => {
expect(resolveSignupBillingIntent("team")).toBe("team");
});
it("returns founder when selected", () => {
expect(resolveSignupBillingIntent("founder")).toBe("founder");
});
});

View file

@ -8,6 +8,7 @@ import { useNavigate } from "react-router-dom";
import { match } from "ts-pattern";
import { api } from "../lib/api";
import {
DEFAULT_SIGNUP_BILLING_INTENT,
PENDING_BILLING_CHECKOUT_PLAN_KEY,
SIGNUP_BILLING_INTENT_KEY,
SignupBillingIntent,
@ -44,6 +45,10 @@ interface AuthResponse {
session: Session | null;
}
export const resolveSignupBillingIntent = (
billingIntent?: SignupBillingIntent
): SignupBillingIntent => billingIntent ?? DEFAULT_SIGNUP_BILLING_INTENT;
export function useSignUp({ redirectUrl }: { redirectUrl: string | null }) {
const navigate = useNavigate();
const [errors, setErrors] = useState<Record<string, string>>({});
@ -63,7 +68,7 @@ export function useSignUp({ redirectUrl }: { redirectUrl: string | null }) {
first_name: data.first_name,
last_name: data.last_name,
business_name: data.business_name,
billing_intent: data.billing_intent ?? "trial",
billing_intent: resolveSignupBillingIntent(data.billing_intent),
},
},
});
@ -76,15 +81,9 @@ export function useSignUp({ redirectUrl }: { redirectUrl: string | null }) {
return response;
},
onSuccess: async (data, variables) => {
const wantsFounderCheckout = variables.billing_intent === "founder";
if (wantsFounderCheckout) {
localStorage.setItem(SIGNUP_BILLING_INTENT_KEY, "founder");
localStorage.setItem(PENDING_BILLING_CHECKOUT_PLAN_KEY, "founder");
} else {
localStorage.removeItem(SIGNUP_BILLING_INTENT_KEY);
localStorage.removeItem(PENDING_BILLING_CHECKOUT_PLAN_KEY);
}
const selectedPlan = resolveSignupBillingIntent(variables.billing_intent);
localStorage.setItem(SIGNUP_BILLING_INTENT_KEY, selectedPlan);
localStorage.setItem(PENDING_BILLING_CHECKOUT_PLAN_KEY, selectedPlan);
// If there's no session, it means email confirmation is required
if (!data.user?.email_confirmed_at) {
@ -94,12 +93,12 @@ export function useSignUp({ redirectUrl }: { redirectUrl: string | null }) {
return;
}
if (wantsFounderCheckout && data.session?.access_token) {
if (data.session?.access_token) {
try {
const checkoutResponse = await api.post(
"/api/v1/stripe/create-checkout-session",
{
plan: "founder",
plan: selectedPlan,
successUrl: `${window.location.origin}/settings?success=true`,
cancelUrl: `${window.location.origin}/settings?canceled=true`,
},
@ -117,9 +116,9 @@ export function useSignUp({ redirectUrl }: { redirectUrl: string | null }) {
return;
}
} catch (error) {
console.error("Founder checkout bootstrap failed after signup:", error);
console.error("Signup checkout bootstrap failed after signup:", error);
toast.add({
title: "Paiement Founder",
title: "Paiement",
description:
"Votre compte est créé, mais la redirection vers le paiement a échoué. Vous pourrez finaliser le paiement dans l'application.",
type: "warning",
@ -129,9 +128,6 @@ export function useSignUp({ redirectUrl }: { redirectUrl: string | null }) {
}
localStorage.removeItem(SIGNUP_BILLING_INTENT_KEY);
if (!wantsFounderCheckout) {
localStorage.removeItem(PENDING_BILLING_CHECKOUT_PLAN_KEY);
}
if (redirectUrl) {
localStorage.removeItem("redirectUrl");

View file

@ -0,0 +1,30 @@
import { describe, expect, it } from "vitest";
import {
DEFAULT_SIGNUP_BILLING_INTENT,
isSignupBillingIntent,
satisfiesPendingCheckoutPlan,
} from "./billing";
describe("billing helpers", () => {
it("defaults signup intent to solo", () => {
expect(DEFAULT_SIGNUP_BILLING_INTENT).toBe("solo");
});
it("recognizes only solo, team and founder as signup intents", () => {
expect(isSignupBillingIntent("solo")).toBe(true);
expect(isSignupBillingIntent("team")).toBe(true);
expect(isSignupBillingIntent("founder")).toBe(true);
expect(isSignupBillingIntent("trial")).toBe(false);
});
it("computes checkout satisfaction against active subscription plan", () => {
expect(satisfiesPendingCheckoutPlan("founder", "annual")).toBe(true);
expect(satisfiesPendingCheckoutPlan("founder", "team")).toBe(false);
expect(satisfiesPendingCheckoutPlan("team", "team")).toBe(true);
expect(satisfiesPendingCheckoutPlan("team", "annual")).toBe(true);
expect(satisfiesPendingCheckoutPlan("solo", "solo")).toBe(true);
expect(satisfiesPendingCheckoutPlan("solo", "team")).toBe(true);
expect(satisfiesPendingCheckoutPlan("solo", "annual")).toBe(true);
expect(satisfiesPendingCheckoutPlan("solo", null)).toBe(false);
});
});

View file

@ -1,4 +1,28 @@
export type SignupBillingIntent = "trial" | "founder";
export type SignupBillingIntent = "solo" | "team" | "founder";
export const DEFAULT_SIGNUP_BILLING_INTENT: SignupBillingIntent = "solo";
export const SIGNUP_BILLING_INTENT_KEY = "signupBillingIntent";
export const PENDING_BILLING_CHECKOUT_PLAN_KEY = "pendingBillingCheckoutPlan";
export const isSignupBillingIntent = (value: string | null): value is SignupBillingIntent =>
value === "solo" || value === "team" || value === "founder";
export const satisfiesPendingCheckoutPlan = (
pendingPlan: SignupBillingIntent,
activePlan: "solo" | "team" | "annual" | null
): boolean => {
if (!activePlan) {
return false;
}
if (pendingPlan === "founder") {
return activePlan === "annual";
}
if (pendingPlan === "team") {
return activePlan === "team" || activePlan === "annual";
}
return activePlan === "solo" || activePlan === "team" || activePlan === "annual";
};

View file

@ -0,0 +1,81 @@
import { fireEvent, screen, waitFor } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "../utils/testHelpers";
import { SignUpV2Page } from "./signup-v2";
const mockSignUp = vi.fn();
vi.mock("../hooks/auth", () => ({
useSignUp: () => ({
mutate: mockSignUp,
isPending: false,
}),
useLoginEmail: () => ({
mutate: vi.fn(),
isPending: false,
}),
useLoginGoogle: () => ({
mutate: vi.fn(),
}),
}));
describe("SignUpV2Page", () => {
beforeEach(() => {
vi.clearAllMocks();
localStorage.clear();
});
const fillAndSubmitForm = () => {
fireEvent.change(screen.getByPlaceholderText(/your first name/i), {
target: { value: "John" },
});
fireEvent.change(screen.getByPlaceholderText(/your last name/i), {
target: { value: "Doe" },
});
fireEvent.change(screen.getByPlaceholderText(/your email/i), {
target: { value: "john@example.com" },
});
fireEvent.change(screen.getAllByPlaceholderText(/your password/i)[0], {
target: { value: "password123" },
});
fireEvent.change(screen.getByPlaceholderText(/confirm your password/i), {
target: { value: "password123" },
});
fireEvent.click(screen.getByRole("checkbox", { name: /i accept/i }));
fireEvent.click(screen.getByRole("button", { name: /create my account/i }));
};
it("submits solo billing intent by default", async () => {
renderWithProviders(<SignUpV2Page />);
fillAndSubmitForm();
await waitFor(() => {
expect(mockSignUp).toHaveBeenCalledWith(expect.objectContaining({ billing_intent: "solo" }));
});
});
it("submits team billing intent when selected", async () => {
renderWithProviders(<SignUpV2Page />);
fireEvent.click(screen.getByRole("button", { name: /team/i }));
fillAndSubmitForm();
await waitFor(() => {
expect(mockSignUp).toHaveBeenCalledWith(expect.objectContaining({ billing_intent: "team" }));
});
});
it("submits founder billing intent when selected", async () => {
renderWithProviders(<SignUpV2Page />);
fireEvent.click(screen.getByRole("button", { name: /founder/i }));
fillAndSubmitForm();
await waitFor(() => {
expect(mockSignUp).toHaveBeenCalledWith(
expect.objectContaining({ billing_intent: "founder" })
);
});
});
});

View file

@ -10,7 +10,7 @@ import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { useSignUp } from "../hooks/auth";
import type { SignupBillingIntent } from "../lib/billing";
import { DEFAULT_SIGNUP_BILLING_INTENT, type SignupBillingIntent } from "../lib/billing";
export function SignUpV2Page() {
const { t } = useTranslation(["auth", "common"]);
@ -30,7 +30,9 @@ export function SignUpV2Page() {
business_name: "",
});
const [termsAccepted, setTermsAccepted] = useState(false);
const [billingIntent, setBillingIntent] = useState<SignupBillingIntent>("trial");
const [billingIntent, setBillingIntent] = useState<SignupBillingIntent>(
DEFAULT_SIGNUP_BILLING_INTENT
);
const { theme, setTheme } = useTheme();
@ -241,16 +243,30 @@ export function SignUpV2Page() {
<div className="grid gap-2">
<button
type="button"
onClick={() => setBillingIntent("trial")}
onClick={() => setBillingIntent("solo")}
className={`w-full rounded-md border p-3 text-left transition-colors ${
billingIntent === "trial"
billingIntent === "solo"
? "border-primary bg-primary/10"
: "border-border hover:bg-muted/40"
}`}
>
<p className="text-sm font-medium">Trial (14 jours)</p>
<p className="text-sm font-medium">Solo (9.99/mois)</p>
<p className="text-xs text-muted-foreground">
Plan Solo ou Teams après la période d'essai.
Pour les indépendants et petites équipes.
</p>
</button>
<button
type="button"
onClick={() => setBillingIntent("team")}
className={`w-full rounded-md border p-3 text-left transition-colors ${
billingIntent === "team"
? "border-primary bg-primary/10"
: "border-border hover:bg-muted/40"
}`}
>
<p className="text-sm font-medium">Team (29/mois)</p>
<p className="text-xs text-muted-foreground">
Collaboration avancée pour votre organisation.
</p>
</button>
<button

View file

@ -193,11 +193,42 @@ describe("SignUpPage", () => {
last_name: "Doe",
confirm_password: "password123",
business_name: "",
billing_intent: "trial",
billing_intent: "solo",
});
});
});
it("submits team billing intent when selected", async () => {
renderWithProviders(<SignUpPage />);
fireEvent.click(screen.getByRole("button", { name: /team/i }));
const submitButton = screen.getByRole("button", { name: /create my account/i });
const termsCheckbox = screen.getByRole("checkbox", { name: /i accept/i });
fireEvent.change(screen.getByPlaceholderText(/your first name/i), {
target: { value: "John" },
});
fireEvent.change(screen.getByPlaceholderText(/your last name/i), {
target: { value: "Doe" },
});
fireEvent.change(screen.getByPlaceholderText(/your email/i), {
target: { value: "john@example.com" },
});
fireEvent.change(screen.getAllByPlaceholderText(/your password/i)[0], {
target: { value: "password123" },
});
fireEvent.change(screen.getByPlaceholderText(/confirm your password/i), {
target: { value: "password123" },
});
fireEvent.click(termsCheckbox);
fireEvent.click(submitButton);
await waitFor(() => {
expect(mockSignUp).toHaveBeenCalledWith(expect.objectContaining({ billing_intent: "team" }));
});
});
it("submits founder billing intent when selected", async () => {
renderWithProviders(<SignUpPage />);

View file

@ -11,7 +11,7 @@ import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { twMerge } from "tailwind-merge";
import { useSignUp } from "../hooks/auth";
import type { SignupBillingIntent } from "../lib/billing";
import { DEFAULT_SIGNUP_BILLING_INTENT, type SignupBillingIntent } from "../lib/billing";
export function SignUpPage() {
const { t } = useTranslation(["auth", "common"]);
@ -31,7 +31,9 @@ export function SignUpPage() {
business_name: "",
});
const [termsAccepted, setTermsAccepted] = useState(false);
const [billingIntent, setBillingIntent] = useState<SignupBillingIntent>("trial");
const [billingIntent, setBillingIntent] = useState<SignupBillingIntent>(
DEFAULT_SIGNUP_BILLING_INTENT
);
// 3D Parallax effect
const cardRef = useRef<HTMLDivElement>(null);
@ -327,17 +329,32 @@ export function SignUpPage() {
<div className="grid gap-2">
<button
type="button"
onClick={() => setBillingIntent("trial")}
onClick={() => setBillingIntent("solo")}
className={twMerge(
"w-full rounded-md border p-3 text-left transition-colors",
billingIntent === "trial"
billingIntent === "solo"
? "border-primary bg-primary/10"
: "border-border hover:bg-muted/40"
)}
>
<p className="text-sm font-medium">Trial (14 jours)</p>
<p className="text-sm font-medium">Solo (9.99/mois)</p>
<p className="text-xs text-muted-foreground">
Plan Solo ou Teams après la période d'essai.
Pour les indépendants et petites équipes.
</p>
</button>
<button
type="button"
onClick={() => setBillingIntent("team")}
className={twMerge(
"w-full rounded-md border p-3 text-left transition-colors",
billingIntent === "team"
? "border-primary bg-primary/10"
: "border-border hover:bg-muted/40"
)}
>
<p className="text-sm font-medium">Team (29/mois)</p>
<p className="text-xs text-muted-foreground">
Collaboration avancée pour votre organisation.
</p>
</button>
<button

View file

@ -0,0 +1,394 @@
# Tablo Overview No-Code Builder V1 Implementation Plan
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Let authorized users reorder the `Aperçu` blocks in `tablo-details` via drag-and-drop (fixed left/right zones), auto-save per tablo, and share the same layout with all viewers.
**Architecture:** Persist a typed `layout_overview_v1` JSON payload on `public.tablos`, then render overview blocks from a sanitized layout model instead of hardcoded JSX order. Add a small DnD interaction layer (same-zone only) with optimistic UI + rollback on save failure. Keep scope strictly to reorder existing overview blocks (`description`, `myTasks`, `files`, `info`) with a reset-to-default action.
**Tech Stack:** Supabase migrations + pgTAP, TypeScript, React 19, React Query, Vitest, Testing Library
---
## Spec Reference
- `/Users/arthur.belleville/Documents/perso/projects/xtablo-source/docs/superpowers/specs/2026-03-14-tablo-overview-nocode-builder-design.md`
## Scope Guardrails (YAGNI)
- No new widget types in V1.
- No hide/show or resize in V1.
- No cross-zone moves in V1.
- No changes to non-`Aperçu` tabs.
## File Structure (lock boundaries before coding)
### Database + shared contract
- Create: `supabase/migrations/20260314110000_add_tablo_overview_layout_v1.sql`
- Responsibility: add nullable JSONB `layout_overview_v1` column on `public.tablos`.
- Modify: `supabase/tests/database/01_schema_structure.test.sql`
- Responsibility: assert new column exists and type is `jsonb` (schema contract safety).
- Modify: `packages/shared-types/src/database.types.ts` (generated)
- Responsibility: include `layout_overview_v1` in `tablos` Row/Insert/Update types.
### Frontend layout domain + UI
- Create: `apps/main/src/pages/tablo-details/overviewLayout.ts`
- Responsibility: typed layout model, defaults, sanitizer, serialization helpers.
- Create: `apps/main/src/pages/tablo-details/overviewLayout.test.ts`
- Responsibility: unit tests for sanitize/default/recovery behavior.
- Create: `apps/main/src/pages/tablo-details/overviewReorder.ts`
- Responsibility: pure reorder helpers (same-zone only, immutable updates).
- Create: `apps/main/src/pages/tablo-details/overviewReorder.test.ts`
- Responsibility: reorder correctness and invalid-move behavior tests.
- Modify: `apps/main/src/pages/tablo-details.tsx`
- Responsibility: edit-mode UX, draggable rendering, optimistic save, rollback, reset.
- Create: `apps/main/src/pages/tablo-details.layout.test.tsx`
- Responsibility: behavior tests for default render, persisted render, save payload, rollback, reset.
### API confidence
- Modify: `apps/api/src/__tests__/routes/tablo.test.ts`
- Responsibility: ensure `PATCH /tablos/update` accepts/saves `layout_overview_v1`.
---
## Chunk 1: Data Contract and Persistence Foundation
### Task 1.1: Add DB column with schema test first
**Files:**
- Create: `supabase/migrations/20260314110000_add_tablo_overview_layout_v1.sql`
- Modify: `supabase/tests/database/01_schema_structure.test.sql`
- Test: `supabase/tests/database/01_schema_structure.test.sql`
- [ ] **Step 1: Write the failing schema test**
```sql
-- Add near other tablos column checks
SELECT has_column('public', 'tablos', 'layout_overview_v1', 'tablos should have layout_overview_v1 column');
SELECT col_type_is('public', 'tablos', 'layout_overview_v1', 'jsonb', 'tablos.layout_overview_v1 should be jsonb');
```
- [ ] **Step 2: Update pgTAP plan count**
```sql
select plan(99); -- was 97, +2 tests for new column
```
- [ ] **Step 3: Run schema test and verify it fails before migration**
Run: `supabase test db --file supabase/tests/database/01_schema_structure.test.sql`
Expected: FAIL mentioning missing `layout_overview_v1` column.
- [ ] **Step 4: Add migration**
```sql
alter table public.tablos
add column if not exists layout_overview_v1 jsonb;
comment on column public.tablos.layout_overview_v1 is
'Per-tablo overview layout config (v1) for tablo-details Aperçu drag-and-drop order.';
```
- [ ] **Step 5: Re-run schema test to verify pass**
Run: `supabase test db --file supabase/tests/database/01_schema_structure.test.sql`
Expected: PASS for new column and type checks.
- [ ] **Step 6: Commit**
```bash
git add supabase/migrations/20260314110000_add_tablo_overview_layout_v1.sql supabase/tests/database/01_schema_structure.test.sql
git commit -m "feat(db): add tablo overview layout json column"
```
### Task 1.2: Regenerate shared DB types and ensure compile contract
**Files:**
- Modify: `packages/shared-types/src/database.types.ts`
- Test: `packages/shared-types/src/database.types.ts` (type presence check via typecheck)
- [ ] **Step 1: Regenerate database types from local Supabase**
Run: `supabase gen types typescript --local > packages/shared-types/src/database.types.ts`
Expected: command completes with no stderr.
- [ ] **Step 2: Verify `layout_overview_v1` appears in `tablos` types**
Run: `rg -n "layout_overview_v1" packages/shared-types/src/database.types.ts`
Expected: matches in `Row`, `Insert`, and `Update` for `tablos`.
- [ ] **Step 3: Run focused typecheck**
Run: `pnpm --filter @xtablo/main typecheck`
Expected: PASS (or unrelated pre-existing failures only).
- [ ] **Step 4: Commit**
```bash
git add packages/shared-types/src/database.types.ts
git commit -m "chore(types): regenerate shared db types for overview layout"
```
---
## Chunk 2: Frontend Layout Model + UI (TDD-first)
### Task 2.1: Add pure layout model and sanitizer with tests
**Files:**
- Create: `apps/main/src/pages/tablo-details/overviewLayout.ts`
- Create: `apps/main/src/pages/tablo-details/overviewLayout.test.ts`
- Test: `apps/main/src/pages/tablo-details/overviewLayout.test.ts`
- [ ] **Step 1: Write failing unit tests for sanitize/default behavior**
```ts
import { describe, expect, it } from "vitest";
import { DEFAULT_OVERVIEW_LAYOUT, sanitizeOverviewLayout } from "./overviewLayout";
describe("sanitizeOverviewLayout", () => {
it("returns default when input is null", () => {
expect(sanitizeOverviewLayout(null)).toEqual(DEFAULT_OVERVIEW_LAYOUT);
});
it("drops unknown block ids and restores required ids", () => {
expect(
sanitizeOverviewLayout({
version: 1,
leftZone: ["description", "unknown"],
rightZone: [],
})
).toEqual({
version: 1,
leftZone: ["description", "myTasks"],
rightZone: ["files", "info"],
});
});
});
```
- [ ] **Step 2: Run test to confirm failure**
Run: `pnpm --filter @xtablo/main test -- src/pages/tablo-details/overviewLayout.test.ts`
Expected: FAIL because module/functions do not exist yet.
- [ ] **Step 3: Implement minimal layout model and sanitizer**
```ts
export const OVERVIEW_LEFT_BLOCKS = ["description", "myTasks"] as const;
export const OVERVIEW_RIGHT_BLOCKS = ["files", "info"] as const;
export const DEFAULT_OVERVIEW_LAYOUT = { version: 1 as const, leftZone: [...OVERVIEW_LEFT_BLOCKS], rightZone: [...OVERVIEW_RIGHT_BLOCKS] };
export function sanitizeOverviewLayout(input: unknown): OverviewLayoutV1 {
// Parse object safely, keep only allowed ids, dedupe, reinsert required ids.
}
```
- [ ] **Step 4: Re-run test to verify pass**
Run: `pnpm --filter @xtablo/main test -- src/pages/tablo-details/overviewLayout.test.ts`
Expected: PASS.
- [ ] **Step 5: Commit**
```bash
git add apps/main/src/pages/tablo-details/overviewLayout.ts apps/main/src/pages/tablo-details/overviewLayout.test.ts
git commit -m "feat(main): add overview layout schema and sanitizer"
```
### Task 2.2: Add pure reorder helpers with tests
**Files:**
- Create: `apps/main/src/pages/tablo-details/overviewReorder.ts`
- Create: `apps/main/src/pages/tablo-details/overviewReorder.test.ts`
- Test: `apps/main/src/pages/tablo-details/overviewReorder.test.ts`
- [ ] **Step 1: Write failing tests for same-zone reorder and cross-zone block**
```ts
import { describe, expect, it } from "vitest";
import { moveWithinZone } from "./overviewReorder";
it("reorders items within a zone", () => {
expect(moveWithinZone(["a", "b", "c"], 0, 2)).toEqual(["b", "c", "a"]);
});
it("returns original list on invalid indices", () => {
expect(moveWithinZone(["a"], 0, 3)).toEqual(["a"]);
});
```
- [ ] **Step 2: Run test to confirm failure**
Run: `pnpm --filter @xtablo/main test -- src/pages/tablo-details/overviewReorder.test.ts`
Expected: FAIL because helper is missing.
- [ ] **Step 3: Implement minimal immutable reorder helper**
```ts
export function moveWithinZone<T>(items: T[], from: number, to: number): T[] {
// Validate bounds, clone array, splice move, return new array.
}
```
- [ ] **Step 4: Re-run tests to verify pass**
Run: `pnpm --filter @xtablo/main test -- src/pages/tablo-details/overviewReorder.test.ts`
Expected: PASS.
- [ ] **Step 5: Commit**
```bash
git add apps/main/src/pages/tablo-details/overviewReorder.ts apps/main/src/pages/tablo-details/overviewReorder.test.ts
git commit -m "test(main): cover overview reorder helper"
```
### Task 2.3: Integrate edit mode + DnD + auto-save in `tablo-details.tsx`
**Files:**
- Modify: `apps/main/src/pages/tablo-details.tsx`
- Create: `apps/main/src/pages/tablo-details.layout.test.tsx`
- Test: `apps/main/src/pages/tablo-details.layout.test.tsx`
- [ ] **Step 1: Write failing page test for persisted layout render order**
```ts
it("renders overview blocks in persisted order", async () => {
// Mock useTablosList returning tablo.layout_overview_v1 with swapped left-zone order.
// Assert rendered block headings follow persisted order.
});
```
- [ ] **Step 2: Write failing page test for save payload on drop**
```ts
it("persists layout_overview_v1 after drop", async () => {
// Mock useUpdateTablo mutate; trigger drag/drop; assert mutate payload includes layout_overview_v1.
});
```
- [ ] **Step 3: Run tests to capture failure**
Run: `pnpm --filter @xtablo/main test -- src/pages/tablo-details.layout.test.tsx`
Expected: FAIL (no layout state/edit mode yet).
- [ ] **Step 4: Implement UI integration minimally**
Implementation checklist:
- Add `isLayoutEditMode` state and gate by editor permission.
- Read `tablo.layout_overview_v1`, sanitize via `sanitizeOverviewLayout`.
- Render overview cards from zone arrays (not hardcoded order).
- Add drag handlers in edit mode (same-zone reorder only).
- On drop: optimistic state update + `useUpdateTablo({ id, layout_overview_v1 })`.
- On mutation error: rollback previous layout + toast error.
- Add `Reset default layout` button in edit mode.
- [ ] **Step 5: Re-run test file and fix until green**
Run: `pnpm --filter @xtablo/main test -- src/pages/tablo-details.layout.test.tsx`
Expected: PASS.
- [ ] **Step 6: Run nearby regression tests**
Run: `pnpm --filter @xtablo/main test -- src/pages/tablo.test.tsx`
Expected: PASS.
- [ ] **Step 7: Commit**
```bash
git add apps/main/src/pages/tablo-details.tsx apps/main/src/pages/tablo-details.layout.test.tsx
git commit -m "feat(main): add no-code overview reorder mode with autosave"
```
---
## Chunk 3: API Confidence + End-to-End Verification
### Task 3.1: Add API route test for layout payload update
**Files:**
- Modify: `apps/api/src/__tests__/routes/tablo.test.ts`
- Test: `apps/api/src/__tests__/routes/tablo.test.ts`
- [ ] **Step 1: Write failing API test**
```ts
it("should allow owner to update layout_overview_v1", async () => {
const res = await updateTabloRequest(ownerUser, client, "test_tablo_owner_private", {
layout_overview_v1: {
version: 1,
leftZone: ["myTasks", "description"],
rightZone: ["files", "info"],
},
});
expect(res.status).toBe(200);
});
```
- [ ] **Step 2: Run focused API test to verify behavior**
Run: `pnpm --filter @xtablo/api test -- src/__tests__/routes/tablo.test.ts`
Expected: PASS after migration/types are in place; FAIL before if column not available in test DB.
- [ ] **Step 3: If failing due missing migration in test DB, run DB reset once**
Run: `supabase db reset`
Expected: migrations re-applied, test DB aligned.
- [ ] **Step 4: Re-run API test**
Run: `pnpm --filter @xtablo/api test -- src/__tests__/routes/tablo.test.ts`
Expected: PASS.
- [ ] **Step 5: Commit**
```bash
git add apps/api/src/__tests__/routes/tablo.test.ts
git commit -m "test(api): cover tablo layout_overview_v1 updates"
```
### Task 3.2: Final verification sweep before merge
**Files:**
- Modify: none (verification only)
- [ ] **Step 1: Run main app tests for changed files**
Run: `pnpm --filter @xtablo/main test -- src/pages/tablo-details/overviewLayout.test.ts src/pages/tablo-details/overviewReorder.test.ts src/pages/tablo-details.layout.test.tsx`
Expected: PASS.
- [ ] **Step 2: Run API test for changed route test file**
Run: `pnpm --filter @xtablo/api test -- src/__tests__/routes/tablo.test.ts`
Expected: PASS.
- [ ] **Step 3: Run typecheck for touched apps**
Run: `pnpm --filter @xtablo/main typecheck && pnpm --filter @xtablo/api typecheck`
Expected: PASS.
- [ ] **Step 4: Run format/lint on touched workspaces if needed**
Run: `pnpm --filter @xtablo/main lint && pnpm --filter @xtablo/api lint`
Expected: PASS or only pre-existing unrelated failures.
- [ ] **Step 5: Squash fixups if needed and prepare PR summary**
```bash
git log --oneline --max-count=8
```
Expected: clean, reviewable commit sequence.
---
## Notes for Execution Agent
- Follow @superpowers/test-driven-development for each code task (tests first).
- Before declaring completion, follow @superpowers/verification-before-completion.
- Keep edits localized; do not refactor unrelated tabs/components.
- If a file grows too large during Task 2.3, extract small focused helpers instead of adding complexity in-place.

View file

@ -0,0 +1,389 @@
# Pricing Signup Three-Plan Happy-Path Verification Implementation Plan
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Ensure signup immediately starts Stripe Checkout for `solo` (monthly), `team` (monthly), and `founder` (yearly), then verify all three happy paths pass in Stripe test mode with evidence.
**Architecture:** Unify signup billing intent to a 3-plan contract (`solo|team|founder`), centralize pending-checkout logic in billing helpers, and make post-signup checkout bootstrap plan-agnostic in auth flow. Keep `create-checkout-session` API as-is (already supports 3 plans), add focused frontend tests for intent submission and pending checkout recovery, then run live manual verification and document outcomes.
**Tech Stack:** TypeScript, React 19, React Query, Vitest, Testing Library, Stripe test mode, Stripe CLI webhook forwarding
---
## Spec Reference
- `/Users/arthur.belleville/Documents/perso/projects/xtablo-source/docs/superpowers/specs/2026-03-15-pricing-signup-three-plan-happy-path-verification-design.md`
## Execution Skills
- `@test-driven-development`
- `@systematic-debugging`
- `@verification-before-completion`
## Scope Guardrails (YAGNI)
- Do not add failure-path handling beyond current behavior (declines/cancellations/webhook outages remain out-of-scope for this cycle).
- Do not redesign billing pages or pricing UI beyond replacing `trial` with explicit `solo`/`team` plans.
- Do not add browser automation (Playwright/Cypress) in this cycle.
## File Structure (lock boundaries before coding)
### Billing contract + shared helpers
- Modify: `apps/main/src/lib/billing.ts`
- Responsibility: canonical signup billing intent types (`solo|team|founder`), localStorage key helpers, and plan satisfaction helper used by auth/bootstrap flows.
- Create: `apps/main/src/lib/billing.test.ts`
- Responsibility: unit tests for plan parsing/defaults/satisfaction behavior.
### Signup UI + intent submission
- Modify: `apps/main/src/pages/signup.tsx`
- Responsibility: expose 3 plan choices and submit selected billing intent.
- Modify: `apps/main/src/pages/signup-v2.tsx`
- Responsibility: keep v2 signup behavior aligned with primary signup flow.
- Modify: `apps/main/src/pages/signup.test.tsx`
- Responsibility: validate default plan + intent payload mapping.
- Create: `apps/main/src/pages/signup-v2.test.tsx`
- Responsibility: parity checks for v2 plan intent submission.
### Checkout bootstrap and retry-on-login component
- Modify: `apps/main/src/hooks/auth.ts`
- Responsibility: create checkout immediately after signup for any selected plan.
- Move/Modify: `apps/main/src/components/PendingFounderCheckout.tsx` -> `apps/main/src/components/PendingSignupCheckout.tsx`
- Responsibility: recover pending checkout for any of the 3 plans once user is authenticated.
- Modify: `apps/main/src/App.tsx`
- Responsibility: mount renamed generalized pending-checkout component.
- Create: `apps/main/src/components/PendingSignupCheckout.test.tsx`
- Responsibility: behavior tests for pending plan recovery (`solo`, `team`, `founder`).
### Verification evidence
- Create: `docs/superpowers/reports/2026-03-15-pricing-signup-three-plan-happy-path-verification.md`
- Responsibility: record pass/fail evidence matrix for all three live flows.
---
## Chunk 1: Contract + Signup Intent Wiring (TDD)
### Task 1.1: Replace legacy signup intent contract (`trial|founder`) with explicit 3-plan contract
**Files:**
- Modify: `apps/main/src/lib/billing.ts`
- Create: `apps/main/src/lib/billing.test.ts`
- Test: `apps/main/src/lib/billing.test.ts`
- [ ] **Step 1: Write failing unit tests for billing intent helpers**
```ts
import { describe, expect, it } from "vitest";
import {
DEFAULT_SIGNUP_BILLING_INTENT,
isSignupBillingIntent,
satisfiesPendingCheckoutPlan,
} from "./billing";
describe("billing helpers", () => {
it("defaults signup intent to solo", () => {
expect(DEFAULT_SIGNUP_BILLING_INTENT).toBe("solo");
});
it("recognizes only solo/team/founder", () => {
expect(isSignupBillingIntent("solo")).toBe(true);
expect(isSignupBillingIntent("team")).toBe(true);
expect(isSignupBillingIntent("founder")).toBe(true);
expect(isSignupBillingIntent("trial")).toBe(false);
});
it("treats founder as satisfied only by annual", () => {
expect(satisfiesPendingCheckoutPlan("founder", "annual")).toBe(true);
expect(satisfiesPendingCheckoutPlan("founder", "team")).toBe(false);
});
});
```
- [ ] **Step 2: Run test to verify failure**
Run: `pnpm --filter @xtablo/main test -- src/lib/billing.test.ts`
Expected: FAIL because constants/helpers do not exist yet.
- [ ] **Step 3: Implement minimal helpers in `billing.ts`**
```ts
export type SignupBillingIntent = "solo" | "team" | "founder";
export const DEFAULT_SIGNUP_BILLING_INTENT: SignupBillingIntent = "solo";
export function isSignupBillingIntent(value: string | null): value is SignupBillingIntent {
return value === "solo" || value === "team" || value === "founder";
}
export function satisfiesPendingCheckoutPlan(
pending: SignupBillingIntent,
active: "solo" | "team" | "annual" | null
): boolean {
// founder => annual only
// team => team or annual
// solo => solo/team/annual
}
```
- [ ] **Step 4: Re-run unit tests**
Run: `pnpm --filter @xtablo/main test -- src/lib/billing.test.ts`
Expected: PASS.
- [ ] **Step 5: Commit**
```bash
git add apps/main/src/lib/billing.ts apps/main/src/lib/billing.test.ts
git commit -m "feat(main): add 3-plan signup billing helpers"
```
### Task 1.2: Update signup forms to submit `solo|team|founder` intents
**Files:**
- Modify: `apps/main/src/pages/signup.tsx`
- Modify: `apps/main/src/pages/signup-v2.tsx`
- Modify: `apps/main/src/pages/signup.test.tsx`
- Create: `apps/main/src/pages/signup-v2.test.tsx`
- Test: `apps/main/src/pages/signup.test.tsx`, `apps/main/src/pages/signup-v2.test.tsx`
- [ ] **Step 1: Update existing signup test to encode new contract first**
Test updates in `signup.test.tsx`:
- default submit sends `billing_intent: "solo"` (instead of `trial`)
- selecting Team sends `billing_intent: "team"`
- selecting Founder sends `billing_intent: "founder"`
- [ ] **Step 2: Add v2 parity test with same assertions**
Create `signup-v2.test.tsx` with one default and two selection cases (`team`, `founder`).
- [ ] **Step 3: Run tests to verify failure before UI changes**
Run: `pnpm --filter @xtablo/main test -- src/pages/signup.test.tsx src/pages/signup-v2.test.tsx`
Expected: FAIL because forms still default to `trial` and do not expose explicit Team option.
- [ ] **Step 4: Implement minimal signup UI changes**
Implementation checklist:
- Default state is `solo` in both signup pages.
- Plan cards/buttons become exactly `Solo`, `Team`, `Founder`.
- Copy reflects cadence (`Solo` + `Team` monthly, `Founder` yearly).
- Submit passes selected plan unchanged as `billing_intent`.
- [ ] **Step 5: Re-run targeted signup tests**
Run: `pnpm --filter @xtablo/main test -- src/pages/signup.test.tsx src/pages/signup-v2.test.tsx`
Expected: PASS.
- [ ] **Step 6: Commit**
```bash
git add apps/main/src/pages/signup.tsx apps/main/src/pages/signup-v2.tsx apps/main/src/pages/signup.test.tsx apps/main/src/pages/signup-v2.test.tsx
git commit -m "feat(main): submit solo team founder intents from signup"
```
---
## Chunk 2: Immediate Checkout Bootstrap + Pending Recovery (TDD)
### Task 2.1: Generalize pending checkout component to all plans
**Files:**
- Move/Modify: `apps/main/src/components/PendingFounderCheckout.tsx` -> `apps/main/src/components/PendingSignupCheckout.tsx`
- Modify: `apps/main/src/App.tsx`
- Create: `apps/main/src/components/PendingSignupCheckout.test.tsx`
- Test: `apps/main/src/components/PendingSignupCheckout.test.tsx`
- [ ] **Step 1: Write failing component tests for plan-aware pending checkout**
Cases to cover:
- pending `solo` triggers `createCheckout({ plan: "solo" })`
- pending `team` triggers `createCheckout({ plan: "team" })`
- pending `founder` + active annual clears localStorage and does not trigger checkout
- invalid pending value clears localStorage and does not trigger checkout
- [ ] **Step 2: Run test to confirm failure**
Run: `pnpm --filter @xtablo/main test -- src/components/PendingSignupCheckout.test.tsx`
Expected: FAIL because component/file does not exist yet.
- [ ] **Step 3: Implement generalized component**
Implementation checklist:
- Parse pending plan via `isSignupBillingIntent`.
- Use `satisfiesPendingCheckoutPlan` against `organizationData.active_subscription_plan`.
- Trigger `useCreateCheckoutSession` with the pending plan when needed.
- Preserve retry behavior on mutation error.
- Update `App.tsx` import/render to `PendingSignupCheckout`.
- [ ] **Step 4: Re-run component test**
Run: `pnpm --filter @xtablo/main test -- src/components/PendingSignupCheckout.test.tsx`
Expected: PASS.
- [ ] **Step 5: Commit**
```bash
git add apps/main/src/components/PendingSignupCheckout.tsx apps/main/src/components/PendingSignupCheckout.test.tsx apps/main/src/App.tsx
git rm apps/main/src/components/PendingFounderCheckout.tsx
git commit -m "refactor(main): generalize pending signup checkout to all plans"
```
### Task 2.2: Make `useSignUp` bootstrap checkout for any selected plan
**Files:**
- Modify: `apps/main/src/hooks/auth.ts`
- Create: `apps/main/src/hooks/auth.signup.test.tsx`
- Test: `apps/main/src/hooks/auth.signup.test.tsx`, `apps/main/src/pages/signup.test.tsx`, `apps/main/src/components/PendingSignupCheckout.test.tsx`
- [ ] **Step 1: Write failing auth hook test for Team checkout bootstrap**
In `auth.signup.test.tsx`, add a test that:
- mocks a confirmed signup response with `session.access_token`
- calls `useSignUp(...).mutate` with `billing_intent: "team"`
- asserts `/api/v1/stripe/create-checkout-session` is called with `{ plan: "team" }`
- asserts `localStorage` pending key behavior is preserved for retry if redirect bootstrap fails
- [ ] **Step 2: Run focused tests to confirm failure**
Run: `pnpm --filter @xtablo/main test -- src/hooks/auth.signup.test.tsx`
Expected: FAIL because `auth.ts` currently founder-special-cases checkout bootstrap.
- [ ] **Step 3: Implement minimal auth flow changes**
Implementation checklist in `auth.ts`:
- treat `variables.billing_intent` as selected checkout plan (`solo|team|founder`)
- set `SIGNUP_BILLING_INTENT_KEY` + `PENDING_BILLING_CHECKOUT_PLAN_KEY` for all plans
- after confirmed signup, call `/api/v1/stripe/create-checkout-session` with `{ plan: selectedPlan }`
- keep pending key for retry path if immediate redirect fails
- remove founder-only branching
- [ ] **Step 4: Re-run focused tests**
Run: `pnpm --filter @xtablo/main test -- src/hooks/auth.signup.test.tsx src/pages/signup.test.tsx src/components/PendingSignupCheckout.test.tsx`
Expected: PASS.
- [ ] **Step 5: Run broader safety checks**
Run: `pnpm --filter @xtablo/main test -- src/hooks/auth.signup.test.tsx src/pages/signup.test.tsx src/pages/signup-v2.test.tsx src/components/PendingSignupCheckout.test.tsx`
Expected: PASS.
- [ ] **Step 6: Commit**
```bash
git add apps/main/src/hooks/auth.ts apps/main/src/hooks/auth.signup.test.tsx apps/main/src/pages/signup.test.tsx
git commit -m "feat(main): bootstrap signup checkout for solo team founder"
```
---
## Chunk 3: Verification and Evidence Capture (Live Stripe Test Mode)
### Task 3.1: Preflight checks before live runs
**Files:**
- Create: `docs/superpowers/reports/2026-03-15-pricing-signup-three-plan-happy-path-verification.md`
- [ ] **Step 1: Confirm plan price IDs exist in API env**
Run: `rg -n "STRIPE_SOLO_PRICE_ID|STRIPE_TEAM_MONTHLY_PRICE_ID|STRIPE_FOUNDER_PRICE_ID" apps/api/src/routers/stripe.ts`
Expected: all three env-backed plan mappings present.
- [ ] **Step 2: Start webhook forwarding in Stripe test mode**
Run: `stripe listen --forward-to http://localhost:3000/api/v1/stripe-webhook`
Expected: CLI prints `Ready!` and webhook signing secret line.
- [ ] **Step 3: Start local apps (if not already running)**
Run in separate terminals:
- `pnpm dev:api`
- `pnpm dev:main`
Expected: API and frontend boot with no fatal startup errors.
- [ ] **Step 4: Commit code before live manual verification starts**
```bash
git status --short
# Expected: empty output (or only known, intentional files for this plan)
```
### Task 3.2: Execute happy-path run for `solo` (monthly)
**Files:**
- Modify: `docs/superpowers/reports/2026-03-15-pricing-signup-three-plan-happy-path-verification.md`
- [ ] **Step 1: Run signup with unique Solo email and select Solo plan**
- [ ] **Step 2: Verify immediate redirect to Stripe Checkout**
- [ ] **Step 3: Complete payment with `4242 4242 4242 4242`**
- [ ] **Step 4: Verify return to success URL and in-app paid status**
- [ ] **Step 5: Record observed billing interval as `month` in report**
### Task 3.3: Execute happy-path run for `team` (monthly)
**Files:**
- Modify: `docs/superpowers/reports/2026-03-15-pricing-signup-three-plan-happy-path-verification.md`
- [ ] **Step 1: Run signup with unique Team email and select Team plan**
- [ ] **Step 2: Verify immediate redirect to Stripe Checkout**
- [ ] **Step 3: Complete payment with `4242 4242 4242 4242`**
- [ ] **Step 4: Verify return to success URL and in-app paid status**
- [ ] **Step 5: Record observed billing interval as `month` in report**
### Task 3.4: Execute happy-path run for `founder` (yearly)
**Files:**
- Modify: `docs/superpowers/reports/2026-03-15-pricing-signup-three-plan-happy-path-verification.md`
- [ ] **Step 1: Run signup with unique Founder email and select Founder plan**
- [ ] **Step 2: Verify immediate redirect to Stripe Checkout**
- [ ] **Step 3: Complete payment with `4242 4242 4242 4242`**
- [ ] **Step 4: Verify return to success URL and in-app paid status**
- [ ] **Step 5: Record observed billing interval as `year` in report**
### Task 3.5: Final verification + completion gates
**Files:**
- Modify: `docs/superpowers/reports/2026-03-15-pricing-signup-three-plan-happy-path-verification.md`
- [ ] **Step 1: Run regression checks for touched frontend files**
Run: `pnpm --filter @xtablo/main test -- src/lib/billing.test.ts src/pages/signup.test.tsx src/pages/signup-v2.test.tsx src/components/PendingSignupCheckout.test.tsx`
Expected: PASS.
- [ ] **Step 2: Run typecheck**
Run: `pnpm --filter @xtablo/main typecheck`
Expected: PASS (or only documented unrelated pre-existing failures).
- [ ] **Step 3: Fill final pass/fail matrix in report**
Required columns:
- `plan`
- `checkout reached`
- `payment completed`
- `success redirect`
- `interval expected`
- `interval observed`
- `result`
- `evidence`
- [ ] **Step 4: Commit verification report and remaining changes**
```bash
git add apps/main/src/lib/billing.ts apps/main/src/lib/billing.test.ts apps/main/src/pages/signup.tsx apps/main/src/pages/signup-v2.tsx apps/main/src/pages/signup.test.tsx apps/main/src/pages/signup-v2.test.tsx apps/main/src/hooks/auth.ts apps/main/src/components/PendingSignupCheckout.tsx apps/main/src/components/PendingSignupCheckout.test.tsx apps/main/src/App.tsx docs/superpowers/reports/2026-03-15-pricing-signup-three-plan-happy-path-verification.md
git commit -m "feat(main): verify solo team founder signup checkout happy paths"
```
---
## Plan Review Checklist (for reviewer subagent)
- [ ] No TODO/TBD placeholders remain.
- [ ] Every task includes explicit files, commands, and expected outcomes.
- [ ] Plan aligns exactly with approved spec scope (happy paths only).
- [ ] New/modified files keep single responsibilities and manageable size.
- [ ] All execution steps use checkbox syntax.

View file

@ -0,0 +1,57 @@
# Pricing Signup Three-Plan Happy-Path Verification Report
Date: 2026-03-15
## Preflight
- `STRIPE_SOLO_PRICE_ID`, `STRIPE_TEAM_MONTHLY_PRICE_ID`, and `STRIPE_FOUNDER_PRICE_ID` are mapped in `apps/api/src/routers/stripe.ts`.
- `stripe` CLI is available (`stripe version 1.37.3`) and authenticated.
- Local frontend is running on `5173`; API process is already listening on `8080`.
## Automated Verification Completed
Commands run successfully:
```bash
cd apps/main && pnpm exec vitest run src/lib/billing.test.ts src/hooks/auth.signup.test.ts src/components/PendingSignupCheckout.test.tsx src/pages/signup.test.tsx src/pages/signup-v2.test.tsx --mode dev --passWithNoTests
pnpm --filter @xtablo/main typecheck
```
Coverage from these checks:
- Signup defaults to `solo` and supports selecting `team` and `founder` in both signup UIs.
- Billing intent contract is now explicit (`solo|team|founder`) with plan satisfaction helpers.
- Pending checkout recovery component supports all three plans and handles already-satisfied or invalid pending states.
- Signup auth path resolves billing intent with default `solo`.
## Live Stripe Happy Paths
Retry with Stripe CLI:
- Started listener: `stripe listen --forward-to http://127.0.0.1:8080/api/v1/stripe-webhook`
- Triggered checkout fixture: `stripe trigger checkout.session.completed`
- Result: all forwarded webhook requests returned `400` from API webhook endpoint.
Observed evidence:
- Listener reported `Ready` and emitted Stripe events.
- Forward logs showed repeated `400` responses for events including:
- `product.created`
- `price.created`
- `checkout.session.completed`
- `payment_intent.created`
- `payment_intent.succeeded`
- Direct probe confirms webhook route is reachable, but signature-sensitive:
- `POST /api/v1/stripe-webhook` with fake header returns `{"error":"Unable to extract timestamp and signatures from header"}`.
Conclusion for live flow retry:
- Stripe CLI path is now working, but webhook processing is failing server-side during forwarded events (likely webhook secret mismatch or environment mismatch on the API instance currently running at `8080`).
- Browser checkout happy paths cannot be marked passing until webhook `400` issue is resolved and end-to-end redirects are exercised.
| plan | checkout reached | payment completed | success redirect | interval expected | interval observed | result | evidence |
|------|------------------|-------------------|------------------|-------------------|-------------------|--------|----------|
| solo | not run | not run | not run | month | not run | blocked | webhook forward returns 400 |
| team | not run | not run | not run | month | not run | blocked | webhook forward returns 400 |
| founder | not run | not run | not run | year | not run | blocked | webhook forward returns 400 |
## Summary
- Code-level and component-level verification is green for the 3-plan signup wiring.
- Live Stripe card-entry happy-path verification is blocked by webhook `400` failures on the current local API endpoint (`:8080`) and still requires browser payment execution once webhook processing is healthy.