Skip to content

feat: one-time $9 30-Day Pro Pass - #809

Merged
rob-gordon merged 1 commit into
devfrom
robgordon/ff-30-day-pass
Jul 22, 2026
Merged

feat: one-time $9 30-Day Pro Pass#809
rob-gordon merged 1 commit into
devfrom
robgordon/ff-30-day-pass

Conversation

@rob-gordon

Copy link
Copy Markdown
Member

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-session accepts plan: "pass"mode: "payment", PaymentIntent metadata.ff_pass, invoice_creation on. Attaches to the existing Stripe customer by email (newest-first, mirroring getCustomerFromToken) so a fresh pass-customer can't shadow subscription history; falls back to customer_email + customer_creation: "always".
  • customer-info lists PaymentIntents alongside subscriptions (parallel) and returns pass: { expiresAt, paymentIntentId } | null via api/_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).
  • AI endpoints accept a pi_… bearer token symmetric to the existing sub_… scheme; pi.customer is guarded so a null can never mint a shared pro_null rate-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's useHasProAccess gate); nothing is deleted.
  • Fixed en route: useProAiToken() sends the subscription id only when active/trialing. customer-info returns the newest sub regardless of status, so a lapsed subscriber holding a pass would otherwise send a dead sub_… 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:

  1. /pricing — ticket strip under the Free/Pro comparison (#pass anchor)
  2. Checkout — dashed callout under the trust box; pass holders instead see "active until {date}" and can still subscribe mid-pass (deliberate: no double-billing risk, and that's when they're most convertible)
  3. FAQ — "What if I just need it for one project?" now leads with the pass (+ all FAQ strings converted to lazy t arrows, fixing a pre-existing locale-timing bug)
  4. PaywallModal (all trigger sites at once) + SandboxWarning — secondary text link, clicked-paywall-button now carries target: "plans" | "pass"
  5. /success?pass=true variant + new_pass_holder Loops event
  6. Account — pass section (status/expiry); Customer Portal & History gated on subscription presence

Testing

  • pnpm -F api test: 12 new predicate tests (refund/dispute/expiry/latest-valid-expiry)
  • pnpm -F app test: new customer-state mocking via AppContext injection + fixtures (fakePassCustomer, fakeLapsedSubWithPassCustomer); hook, Checkout, and Account tests — 23 suites / 322 tests green
  • app/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 dedicated TESTING_EMAIL_PASS account (setup steps in the spec comment) and skips when unset.
  • Backend smoke-tested live: pass session = payment mode, $9.00, invoice on, email locked, success_url correct

Env / release notes

  • STRIPE_PRICE_ID_PASS set in Vercel development + preview (test price price_1TvpkNK9wlDh9GkwZPy4nYfK, product prod_Uvh81iwjCsapw0 via stripe-scripts/create-pass-price.mjs dev). Production price is NOT created yet — run create-pass-price.mjs prod + vercel env add STRIPE_PRICE_ID_PASS production before the release merge.
  • Never enable promo codes on the pass (a 100%-off session creates no PaymentIntent → no entitlement).
  • Noticed while in here: EditWithAI.tsx is imported nowhere (dead code) and its request shape doesn't match /api/prompt/edit — left untouched, worth a separate cleanup.

🤖 Generated with Claude Code

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>
@vercel

vercel Bot commented Jul 22, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
flowchart-fun Ready Ready Preview, Comment Jul 22, 2026 2:56am

Request Review

@rob-gordon
rob-gordon marked this pull request as ready for review July 22, 2026 03:06
@rob-gordon
rob-gordon added this pull request to the merge queue Jul 22, 2026
Merged via the queue into dev with commit afe5107 Jul 22, 2026
5 checks passed
@rob-gordon rob-gordon mentioned this pull request Jul 22, 2026
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