import React, { useState, useEffect } from 'react'; import { useStripe, useElements, PaymentElement } from '@stripe/react-stripe-js'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import { useStartRegistration, useCompleteRegistration } from '@/api/hooks/auth'; import { ThreeDSAuthenticationRequired } from '@/api/types/auth'; import { Button, Card, Input } from '../../ui'; import { Loader2, CreditCard, Lock, AlertCircle, CheckCircle } from 'lucide-react'; import { subscriptionService, apiClient } from '../../../api'; import { useAuthStore } from '../../../stores/auth.store'; import { getSubscriptionFromJWT, getTenantAccessFromJWT, getPrimaryTenantIdFromJWT } from '../../../utils/jwt'; import { showToast } from '../../../utils/toast'; // Supported countries for billing address const SUPPORTED_COUNTRIES = [ { code: 'ES', name: 'España' }, { code: 'FR', name: 'France' }, { code: 'DE', name: 'Deutschland' }, { code: 'IT', name: 'Italia' }, { code: 'PT', name: 'Portugal' }, { code: 'GB', name: 'United Kingdom' }, { code: 'US', name: 'United States' }, ]; interface PaymentStepProps { onNext: () => void; onBack: () => void; onPaymentComplete: (paymentData: any) => void; registrationState: any; updateRegistrationState: (updates: any) => void; } export const PaymentStep: React.FC = ({ onNext, onBack, onPaymentComplete, registrationState, updateRegistrationState }) => { const { t } = useTranslation(); const navigate = useNavigate(); const stripe = useStripe(); const elements = useElements(); // Use React Query mutations for atomic registration const startRegistrationMutation = useStartRegistration(); const completeRegistrationMutation = useCompleteRegistration(); // Helper function to save registration state for 3DS redirect recovery const saveRegistrationStateFor3DSRedirect = (setupIntentId: string) => { const stateToSave = { basicInfo: registrationState.basicInfo, subscription: registrationState.subscription, paymentSetup: { customerId: registrationState.paymentSetup?.customerId || '', paymentMethodId: pendingPaymentMethodId || registrationState.paymentSetup?.paymentMethodId, planId: registrationState.subscription.planId, }, timestamp: new Date().toISOString(), }; const savedStateKey = `registration_state_${setupIntentId}`; sessionStorage.setItem(savedStateKey, JSON.stringify(stateToSave)); console.log('Saved registration state for 3DS redirect recovery:', savedStateKey); }; // Helper function to perform auto-login after registration const performAutoLogin = (result: any) => { if (result.access_token && result.refresh_token) { // Set tokens in API client apiClient.setAuthToken(result.access_token); apiClient.setRefreshToken(result.refresh_token); // Extract subscription from JWT const jwtSubscription = getSubscriptionFromJWT(result.access_token); const jwtTenantAccess = getTenantAccessFromJWT(result.access_token); const primaryTenantId = getPrimaryTenantIdFromJWT(result.access_token); // Update auth store directly using setState useAuthStore.setState({ user: result.user ? { id: result.user.id, email: result.user.email, full_name: result.user.full_name, is_active: result.user.is_active, is_verified: result.user.is_verified || false, created_at: result.user.created_at || new Date().toISOString(), role: result.user.role || 'admin', } : null, token: result.access_token, refreshToken: result.refresh_token, isAuthenticated: true, isLoading: false, error: null, jwtSubscription, jwtTenantAccess, primaryTenantId, pendingSubscriptionId: result.subscription_id || null, }); return true; } return false; }; const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [success, setSuccess] = useState(false); const [requires3DS, setRequires3DS] = useState(false); const [clientSecret, setClientSecret] = useState(null); const [setupIntentId, setSetupIntentId] = useState(null); // Store payment method ID separately for 3DS flow const [pendingPaymentMethodId, setPendingPaymentMethodId] = useState(null); // Track if 3DS has been auto-triggered to prevent multiple calls const [threeDSTriggered, setThreeDSTriggered] = useState(false); const [billingDetails, setBillingDetails] = useState({ name: registrationState.basicInfo.firstName + ' ' + registrationState.basicInfo.lastName, email: registrationState.basicInfo.email, address: { line1: '', city: '', state: '', postal_code: '', country: navigator.language?.split('-')[1]?.toUpperCase() || 'ES', // Auto-detect country from browser }, }); // Helper function to get plan price - uses plan metadata from registration state const getPlanPrice = (planId: string, billingInterval: string, couponCode?: string) => { // Get pricing from registration state (this contains the actual selected plan's pricing) const planMetadata = registrationState.subscription.planMetadata; if (planMetadata) { // Use actual pricing from the plan metadata const price = billingInterval === 'monthly' ? planMetadata.monthly_price : planMetadata.yearly_price; // Apply coupon logic - PILOT2025 shows €0 if (couponCode === 'PILOT2025') { return '€0'; } // Format price properly return subscriptionService.formatPrice(price); } // Fallback pricing if metadata not available (should match backend default pricing) const fallbackPricing = { starter: { monthly: 9.99, yearly: 99.99 }, professional: { monthly: 29.99, yearly: 299.99 }, enterprise: { monthly: 99.99, yearly: 999.99 } }; const price = fallbackPricing[planId as keyof typeof fallbackPricing]?.[billingInterval as 'monthly' | 'yearly'] || 0; // Apply coupon logic - PILOT2025 shows €0 if (couponCode === 'PILOT2025') { return '€0'; } return subscriptionService.formatPrice(price); }; // Helper function to get plan name - uses plan metadata from registration state const getPlanName = (planId: string) => { const planMetadata = registrationState.subscription.planMetadata; if (planMetadata && planMetadata.name) { return planMetadata.name; } // Fallback names switch (planId) { case 'starter': return t('auth:plan_starter', 'Starter'); case 'professional': return t('auth:plan_professional', 'Professional'); case 'enterprise': return t('auth:plan_enterprise', 'Enterprise'); default: return planId; } }; useEffect(() => { // Check if we already have a successful payment setup if (registrationState.paymentSetup?.success) { setSuccess(true); } }, [registrationState.paymentSetup]); // Auto-trigger 3DS authentication when required useEffect(() => { if (requires3DS && clientSecret && stripe && !threeDSTriggered && !loading) { setThreeDSTriggered(true); handle3DSAuthentication(); } }, [requires3DS, clientSecret, stripe, threeDSTriggered, loading]); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setLoading(true); setError(null); if (!stripe || !elements) { setError(t('auth:stripe_not_loaded')); setLoading(false); return; } try { // Submit the payment element to validate all inputs const { error: submitError } = await elements.submit(); if (submitError) { setError(submitError.message || t('auth:payment_validation_failed')); setLoading(false); return; } // Create payment method using PaymentElement const { error: stripeError, paymentMethod } = await stripe.createPaymentMethod({ elements, params: { billing_details: { name: billingDetails.name, email: billingDetails.email, address: billingDetails.address, }, }, }); if (stripeError) { setError(stripeError.message || t('auth:payment_method_creation_failed')); setLoading(false); return; } if (!paymentMethod) { setError(t('auth:no_payment_method_created')); setLoading(false); return; } // Call backend to start registration with SetupIntent-first approach const paymentSetupResult = await startRegistrationMutation.mutateAsync({ email: registrationState.basicInfo.email, password: registrationState.basicInfo.password || '', full_name: `${registrationState.basicInfo.firstName} ${registrationState.basicInfo.lastName}`, subscription_plan: registrationState.subscription.planId, payment_method_id: paymentMethod.id, billing_cycle: registrationState.subscription.billingInterval, coupon_code: registrationState.subscription.couponCode, }); // Handle 3DS authentication requirement if (paymentSetupResult.requires_action) { const setupIntentIdValue = paymentSetupResult.setup_intent_id ?? ''; setRequires3DS(true); setClientSecret(paymentSetupResult.client_secret ?? null); setSetupIntentId(setupIntentIdValue || null); // Store payment method ID for use after 3DS completion setPendingPaymentMethodId(paymentMethod.id); // Update paymentSetup state with customer_id and trial_period_days for redirect recovery updateRegistrationState({ paymentSetup: { success: false, customerId: paymentSetupResult.customer_id ?? paymentSetupResult.payment_customer_id ?? '', subscriptionId: '', paymentMethodId: paymentMethod.id, planId: registrationState.subscription.planId, trialPeriodDays: paymentSetupResult.trial_period_days ?? (registrationState.subscription.useTrial ? 90 : 0), }, }); // Save state for potential 3DS redirect recovery (external bank redirect) if (setupIntentIdValue) { saveRegistrationStateFor3DSRedirect(setupIntentIdValue); } setLoading(false); return; } // Payment setup successful updateRegistrationState({ paymentSetup: { success: true, customerId: paymentSetupResult.customer_id ?? '', subscriptionId: paymentSetupResult.subscription_id ?? '', paymentMethodId: paymentMethod.id, planId: registrationState.subscription.planId, }, }); setSuccess(true); onPaymentComplete(paymentSetupResult); } catch (error) { if (error instanceof ThreeDSAuthenticationRequired) { setRequires3DS(true); setClientSecret(error.client_secret); setSetupIntentId(error.setup_intent_id); // Save state for potential 3DS redirect recovery if (error.setup_intent_id) { saveRegistrationStateFor3DSRedirect(error.setup_intent_id); } } else { setError(error instanceof Error ? error.message : t('auth:payment.payment_setup_failed', 'Payment setup failed')); } } finally { setLoading(false); } }; const handle3DSAuthentication = async () => { if (!stripe || !clientSecret) { setError(t('auth:stripe_not_loaded')); return; } setLoading(true); setError(null); try { // Confirm the SetupIntent with 3DS authentication // Payment method is already attached, so we use confirmCardSetup with just the clientSecret // This will trigger the 3DS modal from Stripe automatically const result = await stripe.confirmCardSetup(clientSecret); // Check for errors if (result.error) { setError(result.error.message || t('auth:3ds_authentication_failed')); setLoading(false); return; } // SetupIntent is returned when confirmation succeeds const setupIntent = result.setupIntent; if (!setupIntent || setupIntent.status !== 'succeeded') { setError(t('auth:3ds_authentication_not_completed')); setLoading(false); return; } // Get the payment method ID from the confirmed SetupIntent or use the pending one const confirmedPaymentMethodId = typeof setupIntent.payment_method === 'string' ? setupIntent.payment_method : setupIntent.payment_method?.id || pendingPaymentMethodId; // Complete registration with verified SetupIntent using React Query mutation // Send coupon_code to backend for trial period calculation const verificationResult = await completeRegistrationMutation.mutateAsync({ setup_intent_id: setupIntentId || '', user_data: { email: registrationState.basicInfo.email, password: registrationState.basicInfo.password || '', full_name: `${registrationState.basicInfo.firstName} ${registrationState.basicInfo.lastName}`, subscription_plan: registrationState.subscription.planId, billing_cycle: registrationState.subscription.billingInterval, payment_method_id: confirmedPaymentMethodId || pendingPaymentMethodId, coupon_code: registrationState.subscription.couponCode, // Pass customer_id for reference customer_id: registrationState.paymentSetup?.customerId || '', // Remove trial_period_days - backend will calculate from coupon_code }, }); if (verificationResult.success) { updateRegistrationState({ paymentSetup: { success: true, customerId: verificationResult.payment_customer_id ?? '', subscriptionId: verificationResult.subscription_id ?? '', paymentMethodId: confirmedPaymentMethodId || '', planId: registrationState.subscription.planId, threedsCompleted: true, }, }); // Perform auto-login if tokens are returned const autoLoginSuccess = performAutoLogin(verificationResult); if (autoLoginSuccess) { // Show success message showToast.success(t('auth:registration_complete', 'Registration complete! Redirecting to onboarding...')); // Navigate directly to onboarding navigate('/app/onboarding'); return; } setSuccess(true); setRequires3DS(false); // Create a combined result for the parent component (fallback if no tokens) const paymentCompleteResult = { ...verificationResult, requires_3ds: true, threeds_completed: true, }; onPaymentComplete(paymentCompleteResult); } else { setError(verificationResult.message || t('auth:payment_verification_failed')); } } catch (err) { setError(err instanceof Error ? err.message : t('auth:3ds_verification_failed')); } finally { setLoading(false); } }; const handleRetry = () => { setRequires3DS(false); setClientSecret(null); setSetupIntentId(null); setError(null); }; if (success) { return (

