Skip to content

Add billing page, usage display, and upgrade prompt#378

Merged
capy-ai[bot] merged 2 commits into
capy/gate-premium-messagesfrom
capy/billing-ui-implementation
Apr 16, 2026
Merged

Add billing page, usage display, and upgrade prompt#378
capy-ai[bot] merged 2 commits into
capy/gate-premium-messagesfrom
capy/billing-ui-implementation

Conversation

@urjitc

@urjitc urjitc commented Apr 16, 2026

Copy link
Copy Markdown
Member

This PR adds a complete billing experience: a /billing page 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 an autumn-js/better-auth compatibility issue.

Billing Page & Layout

  • src/app/billing/layout.tsx: Guarded billing route with AnonymousSessionHandler wrapper, redirects anonymous sessions to sign-up, shows skeleton loader while pending
  • src/app/billing/page.tsx: Full billing page (client component) with usage section showing plan badge and premium message progress bar, dynamic pricing table via useListPlans, subscription management with cancel/portal buttons; runtime-compatible helpers normalize Autumn API differences (customer vs data, features vs balances, productId vs planId)

Upgrade Modal & Re-export

  • src/components/billing/UpgradeModal.tsx: Modal for credit-exhausted state with attach call to productId: "pro", fallback to /billing navigation on error, and "continue with basic models" option
  • src/components/billing/UpgradeDialog.tsx: Re-exports UpgradeModal as UpgradeDialog to preserve existing trigger paths

Credits & Account Integrations

  • src/components/home/UserProfileDropdown.tsx: Added CreditsBadge component showing remaining premium messages with color-coded dot (green/amber/red) for non-unlimited users, inserted in dropdown menu with separator
  • src/components/auth/AccountModal.tsx: Added BillingSection between profile form and danger zone with CreditCard icon, links to /billing for subscription and payment method management

Composer Low-Credits Warning

  • src/components/assistant-ui/thread.tsx: Added low-credits popover indicator next to ModelPicker for authenticated free-tier users with <20% credits remaining; popover shows remaining count, model-specific context, action buttons to upgrade (links to /billing) or switch to free model (selects gemini-3-flash-preview)

UI Primitive

  • src/components/ui/progress.tsx: New Progress component for usage bars, accepts value prop (0-100 normalized), uses transform-based width animation

Dependency Patch

  • patches/autumn-js@1.2.9.patch: Patches dist/better-auth/chunk-LRULDM7S.mjs to import createAuthEndpoint from better-auth/api instead of better-auth/plugins, fixing compatibility with better-auth@1.6.x
  • package.json: Added pnpm.patchedDependencies registration for the autumn-js patch

Open in Capy ENG-53 · 5.4

@urjitc urjitc added the capy Generated by capy.ai label Apr 16, 2026 — with Capy AI
@vercel

vercel Bot commented Apr 16, 2026

Copy link
Copy Markdown
Contributor

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

Project Deployment Actions Updated (UTC)
thinkex Ready Ready Preview, Comment Apr 16, 2026 0:38am

Request Review

@github-project-automation github-project-automation Bot moved this to Backlog in Dev Board Apr 16, 2026
@coderabbitai

coderabbitai Bot commented Apr 16, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c581ed9d-d053-4c53-bd8e-99d12b843bd8

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch capy/billing-ui-implementation

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 and usage tips.

@greptile-apps

greptile-apps Bot commented Apr 16, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a complete billing experience: a /billing page with usage tracking and pricing table, a credits indicator in the user dropdown, a low-credits popover in the composer, a billing link in account settings, an upgrade modal, and a patch for autumn-js/better-auth compatibility.

  • isPortalLoading never resets on success in SubscriptionManagement.handleOpenPortalsetIsPortalLoading(false) is only in the catch block, so if the portal opens successfully in a new tab the button stays permanently disabled until page reload.
  • Nested try/catch swallows network errors in attachPlan (billing page) and handleUpgrade (UpgradeModal) — any genuine network failure on the first attach call retries with a different parameter shape, silently dropping the original error and making two failing requests instead of one.
  • Silent navigation on upgrade error in UpgradeModal — when both attach attempts fail, the outer catch navigates to /billing via window.location.href with no toast or user feedback.

Confidence Score: 3/5

Not 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

