// Auth Modal Component - OTP email verification flow const inputStyle = { width: '100%', padding: '10px 12px', border: '1px solid var(--k-line)', borderRadius: 8, fontSize: 14, fontFamily: 'var(--k-sans)', background: 'var(--k-surface)', color: 'var(--k-ink)', outline: 'none', boxSizing: 'border-box', }; function AuthModal({ mode, onClose, onAuth }) { const [view, setView] = React.useState(mode); // 'signup' | 'signin' const [step, setStep] = React.useState('form'); // 'form' | 'otp' const [form, setForm] = React.useState({ first: '', last: '', email: '', org: '', role: '' }); const [otp, setOtp] = React.useState(''); const [pending, setPending] = React.useState(''); // email awaiting verification const [devCode, setDevCode] = React.useState(''); // pilot: code shown on screen const [error, setError] = React.useState(''); const [loading, setLoading] = React.useState(false); const isValidEmail = v => /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(v); const cap = v => v ? v.charAt(0).toUpperCase() + v.slice(1) : v; const set = (k, v) => setForm(f => ({ ...f, [k]: v })); const switchView = (v) => { setView(v); setStep('form'); setError(''); setOtp(''); }; // ── Step 1: send OTP ──────────────────────────────────────────────── const handleSendCode = async () => { const { first, last, email, org, role } = form; const isSignup = view === 'signup'; if (isSignup) { if (!first) { setError('Please enter your first name.'); return; } if (!last) { setError('Please enter your last name.'); return; } if (!email) { setError('Please enter your email address.'); return; } if (!isValidEmail(email)) { setError("That email address doesn't look right — please check it."); return; } if (!org) { setError('Please enter your organisation.'); return; } if (!role) { setError('Please enter your job title or role.'); return; } } else { if (!email || !isValidEmail(email)) { setError('Please enter a valid email address.'); return; } } setError(''); setLoading(true); try { const body = isSignup ? { email, name: cap(first) + ' ' + cap(last), org, role } : { email }; const res = await fetch('/auth/otp/send', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); const data = await res.json(); if (!res.ok) { setError(data.detail || 'Could not send verification code. Please try again.'); return; } setPending(email); setDevCode(data.dev_code || ''); setStep('otp'); } catch(e) { setError('Something went wrong. Please try again.'); } finally { setLoading(false); } }; // ── Step 2: verify OTP ────────────────────────────────────────────── const handleVerify = async () => { if (!otp || otp.length < 6) { setError('Please enter the 6-digit code.'); return; } setError(''); setLoading(true); try { const res = await fetch('/auth/otp/verify', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: pending, code: otp }), }); const data = await res.json(); if (!res.ok) { setError(data.detail || 'Verification failed. Please try again.'); return; } const name = data.name || ''; const user = { id: data.user_id, name, email: data.email, org: data.org || '', role: data.role || '', firstName: name.split(' ')[0], lastName: name.split(' ').slice(1).join(' '), }; localStorage.setItem('kalibr.user', JSON.stringify(user)); localStorage.setItem('kalibr.session_token', data.access_token); onAuth(); } catch(e) { setError('Verification failed. Please try again.'); } finally { setLoading(false); } }; const Logo = () => (
We sent a 6-digit code to
{pending}