2025-09-25 14:30:47 +02:00
|
|
|
import React, { useState, useEffect, useRef } from 'react';
|
|
|
|
|
import { useTranslation } from 'react-i18next';
|
|
|
|
|
import { Card, Input, Button } from '../../ui';
|
2026-01-11 07:50:34 +01:00
|
|
|
import { PaymentElement, useStripe, useElements } from '@stripe/react-stripe-js';
|
2025-09-25 14:30:47 +02:00
|
|
|
import { AlertCircle, CheckCircle, CreditCard, Lock } from 'lucide-react';
|
|
|
|
|
|
|
|
|
|
interface PaymentFormProps {
|
2026-01-11 07:50:34 +01:00
|
|
|
onPaymentSuccess: (paymentMethodId?: string) => void;
|
2025-09-25 14:30:47 +02:00
|
|
|
onPaymentError: (error: string) => void;
|
|
|
|
|
className?: string;
|
|
|
|
|
bypassPayment?: boolean;
|
|
|
|
|
onBypassToggle?: () => void;
|
2025-10-18 16:03:23 +02:00
|
|
|
userName?: string;
|
|
|
|
|
userEmail?: string;
|
2025-09-25 14:30:47 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const PaymentForm: React.FC<PaymentFormProps> = ({
|
|
|
|
|
onPaymentSuccess,
|
|
|
|
|
onPaymentError,
|
|
|
|
|
className = '',
|
|
|
|
|
bypassPayment = false,
|
2025-10-18 16:03:23 +02:00
|
|
|
onBypassToggle,
|
|
|
|
|
userName = '',
|
|
|
|
|
userEmail = ''
|
2025-09-25 14:30:47 +02:00
|
|
|
}) => {
|
|
|
|
|
const { t } = useTranslation();
|
|
|
|
|
const stripe = useStripe();
|
|
|
|
|
const elements = useElements();
|
|
|
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
|
const [cardComplete, setCardComplete] = useState(false);
|
|
|
|
|
const [billingDetails, setBillingDetails] = useState({
|
2025-10-18 16:03:23 +02:00
|
|
|
name: userName,
|
|
|
|
|
email: userEmail,
|
2025-09-25 14:30:47 +02:00
|
|
|
address: {
|
|
|
|
|
line1: '',
|
|
|
|
|
city: '',
|
|
|
|
|
state: '',
|
|
|
|
|
postal_code: '',
|
|
|
|
|
country: 'ES',
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// For development mode - bypass payment option
|
|
|
|
|
const handleBypassPayment = () => {
|
|
|
|
|
if (onBypassToggle) {
|
|
|
|
|
onBypassToggle();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
|
|
|
e.preventDefault();
|
2026-01-11 07:50:34 +01:00
|
|
|
|
2025-09-25 14:30:47 +02:00
|
|
|
if (!stripe || !elements) {
|
|
|
|
|
// Stripe.js has not loaded yet
|
|
|
|
|
onPaymentError('Stripe.js no ha cargado correctamente');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (bypassPayment) {
|
|
|
|
|
// In development mode, bypass payment processing
|
|
|
|
|
onPaymentSuccess();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setLoading(true);
|
|
|
|
|
setError(null);
|
|
|
|
|
|
|
|
|
|
try {
|
2026-01-11 07:50:34 +01:00
|
|
|
// Submit the payment element to validate all inputs
|
|
|
|
|
const { error: submitError } = await elements.submit();
|
|
|
|
|
|
|
|
|
|
if (submitError) {
|
|
|
|
|
setError(submitError.message || 'Error al validar el formulario');
|
|
|
|
|
onPaymentError(submitError.message || 'Error al validar el formulario');
|
|
|
|
|
setLoading(false);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create payment method using the PaymentElement
|
|
|
|
|
// This is the correct way to create a payment method with PaymentElement
|
|
|
|
|
const { error: paymentError, paymentMethod } = await stripe.createPaymentMethod({
|
|
|
|
|
elements,
|
|
|
|
|
params: {
|
|
|
|
|
billing_details: {
|
|
|
|
|
name: billingDetails.name,
|
|
|
|
|
email: billingDetails.email,
|
|
|
|
|
address: billingDetails.address,
|
|
|
|
|
},
|
2025-09-25 14:30:47 +02:00
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-11 07:50:34 +01:00
|
|
|
if (paymentError) {
|
|
|
|
|
setError(paymentError.message || 'Error al procesar el pago');
|
|
|
|
|
onPaymentError(paymentError.message || 'Error al procesar el pago');
|
2025-09-25 14:30:47 +02:00
|
|
|
setLoading(false);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-11 07:50:34 +01:00
|
|
|
// Send the paymentMethod.id to your server to create a subscription
|
2025-09-25 14:30:47 +02:00
|
|
|
console.log('Payment method created:', paymentMethod);
|
2026-01-11 07:50:34 +01:00
|
|
|
|
|
|
|
|
// Pass the payment method ID to the parent component for server-side processing
|
|
|
|
|
onPaymentSuccess(paymentMethod?.id);
|
2025-09-25 14:30:47 +02:00
|
|
|
} catch (err) {
|
|
|
|
|
const errorMessage = err instanceof Error ? err.message : 'Error desconocido al procesar el pago';
|
|
|
|
|
setError(errorMessage);
|
|
|
|
|
onPaymentError(errorMessage);
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-11 07:50:34 +01:00
|
|
|
const handlePaymentElementChange = (event: any) => {
|
2025-09-25 14:30:47 +02:00
|
|
|
setError(event.error?.message || null);
|
|
|
|
|
setCardComplete(event.complete);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Card className={`p-6 ${className}`}>
|
|
|
|
|
<div className="text-center mb-6">
|
|
|
|
|
<h3 className="text-xl font-bold text-text-primary mb-2 flex items-center justify-center gap-2">
|
|
|
|
|
<CreditCard className="w-5 h-5" />
|
|
|
|
|
{t('auth:payment.payment_info', 'Información de Pago')}
|
|
|
|
|
</h3>
|
|
|
|
|
<p className="text-text-secondary text-sm">
|
|
|
|
|
{t('auth:payment.secure_payment', 'Tu información de pago está protegida con encriptación de extremo a extremo')}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Development mode toggle */}
|
|
|
|
|
<div className="mb-6 p-3 bg-yellow-50 border border-yellow-200 rounded-lg flex items-center justify-between">
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<Lock className="w-4 h-4 text-yellow-600" />
|
|
|
|
|
<span className="text-sm text-yellow-800">
|
|
|
|
|
{t('auth:payment.dev_mode', 'Modo Desarrollo')}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<Button
|
|
|
|
|
variant={bypassPayment ? "primary" : "outline"}
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={handleBypassPayment}
|
|
|
|
|
>
|
|
|
|
|
{bypassPayment
|
|
|
|
|
? t('auth:payment.payment_bypassed', 'Pago Bypassed')
|
|
|
|
|
: t('auth:payment.bypass_payment', 'Bypass Pago')}
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{!bypassPayment && (
|
|
|
|
|
<form onSubmit={handleSubmit}>
|
|
|
|
|
{/* Billing Details */}
|
|
|
|
|
<div className="space-y-4 mb-6">
|
|
|
|
|
<Input
|
|
|
|
|
label={t('auth:payment.cardholder_name', 'Nombre del titular')}
|
|
|
|
|
placeholder="Nombre completo"
|
|
|
|
|
value={billingDetails.name}
|
|
|
|
|
onChange={(e) => setBillingDetails({...billingDetails, name: e.target.value})}
|
|
|
|
|
required
|
|
|
|
|
disabled={loading}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<Input
|
|
|
|
|
type="email"
|
|
|
|
|
label={t('auth:payment.email', 'Correo electrónico')}
|
|
|
|
|
placeholder="tu.email@ejemplo.com"
|
|
|
|
|
value={billingDetails.email}
|
|
|
|
|
onChange={(e) => setBillingDetails({...billingDetails, email: e.target.value})}
|
|
|
|
|
required
|
|
|
|
|
disabled={loading}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
|
|
|
<Input
|
|
|
|
|
label={t('auth:payment.address_line1', 'Dirección')}
|
|
|
|
|
placeholder="Calle y número"
|
|
|
|
|
value={billingDetails.address.line1}
|
|
|
|
|
onChange={(e) => setBillingDetails({...billingDetails, address: {...billingDetails.address, line1: e.target.value}})}
|
|
|
|
|
required
|
|
|
|
|
disabled={loading}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<Input
|
|
|
|
|
label={t('auth:payment.city', 'Ciudad')}
|
|
|
|
|
placeholder="Ciudad"
|
|
|
|
|
value={billingDetails.address.city}
|
|
|
|
|
onChange={(e) => setBillingDetails({...billingDetails, address: {...billingDetails.address, city: e.target.value}})}
|
|
|
|
|
required
|
|
|
|
|
disabled={loading}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
|
|
|
<Input
|
|
|
|
|
label={t('auth:payment.state', 'Estado/Provincia')}
|
|
|
|
|
placeholder="Estado"
|
|
|
|
|
value={billingDetails.address.state}
|
|
|
|
|
onChange={(e) => setBillingDetails({...billingDetails, address: {...billingDetails.address, state: e.target.value}})}
|
|
|
|
|
required
|
|
|
|
|
disabled={loading}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<Input
|
|
|
|
|
label={t('auth:payment.postal_code', 'Código Postal')}
|
|
|
|
|
placeholder="Código postal"
|
|
|
|
|
value={billingDetails.address.postal_code}
|
|
|
|
|
onChange={(e) => setBillingDetails({...billingDetails, address: {...billingDetails.address, postal_code: e.target.value}})}
|
|
|
|
|
required
|
|
|
|
|
disabled={loading}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<Input
|
|
|
|
|
label={t('auth:payment.country', 'País')}
|
|
|
|
|
placeholder="País"
|
|
|
|
|
value={billingDetails.address.country}
|
|
|
|
|
onChange={(e) => setBillingDetails({...billingDetails, address: {...billingDetails.address, country: e.target.value}})}
|
|
|
|
|
required
|
|
|
|
|
disabled={loading}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-01-11 07:50:34 +01:00
|
|
|
{/* Payment Element */}
|
2025-09-25 14:30:47 +02:00
|
|
|
<div className="mb-6">
|
|
|
|
|
<label className="block text-sm font-medium text-text-primary mb-2">
|
2026-01-11 07:50:34 +01:00
|
|
|
{t('auth:payment.payment_details', 'Detalles de Pago')}
|
2025-09-25 14:30:47 +02:00
|
|
|
</label>
|
|
|
|
|
<div className="border border-border-primary rounded-lg p-3 bg-bg-primary">
|
2026-01-11 07:50:34 +01:00
|
|
|
<PaymentElement
|
2025-09-25 14:30:47 +02:00
|
|
|
options={{
|
2026-01-11 07:50:34 +01:00
|
|
|
layout: 'tabs',
|
|
|
|
|
fields: {
|
|
|
|
|
billingDetails: 'auto',
|
|
|
|
|
},
|
|
|
|
|
wallets: {
|
|
|
|
|
applePay: 'auto',
|
|
|
|
|
googlePay: 'auto',
|
2025-09-25 14:30:47 +02:00
|
|
|
},
|
|
|
|
|
}}
|
2026-01-11 07:50:34 +01:00
|
|
|
onChange={handlePaymentElementChange}
|
2025-09-25 14:30:47 +02:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<p className="text-xs text-text-tertiary mt-2 flex items-center gap-1">
|
|
|
|
|
<Lock className="w-3 h-3" />
|
2026-01-11 07:50:34 +01:00
|
|
|
{t('auth:payment.payment_info_secure', 'Tu información de pago está segura')}
|
2025-09-25 14:30:47 +02:00
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Error Message */}
|
|
|
|
|
{error && (
|
|
|
|
|
<div className="mb-4 bg-color-error/10 border border-color-error/20 text-color-error px-4 py-3 rounded-lg text-sm flex items-start space-x-3" role="alert">
|
|
|
|
|
<AlertCircle className="w-5 h-5 mt-0.5 flex-shrink-0" />
|
|
|
|
|
<span>{error}</span>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Submit Button */}
|
|
|
|
|
<Button
|
|
|
|
|
type="submit"
|
|
|
|
|
variant="primary"
|
|
|
|
|
size="lg"
|
|
|
|
|
isLoading={loading}
|
|
|
|
|
loadingText="Procesando pago..."
|
|
|
|
|
disabled={!stripe || loading || (!cardComplete && !bypassPayment)}
|
|
|
|
|
className="w-full"
|
|
|
|
|
>
|
|
|
|
|
{t('auth:payment.process_payment', 'Procesar Pago')}
|
|
|
|
|
</Button>
|
|
|
|
|
</form>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{bypassPayment && (
|
|
|
|
|
<div className="text-center py-8">
|
|
|
|
|
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-green-100 mb-4">
|
|
|
|
|
<CheckCircle className="w-8 h-8 text-green-600" />
|
|
|
|
|
</div>
|
|
|
|
|
<h4 className="text-lg font-semibold text-text-primary mb-2">
|
|
|
|
|
{t('auth:payment.payment_bypassed_title', 'Pago Bypassed')}
|
|
|
|
|
</h4>
|
|
|
|
|
<p className="text-text-secondary mb-6">
|
|
|
|
|
{t('auth:payment.payment_bypassed_description', 'El proceso de pago ha sido omitido en modo desarrollo. El registro continuará normalmente.')}
|
|
|
|
|
</p>
|
|
|
|
|
<Button
|
|
|
|
|
variant="primary"
|
|
|
|
|
size="lg"
|
2026-01-11 07:50:34 +01:00
|
|
|
onClick={() => onPaymentSuccess()}
|
2025-09-25 14:30:47 +02:00
|
|
|
className="w-full"
|
|
|
|
|
>
|
|
|
|
|
{t('auth:payment.continue_registration', 'Continuar con el Registro')}
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</Card>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export default PaymentForm;
|