278 lines
11 KiB
TypeScript
278 lines
11 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { useRouter, useSearchParams } from "next/navigation";
|
|
import { getUserFromSession } from "@/lib/auth/get-user";
|
|
|
|
interface SubscriptionInfo {
|
|
status: "trialing" | "active" | "expired" | "canceled" | "canceling";
|
|
isActive: boolean;
|
|
daysRemainingInTrial: number | null;
|
|
trialEndsAt: string | null;
|
|
subscriptionEndsAt: string | null;
|
|
}
|
|
|
|
export default function BillingPage() {
|
|
const router = useRouter();
|
|
const searchParams = useSearchParams();
|
|
const [subscriptionInfo, setSubscriptionInfo] = useState<SubscriptionInfo | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [checkoutLoading, setCheckoutLoading] = useState(false);
|
|
const [cancelLoading, setCancelLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const canceled = searchParams.get("canceled");
|
|
|
|
useEffect(() => {
|
|
async function loadSubscriptionStatus() {
|
|
try {
|
|
const res = await fetch("/api/subscription/status");
|
|
if (!res.ok) {
|
|
throw new Error("Failed to load subscription status");
|
|
}
|
|
const data = await res.json();
|
|
setSubscriptionInfo(data);
|
|
setLoading(false);
|
|
} catch (err: any) {
|
|
setError(err.message);
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
loadSubscriptionStatus();
|
|
}, [searchParams]);
|
|
|
|
const handleCheckout = async () => {
|
|
setCheckoutLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const res = await fetch("/api/stripe/subscription/checkout", {
|
|
method: "POST",
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const error = await res.json();
|
|
throw new Error(error.error || "Failed to create checkout session");
|
|
}
|
|
|
|
const { url } = await res.json();
|
|
if (url) {
|
|
window.location.href = url;
|
|
}
|
|
} catch (err: any) {
|
|
setError(err.message);
|
|
setCheckoutLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleCancel = async () => {
|
|
setCancelLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const res = await fetch("/api/stripe/subscription/portal", {
|
|
method: "POST",
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const error = await res.json();
|
|
throw new Error(error.error || "Failed to open billing portal");
|
|
}
|
|
|
|
const { url } = await res.json();
|
|
if (url) {
|
|
window.location.href = url;
|
|
}
|
|
} catch (err: any) {
|
|
setError(err.message);
|
|
setCancelLoading(false);
|
|
}
|
|
};
|
|
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="min-h-screen bg-gradient-to-b from-slate-50 to-white flex items-center justify-center">
|
|
<div className="text-center">
|
|
<div className="inline-flex items-center justify-center w-12 h-12 mb-4">
|
|
<span className="w-3 h-3 bg-blue-600 rounded-full animate-spin"></span>
|
|
</div>
|
|
<p className="text-slate-600">Loading billing information…</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gradient-to-b from-slate-50 to-white">
|
|
{/* Navigation */}
|
|
<nav className="fixed top-0 left-0 right-0 z-50 bg-white/80 backdrop-blur-sm border-b border-slate-200">
|
|
<div className="max-w-6xl mx-auto px-6 h-16 flex items-center justify-between">
|
|
<button
|
|
onClick={() => router.push("/dashboard")}
|
|
className="text-xl font-bold bg-gradient-to-r from-blue-600 to-blue-700 bg-clip-text text-transparent hover:opacity-80"
|
|
>
|
|
S2X
|
|
</button>
|
|
<div className="text-sm text-slate-600">Billing</div>
|
|
</div>
|
|
</nav>
|
|
|
|
<main className="pt-20">
|
|
<div className="max-w-3xl mx-auto px-6 py-16">
|
|
{/* Canceled Message */}
|
|
{canceled && (
|
|
<div className="mb-6 p-4 bg-amber-50 border border-amber-200 rounded-lg">
|
|
<p className="text-sm text-amber-700 font-medium">
|
|
Payment was canceled. Please try again or contact support.
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Header */}
|
|
<div className="mb-12">
|
|
<h1 className="text-4xl font-bold text-slate-900 mb-3">Billing & Subscription</h1>
|
|
<p className="text-lg text-slate-600">
|
|
Manage your subscription and billing information.
|
|
</p>
|
|
</div>
|
|
|
|
{/* Error */}
|
|
{error && (
|
|
<div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg">
|
|
<p className="text-sm text-red-700">{error}</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Status Card */}
|
|
{subscriptionInfo && (
|
|
<div className="bg-white border border-slate-200 rounded-xl shadow-sm p-8 mb-8">
|
|
<h2 className="text-2xl font-bold text-slate-900 mb-6">Current Status</h2>
|
|
|
|
{/* Status Badge */}
|
|
<div className="mb-8">
|
|
{subscriptionInfo.status === "active" && (
|
|
<div className="inline-flex items-center gap-2 px-4 py-2 bg-green-100 border border-green-300 rounded-full">
|
|
<span className="w-2 h-2 bg-green-600 rounded-full"></span>
|
|
<span className="text-sm font-semibold text-green-700">Subscription Active</span>
|
|
</div>
|
|
)}
|
|
{subscriptionInfo.status === "trialing" && (
|
|
<div className="inline-flex items-center gap-2 px-4 py-2 bg-blue-100 border border-blue-300 rounded-full">
|
|
<span className="w-2 h-2 bg-blue-600 rounded-full animate-pulse"></span>
|
|
<span className="text-sm font-semibold text-blue-700">
|
|
{subscriptionInfo.daysRemainingInTrial} days free trial remaining
|
|
</span>
|
|
</div>
|
|
)}
|
|
{subscriptionInfo.status === "expired" && (
|
|
<div className="inline-flex items-center gap-2 px-4 py-2 bg-red-100 border border-red-300 rounded-full">
|
|
<span className="w-2 h-2 bg-red-600 rounded-full"></span>
|
|
<span className="text-sm font-semibold text-red-700">Trial Expired</span>
|
|
</div>
|
|
)}
|
|
{subscriptionInfo.status === "canceling" && (
|
|
<div className="flex flex-col gap-3">
|
|
<div className="inline-flex items-center gap-2 px-4 py-2 bg-amber-100 border border-amber-300 rounded-full w-fit">
|
|
<span className="w-2 h-2 bg-amber-600 rounded-full animate-pulse"></span>
|
|
<span className="text-sm font-semibold text-amber-700">Scheduled for Cancellation</span>
|
|
</div>
|
|
{subscriptionInfo.subscriptionEndsAt && (
|
|
<p className="text-sm text-slate-700">
|
|
Ends on <span className="font-semibold text-amber-700">{new Date(subscriptionInfo.subscriptionEndsAt).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}</span>
|
|
</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
{subscriptionInfo.status === "canceled" && (
|
|
<div className="inline-flex items-center gap-2 px-4 py-2 bg-orange-100 border border-orange-300 rounded-full">
|
|
<span className="w-2 h-2 bg-orange-600 rounded-full"></span>
|
|
<span className="text-sm font-semibold text-orange-700">Subscription Canceled</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Manage Subscription Section */}
|
|
{(subscriptionInfo.status === "active" || subscriptionInfo.status === "canceling") && (
|
|
<div className="pt-4 border-t border-slate-200">
|
|
<p className="text-xs text-slate-600">
|
|
{subscriptionInfo.status === "canceling"
|
|
? "You can reactivate your subscription anytime. "
|
|
: "Manage your subscription: "}
|
|
<button
|
|
onClick={handleCancel}
|
|
disabled={cancelLoading}
|
|
className="text-xs text-slate-500 hover:text-slate-700 underline disabled:opacity-50"
|
|
>
|
|
{cancelLoading ? "Opening..." : "Billing Portal"}
|
|
</button>
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Pricing Card */}
|
|
{!subscriptionInfo?.isActive && (
|
|
<div className="bg-gradient-to-br from-blue-50 to-blue-100 border-2 border-blue-200 rounded-xl p-8 mb-8">
|
|
<h2 className="text-2xl font-bold text-slate-900 mb-2">Upgrade to Pro</h2>
|
|
<p className="text-slate-700 mb-6">
|
|
Continue using Stripe to Invoice after your free trial ends.
|
|
</p>
|
|
|
|
<div className="mb-8">
|
|
<div className="flex items-baseline gap-2 mb-4">
|
|
<span className="text-4xl font-bold text-slate-900">£50</span>
|
|
<span className="text-slate-600">/month</span>
|
|
</div>
|
|
<p className="text-sm text-slate-700 mb-4">
|
|
✓ Unlimited invoice creation<br />
|
|
✓ Real-time Stripe to Xero sync<br />
|
|
✓ Priority support<br />
|
|
✓ Cancel anytime
|
|
</p>
|
|
</div>
|
|
|
|
<button
|
|
onClick={handleCheckout}
|
|
disabled={checkoutLoading}
|
|
className="w-full px-6 py-3 bg-gradient-to-r from-blue-600 to-blue-700 text-white font-semibold rounded-lg hover:shadow-lg hover:from-blue-700 hover:to-blue-800 transition disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
{checkoutLoading ? "Redirecting..." : "Upgrade Now →"}
|
|
</button>
|
|
|
|
<p className="text-xs text-slate-600 text-center mt-4">
|
|
Secure payment powered by Stripe
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* FAQ */}
|
|
<div className="bg-white border border-slate-200 rounded-xl p-8">
|
|
<h2 className="text-xl font-bold text-slate-900 mb-6">FAQ</h2>
|
|
<div className="space-y-6">
|
|
<div>
|
|
<h3 className="font-semibold text-slate-900 mb-2">Can I cancel anytime?</h3>
|
|
<p className="text-sm text-slate-600">
|
|
Yes, you can cancel your subscription anytime. Just click "Manage subscription" in the Current Status section above.
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<h3 className="font-semibold text-slate-900 mb-2">What happens after the trial?</h3>
|
|
<p className="text-sm text-slate-600">
|
|
After your 14-day trial ends, your access will be limited until you upgrade to a paid subscription.
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<h3 className="font-semibold text-slate-900 mb-2">Is my payment information secure?</h3>
|
|
<p className="text-sm text-slate-600">
|
|
All payments are processed securely through Stripe. We never see or store your credit card information.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|