vulnzap-mcp
Version:
Multi-ecosystem vulnerability scanning service with MCP interface for LLMs
327 lines (310 loc) • 18.5 kB
JSX
"use client";
import { useEffect, useState } from 'react';
import { DashboardLayout } from '../../../components/dashboard/dashboard-layout';
import { useAuth } from '../../../contexts/auth-context';
import { getUserSubscription, createCheckoutSession, cancelSubscription } from '../../../utils/stripe-client';
import { withAuth } from '../../../contexts/auth-context';
function BillingPage() {
const { user } = useAuth();
const [subscription, setSubscription] = useState({ tier: 'free', status: 'inactive' });
const [loading, setLoading] = useState(true);
const [actionLoading, setActionLoading] = useState(false);
useEffect(() => {
// Load user subscription
const loadSubscription = async () => {
setLoading(true);
try {
const { success, data } = await getUserSubscription();
if (success && data) {
setSubscription(data.subscription || { tier: 'free', status: 'inactive' });
}
} catch (error) {
console.error('Error loading subscription:', error);
} finally {
setLoading(false);
}
};
if (user) {
loadSubscription();
}
}, [user]);
const handleUpgrade = async (tier) => {
setActionLoading(true);
try {
let priceId;
if (tier === 'pro') {
priceId = process.env.NEXT_PUBLIC_STRIPE_PRICE_ID_PRO;
} else if (tier === 'enterprise') {
priceId = process.env.NEXT_PUBLIC_STRIPE_PRICE_ID_ENTERPRISE;
}
if (!priceId) {
throw new Error(`Invalid price ID for tier: ${tier}`);
}
await createCheckoutSession(priceId);
} catch (error) {
console.error('Error creating checkout session:', error);
alert('Failed to redirect to checkout. Please try again.');
} finally {
setActionLoading(false);
}
};
const handleCancel = async () => {
if (!confirm('Are you sure you want to cancel your subscription? You will lose access to premium features at the end of your billing period.')) {
return;
}
setActionLoading(true);
try {
const { success, error } = await cancelSubscription();
if (success) {
setSubscription({ ...subscription, status: 'canceled' });
alert('Your subscription has been canceled. You will have access until the end of your billing period.');
} else {
throw new Error(error || 'Failed to cancel subscription');
}
} catch (error) {
console.error('Error canceling subscription:', error);
alert('Failed to cancel subscription. Please try again.');
} finally {
setActionLoading(false);
}
};
return (
<DashboardLayout>
<div className="grid gap-6">
<div className="flex flex-col space-y-2">
<h1 className="text-3xl font-bold tracking-tight">Billing</h1>
<p className="text-muted-foreground">Manage your subscription and billing information</p>
</div>
{/* Current subscription */}
<div className="rounded-lg border border-border bg-card overflow-hidden">
<div className="p-6">
<h2 className="text-xl font-semibold mb-4">Current Plan</h2>
{loading ? (
<div className="flex items-center justify-center p-8">
<div className="h-6 w-6 animate-spin rounded-full border-2 border-primary border-t-transparent"></div>
</div>
) : (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<h3 className="text-sm font-medium text-muted-foreground">Plan</h3>
<p className="text-lg font-medium capitalize">{subscription.tier}</p>
</div>
<div>
<h3 className="text-sm font-medium text-muted-foreground">Status</h3>
<p className="text-lg font-medium capitalize">{subscription.status || 'inactive'}</p>
</div>
</div>
{/* Action buttons based on subscription status */}
<div className="flex flex-wrap gap-4 mt-6">
{subscription.tier === 'free' && (
<>
<button
onClick={() => handleUpgrade('pro')}
disabled={actionLoading}
className="inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground shadow hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none"
>
{actionLoading ? 'Processing...' : 'Upgrade to Pro'}
</button>
<button
onClick={() => handleUpgrade('enterprise')}
disabled={actionLoading}
className="inline-flex items-center justify-center rounded-md bg-card border border-border px-4 py-2 text-sm font-medium shadow hover:bg-card/80 focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none"
>
{actionLoading ? 'Processing...' : 'Upgrade to Enterprise'}
</button>
</>
)}
{(subscription.tier === 'pro' || subscription.tier === 'enterprise') && (
subscription.status === 'active' ? (
<button
onClick={handleCancel}
disabled={actionLoading}
className="inline-flex items-center justify-center rounded-md bg-destructive/10 text-destructive px-4 py-2 text-sm font-medium shadow hover:bg-destructive/20 focus:outline-none focus:ring-2 focus:ring-destructive focus:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none"
>
{actionLoading ? 'Processing...' : 'Cancel Subscription'}
</button>
) : (
<button
onClick={() => handleUpgrade(subscription.tier)}
disabled={actionLoading}
className="inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground shadow hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none"
>
{actionLoading ? 'Processing...' : 'Renew Subscription'}
</button>
)
)}
</div>
</div>
)}
</div>
</div>
{/* Plans comparison */}
<div className="rounded-lg border border-border overflow-hidden">
<div className="p-6">
<h2 className="text-xl font-semibold mb-4">Compare Plans</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{/* Free Plan */}
<div className={`rounded-lg border ${subscription.tier === 'free' ? 'border-primary' : 'border-border'} p-6`}>
<h3 className="text-xl font-semibold">Free</h3>
<p className="text-3xl font-bold my-3">$0</p>
<p className="text-muted-foreground text-sm mb-6">Basic features for solo developers</p>
<ul className="space-y-2 mb-6">
<li className="flex items-start gap-2">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-primary w-5 h-5 mt-0.5">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
<span>Real-time vulnerability scanning</span>
</li>
<li className="flex items-start gap-2">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-primary w-5 h-5 mt-0.5">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
<span>Basic MCP integration</span>
</li>
<li className="flex items-start gap-2">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-primary w-5 h-5 mt-0.5">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
<span>GitHub Advisory Database</span>
</li>
<li className="flex items-start gap-2">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-muted-foreground w-5 h-5 mt-0.5">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
<span className="text-muted-foreground">Zero-day vulnerability alerts</span>
</li>
<li className="flex items-start gap-2">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-muted-foreground w-5 h-5 mt-0.5">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
<span className="text-muted-foreground">AI-generated code analysis</span>
</li>
</ul>
{subscription.tier === 'free' ? (
<button disabled className="w-full inline-flex items-center justify-center rounded-md bg-primary/30 px-4 py-2 text-sm font-medium text-primary-foreground shadow opacity-50 cursor-not-allowed">
Current Plan
</button>
) : (
<button
onClick={() => handleCancel()}
disabled={actionLoading}
className="w-full inline-flex items-center justify-center rounded-md border border-border bg-card px-4 py-2 text-sm font-medium shadow hover:bg-card/80 focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2"
>
Downgrade
</button>
)}
</div>
{/* Pro Plan */}
<div className={`rounded-lg border ${subscription.tier === 'pro' ? 'border-primary' : 'border-border'} p-6 shadow-lg`}>
<h3 className="text-xl font-semibold">Pro</h3>
<p className="text-3xl font-bold my-3">$9<span className="text-base font-normal">/mo</span></p>
<p className="text-muted-foreground text-sm mb-6">Advanced features for professionals</p>
<ul className="space-y-2 mb-6">
<li className="flex items-start gap-2">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-primary w-5 h-5 mt-0.5">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
<span>Everything in Free</span>
</li>
<li className="flex items-start gap-2">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-primary w-5 h-5 mt-0.5">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
<span>Zero-day vulnerability alerts</span>
</li>
<li className="flex items-start gap-2">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-primary w-5 h-5 mt-0.5">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
<span>AI-generated code analysis</span>
</li>
<li className="flex items-start gap-2">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-primary w-5 h-5 mt-0.5">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
<span>Unlimited vulnerability scanning</span>
</li>
<li className="flex items-start gap-2">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-muted-foreground w-5 h-5 mt-0.5">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
<span className="text-muted-foreground">24/7 dedicated support</span>
</li>
</ul>
{subscription.tier === 'pro' ? (
<button disabled className="w-full inline-flex items-center justify-center rounded-md bg-primary/30 px-4 py-2 text-sm font-medium text-primary-foreground shadow opacity-50 cursor-not-allowed">
Current Plan
</button>
) : (
<button
onClick={() => handleUpgrade('pro')}
disabled={actionLoading}
className="w-full inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground shadow hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2"
>
{subscription.tier === 'enterprise' ? 'Downgrade to Pro' : 'Upgrade to Pro'}
</button>
)}
</div>
{/* Enterprise Plan */}
<div className={`rounded-lg border ${subscription.tier === 'enterprise' ? 'border-primary' : 'border-border'} p-6`}>
<h3 className="text-xl font-semibold">Enterprise</h3>
<p className="text-3xl font-bold my-3">$19<span className="text-base font-normal">/mo</span></p>
<p className="text-muted-foreground text-sm mb-6">Complete solution for businesses</p>
<ul className="space-y-2 mb-6">
<li className="flex items-start gap-2">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-primary w-5 h-5 mt-0.5">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
<span>Everything in Pro</span>
</li>
<li className="flex items-start gap-2">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-primary w-5 h-5 mt-0.5">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
<span>24/7 dedicated support</span>
</li>
<li className="flex items-start gap-2">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-primary w-5 h-5 mt-0.5">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
<span>Custom security policies</span>
</li>
<li className="flex items-start gap-2">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-primary w-5 h-5 mt-0.5">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
<span>SOC2 compliance reporting</span>
</li>
<li className="flex items-start gap-2">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-primary w-5 h-5 mt-0.5">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
<span>Automated remediation</span>
</li>
</ul>
{subscription.tier === 'enterprise' ? (
<button disabled className="w-full inline-flex items-center justify-center rounded-md bg-primary/30 px-4 py-2 text-sm font-medium text-primary-foreground shadow opacity-50 cursor-not-allowed">
Current Plan
</button>
) : (
<button
onClick={() => handleUpgrade('enterprise')}
disabled={actionLoading}
className="w-full inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground shadow hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2"
>
Upgrade to Enterprise
</button>
)}
</div>
</div>
</div>
</div>
</div>
</DashboardLayout>
);
}
export default withAuth(BillingPage);