Files
bakery-ia/frontend/src/components/domain/auth/PaymentStep.tsx
2026-01-15 22:06:36 +01:00

802 lines
31 KiB
TypeScript

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<PaymentStepProps> = ({
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<string | null>(null);
const [success, setSuccess] = useState(false);
const [requires3DS, setRequires3DS] = useState(false);
const [clientSecret, setClientSecret] = useState<string | null>(null);
const [setupIntentId, setSetupIntentId] = useState<string | null>(null);
// Store payment method ID separately for 3DS flow
const [pendingPaymentMethodId, setPendingPaymentMethodId] = useState<string | null>(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 (
<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 mx-auto">
<CheckCircle className="w-8 h-8 text-green-600" />
</div>
<h3 className="text-xl font-semibold mb-2">
{t('auth:payment_setup_complete')}
</h3>
<p className="text-gray-600 mb-6">
{t('auth:payment_setup_complete_description')}
</p>
<div className="mt-6">
<Button
onClick={onNext}
disabled={loading}
className="bg-primary hover:bg-primary/90"
>
{t('auth:continue_to_account_creation')}
</Button>
</div>
</div>
);
}
if (requires3DS) {
return (
<Card className="max-w-md mx-auto">
<div className="p-6">
{/* Show error state with retry option */}
{error ? (
<>
<div className="text-center mb-6">
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-red-100 mb-4 mx-auto">
<AlertCircle className="w-8 h-8 text-red-600" />
</div>
<h3 className="text-xl font-bold text-text-primary mb-2">
{t('auth:verification_failed', 'Verification Failed')}
</h3>
<p className="text-text-secondary text-sm mb-4">
{error}
</p>
</div>
<div className="flex gap-2">
<Button
onClick={() => {
setThreeDSTriggered(false);
setError(null);
handle3DSAuthentication();
}}
disabled={loading}
className="bg-primary hover:bg-primary/90 flex-1"
>
{t('auth:try_again', 'Try Again')}
</Button>
<Button
onClick={handleRetry}
disabled={loading}
variant="outline"
className="flex-1"
>
{t('auth:use_different_card', 'Use Different Card')}
</Button>
</div>
</>
) : (
/* Show loading state - 3DS modal will appear automatically */
<>
<div className="text-center">
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-primary/10 mb-4 mx-auto">
<Loader2 className="w-8 h-8 text-primary animate-spin" />
</div>
<h3 className="text-xl font-bold text-text-primary mb-2">
{t('auth:verifying_payment', 'Verifying Payment')}
</h3>
<p className="text-text-secondary text-sm">
{t('auth:bank_verification_opening', 'Your bank\'s verification window will open automatically...')}
</p>
<p className="text-text-tertiary text-xs mt-4">
{t('auth:complete_in_popup', 'Please complete the verification in the popup window')}
</p>
</div>
</>
)}
</div>
</Card>
);
}
return (
<Card className="w-full max-w-lg mx-auto">
<div className="p-4 sm:p-6">
<div className="text-center mb-4 sm:mb-6">
<h3 className="text-lg sm: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_information', 'Payment Information')}
</h3>
<p className="text-text-secondary text-xs sm:text-sm">
{t('auth:payment.secure_payment', 'Your payment information is protected with end-to-end encryption')}
</p>
</div>
{/* Payment Summary - Mobile optimized */}
<div className="mb-4 sm:mb-6 p-3 sm:p-4 bg-[var(--bg-secondary)] rounded-lg border border-[var(--border-primary)]">
<h4 className="font-semibold text-[var(--text-primary)] mb-2 sm:mb-3 text-sm sm:text-base">
{t('auth:payment.payment_summary_title', 'Payment Summary')}
</h4>
<div className="space-y-2 sm:space-y-3 text-sm">
{/* Selected Plan */}
<div className="flex justify-between items-center">
<span className="text-[var(--text-secondary)]">
{t('auth:payment.selected_plan', 'Selected Plan')}
</span>
<span className="font-medium text-[var(--color-primary)]">
{getPlanName(registrationState.subscription.planId)}
</span>
</div>
{/* Billing Cycle */}
<div className="flex justify-between items-center">
<span className="text-[var(--text-secondary)]">
{t('auth:payment.billing_cycle', 'Billing Cycle')}
</span>
<span className="font-medium text-[var(--text-primary)]">
{registrationState.subscription.billingInterval === 'monthly'
? t('auth:payment.monthly', 'Monthly')
: t('auth:payment.yearly', 'Yearly')}
</span>
</div>
{/* Pilot discount information - shown when pilot is active */}
{registrationState.subscription.useTrial && (
<div className="pt-2 border-t border-[var(--border-primary)]">
<div className="flex justify-between items-center">
<span className="text-[var(--text-secondary)]">
{t('auth:payment.price', 'Price')}
</span>
<span className="font-bold text-[var(--color-primary)]">
0/{registrationState.subscription.billingInterval === 'monthly' ? 'mo' : 'yr'}
</span>
</div>
{/* Small text showing post-trial pricing */}
<p className="text-xs text-[var(--text-tertiary)] mt-1">
{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')
})}
</p>
{/* Informative text about pilot discount */}
<p className="text-xs text-blue-500 mt-1 flex items-center gap-1">
<span className="w-2 h-2 bg-blue-500 rounded-full"></span>
{t('auth:payment.pilot_discount_active', 'Pilot discount active: 3 months free')}
</p>
</div>
)}
{/* Regular price display when not in pilot mode */}
{!registrationState.subscription.useTrial && (
<>
{/* Price */}
<div className="flex justify-between items-center">
<span className="text-[var(--text-secondary)]">
{t('auth:payment.price', 'Price')}
</span>
<span className="font-bold text-[var(--text-primary)]">
{getPlanPrice(registrationState.subscription.planId, registrationState.subscription.billingInterval, registrationState.subscription.couponCode)}/{
registrationState.subscription.billingInterval === 'monthly' ? 'mo' : 'yr'
}
</span>
</div>
{/* Other promotions if not pilot */}
{registrationState.subscription.couponCode && (
<div className="pt-2 border-t border-[var(--border-primary)]">
<div className="flex justify-between items-center">
<span className="text-[var(--text-secondary)]">
{t('auth:payment.promotion', 'Promotion')}
</span>
<span className="font-medium text-green-500 flex items-center gap-1">
<span className="w-2 h-2 bg-green-500 rounded-full"></span>
{t('auth:payment.promotion_applied', 'Promotion applied')}
</span>
</div>
</div>
)}
</>
)}
</div>
</div>
<form onSubmit={handleSubmit}>
{/* Billing Details - Mobile optimized */}
<div className="space-y-3 sm:space-y-4 mb-4 sm:mb-6">
<Input
label={t('auth:payment.cardholder_name', 'Cardholder Name')}
placeholder={t('auth:payment.full_name', 'Full Name')}
value={billingDetails.name}
onChange={(e) => setBillingDetails({...billingDetails, name: e.target.value})}
required
disabled={loading}
/>
<Input
type="email"
label={t('auth:payment.email', 'Email')}
placeholder="tu.email@ejemplo.com"
value={billingDetails.email}
onChange={(e) => setBillingDetails({...billingDetails, email: e.target.value})}
required
disabled={loading}
/>
{/* Address - Stack on mobile */}
<Input
label={t('auth:payment.address_line1', 'Address')}
placeholder={t('auth:payment.street_address', 'Street Address')}
value={billingDetails.address.line1}
onChange={(e) => setBillingDetails({...billingDetails, address: {...billingDetails.address, line1: e.target.value}})}
required
disabled={loading}
/>
<div className="grid grid-cols-2 gap-3">
<Input
label={t('auth:payment.city', 'City')}
placeholder={t('auth:payment.city', 'City')}
value={billingDetails.address.city}
onChange={(e) => setBillingDetails({...billingDetails, address: {...billingDetails.address, city: e.target.value}})}
required
disabled={loading}
/>
<Input
label={t('auth:payment.postal_code', 'Postal Code')}
placeholder={t('auth:payment.postal_code', 'Postal Code')}
value={billingDetails.address.postal_code}
onChange={(e) => setBillingDetails({...billingDetails, address: {...billingDetails.address, postal_code: e.target.value}})}
required
disabled={loading}
/>
</div>
<div className="grid grid-cols-2 gap-3">
<Input
label={t('auth:payment.state', 'State/Province')}
placeholder={t('auth:payment.state', 'State/Province')}
value={billingDetails.address.state}
onChange={(e) => setBillingDetails({...billingDetails, address: {...billingDetails.address, state: e.target.value}})}
required
disabled={loading}
/>
<Input
label={t('auth:payment.country', 'Country')}
placeholder={t('auth:payment.country', 'Country')}
value={billingDetails.address.country}
onChange={(e) => setBillingDetails({...billingDetails, address: {...billingDetails.address, country: e.target.value}})}
required
disabled={loading}
/>
</div>
</div>
{/* Payment Element */}
<div className="mb-4 sm:mb-6">
<label className="block text-sm font-medium text-text-primary mb-2">
{t('auth:payment.payment_details', 'Payment Details')}
</label>
<div className="border border-gray-300 dark:border-gray-600 rounded-lg p-3 bg-white dark:bg-gray-800">
<PaymentElement
options={{
layout: 'tabs',
fields: {
billingDetails: 'auto',
},
wallets: {
applePay: 'auto',
googlePay: 'auto',
},
}}
/>
</div>
<p className="text-xs text-text-tertiary mt-2 flex items-center gap-1">
<Lock className="w-3 h-3" />
{t('auth:payment.payment_info_secure', 'Your payment information is secure')}
</p>
</div>
{/* Error Message */}
{error && (
<div className="mb-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 text-red-600 dark:text-red-400 px-3 py-2 sm:px-4 sm:py-3 rounded-lg text-sm flex items-start gap-2" role="alert">
<AlertCircle className="w-4 h-4 sm:w-5 sm:h-5 mt-0.5 flex-shrink-0" />
<span>{error}</span>
</div>
)}
{/* Submit Button */}
<Button
type="submit"
variant="primary"
size="lg"
isLoading={loading}
loadingText={t('auth:payment.processing', 'Processing...')}
disabled={!stripe || loading}
className="w-full"
>
{t('auth:payment.complete_payment', 'Complete Payment')}
</Button>
</form>
{/* Back Button */}
<div className="mt-3 sm:mt-4">
<Button
type="button"
onClick={onBack}
disabled={loading}
variant="outline"
className="w-full sm:w-auto"
>
{t('auth:payment.back', 'Back')}
</Button>
</div>
</div>
</Card>
);
};
// 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"