Add billing page, usage display, and upgrade prompt#378
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ 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 |
Greptile SummaryThis PR adds a complete billing experience: a
Confidence Score: 3/5Not safe to merge as-is — three P1 bugs affect the billing management and upgrade flows directly. Two confirmed P1 bugs in billing/page.tsx (portal button permanently disabled after success, network errors silently retried and swallowed) and one P1 in UpgradeModal (silent navigation without user feedback) collectively degrade the core billing UX. These are on the primary billing management path and should be fixed before release. src/app/billing/page.tsx and src/components/billing/UpgradeModal.tsx need the most attention. Important Files Changed
Reviews (1): Last reviewed commit: "Create billing page with usage display, ..." | Re-trigger Greptile |
| const handleOpenPortal = async () => { | ||
| setIsPortalLoading(true); | ||
| try { | ||
| await openBillingPortal(customerApi, window.location.href); | ||
| } catch (error) { | ||
| console.error("Failed to open billing portal", error); | ||
| toast.error("Unable to open billing portal", { | ||
| description: "Please try again in a moment.", | ||
| }); | ||
| setIsPortalLoading(false); | ||
| } | ||
| }; |
There was a problem hiding this comment.
isPortalLoading never resets on success
setIsPortalLoading(false) is only called in the catch block. If the portal opens successfully in a new tab and the user returns to the page, the button stays permanently disabled for the rest of the session.
| const handleOpenPortal = async () => { | |
| setIsPortalLoading(true); | |
| try { | |
| await openBillingPortal(customerApi, window.location.href); | |
| } catch (error) { | |
| console.error("Failed to open billing portal", error); | |
| toast.error("Unable to open billing portal", { | |
| description: "Please try again in a moment.", | |
| }); | |
| setIsPortalLoading(false); | |
| } | |
| }; | |
| const handleOpenPortal = async () => { | |
| setIsPortalLoading(true); | |
| try { | |
| await openBillingPortal(customerApi, window.location.href); | |
| } catch (error) { | |
| console.error("Failed to open billing portal", error); | |
| toast.error("Unable to open billing portal", { | |
| description: "Please try again in a moment.", | |
| }); | |
| } finally { | |
| setIsPortalLoading(false); | |
| } | |
| }; |
| async function attachPlan(api: UseCustomerApi, planId: string) { | ||
| const customerApi = api as unknown as { | ||
| attach?: (params: Record<string, unknown>) => Promise<unknown>; | ||
| }; | ||
|
|
||
| if (!customerApi.attach) { | ||
| throw new Error("Attach is not available"); | ||
| } | ||
|
|
||
| try { | ||
| return await customerApi.attach({ productId: planId }); | ||
| } catch (error) { | ||
| return await customerApi.attach({ planId }); | ||
| } | ||
| } |
There was a problem hiding this comment.
Nested try/catch swallows real network errors
The inner catch on attach({ productId }) is meant to handle an API shape mismatch, but it will also catch genuine network errors and retry with { planId }. On a network failure both requests fail, the original error is silently dropped, and the caller's error handler only sees the second (less informative) error. The same pattern exists in UpgradeModal.tsx lines 36–40.
A safer approach is to probe the API shape once (e.g. check typeof customerApi.attach) or version-detect on mount, rather than retrying destructively on every call.
| } catch { | ||
| window.location.href = "/billing"; | ||
| } finally { |
There was a problem hiding this comment.
Silent navigation hides error on upgrade failure
When both attach calls fail, the outer catch block navigates directly to /billing via window.location.href with no user-visible feedback. From the user's perspective, clicking "Upgrade" simply redirects them with no indication that anything went wrong. A toast error before (or instead of) navigating would give context.
| } catch { | |
| window.location.href = "/billing"; | |
| } finally { | |
| } catch { | |
| toast.error("Unable to start checkout. Redirecting to billing page."); | |
| window.location.href = "/billing"; | |
| } |
| onClick={() => { | ||
| setSelectedModelId("gemini-3-flash-preview"); | ||
| setIsLowCreditsPopoverOpen(false); |
There was a problem hiding this comment.
Hardcoded model ID for free-tier fallback
"gemini-3-flash-preview" is hardcoded here. If this model ID is renamed, deprecated, or removed from the available models list, the button silently sets an invalid model. Consider referencing a shared constant (e.g. DEFAULT_FREE_MODEL_ID) from your models config instead.
| const normalizedAmount = amount > 100 ? amount / 100 : amount; | ||
| return `$${normalizedAmount.toFixed(normalizedAmount % 1 === 0 ? 0 : 2)}/${interval === "year" ? "yr" : "mo"}`; |
There was a problem hiding this comment.
Cents-vs-dollars heuristic can misformat prices
amount > 100 ? amount / 100 : amount assumes any amount over 100 is in cents. A plan priced at $101/mo or $200/yr would incorrectly display as $1.01 or $2.00. Consider normalizing on a known unit (always cents, or always dollars) and documenting that contract, rather than guessing from magnitude.
There was a problem hiding this comment.
4 issues found across 11 files
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="src/components/auth/AccountModal.tsx">
<violation number="1" location="src/components/auth/AccountModal.tsx:114">
P2: Avoid nesting a button inside a link. Use `Button asChild` so the link becomes the interactive element with button styling.</violation>
</file>
<file name="src/components/home/UserProfileDropdown.tsx">
<violation number="1" location="src/components/home/UserProfileDropdown.tsx:42">
P2: Normalize `useCustomer()` response here as well (`customer ?? data`) so credits still render across Autumn response variants.</violation>
</file>
<file name="src/app/billing/page.tsx">
<violation number="1" location="src/app/billing/page.tsx:200">
P1: Custom agent: **Flag AI Slop and Fabricated Changes**
Fabricated price-normalization heuristic (`amount > 100 ? amount / 100 : amount`) will silently display incorrect prices. Stripe-based APIs return amounts in cents — this should always divide by 100 (for USD), not guess based on an arbitrary threshold. For example, a $1.00/mo plan (100 cents) would display as "$100/mo".</violation>
</file>
<file name="src/components/billing/UpgradeModal.tsx">
<violation number="1" location="src/components/billing/UpgradeModal.tsx:28">
P2: Custom agent: **Flag AI Slop and Fabricated Changes**
Duplicated patchwork: the `attach` + `productId`/`planId` fallback logic is already implemented as `attachPlan()` in `page.tsx`. Import and reuse the shared helper instead of inlining a copy here.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| <Link href="/billing"> | ||
| <Button variant="outline" className="gap-2"> | ||
| <CreditCard className="h-4 w-4" /> | ||
| Manage Billing | ||
| </Button> | ||
| </Link> |
There was a problem hiding this comment.
P2: Avoid nesting a button inside a link. Use Button asChild so the link becomes the interactive element with button styling.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/components/auth/AccountModal.tsx, line 114:
<comment>Avoid nesting a button inside a link. Use `Button asChild` so the link becomes the interactive element with button styling.</comment>
<file context>
@@ -102,6 +104,23 @@ function ProfileForm({ user }: { user: any }) {
+ <p className="mb-4 text-sm text-muted-foreground">
+ Manage your subscription, view usage, and update payment methods.
+ </p>
+ <Link href="/billing">
+ <Button variant="outline" className="gap-2">
+ <CreditCard className="h-4 w-4" />
</file context>
| <Link href="/billing"> | |
| <Button variant="outline" className="gap-2"> | |
| <CreditCard className="h-4 w-4" /> | |
| Manage Billing | |
| </Button> | |
| </Link> | |
| <Button asChild variant="outline" className="gap-2"> | |
| <Link href="/billing"> | |
| <CreditCard className="h-4 w-4" /> | |
| Manage Billing | |
| </Link> | |
| </Button> |
| const { customer } = useCustomer() as { | ||
| customer?: { | ||
| features?: { | ||
| premium_message?: PremiumBalance; | ||
| }; | ||
| balances?: { | ||
| premium_message?: PremiumBalance; | ||
| }; | ||
| } | null; | ||
| }; |
There was a problem hiding this comment.
P2: Normalize useCustomer() response here as well (customer ?? data) so credits still render across Autumn response variants.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/components/home/UserProfileDropdown.tsx, line 42:
<comment>Normalize `useCustomer()` response here as well (`customer ?? data`) so credits still render across Autumn response variants.</comment>
<file context>
@@ -29,6 +30,50 @@ interface UserProfileDropdownProps {
+};
+
+function CreditsBadge() {
+ const { customer } = useCustomer() as {
+ customer?: {
+ features?: {
</file context>
| const { customer } = useCustomer() as { | |
| customer?: { | |
| features?: { | |
| premium_message?: PremiumBalance; | |
| }; | |
| balances?: { | |
| premium_message?: PremiumBalance; | |
| }; | |
| } | null; | |
| }; | |
| const customerApi = useCustomer() as { | |
| customer?: { | |
| features?: { | |
| premium_message?: PremiumBalance; | |
| }; | |
| balances?: { | |
| premium_message?: PremiumBalance; | |
| }; | |
| } | null; | |
| data?: { | |
| features?: { | |
| premium_message?: PremiumBalance; | |
| }; | |
| balances?: { | |
| premium_message?: PremiumBalance; | |
| }; | |
| } | null; | |
| }; | |
| const customer = customerApi.customer ?? customerApi.data; |
| const handleUpgrade = async () => { | ||
| setIsLoading(true); | ||
| try { | ||
| const autumnApi = customerApi as unknown as { |
There was a problem hiding this comment.
P2: Custom agent: Flag AI Slop and Fabricated Changes
Duplicated patchwork: the attach + productId/planId fallback logic is already implemented as attachPlan() in page.tsx. Import and reuse the shared helper instead of inlining a copy here.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/components/billing/UpgradeModal.tsx, line 28:
<comment>Duplicated patchwork: the `attach` + `productId`/`planId` fallback logic is already implemented as `attachPlan()` in `page.tsx`. Import and reuse the shared helper instead of inlining a copy here.</comment>
<file context>
@@ -0,0 +1,81 @@
+ const handleUpgrade = async () => {
+ setIsLoading(true);
+ try {
+ const autumnApi = customerApi as unknown as {
+ attach?: (params: Record<string, unknown>) => Promise<unknown>;
+ };
</file context>
This PR adds a complete billing experience: a
/billingpage for plan management and usage tracking, a credits indicator in the user dropdown, an upgrade modal for exhausted credits, a billing link in account settings, and a low-credits warning in the composer. It also patches anautumn-js/better-authcompatibility issue.Billing Page & Layout
AnonymousSessionHandlerwrapper, redirects anonymous sessions to sign-up, shows skeleton loader while pendinguseListPlans, subscription management with cancel/portal buttons; runtime-compatible helpers normalize Autumn API differences (customervsdata,featuresvsbalances,productIdvsplanId)Upgrade Modal & Re-export
attachcall toproductId: "pro", fallback to/billingnavigation on error, and "continue with basic models" optionUpgradeModalasUpgradeDialogto preserve existing trigger pathsCredits & Account Integrations
CreditsBadgecomponent showing remaining premium messages with color-coded dot (green/amber/red) for non-unlimited users, inserted in dropdown menu with separatorBillingSectionbetween profile form and danger zone withCreditCardicon, links to/billingfor subscription and payment method managementComposer Low-Credits Warning
/billing) or switch to free model (selectsgemini-3-flash-preview)UI Primitive
valueprop (0-100 normalized), uses transform-based width animationDependency Patch
dist/better-auth/chunk-LRULDM7S.mjsto importcreateAuthEndpointfrombetter-auth/apiinstead ofbetter-auth/plugins, fixing compatibility withbetter-auth@1.6.xpnpm.patchedDependenciesregistration for the autumn-js patch