Filename Overview
src/app/billing/page.tsx Full billing page with usage, pricing table, and subscription management — has two P1 bugs: isPortalLoading never resets on success (button permanently stuck), and nested try/catch in attachPlan swallows network errors; plus a price normalization heuristic that misformats amounts > $100.
src/app/billing/layout.tsx Guards the billing route with session check and redirect for anonymous users; clean implementation with skeleton loading state.
src/components/billing/UpgradeModal.tsx Credit-exhausted upgrade modal — silently navigates to /billing on error without user feedback; same nested try/catch anti-pattern as billing page.
src/components/billing/UpgradeDialog.tsx Thin re-export of UpgradeModal as UpgradeDialog to preserve existing import paths; no issues.
src/components/home/UserProfileDropdown.tsx Adds CreditsBadge with color-coded indicator for remaining premium messages; clean implementation, correctly hidden for unlimited/anonymous users.
src/components/auth/AccountModal.tsx Adds BillingSection with a link to /billing; no issues.
src/components/assistant-ui/thread.tsx Adds low-credits popover to the composer toolbar; hardcodes "gemini-3-flash-preview" as the free model fallback — brittle if the model ID changes.
src/components/ui/progress.tsx New Progress component using translateX-based animation; clean, accessible, normalizes value to 0-100.
patches/autumn-js@1.2.9.patch Patches autumn-js to import createAuthEndpoint from better-auth/api instead of better-auth/plugins, fixing compatibility with better-auth@1.6.x.
package.json Registers the autumn-js patch in pnpm.patchedDependencies.
pnpm-workspace.yaml Adds patchedDependencies config for autumn-js@1.2.9; no issues.

Fix All in Cursor

Reviews (1): Last reviewed commit: "Create billing page with usage display, ..." | Re-trigger Greptile

Comment thread src/app/billing/page.tsx
Comment on lines +468 to +479
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);
}
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 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.

Suggested change
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);
}
};

Fix in Cursor

Comment thread src/app/billing/page.tsx
Comment on lines +85 to +99
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 });
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 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.

Fix in Cursor

Comment on lines +41 to +43
} catch {
window.location.href = "/billing";
} finally {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 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.

Suggested change
} catch {
window.location.href = "/billing";
} finally {
} catch {
toast.error("Unable to start checkout. Redirecting to billing page.");
window.location.href = "/billing";
}

Fix in Cursor

Comment on lines +1199 to +1201
onClick={() => {
setSelectedModelId("gemini-3-flash-preview");
setIsLowCreditsPopoverOpen(false);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 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.

Fix in Cursor

Comment thread src/app/billing/page.tsx Outdated
Comment on lines +200 to +201
const normalizedAmount = amount > 100 ? amount / 100 : amount;
return `$${normalizedAmount.toFixed(normalizedAmount % 1 === 0 ? 0 : 2)}/${interval === "year" ? "yr" : "mo"}`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 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.

Fix in Cursor

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Comment thread src/app/billing/page.tsx Outdated
Comment on lines +114 to +119
<Link href="/billing">
<Button variant="outline" className="gap-2">
<CreditCard className="h-4 w-4" />
Manage Billing
</Button>
</Link>

@cubic-dev-ai cubic-dev-ai Bot Apr 16, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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>
Suggested change
<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>
Fix with Cubic

Comment on lines +42 to +51
const { customer } = useCustomer() as {
customer?: {
features?: {
premium_message?: PremiumBalance;
};
balances?: {
premium_message?: PremiumBalance;
};
} | null;
};

@cubic-dev-ai cubic-dev-ai Bot Apr 16, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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>
Suggested change
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;
Fix with Cubic

const handleUpgrade = async () => {
setIsLoading(true);
try {
const autumnApi = customerApi as unknown as {

@cubic-dev-ai cubic-dev-ai Bot Apr 16, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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>
Fix with Cubic

@capy-ai
capy-ai Bot merged commit f7c9fe7 into capy/gate-premium-messages Apr 16, 2026
4 of 5 checks passed
@github-project-automation github-project-automation Bot moved this from Backlog to Done in Dev Board Apr 16, 2026

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

0 issues found across 2 files (changes from recent commits).

Requires human review: Auto-approval blocked by 4 unresolved issues from previous reviews.

@urjitc
urjitc deleted the capy/billing-ui-implementation branch June 29, 2026 17:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

capy Generated by capy.ai

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant