2025-10-29 21:45:14 +01:00
|
|
|
|
import { useEffect, useState } from 'react';
|
|
|
|
|
|
import { motion } from 'framer-motion';
|
|
|
|
|
|
import { getOrderDetails } from '../api/checkoutClient';
|
|
|
|
|
|
import { OrderDetailsResponse } from '../types';
|
2025-10-30 13:55:51 +01:00
|
|
|
|
import { CarbonImpactComparison } from '../components/CarbonImpactComparison';
|
|
|
|
|
|
import { useCalculatorState } from '../hooks/useCalculatorState';
|
2025-10-29 21:45:14 +01:00
|
|
|
|
|
2025-10-30 13:55:51 +01:00
|
|
|
|
interface CheckoutSuccessProps {
|
|
|
|
|
|
onNavigateHome: () => void;
|
|
|
|
|
|
onNavigateCalculator: () => void;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default function CheckoutSuccess({
|
|
|
|
|
|
onNavigateHome,
|
|
|
|
|
|
onNavigateCalculator
|
|
|
|
|
|
}: CheckoutSuccessProps) {
|
2025-10-29 21:45:14 +01:00
|
|
|
|
const [orderDetails, setOrderDetails] = useState<OrderDetailsResponse | null>(null);
|
|
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
|
|
const [error, setError] = useState<string | null>(null);
|
2025-10-30 13:55:51 +01:00
|
|
|
|
const { clearState } = useCalculatorState();
|
|
|
|
|
|
|
|
|
|
|
|
// Clear calculator state on successful payment (per user preference)
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (orderDetails && (orderDetails.order.status === 'paid' || orderDetails.order.status === 'fulfilled')) {
|
|
|
|
|
|
clearState();
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [orderDetails, clearState]);
|
2025-10-29 21:45:14 +01:00
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const fetchOrderDetails = async () => {
|
|
|
|
|
|
// Get session ID from URL
|
|
|
|
|
|
const params = new URLSearchParams(window.location.search);
|
|
|
|
|
|
const sessionId = params.get('session_id');
|
|
|
|
|
|
|
|
|
|
|
|
if (!sessionId) {
|
|
|
|
|
|
setError('No session ID found in URL');
|
|
|
|
|
|
setLoading(false);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const details = await getOrderDetails(sessionId);
|
|
|
|
|
|
setOrderDetails(details);
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
setError('Failed to load order details');
|
|
|
|
|
|
console.error(err);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
fetchOrderDetails();
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
if (loading) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-cyan-50 flex items-center justify-center p-6">
|
|
|
|
|
|
<motion.div
|
|
|
|
|
|
initial={{ opacity: 0, scale: 0.9 }}
|
|
|
|
|
|
animate={{ opacity: 1, scale: 1 }}
|
|
|
|
|
|
className="text-center"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-blue-500 mx-auto"></div>
|
|
|
|
|
|
<p className="mt-4 text-slate-600 font-medium">Loading your order details...</p>
|
|
|
|
|
|
</motion.div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (error || !orderDetails) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-cyan-50 flex items-center justify-center p-6">
|
|
|
|
|
|
<motion.div
|
|
|
|
|
|
initial={{ opacity: 0, y: 20 }}
|
|
|
|
|
|
animate={{ opacity: 1, y: 0 }}
|
|
|
|
|
|
className="max-w-md w-full bg-white rounded-2xl shadow-luxury p-8 text-center"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="text-red-500 text-5xl mb-4">⚠️</div>
|
|
|
|
|
|
<h2 className="text-2xl font-bold text-slate-800 mb-2">Order Not Found</h2>
|
|
|
|
|
|
<p className="text-slate-600 mb-6">{error || 'Unable to retrieve order details'}</p>
|
2025-10-30 13:55:51 +01:00
|
|
|
|
<button
|
|
|
|
|
|
onClick={onNavigateHome}
|
2025-10-29 21:45:14 +01:00
|
|
|
|
className="inline-block px-6 py-3 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
|
|
|
|
|
|
>
|
|
|
|
|
|
Return to Home
|
2025-10-30 13:55:51 +01:00
|
|
|
|
</button>
|
2025-10-29 21:45:14 +01:00
|
|
|
|
</motion.div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const { order, session } = orderDetails;
|
|
|
|
|
|
const totalAmount = order.totalAmount / 100; // Convert cents to dollars
|
|
|
|
|
|
const baseAmount = order.baseAmount / 100;
|
|
|
|
|
|
const processingFee = order.processingFee / 100;
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-cyan-50 flex items-center justify-center p-6">
|
|
|
|
|
|
<motion.div
|
|
|
|
|
|
initial={{ opacity: 0, y: 20 }}
|
|
|
|
|
|
animate={{ opacity: 1, y: 0 }}
|
|
|
|
|
|
transition={{ duration: 0.5 }}
|
|
|
|
|
|
className="max-w-2xl w-full"
|
|
|
|
|
|
>
|
|
|
|
|
|
{/* Success Header */}
|
|
|
|
|
|
<motion.div
|
|
|
|
|
|
initial={{ scale: 0 }}
|
|
|
|
|
|
animate={{ scale: 1 }}
|
|
|
|
|
|
transition={{ delay: 0.2, type: 'spring', stiffness: 200 }}
|
|
|
|
|
|
className="text-center mb-8"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="text-green-500 text-7xl mb-4">✓</div>
|
|
|
|
|
|
<h1 className="text-4xl md:text-5xl font-bold text-slate-800 mb-2">
|
|
|
|
|
|
Payment Successful!
|
|
|
|
|
|
</h1>
|
|
|
|
|
|
<p className="text-xl text-slate-600">
|
|
|
|
|
|
Thank you for offsetting your carbon footprint
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</motion.div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Order Details Card */}
|
|
|
|
|
|
<motion.div
|
|
|
|
|
|
initial={{ opacity: 0, y: 20 }}
|
|
|
|
|
|
animate={{ opacity: 1, y: 0 }}
|
|
|
|
|
|
transition={{ delay: 0.3 }}
|
|
|
|
|
|
className="bg-white rounded-2xl shadow-luxury p-8 mb-6"
|
|
|
|
|
|
>
|
|
|
|
|
|
<h2 className="text-2xl font-bold text-slate-800 mb-6">Order Summary</h2>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
|
{/* Offset Amount */}
|
|
|
|
|
|
<div className="flex justify-between items-center py-3 border-b border-slate-100">
|
|
|
|
|
|
<span className="text-slate-600 font-medium">Carbon Offset</span>
|
|
|
|
|
|
<span className="text-slate-800 font-bold text-lg">
|
|
|
|
|
|
{order.tons} tons CO₂
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Portfolio */}
|
|
|
|
|
|
<div className="flex justify-between items-center py-3 border-b border-slate-100">
|
|
|
|
|
|
<span className="text-slate-600 font-medium">Portfolio</span>
|
|
|
|
|
|
<span className="text-slate-800 font-semibold">
|
|
|
|
|
|
Portfolio #{order.portfolioId}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Base Amount */}
|
|
|
|
|
|
<div className="flex justify-between items-center py-3">
|
|
|
|
|
|
<span className="text-slate-600">Offset Cost</span>
|
|
|
|
|
|
<span className="text-slate-800 font-semibold">
|
|
|
|
|
|
${baseAmount.toFixed(2)}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Processing Fee */}
|
|
|
|
|
|
<div className="flex justify-between items-center py-3 border-b border-slate-200">
|
2025-10-30 13:55:51 +01:00
|
|
|
|
<span className="text-slate-600">Processing Fee (5%)</span>
|
2025-10-29 21:45:14 +01:00
|
|
|
|
<span className="text-slate-800 font-semibold">
|
|
|
|
|
|
${processingFee.toFixed(2)}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Total */}
|
|
|
|
|
|
<div className="flex justify-between items-center py-4 bg-gradient-to-r from-blue-50 to-cyan-50 rounded-lg px-4">
|
|
|
|
|
|
<span className="text-slate-800 font-bold text-lg">Total Paid</span>
|
|
|
|
|
|
<span className="text-blue-600 font-bold text-2xl">
|
|
|
|
|
|
${totalAmount.toFixed(2)}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Order Info */}
|
|
|
|
|
|
<div className="mt-6 pt-6 border-t border-slate-200">
|
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<span className="text-slate-500">Order ID</span>
|
|
|
|
|
|
<p className="text-slate-800 font-mono text-xs mt-1">{order.id}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<span className="text-slate-500">Status</span>
|
|
|
|
|
|
<p className="mt-1">
|
|
|
|
|
|
<span className={`inline-block px-3 py-1 rounded-full text-xs font-semibold ${
|
|
|
|
|
|
order.status === 'fulfilled'
|
|
|
|
|
|
? 'bg-green-100 text-green-700'
|
|
|
|
|
|
: order.status === 'paid'
|
|
|
|
|
|
? 'bg-blue-100 text-blue-700'
|
|
|
|
|
|
: 'bg-yellow-100 text-yellow-700'
|
|
|
|
|
|
}`}>
|
|
|
|
|
|
{order.status.toUpperCase()}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{session.customerEmail && (
|
|
|
|
|
|
<div className="md:col-span-2">
|
|
|
|
|
|
<span className="text-slate-500">Email</span>
|
|
|
|
|
|
<p className="text-slate-800 mt-1">{session.customerEmail}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</motion.div>
|
|
|
|
|
|
|
2025-10-30 13:55:51 +01:00
|
|
|
|
{/* Impact Comparisons */}
|
2025-10-29 21:45:14 +01:00
|
|
|
|
<motion.div
|
|
|
|
|
|
initial={{ opacity: 0, y: 20 }}
|
|
|
|
|
|
animate={{ opacity: 1, y: 0 }}
|
|
|
|
|
|
transition={{ delay: 0.4 }}
|
2025-10-30 13:55:51 +01:00
|
|
|
|
className="mb-6"
|
2025-10-29 21:45:14 +01:00
|
|
|
|
>
|
2025-10-30 13:55:51 +01:00
|
|
|
|
<div className="bg-gradient-to-br from-emerald-600 via-teal-600 to-cyan-600 rounded-2xl p-8 shadow-luxury">
|
|
|
|
|
|
<CarbonImpactComparison tons={order.tons} variant="success" count={3} />
|
|
|
|
|
|
</div>
|
2025-10-29 21:45:14 +01:00
|
|
|
|
</motion.div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Action Buttons */}
|
|
|
|
|
|
<motion.div
|
|
|
|
|
|
initial={{ opacity: 0 }}
|
|
|
|
|
|
animate={{ opacity: 1 }}
|
|
|
|
|
|
transition={{ delay: 0.5 }}
|
|
|
|
|
|
className="flex flex-col sm:flex-row gap-4 justify-center"
|
|
|
|
|
|
>
|
2025-10-30 13:55:51 +01:00
|
|
|
|
<button
|
|
|
|
|
|
onClick={onNavigateHome}
|
2025-10-29 21:45:14 +01:00
|
|
|
|
className="px-8 py-3 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-all hover:shadow-lg font-semibold text-center"
|
|
|
|
|
|
>
|
|
|
|
|
|
Return to Home
|
2025-10-30 13:55:51 +01:00
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={onNavigateCalculator}
|
|
|
|
|
|
className="px-8 py-3 bg-green-500 text-white rounded-lg hover:bg-green-600 transition-all hover:shadow-lg font-semibold text-center"
|
|
|
|
|
|
>
|
|
|
|
|
|
Calculate Another Offset
|
|
|
|
|
|
</button>
|
2025-10-29 21:45:14 +01:00
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => window.print()}
|
|
|
|
|
|
className="px-8 py-3 bg-white text-slate-700 rounded-lg hover:bg-slate-50 transition-all hover:shadow-lg font-semibold border border-slate-200"
|
|
|
|
|
|
>
|
|
|
|
|
|
Print Receipt
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</motion.div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Confirmation Email Notice */}
|
|
|
|
|
|
{session.customerEmail && (
|
|
|
|
|
|
<motion.p
|
|
|
|
|
|
initial={{ opacity: 0 }}
|
|
|
|
|
|
animate={{ opacity: 1 }}
|
|
|
|
|
|
transition={{ delay: 0.6 }}
|
|
|
|
|
|
className="text-center text-slate-500 text-sm mt-6"
|
|
|
|
|
|
>
|
|
|
|
|
|
A confirmation email has been sent to {session.customerEmail}
|
|
|
|
|
|
</motion.p>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</motion.div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|