feat(frontend): settings v2 billing page (subscription + automation credits) - #12942
Conversation
…on credits tabs Add the billing surface for the Settings V2 layout (SECRT-2272) with two tabs: Subscription (plan, payment method, invoices, autopilot usage) and Automation Credits (balance, auto top-up, usage, transaction history). Backend: - New GET /credits/invoices endpoint returning recent Stripe invoices for the current user, including hosted_invoice_url and invoice_pdf_url so the UI can link to Stripe-hosted views and PDF downloads. - InvoiceListItem model on UserCreditBase; DisabledUserCredit returns [] so the UI degrades gracefully when credits are disabled. Frontend: - Billing page rebuilt around TabsLine with Subscription / Automation Credits. - Per-feature card folders (Component.tsx + useComponent.ts) under components/SubscriptionTab and components/AutomationCreditsTab, following the project's "ComponentName/ComponentName.tsx + useComponentName.ts" rule. - Shared helpers (formatCents, formatShortDate, EASE_OUT, formatRelativeReset) in billing/helpers.ts. - TabsLineList: optional flush prop to remove first-tab left padding so the tab row aligns with the page heading. - placeholder-pages test split: Billing now asserts the real heading + tabs, OAuth Apps keeps the "coming soon" assertion. openapi.json regenerated to include the new /credits/invoices operation.
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughAdds a secured backend route to list Stripe invoices and related model types; expands the frontend billing UI with Subscription and Automation Credits tabs, multiple new cards/hooks (balance, usage, invoices, transactions, auto-refill, payment portal, plan), helper utilities, OpenAPI updates, and test adjustments. Changes
Sequence Diagram(s)sequenceDiagram
participant Client as Client (Browser)
participant API as Backend API
participant Auth as Auth (get_user_id)
participant Model as UserCreditModel
participant Stripe as Stripe API
Client->>API: GET /api/credits/invoices?limit=24 (with auth)
API->>Auth: Validate token -> user_id
Auth-->>API: user_id
API->>Model: list_invoices(user_id, limit)
Model->>Stripe: Invoice.list(customer=..., limit=...)
Stripe-->>Model: invoices[]
Model-->>API: mapped list[InvoiceListItem]
API-->>Client: 200 OK, JSON array
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ 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 |
🔍 PR Overlap DetectionThis check compares your PR against all other open PRs targeting the same branch to detect potential merge conflicts early. 🔴 Merge Conflicts DetectedThe following PRs have been tested and will have merge conflicts if merged after this PR. Consider coordinating with the authors.
🟡 Medium Risk — Some Line OverlapThese PRs have some overlapping changes:
🟢 Low Risk — File Overlap OnlyThese PRs touch the same files but different sections (click to expand)
Summary: 3 conflict(s), 1 medium risk, 2 low risk (out of 6 PRs with file overlap) Auto-generated on push. Ignores: |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## dev #12942 +/- ##
==========================================
+ Coverage 69.56% 69.62% +0.06%
==========================================
Files 2114 2135 +21
Lines 157428 157922 +494
Branches 16230 16317 +87
==========================================
+ Hits 109509 109953 +444
- Misses 44696 44725 +29
- Partials 3223 3244 +21
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Actionable comments posted: 14
🧹 Nitpick comments (4)
autogpt_platform/frontend/src/app/(platform)/settings/billing/components/AutomationCreditsTab/TransactionHistoryCard/useTransactionHistoryCard.ts (1)
31-33: Use a deterministic fallback ID.Falling back to
Math.random()makes row IDs change across renders, which is unstable if this value is used as a React key. Prefer a deterministic fallback derived from the transaction fields so rows don't remount unnecessarily.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@autogpt_platform/frontend/src/app/`(platform)/settings/billing/components/AutomationCreditsTab/TransactionHistoryCard/useTransactionHistoryCard.ts around lines 31 - 33, The current id assignment in useTransactionHistoryCard uses Math.random() as a final fallback which produces non-deterministic IDs; replace that with a stable deterministic fallback derived from transaction fields (e.g., combine tx.transaction_time, tx.amount, tx.transaction_type, tx.transaction_key or a short hash of JSON.stringify(tx)) so the id expression (currently: id: tx.transaction_key ?? `${tx.transaction_time?.toString() ?? Math.random()}`) becomes something like tx.transaction_key ?? `${tx.transaction_time ?? ''}-${tx.amount ?? ''}-${tx.transaction_type ?? ''}` or call a small helper computeStableId(tx) that returns a string hash — update the id assignment to use that helper to ensure keys remain stable across renders.autogpt_platform/frontend/src/app/(platform)/settings/billing/components/SubscriptionTab/YourPlanCard/useYourPlanCard.ts (1)
52-58: TightenchangeTiertyping to remove the cast.
changeTier(tier: string)plusas SubscriptionTierRequestTierweakens type safety. AcceptSubscriptionTierRequestTierdirectly and passtierwithout casting.Proposed fix
- async function changeTier(tier: string) { + async function changeTier(tier: SubscriptionTierRequestTier) { @@ - tier: tier as SubscriptionTierRequestTier, + tier,🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@autogpt_platform/frontend/src/app/`(platform)/settings/billing/components/SubscriptionTab/YourPlanCard/useYourPlanCard.ts around lines 52 - 58, changeTier currently accepts a plain string and then casts it to SubscriptionTierRequestTier which weakens type safety; change the function signature of changeTier to accept tier: SubscriptionTierRequestTier and pass tier directly to updateTier without the as cast (updateTier({ data: { tier, success_url: ..., cancel_url: ... } })). Ensure any call sites of changeTier are updated to provide a SubscriptionTierRequestTier where necessary.autogpt_platform/frontend/src/app/(platform)/settings/billing/components/SubscriptionTab/InvoicesCard/useInvoicesCard.next.ts (1)
1-5: Remove the transitional.nexthook file after switchinguseInvoicesCard.ts.Keeping two near-identical source hooks increases drift risk and makes future fixes easy to apply to the wrong file.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@autogpt_platform/frontend/src/app/`(platform)/settings/billing/components/SubscriptionTab/InvoicesCard/useInvoicesCard.next.ts around lines 1 - 5, Delete the transitional hook file useInvoicesCard.next.ts and consolidate the implementation into useInvoicesCard.ts so there are not two near-identical hooks; specifically remove the file referenced in the diff, ensure any imports or references to useInvoicesCard.next (e.g., from InvoicesCard.tsx or other callers) now point to useInvoicesCard (the canonical hook), and run a quick search to update/replace any remaining usages to avoid drift and duplicate maintenance.autogpt_platform/frontend/src/app/(platform)/settings/billing/helpers.ts (1)
43-253: Split mock billing fixtures out ofhelpers.ts.This module currently mixes formatting utilities with large static sample datasets (including fixed dates like Line 160). Move sample data to dedicated fixtures/mocks so runtime helpers stay focused and less error-prone.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@autogpt_platform/frontend/src/app/`(platform)/settings/billing/helpers.ts around lines 43 - 253, The helpers.ts mixes runtime utilities with large static mock datasets; extract all sample data (SEED_AMOUNTS, DAILY_USAGE, CREDIT_TRANSACTIONS, INVOICES, AUTOMATION_CREDITS_BALANCE, PAYMENT_METHOD, CURRENT_PLAN, AUTOPILOT_USAGE, and any related interfaces/consts used only for fixtures) into a new billing fixtures module and import them from helpers.ts so helpers only exports pure helper functions/types; update references to DAILY_USAGE and the other constants in helpers.ts to import from the new fixtures module and remove hardcoded fixed dates from runtime logic (keep them only in fixtures) so helpers remain focused and deterministic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@autogpt_platform/backend/backend/data/credit.py`:
- Around line 1175-1188: The current mapping in the invoice conversion (creating
InvoiceListItem) uses invoice.amount_paid for amount_paid_cents which shows 0
for open invoices; update the mapping in the function that builds
InvoiceListItem (the block creating InvoiceListItem with id, number, created_at,
amount_paid_cents, etc.) to expose the invoice total instead (map invoice.total
to the returned amount field) or include both fields explicitly (e.g., add
total_cents=invoice.total and keep amount_paid_cents=invoice.amount_paid) so the
UI can display outstanding/open invoice amounts correctly.
- Around line 1163-1173: list_invoices currently calls get_stripe_customer_id
which will create a Stripe customer if missing; change list_invoices so it does
not create customers: add or use a non-creating lookup (e.g.,
get_stripe_customer_id(user_id, create_if_missing=False) or a new helper like
fetch_existing_stripe_customer_id) to only read an existing stripe_customer_id,
and if none is found return [] immediately; only call
run_in_threadpool(stripe.Invoice.list, customer=customer_id, limit=limit) when a
customer_id is present.
In
`@autogpt_platform/frontend/src/app/`(platform)/settings/billing/components/AutomationCreditsTab/AutoRefillCard/useAutoRefillCard.ts:
- Around line 45-57: The client-side validation in the AutoRefillCard component
currently only enforces minimums and lets refillValue be less than
thresholdValue; update the isValid expression to also require refillValue >=
thresholdValue (comparing the displayed numeric values used by thresholdValue
and refillValue) so the Save flow (save and configureAutoTopUp) is blocked for
invalid combos; adjust the isValid variable that’s used by save() (and any UI
disable logic) to include this additional check.
- Around line 51-66: Wrap the async handlers save() and disable() in try/catch
(or return the promise) so mutation failures are not converted to unhandled
rejections; specifically, in save() (which checks isValid, calls
configureAutoTopUp, refetch, and setOpen) and disable() (which calls
configureAutoTopUp, refetch, setOpen) catch any errors from
configureAutoTopUp/refetch, call the UI toast error notifier (e.g., toast.error
or the project's showToast) with a clear message including the error, and avoid
closing the UI (setOpen) on failure; alternatively return the promises so the
calling component can handle errors—ensure you reference configureAutoTopUp,
refetch, save, and disable when making the change.
In
`@autogpt_platform/frontend/src/app/`(platform)/settings/billing/components/AutomationCreditsTab/BalanceCard/useBalanceCard.ts:
- Around line 11-18: The hook currently masks fetch failures by collapsing the
credits result to 0; update useBalanceCard (the hook using
useGetV1GetUserCredits and the balanceCents value) to surface the query error
state instead of defaulting to zero: stop converting a failed/errored response
to 0, return the original isLoading, data (possibly undefined), and error from
useGetV1GetUserCredits so the BalanceCard consumer can render an error state;
update consumers to show the ErrorCard when error is present (per guidelines)
rather than treating undefined/error as a zero balance.
- Around line 25-37: The top-up validation and error handling need to match the
backend: change the client-side check (numericAmount and isValid) to require a
whole-dollar integer >= 5 (e.g., use Number.isFinite(numericAmount) &&
Number.isInteger(numericAmount) && numericAmount >= 5) so amounts like 5.25 are
rejected; update the handleSubmit flow (function handleSubmit, call to
requestTopUp, setOpen) to wrap the async mutation in try/catch, await the
requestTopUp call, show a toast error on any failure (using the app's toast
helper) and only proceed to use the returned checkout_url and close the modal on
success; apply the same validation and error-handling changes to the other
occurrence referenced (lines 47-48).
In
`@autogpt_platform/frontend/src/app/`(platform)/settings/billing/components/AutomationCreditsTab/TransactionHistoryCard/TransactionHistoryCard.tsx:
- Around line 17-21: The hook useTransactionHistoryCard currently returns only
transactions and isLoading; update it to surface the underlying query's status
(e.g., return isSuccess and/or error from useGetV1GetCreditHistory) so callers
can detect failures, and then update TransactionHistoryCard to check that
status: keep the existing Skeleton while isLoading, and render <ErrorCard />
when the hook indicates failure (i.e., !isSuccess or error) before trying to
render transactions; reference useTransactionHistoryCard,
useGetV1GetCreditHistory, TransactionHistoryCard, Skeleton, and ErrorCard when
making the changes.
In
`@autogpt_platform/frontend/src/app/`(platform)/settings/billing/components/AutomationCreditsTab/UsageCard/UsageCard.tsx:
- Around line 22-25: The current early return in UsageCard.tsx (the lines using
useUsageCard() and `if (!hasUsage) return null;`) hides the entire monthly usage
card for zero-usage accounts; instead remove that early return and render the
UsageCard component for all users, using the `hasUsage` flag to render a proper
zero-state (e.g., show usage values as 0 or a "No activity this month" message)
while still rendering real usage when `hasUsage` is true; update the JSX inside
the UsageCard component to conditionally display the zero-state UI using `usage`
and `hasUsage` (from useUsageCard) rather than returning null.
- Around line 115-117: The TooltipTrigger child in UsageCard is a plain div and
not keyboard-focusable; make the chart bar wrapper focusable so keyboard users
can open the tooltip: update the TooltipTrigger child (the div with className
"group flex h-full flex-1 flex-col justify-end") to either be a semantic
focusable element (e.g., a button that forwards props) or add tabIndex={0} and
an appropriate role (e.g., role="button"), and implement onKeyDown (Enter/Space)
to mirror the click/hover behavior used by the tooltip; ensure this change
preserves the asChild pattern and works with the existing motion.div inside the
wrapper.
In
`@autogpt_platform/frontend/src/app/`(platform)/settings/billing/components/AutomationCreditsTab/UsageCard/useUsageCard.ts:
- Around line 27-53: The buckets are created using the local "today" but keys
are generated from transactions with txDate.toISOString() (UTC), causing day
mismatches across time zones; update both bucket creation and transaction keying
to use the same local-date key format (e.g., YYYY-MM-DD derived from local date
components or toLocaleDateString('en-CA')) so keys are computed in local time
consistently; adjust the code in useUsageCard.ts where today, buckets, and key
(derived from txDate) are set so both use the identical local-date routine
(reference WINDOW_DAYS, DAY_MS, today, buckets, txDate, and key).
In
`@autogpt_platform/frontend/src/app/`(platform)/settings/billing/components/SubscriptionTab/AutopilotUsageCard/AutopilotUsageCard.tsx:
- Line 19: The tooltip text in the AutopilotUsageCard component currently uses a
dead markdown link ("[Learn more about usage](#)"); update the string inside the
AutopilotUsageCard component (the tooltip/description constant or JSX prop
containing "Each Autopilot request..." and the markdown link) to point to the
real documentation URL (e.g., your public docs page or internal help route)
instead of "#", preserving the markdown/link formatting so the link remains
clickable in the UI.
In
`@autogpt_platform/frontend/src/app/`(platform)/settings/billing/components/SubscriptionTab/InvoicesCard/InvoicesCard.tsx:
- Line 67: The empty table header for the action column in the InvoicesCard
component (the <th className="w-12 px-4 py-3" /> element) needs an accessible
label; update that <th> to include a descriptive, screen-reader-only label
(e.g., "Actions" or "Download invoice") — either by adding a visually-hidden
<span> (using the app's existing "sr-only" utility) inside the <th> or by adding
an aria-label/aria-labelledby to the <th> so assistive tech can announce the
column name.
In
`@autogpt_platform/frontend/src/app/`(platform)/settings/billing/components/SubscriptionTab/InvoicesCard/useInvoicesCard.ts:
- Around line 3-7: Replace the hand-rolled TOP_UP-based invoice construction in
useInvoicesCard (which currently builds rows and drops hosted/PDF URLs) with the
canonical generated API hook useGetV1ListInvoices: call useGetV1ListInvoices to
fetch invoices, map the returned invoice shape directly into the rows
(preserving hosted_url/pdf_url fields and invoice-specific metadata), and remove
the TOP_UP filtering logic; use the ready-to-paste implementation in
useInvoicesCard.next.ts as a reference, regenerate the client with pnpm
generate:api if the hook is missing, and ensure the mapping preserves
subscription invoices and their hosted/PDF URLs.
In
`@autogpt_platform/frontend/src/app/`(platform)/settings/billing/components/SubscriptionTab/YourPlanCard/useYourPlanCard.ts:
- Around line 52-65: The changeTier function currently lets updateTier errors
bubble silently; wrap the updateTier call in a try/catch (or attach an onError
handler) inside changeTier so mutation failures show a user toast (use your
app's toast utility), call Sentry.captureException(err) for the caught
exception, and ensure subscription.refetch() runs in a finally block (or only
after a successful redirect flow); apply the same guarded pattern to the other
tier-related mutation at the 72-73 area (refer to the same updateTier /
subscription.refetch usage) and do not replace UI-rendered errors with
toasts—use ErrorCard where rendering persistent UI errors is required.
---
Nitpick comments:
In
`@autogpt_platform/frontend/src/app/`(platform)/settings/billing/components/AutomationCreditsTab/TransactionHistoryCard/useTransactionHistoryCard.ts:
- Around line 31-33: The current id assignment in useTransactionHistoryCard uses
Math.random() as a final fallback which produces non-deterministic IDs; replace
that with a stable deterministic fallback derived from transaction fields (e.g.,
combine tx.transaction_time, tx.amount, tx.transaction_type, tx.transaction_key
or a short hash of JSON.stringify(tx)) so the id expression (currently: id:
tx.transaction_key ?? `${tx.transaction_time?.toString() ?? Math.random()}`)
becomes something like tx.transaction_key ?? `${tx.transaction_time ??
''}-${tx.amount ?? ''}-${tx.transaction_type ?? ''}` or call a small helper
computeStableId(tx) that returns a string hash — update the id assignment to use
that helper to ensure keys remain stable across renders.
In
`@autogpt_platform/frontend/src/app/`(platform)/settings/billing/components/SubscriptionTab/InvoicesCard/useInvoicesCard.next.ts:
- Around line 1-5: Delete the transitional hook file useInvoicesCard.next.ts and
consolidate the implementation into useInvoicesCard.ts so there are not two
near-identical hooks; specifically remove the file referenced in the diff,
ensure any imports or references to useInvoicesCard.next (e.g., from
InvoicesCard.tsx or other callers) now point to useInvoicesCard (the canonical
hook), and run a quick search to update/replace any remaining usages to avoid
drift and duplicate maintenance.
In
`@autogpt_platform/frontend/src/app/`(platform)/settings/billing/components/SubscriptionTab/YourPlanCard/useYourPlanCard.ts:
- Around line 52-58: changeTier currently accepts a plain string and then casts
it to SubscriptionTierRequestTier which weakens type safety; change the function
signature of changeTier to accept tier: SubscriptionTierRequestTier and pass
tier directly to updateTier without the as cast (updateTier({ data: { tier,
success_url: ..., cancel_url: ... } })). Ensure any call sites of changeTier are
updated to provide a SubscriptionTierRequestTier where necessary.
In `@autogpt_platform/frontend/src/app/`(platform)/settings/billing/helpers.ts:
- Around line 43-253: The helpers.ts mixes runtime utilities with large static
mock datasets; extract all sample data (SEED_AMOUNTS, DAILY_USAGE,
CREDIT_TRANSACTIONS, INVOICES, AUTOMATION_CREDITS_BALANCE, PAYMENT_METHOD,
CURRENT_PLAN, AUTOPILOT_USAGE, and any related interfaces/consts used only for
fixtures) into a new billing fixtures module and import them from helpers.ts so
helpers only exports pure helper functions/types; update references to
DAILY_USAGE and the other constants in helpers.ts to import from the new
fixtures module and remove hardcoded fixed dates from runtime logic (keep them
only in fixtures) so helpers remain focused and deterministic.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 910e6c41-db58-4602-b58a-5583cd6f069f
📒 Files selected for processing (27)
autogpt_platform/backend/backend/api/features/v1.pyautogpt_platform/backend/backend/data/credit.pyautogpt_platform/frontend/src/app/(platform)/settings/__tests__/placeholder-pages.test.tsxautogpt_platform/frontend/src/app/(platform)/settings/billing/components/AutomationCreditsTab/AutoRefillCard/AutoRefillCard.tsxautogpt_platform/frontend/src/app/(platform)/settings/billing/components/AutomationCreditsTab/AutoRefillCard/AutoRefillDialog.tsxautogpt_platform/frontend/src/app/(platform)/settings/billing/components/AutomationCreditsTab/AutoRefillCard/useAutoRefillCard.tsautogpt_platform/frontend/src/app/(platform)/settings/billing/components/AutomationCreditsTab/AutomationCreditsTab.tsxautogpt_platform/frontend/src/app/(platform)/settings/billing/components/AutomationCreditsTab/BalanceCard/BalanceCard.tsxautogpt_platform/frontend/src/app/(platform)/settings/billing/components/AutomationCreditsTab/BalanceCard/useBalanceCard.tsautogpt_platform/frontend/src/app/(platform)/settings/billing/components/AutomationCreditsTab/TransactionHistoryCard/TransactionHistoryCard.tsxautogpt_platform/frontend/src/app/(platform)/settings/billing/components/AutomationCreditsTab/TransactionHistoryCard/useTransactionHistoryCard.tsautogpt_platform/frontend/src/app/(platform)/settings/billing/components/AutomationCreditsTab/UsageCard/UsageCard.tsxautogpt_platform/frontend/src/app/(platform)/settings/billing/components/AutomationCreditsTab/UsageCard/useUsageCard.tsautogpt_platform/frontend/src/app/(platform)/settings/billing/components/SubscriptionTab/AutopilotUsageCard/AutopilotUsageCard.tsxautogpt_platform/frontend/src/app/(platform)/settings/billing/components/SubscriptionTab/AutopilotUsageCard/useAutopilotUsageCard.tsautogpt_platform/frontend/src/app/(platform)/settings/billing/components/SubscriptionTab/InvoicesCard/InvoicesCard.tsxautogpt_platform/frontend/src/app/(platform)/settings/billing/components/SubscriptionTab/InvoicesCard/useInvoicesCard.next.tsautogpt_platform/frontend/src/app/(platform)/settings/billing/components/SubscriptionTab/InvoicesCard/useInvoicesCard.tsautogpt_platform/frontend/src/app/(platform)/settings/billing/components/SubscriptionTab/PaymentMethodCard/PaymentMethodCard.tsxautogpt_platform/frontend/src/app/(platform)/settings/billing/components/SubscriptionTab/PaymentMethodCard/usePaymentMethodCard.tsautogpt_platform/frontend/src/app/(platform)/settings/billing/components/SubscriptionTab/SubscriptionTab.tsxautogpt_platform/frontend/src/app/(platform)/settings/billing/components/SubscriptionTab/YourPlanCard/YourPlanCard.tsxautogpt_platform/frontend/src/app/(platform)/settings/billing/components/SubscriptionTab/YourPlanCard/useYourPlanCard.tsautogpt_platform/frontend/src/app/(platform)/settings/billing/helpers.tsautogpt_platform/frontend/src/app/(platform)/settings/billing/page.tsxautogpt_platform/frontend/src/app/api/openapi.jsonautogpt_platform/frontend/src/components/molecules/TabsLine/TabsLine.tsx
- credit.py: list_invoices skips Stripe call (and the implicit customer-create) for users without a stripe_customer_id, fixing the Sentry-flagged orphaned-customer bug for BetaUserCredit users; wraps the Stripe call in try/except so a Stripe outage degrades to []. - helpers.ts: drop the unused mock data + interfaces (PlanInfo, PaymentMethod, Invoice, AutopilotUsage*, CreditTransaction, DailyUsage) and their seeded constants. Every card hook reads from real APIs; the constants were dead bundle weight (and PAYMENT_METHOD even embedded a real-looking name). - useTransactionHistoryCard.ts: replace Math.random() row-key fallback with a stable index-based id so React doesn't remount rows. - InvoicesCard.tsx: add noreferrer to window.open so the Stripe-hosted PDF tab does not receive our origin in the Referer header. - useInvoicesCard.next.ts: deleted; left a TODO in useInvoicesCard.ts pointing at the post-generate:api swap so we don't ship two implementations of the same hook. - useYourPlanCard.ts / YourPlanCard.tsx: derive nextTier from the current tier instead of always upgrading to PRO; hide the "Upgrade plan" button entirely on the top tier so the click can't no-op.
- credit.py / openapi.json: InvoiceListItem now exposes total_cents (mapped from invoice.total) alongside amount_paid_cents so open/ unpaid invoices show the correct displayed amount. - useBalanceCard: enforce whole-dollar minimum-$5 top-ups (Number.isInteger guard), wrap requestTopUp in try/catch + destructive toast, expose isError so the consumer can render a real error state. - BalanceCard.tsx: render ErrorCard on isError or null balance instead of silently displaying $0; tighten the dialog input to whole dollars. - useAutoRefillCard: add refillValue >= thresholdValue to isValid (mirrors the backend 422), wrap save/disable mutations in try/catch with destructive toasts. - useTransactionHistoryCard: expose isError + refetch. - TransactionHistoryCard.tsx: render ErrorCard on fetch failure. - useUsageCard: replace toISOString-based bucket keys with a local-date key so usage doesn't shift across timezones; skip transactions with invalid dates. - UsageCard.tsx: gate on isLoading instead of hasUsage so zero-activity accounts still see the monthly usage card; replace each chart bar's div trigger with a focusable <button> + aria-label so keyboard users can reach the per-day tooltip. - AutopilotUsageCard.tsx: drop the dead "[Learn more](#)" markdown link from the explainer copy. - InvoicesCard.tsx: add an sr-only "Download" label inside the empty action <th> so the column has an accessible name. - useYourPlanCard: wrap changeTier in try/catch and surface a destructive toast so failed Stripe calls don't become unhandled rejections.
…n unmount Sentry flagged that subscription.refetch() was awaited after navigating via window.location.href, which would set state on an unmounting tree while Stripe Checkout takes over. Return early when a redirect URL is present; only refetch when staying on the page.
|
Thanks for the summary. The 4 nitpicks were addressed across the polish commits:
|
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
autogpt_platform/frontend/src/app/(platform)/settings/billing/components/AutomationCreditsTab/TransactionHistoryCard/TransactionHistoryCard.tsx (2)
121-127: Use a named props type/interface forThinstead of an inline anonymous object.At Line 124, the inline prop type works but is less consistent with the codebase’s props conventions and makes reuse harder. Prefer a named
ThPropsdeclaration.As per coding guidelines: Component props should use a named
Propstype/interface (non-exported unless needed outside the component).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@autogpt_platform/frontend/src/app/`(platform)/settings/billing/components/AutomationCreditsTab/TransactionHistoryCard/TransactionHistoryCard.tsx around lines 121 - 127, The Th component currently declares its props inline; extract that anonymous type into a named non-exported interface or type (e.g., ThProps) and use it for the Th signature to match project conventions. Update the function declaration from Th({ children, align = "left" }: { children: React.ReactNode; align?: "left" | "right"; }) to Th(props: ThProps) (or keep destructuring) where ThProps is defined above as type ThProps = { children: React.ReactNode; align?: "left" | "right"; }; reference the Th function and the new ThProps type when making the change.
16-119: Consider extracting the table view into a sub-component to reduce render complexity.
TransactionHistoryCardcurrently combines state branching, animation config, and full table markup in one render path. This is above the repo’s “~50 lines” guidance and will be harder to maintain as billing UI evolves.♻️ Suggested refactor sketch
export function TransactionHistoryCard({ index = 0 }: Props) { const reduceMotion = useReducedMotion(); const { transactions, isLoading, isError, refetch } = useTransactionHistoryCard(); if (isLoading) { return <Skeleton className="h-[200px] rounded-[18px]" />; } if (isError) { return ( <ErrorCard context="transaction history" hint="We couldn't load your recent transactions." onRetry={() => void refetch()} /> ); } return ( <motion.section ...> - <div className="overflow-hidden rounded-[18px] border ..."> - {transactions.length === 0 ? ( ... ) : ( <table>...</table> )} - </div> + <TransactionHistoryTable transactions={transactions} /> </motion.section> ); } + +type TransactionHistoryTableProps = { + transactions: ReturnType<typeof useTransactionHistoryCard>["transactions"]; +}; + +function TransactionHistoryTable({ transactions }: TransactionHistoryTableProps) { + if (transactions.length === 0) { + return ( + <div className="overflow-hidden rounded-[18px] border border-zinc-200 bg-white shadow-[0_1px_2px_rgba(15,15,20,0.04)]"> + <div className="px-4 py-6"> + <Text variant="small" as="span" className="text-zinc-500"> + No transactions yet. + </Text> + </div> + </div> + ); + } + // existing table markup +}As per coding guidelines: Keep render functions and hooks under ~50 lines; extract named helpers or sub-components when they grow longer.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@autogpt_platform/frontend/src/app/`(platform)/settings/billing/components/AutomationCreditsTab/TransactionHistoryCard/TransactionHistoryCard.tsx around lines 16 - 119, TransactionHistoryCard is too large—extract the table rendering into a sub-component to keep the main render/hooks under ~50 lines. Create a new component (e.g., TransactionTable or TransactionHistoryList) that accepts transactions (and optional row click handlers) and move the entire <table> ... </table> markup including the transactions.map(...) row rendering and related className logic there; keep TransactionHistoryCard responsible for hooks (useTransactionHistoryCard), loading/error branching, and the motion wrapper, and pass reduceMotion/index only if the sub-component needs animation context. Ensure you reference transaction.id, transaction.date, transaction.description, transaction.amount, transaction.balance, and transaction.kind in the new component and update the JSX to render <TransactionTable transactions={transactions} /> from TransactionHistoryCard.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@autogpt_platform/frontend/src/app/`(platform)/settings/billing/components/AutomationCreditsTab/AutoRefillCard/useAutoRefillCard.ts:
- Around line 44-60: The validation currently allows fractional dollar inputs
like 5.5; update the isValid logic to reject non-whole-dollar values by ensuring
thresholdValue and refillValue are integers (e.g., add
Number.isInteger(thresholdValue) && Number.isInteger(refillValue) to the
existing checks). Keep the rest of the gating (>=5, refillValue >=
thresholdValue) and leave save/configureAutoTopUp unchanged.
In
`@autogpt_platform/frontend/src/app/`(platform)/settings/billing/components/SubscriptionTab/InvoicesCard/InvoicesCard.tsx:
- Around line 28-33: The component currently only checks isLoading and then
renders the empty-state when invoices is empty, so update the use of the hook
useInvoicesCard() to also return (or expose) an error value (e.g., error) and in
InvoicesCard.tsx add a branch that, when error is truthy and isLoading is false,
renders the <ErrorCard /> component instead of the “No invoices yet.” copy;
ensure you reference the hook's returned error (add it to the destructure
alongside invoices and isLoading) and use that error branch before rendering the
empty-state UI.
---
Nitpick comments:
In
`@autogpt_platform/frontend/src/app/`(platform)/settings/billing/components/AutomationCreditsTab/TransactionHistoryCard/TransactionHistoryCard.tsx:
- Around line 121-127: The Th component currently declares its props inline;
extract that anonymous type into a named non-exported interface or type (e.g.,
ThProps) and use it for the Th signature to match project conventions. Update
the function declaration from Th({ children, align = "left" }: { children:
React.ReactNode; align?: "left" | "right"; }) to Th(props: ThProps) (or keep
destructuring) where ThProps is defined above as type ThProps = { children:
React.ReactNode; align?: "left" | "right"; }; reference the Th function and the
new ThProps type when making the change.
- Around line 16-119: TransactionHistoryCard is too large—extract the table
rendering into a sub-component to keep the main render/hooks under ~50 lines.
Create a new component (e.g., TransactionTable or TransactionHistoryList) that
accepts transactions (and optional row click handlers) and move the entire
<table> ... </table> markup including the transactions.map(...) row rendering
and related className logic there; keep TransactionHistoryCard responsible for
hooks (useTransactionHistoryCard), loading/error branching, and the motion
wrapper, and pass reduceMotion/index only if the sub-component needs animation
context. Ensure you reference transaction.id, transaction.date,
transaction.description, transaction.amount, transaction.balance, and
transaction.kind in the new component and update the JSX to render
<TransactionTable transactions={transactions} /> from TransactionHistoryCard.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 32a212ec-64cc-4f9e-b94f-16e8ccc88fa5
📒 Files selected for processing (12)
autogpt_platform/backend/backend/data/credit.pyautogpt_platform/frontend/src/app/(platform)/settings/billing/components/AutomationCreditsTab/AutoRefillCard/useAutoRefillCard.tsautogpt_platform/frontend/src/app/(platform)/settings/billing/components/AutomationCreditsTab/BalanceCard/BalanceCard.tsxautogpt_platform/frontend/src/app/(platform)/settings/billing/components/AutomationCreditsTab/BalanceCard/useBalanceCard.tsautogpt_platform/frontend/src/app/(platform)/settings/billing/components/AutomationCreditsTab/TransactionHistoryCard/TransactionHistoryCard.tsxautogpt_platform/frontend/src/app/(platform)/settings/billing/components/AutomationCreditsTab/TransactionHistoryCard/useTransactionHistoryCard.tsautogpt_platform/frontend/src/app/(platform)/settings/billing/components/AutomationCreditsTab/UsageCard/UsageCard.tsxautogpt_platform/frontend/src/app/(platform)/settings/billing/components/AutomationCreditsTab/UsageCard/useUsageCard.tsautogpt_platform/frontend/src/app/(platform)/settings/billing/components/SubscriptionTab/AutopilotUsageCard/AutopilotUsageCard.tsxautogpt_platform/frontend/src/app/(platform)/settings/billing/components/SubscriptionTab/InvoicesCard/InvoicesCard.tsxautogpt_platform/frontend/src/app/(platform)/settings/billing/components/SubscriptionTab/YourPlanCard/useYourPlanCard.tsautogpt_platform/frontend/src/app/api/openapi.json
🚧 Files skipped from review as they are similar to previous changes (7)
- autogpt_platform/frontend/src/app/(platform)/settings/billing/components/AutomationCreditsTab/UsageCard/useUsageCard.ts
- autogpt_platform/frontend/src/app/(platform)/settings/billing/components/AutomationCreditsTab/UsageCard/UsageCard.tsx
- autogpt_platform/frontend/src/app/(platform)/settings/billing/components/SubscriptionTab/AutopilotUsageCard/AutopilotUsageCard.tsx
- autogpt_platform/frontend/src/app/(platform)/settings/billing/components/AutomationCreditsTab/BalanceCard/BalanceCard.tsx
- autogpt_platform/frontend/src/app/(platform)/settings/billing/components/SubscriptionTab/YourPlanCard/useYourPlanCard.ts
- autogpt_platform/frontend/src/app/(platform)/settings/billing/components/AutomationCreditsTab/BalanceCard/useBalanceCard.ts
- autogpt_platform/frontend/src/app/(platform)/settings/billing/components/AutomationCreditsTab/TransactionHistoryCard/useTransactionHistoryCard.ts
Resolves the lint job failure on CI.
|
Coverage update — codecov gate passed. After df501ad (test additions), all 14 codecov checks now report pass:
Coverage breakdown for this patch:
Final state: 37 CI checks pass, 0 fail, 5 long-pole tests still building ( |
|
for testing this how do i get a stripe enabled account? |
|
Hey @ntindle — here's the full local Stripe setup for testing this PR: 1. Stripe test account
2. Create test products + pricesDashboard → Product catalog → Add product. One per tier you want to exercise:
Copy each 3. LaunchDarkly tier → price mappingBackend reads price IDs from LD flag, not env (see In LaunchDarkly → flag {
"BASIC": "price_1Abc...",
"PRO": "price_1Def...",
"MAX": "price_1Ghi...",
"BUSINESS": "price_1Jkl..."
}Tier missing from JSON → "not offered" → checkout returns 422. 4. Backend
|
| Card | Result |
|---|---|
4242 4242 4242 4242 |
success |
4000 0000 0000 9995 |
insufficient funds |
4000 0025 0000 3155 |
3DS required |
4000 0000 0000 0341 |
attach OK, fail on charge |
Any future expiry, any CVC.
8. Smoke test for this PR
- Login →
/settings/billing. - Subscription tab → upgrade tier → Stripe Checkout opens → pay with
4242…→ return to success URL → CLI logs webhook hit → tier updates. - Automation Credits tab →
BalanceCard,AutoRefillCard,UsageCard,TransactionHistoryCardshould all populate. - Trigger invoice/refund events to exercise
InvoicesCard:stripe trigger invoice.paid stripe trigger charge.refunded stripe trigger customer.subscription.updated
9. Reset between runs
Stripe dashboard → Developers → Test data → Delete all test data, then:
UPDATE "User" SET "stripeCustomerId" = NULL;Happy to pair on it if anything 503s or the LD flag isn't taking — most common gotcha is the webhook secret mismatch (CLI secret ≠ dashboard endpoint secret).
…-page-for-settings-page
…ettings/billing Post-merge adjustments after #12933 introduced NO_TIER as the explicit "no active subscription" state and added has_active_stripe_subscription + current_period_end on SubscriptionStatusResponse: - YourPlanCard: paid-detection now reads has_active_stripe_subscription (more robust than tier comparison). NO_TIER renders the "No active subscription" state with a "Choose a plan" CTA. Surface the renewal date when current_period_end is present. Cancel goes through the Stripe billing portal so cancellation flows match #12933. - TIER_ORDER limited to PRO/MAX/BUSINESS (the launch tiers from the pricing doc). BASIC + NO_TIER + ENTERPRISE are reserved internal states and never offered as upgrade targets in the settings UI. - Tests updated for the NO_TIER + has_active_stripe_subscription shape. Topup redirect target moved from /profile/credits to /settings/billing so users land on the new billing page after a successful Stripe Checkout. Page-level effect calls fulfill_checkout, shows a toast, and strips the query string. Backend success_url, cancel_url, billing portal return_url, and zero/low-balance email links updated.
…tact-sales, pending state YourPlanCard: - Compare-plans link to https://agpt.co/pricing in the section header - Three-button layout: Downgrade · Manage · Upgrade (only those that apply to the current tier are rendered). - Upgrade button text now reflects the next tier ("Get Pro", "Upgrade to Max", "Talk to sales — Team"). Team route opens TEAM_UPGRADE_URL (placeholder agpt.co/contact-sales) in a new tab — no Stripe Checkout for contact-sales tiers. - Downgrade is end-of-period: backend modify_stripe_subscription_for_tier schedules a phase change at current_period_end, no charge today. - Pending state surfaced via SubscriptionStatusResponse.pending_tier: - "Cancellation scheduled" badge + "Ends on …" subtext when pending NO_TIER - "Downgrade scheduled" badge + "Switches to <Tier> on …" when pending paid→paid - Cancel/Upgrade/Downgrade buttons hidden while pending; primary CTA becomes "Resume subscription" or "Cancel downgrade", which POSTs the current tier back to release the schedule. - Treat users without an active Stripe sub as NO_TIER for display, even if the DB tier defaults to PRO (rate-limit default per schema.prisma). Avoids rendering "Pro · No active subscription" simultaneously. - Distinct Button variants per action to fix same-variant collision noted in design review (ghost/outline/secondary/primary). - Loading spinner on Upgrade / Downgrade / Resume / Cancel-downgrade while the mutation is in flight. PaymentMethodCard: - Local isOpening state on Open-portal click → loading spinner during the cross-origin redirect to billing.stripe.com. BalanceCard (Add credits): - Better error surfacing on top-up failure: parse status + body.detail so the toast shows the underlying Stripe error (e.g. stale customer ID, missing product) instead of a generic "no checkout URL" message. Tests: - billing-cards.test.tsx covers PRO/MAX/BUSINESS button matrices, NO_TIER empty state, and pending-downgrade UI. - billing-hooks.test.tsx covers nextTier/previousTier derivation, canDowngrade gating around pending changes, onDowngrade dispatch, and the MAX-onUpgrade-opens-TEAM_UPGRADE_URL branch.
Replaces the single Your-Plan card on /settings/billing with the tier-grid pattern that was already shipping on /profile/credits — three cards (Pro, Max, Team) side by side, click directly on the target tier to switch. Why - Single-card "Upgrade to <next tier>" UX could only step one tier at a time and didn't surface what the user was paying for vs what was available, so jumping PRO → BUSINESS or seeing the rate-limit gap required leaving the page. - The old credits-page picker already had the working pattern: per-tier card, "Current" badge, Upgrade/Downgrade button derived from TIER_ORDER position, confirmation dialogs, pending-change banner, separate Cancel-subscription link. Reusing that shape on the new page matches what the team already validated. What - helpers.ts now exports PLAN_TIERS (Pro/Max/Team), TIER_ORDER, and formatters: getTierLabel, formatTierCost, formatRelativeMultiplier. BASIC and ENTERPRISE stay out of the picker per launch-doc tier list (BASIC is reserved future slot; ENTERPRISE is admin-managed). - useYourPlanCard.ts: rewrite around the old SubscriptionTierSection hook. Returns the full subscription object plus changeTier, handleTierChange (routes downgrades through a confirm callback, upgrades through pendingUpgradeTier dialog state, BUSINESS to the contact-sales URL), cancelPendingChange (releases pending schedule), and Stripe-portal helpers. - YourPlanCard.tsx: tier grid + pending-change banner with inline Resume / Cancel-downgrade button + bottom row with Manage subscription and Cancel subscription link + three confirmation dialogs (downgrade, upgrade, replace-pending). Treats users without an active Stripe sub as NO_TIER for picker comparisons even if the DB tier defaults to PRO, so brand-new users see the "Pick a plan" banner and Pro card as the upgrade target. - ENTERPRISE state shows the admin-managed banner — self-service tier changes blocked by the backend anyway, but surface the reason in UI. - Tests rewritten to assert the new layout: tier-card presence per state, Current badge highlight, Upgrade/Downgrade/Talk-to-sales button routing, pending banner copy, Pick-a-plan banner for unpaid users, hook-level changeTier / handleTierChange / cancelPendingChange paths.
…dits page" This reverts commit 4527929.
- AutoRefillCard: relax validation from Number.isInteger to Number.isFinite so legacy non-whole-dollar amounts (e.g. $7.50 stored as 750 cents) don't permanently disable the save button. Math.round on save still produces integer cents for the backend. - YourPlanCard: gate canUpgrade on !isPendingCancel && !isPendingDowngrade so a pending cancellation no longer shows Resume + Upgrade simultaneously (matches the canDowngrade guard). - Run prettier on touched files.
- Update billing-hooks fractional test: now expects isValid=true for $5.50 since the validator was relaxed from Number.isInteger to Number.isFinite (legacy non-whole-dollar amounts must remain editable). - YourPlanCard badge: use "Inactive" instead of "No active subscription" for the no-tier case, so it doesn't duplicate the plan label and break findByText queries in billing-cards.test.tsx.
… redirect Trust subscription_tier from the backend instead of forcing NO_TIER when no active Stripe subscription — matches the old /profile/credits page so beta users with the default PRO tier see Pro instead of "Upgrade to Pro". Also handle Stripe's ?subscription=success|cancelled redirect by toasting and invalidating the subscription status query, so the new tier shows up without requiring a manual refresh.
Drop the Number.isInteger guard from useBalanceCard for parity with useAutoRefillCard — backend stores credits in integer cents and the hook already rounds on submit, so decimals like $5.50 are valid input and should not be blocked by the frontend. Update the matching unit test to expect isValid=true for $5.25.
Patch coverage on the Settings v2 billing PR was sitting at 77.25%, with the bulk of the uncovered lines living in the page-level redirect handlers and a few card hooks/dialog branches that no test exercised. - billing-page: cover the ?topup=success / cancel and ?subscription=success / cancelled redirect paths via mocked next/navigation; assert router.replace clears the query string. - billing-cards: extend the BalanceCard dialog test to drive the amount input and the Cancel button, exercising the input onChange and dialog-close handlers. - billing-hooks: add useYourPlanCard tests for onResume (active resume vs short-circuit) and verify canManagePortal flips true once the portal URL resolves. Local billing folder coverage now: 98.24% / 91.42% / 87.5% / 100% (was 68.42% / 60% / 87.5% / 66.66%).
…ollar rule Backend top_up_intent rejects amount % 100 != 0, so the dialog must gate decimals client-side instead of letting the user fail at Stripe checkout. Re-introduces Number.isInteger guard on useBalanceCard isValid and updates the matching unit test to expect 5.25 to be invalid.
Why / What / How
Why: The Settings V2 layout (#12885) shipped a
/settings/billingroute with only a "Coming soon" placeholder. This PR replaces it with the real billing experience so users can manage their subscription, payment method, invoices, automation credits, and auto top-up from the new layout.What: A new billing page split into two tabs:
How:
TabsLinemolecule. Each tab is a thin orchestrator that composes per-feature cards.ComponentName/ComponentName.tsxfor render +useComponentName.tsfor logic. Shared formatters live inbilling/helpers.ts.GET /credits/invoicesendpoint backed bystripe.Invoice.list, returning the subset the UI needs (id, number, amount, status, hosted URL, PDF URL).DisabledUserCreditreturns[]so the page degrades gracefully when the credit system is off.openapi.jsonis regenerated to include the new operation; theInvoicesCardhook will switch from the credit-history fallback to the generateduseGetV1ListInvoicesoncepnpm generate:apiruns against this branch (seeuseInvoicesCard.next.tsfor the swap).TabsLineListgains an optionalflushprop so the first tab aligns with the page heading without a stray indent.Changes 🏗️
Backend (
backend/)data/credit.py—InvoiceListItemmodel + abstractlist_invoices(default[]) onUserCreditBase; concreteUserCredit.list_invoicescalls Stripe viarun_in_threadpooland boundslimitto[1, 100].api/features/v1.py—GET /credits/invoices?limit=24(auth required), returnslist[InvoiceListItem].Frontend (
frontend/src/app/(platform)/settings/billing/)page.tsx— replaces the placeholder withTabsLine(Subscription / Automation Credits) and setsdocument.title.helpers.ts—formatCents,formatShortDate,formatRelativeReset,EASE_OUT.components/SubscriptionTab/—YourPlanCard,PaymentMethodCard,InvoicesCard,AutopilotUsageCard.components/AutomationCreditsTab/—BalanceCard,AutoRefillCard(+AutoRefillDialog),UsageCard,TransactionHistoryCard.Frontend (shared)
openapi.json— regenerated (adds/credits/invoices).components/molecules/TabsLine/TabsLine.tsx— addsflushprop onTabsLineListto drop the first trigger's left padding.settings/__tests__/placeholder-pages.test.tsx— Billing now asserts the real heading + tab triggers; OAuth Apps keeps the "coming soon" assertion.Checklist 📋
For code changes:
pytest backend/api/features/test_v1.py -k invoices(or an equivalent smoke test) returns a 200 with[]for a user without a Stripe customer.pnpm test:unit -- placeholder-pagespasses.For configuration changes:
.env.defaultis updated or already compatible with my changesdocker-compose.ymlis updated or already compatible with my changes