Improve the register page
This commit is contained in:
@@ -12,4 +12,12 @@ VITE_ENABLE_WEBSOCKETS=false
|
||||
VITE_ENABLE_OFFLINE=false
|
||||
VITE_ENABLE_OPTIMISTIC_UPDATES=true
|
||||
VITE_ENABLE_DEDUPLICATION=true
|
||||
VITE_ENABLE_METRICS=false
|
||||
VITE_ENABLE_METRICS=false
|
||||
|
||||
# Stripe Configuration (Spanish Market)
|
||||
VITE_STRIPE_PUBLISHABLE_KEY=pk_test_example_key_for_development
|
||||
VITE_STRIPE_WEBHOOK_SECRET=whsec_example_webhook_secret_for_development
|
||||
|
||||
# Development Flags
|
||||
VITE_BYPASS_PAYMENT=true
|
||||
VITE_DEV_MODE=true
|
||||
23
frontend/.env.production
Normal file
23
frontend/.env.production
Normal file
@@ -0,0 +1,23 @@
|
||||
# API Configuration
|
||||
VITE_API_URL=https://api.pania.es/api/v1
|
||||
VITE_API_TIMEOUT=30000
|
||||
VITE_API_RETRIES=3
|
||||
VITE_API_RETRY_DELAY=1000
|
||||
VITE_API_LOGGING=false
|
||||
VITE_API_CACHING=true
|
||||
VITE_API_CACHE_TIMEOUT=300000
|
||||
|
||||
# Feature Flags
|
||||
VITE_ENABLE_WEBSOCKETS=true
|
||||
VITE_ENABLE_OFFLINE=true
|
||||
VITE_ENABLE_OPTIMISTIC_UPDATES=true
|
||||
VITE_ENABLE_DEDUPLICATION=true
|
||||
VITE_ENABLE_METRICS=true
|
||||
|
||||
# Stripe Configuration (Spanish Market)
|
||||
VITE_STRIPE_PUBLISHABLE_KEY=pk_live_your_production_stripe_key
|
||||
VITE_STRIPE_WEBHOOK_SECRET=whsec_your_production_webhook_secret
|
||||
|
||||
# Development Flags (DISABLED IN PRODUCTION)
|
||||
VITE_BYPASS_PAYMENT=false
|
||||
VITE_DEV_MODE=false
|
||||
111
frontend/package-lock.json
generated
111
frontend/package-lock.json
generated
@@ -11,6 +11,8 @@
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.3.1",
|
||||
"@reduxjs/toolkit": "^1.9.5",
|
||||
"@stripe/react-stripe-js": "^3.9.0",
|
||||
"@stripe/stripe-js": "^7.8.0",
|
||||
"clsx": "^2.0.0",
|
||||
"date-fns": "^2.30.0",
|
||||
"date-fns-tz": "^2.0.0",
|
||||
@@ -1213,6 +1215,29 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@stripe/react-stripe-js": {
|
||||
"version": "3.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-3.9.0.tgz",
|
||||
"integrity": "sha512-pN1Re7zUc3m61FFQROok685g3zsBQRzCmZDmTzO8iPU6zhLvu2JnC0LrG0FCzSp6kgGa8AQSzq4rpFSgyhkjKg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prop-types": "^15.7.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@stripe/stripe-js": ">=1.44.1 <8.0.0",
|
||||
"react": ">=16.8.0 <20.0.0",
|
||||
"react-dom": ">=16.8.0 <20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@stripe/stripe-js": {
|
||||
"version": "7.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-7.8.0.tgz",
|
||||
"integrity": "sha512-DNXRfYUgkZlrniQORbA/wH8CdFRhiBSE0R56gYU0V5vvpJ9WZwvGrz9tBAZmfq2aTgw6SK7mNpmTizGzLWVezw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.16"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/forms": {
|
||||
"version": "0.5.10",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.10.tgz",
|
||||
@@ -1242,20 +1267,6 @@
|
||||
"tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": {
|
||||
"version": "6.0.10",
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
|
||||
"integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cssesc": "^3.0.0",
|
||||
"util-deprecate": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/@testing-library/dom": {
|
||||
"version": "10.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
|
||||
@@ -1605,13 +1616,13 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz",
|
||||
"integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==",
|
||||
"version": "24.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.1.tgz",
|
||||
"integrity": "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.8.0"
|
||||
"undici-types": "~7.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/prop-types": {
|
||||
@@ -2559,9 +2570,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/browserslist": {
|
||||
"version": "4.25.1",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz",
|
||||
"integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==",
|
||||
"version": "4.25.2",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.2.tgz",
|
||||
"integrity": "sha512-0si2SJK3ooGzIawRu61ZdPCO1IncZwS8IzuX73sPZsXW6EQ/w/DAfPyKI8l1ETTCr2MnvqWitmlCUxgdul45jA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -2579,8 +2590,8 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"caniuse-lite": "^1.0.30001726",
|
||||
"electron-to-chromium": "^1.5.173",
|
||||
"caniuse-lite": "^1.0.30001733",
|
||||
"electron-to-chromium": "^1.5.199",
|
||||
"node-releases": "^2.0.19",
|
||||
"update-browserslist-db": "^1.1.3"
|
||||
},
|
||||
@@ -2672,9 +2683,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001731",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001731.tgz",
|
||||
"integrity": "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==",
|
||||
"version": "1.0.30001734",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001734.tgz",
|
||||
"integrity": "sha512-uhE1Ye5vgqju6OI71HTQqcBCZrvHugk0MjLak7Q+HfoBgoq5Bi+5YnwjP4fjDgrtYr/l8MVRBvzz9dPD4KyK0A==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -3252,9 +3263,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.194",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.194.tgz",
|
||||
"integrity": "sha512-SdnWJwSUot04UR51I2oPD8kuP2VI37/CADR1OHsFOUzZIvfWJBO6q11k5P/uKNyTT3cdOsnyjkrZ+DDShqYqJA==",
|
||||
"version": "1.5.199",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.199.tgz",
|
||||
"integrity": "sha512-3gl0S7zQd88kCAZRO/DnxtBKuhMO4h0EaQIN3YgZfV6+pW+5+bf2AdQeHNESCoaQqo/gjGVYEf2YM4O5HJQqpQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
@@ -5775,7 +5786,7 @@
|
||||
"postcss": "^8.2.14"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-selector-parser": {
|
||||
"node_modules/postcss-nested/node_modules/postcss-selector-parser": {
|
||||
"version": "6.1.2",
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
|
||||
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
|
||||
@@ -5789,6 +5800,20 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-selector-parser": {
|
||||
"version": "6.0.10",
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
|
||||
"integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cssesc": "^3.0.0",
|
||||
"util-deprecate": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-value-parser": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
||||
@@ -6869,6 +6894,20 @@
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss/node_modules/postcss-selector-parser": {
|
||||
"version": "6.1.2",
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
|
||||
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cssesc": "^3.0.0",
|
||||
"util-deprecate": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/text-table": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
||||
@@ -7033,9 +7072,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.8.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz",
|
||||
"integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==",
|
||||
"version": "7.10.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz",
|
||||
"integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -7575,9 +7614,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz",
|
||||
"integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==",
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz",
|
||||
"integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
|
||||
@@ -14,46 +14,48 @@
|
||||
"lint:fix": "eslint . --ext ts,tsx --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.15.0",
|
||||
"@reduxjs/toolkit": "^1.9.5",
|
||||
"react-redux": "^8.1.2",
|
||||
"i18next": "^23.4.4",
|
||||
"react-i18next": "^13.1.2",
|
||||
"i18next-browser-languagedetector": "^7.1.0",
|
||||
"react-hook-form": "^7.45.4",
|
||||
"@hookform/resolvers": "^3.3.1",
|
||||
"zod": "^3.22.2",
|
||||
"recharts": "^2.8.0",
|
||||
"@reduxjs/toolkit": "^1.9.5",
|
||||
"@stripe/react-stripe-js": "^3.9.0",
|
||||
"@stripe/stripe-js": "^7.8.0",
|
||||
"clsx": "^2.0.0",
|
||||
"date-fns": "^2.30.0",
|
||||
"date-fns-tz": "^2.0.0",
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"i18next": "^23.4.4",
|
||||
"i18next-browser-languagedetector": "^7.1.0",
|
||||
"lucide-react": "^0.263.1",
|
||||
"clsx": "^2.0.0",
|
||||
"tailwind-merge": "^1.14.0"
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hook-form": "^7.45.4",
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"react-i18next": "^13.1.2",
|
||||
"react-redux": "^8.1.2",
|
||||
"react-router-dom": "^6.15.0",
|
||||
"recharts": "^2.8.0",
|
||||
"tailwind-merge": "^1.14.0",
|
||||
"zod": "^3.22.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/forms": "^0.5.4",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^14.4.3",
|
||||
"@types/react": "^18.2.15",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"@vitejs/plugin-react": "^4.0.3",
|
||||
"@vitest/ui": "^0.34.1",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"eslint": "^8.45.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.3",
|
||||
"postcss": "^8.4.27",
|
||||
"tailwindcss": "^3.3.0",
|
||||
"@tailwindcss/forms": "^0.5.4",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"typescript": "^5.0.2",
|
||||
"vite": "^4.4.5",
|
||||
"vitest": "^0.34.1",
|
||||
"@vitest/ui": "^0.34.1",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/user-event": "^14.4.3"
|
||||
"vitest": "^0.34.1"
|
||||
},
|
||||
"keywords": [
|
||||
"bakery",
|
||||
@@ -70,4 +72,4 @@
|
||||
},
|
||||
"author": "PanIA Team",
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
4
frontend/vite-env.d.ts
vendored
4
frontend/vite-env.d.ts
vendored
@@ -13,6 +13,10 @@ interface ImportMetaEnv {
|
||||
readonly VITE_ENABLE_OPTIMISTIC_UPDATES: string
|
||||
readonly VITE_ENABLE_DEDUPLICATION: string
|
||||
readonly VITE_ENABLE_METRICS: string
|
||||
readonly VITE_STRIPE_PUBLISHABLE_KEY: string
|
||||
readonly VITE_STRIPE_WEBHOOK_SECRET: string
|
||||
readonly VITE_BYPASS_PAYMENT: string
|
||||
readonly VITE_DEV_MODE: string
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
|
||||
Reference in New Issue
Block a user