2 Commits

Author SHA1 Message Date
isra el
0bd5d4c900 fix(billing): send the plan name from route params under Next 16
/checkout/pro and /checkout/scale failed with "Plan cannot be purchased"
for every user since the Next 16 upgrade (ea1b00c).

The checkout page still typed `params` as a plain object and read
`params.planName` synchronously. Next 16 removed the sync compat shim, so
the value was undefined, axios dropped the key, and the API ran
findOne({ name: undefined }). Mongoose 9 returns null for that where
older versions stripped the key, so the guard threw a message about a
misconfigured plan for a request that never named one. Plan data and
Polar product ids were correct throughout.

- unwrap params with React's use() and drop the `as string` cast that
  suppressed the type error
- split the conflated guard into PLAN_NAME_REQUIRED, PLAN_NOT_FOUND and
  the existing unpurchasable case, each logged server side
- stop retrying a rejected checkout twice: a 400 is not transient
- mark the Polar product id indexes sparse, they have been failing to
  build with E11000 on every boot because most plans leave them unset

Checkout now defaults to yearly. A URL that names an interval still
redirects straight through, so the marketing funnel is unchanged; the
in-app CTAs that name none get an interval chooser with yearly
preselected instead of a silent monthly default.

Also redesigns the checkout states, which were the only place in the
dashboard hardcoding gray-100/white and so rendering a light slab in dark
mode, and the plan picker, which painted the accent on every tier.

Tests: a checkout e2e spec asserting the request body carries planName,
which is what would have caught this, plus API cases pinning the three
guard failures apart.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 21:47:17 +03:00
isra el
5d43866847 fix: show every plan in onboarding and let finished steps be reopened
The plan step hardcoded Free and Pro inline, so Scale never appeared no
matter what the pricing page offered. Tiers now come from lib/plans,
which mirrors the marketing pricing section, with a test pinning the
values so the two cannot drift silently.

Kept static rather than fetched from /billing/plans. That endpoint
returns Plan documents (limits and cents), not customer-facing copy, and
an environment with no plans rows left the step showing "plans could not
be loaded" with nothing to choose.

Reopening a completed step showed an empty row. Two things caused it:
the body was gated on the step not being done, and an effect cleared the
selection whenever the selected step was done, so clicking a finished
step deselected it on the next render. That effect exists to advance you
when the step you are sitting on completes underneath you, so it now
fires only on that transition. Both paths are covered: reopening the API
key step offers "Generate another API key", and a step completing while
selected still moves the selection on.

Skip is hidden on an already-finished step, where it would mean nothing.

Also corrects the Plan type, which declared amount, currency and
recurringInterval. The plans endpoint has never sent those; they belong
to Subscription. Anything reading plan.amount saw undefined and rendered
a paid plan as free. The fixture encoded the same wrong shape, so tests
would have passed while production broke.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 02:48:12 +03:00