Add subcription feature

This commit is contained in:
Urtzi Alfaro
2026-01-13 22:22:38 +01:00
parent b931a5c45e
commit 6ddf608d37
61 changed files with 7915 additions and 1238 deletions

View File

@@ -12,6 +12,7 @@ interface PaymentFormProps {
onBypassToggle?: () => void;
userName?: string;
userEmail?: string;
isProcessingRegistration?: boolean; // External loading state from parent (registration in progress)
}
const PaymentForm: React.FC<PaymentFormProps> = ({
@@ -21,7 +22,8 @@ const PaymentForm: React.FC<PaymentFormProps> = ({
bypassPayment = false,
onBypassToggle,
userName = '',
userEmail = ''
userEmail = '',
isProcessingRegistration = false
}) => {
const { t } = useTranslation();
const stripe = useStripe();
@@ -57,12 +59,17 @@ const PaymentForm: React.FC<PaymentFormProps> = ({
return;
}
if (bypassPayment) {
// In development mode, bypass payment processing
if (bypassPayment && import.meta.env.MODE === 'development') {
// DEVELOPMENT ONLY: Bypass payment processing
onPaymentSuccess();
return;
}
if (bypassPayment && import.meta.env.MODE === 'production') {
onPaymentError('Payment bypass is not allowed in production');
return;
}
setLoading(true);
setError(null);
@@ -101,13 +108,16 @@ const PaymentForm: React.FC<PaymentFormProps> = ({
console.log('Payment method created:', paymentMethod);
// Pass the payment method ID to the parent component for server-side processing
// Keep loading state active - parent will handle the full registration flow
onPaymentSuccess(paymentMethod?.id);
// DON'T set loading to false here - let parent component control the loading state
// The registration with backend will happen next, and we want to keep button disabled
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Error desconocido al procesar el pago';
setError(errorMessage);
onPaymentError(errorMessage);
} finally {
setLoading(false);
setLoading(false); // Only reset loading on error
}
};
@@ -128,24 +138,26 @@ const PaymentForm: React.FC<PaymentFormProps> = ({
</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>
{/* Development mode toggle - only shown in development */}
{import.meta.env.MODE === 'development' && (
<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>
<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}>
@@ -259,9 +271,9 @@ const PaymentForm: React.FC<PaymentFormProps> = ({
type="submit"
variant="primary"
size="lg"
isLoading={loading}
loadingText="Procesando pago..."
disabled={!stripe || loading || (!cardComplete && !bypassPayment)}
isLoading={loading || isProcessingRegistration}
loadingText={isProcessingRegistration ? "Creando tu cuenta..." : "Procesando pago..."}
disabled={!stripe || loading || isProcessingRegistration || (!cardComplete && !bypassPayment)}
className="w-full"
>
{t('auth:payment.process_payment', 'Procesar Pago')}

View File

@@ -41,6 +41,11 @@ interface SimpleUserRegistration {
acceptTerms: boolean;
marketingConsent: boolean;
analyticsConsent: boolean;
// NEW: Billing address fields for subscription creation
address?: string;
postal_code?: string;
city?: string;
country?: string;
}
// Define the steps for the registration process
@@ -59,14 +64,19 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
confirmPassword: '',
acceptTerms: false,
marketingConsent: false,
analyticsConsent: false
analyticsConsent: false,
// NEW: Initialize billing address fields
address: '',
postal_code: '',
city: '',
country: ''
});
const [errors, setErrors] = useState<Partial<SimpleUserRegistration>>({});
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const { register } = useAuthActions();
const { register, registerWithSubscription } = useAuthActions();
const isLoading = useAuthLoading();
const error = useAuthError();
@@ -74,17 +84,27 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
// Detect pilot program participation
const { isPilot, couponCode, trialMonths } = usePilotDetection();
// Read URL parameters for plan persistence
// Read URL parameters for plan and billing cycle persistence
const [searchParams] = useSearchParams();
const preSelectedPlan = searchParams.get('plan') as string | null; // starter | professional | enterprise
// Validate plan parameter
const VALID_PLANS = ['starter', 'professional', 'enterprise'];
const planParam = searchParams.get('plan');
const preSelectedPlan = (planParam && VALID_PLANS.includes(planParam)) ? planParam : null;
const urlPilotParam = searchParams.get('pilot') === 'true';
// Validate billing cycle parameter
const billingParam = searchParams.get('billing_cycle');
const urlBillingCycle = (billingParam === 'monthly' || billingParam === 'yearly') ? billingParam : null;
// Multi-step form state
const [currentStep, setCurrentStep] = useState<RegistrationStep>('basic_info');
const [selectedPlan, setSelectedPlan] = useState<string>(preSelectedPlan || 'starter');
const [useTrial, setUseTrial] = useState<boolean>(isPilot || urlPilotParam); // Auto-enable trial for pilot customers
const [bypassPayment, setBypassPayment] = useState<boolean>(false);
const [selectedPlanMetadata, setSelectedPlanMetadata] = useState<any>(null);
const [billingCycle, setBillingCycle] = useState<'monthly' | 'yearly'>(urlBillingCycle || 'monthly'); // Track billing cycle, default to URL value if present
// Helper function to determine password match status
const getPasswordMatchStatus = () => {
@@ -205,7 +225,7 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
password: formData.password,
tenant_name: 'Default Bakery', // Default value since we're not collecting it
subscription_plan: selectedPlan,
use_trial: useTrial,
billing_cycle: billingCycle, // Add billing cycle selection
payment_method_id: paymentMethodId,
// Include coupon code if pilot customer
coupon_code: isPilot ? couponCode : undefined,
@@ -214,14 +234,15 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
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,
};
await register(registrationData);
// CRITICAL: Store subscription_tier in localStorage for onboarding flow
// This is required for conditional step rendering in UnifiedOnboardingWizard
console.log('💾 Storing subscription_tier in localStorage:', selectedPlan);
localStorage.setItem('subscription_tier', selectedPlan);
// Use the new registration endpoint with subscription creation
await registerWithSubscription(registrationData);
const successMessage = isPilot
? '¡Bienvenido al programa piloto! Tu cuenta ha sido creada con 3 meses gratis.'
@@ -232,14 +253,14 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
});
onSuccess?.();
} catch (err) {
showToast.error(error || t('auth:register.register_button', 'No se pudo crear la cuenta. Verifica que el email no esté en uso.'), {
showToast.error(error || t('auth:register.register_button', 'No se pudo crear la account. Verifica que el email no esté en uso.'), {
title: t('auth:alerts.error_create', 'Error al crear la cuenta')
});
}
};
const handlePaymentSuccess = () => {
handleRegistrationSubmit(); // In a real app, you would pass the payment method ID
const handlePaymentSuccess = (paymentMethodId?: string) => {
handleRegistrationSubmit(paymentMethodId);
};
const handlePaymentError = (errorMessage: string) => {
@@ -617,6 +638,35 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
</p>
</div>
{/* Billing Cycle Toggle */}
<div className="flex justify-center">
<div className="inline-flex rounded-lg border-2 border-[var(--border-primary)] p-1 bg-[var(--bg-secondary)]">
<button
onClick={() => setBillingCycle('monthly')}
className={`px-6 py-2 rounded-md text-sm font-semibold transition-all ${
billingCycle === 'monthly'
? 'bg-[var(--color-primary)] text-white shadow-md'
: 'text-[var(--text-secondary)] hover:bg-[var(--bg-primary)] hover:text-[var(--text-primary)]'
}`}
>
{t('billing.monthly', 'Mensual')}
</button>
<button
onClick={() => setBillingCycle('yearly')}
className={`px-6 py-2 rounded-md text-sm font-semibold transition-all flex items-center gap-2 ${
billingCycle === 'yearly'
? 'bg-[var(--color-primary)] text-white shadow-md'
: 'text-[var(--text-secondary)] hover:bg-[var(--bg-primary)] hover:text-[var(--text-primary)]'
}`}
>
{t('billing.yearly', 'Anual')}
<span className="text-xs font-bold text-green-600 dark:text-green-400">
{t('billing.save_percent', 'Ahorra 17%')}
</span>
</button>
</div>
</div>
<SubscriptionPricingCards
mode="selection"
selectedPlan={selectedPlan}
@@ -624,6 +674,7 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
showPilotBanner={isPilot}
pilotCouponCode={couponCode}
pilotTrialMonths={trialMonths}
billingCycle={billingCycle} // Pass the selected billing cycle
/>
<div className="flex flex-col sm:flex-row justify-between gap-3 sm:gap-4 pt-2 border-t border-border-primary">
@@ -674,9 +725,25 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
<span className="font-bold text-color-primary text-lg">{selectedPlanMetadata.name}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-text-secondary">{t('auth:payment.monthly_price', 'Precio mensual:')}</span>
<span className="text-text-secondary">{t('auth:payment.billing_cycle', 'Ciclo de facturación:')}</span>
<span className="font-semibold text-text-primary capitalize">
{billingCycle === 'monthly'
? t('billing.monthly', 'Mensual')
: t('billing.yearly', 'Anual')}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-text-secondary">
{billingCycle === 'monthly'
? t('auth:payment.monthly_price', 'Precio mensual:')
: t('auth:payment.yearly_price', 'Precio anual:')}
</span>
<span className="font-semibold text-text-primary">
{subscriptionService.formatPrice(selectedPlanMetadata.monthly_price)}/mes
{subscriptionService.formatPrice(
billingCycle === 'monthly'
? selectedPlanMetadata.monthly_price
: selectedPlanMetadata.yearly_price
)}{billingCycle === 'monthly' ? '/mes' : '/año'}
</span>
</div>
{useTrial && (
@@ -694,7 +761,7 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
</div>
<p className="text-xs text-text-tertiary mt-2 text-center">
{useTrial
? t('auth:payment.billing_message', {price: subscriptionService.formatPrice(selectedPlanMetadata.monthly_price)})
? t('auth:payment.billing_message', {price: subscriptionService.formatPrice(billingCycle === 'monthly' ? selectedPlanMetadata.monthly_price : selectedPlanMetadata.yearly_price)})
: t('auth:payment.payment_required')
}
</p>
@@ -725,6 +792,7 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
onPaymentError={handlePaymentError}
bypassPayment={bypassPayment}
onBypassToggle={() => setBypassPayment(!bypassPayment)}
isProcessingRegistration={isLoading}
/>
</Elements>