"use client"; /* Create account — onboarding screen 04. This creates a whole facility, which is why it asks for the hospital's name: the person signing up becomes its first administrator. */ import { PRIVACY_URL, TERMS_URL } from "@/lib/links"; import Link from "next/link"; import { useState } from "react"; import Turnstile, { awaitTurnstile, resetTurnstile, turnstileOn } from "@/components/Turnstile"; import { track } from "@/lib/analytics"; import { MAuthError, MAuthFooter, MAuthHeader, MField, MShowHide, authInput, authLink } from "@/components/MAuth"; import PlanChoice from "@/components/PlanChoice"; /* `plansLive` comes from the server wrapper at app/m/signup/page.tsx: while it is false the screen says free and asks nothing about plans; once true it offers the two hosted plans. */ export default function MSignup({ plansLive }: { plansLive: boolean }) { const [plan, setPlan] = useState<"hosted_small" | "hosted_facility">("hosted_small"); const [name, setName] = useState(""); const [email, setEmail] = useState(""); const [facility, setFacility] = useState(""); const [pw, setPw] = useState(""); const [show, setShow] = useState(false); const [agree, setAgree] = useState(false); const [cfToken, setCfToken] = useState(""); const [busy, setBusy] = useState(false); const [reveal, setReveal] = useState(false); const [err, setErr] = useState(""); /** Set once the facility exists, so the address it was created with can be shown back. */ const [made, setMade] = useState<{ email: string; mailed: boolean; mail: boolean } | null>(null); const emailOk = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); // The API wants first and last separately; the design asks for one "Full name" field, which is // the kinder question. Split on the last space and keep whatever they typed. const parts = name.trim().split(/\s+/); const first = parts.length > 1 ? parts.slice(0, -1).join(" ") : parts[0] || ""; const last = parts.length > 1 ? parts[parts.length - 1] : ""; const ready = !!(first && last && emailOk && facility.trim() && pw.length >= 8 && agree); async function submit() { if (!ready) { setErr(!name.trim() ? "Enter your name." : !emailOk ? "That email doesn’t look right." : !last ? "Enter your first and last name." : !facility.trim() ? "Which hospital is this for?" : pw.length < 8 ? "Password must be at least 8 characters." : "Tick the box to continue."); return; } setBusy(true); // The widget draws nothing in quiet mode, so nobody can see that it hasn't finished. Wait for // the token rather than posting an empty one and blaming the person for it. const token = cfToken || (turnstileOn() ? await awaitTurnstile() : ""); if (turnstileOn() && !token) { // Eight seconds and no token. Either the check needs an interaction we've asked Turnstile // not to draw, or it couldn't reach Cloudflare at all. Show the widget rather than send an // empty token and let the server answer with a check the person was never shown. setBusy(false); setReveal(true); track("security_check_shown", { screen: "signup" }); setErr("Finish the security check below, then try again."); return; } const r = await fetch("/api/auth/signup", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ first, last, facility, email, password: pw, cfToken: token, ...(plansLive ? { plan } : {}) }), }).catch(() => null); const j = r ? await r.json().catch(() => ({})) : {}; setBusy(false); /* The answer never came back, which is not the same as nothing having happened: the request may well have reached the server and made the facility before the connection went. Without this the promise rejected and the Create button stayed disabled on its spinner for ever. Trying again is safe — a second attempt on an address that did get through is refused as already having an account, which is itself the answer. */ if (!r) { track("signup_failed", { reason: "network" }); setErr("Couldn’t reach the server, so we can’t say whether the account was made. Try again — if it was, you’ll be told the email is already taken."); setCfToken(""); resetTurnstile(); return; } if (!r.ok) { track("signup_failed", { reason: r.status === 429 ? "throttled" : r.status === 400 ? "rejected" : "other" }); setErr(j.error || "Couldn’t create the account."); setCfToken(""); resetTurnstile(); return; } // A facility created itself from a phone — the whole point of building sign-up into the app. track("signup_completed"); /* Show the address back before going anywhere. * * The account is made and signed in either way. But this is the address a password reset goes * to and the only route back into a facility whose one admin is locked out, and a typo in it is * invisible until the day it matters — so it is put in front of the person once, with what * actually happened to it. */ setMade({ email: String(j.email || email), mailed: !!j.mailed, mail: j.mail !== false }); } if (made) { return ( <> Facility
created} back={false} />
You’re signed in as {made.email}.
{!made.mail ? "No mail is configured on this server, so that address hasn’t been checked. Make sure it is right — it is where a password reset would go." : made.mailed ? "We’ve sent a note there — that is the address a password reset goes to. If it doesn’t arrive, the address is wrong: add a second admin under Settings → Users while you’re still signed in." : "We couldn’t send a note to that address. Check it is right, and add a second admin under Settings → Users while you’re still signed in — otherwise a forgotten password locks the facility out."}
A second admin under Settings is the way back in if this account is ever locked out.} label="Start" onSubmit={() => window.location.replace("/m/signed-in?new=1")} busy={false} /> ); } return ( <> Create
account} />
{(c) => ( { setName(e.target.value); setErr(""); }} /> )} {(c) => ( { setEmail(e.target.value); setErr(""); }} /> )} {(c) => ( { setFacility(e.target.value); setErr(""); }} /> )} {plansLive && (
Plan
)} setShow(!show)} />}> {(c) => ( { setPw(e.target.value); setErr(""); }} onKeyDown={(e) => { if (e.key === "Enter") submit(); }} /> )} {turnstileOn() && }
Already have one? Sign in} label="Create account" onSubmit={submit} busy={busy} /> ); }