Add subcription feature
This commit is contained in:
@@ -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')}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -2,11 +2,13 @@ import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Input } from '../../../ui';
|
||||
import { AddressAutocomplete } from '../../../ui/AddressAutocomplete';
|
||||
import { useRegisterBakery, useTenant, useUpdateTenant } from '../../../../api/hooks/tenant';
|
||||
import { BakeryRegistration, TenantUpdate } from '../../../../api/types/tenant';
|
||||
import { useRegisterBakery, useTenant, useUpdateTenant, useRegisterBakeryWithSubscription } from '../../../../api/hooks/tenant';
|
||||
import { BakeryRegistration, TenantUpdate, BakeryRegistrationWithSubscription } from '../../../../api/types/tenant';
|
||||
import { AddressResult } from '../../../../services/api/geocodingApi';
|
||||
import { useWizardContext } from '../context';
|
||||
import { poiContextApi } from '../../../../services/api/poiContextApi';
|
||||
import { useAuthStore } from '../../../../stores/auth.store';
|
||||
import { useUserProgress } from '../../../../api/hooks/onboarding';
|
||||
|
||||
interface RegisterTenantStepProps {
|
||||
onNext: () => void;
|
||||
@@ -38,6 +40,31 @@ export const RegisterTenantStep: React.FC<RegisterTenantStepProps> = ({
|
||||
const wizardContext = useWizardContext();
|
||||
const tenantId = wizardContext.state.tenantId;
|
||||
|
||||
// Get pending subscription ID from auth store (primary source)
|
||||
const { pendingSubscriptionId: authStoreSubscriptionId, setPendingSubscriptionId, user } = useAuthStore(state => ({
|
||||
pendingSubscriptionId: state.pendingSubscriptionId,
|
||||
setPendingSubscriptionId: state.setPendingSubscriptionId,
|
||||
user: state.user
|
||||
}));
|
||||
|
||||
// Fallback: Fetch from onboarding progress API if not in auth store
|
||||
const { data: onboardingProgress } = useUserProgress(user?.id || '');
|
||||
|
||||
// Find the user_registered step in the onboarding progress
|
||||
const userRegisteredStep = onboardingProgress?.steps?.find(step => step.step_name === 'user_registered');
|
||||
const subscriptionIdFromProgress = userRegisteredStep?.data?.subscription_id || null;
|
||||
|
||||
// Determine the subscription ID to use (auth store takes precedence, fallback to onboarding progress)
|
||||
const pendingSubscriptionId = authStoreSubscriptionId || subscriptionIdFromProgress;
|
||||
|
||||
// Sync auth store with onboarding progress if auth store is empty but onboarding has it
|
||||
useEffect(() => {
|
||||
if (!authStoreSubscriptionId && subscriptionIdFromProgress) {
|
||||
console.log('🔄 Syncing subscription ID from onboarding progress to auth store:', subscriptionIdFromProgress);
|
||||
setPendingSubscriptionId(subscriptionIdFromProgress);
|
||||
}
|
||||
}, [authStoreSubscriptionId, subscriptionIdFromProgress, setPendingSubscriptionId]);
|
||||
|
||||
// Check if user is enterprise tier for conditional labels
|
||||
const subscriptionTier = localStorage.getItem('subscription_tier');
|
||||
const isEnterprise = subscriptionTier === 'enterprise';
|
||||
@@ -191,9 +218,30 @@ export const RegisterTenantStep: React.FC<RegisterTenantStepProps> = ({
|
||||
tenant = await updateTenant.mutateAsync({ tenantId, updateData });
|
||||
console.log('✅ Tenant updated successfully:', tenant.id);
|
||||
} else {
|
||||
// Create new tenant
|
||||
tenant = await registerBakery.mutateAsync(formData);
|
||||
console.log('✅ Tenant registered successfully:', tenant.id);
|
||||
// Check if we have a pending subscription to link (from auth store)
|
||||
if (pendingSubscriptionId) {
|
||||
console.log('🔗 Found pending subscription in auth store, linking to new tenant:', {
|
||||
subscriptionId: pendingSubscriptionId
|
||||
});
|
||||
|
||||
// Create tenant with subscription linking
|
||||
const registrationData: BakeryRegistrationWithSubscription = {
|
||||
...formData,
|
||||
subscription_id: pendingSubscriptionId,
|
||||
link_existing_subscription: true
|
||||
};
|
||||
|
||||
tenant = await registerBakeryWithSubscription.mutateAsync(registrationData);
|
||||
console.log('✅ Tenant registered with subscription linking:', tenant.id);
|
||||
|
||||
// Clean up pending subscription ID from store after successful linking
|
||||
setPendingSubscriptionId(null);
|
||||
console.log('🧹 Cleaned up subscription data from auth store');
|
||||
} else {
|
||||
// Create new tenant without subscription linking (fallback)
|
||||
tenant = await registerBakery.mutateAsync(formData);
|
||||
console.log('✅ Tenant registered successfully (no subscription linking):', tenant.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger POI detection in the background (non-blocking)
|
||||
|
||||
@@ -45,12 +45,25 @@ export const PaymentMethodUpdateModal: React.FC<PaymentMethodUpdateModalProps> =
|
||||
if (isOpen) {
|
||||
const loadStripe = async () => {
|
||||
try {
|
||||
// Get Stripe publishable key from runtime config or build-time env
|
||||
const getStripePublishableKey = () => {
|
||||
if (typeof window !== 'undefined' && (window as any).__RUNTIME_CONFIG__?.VITE_STRIPE_PUBLISHABLE_KEY) {
|
||||
return (window as any).__RUNTIME_CONFIG__.VITE_STRIPE_PUBLISHABLE_KEY;
|
||||
}
|
||||
return import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY;
|
||||
};
|
||||
|
||||
const stripeKey = getStripePublishableKey();
|
||||
if (!stripeKey) {
|
||||
throw new Error('Stripe publishable key not configured');
|
||||
}
|
||||
|
||||
// Load Stripe.js from CDN
|
||||
const stripeScript = document.createElement('script');
|
||||
stripeScript.src = 'https://js.stripe.com/v3/';
|
||||
stripeScript.async = true;
|
||||
stripeScript.onload = () => {
|
||||
const stripeInstance = (window as any).Stripe('pk_test_your_publishable_key'); // Replace with actual key
|
||||
const stripeInstance = (window as any).Stripe(stripeKey);
|
||||
setStripe(stripeInstance);
|
||||
const elementsInstance = stripeInstance.elements();
|
||||
setElements(elementsInstance);
|
||||
@@ -61,7 +74,7 @@ export const PaymentMethodUpdateModal: React.FC<PaymentMethodUpdateModalProps> =
|
||||
setError('Failed to load payment processor');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
loadStripe();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
@@ -13,6 +13,7 @@ export const PricingSection: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const [showComparisonModal, setShowComparisonModal] = useState(false);
|
||||
const [plans, setPlans] = useState<Record<SubscriptionTier, PlanMetadata> | null>(null);
|
||||
const [billingCycle, setBillingCycle] = useState<'monthly' | 'yearly'>('monthly');
|
||||
|
||||
useEffect(() => {
|
||||
loadPlans();
|
||||
@@ -28,17 +29,48 @@ export const PricingSection: React.FC = () => {
|
||||
};
|
||||
|
||||
const handlePlanSelect = (tier: string) => {
|
||||
navigate(getRegisterUrl(tier));
|
||||
// Use the updated getRegisterUrl function that supports billing cycle
|
||||
navigate(getRegisterUrl(tier, billingCycle));
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Billing Cycle Toggle */}
|
||||
<div className="flex justify-center mb-8">
|
||||
<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>
|
||||
|
||||
{/* Pricing Cards */}
|
||||
<SubscriptionPricingCards
|
||||
mode="landing"
|
||||
showPilotBanner={true}
|
||||
pilotTrialMonths={3}
|
||||
showComparison={false}
|
||||
billingCycle={billingCycle} // Pass selected billing cycle
|
||||
/>
|
||||
|
||||
{/* Feature Comparison Link */}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
subscriptionService,
|
||||
type PlanMetadata,
|
||||
type SubscriptionTier,
|
||||
type BillingCycle,
|
||||
SUBSCRIPTION_TIERS
|
||||
} from '../../api';
|
||||
import { getRegisterUrl } from '../../utils/navigation';
|
||||
@@ -23,6 +24,8 @@ interface SubscriptionPricingCardsProps {
|
||||
pilotTrialMonths?: number;
|
||||
showComparison?: boolean;
|
||||
className?: string;
|
||||
billingCycle?: BillingCycle;
|
||||
onBillingCycleChange?: (cycle: BillingCycle) => void;
|
||||
}
|
||||
|
||||
export const SubscriptionPricingCards: React.FC<SubscriptionPricingCardsProps> = ({
|
||||
@@ -33,14 +36,19 @@ export const SubscriptionPricingCards: React.FC<SubscriptionPricingCardsProps> =
|
||||
pilotCouponCode,
|
||||
pilotTrialMonths = 3,
|
||||
showComparison = false,
|
||||
className = ''
|
||||
className = '',
|
||||
billingCycle: externalBillingCycle,
|
||||
onBillingCycleChange
|
||||
}) => {
|
||||
const { t } = useTranslation('subscription');
|
||||
const [plans, setPlans] = useState<Record<SubscriptionTier, PlanMetadata> | null>(null);
|
||||
const [billingCycle, setBillingCycle] = useState<BillingCycle>('monthly');
|
||||
const [internalBillingCycle, setInternalBillingCycle] = useState<BillingCycle>('monthly');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Use external billing cycle if provided, otherwise use internal state
|
||||
const billingCycle = externalBillingCycle || internalBillingCycle;
|
||||
|
||||
useEffect(() => {
|
||||
loadPlans();
|
||||
}, []);
|
||||
@@ -145,34 +153,48 @@ export const SubscriptionPricingCards: React.FC<SubscriptionPricingCardsProps> =
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Billing Cycle Toggle */}
|
||||
<div className="flex justify-center mb-8">
|
||||
<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>
|
||||
{/* Billing Cycle Toggle - Only show if not externally controlled */}
|
||||
{!externalBillingCycle && (
|
||||
<div className="flex justify-center mb-8">
|
||||
<div className="inline-flex rounded-lg border-2 border-[var(--border-primary)] p-1 bg-[var(--bg-secondary)]">
|
||||
<button
|
||||
onClick={() => {
|
||||
const newCycle: BillingCycle = 'monthly';
|
||||
setInternalBillingCycle(newCycle);
|
||||
if (onBillingCycleChange) {
|
||||
onBillingCycleChange(newCycle);
|
||||
}
|
||||
}}
|
||||
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={() => {
|
||||
const newCycle: BillingCycle = 'yearly';
|
||||
setInternalBillingCycle(newCycle);
|
||||
if (onBillingCycleChange) {
|
||||
onBillingCycleChange(newCycle);
|
||||
}
|
||||
}}
|
||||
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>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Simplified Plans Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 items-start lg:items-stretch">
|
||||
@@ -186,7 +208,7 @@ export const SubscriptionPricingCards: React.FC<SubscriptionPricingCardsProps> =
|
||||
const CardWrapper = mode === 'landing' ? Link : 'div';
|
||||
const isCurrentPlan = mode === 'settings' && selectedPlan === tier;
|
||||
const cardProps = mode === 'landing'
|
||||
? { to: getRegisterUrl(tier) }
|
||||
? { to: getRegisterUrl(tier, billingCycle) }
|
||||
: mode === 'selection' || (mode === 'settings' && !isCurrentPlan)
|
||||
? { onClick: () => handlePlanAction(tier, plan) }
|
||||
: {};
|
||||
|
||||
Reference in New Issue
Block a user