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.
134 lines
4.7 KiB
TypeScript
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>
|
|
);
|
|
};
|