Skip to content

refactor(plans): render every surface from one plan catalogue - #22

Merged
kipavy merged 2 commits into
mainfrom
refactor/shared-plans
Sep 12, 2026
Merged

refactor(plans): render every surface from one plan catalogue#22
kipavy merged 2 commits into
mainfrom
refactor/shared-plans

Conversation

@kipavy

@kipavy kipavy commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

Plan data was maintained by hand in three places — the pricing cards, the portal's own list, and the billing terms as prose. They drifted three separate times today:

  • the portal priced Business at $30 instead of $25, with no annual saving shown
  • the portal showed Teams as "Coming soon" while the site was selling it, which is why Teams could not be tested from the one surface that has a seat selector
  • the terms promised a Teams trial before that trial existed

shared/plans.ts is now the single source for prices, savings, descriptions, features, trials and which plans bill per seat. All three surfaces derive from it and keep only what is genuinely local: the landing cards keep their highlight flag and CTA targets, the portal keeps its checkout behaviour.

Two mechanical details, both found by building rather than type-checking

Turbopack does not resolve tsconfig paths pointing outside the app root. tsc --noEmit passed happily against @shared/plans while next build failed with Module not found: Can't resolve '@shared/plans'. Each app now widens turbopack.root to the repository and spells out the alias. Both apps are verified with a real next build:

landing   ✓ Compiled successfully in 48s
portal    ✓ Compiled successfully in 43s

Vitest does not read tsconfig paths either and needs the same alias in vitest.config.mts.

The portal had no CI

That is why its copy drifted unnoticed: landing.yml only triggers on landing/**, and Vercel's deploy checks do not type-check. The portal gains a workflow running tsc --noEmit, landing gains a tsc step alongside its tests, and both now trigger on changes to shared/ — otherwise editing the catalogue would run neither.

Tests

Six new tests pin the invariants that matter commercially, not just structurally:

annual never costs more than monthly
per-seat plans are exactly Teams and Business   (mirrors PER_SEAT_PLANS server-side)
Business never undercuts Teams at the shared seat floor
only the Pro trial is free of a credit card
Business is sold without a trial

vitest run — 41 passed across 2 files.

Deliberate behaviour change

Feature lists are unified to the fuller landing copy, so the portal now also shows "Cross-device sessions" on Pro and "Persistent sessions & workspace restore" on Free. The two lists had silently diverged; this picks the more complete one rather than the portal's.

Not included

PLAN_FEATURES in the desktop client is a fourth copy, in a different repository. It carries only the feature matrix rather than prices, so sharing it would mean publishing a package for modest benefit. The server stays authoritative for the seat floor and variant ids — those are enforcement, and duplicating them into a display module would invite the opposite bug.

Plan data was maintained by hand in three places: the pricing cards, the
portal's own list, and the billing terms as prose. They drifted three times in
a single day — the portal priced Business at $30 instead of $25, showed Teams
as "Coming soon" while the site sold it, and the terms promised a Teams trial
that did not exist yet.

shared/plans.ts is now the single source for prices, savings, descriptions,
features, trials and which plans bill per seat. All three surfaces derive from
it, keeping only what is genuinely local: the landing cards keep their
highlight flag and CTA targets, the portal keeps its checkout behaviour.

Two mechanical details, both found by building rather than by type-checking:

Turbopack does not resolve tsconfig paths pointing outside the app root, so
each app widens turbopack.root to the repository and spells out the @shared
alias. `tsc --noEmit` passed against the shared module while `next build`
failed with "Can't resolve '@shared/plans'"; only a real build proves this.
Vitest does not read tsconfig paths either and needs the same alias.

The portal had no CI at all, which is why its copy drifted unnoticed — Vercel
deploy checks do not type-check. It gains a workflow running tsc, landing gains
a tsc step alongside its tests, and both now trigger on changes to shared/.

Six tests pin the invariants that matter commercially: annual never exceeds
monthly, per-seat plans match the server's list, Business cannot undercut Teams
at the shared seat floor, only Pro's trial skips the credit card, and Business
carries no trial at all.

Feature lists are unified to the fuller landing copy, so the portal now also
shows cross-device sessions on Pro and workspace restore on Free.
@vercel

vercel Bot commented Sep 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
voltius Ready Ready Preview Sep 12, 2026 10:54pm UTC
voltius-portal Ready Ready Preview Sep 12, 2026 10:54pm UTC

portal/pnpm-workspace.yaml still carried the literal placeholder "set this to
true or false" for sharp and unrs-resolver. pnpm 11 treats that as unresolved
and fails the install with ERR_PNPM_IGNORED_BUILDS, so the new portal CI job
could not get past `pnpm install --frozen-lockfile`. The same error silently
aborts `pnpm exec` locally, which is how it stayed invisible: Vercel tolerates
it, and nothing else in CI ever installed this package.

landing/pnpm-workspace.yaml already answers both with true; this matches it.
@kipavy
kipavy merged commit 91329f4 into main Sep 12, 2026
6 checks passed
@kipavy
kipavy deleted the refactor/shared-plans branch September 12, 2026 22:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant