mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-25 18:55:19 +02:00
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
This commit is contained in:
parent
eb4b11a530
commit
7cd661d726
5 changed files with 73 additions and 12 deletions
|
|
@ -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 (
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-xs font-medium text-muted-foreground">{label}</p>
|
||||
{helper ? <p className="text-[11px] text-muted-foreground">{helper}</p> : null}
|
||||
</div>
|
||||
<p className="shrink-0 text-xs font-medium tabular-nums">
|
||||
{pct}%
|
||||
</p>
|
||||
</div>
|
||||
<div className="h-2 overflow-hidden rounded-full bg-muted">
|
||||
<div className="h-full rounded-full bg-primary transition-all" style={{ width: `${pct}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function AccountSettings({ dialogOpen }: AccountSettingsProps) {
|
||||
const [isRowboatConnected, setIsRowboatConnected] = useState(false)
|
||||
const [connectionLoading, setConnectionLoading] = useState(true)
|
||||
|
|
@ -164,7 +197,7 @@ export function AccountSettings({ dialogOpen }: AccountSettingsProps) {
|
|||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium capitalize">
|
||||
{billing.subscriptionPlan ? `${billing.subscriptionPlan} Plan` : 'No Plan'}
|
||||
{formatPlanName(billing.subscriptionPlan)}
|
||||
</p>
|
||||
{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 && (
|
||||
<p className="text-xs text-muted-foreground">Subscribe to access AI features</p>
|
||||
)}
|
||||
{billing.subscriptionPlan === 'free' && (
|
||||
<p className="text-xs text-muted-foreground">Free usage resets daily at 00:00 UTC.</p>
|
||||
)}
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onClick={() => appUrl && window.open(`${appUrl}?intent=upgrade`)}>
|
||||
{!billing.subscriptionPlan ? 'Subscribe' : billing.subscriptionPlan === 'free' ? 'Upgrade' : 'Change plan'}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="space-y-3 border-t pt-3">
|
||||
<CreditUsageBar label="Monthly credits" bucket={billing.monthly} />
|
||||
<CreditUsageBar
|
||||
label="Daily credits"
|
||||
bucket={billing.daily}
|
||||
helper="Resets daily at 00:00 UTC"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-xs text-muted-foreground">Unable to load plan details</p>
|
||||
|
|
|
|||
|
|
@ -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({
|
|||
<div className="flex items-center justify-between rounded-lg border border-sidebar-border bg-sidebar-accent/20 px-3 py-2">
|
||||
<div className="min-w-0">
|
||||
<span className="text-xs font-medium capitalize text-sidebar-foreground">
|
||||
{billing.subscriptionPlan ? `${billing.subscriptionPlan} plan` : 'No plan'}
|
||||
{formatBillingPlanName(billing.subscriptionPlan)}
|
||||
</span>
|
||||
{billing.subscriptionStatus === 'trialing' && billing.trialExpiresAt && (() => {
|
||||
const days = Math.max(0, Math.ceil((new Date(billing.trialExpiresAt).getTime() - Date.now()) / (1000 * 60 * 60 * 24)))
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -20,8 +20,17 @@ export async function getBillingInfo(): Promise<BillingInfo> {
|
|||
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<BillingInfo> {
|
|||
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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,13 +3,22 @@ import { z } from 'zod';
|
|||
export const BillingPlanSchema = z.enum(['free', 'starter', 'pro']);
|
||||
export type BillingPlan = z.infer<typeof BillingPlanSchema>;
|
||||
|
||||
export const BillingUsageBucketSchema = z.object({
|
||||
sanctionedCredits: z.number(),
|
||||
usedCredits: z.number(),
|
||||
availableCredits: z.number(),
|
||||
});
|
||||
export type BillingUsageBucket = z.infer<typeof BillingUsageBucketSchema>;
|
||||
|
||||
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<typeof BillingInfoSchema>;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue