diff --git a/src/App.tsx b/src/App.tsx index 59c83bf..66d0da4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -16,6 +16,7 @@ import Explore from "./pages/Explore"; import { ListingDetails } from "./pages/ListingDetails"; import OfferDetails from "./pages/OfferDetails"; import Checkout from "./pages/Checkout"; +import PaymentError from "./pages/PaymentError"; import OrderConfirmation from "./pages/OrderConfirmation"; import DashboardLayout from "./components/DashboardLayout"; import Dashboard from "./pages/dashboard/Dashboard"; @@ -124,6 +125,11 @@ const AppRouter = () => ( } /> + + + + } /> diff --git a/src/pages/Checkout.tsx b/src/pages/Checkout.tsx index 411332c..db67788 100644 --- a/src/pages/Checkout.tsx +++ b/src/pages/Checkout.tsx @@ -1,5 +1,6 @@ import React, { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; +import { Elements, CardElement, useElements, useStripe as useStripeHook } from '@stripe/react-stripe-js'; import { useCart } from '@/contexts/CartContext'; import { Button } from '@/components/ui/button'; 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 { Separator } from '@/components/ui/separator'; 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 Footer from '@/components/Footer'; import { useToast } from '@/hooks/use-toast'; import { useBooking } from '@/hooks/useBooking'; import { useStripe } from '@/hooks/useStripe'; import { paymentService } from '@/services/paymentService'; +import { Alert, AlertDescription } from '@/components/ui/alert'; interface CustomerInfo { firstName: string; @@ -30,12 +32,175 @@ interface CustomerInfo { interface PaymentInfo { 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(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 ( +
+ {paymentError && ( + + + {paymentError} + + )} + +
+ + +
+ + +
+ ); +}; + const Checkout = () => { const navigate = useNavigate(); const { items, getTotalPrice, clearCart } = useCart(); @@ -58,10 +223,6 @@ const Checkout = () => { const [paymentInfo, setPaymentInfo] = useState({ method: 'credit_card', - cardNumber: '', - expiryDate: '', - cvv: '', - cardName: '' }); const [specialRequests, setSpecialRequests] = useState(''); @@ -101,7 +262,7 @@ const Checkout = () => { }; const handlePaymentInfoChange = (field: keyof PaymentInfo, value: string) => { - setPaymentInfo(prev => ({ ...prev, [field]: value })); + setPaymentInfo(prev => ({ ...prev, [field]: value as any })); }; const saveOrderToJSON = (orderData: any) => { @@ -110,35 +271,57 @@ const Checkout = () => { 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); try { - // Si el método de pago es tarjeta de crédito y Stripe está disponible - 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 + // Create reservations for non-card payments const reservationPromises = items.map(async (item) => { const checkInDate = item.selectedDate ? new Date(item.selectedDate) : new Date(); - // Add time slot if available if (item.timeSlot) { const [hours, minutes] = item.timeSlot.split(':'); checkInDate.setHours(parseInt(hours), parseInt(minutes), 0, 0); @@ -159,54 +342,22 @@ const Checkout = () => { }); const results = await Promise.all(reservationPromises); - - // Check if all reservations were created successfully const allSuccess = results.every(result => result.success); if (!allSuccess) { - throw new Error('Some reservations failed to create'); + throw new Error('Algunas reservas fallaron al crearse'); } - // Simulate payment processing - await new Promise(resolve => setTimeout(resolve, 2000)); + // Simulate processing + await new Promise(resolve => setTimeout(resolve, 1500)); - const orderData = { - id: Date.now().toString(), - 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', + handlePaymentSuccess({ + paymentIntentId: null, 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.", }); - - navigate('/order-confirmation', { state: { orderData } }); - } catch (error) { + } catch (error: any) { console.error('Payment error:', error); - toast({ - title: "Error al procesar el pago", - description: "Hubo un problema al procesar tu reserva. Por favor intenta nuevamente.", - variant: "destructive", - }); + handlePaymentError(error.message); } finally { setIsProcessing(false); } @@ -217,10 +368,7 @@ const Checkout = () => { }; const validateStep2 = () => { - if (paymentInfo.method === 'credit_card') { - return paymentInfo.cardNumber && paymentInfo.expiryDate && paymentInfo.cvv && paymentInfo.cardName; - } - return true; + return true; // Stripe Elements handles validation }; return ( @@ -428,67 +576,44 @@ const Checkout = () => { })} - {/* Credit Card Form */} - {paymentInfo.method === 'credit_card' && ( + {/* Credit Card Form with Stripe Elements */} + {paymentInfo.method === 'credit_card' && stripe && credentials?.enabled && (
- {credentials?.enabled && ( -
-
- -
-

Pago Seguro

-

- Tu pago está protegido por Stripe. Tus datos son encriptados y seguros. - {credentials.testMode && ' (Modo de prueba activo)'} -

-
+
+
+ +
+

Pago Seguro

+

+ Tu pago está protegido por Stripe. Tus datos son encriptados y nunca se almacenan en nuestros servidores. + {credentials.testMode && ' (Modo de prueba activo)'} +

- )} - -
- - handlePaymentInfoChange('cardName', e.target.value)} - placeholder="Como aparece en la tarjeta" +
+ + + -
- -
- - handlePaymentInfoChange('cardNumber', e.target.value)} - placeholder="1234 5678 9012 3456" - maxLength={19} - /> -
- -
-
- - handlePaymentInfoChange('expiryDate', e.target.value)} - placeholder="MM/YY" - maxLength={5} - /> -
-
- - handlePaymentInfoChange('cvv', e.target.value)} - placeholder="123" - maxLength={4} - /> -
-
+ +
+ )} + + {paymentInfo.method === 'credit_card' && (!stripe || !credentials?.enabled) && ( +
+ + + + Los pagos con tarjeta no están disponibles en este momento. Por favor selecciona otro método de pago o contacta al administrador. + +
)} @@ -523,18 +648,26 @@ const Checkout = () => {
)} -
- + +
+ )} + + {paymentInfo.method === 'credit_card' && ( + - -
+ )} )} @@ -556,9 +689,6 @@ const Checkout = () => {

Método de Pago

{paymentMethods.find(m => m.id === paymentInfo.method)?.name} - {paymentInfo.method === 'credit_card' && paymentInfo.cardNumber && - ` terminada en ${paymentInfo.cardNumber.slice(-4)}` - }

@@ -585,10 +715,17 @@ const Checkout = () => { diff --git a/src/pages/OrderConfirmation.tsx b/src/pages/OrderConfirmation.tsx index baba3aa..e0e77f2 100644 --- a/src/pages/OrderConfirmation.tsx +++ b/src/pages/OrderConfirmation.tsx @@ -201,12 +201,17 @@ const OrderConfirmation = () => {
- 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' + }
- {orderData.paymentInfo.cardLast4 && ( -

- Tarjeta terminada en {orderData.paymentInfo.cardLast4} + {orderData.paymentInfo.paymentIntentId && ( +

+ ID de Transacción: {orderData.paymentInfo.paymentIntentId}

)} diff --git a/src/pages/PaymentError.tsx b/src/pages/PaymentError.tsx new file mode 100644 index 0000000..7824d8a --- /dev/null +++ b/src/pages/PaymentError.tsx @@ -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 ( +
+
+ +
+
+ {/* Error Header */} +
+
+ +
+

+ Error en el Pago +

+

+ No pudimos procesar tu pago. No te preocupes, no se ha realizado ningún cargo. +

+
+ + {/* Error Details */} + + + Detalles del Error + + +
+

{errorMessage}

+
+ +
+

Razones comunes:

+
    +
  • • Fondos insuficientes en tu tarjeta
  • +
  • • Información de la tarjeta incorrecta
  • +
  • • Tu banco bloqueó la transacción por seguridad
  • +
  • • La tarjeta ha expirado o fue rechazada
  • +
+
+ +
+

¿Qué puedes hacer?

+
    +
  • • Verifica los datos de tu tarjeta e intenta nuevamente
  • +
  • • Intenta con otra tarjeta o método de pago
  • +
  • • Contacta a tu banco para autorizar la transacción
  • +
  • • Contacta nuestro soporte si el problema persiste
  • +
+
+
+
+ + {/* Action Buttons */} +
+ + +
+ + {/* Support Info */} +
+

¿Necesitas Ayuda?

+

+ Nuestro equipo de soporte está disponible 24/7 para ayudarte +

+ +
+
+
+ +
+ ); +}; + +export default PaymentError;