Implement Stripe Phase 2
This commit is contained in:
@@ -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 />
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
101
src/pages/PaymentError.tsx
Normal 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;
|
||||||
Reference in New Issue
Block a user