feat: complete auth and gateway agnostic billing implementation
This commit is contained in:
@@ -11,89 +11,13 @@ 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)
|
||||
const handleUpgrade = (tier: string) => {
|
||||
if (userLoading) return
|
||||
if (!userId) {
|
||||
window.location.href = `/login?redirect=/checkout?tier=${tier}`
|
||||
return
|
||||
}
|
||||
window.location.href = `/checkout?tier=${tier}`
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -140,9 +64,9 @@ export default function UpgradePage() {
|
||||
<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}
|
||||
disabled={userLoading}
|
||||
>
|
||||
{loadingTier === "Pro" ? <Loader2 className="animate-spin" /> : "Get Pro"}
|
||||
{userLoading ? <Loader2 className="animate-spin" /> : "Get Pro"}
|
||||
</Button>
|
||||
</Card>
|
||||
|
||||
@@ -164,9 +88,9 @@ export default function UpgradePage() {
|
||||
<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}
|
||||
disabled={userLoading}
|
||||
>
|
||||
{loadingTier === "Al_Coach" ? <Loader2 className="animate-spin" /> : "Get AI Coach"}
|
||||
{userLoading ? <Loader2 className="animate-spin" /> : "Get AI Coach"}
|
||||
</Button>
|
||||
</Card>
|
||||
</div>
|
||||
@@ -237,54 +161,7 @@ export default function UpgradePage() {
|
||||
</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>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user