kaboot/components/PaymentResult.tsx
Joey Yakimowich-Payne 2e12edc249
Add Stripe payment integration for AI subscriptions
Implement subscription-based AI access with 250 generations/month at $5/month or $50/year.

Changes:
- Backend: Stripe service, payment routes, webhook handlers, generation tracking
- Frontend: Upgrade page with pricing, payment success/cancel pages, UI prompts
- Database: Add subscription fields to users, payments table, migrations
- Config: Stripe env vars to .env.example, docker-compose.prod.yml, PRODUCTION.md
- Tests: Payment route tests, component tests, subscription hook tests

Users without AI access see upgrade prompts; subscribers see remaining generation count.
2026-01-21 16:11:03 -07:00

134 lines
4.7 KiB
TypeScript

import React, { useEffect, useState } from 'react';
import { motion } from 'framer-motion';
import { CheckCircle2, XCircle, Loader2, PartyPopper, ArrowLeft } from 'lucide-react';
import confetti from 'canvas-confetti';
interface PaymentResultProps {
status: 'success' | 'cancel' | 'loading';
onBack?: () => void;
}
export const PaymentResult: React.FC<PaymentResultProps> = ({ status, onBack }) => {
const [showConfetti, setShowConfetti] = useState(false);
useEffect(() => {
if (status === 'success' && !showConfetti) {
setShowConfetti(true);
const duration = 3000;
const end = Date.now() + duration;
const frame = () => {
confetti({
particleCount: 3,
angle: 60,
spread: 55,
origin: { x: 0, y: 0.7 },
colors: ['#8B5CF6', '#6366F1', '#EC4899', '#F59E0B']
});
confetti({
particleCount: 3,
angle: 120,
spread: 55,
origin: { x: 1, y: 0.7 },
colors: ['#8B5CF6', '#6366F1', '#EC4899', '#F59E0B']
});
if (Date.now() < end) {
requestAnimationFrame(frame);
}
};
frame();
}
}, [status, showConfetti]);
if (status === 'loading') {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-center">
<Loader2 className="w-12 h-12 animate-spin text-theme-primary mx-auto mb-4" />
<p className="text-gray-500 font-bold">Processing your payment...</p>
</div>
</div>
);
}
const isSuccess = status === 'success';
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
<motion.div
initial={{ scale: 0.8, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ type: 'spring', bounce: 0.4 }}
className="max-w-md w-full bg-white rounded-[2.5rem] p-8 shadow-[0_20px_50px_rgba(0,0,0,0.1)] border-4 border-white text-center relative overflow-hidden"
>
{isSuccess && (
<div className="absolute inset-0 bg-gradient-to-br from-green-50 to-emerald-50 pointer-events-none" />
)}
<div className="relative z-10">
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ delay: 0.2, type: 'spring', bounce: 0.5 }}
className={`w-24 h-24 rounded-3xl mx-auto mb-6 shadow-xl flex items-center justify-center ${
isSuccess
? 'bg-gradient-to-br from-green-400 to-emerald-500'
: 'bg-gradient-to-br from-gray-300 to-gray-400'
}`}
>
{isSuccess ? (
<PartyPopper className="w-12 h-12 text-white" />
) : (
<XCircle className="w-12 h-12 text-white" />
)}
</motion.div>
<motion.div
initial={{ y: 20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ delay: 0.3 }}
>
<h2 className="text-3xl font-black text-gray-900 mb-2">
{isSuccess ? 'Welcome to Pro!' : 'Payment Cancelled'}
</h2>
<p className="text-gray-500 font-bold mb-8">
{isSuccess
? 'Your AI powers are now unlocked. Time to create amazing quizzes!'
: 'No worries! You can upgrade anytime when you\'re ready.'}
</p>
</motion.div>
{isSuccess && (
<motion.div
initial={{ y: 20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ delay: 0.4 }}
className="bg-green-50 border-2 border-green-200 rounded-2xl p-4 mb-8"
>
<div className="flex items-center justify-center gap-2 text-green-700 font-bold">
<CheckCircle2 size={20} />
<span>250 AI generations ready to use</span>
</div>
</motion.div>
)}
<motion.button
initial={{ y: 20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ delay: 0.5 }}
onClick={onBack}
className={`w-full py-4 rounded-2xl font-black text-lg shadow-[0_6px_0] active:shadow-none active:translate-y-[6px] transition-all flex items-center justify-center gap-2 ${
isSuccess
? 'bg-gray-900 text-white shadow-black hover:bg-black'
: 'bg-theme-primary text-white shadow-theme-primary-dark hover:brightness-110'
}`}
>
<ArrowLeft size={20} />
{isSuccess ? 'Start Creating' : 'Go Back'}
</motion.button>
</div>
</motion.div>
</div>
);
};