From 7cd661d726b8bda3e3f4259de3e5be4361359e1c Mon Sep 17 00:00:00 2001 From: Ramnique Singh <30795890+ramnique@users.noreply.github.com> Date: Sun, 24 May 2026 12:46:54 +0530 Subject: [PATCH] Show split monthly and daily credits - update billing contract to consume split monthly/daily usage - show monthly and daily credit percentage bars in account settings - keep sidebar plan labels normalized - update out-of-credits copy for daily credits --- .../components/settings/account-settings.tsx | 46 +++++++++++++++++-- .../src/components/sidebar-content.tsx | 7 ++- apps/x/apps/renderer/src/lib/billing-error.ts | 2 +- apps/x/packages/core/src/billing/billing.ts | 17 +++++-- apps/x/packages/shared/src/billing.ts | 13 +++++- 5 files changed, 73 insertions(+), 12 deletions(-) diff --git a/apps/x/apps/renderer/src/components/settings/account-settings.tsx b/apps/x/apps/renderer/src/components/settings/account-settings.tsx index feee291b..c40f411e 100644 --- a/apps/x/apps/renderer/src/components/settings/account-settings.tsx +++ b/apps/x/apps/renderer/src/components/settings/account-settings.tsx @@ -17,11 +17,44 @@ import { import { Separator } from "@/components/ui/separator" import { useBilling } from "@/hooks/useBilling" import { toast } from "sonner" +import type { BillingUsageBucket } from "@x/shared/dist/billing.js" interface AccountSettingsProps { dialogOpen: boolean } +function formatPlanName(plan: string | null | undefined) { + if (!plan) return 'No Plan' + return `${plan.charAt(0).toUpperCase()}${plan.slice(1)} Plan` +} + +function CreditUsageBar({ label, bucket, helper }: { + label: string + bucket: BillingUsageBucket + helper?: string +}) { + const pct = bucket.sanctionedCredits > 0 + ? Math.min(100, Math.max(0, Math.round((bucket.usedCredits / bucket.sanctionedCredits) * 100))) + : 0 + + return ( +
+
+
+

{label}

+ {helper ?

{helper}

: null} +
+

+ {pct}% +

+
+
+
+
+
+ ) +} + export function AccountSettings({ dialogOpen }: AccountSettingsProps) { const [isRowboatConnected, setIsRowboatConnected] = useState(false) const [connectionLoading, setConnectionLoading] = useState(true) @@ -164,7 +197,7 @@ export function AccountSettings({ dialogOpen }: AccountSettingsProps) {

- {billing.subscriptionPlan ? `${billing.subscriptionPlan} Plan` : 'No Plan'} + {formatPlanName(billing.subscriptionPlan)}

{billing.subscriptionStatus === 'trialing' && billing.trialExpiresAt ? (() => { const days = Math.max(0, Math.ceil((new Date(billing.trialExpiresAt).getTime() - Date.now()) / (1000 * 60 * 60 * 24))) @@ -179,14 +212,19 @@ export function AccountSettings({ dialogOpen }: AccountSettingsProps) { {!billing.subscriptionPlan && (

Subscribe to access AI features

)} - {billing.subscriptionPlan === 'free' && ( -

Free usage resets daily at 00:00 UTC.

- )}
+
+ + +
) : (

Unable to load plan details

diff --git a/apps/x/apps/renderer/src/components/sidebar-content.tsx b/apps/x/apps/renderer/src/components/sidebar-content.tsx index a679c4fd..dbb5edb5 100644 --- a/apps/x/apps/renderer/src/components/sidebar-content.tsx +++ b/apps/x/apps/renderer/src/components/sidebar-content.tsx @@ -96,6 +96,11 @@ function displayNoteName(node: TreeNode): string { return node.name } +function formatBillingPlanName(plan: string | null | undefined) { + if (!plan) return 'No plan' + return `${plan.charAt(0).toUpperCase()}${plan.slice(1)} plan` +} + function formatAgo(ms: number): string { const diffMs = Math.max(0, Date.now() - ms) const min = Math.floor(diffMs / 60000) @@ -921,7 +926,7 @@ export function SidebarContentPanel({
- {billing.subscriptionPlan ? `${billing.subscriptionPlan} plan` : 'No plan'} + {formatBillingPlanName(billing.subscriptionPlan)} {billing.subscriptionStatus === 'trialing' && billing.trialExpiresAt && (() => { const days = Math.max(0, Math.ceil((new Date(billing.trialExpiresAt).getTime() - Date.now()) / (1000 * 60 * 60 * 24))) diff --git a/apps/x/apps/renderer/src/lib/billing-error.ts b/apps/x/apps/renderer/src/lib/billing-error.ts index ffb77470..bfbb3266 100644 --- a/apps/x/apps/renderer/src/lib/billing-error.ts +++ b/apps/x/apps/renderer/src/lib/billing-error.ts @@ -8,7 +8,7 @@ export const BILLING_ERROR_PATTERNS = [ { pattern: /not enough credits/i, title: "You've run out of credits", - subtitle: 'Upgrade your plan for more credits. Free usage resets daily at 00:00 UTC.', + subtitle: 'Upgrade your plan for more monthly credits. Daily credits reset at 00:00 UTC.', cta: 'Upgrade plan', }, { diff --git a/apps/x/packages/core/src/billing/billing.ts b/apps/x/packages/core/src/billing/billing.ts index 22bc3d5a..0b088be4 100644 --- a/apps/x/packages/core/src/billing/billing.ts +++ b/apps/x/packages/core/src/billing/billing.ts @@ -20,8 +20,17 @@ export async function getBillingInfo(): Promise { status: string | null; trialExpiresAt: string | null; usage: { - sanctionedCredits: number; - availableCredits: number; + monthly: { + sanctionedCredits: number; + usedCredits: number; + availableCredits: number; + }; + daily: { + sanctionedCredits: number; + usedCredits: number; + availableCredits: number; + usageDay: string; + }; }; }; }; @@ -31,7 +40,7 @@ export async function getBillingInfo(): Promise { subscriptionPlan: body.billing.plan, subscriptionStatus: body.billing.status, trialExpiresAt: body.billing.trialExpiresAt ?? null, - sanctionedCredits: body.billing.usage.sanctionedCredits, - availableCredits: body.billing.usage.availableCredits, + monthly: body.billing.usage.monthly, + daily: body.billing.usage.daily, }; } diff --git a/apps/x/packages/shared/src/billing.ts b/apps/x/packages/shared/src/billing.ts index dca3343a..59fe68d3 100644 --- a/apps/x/packages/shared/src/billing.ts +++ b/apps/x/packages/shared/src/billing.ts @@ -3,13 +3,22 @@ import { z } from 'zod'; export const BillingPlanSchema = z.enum(['free', 'starter', 'pro']); export type BillingPlan = z.infer; +export const BillingUsageBucketSchema = z.object({ + sanctionedCredits: z.number(), + usedCredits: z.number(), + availableCredits: z.number(), +}); +export type BillingUsageBucket = z.infer; + export const BillingInfoSchema = z.object({ userEmail: z.string().nullable(), userId: z.string().nullable(), subscriptionPlan: BillingPlanSchema.nullable(), subscriptionStatus: z.string().nullable(), trialExpiresAt: z.string().nullable(), - sanctionedCredits: z.number(), - availableCredits: z.number(), + monthly: BillingUsageBucketSchema, + daily: BillingUsageBucketSchema.extend({ + usageDay: z.string(), + }), }); export type BillingInfo = z.infer;