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

View file

@ -17,8 +17,9 @@ const GUEST_BILLING_CUSTOMER = {
name: "Guest", name: "Guest",
email: "guest@rowboatlabs.com", email: "guest@rowboatlabs.com",
stripeCustomerId: "guest", stripeCustomerId: "guest",
stripeSubscriptionId: "test",
subscriptionPlan: "free" as const, subscriptionPlan: "free" as const,
subscriptionActive: true, subscriptionStatus: "active" as const,
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),
updatedAt: 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> { 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', method: 'POST',
headers: { headers: {
'Authorization': `Bearer ${BILLING_API_KEY}`, '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>>> { export async function requireActiveBillingSubscription(): Promise<WithStringId<z.infer<typeof Customer>>> {
const billingCustomer = await requireBillingCustomer(); const billingCustomer = await requireBillingCustomer();
if (USE_BILLING && !billingCustomer?.subscriptionActive) { if (USE_BILLING && billingCustomer.subscriptionStatus !== "active" && billingCustomer.subscriptionStatus !== "past_due") {
redirect('/billing/checkout'); redirect('/billing');
} }
return billingCustomer; return billingCustomer;
} }

View file

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

View file

@ -1,7 +1,10 @@
'use client'; 'use client';
import { ReactNode, useState } from 'react'; import { ReactNode, useEffect, useState } from 'react';
import Sidebar from './sidebar'; import Sidebar from './sidebar';
import { usePathname } from 'next/navigation'; import { usePathname } from 'next/navigation';
import { getCustomer } from '../../../actions/billing_actions';
import { Button } from '@heroui/react';
import { useRouter } from 'next/navigation';
interface AppLayoutProps { interface AppLayoutProps {
children: ReactNode; children: ReactNode;
@ -11,14 +14,31 @@ interface AppLayoutProps {
} }
export default function AppLayout({ children, useRag = false, useAuth = false, useBilling = false }: AppLayoutProps) { export default function AppLayout({ children, useRag = false, useAuth = false, useBilling = false }: AppLayoutProps) {
const router = useRouter();
const [sidebarCollapsed, setSidebarCollapsed] = useState(true); const [sidebarCollapsed, setSidebarCollapsed] = useState(true);
const [billingPastDue, setBillingPastDue] = useState(false);
const pathname = usePathname(); const pathname = usePathname();
let projectId: string|null = null; let projectId: string | null = null;
if (pathname.startsWith('/projects')) { if (pathname.startsWith('/projects')) {
projectId = pathname.split('/')[2]; 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 // Layout with sidebar for all routes
return ( return (
<div className="h-screen flex gap-5 p-5 bg-zinc-50 dark:bg-zinc-900"> <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> </div>
{/* Main content area */} {/* Main content area */}
<main className="flex-1 overflow-auto rounded-xl bg-white dark:bg-zinc-800 shadow-sm p-4"> <main className="flex gap-2 flex-col flex-1 overflow-auto rounded-xl bg-white dark:bg-zinc-800 shadow-sm p-4">
{children} {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> </main>
</div> </div>
); );