import { useCallback, useEffect, useRef, useState } from "react"; import { useOrganizationServiceContext, type SinchFaxService, type SinchReceivingStatus } from "../../../services"; import { Input } from "../../../components/ui/input.tsx"; import { Alert, AlertDescription } from "../../../components/ui/alert.tsx"; import { RadioGroup, RadioGroupItem } from "../../../components/ui/radio-group.tsx"; import { relayServerFailure } from "../../../utils/fetchFailureMessage"; import { isSaveRetired, isSaveTimedOut, TIMED_OUT_MAY_HAVE_SAVED, useSaveScope, type SaveOperation } from "../../../utils/saveOperation"; import { holdFaxSetup, unresolvedFaxSetup, useUnresolvedFaxSetup, watchFaxWrite, type ReceivingAttempt } from "./unresolvedFaxSetup"; import type { OrganizationFaxSettings } from "../../../utils/organizationFaxSettings"; // - RECEIVE faxes on the practice's own Sinch number (inbound fax, stage 2). // // THE SERVER DECIDES, THIS SCREEN ASKS. PUT /R4/fax-settings `step: 'services'` reports each of the // practice's Sinch services and whether its incoming webhook is free, already this practice's, or taken // by something else; a save with `receive` runs the setup and answers active, pending, refused or failed. // Every answer but "active" is shown in the server's own words, because the server is the one that knows // what Sinch said. // // â›” A TAKEN SERVICE CANNOT BE CHOSEN. Setting its webhook would cut off whatever receives there now, and // Sinch masks what it stores, so it could never be put back. The server refuses it too; the radio being // disabled just means nobody has to be told that after pressing a button. // // No deck draws this section (Captain, 2026-10-07): it reuses the Faxing card's own label, help and // radio treatment, so it reads as the second half of the same page. const WEBHOOK_STATE: Record = { free: "Nothing receives faxes here yet.", ours: "Receiving faxes here.", taken: "Already sends incoming faxes elsewhere.", }; type Answer = | { kind: "active"; faxNumber: string } | { kind: "notice" | "problem"; message: string }; interface Props { /** The practice, right to configure and stored account this setup was opened for. */ owner: string; /** Whose unconfirmed setup writes apply: the person signed in and the practice. */ setupHolder: string; /** No account save is in flight or unconfirmed, so the stored account is the one these services were read for. */ accountSettled: boolean; /** The saved from-number. Sent back on the save, because a save without one clears it. */ fromNumber?: string; onSaved: (settings: OrganizationFaxSettings) => void; } const EARLIER_UNKNOWN = "Whether the earlier request ran is still not known."; /** What a status that does not settle an attempt does say. */ const unsettled = (status: SinchReceivingStatus | undefined, attempt: ReceivingAttempt, name: string): string => { if (!status || !["active", "pending", "none"].includes(status.attempt)) return "Where receiving stands could not be read yet. Try again in a minute."; if (status.attempt !== "none") return `A setup for ${name} on ${attempt.faxNumber} is stored for another Sinch project. ${EARLIER_UNKNOWN}`; const now = status.current ? ` Receiving is set up on ${status.current.faxNumber} now.` : ""; return `No setup for ${name} on ${attempt.faxNumber} is stored now.${now} ${EARLIER_UNKNOWN}`; }; const ReceiveFaxesSection = ({ owner, setupHolder, accountSettled, fromNumber, onSaved }: Props) => { const { listSinchFaxServices, readSinchReceiving, updateFaxSettings } = useOrganizationServiceContext(); const [services, setServices] = useState(null); const [loadError, setLoadError] = useState(null); const [serviceId, setServiceId] = useState(""); const [faxNumber, setFaxNumber] = useState(""); const [busy, setBusy] = useState(false); const [answer, setAnswer] = useState(null); // Bumped when the person signed in changes, so the list is read again for them. const [opening, setOpening] = useState(0); // Every read and enable belongs to this opening. Closing it, or another person signing in, retires them all: no // list, answer or saved account they bring back is shown or handed to the page, and nothing more is sent. const scope = useSaveScope(owner, () => { setServices(null); setLoadError(null); setServiceId(""); setFaxNumber(""); setBusy(false); setAnswer(null); setOpening((n) => n + 1); }); // Only the newest read may publish: an older list answering late never replaces a newer one. const reading = useRef(null); // An enable sent and never answered: its service may be receiving now, so it is not switched on again blind. const unconfirmed = useUnresolvedFaxSetup(setupHolder).receiving; const load = useCallback(async (): Promise => { reading.current?.retire(); const operation = scope.start(); reading.current = operation; setLoadError(null); try { const result = await operation.step(() => listSinchFaxServices(operation.transport)); const listed = result?.services ?? []; setServices(listed); return listed; } catch (err) { if (isSaveRetired(err) || !operation.belongs()) return undefined; setLoadError(isSaveTimedOut(err) ? "Your Sinch fax services could not be listed." : relayServerFailure(err, "Your Sinch fax services could not be listed.")); return undefined; } finally { operation.retire(); } }, [listSinchFaxServices, scope]); // Settling an unconfirmed enable from where receiving stands, read under the practice's fax lock. Active or pending // for exactly that attempt, in the account stored now, settles it. Anything else is the current configuration, not // proof the earlier request never ran, so the hold stays and the words say what is and is not known. const check = async () => { const attempt = unconfirmed; if (!attempt) return; const holder = setupHolder; const operation = scope.start(); try { const result = await operation.step(() => readSinchReceiving( { serviceId: attempt.serviceId, faxNumber: attempt.faxNumber }, operation.transport)); // Admitted for this opening; and only for the attempt it asked about, if that is still the one held. if (unresolvedFaxSetup(holder).receiving?.sentAt !== attempt.sentAt) return; const status = result?.receiving; if (status?.sameProject && status.attempt === "active") { holdFaxSetup(holder, { receiving: undefined }); setAnswer({ kind: "active", faxNumber: attempt.faxNumber }); void load(); return; } if (status?.sameProject && status.attempt === "pending") { holdFaxSetup(holder, { receiving: undefined }); setAnswer({ kind: "notice", message: "Receiving is not switched on yet. Save again to finish." }); return; } setAnswer({ kind: "notice", message: unsettled(status, attempt, nameOf(attempt.serviceId)) }); } catch (err) { if (isSaveRetired(err) || !operation.belongs()) return; setAnswer({ kind: "notice", message: "Where receiving stands could not be read yet. Try again in a minute." }); } finally { operation.retire(); } }; const nameOf = (id: string) => services?.find((service) => service.serviceId === id)?.name || id; // While an account save is unsettled, the stored keys may already be new ones: nothing started for the account // these services were read with is sent, and nothing new starts until the save settles. useEffect(() => { if (accountSettled) return; scope.renew(); setBusy(false); }, [accountSettled, scope]); useEffect(() => { if (accountSettled) void load(); }, [load, opening]); // eslint-disable-line react-hooks/exhaustive-deps // Settled again: a confirmed save opens a new account, which reads its own list; after a refusal the account and its // list are the same, so only a read the save cancelled is made again. const shownServices = useRef(services); shownServices.current = services; const wasSettled = useRef(accountSettled); useEffect(() => { const settledNow = accountSettled && !wasSettled.current; wasSettled.current = accountSettled; if (settledNow && shownServices.current === null) void load(); }, [accountSettled]); // eslint-disable-line react-hooks/exhaustive-deps const switchOn = async () => { const operation = scope.start(); const holder = setupHolder; const chosen = serviceId; const number = faxNumber.trim(); const write = watchFaxWrite((onSend) => updateFaxSettings({ vendorId: "sinch", ...(fromNumber ? { fromNumber } : {}), receive: { serviceId: chosen, faxNumber: number }, }, { ...operation.transport, onSend })); setBusy(true); setAnswer(null); try { const result = await operation.step(write.run, true); if (result?.settings) onSaved(result.settings as OrganizationFaxSettings); const receiving = result?.receiving; if (receiving?.status === "active") { setAnswer({ kind: "active", faxNumber: receiving.faxNumber }); void load(); } else if (receiving) { setAnswer({ kind: receiving.status === "pending" ? "notice" : "problem", message: receiving.message }); } } catch (err) { // Left with no answer: receiving may be on. Held for whoever sent it, not called a failure, not sent again blind. if (write.unconfirmed()) holdFaxSetup(holder, { receiving: { serviceId: chosen, faxNumber: number, sentAt: write.sentAt() } }); if (isSaveRetired(err) || !operation.belongs() || write.unconfirmed()) return; setAnswer({ kind: "problem", message: relayServerFailure(err, "Receiving could not be switched on.") }); } finally { if (operation.belongs()) setBusy(false); operation.retire(); } }; const ready = serviceId !== "" && faxNumber.trim() !== "" && !busy && !unconfirmed && accountSettled; return (

Receive faxes

Faxes sent to your Sinch number arrive in Referrals, held until a scan finds them safe to open.

{loadError && ( {loadError} )} {!accountSettled && (

Receiving waits for the fax account. Until it is confirmed saved, these services may belong to the account saved before.

)} {unconfirmed && ( {TIMED_OUT_MAY_HAVE_SAVED} )} {!loadError && services === null &&

Loading your Sinch fax services…

} {services !== null && services.length === 0 && (

Your Sinch project has no fax services yet. Add one in your Sinch dashboard first.

)} {services !== null && services.length > 0 && ( <>
Which Sinch fax service should receive? { setServiceId(next); setAnswer(null); }} className="gap-2"> {services.map((service) => ( ))}
{ setFaxNumber(e.target.value); setAnswer(null); }} placeholder="A number on that service, such as +12015550100" autoComplete="off" />
{answer?.kind === "active" && ( {`Receiving faxes on ${answer.faxNumber}. They arrive in Referrals, held until a scan finds them safe to open.`} )} {answer && answer.kind !== "active" && ( {answer.message} )}
)}
); }; export default ReceiveFaxesSection;