nordicstorium/src/app/me/page.tsx

882 lines
44 KiB
TypeScript
Raw Normal View History

2026-02-02 15:09:01 +00:00
"use client";
/* eslint-disable @next/next/no-img-element */
import { useEffect, useState, useRef } from 'react';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import { useAuth } from '@/context/AuthContext';
import { validateSwedishMobile, normalizeSwedishMobile, validatePersonnummer, validateAddress, lookupSwedishCity, lookupSwedishAddress } from '@/lib/validation';
import { Settings, X } from 'lucide-react';
import '@/styles/Auth.css';
export default function MePage() {
const router = useRouter();
const { user, login, logout, loading: authLoading, isAuthenticated, refreshUser } = useAuth();
const [loading, setLoading] = useState(true);
const [isEditing, setIsEditing] = useState(false);
const [formData, setFormData] = useState({
name: '',
personnummer: '',
mobile: '',
address: '',
zip_code: '',
city: '',
country: ''
});
// Account Management States
const [actionLoading, setActionLoading] = useState(false);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [showChangePassword, setShowChangePassword] = useState(false);
const [passwords, setPasswords] = useState({
current: '',
new: '',
confirm: ''
});
// 2FA Setup States
const [show2FASetup, setShow2FASetup] = useState(false);
const [qrCode, setQrCode] = useState('');
const [twoFactorCode, setTwoFactorCode] = useState('');
const [twoFactorToken, setTwoFactorToken] = useState('');
const [setupError, setSetupError] = useState('');
const [setupSuccess, setSetupSuccess] = useState('');
const successTimeoutRef = useRef<NodeJS.Timeout | null>(null);
useEffect(() => {
if (user) {
setFormData({
name: user.name || '',
personnummer: user.personnummer || '',
mobile: user.mobile || '',
address: user.address || '',
zip_code: user.zip_code || '',
city: user.city || '',
country: user.country || ''
});
}
}, [user]);
// Auto-fetch city when zip code is 5 digits
useEffect(() => {
const fetchCity = async () => {
if (formData.zip_code.length === 5) {
const city = await lookupSwedishCity(formData.zip_code);
if (city) {
setFormData(prev => ({ ...prev, city }));
}
}
};
fetchCity();
}, [formData.zip_code]);
useEffect(() => {
if (!authLoading) {
if (!isAuthenticated) {
router.push('/login');
} else {
setLoading(false);
}
}
}, [authLoading, isAuthenticated, router]);
const handleLogout = () => {
logout();
};
const initiation2FASetup = async () => {
setSetupError('');
setSetupSuccess('');
try {
const token = localStorage.getItem('token');
const res = await fetch('/api/auth/2fa/setup', {
headers: { 'Authorization': `Bearer ${token}` }
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Kunde inte initiera 2FA');
setQrCode(data.qrCodeDataUrl);
setTwoFactorToken(data.secret);
setShow2FASetup(true);
} catch (err: unknown) {
setSetupError(err instanceof Error ? err.message : 'Ett oväntat fel uppstod');
}
};
const verifyAndEnable2FA = async (e: React.FormEvent) => {
e.preventDefault();
setSetupError('');
setActionLoading(true);
try {
const token = localStorage.getItem('token');
const res = await fetch('/api/auth/2fa/verify', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ code: twoFactorCode })
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Felaktig kod');
setSetupSuccess('Tvåfaktorsautentisering har aktiverats!');
setShow2FASetup(false);
setTwoFactorCode('');
if (user) {
const updatedUser = { ...user, two_factor_enabled: true };
login(token || '', updatedUser);
}
} catch (err: unknown) {
setSetupError(err instanceof Error ? err.message : 'Ett oväntat fel uppstod');
} finally {
setActionLoading(false);
}
};
const handleDisable2FA = async () => {
if (!confirm('Är du säker på att du vill inaktivera 2FA? Detta gör ditt konto mindre säkert.')) return;
setActionLoading(true);
setSetupError('');
setSetupSuccess('');
try {
const token = localStorage.getItem('token');
const res = await fetch('/api/auth/2fa/disable', {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}` }
});
if (!res.ok) throw new Error('Kunde inte inaktivera 2FA');
setSetupSuccess('2FA har inaktiverats.');
if (user) {
const updatedUser = { ...user, two_factor_enabled: false };
login(token || '', updatedUser);
}
} catch (err: unknown) {
setSetupError(err instanceof Error ? err.message : 'Ett fel uppstod');
} finally {
setActionLoading(false);
}
};
const handleNewsletterToggle = async () => {
const newValue = !user?.newsletter_subscribed;
setActionLoading(true);
setSetupError('');
try {
const token = localStorage.getItem('token');
const res = await fetch('/api/auth/newsletter/toggle', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ subscribed: newValue })
});
if (!res.ok) throw new Error('Kunde inte uppdatera nyhetsbrev');
if (user) {
const updatedUser = { ...user, newsletter_subscribed: newValue };
login(token || '', updatedUser);
}
if (newValue) {
setSetupSuccess('Välkommen! Du prenumererar nu på vårt nyhetsbrev.');
} else {
setSetupSuccess('Du har avprenumererat dig från vårt nyhetsbrev.');
}
// Reset Timer: Clear existing and start fresh
if (successTimeoutRef.current) clearTimeout(successTimeoutRef.current);
successTimeoutRef.current = setTimeout(() => setSetupSuccess(''), 5000);
} catch (err: unknown) {
setSetupError(err instanceof Error ? err.message : 'Ett fel uppstod');
} finally {
setActionLoading(false);
}
};
const handleUpdateProfile = async (e: React.FormEvent) => {
e.preventDefault();
setSetupError('');
setSetupSuccess('');
setActionLoading(true);
if (formData.name && formData.name.length < 2) {
setSetupError('Namnet är för kort.');
setActionLoading(false);
return;
}
if (formData.personnummer && !validatePersonnummer(formData.personnummer)) {
setSetupError('Ogiltigt personnummer. Använd formatet YYYYMMDD-XXXX.');
setActionLoading(false);
return;
}
if (formData.mobile && !validateSwedishMobile(formData.mobile)) {
setSetupError('Mobilnummeret är ogiltigt. Det måste vara ett svenskt mobilnummer (t.ex. 070-123 45 67).');
setActionLoading(false);
return;
}
if (formData.address && !validateAddress(formData.address)) {
setSetupError('Ogiltig adress. Ange gatuadress och nummer.');
setActionLoading(false);
return;
}
// Verify physical address exists (External API)
try {
const addressExists = await lookupSwedishAddress(formData.address, formData.zip_code, formData.city);
if (!addressExists) {
setSetupError('Adressen verkar inte existera. Vänligen kontrollera stavning och adressnummer.');
setActionLoading(false);
return;
}
} catch (err) {
console.error('Profile address verification error:', err);
// Fail open on network errors
}
const zipRegex = /^\d{5}$/;
if (formData.zip_code && !zipRegex.test(formData.zip_code.replace(/\s/g, ''))) {
setSetupError('Postnummer måste bestå av 5 siffror');
setActionLoading(false);
return;
}
try {
const token = localStorage.getItem('token') || sessionStorage.getItem('token');
const res = await fetch('/api/auth/me', {
method: 'PATCH',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
full_name: formData.name,
personnummer: formData.personnummer,
mobile: formData.mobile ? normalizeSwedishMobile(formData.mobile) : '',
address: formData.address,
zip_code: formData.zip_code,
city: formData.city
// Country is locked to 'Sverige'
})
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Kunde inte uppdatera profilen');
const updatedMobile = formData.mobile ? normalizeSwedishMobile(formData.mobile) : '';
if (user) {
const updatedUser = {
...user,
name: formData.name,
personnummer: formData.personnummer,
mobile: updatedMobile,
address: formData.address,
zip_code: formData.zip_code,
city: formData.city,
country: formData.country
};
login(token || '', updatedUser);
}
setFormData(prev => ({ ...prev, mobile: updatedMobile }));
setSetupSuccess('Profilen har uppdaterats!');
setIsEditing(false);
if (successTimeoutRef.current) clearTimeout(successTimeoutRef.current);
successTimeoutRef.current = setTimeout(() => setSetupSuccess(''), 5000);
} catch (err: unknown) {
setSetupError(err instanceof Error ? err.message : 'Ett fel uppstod');
} finally {
setActionLoading(false);
}
};
const handleChangePassword = async (e: React.FormEvent) => {
e.preventDefault();
setSetupError('');
setSetupSuccess('');
if (passwords.new !== passwords.confirm) {
setSetupError('Lösenorden matchar inte');
return;
}
setActionLoading(true);
try {
const token = localStorage.getItem('token') || sessionStorage.getItem('token');
const res = await fetch('/api/auth/change-password', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
currentPassword: passwords.current,
newPassword: passwords.new
})
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Gick inte att byta lösenord');
setSetupSuccess('Ditt lösenord har uppdaterats!');
setShowChangePassword(false);
setPasswords({ current: '', new: '', confirm: '' });
// Update local user state
if (user) {
login(token || '', { ...user, has_password: true });
}
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : "Ett fel uppstod";
setSetupError(msg);
if (msg.includes("Nuvarande lösenord krävs") && !user?.has_password) {
await refreshUser();
setPasswords({ current: "", new: "", confirm: "" });
}
} finally {
setActionLoading(false);
}
};
const handleDeleteAccount = async () => {
setActionLoading(true);
setSetupError('');
try {
const token = localStorage.getItem('token') || sessionStorage.getItem('token');
const res = await fetch('/api/auth/account', {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${token}` }
});
if (!res.ok) throw new Error('Kunde inte ta bort kontot');
logout();
router.push('/register?deleted=true');
} catch (err: unknown) {
setSetupError(err instanceof Error ? err.message : 'Ett fel uppstod');
setActionLoading(false);
}
};
if (authLoading || loading) {
return (
<div className="auth-page-container">
<div className="text-xl">Laddar...</div>
</div>
);
}
if (!user) return null;
return (
<div className="auth-page-container">
<div className="card-lg">
<div className="flex items-center justify-between mb-12 border-b border-gray-100 dark:border-white/10 pb-8">
<div className="flex flex-col gap-2">
<h2 className="auth-title" style={{ textAlign: 'left', marginBottom: 0, fontSize: '2.5rem' }}>Min Profil</h2>
{user.email_verified ? (
<span className="status-badge status-verified" style={{ alignSelf: 'flex-start' }}>Verifierad</span>
) : (
<span className="status-badge status-unverified" style={{ alignSelf: 'flex-start' }}>Ej Verifierad</span>
)}
</div>
{user.role === 'admin' && (
<Link href="/admin" className="auth-button secondary btn-admin">
ADMIN VIEW
</Link>
)}
</div>
{(!user.personnummer || !user.address || !user.mobile) && (
<div className="auth-error mb-8 flex items-center gap-3" style={{ background: 'oklch(0.962 0.018 272.314/0.1)', color: 'var(--text-secondary)', border: '1px solid var(--border-color)' }}>
<p className="text-xs font-bold uppercase tracking-wider mb-0">Din profil är inte komplett. Vänligen ange personnummer och adress för att kunna använda alla tjänster.</p>
</div>
)}
<div className="profile-main-grid w-full">
<div className="profile-card">
<div className="profile-card-header">
<div className="header-title-group">
<h3 className="header-title">Personlig Information</h3>
</div>
<div className="flex-shrink-0">
{!isEditing ? (
<button
onClick={() => setIsEditing(true)}
className="p-2 rounded-full transition-colors text-black dark:text-white hover:bg-black/5 dark:hover:bg-white/5 flex items-center justify-center"
aria-label="Redigera profil"
title="Redigera profil"
>
<Settings size={24} className="header-icon" />
</button>
) : (
<button
onClick={() => setIsEditing(false)}
className="p-2 rounded-full transition-colors text-gray-400 hover:text-black dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 flex items-center justify-center"
aria-label="Avbryt redigering"
title="Avbryt redigering"
>
<X size={24} className="header-icon" />
</button>
)}
</div>
</div>
<form onSubmit={handleUpdateProfile} className="profile-form-grid">
<div className="profile-form-row">
<div className="form-group">
<label className="form-label">Namn</label>
{isEditing ? (
<input
type="text"
className="form-input form-input-editable w-full"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
required
/>
) : (
<div className="form-input form-input-readonly">{user.name}</div>
)}
</div>
<div className="form-group">
<label className="form-label">Roll</label>
<div className="form-input form-input-readonly">
<span className={`font-black uppercase ${user.role === 'admin' ? 'text-red-500' : 'text-gray-500'}`}>
{user.role}
</span>
</div>
</div>
</div>
<div className="profile-form-row">
<div className="form-group">
<label className="form-label">Användarnamn</label>
<div className="form-input form-input-readonly">{user.username}</div>
</div>
<div className="form-group">
<label className="form-label">E-postadress</label>
<div className="form-input form-input-readonly">{user.email}</div>
</div>
</div>
<div className="profile-form-row">
<div className="form-group">
<label className="form-label">Personnummer</label>
{isEditing ? (
<input
type="text"
className="form-input form-input-editable w-full"
placeholder="ÅÅÅÅMMDD-XXXX"
maxLength={13}
value={formData.personnummer}
onChange={(e) => {
let val = e.target.value.replace(/\D/g, '');
if (val.length > 8) {
val = val.slice(0, 8) + '-' + val.slice(8, 12);
}
setFormData({ ...formData, personnummer: val });
}}
/>
) : (
<div className="form-input form-input-readonly">{user.personnummer || '-'}</div>
)}
</div>
<div className="form-group">
<label className="form-label">Mobil</label>
{isEditing ? (
<input
type="tel"
className="form-input form-input-editable w-full"
placeholder="+46 70 123 45 67"
value={formData.mobile}
onChange={(e) => setFormData({ ...formData, mobile: e.target.value })}
/>
) : (
<div className="form-input form-input-readonly">{user.mobile || '-'}</div>
)}
</div>
</div>
<div className="form-group">
<label className="form-label">Adress</label>
{isEditing ? (
<input
type="text"
className="form-input form-input-editable w-full"
value={formData.address}
onChange={(e) => setFormData({ ...formData, address: e.target.value })}
/>
) : (
<div className="form-input form-input-readonly">{user.address || '-'}</div>
)}
</div>
<div className="profile-form-row">
<div className="form-group">
<label className="form-label">Postnummer</label>
{isEditing ? (
<input
type="text"
className="form-input form-input-editable w-full"
maxLength={5}
value={formData.zip_code}
onChange={(e) => setFormData({ ...formData, zip_code: e.target.value.replace(/\D/g, '') })}
/>
) : (
<div className="form-input form-input-readonly">{user.zip_code || '-'}</div>
)}
</div>
<div className="form-group">
<label className="form-label">Ort</label>
{isEditing ? (
<input
type="text"
className="form-input form-input-editable w-full"
value={formData.city}
onChange={(e) => setFormData({ ...formData, city: e.target.value })}
required
/>
) : (
<div className="form-input form-input-readonly">{user.city || '-'}</div>
)}
</div>
</div>
<div className="profile-form-row">
<div className="form-group">
<label className="form-label">Land</label>
<div className="form-input form-input-readonly">Sverige</div>
</div>
<div className="form-group">
<label className="form-label">Medlem sedan</label>
<div className="form-input form-input-readonly">
{new Date(user.created_at).toLocaleDateString('sv-SE')}
</div>
</div>
</div>
{isEditing && (
<div className="mt-8">
<button
type="submit"
className="auth-button success"
disabled={actionLoading}
>
{actionLoading ? 'Sparar...' : 'Spara ändringar'}
</button>
</div>
)}
</form>
<div className="logout-section mt-12">
<button onClick={handleLogout} className="auth-button danger">
Logga ut Nordic Storium
</button>
</div>
</div>
{/* CARD 2: Settings & Security */}
<div className="profile-card">
<div className="profile-card-header">
<div className="header-title-group">
<h3 className="header-title">Inställningar & Säkerhet</h3>
</div>
<div className="flex-shrink-0 invisible pointer-events-none select-none">
<div className="p-2 flex items-center justify-center">
<Settings size={24} />
</div>
</div>
</div>
<div className="settings-internal-grid">
{/* Row 1: Password & Newsletter */}
{/* Password Change */}
{/* Row 1: Password & Newsletter */}
{/* Password Change */}
<div className="bg-white dark:bg-transparent p-6 flex flex-col justify-between">
<div className="mb-6">
<h4 className="font-black text-sm uppercase tracking-wider mb-2">
{user.has_password ? 'Ändra Lösenord' : 'Ställ in Lösenord'}
</h4>
<p className="text-xs text-gray-500 leading-relaxed">
{user.has_password
? 'Det är rekommenderat att byta lösenord med jämna mellanrum.'
: 'Ditt konto saknar lösenord då du använder social inloggning. Ställ in ett för att kunna logga in utan Google.'
}
</p>
</div>
{!showChangePassword ? (
<button
className={user.has_password ? "auth-button danger" : "auth-button success"}
onClick={() => setShowChangePassword(true)}
>
{user.has_password ? 'Ändra' : 'Sätt lösenord'}
</button>
) : (
<div className="animate-fade-in">
<div className="flex justify-between items-center mb-4">
<button
className="auth-button secondary text-sm py-2 px-4"
onClick={() => setShowChangePassword(false)}
>
Tillbaka
</button>
</div>
<form onSubmit={handleChangePassword} className="space-y-4">
{user.has_password && (
<div className="form-group">
<div className="form-label">Nuvarande lösenord</div>
<input
type="password"
required
className="form-input w-full"
value={passwords.current}
onChange={(e) => setPasswords({ ...passwords, current: e.target.value })}
/>
</div>
)}
<div className="form-group">
<div className="form-label">Nytt lösenord</div>
<input
type="password"
required
className="form-input w-full"
value={passwords.new}
onChange={(e) => setPasswords({ ...passwords, new: e.target.value })}
/>
</div>
<div className="form-group">
<div className="form-label">Bekräfta nytt lösenord</div>
<input
type="password"
required
className="form-input w-full"
value={passwords.confirm}
onChange={(e) => setPasswords({ ...passwords, confirm: e.target.value })}
/>
</div>
<button
type="submit"
className="auth-button success mt-4"
disabled={actionLoading}
>
{actionLoading ? 'Sparar...' : 'Bekräfta'}
</button>
</form>
</div>
)}
</div>
{/* Newsletter */}
<div className="bg-white dark:bg-transparent p-6 flex flex-col justify-between">
<div className="mb-6">
<h4 className="font-black text-sm uppercase tracking-wider mb-2">Nyhetsbrev</h4>
<p className="text-xs text-gray-500 leading-relaxed"> de senaste nordic-storium erbjudandena direkt i din inkorg.</p>
</div>
<button
className={`auth-button ${user.newsletter_subscribed ? 'danger' : 'success'}`}
onClick={handleNewsletterToggle}
disabled={actionLoading}
>
{user.newsletter_subscribed ? 'Avprenumerera' : 'Prenumerera'}
</button>
</div>
{/* Row 2: 2FA & Remove Account */}
{/* 2FA */}
<div className="bg-white dark:bg-transparent p-6 flex flex-col justify-between">
<div className="mb-6">
<h4 className="font-black text-sm uppercase tracking-wider mb-2">Säker Inloggning</h4>
<p className="text-xs text-gray-500 leading-relaxed">Säkra ditt konto med tvåfaktorsautentisering vid varje inloggning</p>
</div>
{!show2FASetup ? (
<button
className={`auth-button ${user.two_factor_enabled ? 'danger' : 'success'}`}
onClick={user.two_factor_enabled ? handleDisable2FA : initiation2FASetup}
disabled={actionLoading}
>
{user.two_factor_enabled ? 'Ta bort' : 'Aktivera'}
</button>
) : (
<div className="animate-fade-in">
<div className="flex justify-between items-center mb-4">
<button
className="auth-button secondary text-sm py-2 px-4"
onClick={() => setShow2FASetup(false)}
>
Avbryt
</button>
</div>
<div className="flex flex-col gap-6 w-full">
{/* 1. QR Code */}
<div className="flex justify-center w-full">
<img
src={qrCode}
alt="2FA QR"
className="qr-code-img"
/>
</div>
{/* 2. Manual Code Copy Section */}
<div className="w-full text-center space-y-3">
<p className="text-[10px] uppercase font-black text-gray-500">
Alternativ kod för manuell inmatning
</p>
<div className="form-group w-full">
<div
className="form-input w-full cursor-pointer relative"
onClick={async (e) => {
const element = e.currentTarget;
try {
await navigator.clipboard.writeText(twoFactorToken);
// Add green flash animation
element.classList.add('green-flash');
// Show compact feedback overlay
const overlay = document.createElement('div');
overlay.className = 'copy-success-overlay';
overlay.innerHTML = `
<div class="success-message-box">
<svg class="w-3 h-3 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7"></path>
</svg>
<span>Kopierad</span>
</div>
`;
element.appendChild(overlay);
// Remove after 1.5 seconds
setTimeout(() => {
element.classList.remove('green-flash');
if (overlay.parentNode === element) {
element.removeChild(overlay);
}
}, 1500);
} catch (err) {
console.error('Kunde inte kopiera:', err);
}
}}
>
<div className="form-label">Klicka & kopiera</div>
<div className="flex items-center justify-center">
<code className="font-mono text-sm font-black tracking-[0.1em] select-all text-center">
{twoFactorToken.replace(/(.{4})/g, '$1 ').trim()}
</code>
</div>
</div>
</div>
</div>
{/* 3. Verification Form */}
<form onSubmit={verifyAndEnable2FA} className="w-full space-y-2 mt-4">
<div className="form-group">
<div className="form-label">Autentiseringskod</div>
<input
type="text"
className="form-input w-full text-left tracking-[0.2em] text-lg"
maxLength={6}
placeholder="000000"
value={twoFactorCode}
onChange={(e) => setTwoFactorCode(e.target.value.replace(/\D/g, ''))}
/>
</div>
<button
type="submit"
className="auth-button success mt-6 w-full"
disabled={actionLoading || twoFactorCode.length !== 6}
>
{actionLoading ? 'Verifierar...' : 'Aktivera Nu'}
</button>
</form>
</div>
</div>
)}
</div>
{/* Remove Account */}
<div className="bg-white dark:bg-transparent p-6 flex flex-col justify-between">
<div className="mb-6">
<h4 className="font-black text-sm uppercase tracking-wider mb-2 text-red-600">Radera Konto</h4>
<p className="text-xs text-gray-500 leading-relaxed">Radera ditt konto och all data kopplade till kontot permanent.</p>
</div>
{!showDeleteConfirm ? (
<button
className="auth-button danger"
onClick={() => setShowDeleteConfirm(true)}
>
Radera konto
</button>
) : (
<div className="animate-fade-in flex flex-col h-full justify-between">
<div className="flex justify-end mb-2">
<button
className="auth-button secondary text-sm py-2 px-4"
onClick={() => setShowDeleteConfirm(false)}
>
Tillbaka
</button>
</div>
<p className="text-sm text-red-600 font-medium mb-4">
Är du säker? Denna åtgärd går inte att ångra.
</p>
<div className="flex gap-2">
<button
className="auth-button danger flex-1"
onClick={handleDeleteAccount}
disabled={actionLoading}
>
{actionLoading ? 'Raderar...' : 'Ja, Radera'}
</button>
</div>
</div>
)}
</div>
</div>
{setupSuccess && (
<div className="auth-success mt-8 animate-fade-in">
{setupSuccess}
</div>
)}
{setupError && (
<div className="auth-error mt-8 animate-fade-in">
{setupError}
</div>
)}
</div>
</div>
</div>
</div>
);
}