billing updates

This commit is contained in:
Ramnique Singh 2025-06-08 14:54:53 +05:30
parent 4183b67e59
commit f25e3e2ed4
4 changed files with 63 additions and 15 deletions

View file

@ -1,6 +1,6 @@
'use client';
import { Progress, Badge } from "@heroui/react";
import { Progress, Badge, Chip } from "@heroui/react";
import { Button } from "@/components/ui/button";
import { Label } from "@/app/lib/components/label";
import { Customer, UsageResponse, UsageType } from "@/app/lib/types/billing_types";
@ -32,9 +32,19 @@ interface BillingPageProps {
usage: z.infer<typeof UsageResponse>;
}
function getDisplayStatus(status: string | undefined) {
if (status === "active") {
return "Active";
} else if (status === "past_due") {
return "Past Due!";
} else {
return "Inactive";
}
}
export function BillingPage({ customer, usage }: BillingPageProps) {
const plan = customer.subscriptionPlan || "free";
const isActive = customer.subscriptionActive || false;
const displayStatus = getDisplayStatus(customer.subscriptionStatus);
const planInfo = planDetails[plan];
async function handleManageSubscription() {
@ -77,13 +87,13 @@ export function BillingPage({ customer, usage }: BillingPageProps) {
)}>
{planInfo.name}
</h3>
<Badge
color={isActive ? "success" : "warning"}
<Chip
color={customer.subscriptionStatus === "active" ? "success" : "danger"}
variant="flat"
className="text-xs"
>
{isActive ? "Active" : "Inactive"}
</Badge>
{displayStatus}
</Chip>
</div>
</div>
<form action={handleManageSubscription}>

View file

@ -17,8 +17,9 @@ const GUEST_BILLING_CUSTOMER = {
name: "Guest",
email: "guest@rowboatlabs.com",
stripeCustomerId: "guest",
stripeSubscriptionId: "test",
subscriptionPlan: "free" as const,
subscriptionActive: true,
subscriptionStatus: "active" as const,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
@ -190,7 +191,7 @@ export async function getPrices(): Promise<z.infer<typeof PricesResponse>> {
}
export async function updateSubscriptionPlan(customerId: string, request: z.infer<typeof UpdateSubscriptionPlanRequest>): Promise<string> {
const response = await fetch(`${BILLING_API_URL}/api/customers/${customerId}/update-subscription-plan`, {
const response = await fetch(`${BILLING_API_URL}/api/customers/${customerId}/update-sub-session`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${BILLING_API_KEY}`,
@ -296,8 +297,8 @@ export async function requireBillingCustomer(): Promise<WithStringId<z.infer<typ
export async function requireActiveBillingSubscription(): Promise<WithStringId<z.infer<typeof Customer>>> {
const billingCustomer = await requireBillingCustomer();
if (USE_BILLING && !billingCustomer?.subscriptionActive) {
redirect('/billing/checkout');
if (USE_BILLING && billingCustomer.subscriptionStatus !== "active" && billingCustomer.subscriptionStatus !== "past_due") {
redirect('/billing');
}
return billingCustomer;
}

View file

@ -13,8 +13,9 @@ export const Customer = z.object({
userId: z.string(),
email: z.string(),
stripeCustomerId: z.string(),
stripeSubscriptionId: z.string().optional(),
subscriptionPlan: SubscriptionPlan.optional(),
subscriptionActive: z.boolean().optional(),
subscriptionStatus: z.enum([ 'active', 'past_due' ]).optional(),
createdAt: z.string().datetime(),
updatedAt: z.string().datetime(),
subscriptionPlanUpdatedAt: z.string().datetime().optional(),

View file

@ -1,7 +1,10 @@
'use client';
import { ReactNode, useState } from 'react';
import { ReactNode, useEffect, useState } from 'react';
import Sidebar from './sidebar';
import { usePathname } from 'next/navigation';
import { getCustomer } from '../../../actions/billing_actions';
import { Button } from '@heroui/react';
import { useRouter } from 'next/navigation';
interface AppLayoutProps {
children: ReactNode;
@ -11,14 +14,31 @@ interface AppLayoutProps {
}
export default function AppLayout({ children, useRag = false, useAuth = false, useBilling = false }: AppLayoutProps) {
const router = useRouter();
const [sidebarCollapsed, setSidebarCollapsed] = useState(true);
const [billingPastDue, setBillingPastDue] = useState(false);
const pathname = usePathname();
let projectId: string|null = null;
let projectId: string | null = null;
if (pathname.startsWith('/projects')) {
projectId = pathname.split('/')[2];
}
useEffect(() => {
async function checkBillingPastDue() {
const billingCustomer = await getCustomer();
if (billingCustomer.subscriptionStatus === "past_due") {
setBillingPastDue(true);
}
}
if (!useBilling) {
return;
}
checkBillingPastDue();
}, [useBilling]);
// Layout with sidebar for all routes
return (
<div className="h-screen flex gap-5 p-5 bg-zinc-50 dark:bg-zinc-900">
@ -35,8 +55,24 @@ export default function AppLayout({ children, useRag = false, useAuth = false, u
</div>
{/* Main content area */}
<main className="flex-1 overflow-auto rounded-xl bg-white dark:bg-zinc-800 shadow-sm p-4">
{children}
<main className="flex gap-2 flex-col flex-1 overflow-auto rounded-xl bg-white dark:bg-zinc-800 shadow-sm p-4">
{billingPastDue && <div className="shrink-0">
<div className="bg-red-50 text-red-500 px-2 py-1 text-sm rounded-md flex items-center gap-2">
<span>Your subscription is past due. Please update your payment information to avoid losing access to your projects.</span>
<Button
variant="flat"
color="danger"
size="sm"
onPress={() => {
router.push('/billing');
}}>
Resolve
</Button>
</div>
</div>}
<div className="flex-1 overflow-auto">
{children}
</div>
</main>
</div>
);