Skip to content

Subscription pricing: free hour, monthly and yearly plans - #144

Open
alpha5611331 wants to merge 17 commits into
mainfrom
feat/subscription-pricing
Open

alpha5611331 wants to merge 17 commits into
mainfrom
feat/subscription-pricing

Conversation

@alpha5611331

@alpha5611331 alpha5611331 commented Oct 13, 2026 •

Copy link
Copy Markdown
Member

Closes #143

Moves the app to subscription pricing (PowerInterviewAI/backend#76): a free hour once on the free-tier model, then a $20 monthly or $96 yearly prepaid pass with unlimited interviews on the SOTA model.

Changes

  • State. AppState gains subscribed, subscriptionExpiresAt and subscriptionPlan. credits stays the free balance: balanceState (src/main/utils/balance.ts) reads free_credits when the backend sends it, because the backend's own credits is, for a subscriber, a compatibility figure for clients older than this one. Against an older backend it falls back to credits and reads as not subscribed.
  • Subscription page (was Buy Credits). Shows the current plan (pass and end date with days left, or free time left) and the Monthly and Yearly cards from /api/payment/subscription-plans, with "Save 60%" computed from the served prices. It goes through the existing crypto checkout and notes that passes never renew. Buying while subscribed says the days are added after the current end.
  • Status everywhere. The title bar, stealth status panel and home strip show "Monthly · Unlimited until Oct 30, 2026", or "Free · 45 mins free". In the last 3 days the title bar shows "Ends in N days" in yellow.
  • Expiry toasts. useSubscriptionExpiryWarning, mounted in MainFrame, toasts at 3, 2 and 1 days left (re-armed when a new pass moves the end date) and once when a subscription seen active this session lapses, each with "See plans". Never for one that lapsed before launch, and never across a sign-out.
  • Start gate. Subscribed, or at least a minute of free time. Subscribers get no low-balance toasts, and the mock setup says "Unlimited with your subscription".
  • Copy (en, ru). The gate, the 4402 out-of-time toast, the home cards and the trial notice point to the subscription. An account with nothing left reads "No free time left", which is also true of a reseller's customer and a lapsed subscriber. Payment history shows days, or credits on older orders.
  • useNow keeps the clock in state, since the React Compiler refuses Date.now() in render.
  • Removed the unused balance fetch (usePayment().getCredits, the payment:get-credits IPC and bridge, PaymentService.getCredits) and the never-called pollPaymentStatus; the ping refreshes the balance every 5 seconds.

Deploy

Ship after the backend, and confirm /api/payment/subscription-plans is in prod openapi.json. Against an older backend, the plan list fails with the plans error and everything else falls back to the credits view. This carries new features, so dispatch the release with minor.

Testing

  • pnpm lint, tsc -p tsconfig.electron.json, tsc -b, vite build, pnpm test:main: all pass.
  • New test/balance.test.mjs (subscriber / free / old backend), plus subscriber cases in credit-gate.test.mjs.
  • The ping, /credits and /subscription-plans fields this client reads match the backend branch's generated OpenAPI schema.
  • End to end against the backend branch (2026-10-14), through the built main-process services under the test harness's Electron stub: a migrated subscriber reads free 0, subscribed, about 13 days left; a free user after a refunded pass reads 300 free and not subscribed; history carries days_granted; plans map to monthly $20 and yearly $96 at $8; sign-in and sign-out clear the balance.
  • The Electron window itself was not driven, so the renderer was checked by tsc, the build and the source-level tests only.

🤖 Generated with Claude Code

alpha5611331 and others added 3 commits October 13, 2026 10:19
…on-plans

AppState gains subscribed, subscriptionExpiresAt and subscriptionPlan.
credits stays the free balance: balanceState reads free_credits when the
backend sends it, since its own credits is, for a subscriber, a
compatibility figure for clients that predate subscriptions. An older
backend's credits is used as before. Plans now come from
/api/payment/subscription-plans, and history rows carry days.

Refs #143

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The payment page becomes Subscription: the current plan (pass and end
date, or free time left) and the monthly and yearly passes through the
existing crypto checkout. The title bar, stealth status panel and home
strip show the plan and end date or the free time left, warning in the
last three days. Subscribers may always start and get no low-balance
toasts; the mock setup says unlimited. Copy for the gate, 4402 and the
trial notice points to the subscription, in English and Russian.

Refs #143

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@gitar-bot

gitar-bot Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

Gitar is working

Verification

Rules No rules evaluated

Functional validation Not enabled

Set up

Auto-approval Not enabled

Set up

Gitar

alpha5611331 and others added 14 commits October 13, 2026 11:52
"Your free hour is used up" was shown to anyone unsubscribed at a zero
balance, including a reseller's customer who never had a free hour and a
subscriber whose pass lapsed. Say "No free time left" instead (en, ru).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
- isPro becomes isPopular: the highlighted card is whichever plan the backend
  marks popular, and no plan is called Pro any more.
- One planNameOf helper replaces a cast repeated at the purchase summary.
- The history column key is purchase, matching its label.
- The balance fetch error says the plan failed to load instead of "credits".

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…accounts

The renderer could write subscribed / subscriptionExpiresAt /
subscriptionPlan through app:update-state, and subscribed overrides the
free balance in the start gate. Sign-in, sign-out and an unanswered first
ping now reset the whole balance slice, so a subscriber's plan no longer
shows for the next account on the machine. Subscribed without an end date
reads as not subscribed, so the gate and the display cannot disagree.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
A refused start said 'No free time left' while the title bar showed time
left. One freeTimeLabel now serves the title bar and the home plan line,
and the Plans tab and refusal copy say less than a minute.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Nothing called them: the renderer's usePayment().getCredits had no
consumer, so the payment:get-credits IPC, its preload bridge and
PaymentService.getCredits were reachable only from each other, and
PaymentService.pollPaymentStatus had no caller at all. The status tab
polls through getStatus, and the ping already refreshes the balance and
subscription every few seconds. The PR had ported the dead path to the
subscription balance; removing it instead, with the two error strings
only it used.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Passes never renew, and the only warning was the title bar turning
"Ends in N days" yellow, which nobody reads until an interview stops at
zero free time. useSubscriptionExpiryWarning, mounted in MainFrame so it
runs on every route, toasts once at each of 3, 2 and 1 days left (keyed
by the expiry, so buying another pass re-arms it) and once when a
subscription this session saw active lapses. A lapse before launch and
the undefined in between two accounts on sign-out never read as one.
Both offer "See plans". SPEC also stops naming /api/payment/credits,
which the client no longer calls.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…nest

Source-level, like interview-lock.test.mjs, because each condition reads
like something to simplify and none breaks a type or a lint: "ended"
fires only on an active-to-lapsed transition (not on launch, not across
sign-out), "ending soon" is keyed by expiry and day count, and the hook
stays mounted in MainFrame so a lapse on any route is announced.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

This branch has not been deployed

No deployments
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.

Subscription pricing: free hour, monthly and yearly plans

1 participant