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
40 changes: 40 additions & 0 deletions src/pages/MarketingPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,46 @@ describe('MarketingPage — claim consistency (T18 P1-4 / P1-6)', () => {
expect(text).toMatch(/~60s/)
})

// TEAM-GATE reconciliation (2026-06-08): the homepage Team tile's CTA used
// to point at the self-serve /app/checkout?plan=team path — a tier the
// server-side gate rejects with 400 tier_not_yet_available, so the homepage
// was marketing a plan the platform can't actually sell. Team is
// contact-sales only until its delivery is proven built (CEO TEAM-GATE
// directive). These guards pin the homepage Team CTA to a contact-sales
// mailto and forbid the self-serve checkout link from creeping back, AND
// confirm Team is NOT badged "Most popular" (Pro is the highlighted tier).
it('homepage Team tile CTA is a contact-sales mailto, NOT a self-serve checkout', () => {
const { container } = render(
<MemoryRouter initialEntries={['/']}>
<MarketingPage />
</MemoryRouter>,
)
const teamCta = findAnchorByText('Contact sales →')
expect(teamCta).not.toBeNull()
expect(teamCta!.getAttribute('href')).toContain('mailto:sales@instanode.dev')
// The retired self-serve Team checkout link must not exist anywhere on the page.
const hrefs = Array.from(container.querySelectorAll('a')).map((a) => a.getAttribute('href') ?? '')
expect(hrefs.some((h) => h.includes('plan=team'))).toBe(false)
// And the old buyable label must be gone.
expect(findAnchorByText('Start team →')).toBeNull()
})

it('only Pro is badged "Most popular" — Team must not be highlighted/badged as buyable', () => {
const { container } = render(
<MemoryRouter initialEntries={['/']}>
<MarketingPage />
</MemoryRouter>,
)
const badges = Array.from(container.querySelectorAll('.mkt-featured-flag')).filter(
(el) => (el.textContent ?? '').trim() === 'Most popular',
)
expect(badges.length).toBe(1)
// The badge sits inside the Pro card, not the Team card.
const card = badges[0].closest('.mkt-price-card')
expect(card?.textContent).toContain('Pro')
expect(card?.textContent).not.toContain('Team')
})

// BIZ-3 (2026-05-29): the landing pricing tile shipped "1 small deployment"
// and "10 medium deployments" copy from the days when /deploy/new had a
// deployment_size field. The backend dropped that field; marketing
Expand Down
26 changes: 17 additions & 9 deletions src/pages/MarketingPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,13 @@ const ROUTES = {
playground: '#playground',
} as const

// Team is sales-assisted only (TEAM-GATE, 2026-06-04 CEO directive): it is NOT
// self-serve and must not route to /app/checkout until its dedicated-infra
// delivery is proven built — the server-side gate rejects a Team checkout with
// 400 tier_not_yet_available. The Team CTA points here, matching the
// contact-sales action PricingPage.tsx and BillingPage.tsx already use.
const SALES_MAILTO_TEAM = 'mailto:sales@instanode.dev?subject=Team%20plan%20enquiry'

type Service = {
id: 'pg' | 'rd' | 'mg' | 'vc' | 'qu' | 'st' | 'wh' | 'dp'
name: string
Expand Down Expand Up @@ -203,12 +210,13 @@ const PLANS: Plan[] = [
// Team tier — $199/mo. strict-80% margin redesign (2026-06-05): every
// Team limit is now a finite plans.yaml cap (was -1/unlimited). Above
// these caps = Enterprise (contact sales).
// NOTE (pre-existing, out of scope here): this CTA still points at the
// self-serve /app/checkout?plan=team path, which the server-side Team
// gate rejects with 400 tier_not_yet_available. The PricingPage Team CTA
// already uses a contact-sales mailto. This MarketingPage CTA should be
// reconciled with the gate in a follow-up (not changed here — the
// strict-margin task is explicitly copy-only and must not touch the gate).
// TEAM-GATE reconciliation (2026-06-08): the CTA previously pointed at the
// self-serve /app/checkout?plan=team path, which the server-side Team gate
// rejects with 400 tier_not_yet_available — so the homepage was offering a
// tier the platform can't sell. Now reconciled with PricingPage.tsx +
// BillingPage.tsx: Team is contact-sales only until its delivery is proven
// built. Do NOT re-point this at /app/checkout. Ref TEAM-GATE directive
// (2026-06-04) + docs/sessions/2026-06-04/TEAM-PLAN-GATE-AND-BUILD.md.
id: 'team',
name: 'Team',
tagline: 'For the engineering org. Dedicated infra, RBAC + audit, with SSO & SLA on the way.',
Expand All @@ -221,8 +229,8 @@ const PLANS: Plan[] = [
'Need more? Enterprise — contact sales',
],
cta: {
label: 'Start team →',
href: '/app/checkout?plan=team&frequency=monthly',
label: 'Contact sales →',
href: SALES_MAILTO_TEAM,
variant: 'secondary',
},
},
Expand Down Expand Up @@ -311,7 +319,7 @@ export function MarketingPage() {
<p className="mkt-hero-sub">
Postgres, Redis, MongoDB, vectors, queues, storage, webhooks, and deployments.{' '}
<strong>Provisioned in &lt;2 seconds.</strong>{' '}
No signup, no Docker, no waitlist.
Self-serve from the first call to a paid plan — no signup, no Docker, no sales call.
</p>
<HeroPromptCard />

Expand Down
Loading