puffin-app/src/pages/CheckoutSuccess.tsx

235 lines
8.7 KiB
TypeScript
Raw Normal View History

Integrate Stripe Checkout and add comprehensive UI enhancements ## Stripe Payment Integration - Add Express.js backend server with Stripe Checkout Sessions - Create SQLite database for order tracking - Implement Stripe webhook handlers for payment events - Integrate with Wren Climate API for carbon offset fulfillment - Add CheckoutSuccess and CheckoutCancel pages - Create checkout API client for frontend - Update OffsetOrder component to redirect to Stripe Checkout - Add processing fee calculation (3% of base amount) - Implement order status tracking (pending → paid → fulfilled) Backend (server/): - Express server with CORS and middleware - SQLite database with Order schema - Stripe configuration and client - Order CRUD operations model - Checkout session creation endpoint - Webhook handler for payment confirmation - Wren API client for offset fulfillment Frontend: - CheckoutSuccess page with order details display - CheckoutCancel page with retry encouragement - Updated OffsetOrder to use Stripe checkout flow - Added checkout routes to App.tsx - TypeScript interfaces for checkout flow ## Visual & UX Enhancements - Add CertificationBadge component for project verification status - Create PortfolioDonutChart for visual portfolio allocation - Implement RadialProgress for percentage displays - Add reusable form components (FormInput, FormTextarea, FormSelect, FormFieldWrapper) - Refactor OffsetOrder with improved layout and animations - Add offset percentage slider with visual feedback - Enhance MobileOffsetOrder with better responsive design - Improve TripCalculator with cleaner UI structure - Update CurrencySelect with better styling - Add portfolio distribution visualization - Enhance project cards with hover effects and animations - Improve color palette and gradient usage throughout ## Configuration - Add VITE_API_BASE_URL environment variable - Create backend .env.example template - Update frontend .env.example with API URL - Add Stripe documentation references 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
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';
export default function CheckoutSuccess() {
const [orderDetails, setOrderDetails] = useState<OrderDetailsResponse | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
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>
<a
href="/"
className="inline-block px-6 py-3 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
>
Return to Home
</a>
</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">
<span className="text-slate-600">Processing Fee (3%)</span>
<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>
{/* Impact Message */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4 }}
className="bg-gradient-to-r from-green-500 to-emerald-500 rounded-2xl shadow-luxury p-6 text-white text-center mb-6"
>
<h3 className="text-2xl font-bold mb-2">🌍 Making an Impact</h3>
<p className="text-green-50 text-lg">
You've offset {order.tons} tons of CO₂ - equivalent to planting approximately{' '}
{Math.round(order.tons * 50)} trees!
</p>
</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"
>
<a
href="/"
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
</a>
<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>
);
}