Add subcription feature 2
This commit is contained in:
@@ -70,6 +70,52 @@ const PaymentForm: React.FC<PaymentFormProps> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
// Perform client-side validation before sending to Stripe
|
||||
if (!billingDetails.name.trim()) {
|
||||
setError('El nombre del titular es obligatorio');
|
||||
onPaymentError('El nombre del titular es obligatorio');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!billingDetails.email.trim()) {
|
||||
setError('El correo electrónico es obligatorio');
|
||||
onPaymentError('El correo electrónico es obligatorio');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Basic email validation
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(billingDetails.email)) {
|
||||
setError('Por favor, ingrese un correo electrónico válido');
|
||||
onPaymentError('Por favor, ingrese un correo electrónico válido');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate required address fields
|
||||
if (!billingDetails.address.line1.trim()) {
|
||||
setError('La dirección es obligatoria');
|
||||
onPaymentError('La dirección es obligatoria');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!billingDetails.address.city.trim()) {
|
||||
setError('La ciudad es obligatoria');
|
||||
onPaymentError('La ciudad es obligatoria');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!billingDetails.address.postal_code.trim()) {
|
||||
setError('El código postal es obligatorio');
|
||||
onPaymentError('El código postal es obligatorio');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
@@ -126,6 +172,16 @@ const PaymentForm: React.FC<PaymentFormProps> = ({
|
||||
setCardComplete(event.complete);
|
||||
};
|
||||
|
||||
// Add a cleanup function to ensure loading state is reset when component unmounts
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
// Ensure loading is reset when component unmounts to prevent stuck states
|
||||
if (loading) {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Card className={`p-6 ${className}`}>
|
||||
<div className="text-center mb-6">
|
||||
|
||||
@@ -75,6 +75,18 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
|
||||
const [errors, setErrors] = useState<Partial<SimpleUserRegistration>>({});
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
||||
const [loading, setLoading] = useState(false); // Local loading state for 3DS flow
|
||||
|
||||
// State for pending subscription data (needed for post-3DS completion)
|
||||
const [pendingSubscriptionData, setPendingSubscriptionData] = useState<{
|
||||
customer_id: string;
|
||||
plan_id: string;
|
||||
payment_method_id: string;
|
||||
trial_period_days?: number;
|
||||
user_id: string;
|
||||
billing_interval: string;
|
||||
setup_intent_id?: string;
|
||||
} | null>(null);
|
||||
|
||||
const { register, registerWithSubscription } = useAuthActions();
|
||||
const isLoading = useAuthLoading();
|
||||
@@ -144,14 +156,76 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
|
||||
// directly to backend via secure API calls.
|
||||
|
||||
// Clean up any old registration_progress data on mount (security fix)
|
||||
// Also check for Stripe 3DS redirect parameters
|
||||
useEffect(() => {
|
||||
try {
|
||||
localStorage.removeItem('registration_progress');
|
||||
localStorage.removeItem('wizardState'); // Clean up wizard state too
|
||||
} catch (err) {
|
||||
console.error('Error cleaning up old localStorage data:', err);
|
||||
// Prevent multiple executions of 3DS redirect handling
|
||||
const hasProcessedRedirect = sessionStorage.getItem('3ds_redirect_processed');
|
||||
|
||||
if (hasProcessedRedirect) {
|
||||
return;
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Clean up old data
|
||||
localStorage.removeItem('registration_progress');
|
||||
localStorage.removeItem('wizardState');
|
||||
|
||||
// Check if this is a Stripe redirect after 3DS authentication
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const setupIntent = urlParams.get('setup_intent');
|
||||
const redirectStatus = urlParams.get('redirect_status');
|
||||
|
||||
if (setupIntent && redirectStatus) {
|
||||
console.log('3DS redirect detected:', { setupIntent, redirectStatus });
|
||||
|
||||
// Mark as processed
|
||||
sessionStorage.setItem('3ds_redirect_processed', 'true');
|
||||
|
||||
// Clean the URL immediately
|
||||
const cleanUrl = window.location.pathname;
|
||||
window.history.replaceState({}, '', cleanUrl);
|
||||
|
||||
if (redirectStatus === 'succeeded') {
|
||||
// Retrieve pending registration data (NEW ARCHITECTURE)
|
||||
const pendingDataStr = sessionStorage.getItem('pending_registration_data');
|
||||
|
||||
if (pendingDataStr) {
|
||||
try {
|
||||
setLoading(true);
|
||||
const pendingData = JSON.parse(pendingDataStr);
|
||||
console.log('3DS redirect detected - completing registration (NEW ARCHITECTURE)');
|
||||
|
||||
// Complete the registration (create user and subscription)
|
||||
completeRegistrationAfterSetupIntent(setupIntent, pendingData);
|
||||
} catch (err) {
|
||||
console.error('Error parsing pending registration data:', err);
|
||||
showToast.error('Error al procesar datos de registro', {
|
||||
title: 'Error'
|
||||
});
|
||||
setLoading(false);
|
||||
}
|
||||
} else {
|
||||
console.warn('No pending registration data found after 3DS redirect');
|
||||
showToast.error('No se encontraron datos de registro pendientes', {
|
||||
title: 'Error'
|
||||
});
|
||||
setLoading(false);
|
||||
}
|
||||
} else if (redirectStatus === 'failed') {
|
||||
sessionStorage.removeItem('pending_registration_data');
|
||||
showToast.error('La autenticación 3D Secure ha fallado', {
|
||||
title: 'Error de autenticación'
|
||||
});
|
||||
setCurrentStep('payment');
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
// Clear the flag
|
||||
setTimeout(() => {
|
||||
sessionStorage.removeItem('3ds_redirect_processed');
|
||||
}, 1000);
|
||||
}
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
|
||||
const validateForm = (): boolean => {
|
||||
const newErrors: Partial<SimpleUserRegistration> = {};
|
||||
@@ -219,31 +293,71 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
|
||||
|
||||
const handleRegistrationSubmit = async (paymentMethodId?: string) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const registrationData = {
|
||||
full_name: formData.full_name,
|
||||
email: formData.email,
|
||||
password: formData.password,
|
||||
tenant_name: 'Default Bakery', // Default value since we're not collecting it
|
||||
tenant_name: 'Default Bakery',
|
||||
subscription_plan: selectedPlan,
|
||||
billing_cycle: billingCycle, // Add billing cycle selection
|
||||
billing_cycle: billingCycle,
|
||||
payment_method_id: paymentMethodId,
|
||||
// Include coupon code if pilot customer
|
||||
coupon_code: isPilot ? couponCode : undefined,
|
||||
// Include consent data
|
||||
terms_accepted: formData.acceptTerms,
|
||||
privacy_accepted: formData.acceptTerms,
|
||||
marketing_consent: formData.marketingConsent,
|
||||
analytics_consent: formData.analyticsConsent,
|
||||
// NEW: Include billing address data for subscription creation
|
||||
address: formData.address,
|
||||
postal_code: formData.postal_code,
|
||||
city: formData.city,
|
||||
country: formData.country,
|
||||
};
|
||||
|
||||
// Use the new registration endpoint with subscription creation
|
||||
await registerWithSubscription(registrationData);
|
||||
// Call registration endpoint
|
||||
const response = await registerWithSubscription(registrationData);
|
||||
|
||||
// Validate response exists
|
||||
if (!response) {
|
||||
throw new Error('Registration failed: No response from server');
|
||||
}
|
||||
|
||||
// NEW ARCHITECTURE: Check if SetupIntent verification is required
|
||||
if (response.requires_action && response.client_secret) {
|
||||
console.log('SetupIntent verification required (NEW ARCHITECTURE - no user created yet):', {
|
||||
action_type: response.action_type,
|
||||
setup_intent_id: response.setup_intent_id,
|
||||
customer_id: response.customer_id,
|
||||
email: response.email
|
||||
});
|
||||
|
||||
// Store pending registration data for post-3DS completion
|
||||
const pendingData = {
|
||||
email: registrationData.email,
|
||||
password: registrationData.password,
|
||||
full_name: registrationData.full_name,
|
||||
setup_intent_id: response.setup_intent_id!,
|
||||
plan_id: response.plan_id!,
|
||||
payment_method_id: paymentMethodId!,
|
||||
billing_interval: billingCycle,
|
||||
coupon_code: registrationData.coupon_code,
|
||||
customer_id: response.customer_id!,
|
||||
payment_customer_id: response.payment_customer_id!,
|
||||
trial_period_days: response.trial_period_days ?? undefined,
|
||||
client_secret: response.client_secret!
|
||||
};
|
||||
|
||||
setPendingSubscriptionData(pendingData);
|
||||
sessionStorage.setItem('pending_registration_data', JSON.stringify(pendingData));
|
||||
|
||||
setLoading(false);
|
||||
|
||||
// Handle SetupIntent confirmation (with possible 3DS)
|
||||
await handleSetupIntentConfirmation(response.client_secret, paymentMethodId!, pendingData);
|
||||
return;
|
||||
}
|
||||
|
||||
// Subscription created successfully without 3DS
|
||||
const successMessage = isPilot
|
||||
? '¡Bienvenido al programa piloto! Tu cuenta ha sido creada con 3 meses gratis.'
|
||||
: '¡Bienvenido! Tu cuenta ha sido creada correctamente.';
|
||||
@@ -251,9 +365,12 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
|
||||
showToast.success(t('auth:register.registering', successMessage), {
|
||||
title: t('auth:alerts.success_create', 'Cuenta creada exitosamente')
|
||||
});
|
||||
|
||||
onSuccess?.();
|
||||
} catch (err) {
|
||||
showToast.error(error || t('auth:register.register_button', 'No se pudo crear la account. Verifica que el email no esté en uso.'), {
|
||||
setLoading(false);
|
||||
const errorMessage = err instanceof Error ? err.message : 'No se pudo crear la cuenta';
|
||||
showToast.error(errorMessage, {
|
||||
title: t('auth:alerts.error_create', 'Error al crear la cuenta')
|
||||
});
|
||||
}
|
||||
@@ -269,6 +386,141 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
|
||||
});
|
||||
};
|
||||
|
||||
// Handle SetupIntent confirmation with 3DS support
|
||||
const handleSetupIntentConfirmation = async (
|
||||
clientSecret: string,
|
||||
paymentMethodId: string,
|
||||
pendingData: {
|
||||
email: string;
|
||||
password: string;
|
||||
full_name: string;
|
||||
setup_intent_id: string;
|
||||
plan_id: string;
|
||||
payment_method_id: string;
|
||||
billing_interval: string;
|
||||
coupon_code?: string;
|
||||
customer_id: string;
|
||||
payment_customer_id: string;
|
||||
trial_period_days?: number;
|
||||
client_secret: string;
|
||||
}
|
||||
) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const stripeInstance = await stripePromise;
|
||||
|
||||
if (!stripeInstance) {
|
||||
throw new Error('Stripe.js has not loaded correctly.');
|
||||
}
|
||||
|
||||
console.log('Confirming SetupIntent (NEW ARCHITECTURE - no user created yet):', { clientSecret });
|
||||
|
||||
// Confirm the SetupIntent with 3DS authentication if needed
|
||||
const returnUrl = `${window.location.origin}${window.location.pathname}`;
|
||||
|
||||
const { error: stripeError, setupIntent } = await stripeInstance.confirmCardSetup(clientSecret, {
|
||||
payment_method: paymentMethodId,
|
||||
return_url: returnUrl
|
||||
});
|
||||
|
||||
if (stripeError) {
|
||||
console.error('SetupIntent confirmation error:', stripeError);
|
||||
showToast.error(stripeError.message || 'Error en la verificación del método de pago', {
|
||||
title: 'Error de autenticación'
|
||||
});
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if setup intent succeeded (no 3DS required, or 3DS completed in modal)
|
||||
if (setupIntent && setupIntent.status === 'succeeded') {
|
||||
console.log('SetupIntent succeeded, completing registration (NEW ARCHITECTURE - creating user now)');
|
||||
await completeRegistrationAfterSetupIntent(setupIntent.id, pendingData);
|
||||
} else if (setupIntent && setupIntent.status === 'requires_action') {
|
||||
// 3DS redirect happened - the completion will be handled in URL redirect handler
|
||||
console.log('SetupIntent requires 3DS redirect, waiting for return...');
|
||||
// Don't set loading to false - keep spinner while redirecting
|
||||
} else {
|
||||
console.error('Unexpected SetupIntent status:', setupIntent?.status);
|
||||
showToast.error(`Estado inesperado: ${setupIntent?.status}`, {
|
||||
title: 'Error'
|
||||
});
|
||||
setLoading(false);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error during SetupIntent confirmation:', err);
|
||||
const errorMessage = err instanceof Error ? err.message : 'Error durante la verificación';
|
||||
showToast.error(errorMessage, {
|
||||
title: 'Error de autenticación'
|
||||
});
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Complete subscription creation after SetupIntent confirmation
|
||||
// Complete registration after SetupIntent confirmation (NEW ARCHITECTURE)
|
||||
const completeRegistrationAfterSetupIntent = async (
|
||||
setupIntentId: string,
|
||||
pendingData: {
|
||||
email: string;
|
||||
password: string;
|
||||
full_name: string;
|
||||
setup_intent_id: string;
|
||||
plan_id: string;
|
||||
payment_method_id: string;
|
||||
billing_interval: string;
|
||||
coupon_code?: string;
|
||||
customer_id: string;
|
||||
payment_customer_id: string;
|
||||
trial_period_days?: number;
|
||||
client_secret: string;
|
||||
}
|
||||
) => {
|
||||
try {
|
||||
console.log('Completing registration after SetupIntent confirmation (NEW ARCHITECTURE - creating user now)');
|
||||
|
||||
// Call backend to complete registration (create user and subscription)
|
||||
const completionData = {
|
||||
email: pendingData.email,
|
||||
password: pendingData.password,
|
||||
full_name: pendingData.full_name,
|
||||
setup_intent_id: setupIntentId,
|
||||
plan_id: pendingData.plan_id,
|
||||
payment_method_id: pendingData.payment_method_id,
|
||||
billing_interval: pendingData.billing_interval,
|
||||
coupon_code: pendingData.coupon_code
|
||||
};
|
||||
|
||||
const response = await completeRegistrationAfterSetupIntent(completionData);
|
||||
|
||||
if (response.access_token) {
|
||||
console.log('Registration completed successfully - user created and authenticated');
|
||||
const successMessage = isPilot
|
||||
? '¡Bienvenido al programa piloto! Tu cuenta ha sido creada con 3 meses gratis.'
|
||||
: '¡Bienvenido! Tu cuenta ha sido creada correctamente.';
|
||||
|
||||
showToast.success(t('auth:register.registering', successMessage), {
|
||||
title: t('auth:alerts.success_create', 'Cuenta creada exitosamente')
|
||||
});
|
||||
|
||||
// Clear pending registration data
|
||||
sessionStorage.removeItem('pending_registration_data');
|
||||
setPendingSubscriptionData(null);
|
||||
|
||||
onSuccess?.();
|
||||
} else {
|
||||
throw new Error('Registration completion failed: No access token received');
|
||||
}
|
||||
} catch (err) {
|
||||
setLoading(false);
|
||||
const errorMessage = err instanceof Error ? err.message : 'Error completando el registro';
|
||||
showToast.error(errorMessage, {
|
||||
title: 'Error de registro'
|
||||
});
|
||||
console.error('Error completing registration:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputChange = (field: keyof SimpleUserRegistration) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value;
|
||||
setFormData(prev => ({ ...prev, [field]: value }));
|
||||
|
||||
@@ -33,6 +33,7 @@ export const PaymentMethodUpdateModal: React.FC<PaymentMethodUpdateModalProps> =
|
||||
}) => {
|
||||
const { t } = useTranslation('subscription');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [authenticating, setAuthenticating] = useState(false);
|
||||
const [paymentMethodId, setPaymentMethodId] = useState('');
|
||||
const [stripe, setStripe] = useState<any>(null);
|
||||
const [elements, setElements] = useState<any>(null);
|
||||
@@ -141,24 +142,139 @@ export const PaymentMethodUpdateModal: React.FC<PaymentMethodUpdateModalProps> =
|
||||
// Call backend to update payment method
|
||||
const result = await subscriptionService.updatePaymentMethod(tenantId, paymentMethod.id);
|
||||
|
||||
if (result.success) {
|
||||
setSuccess(true);
|
||||
showToast.success(result.message);
|
||||
|
||||
// Notify parent component about the update
|
||||
onPaymentMethodUpdated({
|
||||
brand: result.brand,
|
||||
last4: result.last4,
|
||||
exp_month: result.exp_month,
|
||||
exp_year: result.exp_year,
|
||||
// Log 3DS requirement status
|
||||
if (result.requires_action) {
|
||||
console.log('3DS authentication required for payment method update', {
|
||||
payment_method_id: paymentMethod.id,
|
||||
setup_intent_id: result.payment_intent_id,
|
||||
action_required: result.requires_action
|
||||
});
|
||||
|
||||
// Close modal after a brief delay to show success message
|
||||
setTimeout(() => {
|
||||
onClose();
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
if (result.success) {
|
||||
// Check if 3D Secure authentication is required
|
||||
if (result.requires_action && result.client_secret) {
|
||||
setAuthenticating(true);
|
||||
setLoading(false);
|
||||
setError(null);
|
||||
|
||||
console.log('Starting 3DS authentication process', {
|
||||
client_secret: result.client_secret.substring(0, 20) + '...', // Log partial secret for security
|
||||
payment_method_id: paymentMethod.id,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
try {
|
||||
// Handle 3D Secure authentication
|
||||
const { error: confirmError, setupIntent } = await stripe.confirmCardSetup(result.client_secret);
|
||||
|
||||
console.log('3DS authentication completed', {
|
||||
setup_intent_status: setupIntent?.status,
|
||||
error: confirmError ? confirmError.message : 'none',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
if (confirmError) {
|
||||
// Handle specific 3D Secure error types
|
||||
if (confirmError.type === 'card_error') {
|
||||
setError(confirmError.message || 'Card authentication failed');
|
||||
} else if (confirmError.type === 'validation_error') {
|
||||
setError('Invalid authentication request');
|
||||
} else if (confirmError.type === 'api_error') {
|
||||
setError('Authentication service unavailable');
|
||||
} else {
|
||||
setError(confirmError.message || '3D Secure authentication failed');
|
||||
}
|
||||
setAuthenticating(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check setup intent status after authentication
|
||||
if (setupIntent && setupIntent.status === 'succeeded') {
|
||||
setSuccess(true);
|
||||
showToast.success('Payment method updated and authenticated successfully');
|
||||
|
||||
console.log('3DS authentication successful', {
|
||||
payment_method_id: paymentMethod.id,
|
||||
setup_intent_id: setupIntent.id,
|
||||
setup_intent_status: setupIntent.status,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
// Notify parent component about the update
|
||||
onPaymentMethodUpdated({
|
||||
brand: result.brand,
|
||||
last4: result.last4,
|
||||
exp_month: result.exp_month,
|
||||
exp_year: result.exp_year,
|
||||
});
|
||||
|
||||
// Close modal after a brief delay to show success message
|
||||
setTimeout(() => {
|
||||
onClose();
|
||||
}, 2000);
|
||||
} else {
|
||||
console.log('3DS authentication completed with non-success status', {
|
||||
setup_intent_status: setupIntent?.status,
|
||||
payment_method_id: paymentMethod.id,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
setError('3D Secure authentication completed but payment method not confirmed');
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
} catch (authError) {
|
||||
console.error('Error during 3D Secure authentication:', authError);
|
||||
|
||||
// Enhanced error handling for 3DS failures
|
||||
if (authError instanceof Error) {
|
||||
if (authError.message.includes('canceled') || authError.message.includes('user closed')) {
|
||||
setError('3D Secure authentication was canceled. You can try again with the same card.');
|
||||
} else if (authError.message.includes('failed') || authError.message.includes('declined')) {
|
||||
setError('3D Secure authentication failed. Please try again or use a different card.');
|
||||
} else if (authError.message.includes('timeout') || authError.message.includes('network')) {
|
||||
setError('Network error during 3D Secure authentication. Please check your connection and try again.');
|
||||
} else {
|
||||
setError(`3D Secure authentication error: ${authError.message}`);
|
||||
}
|
||||
} else {
|
||||
setError('3D Secure authentication error. Please try again.');
|
||||
}
|
||||
|
||||
setAuthenticating(false);
|
||||
}
|
||||
} else {
|
||||
// No authentication required
|
||||
setSuccess(true);
|
||||
showToast.success(result.message);
|
||||
|
||||
// Notify parent component about the update
|
||||
onPaymentMethodUpdated({
|
||||
brand: result.brand,
|
||||
last4: result.last4,
|
||||
exp_month: result.exp_month,
|
||||
exp_year: result.exp_year,
|
||||
});
|
||||
|
||||
// Close modal after a brief delay to show success message
|
||||
setTimeout(() => {
|
||||
onClose();
|
||||
}, 2000);
|
||||
}
|
||||
} else {
|
||||
setError(result.message || 'Failed to update payment method');
|
||||
// Handle different payment intent statuses
|
||||
if (result.payment_intent_status === 'requires_payment_method') {
|
||||
setError('Your card was declined. Please try a different payment method.');
|
||||
} else if (result.payment_intent_status === 'requires_action') {
|
||||
setError('Authentication required but client secret missing. Please try again.');
|
||||
} else if (result.payment_intent_status === 'processing') {
|
||||
setError('Payment is processing. Please wait a few minutes and try again.');
|
||||
} else if (result.payment_intent_status === 'canceled') {
|
||||
setError('Payment was canceled. Please try again.');
|
||||
} else {
|
||||
setError(result.message || 'Failed to update payment method');
|
||||
}
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
@@ -223,10 +339,28 @@ export const PaymentMethodUpdateModal: React.FC<PaymentMethodUpdateModalProps> =
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{authenticating && (
|
||||
<div className="p-3 bg-blue-500/10 border border-blue-500/20 rounded-lg flex items-center gap-2 text-blue-500">
|
||||
<CreditCard className="w-4 h-4" />
|
||||
<span className="text-sm">Completing secure authentication with your bank...</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="p-3 bg-red-500/10 border border-red-500/20 rounded-lg flex items-center gap-2 text-red-500">
|
||||
<AlertCircle className="w-4 h-4" />
|
||||
<span className="text-sm">{error}</span>
|
||||
{error.includes('3D Secure') && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="text"
|
||||
size="sm"
|
||||
onClick={() => setError(null)}
|
||||
className="ml-auto text-red-600 hover:text-red-800"
|
||||
>
|
||||
Try Again
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -242,18 +376,18 @@ export const PaymentMethodUpdateModal: React.FC<PaymentMethodUpdateModalProps> =
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={handleClose}
|
||||
disabled={loading}
|
||||
disabled={loading || authenticating}
|
||||
>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
disabled={loading || !cardElement}
|
||||
disabled={loading || authenticating || !cardElement}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
{loading && <Loader2 className="w-4 h-4 animate-spin" />}
|
||||
{loading ? 'Procesando...' : 'Actualizar Método de Pago'}
|
||||
{(loading || authenticating) && <Loader2 className="w-4 h-4 animate-spin" />}
|
||||
{authenticating ? 'Autenticando...' : loading ? 'Procesando...' : 'Actualizar Método de Pago'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
Reference in New Issue
Block a user