From e40e3f1743674c507772ea164654b47f268f267d Mon Sep 17 00:00:00 2001 From: AINative Admin Date: Tue, 6 Oct 2026 09:03:16 -0700 Subject: [PATCH 1/3] feat(state): add growthIntent flag and SET_GROWTH_INTENT action (#994) --- .../lib/build/state-growth-intent.test.ts | 19 +++++++++++++++++++ lib/build/state.ts | 5 +++++ 2 files changed, 24 insertions(+) create mode 100644 __tests__/lib/build/state-growth-intent.test.ts diff --git a/__tests__/lib/build/state-growth-intent.test.ts b/__tests__/lib/build/state-growth-intent.test.ts new file mode 100644 index 00000000..945b2d2b --- /dev/null +++ b/__tests__/lib/build/state-growth-intent.test.ts @@ -0,0 +1,19 @@ +import { describe, it, expect } from 'vitest' +import { buildReducer, initialBuildState } from '@/lib/build/state' + +describe('growthIntent (#BLD-10)', () => { + it('defaults to false', () => { + expect(initialBuildState.growthIntent).toBe(false) + }) + + it('SET_GROWTH_INTENT sets the flag', () => { + const next = buildReducer(initialBuildState, { type: 'SET_GROWTH_INTENT', value: true }) + expect(next.growthIntent).toBe(true) + }) + + it('SET_GROWTH_INTENT can clear the flag back to false', () => { + const grown = buildReducer(initialBuildState, { type: 'SET_GROWTH_INTENT', value: true }) + const cleared = buildReducer(grown, { type: 'SET_GROWTH_INTENT', value: false }) + expect(cleared.growthIntent).toBe(false) + }) +}) diff --git a/lib/build/state.ts b/lib/build/state.ts index ed189af6..7a2a9b01 100644 --- a/lib/build/state.ts +++ b/lib/build/state.ts @@ -101,6 +101,7 @@ export interface BuildState { conflictResolved: boolean conflictView: string // the upstream artifact whose edit triggered the conflict ('' = none) answers: { privacy?: PrivacyAnswer; [k: string]: string | undefined } + growthIntent: boolean // #BLD-10 — true when the visitor picked "Grow my company" on Start.tsx companyName: string appSub: string // staging subdomain, e.g. {appSub}.ainative.studio // A ?company= deep link whose slug turned out not to resolve to any real, @@ -198,6 +199,7 @@ export const initialBuildState: BuildState = { conflictResolved: false, conflictView: '', answers: {}, + growthIntent: false, companyName: '', appSub: '', deepLinkNotFound: null, @@ -240,6 +242,7 @@ export type BuildAction = // before kickoff's own 3rd question has been answered. Pure data, no // side effects — mirrors SET_IDEA's own shape. | { type: 'SET_BRAND_DRAFT'; appSub: string; companyName: string; brandTagline: string; brandColor: string } + | { type: 'SET_GROWTH_INTENT'; value: boolean } | { type: 'GEN_DONE'; view: string; content: unknown } | { type: 'GEN_FAIL'; view: string; error: string } /** Inline artifact edit (GR-16 #329): replace a view's generated content with @@ -407,6 +410,8 @@ export function buildReducer(state: BuildState, action: BuildAction): BuildState brandTagline: action.brandTagline, brandColor: action.brandColor, } + case 'SET_GROWTH_INTENT': + return { ...state, growthIntent: action.value } case 'GEN_DONE': return { ...state, From 353f63ef47db018843b54c964df6fb8cd4900b81 Mon Sep 17 00:00:00 2001 From: AINative Admin Date: Tue, 6 Oct 2026 09:04:17 -0700 Subject: [PATCH 2/3] fix(start): route Grow my company into kickoff instead of a dead-end login wall (#994) --- __tests__/components/build/Start.test.tsx | 49 +++++++++++++++++++++++ components/build/screens/Start.tsx | 13 +++--- 2 files changed, 57 insertions(+), 5 deletions(-) create mode 100644 __tests__/components/build/Start.test.tsx diff --git a/__tests__/components/build/Start.test.tsx b/__tests__/components/build/Start.test.tsx new file mode 100644 index 00000000..3dedeb29 --- /dev/null +++ b/__tests__/components/build/Start.test.tsx @@ -0,0 +1,49 @@ +// @vitest-environment jsdom +import { describe, it, expect, vi, beforeAll, afterEach } from 'vitest' +import React from 'react' +import { createRoot, type Root } from 'react-dom/client' +import { act } from 'react' + +beforeAll(() => { + ;(globalThis as any).React = React + ;(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true +}) + +const dispatchMock = vi.fn() +vi.mock('@/contexts/build-context', () => ({ useBuild: () => ({ dispatch: dispatchMock }) })) + +import { Start } from '@/components/build/screens/Start' + +let host: HTMLElement +let root: Root +function render(node: React.ReactElement) { + host = document.createElement('div') + document.body.appendChild(host) + root = createRoot(host) + act(() => { root.render(node) }) +} +afterEach(() => { + act(() => { root?.unmount() }) + host?.remove() + vi.clearAllMocks() +}) + +describe('Start — "Grow my company" reaches kickoff (#BLD-10)', () => { + it('picking "Grow my company" then Continue dispatches GOTO_SCREEN build AND SET_GROWTH_INTENT true', async () => { + render(React.createElement(Start)) + const growBtn = host.querySelector('[data-testid="start-grow"]') as HTMLButtonElement + const continueBtn = host.querySelector('[data-testid="start-continue"]') as HTMLButtonElement + await act(async () => { growBtn.click() }) + await act(async () => { continueBtn.click() }) + expect(dispatchMock).toHaveBeenCalledWith({ type: 'SET_GROWTH_INTENT', value: true }) + expect(dispatchMock).toHaveBeenCalledWith({ type: 'GOTO_SCREEN', screen: 'build' }) + }) + + it('picking "Create a new company" then Continue dispatches SET_GROWTH_INTENT false', async () => { + render(React.createElement(Start)) + const continueBtn = host.querySelector('[data-testid="start-continue"]') as HTMLButtonElement + await act(async () => { continueBtn.click() }) + expect(dispatchMock).toHaveBeenCalledWith({ type: 'SET_GROWTH_INTENT', value: false }) + expect(dispatchMock).toHaveBeenCalledWith({ type: 'GOTO_SCREEN', screen: 'build' }) + }) +}) diff --git a/components/build/screens/Start.tsx b/components/build/screens/Start.tsx index 29ccc896..48bb1c05 100644 --- a/components/build/screens/Start.tsx +++ b/components/build/screens/Start.tsx @@ -4,8 +4,10 @@ * Start — funnel step 1 (Claude Design handoff). "Let's get started." The visitor * picks whether they're creating a new company or growing an existing one. * - * Create a new company → step 2 (Build: "Let's build something") - * Grow my company → straight to auth (they already have a business) + * #BLD-10 — both paths now reach the same build/kickoff flow; growthIntent + * (set here) selects which kickoff question variant KickoffQuestions.tsx + * shows. The auth-wall decision is untouched — it stays in + * KickoffQuestions.tsx's proceed(), same as the "create" path already used. * * Selection is a two-card single-choice; Continue commits it. Matches the * prototype's create/grow branch (Landing & Signup.dc.html). @@ -35,9 +37,10 @@ export function Start() { const goBuild = () => { window.scrollTo(0, 0) - // "Grow my company" already has a business → go to auth. "Create" continues - // into the build-idea step. - dispatch({ type: 'GOTO_SCREEN', screen: path === 'create' ? 'build' : 'login' }) + // #BLD-10 — both paths reach the same idea-entry/kickoff flow now; + // growthIntent tells KickoffQuestions.tsx which question variant to show. + dispatch({ type: 'SET_GROWTH_INTENT', value: path === 'grow' }) + dispatch({ type: 'GOTO_SCREEN', screen: 'build' }) } return ( From 9690713b443777642e48347b93269fdd7e36124c Mon Sep 17 00:00:00 2001 From: AINative Admin Date: Tue, 6 Oct 2026 09:04:39 -0700 Subject: [PATCH 3/3] fix(kickoff): activate growth question variant via state.growthIntent (#994) --- .../build/KickoffQuestions-growth.test.tsx | 46 +++++++++++++++++++ components/build/screens/KickoffQuestions.tsx | 2 +- 2 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 __tests__/components/build/KickoffQuestions-growth.test.tsx diff --git a/__tests__/components/build/KickoffQuestions-growth.test.tsx b/__tests__/components/build/KickoffQuestions-growth.test.tsx new file mode 100644 index 00000000..193d090a --- /dev/null +++ b/__tests__/components/build/KickoffQuestions-growth.test.tsx @@ -0,0 +1,46 @@ +// @vitest-environment jsdom +import { describe, it, expect, vi, beforeAll, afterEach } from 'vitest' +import React from 'react' +import { createRoot, type Root } from 'react-dom/client' +import { act } from 'react' + +beforeAll(() => { + ;(globalThis as any).React = React + ;(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true +}) + +vi.mock('next-auth/react', () => ({ useSession: () => ({ status: 'unauthenticated' }), getSession: vi.fn(async () => null) })) +vi.mock('@/components/analytics/google-analytics', () => ({ trackEvent: vi.fn() })) +vi.mock('@/lib/build/value-moment', () => ({ decideLimitAction: () => 'continue' })) +vi.mock('@/lib/build/anon-draft-token', () => ({ getOrCreateAnonDraftToken: () => 'tok-growth' })) +const dispatchMock = vi.fn() +vi.mock('@/contexts/build-context', () => ({ + useBuild: () => ({ + state: { track: 'company', idea: 'x', appSub: 'acme', companyName: 'Acme', brandTagline: '', brandColor: '#000', answers: {}, growthIntent: true }, + dispatch: dispatchMock, + }), +})) + +import { KickoffQuestions } from '@/components/build/screens/KickoffQuestions' +import { COMPANY_GROW_BUSINESS_QUESTIONS } from '@/lib/build/kickoff-questions' + +let host: HTMLElement +let root: Root +function render(node: React.ReactElement) { + host = document.createElement('div') + document.body.appendChild(host) + root = createRoot(host) + act(() => { root.render(node) }) +} +afterEach(() => { + act(() => { root?.unmount() }) + host?.remove() + vi.clearAllMocks() +}) + +describe('KickoffQuestions — growth variant reachable (#BLD-10)', () => { + it('shows the first COMPANY_GROW_BUSINESS_QUESTIONS question when state.growthIntent is true', () => { + render(React.createElement(KickoffQuestions)) + expect(host.textContent).toContain(COMPANY_GROW_BUSINESS_QUESTIONS[0].text) + }) +}) diff --git a/components/build/screens/KickoffQuestions.tsx b/components/build/screens/KickoffQuestions.tsx index bac22ec2..9f249cf1 100644 --- a/components/build/screens/KickoffQuestions.tsx +++ b/components/build/screens/KickoffQuestions.tsx @@ -25,7 +25,7 @@ export function KickoffQuestions() { const [index, setIndex] = useState(0) const [freeText, setFreeText] = useState('') const [proceeding, setProceeding] = useState(false) - const questions = getKickoffQuestions(state.track, false) + const questions = getKickoffQuestions(state.track, state.growthIntent ?? false) const question = questions[index] const answer = (value: string) => {