Skip to content

feat(billing): Automatic top-up toggle in profile settings#1862

Merged
sweetmantech merged 2 commits into
mainfrom
feat/1861-auto-topup-toggle
Jul 9, 2026
Merged

feat(billing): Automatic top-up toggle in profile settings#1862
sweetmantech merged 2 commits into
mainfrom
feat/1861-auto-topup-toggle

Conversation

@sweetmantech

@sweetmantech sweetmantech commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

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 original AutoTopUpMenuItem dropdown 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 the useCredits pattern: Privy bearer from the provider, account from useUserProvider, cache updated from the PATCH response.
  • lib/recoup/getAutoRechargeSetting / updateAutoRechargeSetting — fetch helpers mirroring getAccountCredits, hitting GET/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

  • TDD: fetch-helper tests were RED (module not found) before implementation, GREEN after — 35 files / 130 tests pass.
  • eslint + tsc --noEmit clean on all touched files (next lint itself is broken in Next 16 — build is the gate).
  • pnpm build compiles successfully, 43/43 static pages generated.
  • Live end-to-end toggle verification happens against the api preview (results will be posted on api#769).

🤖 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-recharge so the UI matches actual charge consent.

  • New Features

    • Labeled “Automatic top-up” Switch with 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

    • Requires api#769 (contract in docs#270); merge order: docs#270 → api#769 → this.

Written for commit 4509968. Summary will update on new commits.

Review in cubic

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

vercel Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

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

Project Deployment Actions Updated (UTC)
chat Ready Ready Preview Jul 9, 2026 8:55pm

Request Review

@cursor

cursor Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

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.

@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@sweetmantech, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 1 second

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0fcf5110-bfe4-4ced-8e31-1edc9bdb6080

📥 Commits

Reviewing files that changed from the base of the PR and between 8474780 and 4509968.

⛔ Files ignored due to path filters (2)
  • lib/recoup/__tests__/getAutoRechargeSetting.test.ts is excluded by !**/*.test.* and included by lib/**
  • lib/recoup/__tests__/updateAutoRechargeSetting.test.ts is excluded by !**/*.test.* and included by lib/**
📒 Files selected for processing (5)
  • components/Account/Account.tsx
  • components/Account/BillingSection.tsx
  • hooks/useAutoRecharge.ts
  • lib/recoup/getAutoRechargeSetting.ts
  • lib/recoup/updateAutoRechargeSetting.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/1861-auto-topup-toggle

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 7 files

Confidence score: 3/5

  • In hooks/useAutoRecharge.ts, defaulting enabled to true before 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 (or false) 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

Comment thread hooks/useAutoRecharge.ts
});

return {
enabled: query.data?.enabled ?? true,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

@sweetmantech

Copy link
Copy Markdown
Collaborator Author

Correction on the build claim in the PR body: my local pnpm build in the worktree fails at static-page generation with Error: supabaseKey is required — the worktree has no .env.local (untracked), so prerender steps that construct the Supabase client die locally. This is environmental, not a defect in the change: the Vercel build of this PR passes (see the green Vercel – chat check, deployment completed), as do unit tests (35 files / 130 tests), CodeRabbit, and cubic. Treat the Vercel build as the gate; the PR-body sentence "production build compiles successfully" was based on the compile phase completing and should be read with this caveat.

@sweetmantech

Copy link
Copy Markdown
Collaborator Author

Preview verification — 2026-07-09

Tested on the preview for this PR's head commit (e12b279): chat-git-feat-1861-auto-topup-toggle-recoup.vercel.app (deployment confirmed built from e12b279 via the GitHub deployments API). Logged in as the #1861 test account (2d3f7de4-…, voicefirsttech@gmail.com) — the account with a saved card and known Stripe opt-out state from the api#769 verification. Driven with Chrome DevTools; every state assertion below was read from the a11y tree / network log, not eyeballed.

⚠️ Found first: the toggle is dead on previews as-deployed (env, not this PR's code)

Non-production chat builds point NEW_API_BASE_URL at https://test-recoup-api.vercel.app (lib/consts.ts L4-6) — the api test-branch deployment, which predates today's api#769 merge. On the untouched preview every GET/PATCH …/auto-recharge returned 404 (observed: 1 GET + 3 react-query retries, then a click fired a PATCH → 404), and the failure is silent: the toggle renders checked (enabled ?? true fallback in useAutoRecharge) and clicks visibly do nothing. Production is unaffected — prod chat points at api.recoupable.dev, where I verified the endpoint live today (401/400 contract probes after the merge).

Action needed (not a code change in this PR): sync/redeploy test-recoup-api from api main before relying on chat previews for this feature — the test-branch PR flow was retired (api#750) and that deployment has drifted behind main.

To verify the PR's actual code against the real endpoint, I used the app's built-in recoup_api_override sessionStorage escape hatch pointed at the api#769 head preview (api-nkzuglix4-recoup.vercel.app, same merged code, accepts preview-app Privy tokens). Everything below ran against that.

Results

# 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):

Toggle on

OFF (after click — PATCH 200, unchecked, menu still open):

Toggle off

OFF after full reload (persistence via live GET):

Toggle off after reload

Observations (non-blocking)

  • Silent failure mode. The mutation has no onError handling 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. onSuccess seeds 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.

@sweetmantech

Copy link
Copy Markdown
Collaborator Author

Env caveat resolved — re-verified on the default preview config, 2026-07-09

test-recoup-api.vercel.app has been synced with api main (merge commit 5398a90a on test, following the branch's established "Merge origin/main into test (sync: …)" pattern; merged tree is byte-identical to main — 0 files differ). The endpoint went live on the test deployment ~75s after push.

Re-ran the full toggle flow on this PR's preview with no recoup_api_override — the exact config every future preview user gets:

Check Actual (default config, test-recoup-api) Result
Initial GET 200, toggle checked (live state)
Toggle off PATCH 200 → aria-checked="false"
Full reload fresh GET 200 → still unchecked
Toggle on (restore) PATCH 200 → aria-checked="true"

Toggle off on default config

The 404/silent-failure behavior from the previous comment is no longer reproducible on previews. The silent-error observation (no onError surfacing on the mutation) still stands as a non-blocking hardening suggestion. Account left in its original state (enabled: true). All chat-side Done-when criteria of #1861 now verified on the unmodified preview.

…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>
@cursor

cursor Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

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.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

@sweetmantech

Copy link
Copy Markdown
Collaborator Author

Redesign: toggle moved to a Billing section in the Account modal — verified on preview, 2026-07-09

Per 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 (useAutoRecharge onError) instead of failing silently.

useAutoRecharge and the lib/recoup fetchers are otherwise unchanged; all 130 tests pass, eslint + tsc clean on the changed files.

Verified on the preview for 4509968 (default config, test-recoup-api)

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:

Billing section, toggle on

OFF (after click — PATCH 200):

Billing section, toggle off

@sweetmantech sweetmantech merged commit 3dccdae into main Jul 9, 2026
4 checks passed
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