mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-04-25 00:16:29 +02:00
billing updates
This commit is contained in:
parent
4183b67e59
commit
f25e3e2ed4
4 changed files with 63 additions and 15 deletions
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue