Implement Stripe Phase 2

This commit is contained in:
gpt-engineer-app[bot]
2025-10-10 23:01:23 +00:00
parent 908b09a1b1
commit cec20ed332
4 changed files with 399 additions and 150 deletions

View File

@@ -16,6 +16,7 @@ import Explore from "./pages/Explore";
import { ListingDetails } from "./pages/ListingDetails"; import { ListingDetails } from "./pages/ListingDetails";
import OfferDetails from "./pages/OfferDetails"; import OfferDetails from "./pages/OfferDetails";
import Checkout from "./pages/Checkout"; import Checkout from "./pages/Checkout";
import PaymentError from "./pages/PaymentError";
import OrderConfirmation from "./pages/OrderConfirmation"; import OrderConfirmation from "./pages/OrderConfirmation";
import DashboardLayout from "./components/DashboardLayout"; import DashboardLayout from "./components/DashboardLayout";
import Dashboard from "./pages/dashboard/Dashboard"; import Dashboard from "./pages/dashboard/Dashboard";
@@ -124,6 +125,11 @@ const AppRouter = () => (
<Checkout /> <Checkout />
</FrontendLayout> </FrontendLayout>
} /> } />
<Route path="/payment-error" element={
<FrontendLayout>
<PaymentError />
</FrontendLayout>
} />
<Route path="/order-confirmation" element={ <Route path="/order-confirmation" element={
<FrontendLayout> <FrontendLayout>
<OrderConfirmation /> <OrderConfirmation />

View File

@@ -1,5 +1,6 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { Elements, CardElement, useElements, useStripe as useStripeHook } from '@stripe/react-stripe-js';
import { useCart } from '@/contexts/CartContext'; import { useCart } from '@/contexts/CartContext';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
@@ -9,13 +10,14 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from '@/components/ui/textarea';
import { Separator } from '@/components/ui/separator'; import { Separator } from '@/components/ui/separator';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { ArrowLeft, CreditCard, Smartphone, Building, Truck, Shield, Check, X, Loader2 } from 'lucide-react'; import { ArrowLeft, CreditCard, Smartphone, Building, Truck, Shield, Check, X, Loader2, AlertCircle } from 'lucide-react';
import Header from '@/components/Header'; import Header from '@/components/Header';
import Footer from '@/components/Footer'; import Footer from '@/components/Footer';
import { useToast } from '@/hooks/use-toast'; import { useToast } from '@/hooks/use-toast';
import { useBooking } from '@/hooks/useBooking'; import { useBooking } from '@/hooks/useBooking';
import { useStripe } from '@/hooks/useStripe'; import { useStripe } from '@/hooks/useStripe';
import { paymentService } from '@/services/paymentService'; import { paymentService } from '@/services/paymentService';
import { Alert, AlertDescription } from '@/components/ui/alert';
interface CustomerInfo { interface CustomerInfo {
firstName: string; firstName: string;
@@ -30,12 +32,175 @@ interface CustomerInfo {
interface PaymentInfo { interface PaymentInfo {
method: 'credit_card' | 'paypal' | 'bank_transfer' | 'cash'; method: 'credit_card' | 'paypal' | 'bank_transfer' | 'cash';
cardNumber: string;
expiryDate: string;
cvv: string;
cardName: string;
} }
// Card Element Styles
const CARD_ELEMENT_OPTIONS = {
style: {
base: {
color: '#32325d',
fontFamily: '"Helvetica Neue", Helvetica, sans-serif',
fontSmoothing: 'antialiased',
fontSize: '16px',
'::placeholder': {
color: '#aab7c4',
},
},
invalid: {
color: '#fa755a',
iconColor: '#fa755a',
},
},
};
// CheckoutForm Component
const CheckoutForm = ({
customerInfo,
paymentInfo,
specialRequests,
items,
finalTotal,
onSuccess,
onError
}: any) => {
const stripeHook = useStripeHook();
const elements = useElements();
const { createBooking } = useBooking();
const [isProcessing, setIsProcessing] = useState(false);
const [paymentError, setPaymentError] = useState<string | null>(null);
const processStripePayment = async () => {
if (!stripeHook || !elements) {
setPaymentError('Stripe no está disponible. Por favor recarga la página.');
return;
}
setIsProcessing(true);
setPaymentError(null);
try {
const cardElement = elements.getElement(CardElement);
if (!cardElement) {
throw new Error('Elemento de tarjeta no encontrado');
}
// Crear PaymentIntent desde el backend
const paymentIntent = await paymentService.createPaymentIntent({
amount: Math.round(finalTotal * 100),
currency: 'usd',
description: `Reserva de ${items.length} item(s)`,
metadata: {
customerName: `${customerInfo.firstName} ${customerInfo.lastName}`,
customerEmail: customerInfo.email,
},
});
// Confirmar el pago con Stripe Elements
const { error, paymentIntent: confirmedPaymentIntent } = await stripeHook.confirmCardPayment(
paymentIntent.clientSecret,
{
payment_method: {
card: cardElement,
billing_details: {
name: `${customerInfo.firstName} ${customerInfo.lastName}`,
email: customerInfo.email,
phone: customerInfo.phone,
address: {
line1: customerInfo.address,
city: customerInfo.city,
country: customerInfo.country === 'Dominican Republic' ? 'DO' : 'US',
postal_code: customerInfo.zipCode,
},
},
},
}
);
if (error) {
throw new Error(error.message);
}
if (confirmedPaymentIntent?.status === 'succeeded') {
// Crear reservas después de confirmar el pago
const reservationPromises = items.map(async (item: any) => {
const checkInDate = item.selectedDate
? new Date(item.selectedDate)
: new Date();
if (item.timeSlot) {
const [hours, minutes] = item.timeSlot.split(':');
checkInDate.setHours(parseInt(hours), parseInt(minutes), 0, 0);
}
const bookingData = {
listingId: item.id,
guestName: `${customerInfo.firstName} ${customerInfo.lastName}`,
guestEmail: customerInfo.email,
guestPhone: customerInfo.phone,
checkIn: checkInDate.toISOString(),
guests: item.guests || 1,
totalAmount: item.price * (item.guests || 1),
specialRequests: specialRequests,
};
return await createBooking(bookingData);
});
const results = await Promise.all(reservationPromises);
const allSuccess = results.every(result => result.success);
if (!allSuccess) {
throw new Error('Algunas reservas fallaron al crearse');
}
onSuccess({
paymentIntentId: confirmedPaymentIntent.id,
reservations: results.map(r => r.data),
});
} else {
throw new Error('El pago no se pudo completar');
}
} catch (error: any) {
console.error('Payment error:', error);
setPaymentError(error.message || 'Error al procesar el pago');
onError(error.message);
} finally {
setIsProcessing(false);
}
};
return (
<div className="space-y-6">
{paymentError && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{paymentError}</AlertDescription>
</Alert>
)}
<div className="p-4 border rounded-lg">
<Label className="mb-2 block">Información de Tarjeta</Label>
<CardElement options={CARD_ELEMENT_OPTIONS} />
</div>
<Button
className="w-full"
onClick={processStripePayment}
disabled={isProcessing || !stripeHook}
>
{isProcessing ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Procesando pago...
</>
) : (
`Pagar $${finalTotal.toFixed(2)}`
)}
</Button>
</div>
);
};
const Checkout = () => { const Checkout = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const { items, getTotalPrice, clearCart } = useCart(); const { items, getTotalPrice, clearCart } = useCart();
@@ -58,10 +223,6 @@ const Checkout = () => {
const [paymentInfo, setPaymentInfo] = useState<PaymentInfo>({ const [paymentInfo, setPaymentInfo] = useState<PaymentInfo>({
method: 'credit_card', method: 'credit_card',
cardNumber: '',
expiryDate: '',
cvv: '',
cardName: ''
}); });
const [specialRequests, setSpecialRequests] = useState(''); const [specialRequests, setSpecialRequests] = useState('');
@@ -101,7 +262,7 @@ const Checkout = () => {
}; };
const handlePaymentInfoChange = (field: keyof PaymentInfo, value: string) => { const handlePaymentInfoChange = (field: keyof PaymentInfo, value: string) => {
setPaymentInfo(prev => ({ ...prev, [field]: value })); setPaymentInfo(prev => ({ ...prev, [field]: value as any }));
}; };
const saveOrderToJSON = (orderData: any) => { const saveOrderToJSON = (orderData: any) => {
@@ -110,35 +271,57 @@ const Checkout = () => {
localStorage.setItem('karibeo_orders', JSON.stringify(orders)); localStorage.setItem('karibeo_orders', JSON.stringify(orders));
}; };
const processPayment = async () => { const handlePaymentSuccess = (paymentData: any) => {
const orderData = {
id: Date.now().toString(),
date: new Date().toISOString(),
items: items,
customerInfo,
paymentInfo: {
method: paymentInfo.method,
paymentIntentId: paymentData.paymentIntentId,
},
specialRequests,
pricing: {
subtotal: totalPrice,
taxes,
serviceFee,
total: finalTotal
},
status: 'confirmed',
reservations: paymentData.reservations,
stripeEnabled: !!credentials?.enabled,
};
saveOrderToJSON(orderData);
clearCart();
toast({
title: "¡Pago procesado exitosamente!",
description: "Tus reservas han sido confirmadas. Recibirás un email de confirmación.",
});
navigate('/order-confirmation', { state: { orderData } });
};
const handlePaymentError = (errorMessage: string) => {
toast({
title: "Error al procesar el pago",
description: errorMessage || "Hubo un problema al procesar tu reserva. Por favor intenta nuevamente.",
variant: "destructive",
});
};
const processOtherPayment = async () => {
setIsProcessing(true); setIsProcessing(true);
try { try {
// Si el método de pago es tarjeta de crédito y Stripe está disponible // Create reservations for non-card payments
if (paymentInfo.method === 'credit_card' && stripe && credentials) {
// Crear PaymentIntent desde el backend
const paymentIntent = await paymentService.createPaymentIntent({
amount: Math.round(finalTotal * 100), // Convertir a centavos
currency: 'usd',
description: `Reserva de ${items.length} item(s)`,
metadata: {
customerName: `${customerInfo.firstName} ${customerInfo.lastName}`,
customerEmail: customerInfo.email,
},
});
// En un entorno real, aquí se confirmaría el pago con Stripe Elements
// Por ahora, simulamos que el pago fue exitoso
console.log('Payment Intent created:', paymentIntent);
}
// Create reservations for each cart item
const reservationPromises = items.map(async (item) => { const reservationPromises = items.map(async (item) => {
const checkInDate = item.selectedDate const checkInDate = item.selectedDate
? new Date(item.selectedDate) ? new Date(item.selectedDate)
: new Date(); : new Date();
// Add time slot if available
if (item.timeSlot) { if (item.timeSlot) {
const [hours, minutes] = item.timeSlot.split(':'); const [hours, minutes] = item.timeSlot.split(':');
checkInDate.setHours(parseInt(hours), parseInt(minutes), 0, 0); checkInDate.setHours(parseInt(hours), parseInt(minutes), 0, 0);
@@ -159,54 +342,22 @@ const Checkout = () => {
}); });
const results = await Promise.all(reservationPromises); const results = await Promise.all(reservationPromises);
// Check if all reservations were created successfully
const allSuccess = results.every(result => result.success); const allSuccess = results.every(result => result.success);
if (!allSuccess) { if (!allSuccess) {
throw new Error('Some reservations failed to create'); throw new Error('Algunas reservas fallaron al crearse');
} }
// Simulate payment processing // Simulate processing
await new Promise(resolve => setTimeout(resolve, 2000)); await new Promise(resolve => setTimeout(resolve, 1500));
const orderData = { handlePaymentSuccess({
id: Date.now().toString(), paymentIntentId: null,
date: new Date().toISOString(),
items: items,
customerInfo,
paymentInfo: {
method: paymentInfo.method,
cardLast4: paymentInfo.method === 'credit_card' ? paymentInfo.cardNumber.slice(-4) : undefined
},
specialRequests,
pricing: {
subtotal: totalPrice,
taxes,
serviceFee,
total: finalTotal
},
status: 'confirmed',
reservations: results.map(r => r.data), reservations: results.map(r => r.data),
stripeEnabled: !!credentials?.enabled,
};
saveOrderToJSON(orderData);
clearCart();
toast({
title: "¡Pago procesado exitosamente!",
description: "Tus reservas han sido confirmadas. Recibirás un email de confirmación.",
}); });
} catch (error: any) {
navigate('/order-confirmation', { state: { orderData } });
} catch (error) {
console.error('Payment error:', error); console.error('Payment error:', error);
toast({ handlePaymentError(error.message);
title: "Error al procesar el pago",
description: "Hubo un problema al procesar tu reserva. Por favor intenta nuevamente.",
variant: "destructive",
});
} finally { } finally {
setIsProcessing(false); setIsProcessing(false);
} }
@@ -217,10 +368,7 @@ const Checkout = () => {
}; };
const validateStep2 = () => { const validateStep2 = () => {
if (paymentInfo.method === 'credit_card') { return true; // Stripe Elements handles validation
return paymentInfo.cardNumber && paymentInfo.expiryDate && paymentInfo.cvv && paymentInfo.cardName;
}
return true;
}; };
return ( return (
@@ -428,67 +576,44 @@ const Checkout = () => {
})} })}
</div> </div>
{/* Credit Card Form */} {/* Credit Card Form with Stripe Elements */}
{paymentInfo.method === 'credit_card' && ( {paymentInfo.method === 'credit_card' && stripe && credentials?.enabled && (
<div className="space-y-4 border-t pt-6"> <div className="space-y-4 border-t pt-6">
{credentials?.enabled && ( <div className="bg-green-50 border border-green-200 rounded-lg p-4 mb-4">
<div className="bg-green-50 border border-green-200 rounded-lg p-4 mb-4"> <div className="flex items-start gap-3">
<div className="flex items-start gap-3"> <Shield className="w-5 h-5 text-green-600 mt-0.5" />
<Shield className="w-5 h-5 text-green-600 mt-0.5" /> <div>
<div> <h4 className="font-medium text-green-900">Pago Seguro</h4>
<h4 className="font-medium text-green-900">Pago Seguro</h4> <p className="text-sm text-green-700">
<p className="text-sm text-green-700"> Tu pago está protegido por Stripe. Tus datos son encriptados y nunca se almacenan en nuestros servidores.
Tu pago está protegido por Stripe. Tus datos son encriptados y seguros. {credentials.testMode && ' (Modo de prueba activo)'}
{credentials.testMode && ' (Modo de prueba activo)'} </p>
</p>
</div>
</div> </div>
</div> </div>
)} </div>
<div> <Elements stripe={stripe}>
<Label htmlFor="cardName">Nombre en la Tarjeta *</Label> <CheckoutForm
<Input customerInfo={customerInfo}
id="cardName" paymentInfo={paymentInfo}
value={paymentInfo.cardName} specialRequests={specialRequests}
onChange={(e) => handlePaymentInfoChange('cardName', e.target.value)} items={items}
placeholder="Como aparece en la tarjeta" finalTotal={finalTotal}
onSuccess={handlePaymentSuccess}
onError={handlePaymentError}
/> />
</div> </Elements>
</div>
)}
<div> {paymentInfo.method === 'credit_card' && (!stripe || !credentials?.enabled) && (
<Label htmlFor="cardNumber">Número de Tarjeta *</Label> <div className="space-y-4 border-t pt-6">
<Input <Alert>
id="cardNumber" <AlertCircle className="h-4 w-4" />
value={paymentInfo.cardNumber} <AlertDescription>
onChange={(e) => handlePaymentInfoChange('cardNumber', e.target.value)} Los pagos con tarjeta no están disponibles en este momento. Por favor selecciona otro método de pago o contacta al administrador.
placeholder="1234 5678 9012 3456" </AlertDescription>
maxLength={19} </Alert>
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label htmlFor="expiryDate">Fecha de Vencimiento *</Label>
<Input
id="expiryDate"
value={paymentInfo.expiryDate}
onChange={(e) => handlePaymentInfoChange('expiryDate', e.target.value)}
placeholder="MM/YY"
maxLength={5}
/>
</div>
<div>
<Label htmlFor="cvv">CVV *</Label>
<Input
id="cvv"
value={paymentInfo.cvv}
onChange={(e) => handlePaymentInfoChange('cvv', e.target.value)}
placeholder="123"
maxLength={4}
/>
</div>
</div>
</div> </div>
)} )}
@@ -523,18 +648,26 @@ const Checkout = () => {
</div> </div>
)} )}
<div className="flex gap-3"> {paymentInfo.method !== 'credit_card' && (
<Button variant="outline" onClick={() => setStep(1)}> <div className="flex gap-3">
<Button variant="outline" onClick={() => setStep(1)}>
Volver
</Button>
<Button
className="flex-1"
onClick={() => setStep(3)}
disabled={!validateStep2()}
>
Revisar Orden
</Button>
</div>
)}
{paymentInfo.method === 'credit_card' && (
<Button variant="outline" onClick={() => setStep(1)} className="w-full">
Volver Volver
</Button> </Button>
<Button )}
className="flex-1"
onClick={() => setStep(3)}
disabled={!validateStep2()}
>
Revisar Orden
</Button>
</div>
</CardContent> </CardContent>
</Card> </Card>
)} )}
@@ -556,9 +689,6 @@ const Checkout = () => {
<h3 className="font-semibold mb-2">Método de Pago</h3> <h3 className="font-semibold mb-2">Método de Pago</h3>
<p className="text-sm"> <p className="text-sm">
{paymentMethods.find(m => m.id === paymentInfo.method)?.name} {paymentMethods.find(m => m.id === paymentInfo.method)?.name}
{paymentInfo.method === 'credit_card' && paymentInfo.cardNumber &&
` terminada en ${paymentInfo.cardNumber.slice(-4)}`
}
</p> </p>
</div> </div>
@@ -585,10 +715,17 @@ const Checkout = () => {
</Button> </Button>
<Button <Button
className="flex-1" className="flex-1"
onClick={processPayment} onClick={processOtherPayment}
disabled={isProcessing} disabled={isProcessing}
> >
{isProcessing ? 'Procesando...' : `Confirmar y Pagar $${finalTotal.toFixed(2)}`} {isProcessing ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Procesando...
</>
) : (
`Confirmar y Pagar $${finalTotal.toFixed(2)}`
)}
</Button> </Button>
</div> </div>
</CardContent> </CardContent>

View File

@@ -201,12 +201,17 @@ const OrderConfirmation = () => {
<div className="flex items-center text-blue-800"> <div className="flex items-center text-blue-800">
<CheckCircle className="w-5 h-5 mr-2" /> <CheckCircle className="w-5 h-5 mr-2" />
<span className="font-medium"> <span className="font-medium">
Pago procesado via {orderData.paymentInfo.method === 'credit_card' ? 'Tarjeta de Crédito' : orderData.paymentInfo.method} Pago procesado via {
orderData.paymentInfo.method === 'credit_card' ? 'Tarjeta de Crédito' :
orderData.paymentInfo.method === 'paypal' ? 'PayPal' :
orderData.paymentInfo.method === 'bank_transfer' ? 'Transferencia Bancaria' :
'Efectivo'
}
</span> </span>
</div> </div>
{orderData.paymentInfo.cardLast4 && ( {orderData.paymentInfo.paymentIntentId && (
<p className="text-sm text-blue-700 mt-1"> <p className="text-xs text-blue-700 mt-2 font-mono">
Tarjeta terminada en {orderData.paymentInfo.cardLast4} ID de Transacción: {orderData.paymentInfo.paymentIntentId}
</p> </p>
)} )}
</div> </div>

101
src/pages/PaymentError.tsx Normal file
View File

@@ -0,0 +1,101 @@
import React from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { XCircle, ArrowLeft, RefreshCw } from 'lucide-react';
import Header from '@/components/Header';
import Footer from '@/components/Footer';
const PaymentError = () => {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const errorMessage = searchParams.get('message') || 'Hubo un problema al procesar tu pago';
return (
<div className="min-h-screen bg-gray-50">
<Header />
<div className="container mx-auto px-4 pt-24 pb-12">
<div className="max-w-2xl mx-auto">
{/* Error Header */}
<div className="text-center mb-8">
<div className="w-20 h-20 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-4">
<XCircle className="w-12 h-12 text-red-600" />
</div>
<h1 className="text-3xl font-bold text-gray-900 mb-2">
Error en el Pago
</h1>
<p className="text-gray-600">
No pudimos procesar tu pago. No te preocupes, no se ha realizado ningún cargo.
</p>
</div>
{/* Error Details */}
<Card className="mb-6">
<CardHeader>
<CardTitle>Detalles del Error</CardTitle>
</CardHeader>
<CardContent>
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<p className="text-red-800">{errorMessage}</p>
</div>
<div className="mt-6 space-y-3">
<h3 className="font-semibold text-gray-900">Razones comunes:</h3>
<ul className="text-sm text-gray-700 space-y-2">
<li> Fondos insuficientes en tu tarjeta</li>
<li> Información de la tarjeta incorrecta</li>
<li> Tu banco bloqueó la transacción por seguridad</li>
<li> La tarjeta ha expirado o fue rechazada</li>
</ul>
</div>
<div className="mt-6 space-y-3">
<h3 className="font-semibold text-gray-900">¿Qué puedes hacer?</h3>
<ul className="text-sm text-gray-700 space-y-2">
<li> Verifica los datos de tu tarjeta e intenta nuevamente</li>
<li> Intenta con otra tarjeta o método de pago</li>
<li> Contacta a tu banco para autorizar la transacción</li>
<li> Contacta nuestro soporte si el problema persiste</li>
</ul>
</div>
</CardContent>
</Card>
{/* Action Buttons */}
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<Button onClick={() => navigate('/checkout')} variant="default">
<RefreshCw className="w-4 h-4 mr-2" />
Intentar Nuevamente
</Button>
<Button onClick={() => navigate('/explore')} variant="outline">
<ArrowLeft className="w-4 h-4 mr-2" />
Volver a Explorar
</Button>
</div>
{/* Support Info */}
<div className="mt-8 p-4 bg-blue-50 border border-blue-200 rounded-lg text-center">
<h3 className="font-semibold text-blue-900 mb-2">¿Necesitas Ayuda?</h3>
<p className="text-sm text-blue-700 mb-3">
Nuestro equipo de soporte está disponible 24/7 para ayudarte
</p>
<div className="flex flex-col sm:flex-row gap-2 justify-center text-sm">
<a href="tel:+18091234567" className="text-blue-600 hover:text-blue-800 font-medium">
📞 +1 (809) 123-4567
</a>
<span className="hidden sm:inline text-blue-400">|</span>
<a href="mailto:soporte@karibeo.com" className="text-blue-600 hover:text-blue-800 font-medium">
soporte@karibeo.com
</a>
</div>
</div>
</div>
</div>
<Footer />
</div>
);
};
export default PaymentError;