232 lines
10 KiB
TypeScript
232 lines
10 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 } 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 md:grid-cols-3 gap-8 items-start">
|
|
{/* Free Tier */}
|
|
<Card className="border-border">
|
|
<CardHeader>
|
|
<CardTitle className="text-2xl">Free</CardTitle>
|
|
<CardDescription>Basic access for everyone.</CardDescription>
|
|
<div className="mt-4 font-black text-4xl">₹0<span className="text-xl text-muted-foreground font-normal">/mo</span></div>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4 text-sm font-medium">
|
|
<div className="flex items-center gap-3"><Check className="text-green-500" size={18}/> Subject-wise diagnostics</div>
|
|
<div className="flex items-center gap-3"><Check className="text-green-500" size={18}/> Basic progress tracking</div>
|
|
<div className="flex items-center gap-3 text-muted-foreground opacity-50"><X size={18}/> Unlimited Tests</div>
|
|
<div className="flex items-center gap-3 text-muted-foreground opacity-50"><X size={18}/> AI Spaced Repetition</div>
|
|
<div className="flex items-center gap-3 text-muted-foreground opacity-50"><X size={18}/> Parent Analytics</div>
|
|
</CardContent>
|
|
<CardFooter>
|
|
<Button variant="outline" className="w-full" disabled>Current Plan</Button>
|
|
</CardFooter>
|
|
</Card>
|
|
|
|
{/* Pro Tier */}
|
|
<Card className="border-border border-primary shadow-lg shadow-primary/10 relative">
|
|
<div className="absolute top-0 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-primary text-primary-foreground px-3 py-1 rounded-full text-xs font-bold tracking-wider uppercase">Most Popular</div>
|
|
<CardHeader>
|
|
<CardTitle className="text-2xl text-primary">Pro</CardTitle>
|
|
<CardDescription>For serious aspirants.</CardDescription>
|
|
<div className="mt-4 font-black text-4xl">₹299<span className="text-xl text-muted-foreground font-normal">/mo</span></div>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4 text-sm font-medium">
|
|
<div className="flex items-center gap-3"><Check className="text-green-500" size={18}/> Everything in Free</div>
|
|
<div className="flex items-center gap-3"><Check className="text-green-500" size={18}/> Unlimited Sectional Tests</div>
|
|
<div className="flex items-center gap-3"><Check className="text-green-500" size={18}/> Performance Analytics</div>
|
|
<div className="flex items-center gap-3"><Check className="text-green-500" size={18}/> Parent Dashboard</div>
|
|
<div className="flex items-center gap-3 text-muted-foreground opacity-50"><X size={18}/> AI Mistake Heatmap</div>
|
|
<div className="flex items-center gap-3 text-muted-foreground opacity-50"><X size={18}/> Spaced Repetition (SM-2)</div>
|
|
</CardContent>
|
|
<CardFooter>
|
|
<Button
|
|
className="w-full"
|
|
onClick={() => handleUpgrade("Pro")}
|
|
disabled={!!loadingTier}
|
|
>
|
|
{loadingTier === "Pro" ? <Loader2 className="animate-spin" /> : "Upgrade to Pro"}
|
|
</Button>
|
|
</CardFooter>
|
|
</Card>
|
|
|
|
{/* AI Coach Tier */}
|
|
<Card className="border-border bg-gradient-to-b from-primary/10 to-background border-primary">
|
|
<CardHeader>
|
|
<CardTitle className="text-2xl flex items-center gap-2">AI Coach <ShieldCheck size={24} className="text-primary"/></CardTitle>
|
|
<CardDescription>Personalized mastery loop.</CardDescription>
|
|
<div className="mt-4 font-black text-4xl">₹599<span className="text-xl text-muted-foreground font-normal">/mo</span></div>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4 text-sm font-medium">
|
|
<div className="flex items-center gap-3"><Check className="text-green-500" size={18}/> Everything in Pro</div>
|
|
<div className="flex items-center gap-3"><Check className="text-green-500" size={18}/> Daily AI Error Review Queue</div>
|
|
<div className="flex items-center gap-3"><Check className="text-green-500" size={18}/> SM-2 Spaced Repetition</div>
|
|
<div className="flex items-center gap-3"><Check className="text-green-500" size={18}/> Mistake Heatmap Tracking</div>
|
|
<div className="flex items-center gap-3"><Check className="text-green-500" size={18}/> AI Chat Tutor Support</div>
|
|
</CardContent>
|
|
<CardFooter>
|
|
<Button
|
|
className="w-full font-bold"
|
|
size="lg"
|
|
onClick={() => handleUpgrade("Al_Coach")}
|
|
disabled={!!loadingTier}
|
|
>
|
|
{loadingTier === "Al_Coach" ? <Loader2 className="animate-spin" /> : "Get AI Coach"}
|
|
</Button>
|
|
</CardFooter>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* 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-16 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>
|
|
)
|
|
}
|