/* ============================================================ login.jsx — production sign-in + self-serve signup (mail-free). One universal login: email + password or Google. The account's role decides the app after sign-in (no persona picker). New companies self-register (company + GSTIN + owner) and are signed straight in as the finance owner. Demo logins are tucked into a dev-only panel. ============================================================ */ // "Sign in with Google" — renders via Google Identity Services once its script // (loaded async in index.html) is ready; hands back a signed ID token. function GoogleButton({ clientId, onCredential }) { const ref = React.useRef(null); const [busy, setBusy] = React.useState(false); const [err, setErr] = React.useState(""); const desktop = !!(window.isDesktopApp && window.isDesktopApp()); React.useEffect(() => { // WEB only: render Google's GIS button. Desktop uses the native flow below (GIS can't // run in the webview), so the effect no-ops there. if (desktop || !clientId || !ref.current) return; let tries = 0; const t = setInterval(() => { const g = window.google && window.google.accounts && window.google.accounts.id; if (g) { clearInterval(t); window.google.accounts.id.initialize({ client_id: clientId, callback: (resp) => onCredential(resp.credential), }); window.google.accounts.id.renderButton(ref.current, { theme: "outline", size: "large", width: 300, text: "continue_with", }); } else if (++tries > 60) { clearInterval(t); } }, 100); return () => clearInterval(t); }, [clientId, desktop]); if (desktop) { // DESKTOP: open the system browser (native loopback + PKCE) → id_token → login return (
{err &&
{err}
}
); } return
; } function Field({ label, hint, ...props }) { return (
{label}
{hint &&
{hint}
}
); } // --- sign-in form --------------------------------------------------------- function SignInForm({ onLogin, err, setErr }) { const [email, setEmail] = React.useState(""); const [password, setPassword] = React.useState(""); const [busy, setBusy] = React.useState(false); const submit = async (e) => { if (e) e.preventDefault(); setErr(""); setBusy(true); try { await onLogin(email.trim(), password); } catch (ex) { setErr(ex && ex.status === 429 ? "Too many attempts — try again in a few minutes." : "Invalid email or password."); } finally { setBusy(false); } }; return (
setEmail(e.target.value)} placeholder="you@company.in" /> setPassword(e.target.value)} placeholder="••••••••" /> {err &&
{err}
}
Forgot your password? Ask your organisation owner (or Lekha admin) to reset it.
); } // --- self-serve signup form ---------------------------------------------- function SignUpForm({ onSignup, err, setErr }) { const [f, setF] = React.useState({ company_name: "", name: "", email: "", password: "", confirm: "" }); const [busy, setBusy] = React.useState(false); const set = (k) => (e) => setF({ ...f, [k]: e.target.value }); // Client-side validation (mirrors the server): button stays disabled until valid. const cn = f.company_name.trim(); const nameOk = cn.length >= 2 && cn.length <= 120; const emailOk = /^\S+@\S+\.\S+$/.test(f.email.trim()); const ownerOk = f.name.trim().length > 0; const pwOk = f.password.length >= 10; const matchOk = f.confirm.length > 0 && f.password === f.confirm; const valid = nameOk && emailOk && ownerOk && pwOk && matchOk; const submit = async (e) => { if (e) e.preventDefault(); if (!valid || busy) return; setErr(""); setBusy(true); try { await onSignup({ company_name: cn, name: f.name.trim(), email: f.email.trim(), password: f.password, }); // success → the auth hook signs the owner in; the app re-renders. } catch (ex) { setErr((ex && ex.message) || "Could not create your account."); } finally { setBusy(false); } }; return (
{f.confirm.length > 0 && !matchOk && (
Passwords don't match.
)} {err &&
{err}
} ); } function LoginScreen({ onLogin, onGoogleLogin, onSignup }) { const [mode, setMode] = React.useState("login"); // 'login' | 'signup' const [err, setErr] = React.useState(""); const [googleClientId, setGoogleClientId] = React.useState(""); const [googleOn, setGoogleOn] = React.useState(false); // show the Google button? // single-session: show why the previous device was signed out (set in api 401) const [superseded] = React.useState(() => { const v = sessionStorage.getItem("lekha_signout") === "superseded"; sessionStorage.removeItem("lekha_signout"); return v; }); React.useEffect(() => { const desktop = !!(window.isDesktopApp && window.isDesktopApp()); api.get("/api/auth/config") .then((c) => { setGoogleClientId(c.google_client_id || ""); // desktop shows the button when the DESKTOP client is set; web when the web one is setGoogleOn(desktop ? !!c.google_desktop_client_id : !!c.google_client_id); }) .catch(() => {}); }, []); const onGoogleCredential = async (credential) => { setErr(""); try { await onGoogleLogin(credential); } catch (_) { setErr("This Google account isn’t authorised for Lekha. Ask an admin to add it."); } }; const switchMode = (m) => { setMode(m); setErr(""); }; const signup = mode === "signup"; return (
Lekha AI
Lekha AI books it.
{superseded && (
You were signed out because your account was signed in on another device. Only one active session is allowed at a time.
)}
{signup ? "Create your organisation" : "Sign in"}
{signup ? "Register your company and become its finance owner." : "Welcome back — sign in to your account."}
{signup ? : } {googleOn && !signup && (
); } Object.assign(window, { LoginScreen });