Support subcription payments

This commit is contained in:
Urtzi Alfaro
2025-09-25 14:30:47 +02:00
parent f02a980c87
commit 89b75bd7af
22 changed files with 2119 additions and 364 deletions

View File

@@ -0,0 +1,285 @@
import React, { useState, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { Card, Input, Button } from '../../ui';
import { CardElement, useStripe, useElements } from '@stripe/react-stripe-js';
import { AlertCircle, CheckCircle, CreditCard, Lock } from 'lucide-react';
interface PaymentFormProps {
onPaymentSuccess: () => void;
onPaymentError: (error: string) => void;
className?: string;
bypassPayment?: boolean;
onBypassToggle?: () => void;
}
const PaymentForm: React.FC<PaymentFormProps> = ({
onPaymentSuccess,
onPaymentError,
className = '',
bypassPayment = false,
onBypassToggle
}) => {
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({
name: '',
email: '',
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();
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 {
// Create payment method
const { error, paymentMethod } = await stripe.createPaymentMethod({
type: 'card',
card: elements.getElement('card')!,
billing_details: {
name: billingDetails.name,
email: billingDetails.email,
address: billingDetails.address,
},
});
if (error) {
setError(error.message || 'Error al procesar el pago');
onPaymentError(error.message || 'Error al procesar el pago');
setLoading(false);
return;
}
// In a real application, you would send the paymentMethod.id to your server
// to create a subscription. For now, we'll simulate success.
console.log('Payment method created:', paymentMethod);
onPaymentSuccess();
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Error desconocido al procesar el pago';
setError(errorMessage);
onPaymentError(errorMessage);
} finally {
setLoading(false);
}
};
const handleCardChange = (event: any) => {
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>
{/* Card Element */}
<div className="mb-6">
<label className="block text-sm font-medium text-text-primary mb-2">
{t('auth:payment.card_details', 'Detalles de la tarjeta')}
</label>
<div className="border border-border-primary rounded-lg p-3 bg-bg-primary">
<CardElement
options={{
style: {
base: {
fontSize: '16px',
color: '#424770',
'::placeholder': {
color: '#aab7c4',
},
},
invalid: {
color: '#9e2146',
},
},
}}
onChange={handleCardChange}
/>
</div>
<p className="text-xs text-text-tertiary mt-2 flex items-center gap-1">
<Lock className="w-3 h-3" />
{t('auth:payment.card_info_secure', 'Tu información de tarjeta está segura')}
</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"
onClick={onPaymentSuccess}
className="w-full"
>
{t('auth:payment.continue_registration', 'Continuar con el Registro')}
</Button>
</div>
)}
</Card>
);
};
export default PaymentForm;