import { useEffect, useRef, useState } from "react"; import { Link, useNavigate, useSearchParams } from "react-router-dom"; import type { Organization } from "fhir/r4"; import { Heading } from "../ui/heading"; import { InputControl } from "../ui/input-control"; import { DismissibleAlert } from "../ui/dismissible-alert"; import { useAuthServiceContext } from "../../services/AuthService"; import { environment } from "../../environments/environment"; import { AKASHIC_SUPPORT_ADDRESS } from "../../config/support"; import PracticeDoorCard from "../../pages/LoginPage/PracticeDoorCard"; import { claimOwnerAccept, previewOwnerAccept, resendOwnerAccept, signInWithOwnerSecret, type OwnerAcceptPreview, } from "../../services/ownerAccept"; /** * Where the owner email's one button lands (Spencer: one Accept that claims the practice and signs the owner * in). Opening the page only reads, so a mail scanner that fetches the link claims nothing. The press records * the owner statement shown here, claims the practice and signs in with no code. A link that is spent or has * run out sends a fresh one to the same address. */ type Phase = | { kind: "loading" } | { kind: "unreachable" } | { kind: "ready"; preview: OwnerAcceptPreview } | { kind: "signing-in" } | { kind: "accepted-sign-in"; email: string } | { kind: "sent"; email?: string }; const CLOSED_COPY: Record = { expired: { title: "This link has run out", body: "Accept links work for 48 hours. We can send a new one to the same address.", resend: true }, spent: { title: "This link has been used", body: "Each Accept link works once. If the practice is not open to you yet, we can send a new one to the same address.", resend: true }, superseded: { title: "A newer link was sent", body: "Only the newest Accept link works. Use the latest email, or we can send another to the same address.", resend: true }, accepted: { title: "This practice already has its owner", body: "Ownership has been accepted. Sign in to open the practice.", resend: false }, invalid: { title: "This link does not work", body: "It may be incomplete, or the contractor may have named a different owner since it was sent. Ask the contractor that set the practice up to send it again.", resend: false }, }; // A stranger's link is reissued only by support, who read the claim's eligibility again first. const STRANGER_CLOSED_COPY: Record = { expired: { title: "This link has run out", body: "Accept links work for 48 hours." }, spent: { title: "This link has been used", body: "Each Accept link works once." }, superseded: { title: "A newer link was sent", body: "Only the newest Accept link works. Use the latest email." }, }; const AskSupport = () => (

