Repository navigation
STU-382: Build the plans surface and paywall states on existing subscription contracts - #90
Conversation
…sting subscription contracts - Unify Free/Pro/Power tier labels + limits across topbar, account badge, plans page, usage dialog and generation gating via a single src/plansSurface.js - Reconcile the plan feature matrix to CURRENT supported capabilities: drop unsupported 'API & MCP Access' and 'early access' prototype promises, keep BYOK (Configure API Keys), Code Regeneration, Code Review - Defer the 'plan active' confirmation until the get-subscription contract confirms a paid tier; a checkout return alone never claims subscribed - Add plans/paywall e2e coverage (port 3092) and plansSurface unit tests Generated-by: Claude
… catch a hidden mock The design's mock success copy lived in a hidden modal, so a body.innerText scan could not catch its reintroduction. Assert on page.content() so an inert/hidden mock is caught. Falsified: injecting the mock reddened the test; restored: green. Generated-by: Claude
|
Product decisions from the owner (2026-09-22), recorded so they are not re-litigated:
A follow-up pass is also addressing the adversarial review findings. |
|
Correction to the product decision above (owner, 2026-09-22). API & MCP Access IS a shipped capability. It is not self-serve — the owner's words: “it's actually shipped. You've basically got to get in contact with us to learn more.” So the restored row must not be worded as unavailable or "coming soon". It should present the capability as real and available, with the access path stated honestly: contact-us / talk-to-us rather than self-serve signup. The "All models + early access" row should likewise be presented as available. The general criterion still applies — no row may promise something the product cannot deliver — but for these two rows the product can deliver, so the truthful framing is "available, get in touch", not "not yet available". Nothing else in the review findings changes. |
…ore owner rows
Addresses the four review findings on the plans/paywall surface:
1. [BLOCKING] Make the plans-card price assertions hermetic. C5 asserted
A\$11/A\$49 while formatPrice converts whenever the detected currency is
not AUD, so CI (UTC) rendered \$7.15/\$31.85 from the same code that a
Sydney machine renders A\$11/A\$49 from. Pin the suite's timezone/locale
to Australia/Sydney/en-AU so the verdict no longer depends on the host;
the now-failed-in-CI assertion passes deterministically in any timezone.
2. [major] Wire PLAN_FEATURES to the UI instead of leaving it a dead
constant. app.js now renders every .pm-card's feature list (both the
plans page and the pricing modal) from src/plansSurface.js PLAN_FEATURES,
and index.html's duplicated hardcoded row copies are removed. The unit
and browser tests now assert the constant that actually drives the DOM,
and the pricing modal's unsupported-promise path is covered, not just the
plans page.
3. [minor] Make the "verified return refreshes allowance" test title honest:
assert #topbar-credits-count shows the refreshed allowance (50-12=38)
rather than only claiming it.
4. [PRODUCT DECISION] Restore the "API & MCP access" and "All models + early
access" rows the owner asked to keep, in both rendered surfaces, marked
clearly not-yet-available ("(coming soon)") so nothing unavailable reads
as available. Pro/Power remain the canonical labels (the divergent
"Professional"/"Power Developer" display map is gone).
The previous revision restored the owner-requested rows but marked them unavailable, on the stated reasoning that "the product exposes no user-facing API/MCP surface". That inference was drawn from the codebase and is wrong: both capabilities ship. API & MCP access is reached by contacting us rather than through a self-serve signup. - API & MCP access now renders as "API & MCP access (contact us)" — the capability is presented as available and names its access path. - "All models + early access" is no longer suffixed. - Replace UNSUPPORTED_PROMISES/UNAVAILABLE_MARKER with CONTACT_TO_ACCESS_MARKER. The two rows are now entries in the supported-capability whitelist, so the criterion-5 guard rejects a not-yet-available reading rather than requiring it. - Tests assert the rows are NOT marked unavailable. Falsified: reintroducing "(coming soon)" fails 1 unit test and the C5 browser test; restoring passes 296/296 and 9/9. Full CI sequence green: npm test 296, build, 54 browser, 7 accessibility modals, 9/9 deploy guard, 30/30 proxies, leak scan clean.
9dcb8ce
into
milestone/stu-implement-updated-ui-ux
| confirmCheckoutAfterReconcile(); | ||
| updateSubscriptionUI(); | ||
| updatePricingDisplay(); | ||
| renderPlanFeatureLists(); |
There was a problem hiding this comment.
🟡 Plan features disappear during startup
When startup requests stall, renderPlanFeatureLists has not run and every plan card shows an empty feature list. The previous static rows remained visible during the same delay.
Learn more
The feature rows now exist only in JavaScript. This call runs after initializeAuth() and fetchSubscription(), which each perform network requests without a request timeout. The document remains interactive while the async DOMContentLoaded callback waits, so users can open the pricing modal and see cards with no features.
Example: A session refresh takes 20 seconds. During those 20 seconds, opening the pricing modal displays Free, Pro, and Power cards with empty feature lists instead of their plan capabilities.
Recommended fix: Call renderPlanFeatureLists() before the first startup await, since its data is local and independent of authentication. Alternatively, retain static HTML rows as the initial fallback and let the renderer replace them later.
Was this helpful? React with 👍 or 👎 to provide feedback.
| power: Object.freeze([ | ||
| "2000 generations/month", | ||
| "All models", | ||
| "Code Regeneration", | ||
| "Bring your own key (BYOK)", | ||
| "Code Review", | ||
| "All models + early access", | ||
| `API & MCP access ${CONTACT_TO_ACCESS_MARKER}`, |
There was a problem hiding this comment.
| // The feature list also encodes criterion 5: a row must EITHER map to a CURRENT | ||
| // supported capability of the product, OR — when the owner has asked the row be | ||
| // kept (the "API & MCP access" and "All models + early access" rows) — be | ||
| // presented as CLEARLY not-yet-available rather than promised. Anything not | ||
| // live yet is suffixed "(coming soon)" so it can never read as a capability the | ||
| // app delivers today. This is the single source both the plans page and the | ||
| // pricing modal render from, so the two surfaces cannot drift. BYOK is real (the | ||
| // "Configure API Keys" surface) and remains listed. |
There was a problem hiding this comment.
Summary
STU-382 — plans surface and paywall states backed by EXISTING subscription contracts and real tier limits, not the hero prototype's mock A$29 or a simulated purchase success.
The master tip already carried most of the plans surface. This PR closes the genuine gaps and pins behavior with falsifiable tests:
Professional/Power Developerwhile topbar, plans cards, usage dialog and gating saidPro/Power. A single sourcesrc/plansSurface.jsnow drives all of them (Free/Pro/Power, limits 2/50/2000) andTIER_LIMITSin app.js is derived from it.?checkout=successreturn param. Now the confirmation is deferred until theget-subscriptioncontract reconciles to a paid tier (confirmCheckoutAfterReconcile); a blocked/lagging/cancelled return shows an honest "confirming your subscription" message and the tier stays whatever the contract says. Used real A$11/A$49 base prices, existingformatPrice/detectUserCurrency; paid downgrades/cancellation still go through the billing portal.Acceptance-criteria checklist
currency=AUD; USD (America/New_York) displays $7.15 (11×0.65 fixture rate) and postscurrency=USD.?checkout=successreturn whose contract is still Free also renders Free (return alone grants nothing); a verified return (contract→professional) renders Pro and unlocks Pro models./stripe/create-checkout-session-intl(real Stripe path) without locally writing a paid subscription.Tests + results
npm test→ 296 pass / 0 fail (290 baseline + 6 newsrc/plansSurface.test.js). Build:npm run buildpasses.e2e/stu382-plans-paywall.spec.js).Falsification evidence (break → FAIL → restore → PASS)
professionallabel to "Professional": unit testplanLabel('professional')==='Pro'FAILED, e2e account badge "Pro" FAILED; restored → PASS.?checkout=success: the "return alone never grants a paid tier" e2e FAILED (asserted honest "confirming" copy / 0 "Subscription active"); restored → PASS.currency="EUR"while AUD is displayed: AUD e2e assertedpostedCurrency==="AUD"FAILED; restored → PASS.Screenshots (changed surface)
screenshots/stu382-plans-professional-1440x900.png— plans page for a professional contract (current-plan indicator, A$ prices, honest feature rows).screenshots/stu382-plans-guest-360x800.png— plans page at mobile width, signed-out.What is verified vs NOT
BASE_PRICES_AUD, matching the checkout'stierId→STRIPE_PRICE_IDSrequest, but no real Stripe session was created. Screenshots are from the fixture-rendered app, not a real user account.Linear
https://linear.app/stuart-gardoll/issue/STU-382/build-the-plans-surface-and-paywall-states-on-existing-subscription