/* auth-app.jsx — Radish auth entry screens (design_handoff_auth_flow).
*
* Six screens in one shell: sign in, create account, forgot password,
* check-your-email, set a new password, signed out. The screen is derived from
* location.pathname (auth-helpers.js), so each one is a real URL.
*
* The screens talk to the real /auth/* API (routes_auth.py): POST /auth/session,
* POST /auth/users (a new company, or an invite token), POST /auth/reset-requests,
* POST /auth/reset, and the public GET /auth/invite-preview. An invite link
* (/signup?invite=...) fixes both the company and the email. Sign-in and
* create-account land on a validated `?next` (auth-helpers.js safeNext), else "/";
* `next` is carried between sign-in / create-account / forgot so a deep-link
* survives the hop. auth-guard.js is deliberately NOT loaded on this page: it
* redirects to /signin on any 401, which would loop here.
* Pure logic (password rules, validation, failure→banner, safeNext) lives in
* auth-helpers.js so it is unit-testable: `node tests/auth_helpers.test.mjs`.
*/
const { useState, useEffect, useRef, useId } = React;
const A = window.__AUTH;
const { PasswordField } = window.RadishPassword;
/* Destination of "Back to radish.app" on the signed-out screen (per the handoff copy). */
const MARKETING_URL = "https://radish.app";
const EMAIL_MSG = "Enter a valid email address.";
const PW_MSG = "Use 10+ characters with a number and a symbol.";
/* ── API (the /auth/* endpoints named in the header) ─────────────────────── */
async function authRequest(method, path, body) {
try {
const res = await fetch(path, {
method,
headers: body ? { "Content-Type": "application/json" } : undefined,
body: body ? JSON.stringify(body) : undefined,
credentials: "same-origin",
});
return { ok: res.ok, status: res.status };
} catch (e) {
return { ok: false, status: 0 };
}
}
const authApi = {
signIn: (email, password, remember) => authRequest("POST", "/auth/session", { email, password, remember }),
createUser: (name, email, password, company, invite) =>
authRequest("POST", "/auth/users", { name, email, password, company, invite }),
requestReset: (email) => authRequest("POST", "/auth/reset-requests", { email }),
reset: (token, password) => authRequest("POST", "/auth/reset", { token, password }),
};
/* ── Navigation ──────────────────────────────────────────────────────────── */
const NavCtx = React.createContext(() => {});
/* URL of a screen. The validated `next` of the current page is carried (and only
* `next`: never invite/token) to the screens a deep-linked user hops between, so
* /signin?next=/plan/abc -> Create an account -> still lands on /plan/abc. */
const NEXT_SCREENS = ["signin", "signup", "forgot"];
function screenUrl(screen) {
const path = A.pathForScreen(screen);
if (NEXT_SCREENS.indexOf(screen) === -1) return path;
const next = A.safeNext(new URLSearchParams(window.location.search).get("next"));
return next === "/" ? path : path + "?next=" + encodeURIComponent(next);
}
/* Real (open-in-new-tab, copy-link work); plain clicks route client-side. */
function AuthLink({ to, email, className, children }) {
const nav = React.useContext(NavCtx);
const onClick = (e) => {
if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
e.preventDefault();
nav(to, email);
};
return {children};
}
/* ── Shell ───────────────────────────────────────────────────────────────── */
function AuthShell({ children }) {
return (
{children}
);
}
/* ── Form atoms ──────────────────────────────────────────────────────────── */
function Banner({ banner }) {
if (!banner) return null;
return (