Update landing page
This commit is contained in:
@@ -1,19 +1,30 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { Button, Input, Card } from '../../ui';
|
||||
import { PasswordCriteria, validatePassword, getPasswordErrors } from '../../ui/PasswordCriteria';
|
||||
import { useAuthActions, useAuthLoading, useAuthError } from '../../../stores/auth.store';
|
||||
import { useToast } from '../../../hooks/ui/useToast';
|
||||
import { SubscriptionSelection } from './SubscriptionSelection';
|
||||
import { SubscriptionPricingCards } from '../../subscription/SubscriptionPricingCards';
|
||||
import PaymentForm from './PaymentForm';
|
||||
import PilotBanner from './PilotBanner';
|
||||
import { loadStripe } from '@stripe/stripe-js';
|
||||
import { Elements } from '@stripe/react-stripe-js';
|
||||
import { CheckCircle } from 'lucide-react';
|
||||
import { CheckCircle, Clock } from 'lucide-react';
|
||||
import { usePilotDetection } from '../../../hooks/usePilotDetection';
|
||||
import { subscriptionService } from '../../../api';
|
||||
|
||||
// Initialize Stripe - In production, use environment variable
|
||||
const stripePromise = loadStripe(import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY || 'pk_test_51234567890123456789012345678901234567890123456789012345678901234567890123456789012345');
|
||||
// Helper to get Stripe key from runtime config or build-time env
|
||||
const getStripeKey = (): string => {
|
||||
// Try runtime config first (Kubernetes/Docker)
|
||||
if (typeof window !== 'undefined' && window.__RUNTIME_CONFIG__?.VITE_STRIPE_PUBLISHABLE_KEY) {
|
||||
return window.__RUNTIME_CONFIG__.VITE_STRIPE_PUBLISHABLE_KEY;
|
||||
}
|
||||
// Fallback to build-time env (local development)
|
||||
return import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY || 'pk_test_51234567890123456789012345678901234567890123456789012345678901234567890123456789012345';
|
||||
};
|
||||
|
||||
// Initialize Stripe with runtime or build-time key
|
||||
const stripePromise = loadStripe(getStripeKey());
|
||||
|
||||
interface RegisterFormProps {
|
||||
onSuccess?: () => void;
|
||||
@@ -62,11 +73,17 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
|
||||
// Detect pilot program participation
|
||||
const { isPilot, couponCode, trialMonths } = usePilotDetection();
|
||||
|
||||
// Read URL parameters for plan persistence
|
||||
const [searchParams] = useSearchParams();
|
||||
const preSelectedPlan = searchParams.get('plan') as string | null; // starter | professional | enterprise
|
||||
const urlPilotParam = searchParams.get('pilot') === 'true';
|
||||
|
||||
// Multi-step form state
|
||||
const [currentStep, setCurrentStep] = useState<RegistrationStep>('basic_info');
|
||||
const [selectedPlan, setSelectedPlan] = useState<string>('starter');
|
||||
const [useTrial, setUseTrial] = useState<boolean>(isPilot); // Auto-enable trial for pilot customers
|
||||
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);
|
||||
|
||||
// Helper function to determine password match status
|
||||
const getPasswordMatchStatus = () => {
|
||||
@@ -77,6 +94,58 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
|
||||
|
||||
const passwordMatchStatus = getPasswordMatchStatus();
|
||||
|
||||
// Load plan metadata when plan changes
|
||||
useEffect(() => {
|
||||
const loadPlanMetadata = async () => {
|
||||
try {
|
||||
const plans = await subscriptionService.fetchAvailablePlans();
|
||||
const planData = plans.plans[selectedPlan as keyof typeof plans.plans];
|
||||
setSelectedPlanMetadata(planData);
|
||||
} catch (err) {
|
||||
console.error('Failed to load plan metadata:', err);
|
||||
}
|
||||
};
|
||||
loadPlanMetadata();
|
||||
}, [selectedPlan]);
|
||||
|
||||
// Save form progress to localStorage
|
||||
useEffect(() => {
|
||||
const formState = {
|
||||
formData,
|
||||
selectedPlan,
|
||||
useTrial,
|
||||
currentStep,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
localStorage.setItem('registration_progress', JSON.stringify(formState));
|
||||
}, [formData, selectedPlan, useTrial, currentStep]);
|
||||
|
||||
// Recover form state on mount (if less than 24 hours old)
|
||||
useEffect(() => {
|
||||
// Only recover if not coming from a direct link with plan pre-selected
|
||||
if (preSelectedPlan) return;
|
||||
|
||||
const saved = localStorage.getItem('registration_progress');
|
||||
if (saved) {
|
||||
try {
|
||||
const state = JSON.parse(saved);
|
||||
const age = Date.now() - state.timestamp;
|
||||
const maxAge = 24 * 60 * 60 * 1000; // 24 hours
|
||||
|
||||
if (age < maxAge) {
|
||||
// Optionally restore state (for now, just log it exists)
|
||||
console.log('Found saved registration progress');
|
||||
} else {
|
||||
// Clear old state
|
||||
localStorage.removeItem('registration_progress');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to parse saved registration state:', err);
|
||||
localStorage.removeItem('registration_progress');
|
||||
}
|
||||
}
|
||||
}, [preSelectedPlan]);
|
||||
|
||||
const validateForm = (): boolean => {
|
||||
const newErrors: Partial<SimpleUserRegistration> = {};
|
||||
|
||||
@@ -120,17 +189,25 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
|
||||
if (!validateForm()) {
|
||||
return;
|
||||
}
|
||||
setCurrentStep('subscription');
|
||||
// OPTION A: Skip subscription step if plan was pre-selected from pricing page
|
||||
if (preSelectedPlan) {
|
||||
setCurrentStep('payment');
|
||||
} else {
|
||||
setCurrentStep('subscription');
|
||||
}
|
||||
} else if (currentStep === 'subscription') {
|
||||
setCurrentStep('payment');
|
||||
}
|
||||
};
|
||||
|
||||
const handlePreviousStep = () => {
|
||||
if (currentStep === 'subscription') {
|
||||
if (currentStep === 'payment' && preSelectedPlan) {
|
||||
// Go back to basic_info (skip subscription step)
|
||||
setCurrentStep('basic_info');
|
||||
} else if (currentStep === 'payment') {
|
||||
setCurrentStep('subscription');
|
||||
} else if (currentStep === 'subscription') {
|
||||
setCurrentStep('basic_info');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -190,11 +267,17 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
|
||||
|
||||
// Render step indicator
|
||||
const renderStepIndicator = () => {
|
||||
const steps = [
|
||||
{ key: 'basic_info', label: 'Información', number: 1 },
|
||||
{ key: 'subscription', label: 'Plan', number: 2 },
|
||||
{ key: 'payment', label: 'Pago', number: 3 }
|
||||
];
|
||||
// Show 2 steps if plan is pre-selected, 3 steps otherwise
|
||||
const steps = preSelectedPlan
|
||||
? [
|
||||
{ key: 'basic_info', label: 'Información', number: 1, time: '2 min' },
|
||||
{ key: 'payment', label: 'Pago', number: 2, time: '2 min' }
|
||||
]
|
||||
: [
|
||||
{ key: 'basic_info', label: 'Información', number: 1, time: '2 min' },
|
||||
{ key: 'subscription', label: 'Plan', number: 2, time: '1 min' },
|
||||
{ key: 'payment', label: 'Pago', number: 3, time: '2 min' }
|
||||
];
|
||||
|
||||
const getStepIndex = (step: RegistrationStep) => {
|
||||
return steps.findIndex(s => s.key === step);
|
||||
@@ -221,11 +304,19 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
|
||||
step.number
|
||||
)}
|
||||
</div>
|
||||
<span className={`mt-1 sm:mt-2 text-[10px] sm:text-xs font-medium hidden sm:block ${
|
||||
index <= currentIndex ? 'text-text-primary' : 'text-text-secondary'
|
||||
}`}>
|
||||
{step.label}
|
||||
</span>
|
||||
<div className="flex flex-col items-center mt-1 sm:mt-2">
|
||||
<span className={`text-[10px] sm:text-xs font-medium block ${
|
||||
index <= currentIndex ? 'text-text-primary' : 'text-text-secondary'
|
||||
}`}>
|
||||
{step.label}
|
||||
</span>
|
||||
<span className={`text-[8px] sm:text-[10px] flex items-center gap-0.5 ${
|
||||
index === currentIndex ? 'text-text-tertiary' : 'hidden sm:flex text-text-tertiary'
|
||||
}`}>
|
||||
<Clock className="w-2 h-2 sm:w-3 sm:h-3" />
|
||||
{step.time}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{index < steps.length - 1 && (
|
||||
<div className={`h-0.5 w-8 sm:w-16 mx-2 sm:mx-4 transition-all duration-200 ${
|
||||
@@ -497,16 +588,13 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{isPilot && couponCode && (
|
||||
<PilotBanner couponCode={couponCode} trialMonths={trialMonths} />
|
||||
)}
|
||||
|
||||
<SubscriptionSelection
|
||||
<SubscriptionPricingCards
|
||||
mode="selection"
|
||||
selectedPlan={selectedPlan}
|
||||
onPlanSelect={setSelectedPlan}
|
||||
showTrialOption={!isPilot}
|
||||
onTrialSelect={setUseTrial}
|
||||
trialSelected={useTrial}
|
||||
showPilotBanner={isPilot}
|
||||
pilotCouponCode={couponCode}
|
||||
pilotTrialMonths={trialMonths}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col sm:flex-row justify-between gap-3 sm:gap-4 pt-2 border-t border-border-primary">
|
||||
@@ -544,8 +632,52 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Plan Summary Card */}
|
||||
{selectedPlanMetadata && (
|
||||
<Card className="p-6 bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-blue-900/20 dark:to-indigo-900/20 border-2 border-blue-200 dark:border-blue-800">
|
||||
<h3 className="text-lg font-bold text-text-primary mb-4 flex items-center gap-2">
|
||||
<CheckCircle className="w-5 h-5 text-color-primary" />
|
||||
Resumen de tu Plan
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-text-secondary">Plan seleccionado:</span>
|
||||
<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">Precio mensual:</span>
|
||||
<span className="font-semibold text-text-primary">
|
||||
{subscriptionService.formatPrice(selectedPlanMetadata.monthly_price)}/mes
|
||||
</span>
|
||||
</div>
|
||||
{useTrial && (
|
||||
<div className="flex justify-between items-center pt-3 border-t border-blue-200 dark:border-blue-800">
|
||||
<span className="text-green-700 dark:text-green-400 font-medium">Período de prueba:</span>
|
||||
<span className="font-bold text-green-700 dark:text-green-400">
|
||||
{isPilot ? `${trialMonths} meses GRATIS` : '14 días gratis'}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="pt-3 border-t border-blue-200 dark:border-blue-800">
|
||||
<div className="flex justify-between items-center text-sm">
|
||||
<span className="text-text-tertiary">Total hoy:</span>
|
||||
<span className="font-bold text-xl text-color-success">€0.00</span>
|
||||
</div>
|
||||
<p className="text-xs text-text-tertiary mt-2 text-center">
|
||||
{useTrial
|
||||
? `Se te cobrará ${subscriptionService.formatPrice(selectedPlanMetadata.monthly_price)} después del período de prueba`
|
||||
: 'Tarjeta requerida para validación'
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Elements stripe={stripePromise}>
|
||||
<PaymentForm
|
||||
userName={formData.full_name}
|
||||
userEmail={formData.email}
|
||||
onPaymentSuccess={handlePaymentSuccess}
|
||||
onPaymentError={handlePaymentError}
|
||||
bypassPayment={bypassPayment}
|
||||
|
||||
Reference in New Issue
Block a user