Skip to content

fix(lists): tell capped users they are capped, on every creation path - #224

Merged
brianorwhatever merged 1 commit into
mainfrom
fix/plan-limit-messaging
Aug 21, 2026
Merged

fix(lists): tell capped users they are capped, on every creation path#224
brianorwhatever merged 1 commit into
mainfrom
fix/plan-limit-messaging

Conversation

@brianorwhatever

Copy link
Copy Markdown
Contributor

A user reported not being able to create a list. The quota is working correctly — they hold 5 lists with no subscription, so assertListQuota throws PLAN_LIMIT before the insert. The bug is what they were told about it.

Only one of four paths explained the problem

Path What a capped user saw
CreateListModal ✅ Upgrade prompt with referral CTA and pricing link
TemplatePickerModal (built-in + saved) ❌ "Failed to create list. Please try again."
Templates.tsx Nothing at all
OnboardingFlow ❌ "Couldn't create list. Try again."

Whether a user learned they could upgrade depended on which button they pressed.

"Please try again" is the worst part: retrying at the cap fails identically every time, so it sends someone into a loop that cannot succeed. Templates.tsx was worse still — both catch blocks logged to console, cleared the spinner and returned, so the button just stopped working with no on-screen explanation.

The fix

src/lib/planLimit.ts becomes the single place that knows what the quota error looks like, and every creation path routes through it:

  • isPlanLimitError(err) — matches on substring, not prefix. Convex wraps mutation errors ([CONVEX M(lists:createList)] Uncaught Error: PLAN_LIMIT: …), so the marker never arrives at position 0. There's a test for the wrapped form specifically.
  • listCreationErrorMessage(err, fallback) — the actionable cap message, or a generic fallback for genuinely unexpected failures.

Templates.tsx gains an error surface it never had, with a link to pricing.

Home.tsx is deliberately untouched: it creates the demo list for brand-new users, who are at zero lists and cannot hit the cap.

Tests

Four cover the helper — the real server string, the Convex-wrapped form, non-quota errors not being misread, and the cap message never saying "try again".

The fifth is the one that matters: it asserts every list-creating component recognises the quota. The defect was never in the logic, it was in four call sites not asking — so the regression test has to check coverage, not behaviour. Confirmed non-vacuous: it fails on main.

169 tests pass; tsc -b clean; the 7 remaining eslint errors in these files are pre-existing (8 before this change).

Not addressed here

Whether 5 is the right cap, and whether raedugas@gmail.com should be granted headroom — bonusLists or referralProUntil are the existing levers, both writes to production user data.

🤖 Generated with Claude Code

A user at the free-plan limit reported not being able to create a list. The
quota is working as intended — they hold 5 lists with no subscription, so
assertListQuota throws PLAN_LIMIT before the insert. The bug is what they were
told about it.

Only CreateListModal recognised PLAN_LIMIT and showed the upgrade prompt. The
other paths did not:

  - TemplatePickerModal (both built-in and saved templates) showed
    "Failed to create list. Please try again." — advice that can never work,
    since retrying at the cap fails identically every time.
  - Templates.tsx showed NOTHING. Both catch blocks logged to console, cleared
    the spinner and returned, so the button simply stopped responding with no
    explanation on screen.
  - OnboardingFlow showed "Couldn't create list. Try again."

So whether a capped user learned they could upgrade depended entirely on which
button they pressed.

Adds src/lib/planLimit.ts as the single place that knows what the quota error
looks like, and routes every creation path through it. Templates.tsx gains an
error surface it never had, with a link to pricing. Home.tsx is deliberately
untouched: it creates the demo list for brand-new users, who are at zero lists
and cannot hit the cap.

The detector matches on substring, not prefix — Convex wraps mutation errors,
so the marker never arrives at position 0. There is a test for the wrapped form
specifically.

The last test is the one that matters: it asserts every list-creating component
recognises the quota, since the defect was never in the logic but in four call
sites not asking. Confirmed non-vacuous — it fails on main.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@brianorwhatever
brianorwhatever merged commit ab39037 into main Aug 21, 2026
7 checks passed
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