import { json, type LoaderFunctionArgs, type ActionFunctionArgs, } from "@remix-run/node"; import { useLoaderData, useFetcher } from "@remix-run/react"; import { requireUser, requireWorkpace } from "~/services/session.server"; import { getUsageSummary } from "~/services/billing.server"; import { createCheckoutSession, createBillingPortalSession, downgradeSubscription, } from "~/services/stripe.server"; import { CreditCard, TrendingUp, Calendar, AlertCircle } from "lucide-react"; import { Button } from "~/components/ui/button"; import { Card } from "~/components/ui/card"; import { Badge } from "~/components/ui/badge"; import { Progress } from "~/components/ui/progress"; import { useState } from "react"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "~/components/ui/dialog"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "~/components/ui/alert-dialog"; import { prisma } from "~/db.server"; import { isBillingEnabled } from "~/config/billing.server"; export const loader = async ({ request }: LoaderFunctionArgs) => { const user = await requireUser(request); const workspace = await requireWorkpace(request); // Get usage summary const usageSummary = await getUsageSummary(workspace.id); // Get billing history const subscription = await prisma.subscription.findUnique({ where: { workspaceId: workspace.id }, include: { BillingHistory: { orderBy: { createdAt: "desc" }, take: 10, }, }, }); const billingEnabled = isBillingEnabled(); return json({ user, workspace, usageSummary: usageSummary as any, billingHistory: subscription?.BillingHistory || [], billingEnabled, subscription: subscription ? { status: subscription.status, planType: subscription.planType, currentPeriodEnd: subscription.currentPeriodEnd, } : null, }); }; export const action = async ({ request }: ActionFunctionArgs) => { const user = await requireUser(request); const workspace = await requireWorkpace(request); const formData = await request.formData(); const intent = formData.get("intent"); if (intent === "upgrade") { const planType = formData.get("planType") as "PRO" | "MAX"; const origin = new URL(request.url).origin; const checkoutUrl = await createCheckoutSession({ workspaceId: workspace.id, planType, email: user.email, successUrl: `${origin}/settings/billing?success=true`, cancelUrl: `${origin}/settings/billing?canceled=true`, }); return json({ checkoutUrl }); } if (intent === "manage") { const origin = new URL(request.url).origin; const portalUrl = await createBillingPortalSession({ workspaceId: workspace.id, returnUrl: `${origin}/settings/billing`, }); return json({ portalUrl }); } if (intent === "downgrade") { const targetPlan = formData.get("planType") as "FREE" | "PRO"; // Downgrade subscription - keeps credits until period end, then switches to new plan await downgradeSubscription({ workspaceId: workspace.id, newPlanType: targetPlan, }); return json({ success: true, message: `Successfully scheduled downgrade to ${targetPlan}. Your current credits will remain available until the end of your billing period.`, }); } return json({ error: "Invalid intent" }, { status: 400 }); }; export default function BillingSettings() { const { usageSummary, billingHistory, billingEnabled, subscription } = useLoaderData(); const fetcher = useFetcher(); const [showPlansModal, setShowPlansModal] = useState(false); const [showDowngradeDialog, setShowDowngradeDialog] = useState(false); const [targetDowngradePlan, setTargetDowngradePlan] = useState< "FREE" | "PRO" | null >(null); // Handle upgrade action const handleUpgrade = (planType: "PRO" | "MAX") => { fetcher.submit({ intent: "upgrade", planType }, { method: "POST" }); }; // Handle downgrade action const handleDowngrade = (planType: "FREE" | "PRO") => { setTargetDowngradePlan(planType); setShowDowngradeDialog(true); }; // Confirm and execute downgrade const confirmDowngrade = () => { if (targetDowngradePlan) { fetcher.submit( { intent: "downgrade", planType: targetDowngradePlan }, { method: "POST" }, ); setShowDowngradeDialog(false); setTargetDowngradePlan(null); } }; // Determine if plan is upgrade, downgrade, or current const getPlanAction = (targetPlan: "FREE" | "PRO" | "MAX") => { const planOrder = { FREE: 0, PRO: 1, MAX: 2 }; const currentOrder = planOrder[usageSummary.plan.type as keyof typeof planOrder]; const targetOrder = planOrder[targetPlan]; if (currentOrder === targetOrder) return "current"; if (targetOrder > currentOrder) return "upgrade"; return "downgrade"; }; // Handle plan selection const handlePlanSelect = (planType: "FREE" | "PRO" | "MAX") => { const action = getPlanAction(planType); if (action === "current") return; if (action === "upgrade") { handleUpgrade(planType as "PRO" | "MAX"); } else { handleDowngrade(planType as "FREE" | "PRO"); } }; // Show success message after downgrade if (fetcher.data && "success" in fetcher.data && fetcher.data.success) { // Close modal and show message setTimeout(() => { setShowPlansModal(false); window.location.reload(); // Reload to show updated plan info }, 1500); } // Redirect to checkout/portal when URL is received if ( fetcher.data && "checkoutUrl" in fetcher.data && fetcher.data.checkoutUrl ) { window.location.href = fetcher.data.checkoutUrl; } if (fetcher.data && "portalUrl" in fetcher.data && fetcher.data.portalUrl) { window.location.href = fetcher.data.portalUrl; } if (!billingEnabled) { return (

Billing

Billing is disabled in self-hosted mode. You have unlimited usage.

); } if (!usageSummary) { return (

Billing

No billing information available.

); } return (
{/* Header */}

Billing

Manage your subscription, usage, and billing history

{/* Usage Section */}

Current Usage

{/* Credits Card */}
Credits
{usageSummary.credits.available} {" "} / {usageSummary.credits.monthly}

{usageSummary.credits.percentageUsed}% used this period

{/* Usage Breakdown */}
Usage Breakdown
Episodes {usageSummary.usage.episodes}
Searches {usageSummary.usage.searches}
Chat {usageSummary.usage.chat}
{/* Billing Cycle */}
Billing Cycle
{usageSummary.billingCycle.daysRemaining} days left

Resets on{" "} {new Date(usageSummary.billingCycle.end).toLocaleDateString()}

{/* Overage Warning */} {usageSummary.credits.overage > 0 && (

Overage Usage Detected

You've used {usageSummary.credits.overage} additional credits beyond your monthly allocation. {usageSummary.overage.enabled && usageSummary.overage.pricePerCredit && ( <> {" "} This will cost $ {( usageSummary.credits.overage * usageSummary.overage.pricePerCredit ).toFixed(2)}{" "} extra this month. )}

)}
{/* Plan Section */}

Plan

{usageSummary.plan.name}

{usageSummary.plan.type}

{usageSummary.credits.monthly} credits/month {usageSummary.overage.enabled && ( <> + ${usageSummary.overage.pricePerCredit}/credit overage )}

{subscription?.status === "CANCELED" && subscription.planType !== "FREE" && (

Downgrading to FREE plan on{" "} {new Date( subscription.currentPeriodEnd, ).toLocaleDateString()} . Your current credits and plan will remain active until then.

)}
{/* Invoices Section */}

Invoices

{billingHistory.length === 0 ? (

No invoices yet

) : (
{billingHistory.map((invoice) => (

{new Date(invoice.periodStart).toLocaleDateString()} -{" "} {new Date(invoice.periodEnd).toLocaleDateString()}

${invoice.totalAmount.toFixed(2)}

{invoice.stripePaymentStatus || "pending"}
))}
)}
{/* Plans Modal */} Choose Your CORE Plan Unlock the power of portable memory
{/* Free Plan */}

Free

No credit card required

$0 /month
  • Memory facts: 3k/mo
  • NO USAGE BASED
{/* Pro Plan */}

Pro

For Everyday Productivity

$19 /month
  • Memory facts: 15k/mo
  • $0.299 /1K ADDITIONAL FACTS
{/* Max Plan */}

Max

Get the most out of CORE

$99 /month
  • Memory facts: 100k/mo
  • $0.249 /1K ADDITIONAL FACTS
{/* Downgrade Confirmation Dialog */} Confirm Downgrade Are you sure you want to downgrade to the{" "} {targetDowngradePlan} plan? Your current credits will remain available until the end of your billing period, then you'll be switched to the {targetDowngradePlan} plan. Cancel Continue
); }