{t('auth:payment_setup_complete')}

{t('auth:payment_setup_complete_description')}

); } if (requires3DS) { return (
{/* Show error state with retry option */} {error ? ( <>

{t('auth:verification_failed', 'Verification Failed')}

{error}

) : ( /* Show loading state - 3DS modal will appear automatically */ <>

{t('auth:verifying_payment', 'Verifying Payment')}

{t('auth:bank_verification_opening', 'Your bank\'s verification window will open automatically...')}

{t('auth:complete_in_popup', 'Please complete the verification in the popup window')}

)}
); } return (

{t('auth:payment.payment_information', 'Payment Information')}

{t('auth:payment.secure_payment', 'Your payment information is protected with end-to-end encryption')}

{/* Payment Summary - Mobile optimized */}

{t('auth:payment.payment_summary_title', 'Payment Summary')}

{/* Selected Plan */}
{t('auth:payment.selected_plan', 'Selected Plan')} {getPlanName(registrationState.subscription.planId)}
{/* Billing Cycle */}
{t('auth:payment.billing_cycle', 'Billing Cycle')} {registrationState.subscription.billingInterval === 'monthly' ? t('auth:payment.monthly', 'Monthly') : t('auth:payment.yearly', 'Yearly')}
{/* Pilot discount information - shown when pilot is active */} {registrationState.subscription.useTrial && (
{t('auth:payment.price', 'Price')} €0/{registrationState.subscription.billingInterval === 'monthly' ? 'mo' : 'yr'}
{/* Small text showing post-trial pricing */}

{t('auth:payment.pilot_future_payment', 'After 3 months, payment will be €{price}/{interval}', { price: getPlanPrice(registrationState.subscription.planId, registrationState.subscription.billingInterval), interval: registrationState.subscription.billingInterval === 'monthly' ? t('auth:payment.per_month', 'mo') : t('auth:payment.per_year', 'yr') })}

{/* Informative text about pilot discount */}

{t('auth:payment.pilot_discount_active', 'Pilot discount active: 3 months free')}

)} {/* Regular price display when not in pilot mode */} {!registrationState.subscription.useTrial && ( <> {/* Price */}
{t('auth:payment.price', 'Price')} {getPlanPrice(registrationState.subscription.planId, registrationState.subscription.billingInterval, registrationState.subscription.couponCode)}/{ registrationState.subscription.billingInterval === 'monthly' ? 'mo' : 'yr' }
{/* Other promotions if not pilot */} {registrationState.subscription.couponCode && (
{t('auth:payment.promotion', 'Promotion')} {t('auth:payment.promotion_applied', 'Promotion applied')}
)} )}
{/* Billing Details - Mobile optimized */}
setBillingDetails({...billingDetails, name: e.target.value})} required disabled={loading} /> setBillingDetails({...billingDetails, email: e.target.value})} required disabled={loading} /> {/* Address - Stack on mobile */} setBillingDetails({...billingDetails, address: {...billingDetails.address, line1: e.target.value}})} required disabled={loading} />
setBillingDetails({...billingDetails, address: {...billingDetails.address, city: e.target.value}})} required disabled={loading} /> setBillingDetails({...billingDetails, address: {...billingDetails.address, postal_code: e.target.value}})} required disabled={loading} />
setBillingDetails({...billingDetails, address: {...billingDetails.address, state: e.target.value}})} required disabled={loading} /> setBillingDetails({...billingDetails, address: {...billingDetails.address, country: e.target.value}})} required disabled={loading} />
{/* Payment Element */}

