feat(billing): Automatic top-up toggle in profile settings#1862
Conversation
Adds an 'Automatic top-up' checkbox item to the user-profile dropdown's
settings group, backed by GET/PATCH /api/accounts/{id}/auto-recharge
(recoupable/api#769). Renders the live Stripe-read state — never a cached
local flag — so the toggle always shows the consent the charge path will
honor. Disabling keeps the saved card and manual checkout top-ups working.
TDD: fetch-helper tests RED before implementation; 35 files / 130 tests
pass; eslint + tsc clean on touched files; production build succeeds.
Part of #1861 (merge order: docs#270 → api#769 → this).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
|
Warning Review limit reached
Next review available in: 1 second Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (2)
📒 Files selected for processing (5)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
1 issue found across 7 files
Confidence score: 3/5
- In
hooks/useAutoRecharge.ts, defaultingenabledtotruebefore the Stripe-backed value is loaded can show the auto top-up consent toggle as checked when the real state is still unknown (or failed to load), which risks misleading users about their billing preference—default to a neutral/loading state (orfalse) until live data is confirmed before merging.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="hooks/useAutoRecharge.ts">
<violation number="1" location="hooks/useAutoRecharge.ts:44">
P2: The automatic top-up toggle can appear checked before the live Stripe-read setting has loaded (or if loading fails), because `enabled` falls back to `true` when `query.data` is missing. Since this is a consent-style setting, it would be safer not to render an opt-in state unless the API has actually returned `enabled: true`.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| }); | ||
|
|
||
| return { | ||
| enabled: query.data?.enabled ?? true, |
There was a problem hiding this comment.
P2: The automatic top-up toggle can appear checked before the live Stripe-read setting has loaded (or if loading fails), because enabled falls back to true when query.data is missing. Since this is a consent-style setting, it would be safer not to render an opt-in state unless the API has actually returned enabled: true.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At hooks/useAutoRecharge.ts, line 44:
<comment>The automatic top-up toggle can appear checked before the live Stripe-read setting has loaded (or if loading fails), because `enabled` falls back to `true` when `query.data` is missing. Since this is a consent-style setting, it would be safer not to render an opt-in state unless the API has actually returned `enabled: true`.</comment>
<file context>
@@ -0,0 +1,51 @@
+ });
+
+ return {
+ enabled: query.data?.enabled ?? true,
+ isLoading: query.isLoading,
+ isUpdating: mutation.isPending,
</file context>
|
Correction on the build claim in the PR body: my local |
Preview verification — 2026-07-09Tested on the preview for this PR's head commit (e12b279):
|
| # | Check (issue #1861, chat step) | Expected | Actual | Result |
|---|---|---|---|---|
| 1 | Toggle renders in the profile-dropdown settings group | "Automatic top-up" between Subscribe and Connectors | Renders exactly there as a menuitemcheckbox |
✅ |
| 2 | Initial state is the live Stripe read | Checked (account is opted in) after GET 200 |
GET 200 → checked; while the GET is in flight the item shows the default checked state disabled (isLoading) |
✅ |
| 3 | Toggle off | PATCH {enabled:false} 200; unchecks; no card removal |
PATCH 200; item disabled during flight (isUpdating), then unchecked; dropdown stays open (onSelect preventDefault) |
✅ |
| 4 | State survives reload (live read, not cached) | Fresh GET → unchecked |
Full page reload → GET 200 → aria-checked="false" |
✅ |
| 5 | Toggle back on | PATCH {enabled:true} 200; re-checks; no card re-entry |
PATCH 200 → aria-checked="true"; account left in its original enabled state |
✅ |
| 6 | Network calls match the docs contract | GET/PATCH /api/accounts/{id}/auto-recharge, Privy bearer |
Exactly those calls, no others; account id derived from auth context | ✅ |
Screenshots
ON (initial live state — checked, between Subscribe and Connectors):
OFF (after click — PATCH 200, unchecked, menu still open):
OFF after full reload (persistence via live GET):
Observations (non-blocking)
- Silent failure mode. The mutation has no
onErrorhandling and the query error state isn't surfaced: when the api is unreachable/404 (exactly the current preview default), the toggle shows checked and clicks no-op with zero user feedback. A toast on mutation error (and/or rendering the query error) would make this fail loud. This is also the mitigation if the api is ever down: today an opted-out user would see "on". - The Stripe search-lag wrinkle is well handled for the common case.
onSuccessseeds the query cache from the PATCH response instead of refetching, so the toggle never bounces through the eventually-consistent GET right after a flip — nice. The residual gap (first-ever opt-out by a card-less account reading back "on" for ~1–2.5 min across a reload) remains, per the api#769 findings, but it's cosmetic and safety-unaffected. - Screenshots are hosted on the throwaway branch
assets/pr1862-preview-verification— safe to delete after merge.
With the env caveat above, the PR's code fully satisfies the chat-side Done-when criteria of #1861 against the real (merged) api.
Env caveat resolved — re-verified on the default preview config, 2026-07-09
Re-ran the full toggle flow on this PR's preview with no
The 404/silent-failure behavior from the previous comment is no longer reproducible on previews. The silent-error observation (no |
…account modal Review feedback: the profile dropdown is primarily navigational; a billing- consent control deserves explanation a menu item can't carry. The toggle is now a labeled Switch in a divided Billing section of the Account modal with helper text stating exactly what gets charged ($5 for 500 credits) and that disabling never removes the card. The section is visually separated because it applies instantly via PATCH, unlike the save-to-apply form above it. Also surfaces mutation failures via toast (was silent). useAutoRecharge and the lib/recoup fetchers are unchanged aside from onError; all 130 tests pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
There was a problem hiding this comment.
1 issue found across 5 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="components/Account/BillingSection.tsx">
<violation number="1" location="components/Account/BillingSection.tsx:29">
P2: The automatic top-up switch can be interactive before the account has finished loading, because `disabled` only checks the query/mutation states. Since `useAutoRecharge` skips the query until `accountId` exists but still lets the mutation call `updateAutoRechargeSetting(accountId as string, ...)`, an early toggle can PATCH `/api/accounts/undefined/auto-recharge` and show a failed update. Consider disabling the switch until the hook has a real account id, or have the hook expose an `isReady`/`canUpdate` state and guard the mutation.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| <Switch | ||
| id="auto-topup" | ||
| checked={enabled} | ||
| disabled={isLoading || isUpdating} |
There was a problem hiding this comment.
P2: The automatic top-up switch can be interactive before the account has finished loading, because disabled only checks the query/mutation states. Since useAutoRecharge skips the query until accountId exists but still lets the mutation call updateAutoRechargeSetting(accountId as string, ...), an early toggle can PATCH /api/accounts/undefined/auto-recharge and show a failed update. Consider disabling the switch until the hook has a real account id, or have the hook expose an isReady/canUpdate state and guard the mutation.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At components/Account/BillingSection.tsx, line 29:
<comment>The automatic top-up switch can be interactive before the account has finished loading, because `disabled` only checks the query/mutation states. Since `useAutoRecharge` skips the query until `accountId` exists but still lets the mutation call `updateAutoRechargeSetting(accountId as string, ...)`, an early toggle can PATCH `/api/accounts/undefined/auto-recharge` and show a failed update. Consider disabling the switch until the hook has a real account id, or have the hook expose an `isReady`/`canUpdate` state and guard the mutation.</comment>
<file context>
@@ -0,0 +1,38 @@
+ <Switch
+ id="auto-topup"
+ checked={enabled}
+ disabled={isLoading || isUpdating}
+ onCheckedChange={setEnabled}
+ className="mt-0.5"
</file context>
Redesign: toggle moved to a Billing section in the Account modal — verified on preview, 2026-07-09Per review discussion (Sweets): the profile dropdown is primarily navigational, and a billing-consent control deserves explanation a bare menu item can't carry — this feature exists because of a surprise-charge incident, so the setting should say what it does. Commit 4509968: the dropdown checkbox is gone; the toggle is now a labeled Switch in a divided Billing section of the Account (Profile) modal with helper text: "When your credits run out, automatically charge your saved card $5 for 500 credits. Turning this off never removes your card — you can still top up manually anytime." The section sits apart from the form fields because it applies instantly via PATCH, not on Save. Mutation failures now surface as a toast (
Verified on the preview for 4509968 (default config,
|
| Check | Actual | Result |
|---|---|---|
| Dropdown menu no longer contains the toggle | "Automatic top-up" absent; Subscribe → Connectors adjacency restored | ✅ |
| Billing section renders in the Profile modal | Divider + "Billing" header + labeled switch + helper text, above Save Changes | ✅ |
| Initial state = live GET | GET 200 → switch data-state="checked" |
✅ |
| Toggle off | PATCH 200 → unchecked |
✅ |
| Full reload → reopen modal | fresh GET 200 → still unchecked |
✅ |
| Toggle back on (restore) | PATCH 200 → checked; account left enabled: true |
✅ |
ON:
OFF (after click — PATCH 200):






Summary
Chat slice of #1861 — a user-facing kill switch for automatic credit top-up, so opting out of off-session charges is a setting instead of a support ticket (or removing your card).
BillingSection— a divided Billing section in the Account (Profile) modal: labeled Switch + helper text stating exactly what gets charged ($5 for 500 credits) and that disabling never removes the card. Applies instantly via PATCH (visually separated from the save-to-apply form); mutation failures surface as a toast. (Supersedes the originalAutoTopUpMenuItemdropdown checkbox — moved per review 2026-07-09: a billing-consent control needs explanation a menu item can't carry; see the redesign comment for screenshots.)useAutoRecharge— react-query hook (query + mutation) following theuseCreditspattern: Privy bearer from the provider, account fromuseUserProvider, cache updated from the PATCH response.lib/recoup/getAutoRechargeSetting/updateAutoRechargeSetting— fetch helpers mirroringgetAccountCredits, hittingGET/PATCH /api/accounts/{id}/auto-recharge(api#769, contract in docs#270).The toggle renders the live Stripe-read state from the api (the setting lives on the Stripe customer record) — never a cached local flag — per the #1861 design decision.
Merge order
docs#270 → api#769 → this. The toggle 404s against an api without #769, so this merges last.
Verification
eslint+tsc --noEmitclean on all touched files (next lintitself is broken in Next 16 — build is the gate).pnpm buildcompiles successfully, 43/43 static pages generated.🤖 Generated with Claude Code
Summary by cubic
Adds an Automatic top-up toggle in the Account modal’s Billing section so users can opt out of off-session credit top-ups (Linear #1861). The switch reads and updates the live Stripe-backed setting via
GET/PATCH /api/accounts/{id}/auto-rechargeso the UI matches actual charge consent.New Features
Switchwith helper text ($5 for 500 credits; disabling doesn’t remove the card); applies instantly on flip.useAutoRecharge(@tanstack/react-query) loads/updates with a Privy bearer, updates cache from the PATCH response, and shows a toast on failure.Dependencies
api#769(contract indocs#270); merge order:docs#270 → api#769 → this.Written for commit 4509968. Summary will update on new commits.