// Access Code Gate — landing screen // Calls /api/auth/verify-code via tmApi (browser API client at /lib/tm-browser.js) const CodeGate = ({ onSuccess, onAdminClick }) => { const [code, setCode] = React.useState(''); const [error, setError] = React.useState(false); const [errorMsg, setErrorMsg] = React.useState(''); const [shake, setShake] = React.useState(false); const [rateLimit, setRateLimit] = React.useState(false); const [rateLimitMin, setRateLimitMin] = React.useState(15); const [submitting, setSubmitting] = React.useState(false); const [attempts, setAttempts] = React.useState(0); const inputRef = React.useRef(null); React.useEffect(() => { inputRef.current?.focus(); }, []); const formatCode = (raw) => { const clean = raw.replace(/[^A-Z0-9]/gi, '').toUpperCase().slice(0, 16); return clean.match(/.{1,4}/g)?.join('-') || ''; }; const handleChange = (e) => { setCode(formatCode(e.target.value)); setError(false); setErrorMsg(''); }; const submit = async () => { if (submitting || rateLimit) return; const stripped = code.replace(/-/g, ''); if (stripped.length !== 16) { triggerError('Code must be 16 characters.'); return; } setSubmitting(true); try { const res = await tmApi.customer.verifyCode(code); // sends e.g. "DEMO-CODE-2026-ENTR" onSuccess({ codePreview: res?.codePreview || '' }); } catch (e) { const c = e.code || 'http_error'; if (c === 'rate_limited') { const m = /(\d+)\s*minutes/.exec(e.message || ''); const mins = m ? Number(m[1]) : 15; setRateLimitMin(mins); setRateLimit(true); setTimeout(() => setRateLimit(false), Math.min(mins, 15) * 60 * 1000); } else if (c === 'invalid_code' || c === 'invalid_code_format') { triggerError('Invalid code.'); } else if (c === 'key_revoked') { triggerError('This code has been revoked.'); } else if (c === 'key_expired') { triggerError('This code has expired.'); } else { triggerError(e.message || 'Sign-in failed.'); } setAttempts((a) => a + 1); } finally { setSubmitting(false); } }; const triggerError = (msg) => { setError(true); setErrorMsg(msg || ''); setShake(true); setTimeout(() => setShake(false), 500); }; const fillDemo = () => setCode('DEMO-CODE-2026-ENTR'); return (
{/* subtle grid bg */}
{/* Logo + wordmark */}
TEMPMAIL.LOCAL
{/* SMTP status dotted line */}
INVITE-ONLY · PRIVATE RELAY

Enter access code

Paste or type the 16-character code you were issued.

e.key === 'Enter' && submit()} placeholder="XXXX-XXXX-XXXX-XXXX" disabled={rateLimit || submitting} style={{ width: '100%', background: '#141414', border: `1px solid ${error ? '#DC2626' : '#262626'}`, color: '#EDEDED', fontFamily: "'JetBrains Mono', monospace", fontSize: 18, padding: '16px 18px', borderRadius: 6, outline: 'none', letterSpacing: '0.08em', textAlign: 'center', transition: 'border-color 150ms ease, background 150ms ease', boxSizing: 'border-box', }} onFocus={(e) => !error && (e.target.style.borderColor = '#404040')} onBlur={(e) => !error && (e.target.style.borderColor = '#262626')} />
{error && !rateLimit && (
{errorMsg || 'Invalid code.'}
)} {rateLimit && (
Too many attempts. Try again in {rateLimitMin} minutes.
)} {/* "Are you an admin?" link — sits between Enter button and contact-admin row */}

Don't have a code? Contact admin.

{/* footer */}
v0.4.2 · status: operational · build 7f9a2c
); }; window.CodeGate = CodeGate;