Improve the register page
This commit is contained in:
@@ -1,12 +1,48 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Eye, EyeOff, Loader2, Check } from 'lucide-react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Eye, EyeOff, Loader2, Check, CreditCard, Shield, ArrowRight } from 'lucide-react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { loadStripe } from '@stripe/stripe-js';
|
||||
import {
|
||||
Elements,
|
||||
CardElement,
|
||||
useStripe,
|
||||
useElements
|
||||
} from '@stripe/react-stripe-js';
|
||||
|
||||
import {
|
||||
useAuth,
|
||||
RegisterRequest
|
||||
} from '../../api';
|
||||
|
||||
// Development flags
|
||||
const isDevelopment = import.meta.env.DEV;
|
||||
const bypassPayment = import.meta.env.VITE_BYPASS_PAYMENT === 'true';
|
||||
|
||||
// Initialize Stripe with Spanish market configuration (only if not bypassing)
|
||||
const stripePromise = !bypassPayment ? loadStripe(import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY || '', {
|
||||
locale: 'es'
|
||||
}) : null;
|
||||
|
||||
// Stripe card element options for Spanish market
|
||||
const cardElementOptions = {
|
||||
style: {
|
||||
base: {
|
||||
fontSize: '16px',
|
||||
color: '#374151',
|
||||
'::placeholder': {
|
||||
color: '#9CA3AF',
|
||||
},
|
||||
},
|
||||
invalid: {
|
||||
color: '#EF4444',
|
||||
},
|
||||
},
|
||||
hidePostalCode: false, // Keep postal code for better fraud protection
|
||||
};
|
||||
|
||||
// Subscription pricing (monthly)
|
||||
const SUBSCRIPTION_PRICE_EUR = 29.99;
|
||||
|
||||
interface RegisterPageProps {
|
||||
onLogin: (user: any, token: string) => void;
|
||||
onNavigateToLogin: () => void;
|
||||
@@ -15,27 +51,201 @@ interface RegisterPageProps {
|
||||
interface RegisterForm {
|
||||
fullName: string;
|
||||
email: string;
|
||||
confirmEmail: string;
|
||||
password: string;
|
||||
confirmPassword: string;
|
||||
acceptTerms: boolean;
|
||||
paymentCompleted: boolean;
|
||||
}
|
||||
|
||||
const RegisterPage: React.FC<RegisterPageProps> = ({ onLogin, onNavigateToLogin }) => {
|
||||
const { register, isLoading, error } = useAuth();
|
||||
const { register, isLoading } = useAuth();
|
||||
|
||||
const [formData, setFormData] = useState<RegisterForm>({
|
||||
fullName: '',
|
||||
email: '',
|
||||
confirmEmail: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
acceptTerms: false
|
||||
acceptTerms: false,
|
||||
paymentCompleted: false
|
||||
});
|
||||
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
||||
const [errors, setErrors] = useState<Partial<RegisterForm>>({});
|
||||
const [passwordStrength, setPasswordStrength] = useState<{
|
||||
score: number;
|
||||
checks: { [key: string]: boolean };
|
||||
message: string;
|
||||
}>({ score: 0, checks: {}, message: '' });
|
||||
const [paymentStep, setPaymentStep] = useState<'form' | 'payment' | 'processing' | 'completed'>('form');
|
||||
const [paymentLoading, setPaymentLoading] = useState(false);
|
||||
|
||||
const validateForm = (): boolean => {
|
||||
// Update password strength in real-time
|
||||
useEffect(() => {
|
||||
if (formData.password) {
|
||||
const validation = validatePassword(formData.password);
|
||||
setPasswordStrength(validation);
|
||||
}
|
||||
}, [formData.password]);
|
||||
|
||||
// Payment processing component
|
||||
const PaymentForm: React.FC<{ onPaymentSuccess: () => void }> = ({ onPaymentSuccess }) => {
|
||||
const stripe = useStripe();
|
||||
const elements = useElements();
|
||||
|
||||
const handlePayment = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!stripe || !elements) {
|
||||
toast.error('Stripe no está cargado correctamente');
|
||||
return;
|
||||
}
|
||||
|
||||
const card = elements.getElement(CardElement);
|
||||
if (!card) {
|
||||
toast.error('Elemento de tarjeta no encontrado');
|
||||
return;
|
||||
}
|
||||
|
||||
setPaymentLoading(true);
|
||||
|
||||
try {
|
||||
// Create payment method
|
||||
const { error } = await stripe.createPaymentMethod({
|
||||
type: 'card',
|
||||
card,
|
||||
billing_details: {
|
||||
name: formData.fullName,
|
||||
email: formData.email,
|
||||
},
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw new Error(error.message);
|
||||
}
|
||||
|
||||
// Here you would typically create the subscription via your backend
|
||||
// For now, we'll simulate a successful payment
|
||||
toast.success('¡Pago procesado correctamente!');
|
||||
|
||||
setFormData(prev => ({ ...prev, paymentCompleted: true }));
|
||||
setPaymentStep('completed');
|
||||
onPaymentSuccess();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Payment error:', error);
|
||||
toast.error(error instanceof Error ? error.message : 'Error procesando el pago');
|
||||
} finally {
|
||||
setPaymentLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handlePayment} className="space-y-6">
|
||||
<div className="bg-primary-50 border border-primary-200 rounded-xl p-6">
|
||||
<div className="flex items-center space-x-3 mb-4">
|
||||
<div className="w-10 h-10 bg-primary-500 rounded-full flex items-center justify-center">
|
||||
<CreditCard className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900">Suscripción PanIA Pro</h3>
|
||||
<p className="text-sm text-gray-600">Facturación mensual</p>
|
||||
</div>
|
||||
<div className="ml-auto text-right">
|
||||
<div className="text-2xl font-bold text-primary-600">€{SUBSCRIPTION_PRICE_EUR}</div>
|
||||
<div className="text-sm text-gray-500">/mes</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 text-sm text-gray-600">
|
||||
<div className="flex items-center">
|
||||
<Check className="w-4 h-4 text-green-500 mr-2" />
|
||||
Predicciones de demanda ilimitadas
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Check className="w-4 h-4 text-green-500 mr-2" />
|
||||
Análisis de tendencias avanzado
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Check className="w-4 h-4 text-green-500 mr-2" />
|
||||
Soporte técnico prioritario
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Check className="w-4 h-4 text-green-500 mr-2" />
|
||||
Integración con sistemas de punto de venta
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<label className="block">
|
||||
<span className="text-sm font-medium text-gray-700 mb-2 block">
|
||||
Información de la tarjeta
|
||||
</span>
|
||||
<div className="border border-gray-300 rounded-xl p-4 focus-within:ring-2 focus-within:ring-primary-500 focus-within:border-primary-500">
|
||||
<CardElement options={cardElementOptions} />
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<div className="flex items-center space-x-2 text-sm text-gray-500">
|
||||
<Shield className="w-4 h-4" />
|
||||
<span>Pago seguro con encriptación SSL. Powered by Stripe.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!stripe || paymentLoading}
|
||||
className="w-full bg-primary-500 text-white py-3 px-4 rounded-xl font-medium hover:bg-primary-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex items-center justify-center"
|
||||
>
|
||||
{paymentLoading ? (
|
||||
<>
|
||||
<Loader2 className="w-5 h-5 mr-2 animate-spin" />
|
||||
Procesando pago...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CreditCard className="w-5 h-5 mr-2" />
|
||||
Pagar €{SUBSCRIPTION_PRICE_EUR}/mes
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
// Password validation based on backend rules
|
||||
const validatePassword = (password: string) => {
|
||||
const checks = {
|
||||
length: password.length >= 8,
|
||||
uppercase: /[A-Z]/.test(password),
|
||||
lowercase: /[a-z]/.test(password),
|
||||
numbers: /\d/.test(password),
|
||||
// symbols: /[!@#$%^&*(),.?":{}|<>]/.test(password) // Backend doesn't require symbols
|
||||
};
|
||||
|
||||
const score = Object.values(checks).filter(Boolean).length;
|
||||
|
||||
let message = '';
|
||||
if (score < 4) {
|
||||
if (!checks.length) message += 'Mínimo 8 caracteres. ';
|
||||
if (!checks.uppercase) message += 'Una mayúscula. ';
|
||||
if (!checks.lowercase) message += 'Una minúscula. ';
|
||||
if (!checks.numbers) message += 'Un número. ';
|
||||
} else {
|
||||
message = '¡Contraseña segura!';
|
||||
}
|
||||
|
||||
return { score, checks, message: message.trim() };
|
||||
};
|
||||
|
||||
|
||||
const handleFormSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
// Validate form but exclude payment requirement for first step
|
||||
const newErrors: Partial<RegisterForm> = {};
|
||||
|
||||
if (!formData.fullName.trim()) {
|
||||
@@ -50,10 +260,19 @@ const RegisterPage: React.FC<RegisterPageProps> = ({ onLogin, onNavigateToLogin
|
||||
newErrors.email = 'El email no es válido';
|
||||
}
|
||||
|
||||
if (!formData.confirmEmail) {
|
||||
newErrors.confirmEmail = 'Confirma tu email';
|
||||
} else if (formData.email !== formData.confirmEmail) {
|
||||
newErrors.confirmEmail = 'Los emails no coinciden';
|
||||
}
|
||||
|
||||
if (!formData.password) {
|
||||
newErrors.password = 'La contraseña es obligatoria';
|
||||
} else if (formData.password.length < 8) {
|
||||
newErrors.password = 'La contraseña debe tener al menos 8 caracteres';
|
||||
} else {
|
||||
const passwordValidation = validatePassword(formData.password);
|
||||
if (passwordValidation.score < 4) {
|
||||
newErrors.password = passwordValidation.message;
|
||||
}
|
||||
}
|
||||
|
||||
if (formData.password !== formData.confirmPassword) {
|
||||
@@ -65,13 +284,29 @@ const RegisterPage: React.FC<RegisterPageProps> = ({ onLogin, onNavigateToLogin
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
|
||||
if (Object.keys(newErrors).length > 0) return;
|
||||
|
||||
// Move to payment step, or bypass if in development mode
|
||||
if (bypassPayment) {
|
||||
// Development bypass: simulate payment completion
|
||||
setFormData(prev => ({ ...prev, paymentCompleted: true }));
|
||||
setPaymentStep('completed');
|
||||
toast.success('🚀 Modo desarrollo: Pago omitido');
|
||||
// Proceed directly to registration
|
||||
setTimeout(() => {
|
||||
handleRegistrationComplete();
|
||||
}, 1500);
|
||||
} else {
|
||||
setPaymentStep('payment');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!validateForm()) return;
|
||||
const handleRegistrationComplete = async () => {
|
||||
if (!bypassPayment && !formData.paymentCompleted) {
|
||||
toast.error('El pago debe completarse antes del registro');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const registerData: RegisterRequest = {
|
||||
@@ -97,6 +332,9 @@ const RegisterPage: React.FC<RegisterPageProps> = ({ onLogin, onNavigateToLogin
|
||||
} catch (error) {
|
||||
console.error('Registration error:', error);
|
||||
toast.error(error instanceof Error ? error.message : 'Error en el registro');
|
||||
// Reset payment if registration fails
|
||||
setFormData(prev => ({ ...prev, paymentCompleted: false }));
|
||||
setPaymentStep('payment');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -116,19 +354,68 @@ const RegisterPage: React.FC<RegisterPageProps> = ({ onLogin, onNavigateToLogin
|
||||
}
|
||||
};
|
||||
|
||||
const getPasswordStrength = (password: string) => {
|
||||
let strength = 0;
|
||||
if (password.length >= 8) strength++;
|
||||
if (/[a-z]/.test(password)) strength++;
|
||||
if (/[A-Z]/.test(password)) strength++;
|
||||
if (/\d/.test(password)) strength++;
|
||||
if (/[^A-Za-z0-9]/.test(password)) strength++;
|
||||
return strength;
|
||||
};
|
||||
|
||||
const passwordStrength = getPasswordStrength(formData.password);
|
||||
const strengthLabels = ['Muy débil', 'Débil', 'Regular', 'Buena', 'Excelente'];
|
||||
const strengthColors = ['bg-red-500', 'bg-orange-500', 'bg-yellow-500', 'bg-blue-500', 'bg-green-500'];
|
||||
// Render different content based on payment step
|
||||
if (paymentStep === 'payment' && !bypassPayment) {
|
||||
return (
|
||||
<Elements stripe={stripePromise}>
|
||||
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-primary-50 to-orange-100 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-md w-full space-y-8">
|
||||
{/* Logo and Header */}
|
||||
<div className="text-center">
|
||||
<div className="mx-auto h-20 w-20 bg-primary-500 rounded-2xl flex items-center justify-center mb-6 shadow-lg">
|
||||
<span className="text-white text-2xl font-bold">🥖</span>
|
||||
</div>
|
||||
<h1 className="text-4xl font-bold font-display text-gray-900 mb-2">
|
||||
Finalizar Registro
|
||||
</h1>
|
||||
<p className="text-gray-600 text-lg">
|
||||
Solo un paso más para comenzar
|
||||
</p>
|
||||
<p className="text-gray-500 text-sm mt-2">
|
||||
Suscripción segura con Stripe
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Payment Form */}
|
||||
<div className="bg-white py-8 px-6 shadow-strong rounded-3xl">
|
||||
<PaymentForm onPaymentSuccess={handleRegistrationComplete} />
|
||||
|
||||
<button
|
||||
onClick={() => setPaymentStep('form')}
|
||||
className="w-full mt-4 text-sm text-gray-500 hover:text-gray-700 transition-colors"
|
||||
>
|
||||
← Volver al formulario
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Elements>
|
||||
);
|
||||
}
|
||||
|
||||
if (paymentStep === 'completed') {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-primary-50 to-orange-100 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-md w-full space-y-8">
|
||||
<div className="text-center">
|
||||
<div className="mx-auto h-20 w-20 bg-green-500 rounded-2xl flex items-center justify-center mb-6 shadow-lg">
|
||||
<Check className="text-white text-2xl" />
|
||||
</div>
|
||||
<h1 className="text-4xl font-bold font-display text-gray-900 mb-2">
|
||||
¡Bienvenido a PanIA!
|
||||
</h1>
|
||||
<p className="text-gray-600 text-lg">
|
||||
Tu cuenta ha sido creada exitosamente
|
||||
</p>
|
||||
<p className="text-gray-500 text-sm mt-2">
|
||||
Redirigiendo al panel de control...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-primary-50 to-orange-100 px-4 sm:px-6 lg:px-8">
|
||||
@@ -151,7 +438,7 @@ const RegisterPage: React.FC<RegisterPageProps> = ({ onLogin, onNavigateToLogin
|
||||
|
||||
{/* Register Form */}
|
||||
<div className="bg-white py-8 px-6 shadow-strong rounded-3xl">
|
||||
<form className="space-y-6" onSubmit={handleSubmit}>
|
||||
<form className="space-y-6" onSubmit={handleFormSubmit}>
|
||||
{/* Full Name Field */}
|
||||
<div>
|
||||
<label htmlFor="fullName" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
@@ -212,6 +499,43 @@ const RegisterPage: React.FC<RegisterPageProps> = ({ onLogin, onNavigateToLogin
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Confirm Email Field */}
|
||||
<div>
|
||||
<label htmlFor="confirmEmail" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Confirmar correo electrónico
|
||||
</label>
|
||||
<input
|
||||
id="confirmEmail"
|
||||
name="confirmEmail"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
required
|
||||
value={formData.confirmEmail}
|
||||
onChange={handleInputChange}
|
||||
className={`
|
||||
appearance-none relative block w-full px-4 py-3 border rounded-xl
|
||||
placeholder-gray-400 text-gray-900 focus:outline-none focus:ring-2
|
||||
focus:ring-primary-500 focus:border-primary-500 focus:z-10
|
||||
transition-all duration-200
|
||||
${errors.confirmEmail
|
||||
? 'border-red-300 bg-red-50'
|
||||
: formData.confirmEmail && formData.email === formData.confirmEmail
|
||||
? 'border-green-300 bg-green-50'
|
||||
: 'border-gray-300 hover:border-gray-400'
|
||||
}
|
||||
`}
|
||||
placeholder="tu@panaderia.com"
|
||||
/>
|
||||
{formData.confirmEmail && formData.email === formData.confirmEmail && (
|
||||
<div className="absolute inset-y-0 right-3 flex items-center mt-8">
|
||||
<Check className="h-5 w-5 text-green-500" />
|
||||
</div>
|
||||
)}
|
||||
{errors.confirmEmail && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.confirmEmail}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Password Field */}
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
@@ -251,21 +575,30 @@ const RegisterPage: React.FC<RegisterPageProps> = ({ onLogin, onNavigateToLogin
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Password Strength Indicator */}
|
||||
{/* Enhanced Password Strength Indicator */}
|
||||
{formData.password && (
|
||||
<div className="mt-2">
|
||||
<div className="flex space-x-1">
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`h-1 flex-1 rounded ${
|
||||
i < passwordStrength ? strengthColors[passwordStrength - 1] : 'bg-gray-200'
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
<div className="grid grid-cols-4 gap-1">
|
||||
{Object.entries(passwordStrength.checks).map(([key, passed], index) => {
|
||||
const labels = {
|
||||
length: '8+ caracteres',
|
||||
uppercase: 'Mayúscula',
|
||||
lowercase: 'Minúscula',
|
||||
numbers: 'Número'
|
||||
};
|
||||
return (
|
||||
<div key={key} className={`text-xs p-1 rounded text-center ${
|
||||
passed ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-500'
|
||||
}`}>
|
||||
{passed ? '✓' : '○'} {labels[key as keyof typeof labels]}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<p className="text-xs text-gray-600 mt-1">
|
||||
Seguridad: {strengthLabels[passwordStrength - 1] || 'Muy débil'}
|
||||
<p className={`text-xs mt-1 ${
|
||||
passwordStrength.score === 4 ? 'text-green-600' : 'text-gray-600'
|
||||
}`}>
|
||||
{passwordStrength.message}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -370,10 +703,13 @@ const RegisterPage: React.FC<RegisterPageProps> = ({ onLogin, onNavigateToLogin
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Loader2 className="h-5 w-5 mr-2 animate-spin" />
|
||||
Creando cuenta...
|
||||
Validando...
|
||||
</>
|
||||
) : (
|
||||
'Crear cuenta gratis'
|
||||
<>
|
||||
{bypassPayment ? 'Crear Cuenta (Dev)' : 'Continuar al Pago'}
|
||||
<ArrowRight className="h-5 w-5 ml-2" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
@@ -395,8 +731,18 @@ const RegisterPage: React.FC<RegisterPageProps> = ({ onLogin, onNavigateToLogin
|
||||
|
||||
{/* Benefits */}
|
||||
<div className="text-center">
|
||||
{bypassPayment && (
|
||||
<div className="mb-4 p-2 bg-yellow-100 border border-yellow-300 rounded-lg">
|
||||
<p className="text-xs text-yellow-800">
|
||||
🚀 Modo Desarrollo: Pago omitido para pruebas
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<p className="text-xs text-gray-500 mb-4">
|
||||
Al registrarte obtienes acceso completo durante 30 días gratis
|
||||
{bypassPayment
|
||||
? 'Desarrollo • Pruebas • Sin pago requerido'
|
||||
: 'Proceso seguro • Cancela en cualquier momento • Soporte 24/7'
|
||||
}
|
||||
</p>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 text-xs text-gray-400">
|
||||
<div className="flex items-center justify-center">
|
||||
@@ -405,11 +751,11 @@ const RegisterPage: React.FC<RegisterPageProps> = ({ onLogin, onNavigateToLogin
|
||||
</div>
|
||||
<div className="flex items-center justify-center">
|
||||
<span className="w-2 h-2 bg-success-500 rounded-full mr-2"></span>
|
||||
Soporte 24/7
|
||||
Análisis de demanda
|
||||
</div>
|
||||
<div className="flex items-center justify-center">
|
||||
<span className="w-2 h-2 bg-success-500 rounded-full mr-2"></span>
|
||||
Sin compromiso
|
||||
Reduce desperdicios
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user