feat: one-time $9 30-Day Pro Pass - #809
Merged
Merged
Conversation
Adds a never-renewing, one-time-payment pass granting 30 days of full Pro access, aimed at episodic users who won't subscribe. Priced above monthly ($6) so it can't undercut subscriptions. Backend (no webhooks, no DB — matches the live-Stripe-lookup pattern): - create-checkout-session accepts plan "pass": mode "payment", PaymentIntent metadata ff_pass, invoice_creation on, attaches to the existing Stripe customer by email so a new pass-customer can't shadow subscription history - customer-info also lists PaymentIntents and returns an active pass (latest valid expiry; refunds and disputes invalidate) via _lib/_pass - AI endpoints accept a pass PaymentIntent id as the bearer token; bogus tokens now fall through to the free tier instead of throwing Frontend: - useHasProAccess/useIsProUser honor an active pass; useProAiToken sends the sub id only when active/trialing (a lapsed subscriber with a pass previously would have sent the dead sub id) - Pricing page ticket below the plan comparison, checkout callout below the trust box, FAQ answer, paywall + sandbox-warning secondary links - Pass-specific /success variant + new_pass_holder Loops event - Account shows a pass section; subscription-only sections gated on subscription presence - FAQ strings converted to lazy t`` arrows (locale-timing fix) Tests: api jest suite for the pass predicate; app hook/component tests with new customer fixtures; e2e pass.spec.ts (CI-safe redirect checks + full-purchase block gated on a dedicated TESTING_EMAIL_PASS account). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
A $9 one-time, 30-day Pro Pass that never auto-renews — aimed at episodic users with a singular immediate need who won't subscribe. Priced above monthly ($6) so it structurally can't undercut subscriptions; anyone needing Pro ~6+ months/year is still better off on yearly ($48).
Design/research context: proposal artifact + PostHog analysis from the planning session (subs ~7/mo mid-2026, ~90% of signed-in users never return after week one — task-shaped usage the pass monetizes).
How it works
No webhooks, no DB — matches the existing live-Stripe-lookup pattern and fails closed (worst case: paying user temporarily unrecognized; never: user charged unexpectedly).
create-checkout-sessionacceptsplan: "pass"→mode: "payment", PaymentIntentmetadata.ff_pass,invoice_creationon. Attaches to the existing Stripe customer by email (newest-first, mirroringgetCustomerFromToken) so a fresh pass-customer can't shadow subscription history; falls back tocustomer_email+customer_creation: "always".customer-infolists PaymentIntents alongside subscriptions (parallel) and returnspass: { expiresAt, paymentIntentId } | nullviaapi/_lib/_pass.ts: valid = succeeded + metadata + charge not refunded and not disputed; latest valid expiry wins (a refunded newer pass can't kill a valid older one).pi_…bearer token symmetric to the existingsub_…scheme;pi.customeris guarded so a null can never mint a sharedpro_nullrate-limit key. Bogus tokens now fall through to the free tier instead of throwing.useHasProAccess()/useIsProUser()honor the pass — every feature gate flows through them. Post-expiry, hosted charts go read-only (EditHosted'suseHasProAccessgate); nothing is deleted.useProAiToken()sends the subscription id only when active/trialing.customer-inforeturns the newest sub regardless of status, so a lapsed subscriber holding a pass would otherwise send a deadsub_…id and get anonymous AI limits — and that user is the pass's core persona.UI
Pass is a callout below the normal options, never a third plan column:
/pricing— ticket strip under the Free/Pro comparison (#passanchor)tarrows, fixing a pre-existing locale-timing bug)clicked-paywall-buttonnow carriestarget: "plans" | "pass"/success?pass=truevariant +new_pass_holderLoops eventTesting
pnpm -F api test: 12 new predicate tests (refund/dispute/expiry/latest-valid-expiry)pnpm -F app test: new customer-state mocking viaAppContextinjection + fixtures (fakePassCustomer,fakeLapsedSubWithPassCustomer); hook, Checkout, and Account tests — 23 suites / 322 tests greenapp/e2e/pass.spec.ts: CI-safe specs (ticket visible; free user reaches Stripe checkout showing $9; subscriber sees no pass CTA) — verified green locally against vercel dev + test-mode Stripe. Full test-card purchase spec is gated on a dedicatedTESTING_EMAIL_PASSaccount (setup steps in the spec comment) and skips when unset.success_urlcorrectEnv / release notes
STRIPE_PRICE_ID_PASSset in Vercel development + preview (test priceprice_1TvpkNK9wlDh9GkwZPy4nYfK, productprod_Uvh81iwjCsapw0viastripe-scripts/create-pass-price.mjs dev). Production price is NOT created yet — runcreate-pass-price.mjs prod+vercel env add STRIPE_PRICE_ID_PASS productionbefore the release merge.EditWithAI.tsxis imported nowhere (dead code) and its request shape doesn't match/api/prompt/edit— left untouched, worth a separate cleanup.🤖 Generated with Claude Code