{t('auth:payment.payment_info_secure', 'Your payment information is secure')}

{/* Error Message */} {error && (
{error}
)} {/* Submit Button */}
{/* Back Button */}
); }; // Add to i18n translations // payment_information: "Payment Information" // payment_information_description: "Please enter your payment details to complete registration" // payment_setup_complete: "Payment Setup Complete" // payment_setup_complete_description: "Your payment method has been successfully set up" // continue_to_account_creation: "Continue to Account Creation" // 3ds_authentication_required: "3D Secure Authentication Required" // 3ds_authentication_description: "Your bank requires additional authentication for this payment" // complete_3ds_authentication: "Complete Authentication" // complete_payment: "Complete Payment" // stripe_not_loaded: "Payment system not loaded. Please refresh the page." // payment_method_creation_failed: "Failed to create payment method" // no_payment_method_created: "No payment method was created" // payment_setup_failed: "Payment setup failed. Please try again." // 3ds_authentication_failed: "3D Secure authentication failed" // 3ds_authentication_not_completed: "3D Secure authentication not completed" // payment_verification_failed: "Payment verification failed" // 3ds_verification_failed: "3D Secure verification failed" // processing: "Processing..." // back: "Back" // cancel: "Cancel" // cardholder_name: "Cardholder Name" // full_name: "Full Name" // email: "Email" // address_line1: "Address" // street_address: "Street Address" // city: "City" // state: "State/Province" // postal_code: "Postal Code" // country: "Country" // payment_details: "Payment Details" // secure_payment: "Your payment information is protected with end-to-end encryption" // payment_info_secure: "Your payment information is secure" // payment_summary_title: "Payment Summary" // selected_plan: "Selected Plan" // billing_cycle: "Billing Cycle" // price: "Price" // promotion: "Promotion" // three_months_free: "3 months free" // plan_starter: "Starter" // plan_professional: "Professional" // plan_enterprise: "Enterprise" // monthly: "Monthly" // yearly: "Yearly"