mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-16 18:25:17 +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';
|
'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}>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue