292 lines
15 KiB
TypeScript
292 lines
15 KiB
TypeScript
"use client"
|
|
|
|
import { useState } from "react"
|
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from "@/components/ui/card"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Check, X, ShieldCheck, Loader2, Minus, CheckCircle2 } from "lucide-react"
|
|
|
|
import { useActiveUser } from "@/hooks/useActiveUser"
|
|
|
|
const WEBHOOK_SECRET = "local_webhook_secret"
|
|
|
|
export default function UpgradePage() {
|
|
const { userId, loading: userLoading, refreshUser } = useActiveUser()
|
|
const [loadingTier, setLoadingTier] = useState<string | null>(null)
|
|
const [showModal, setShowModal] = useState(false)
|
|
const [orderData, setOrderData] = useState<any>(null)
|
|
const [processing, setProcessing] = useState(false)
|
|
const [success, setSuccess] = useState(false)
|
|
|
|
const handleUpgrade = async (tier: string) => {
|
|
if (userLoading || !userId) return
|
|
setLoadingTier(tier)
|
|
try {
|
|
const res = await fetch(`/api/billing/create-order?user_id=${userId}&plan_tier=${tier}`, {
|
|
method: "POST"
|
|
})
|
|
if (!res.ok) throw new Error("Order creation failed")
|
|
const order = await res.json()
|
|
setOrderData({ ...order, tier })
|
|
setShowModal(true)
|
|
} catch (err) {
|
|
console.error(err)
|
|
alert("Failed to create order")
|
|
} finally {
|
|
setLoadingTier(null)
|
|
}
|
|
}
|
|
|
|
const generateMockSignature = async (payloadStr: string) => {
|
|
const encoder = new TextEncoder();
|
|
const key = await crypto.subtle.importKey(
|
|
'raw',
|
|
encoder.encode(WEBHOOK_SECRET),
|
|
{ name: 'HMAC', hash: 'SHA-256' },
|
|
false,
|
|
['sign']
|
|
);
|
|
const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(payloadStr));
|
|
return Array.from(new Uint8Array(signature)).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
}
|
|
|
|
const simulatePayment = async () => {
|
|
if (!userId) return
|
|
setProcessing(true)
|
|
try {
|
|
const payloadObj = {
|
|
event: "payment.captured",
|
|
payload: {
|
|
payment: {
|
|
entity: {
|
|
id: `pay_mock_${Date.now()}`,
|
|
amount: orderData.amount,
|
|
notes: {
|
|
user_id: userId,
|
|
tier_requested: orderData.tier
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const payloadStr = JSON.stringify(payloadObj)
|
|
const signature = await generateMockSignature(payloadStr)
|
|
|
|
const res = await fetch(`/api/billing/webhook/razorpay`, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"X-Razorpay-Signature": signature
|
|
},
|
|
body: payloadStr
|
|
})
|
|
|
|
if (!res.ok) throw new Error("Webhook failed")
|
|
|
|
setSuccess(true)
|
|
await refreshUser()
|
|
setTimeout(() => {
|
|
window.location.href = "/dashboard"
|
|
}, 2000)
|
|
|
|
} catch (err) {
|
|
console.error(err)
|
|
alert("Payment simulation failed")
|
|
setProcessing(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="p-8 max-w-7xl mx-auto animate-in fade-in duration-500 min-h-screen">
|
|
<div className="text-center max-w-2xl mx-auto mb-16">
|
|
<h1 className="text-4xl font-extrabold tracking-tight mb-4">Upgrade Your Preparation</h1>
|
|
<p className="text-xl text-muted-foreground">Unlock the full power of PragatiVeer to master JEE & NEET.</p>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 max-w-5xl mx-auto items-stretch">
|
|
{/* Free Tier
|
|
<Card className="bg-background/80 backdrop-blur-md border border-border p-6 rounded-[2rem] flex flex-col justify-between h-full z-10 relative shadow-xl hover:border-border/80 transition-colors">
|
|
<div>
|
|
<h3 className="text-3xl font-bold mb-2 text-foreground">Free</h3>
|
|
<p className="text-muted-foreground mb-6 text-base">Perfect to get started.</p>
|
|
<div className="text-6xl font-bold tracking-tighter mb-6 text-foreground">
|
|
₹0<span className="text-xl text-white/60 font-medium tracking-normal ml-1">/year</span>
|
|
</div>
|
|
<ul className="space-y-3 mb-6 font-medium text-lg">
|
|
<li className="flex items-center gap-4"><CheckCircle2 className="text-foreground shrink-0" size={24}/> 1 Daily Practice Sheet</li>
|
|
<li className="flex items-center gap-4"><CheckCircle2 className="text-foreground shrink-0" size={24}/> Basic Performance Analytics</li>
|
|
<li className="flex items-center gap-4"><CheckCircle2 className="text-foreground shrink-0" size={24}/> Community Access</li>
|
|
</ul>
|
|
</div>
|
|
<Button variant="outline" className="w-full rounded-2xl h-12 border-border text-foreground font-bold text-lg hover:bg-secondary" disabled>Get Started Free</Button>
|
|
</Card>
|
|
*/}
|
|
|
|
{/* Pro Tier */}
|
|
<Card className="bg-background/80 backdrop-blur-md border border-border p-6 rounded-[2rem] flex flex-col justify-between h-full z-10 relative shadow-xl hover:border-border/80 transition-colors">
|
|
<div>
|
|
<h3 className="text-3xl font-bold mb-2 text-yellow-500">Pro</h3>
|
|
<p className="text-muted-foreground mb-6 text-base">Perfect for self-paced practice.</p>
|
|
<div className="text-6xl font-bold tracking-tighter mb-6 text-yellow-500">
|
|
₹3,499<span className="text-xl text-yellow-500/60 font-medium tracking-normal ml-1">/year</span>
|
|
</div>
|
|
<ul className="space-y-3 mb-6 font-medium text-lg">
|
|
<li className="flex items-center gap-4"><CheckCircle2 className="text-yellow-500 shrink-0" size={24}/> Unlimited Daily Practice Sheets</li>
|
|
<li className="flex items-center gap-4"><CheckCircle2 className="text-yellow-500 shrink-0" size={24}/> Weekly Test Series & Mock Tests</li>
|
|
<li className="flex items-center gap-4"><CheckCircle2 className="text-yellow-500 shrink-0" size={24}/> Detailed Performance Analytics</li>
|
|
<li className="flex items-center gap-4"><CheckCircle2 className="text-yellow-500 shrink-0" size={24}/> Parent Dashboard Access</li>
|
|
</ul>
|
|
</div>
|
|
<Button
|
|
className="w-full bg-yellow-500 text-black hover:bg-yellow-400 font-bold text-lg rounded-2xl h-12"
|
|
onClick={() => handleUpgrade("Pro")}
|
|
disabled={!!loadingTier}
|
|
>
|
|
{loadingTier === "Pro" ? <Loader2 className="animate-spin" /> : "Get Pro"}
|
|
</Button>
|
|
</Card>
|
|
|
|
{/* AI Coach Tier */}
|
|
<Card className="bg-background/80 backdrop-blur-md border border-border p-6 rounded-[2rem] flex flex-col justify-between h-full z-10 relative shadow-xl hover:border-border/80 transition-colors">
|
|
<div>
|
|
<h3 className="text-3xl font-bold mb-2 text-[#00F0FF]">AI Coach</h3>
|
|
<p className="text-muted-foreground mb-6 text-base">The complete AI-driven system.</p>
|
|
<div className="text-6xl font-bold tracking-tighter mb-6 text-[#00F0FF]">
|
|
₹11,999<span className="text-xl text-[#00F0FF]/60 font-medium tracking-normal ml-1">/year</span>
|
|
</div>
|
|
<ul className="space-y-3 mb-6 font-medium text-lg">
|
|
<li className="flex items-center gap-4"><CheckCircle2 className="text-[#00F0FF] shrink-0" size={24}/> Everything in Pro</li>
|
|
<li className="flex items-center gap-4"><CheckCircle2 className="text-[#00F0FF] shrink-0" size={24}/> AI Spaced Repetition (Mistakes)</li>
|
|
<li className="flex items-center gap-4"><CheckCircle2 className="text-[#00F0FF] shrink-0" size={24}/> 24/7 Streaming AI Tutor Chat</li>
|
|
<li className="flex items-center gap-4"><CheckCircle2 className="text-[#00F0FF] shrink-0" size={24}/> Predictive Rank & College Predictor</li>
|
|
</ul>
|
|
</div>
|
|
<Button
|
|
className="w-full font-bold text-lg rounded-2xl h-12 bg-[#00F0FF] text-black hover:bg-[#00F0FF]/90 transition-all"
|
|
onClick={() => handleUpgrade("Al_Coach")}
|
|
disabled={!!loadingTier}
|
|
>
|
|
{loadingTier === "Al_Coach" ? <Loader2 className="animate-spin" /> : "Get AI Coach"}
|
|
</Button>
|
|
</Card>
|
|
</div>
|
|
|
|
<p className="text-center mt-8 text-sm text-muted-foreground font-medium w-full">
|
|
* All above prices are inclusive of GST.
|
|
</p>
|
|
|
|
{/* ── Tier Comparison Matrix ── */}
|
|
<section className="mt-20 py-8">
|
|
<div className="text-center mb-12">
|
|
<h2 className="text-3xl md:text-4xl font-extrabold tracking-tight mb-3">Compare All Features</h2>
|
|
<p className="text-muted-foreground text-base md:text-lg max-w-2xl mx-auto">
|
|
Detailed breakdown of capabilities across Free, Pro, and AI Coach tiers.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="overflow-x-auto rounded-2xl border border-border/50 bg-secondary/10 backdrop-blur-sm p-3 md:p-6 shadow-2xl">
|
|
<table className="w-full text-left border-collapse min-w-[700px]">
|
|
<thead>
|
|
<tr className="border-b border-border/50 bg-background/50">
|
|
<th className="p-4 md:p-6 font-bold text-lg w-1/2 border-l border-t border-border/50">Feature</th>
|
|
{/* <th className="p-4 md:p-6 font-bold text-lg text-center w-1/5 border-x border-t border-white">Free</th> */}
|
|
<th className="p-4 md:p-6 font-bold text-lg text-center w-1/4 text-yellow-500 border-x border-t border-yellow-500">Pro</th>
|
|
<th className="p-4 md:p-6 font-bold text-lg text-center w-1/4 text-[#00F0FF] bg-[#00F0FF]/5 border-x border-t border-[#00F0FF]">AI Coach</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-border/30 text-sm md:text-base">
|
|
{[
|
|
{ name: "Daily Practice Sheets", free: "1/day", pro: "Unlimited", ai: "Unlimited" },
|
|
{ name: "Answer Keys", free: true, pro: true, ai: true },
|
|
{ name: "Basic Progress Dashboard", free: true, pro: true, ai: true },
|
|
{ name: "Streak Tracking", free: true, pro: true, ai: true },
|
|
{ name: "Leaderboard (City/School)", free: true, pro: true, ai: true },
|
|
{ name: "Topic-wise Micro-tests", free: "5/day", pro: "Unlimited", ai: "Unlimited" },
|
|
{ name: "Weekly Test Series", free: false, pro: true, ai: true },
|
|
{ name: "Full-Length Mock Tests", free: false, pro: "4/month", ai: "Unlimited" },
|
|
{ name: "Previous Year Papers", free: false, pro: true, ai: true },
|
|
{ name: "Detailed Performance Analytics", free: false, pro: true, ai: true },
|
|
{ name: "Chapter-wise Progress", free: false, pro: true, ai: true },
|
|
{ name: "Doubt Resolution (Community)", free: false, pro: true, ai: true },
|
|
{ name: "Parent Dashboard", free: false, pro: true, ai: true },
|
|
{ name: "Offline Mode (Download DPS)", free: false, pro: true, ai: true },
|
|
{ name: "AI Mistake Tracking", free: false, pro: false, ai: true },
|
|
{ name: "Spaced Repetition (SM-2)", free: false, pro: false, ai: true },
|
|
{ name: "Personalized Daily Study Plan", free: false, pro: false, ai: true },
|
|
{ name: "AI Tutor Chat", free: false, pro: false, ai: true },
|
|
{ name: "Exam Readiness Score", free: false, pro: false, ai: true },
|
|
{ name: "Exam Day Strategy", free: false, pro: false, ai: true },
|
|
{ name: "College Predictor", free: false, pro: false, ai: true },
|
|
{ name: "Study Time Optimization", free: false, pro: false, ai: true },
|
|
].map((feature, idx) => (
|
|
<tr key={idx} className="hover:bg-background/40 transition-colors">
|
|
<td className="p-4 md:p-6 font-medium border-y border-l border-border/30">{feature.name}</td>
|
|
{/* <td className="p-4 md:p-6 text-center text-muted-foreground border border-white">
|
|
{typeof feature.free === 'boolean' ? (feature.free ? <CheckCircle2 className="mx-auto text-white" size={20} /> : <Minus className="mx-auto text-white/30" size={20} />) : <span className="font-semibold text-white">{feature.free}</span>}
|
|
</td> */}
|
|
<td className="p-4 md:p-6 text-center border border-yellow-500">
|
|
{typeof feature.pro === 'boolean' ? (feature.pro ? <CheckCircle2 className="mx-auto text-yellow-500" size={20} /> : <Minus className="mx-auto text-yellow-500/30" size={20} />) : <span className="font-semibold text-yellow-500">{feature.pro}</span>}
|
|
</td>
|
|
<td className="p-4 md:p-6 text-center bg-[#00F0FF]/5 border border-[#00F0FF]">
|
|
{typeof feature.ai === 'boolean' ? (feature.ai ? <CheckCircle2 className="mx-auto text-[#00F0FF]" size={20} /> : <Minus className="mx-auto text-[#00F0FF]/30" size={20} />) : <span className="font-semibold text-[#00F0FF]">{feature.ai}</span>}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Mock Razorpay Modal */}
|
|
{showModal && (
|
|
<div className="fixed inset-0 bg-background/80 backdrop-blur-sm flex items-center justify-center z-50 p-4">
|
|
<Card className="max-w-md w-full shadow-2xl border-primary">
|
|
<CardHeader className="text-center pb-2">
|
|
<div className="mx-auto w-16 h-12 bg-blue-600 rounded-lg flex items-center justify-center mb-4">
|
|
<span className="text-white font-black text-2xl">RZP</span>
|
|
</div>
|
|
<CardTitle>Mock Checkout Window</CardTitle>
|
|
<CardDescription>Razorpay Test Sandbox Simulation</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4 mt-4">
|
|
<div className="bg-secondary p-4 rounded-xl space-y-2 text-sm font-mono">
|
|
<div className="flex justify-between">
|
|
<span className="text-muted-foreground">Order ID:</span>
|
|
<span className="font-bold">{orderData?.id}</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-muted-foreground">Amount:</span>
|
|
<span className="font-bold">₹{(orderData?.amount / 100).toFixed(2)}</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-muted-foreground">Tier:</span>
|
|
<span className="font-bold">{orderData?.tier}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{success ? (
|
|
<div className="p-4 bg-green-500/10 text-green-500 rounded-xl text-center font-bold animate-in zoom-in">
|
|
Payment Captured! Redirecting...
|
|
</div>
|
|
) : (
|
|
<p className="text-sm text-muted-foreground text-center">
|
|
Clicking confirm will securely sign and POST a payment.captured webhook to the backend.
|
|
</p>
|
|
)}
|
|
</CardContent>
|
|
<CardFooter className="flex gap-3">
|
|
<Button variant="outline" className="w-full" onClick={() => setShowModal(false)} disabled={processing || success}>
|
|
Cancel
|
|
</Button>
|
|
<Button className="w-full bg-blue-600 hover:bg-blue-700 text-white" onClick={simulatePayment} disabled={processing || success}>
|
|
{processing ? <Loader2 className="animate-spin" /> : "Confirm Payment"}
|
|
</Button>
|
|
</CardFooter>
|
|
</Card>
|
|
</div>
|
|
)}
|
|
|
|
</div>
|
|
)
|
|
}
|