For a new link, ask Akashic support at {AKASHIC_SUPPORT_ADDRESS}.

); const ContractorsPortalNote = ({ agency }: { agency?: string }) => (

{agency ? `${agency} set this practice up` : "This practice was set up by a contractor"} in the{" "} Akashic contractors portal, where web designers and other contractors manage the practices they build. It is their side, not yours: you run the practice here, and as its owner nobody else, the contractor included, can remove you.

); export const OwnerAcceptLanding = () => { const [params] = useSearchParams(); const token = params.get("t") ?? ""; return ; }; const OwnerAcceptPage = ({ token }: { token: string }) => { const navigate = useNavigate(); const { login, admitSignIn, setLoggedInAs, wpOrganization } = useAuthServiceContext(); const [phase, setPhase] = useState({ kind: "loading" }); const [busy, setBusy] = useState(false); const [error, setError] = useState(); const [canResend, setCanResend] = useState(false); const [firstName, setFirstName] = useState(""); const [lastName, setLastName] = useState(""); const asked = useRef(false); useEffect(() => { if (asked.current) return; asked.current = true; void previewOwnerAccept(token).then((preview) => setPhase(preview ? { kind: "ready", preview } : { kind: "unreachable" })); }, [token]); const preview = phase.kind === "ready" ? phase.preview : undefined; const door: Organization | null = wpOrganization?.current?.id && wpOrganization.current.id === preview?.organizationId ? (wpOrganization.current as Organization) : preview?.organizationName ? { resourceType: "Organization", name: preview.organizationName } : null; const resend = async () => { setBusy(true); setError(undefined); const sent = await resendOwnerAccept(token); setBusy(false); if (sent) setPhase({ kind: "sent", email: preview?.ownerEmail }); else setError("We could not send a new link just now. Please try again."); }; const accept = async () => { // Finishing an admitted acceptance needs no statement: the one it was admitted with stands. if (!preview || (preview.state === "open" && !preview.attestation)) return; setBusy(true); setError(undefined); setCanResend(false); const claim = await claimOwnerAccept( token, preview.attestation?.version ?? "", preview.state === "open" && preview.needsName ? { firstName: firstName.trim(), lastName: lastName.trim() } : undefined, ); if (claim.kind === "closed") { setBusy(false); setPhase({ kind: "ready", preview: { ...preview, state: claim.state } }); return; } if (claim.kind === "refused" || claim.kind === "failed") { setBusy(false); setError(claim.message ?? "We could not accept the practice just now. Please try again."); setCanResend(claim.kind === "failed" && claim.canResend); return; } if (!claim.secret) { setBusy(false); setPhase({ kind: "accepted-sign-in", email: claim.email }); return; } setPhase({ kind: "signing-in" }); try { setLoggedInAs(undefined); // Admitted before the secret is answered, at the staff client it mints on; only this admission may commit it. const ticket = await admitSignIn(undefined, environment.cognitoPractitionerClient); const tokens = await signInWithOwnerSecret(claim.email, claim.secret, claim.organizationId); if (!tokens) throw new Error("no tokens"); await login(tokens, ticket); navigate("/dashboard/home", { replace: true }); } catch { // The practice is accepted either way; only the automatic sign-in did not finish. setBusy(false); setPhase({ kind: "accepted-sign-in", email: claim.email }); } }; const stranger = preview?.purpose === "stranger-claim"; const strangerClosed = stranger && preview ? STRANGER_CLOSED_COPY[preview.state] : undefined; const closed = preview && !strangerClosed && preview.state !== "open" && preview.state !== "accepting" ? CLOSED_COPY[preview.state] : undefined; const nameMissing = Boolean(preview?.needsName) && (!firstName.trim() || !lastName.trim()); return (
{error && {error}} {phase.kind === "loading" &&

Opening your invitation...

} {phase.kind === "unreachable" && ( <> We could not open this invitation

Check your connection and open the link from your email again.

)} {phase.kind === "signing-in" &&

Accepted. Opening your practice...

} {phase.kind === "accepted-sign-in" && ( <> You are the owner

The practice is yours. Sign in with {phase.email} to open it.

Sign in )} {phase.kind === "sent" && ( <> A new link is on its way

We sent it to {phase.email ?? "the address the contractor named"}. Open that email and press Accept.

)} {closed && ( <> {closed.title}

{closed.body}

{closed.resend && ( )} {!closed.resend && Go to the staff sign-in} )} {strangerClosed && ( <> {strangerClosed.title}

{strangerClosed.body}

)} {preview?.state === "accepting" && ( <> Your acceptance is still being finished

You accepted {preview.organizationName ?? "this practice"}, but Akashic has not finished making you its owner, so it is not open to you yet. Finish with this same link; nothing is accepted twice.

)} {preview?.state === "open" && preview.attestation && ( <> Accept {preview.organizationName ?? "this practice"} {stranger ? (

Akashic support approved your claim to own {preview.organizationName ?? "this practice"} {preview.ownerEmail ? <> as {preview.ownerEmail} : null}, after everyone with authority there was told and nobody objected. The owner is the one person nobody else can remove from the practice's account.

) : (

You were named the owner of {preview.organizationName ?? "this practice"} {preview.ownerEmail ? <> as {preview.ownerEmail} : null}. The owner is the one person nobody else can remove from the practice's account.

)}

By accepting you confirm:

{preview.attestation.text}

{preview.needsName && ( <>

This address has no Akashic account yet, so tell us your name for it.

setFirstName(v ?? "")} /> setLastName(v ?? "")} /> )} {canResend && ( )} {!stranger && } )}
); }; export default OwnerAcceptLanding;