/** * SignatureRequestModal — the practice-side "Documenti firmati" modal that * dispatches a document-signature request: assign the operator who must * counter-sign (or leave the request unassigned) and how their request is * delivered (their system-defined default channel arrives preselected), * build the patient-side signer roster (up to `maxSigners`, each with their * own delivery channel and, for on-device signing, an OTP waiver), and send. * * Runtime pattern: real consuming apps mount this in production (surfaced via * `src/patterns/index.ts` and the `./patterns/signature-request` subpath). * The component owns the form state — re-seeded from the `initial*` props * every time `open` flips back to `true` — while the host owns `open`, the * option lists, and what happens on submit. Multiple on-device signers sign * sequentially in roster order — the execution side is the Signing pattern's * multi-signer flow; this modal only encodes the intents. * * Via QR is a second step of the same dialog: after a submit with a `'qr'` * signer the dialog stays open and waits, then shows one server-issued code * per QR signer once the host passes `qrCodes`. The kit never builds a link. * * Signed versions: when the host passes `signatures` (or * `signaturesLoading`) the form shows the document's past signature requests * in a table above the operator and signers, on one screen as platform's * "Documenti firmati" modal does. A host that passes neither sees the plain * form. * * Required documents mode: when the host passes `requiredItems` (or * `requiredItemsLoading`) the form asks the patient to sign every * outstanding document and anamnesis interview in one batch. Each item that * needs an operator gets its own picker in place of the single operator row * (and, with `showOperatorChannels`, that operator's channel); the roster * applies to the whole batch. A second signee follows platform's rules: * every item can carry it, the main signer signs on this device, and the * main signer is a registered signee, not the patient. * * Channel mode (`channelMode`), in every mode: `'perSigner'` (the default) * gives each signer row its own channel; `'batch'` puts one channel on the * whole roster (platform's sign mode) with one OTP waiver, and submits * `signMode`. * * Outcome states (`outcome`) replace every other view with a final screen * and a Close action; credits used up can also go back to the form. * * Channel rules: by default the operator may sign on this device only while * a patient signer does. A host with its own rules passes `resolveChannels`, * which replaces that rule. */ import { type ComponentPropsWithoutRef } from 'react'; import { type OperatorChannel, type SignatureChannel } from './channel-meta'; import { type SignatureDownloadVersion, type SignatureRecord, type SignatureShareTarget } from './signature-history'; import { type RequiredSignatureItem } from './required-documents'; import { type SignatureRequestOutcome } from './signature-outcome'; export type { OperatorChannel, SignatureChannel } from './channel-meta'; export type { SignatureDownloadVersion, SignatureLegalLevel, SignatureRecord, SignatureShareTarget, } from './signature-history'; export type { RequiredSignatureItem, RequiredSignatureItemKind, } from './required-documents'; export type { SignatureRequestOutcome } from './signature-outcome'; /** * How patient signers get their channel. `'perSigner'`: each signer row has * its own channel dropdown beside the name. `'batch'`: one channel for the * whole roster, under the signers heading, submitted as `signMode`. */ export type SignatureChannelMode = 'perSigner' | 'batch'; /** The channel choices on the form, handed to `resolveChannels`. */ export interface SignatureChannelDraft { /** The assigned operator; `null` with no operator row or on Assign later. */ operatorId: string | null; /** The operator's channel; `null` whenever `operatorId` is. */ operatorChannel: OperatorChannel | null; /** The patient-side roster, in order. Empty when the patient does not sign. */ signers: SignerRequest[]; /** * What the user just changed: `'operator'` (the operator or their * channel) or `'signers'` (a signer's channel or person, or a row added or * removed). `null` when the dialog opens, and when the modal reads the * available channels while rendering. */ changed: 'operator' | 'signers' | null; /** * Channels each draft signer has greyed out (`unavailableChannels` on * their `SignerOption`), by `signerId`. Absent when no signer has any. * The modal never applies one, whatever the rule returns. */ signerUnavailableChannels?: Record; } /** What `resolveChannels` returns: the channels to use and to offer. */ export interface SignatureChannelResolution { /** The operator's channel to use. Ignored while the draft's is `null`. */ operatorChannel: OperatorChannel | null; /** * The roster with the channels to use. Only `channel` is read, matched by * `signerId`; the roster itself never changes here. */ signers: SignerRequest[]; /** * Channels the operator's dropdown offers. Omit to offer SMS, e-mail and * on this device. WhatsApp is offered only when this list names it. */ operatorAvailableChannels?: OperatorChannel[]; /** * Channels each signer's dropdown offers, by `signerId`. Narrows that * person's own `availableChannels`; a list that leaves nothing selectable * is ignored. It cannot re-enable a channel in the person's * `unavailableChannels`: that one stays greyed out. */ signerAvailableChannels?: Record; } /** One selectable person/role for the operator or signer selects. */ export interface SignerOption { value: string; label: string; /** * Delivery channels this person can actually receive. Omit (or pass an * empty list) to offer every channel, Via QR included. Hosts gate this * from per-patient capability flags — e.g. a patient with no mobile number * drops `'sms'` and `'whatsapp'`. Only meaningful for signer options, not * operators. */ availableChannels?: SignatureChannel[]; /** * Channels to show greyed out, each with the host-translated reason shown * under it (e.g. SMS and WhatsApp with "No mobile number"), instead of * hiding them. Wins over `availableChannels`; a channel that list leaves * out stays hidden. Never selected, seeded or submitted: the row falls * back to the first channel still available, and a signer with none * shows the reasons under the row and blocks the request. Only meaningful * for signer options, not operators. * * `actionHref` adds a link under the person that fixes the reason, such * as platform's patient edit page with a redirect back. It reads "Add * mobile number" for SMS and WhatsApp, "Add e-mail address" for e-mail * and "Edit contact details" for any other channel, unless `actionLabel` * (host-translated) says otherwise; channels with the same link share * one. A `javascript:` URL is ignored. In `channelMode: 'batch'` the main * signer's links show under the channel select. */ unavailableChannels?: ReadonlyArray<{ channel: SignatureChannel; reason: string; /** Where the user fixes the reason, e.g. the person's edit page. */ actionHref?: string; /** The link's text. Defaults by channel; see above. */ actionLabel?: string; }>; /** * The delivery channel this operator's own settings resolve to — the * system-defined default, preselected when the operator is assigned and * again whenever a different operator is picked (a channel the user * changed by hand is kept until then). Falls back to `'email'` when * omitted. `'on-device'` applies only where it is offered: in the request * form, while a patient signer signs on this device (else e-mail), unless * `resolveChannels` decides. `'whatsapp'` applies only where a host rule * offers it, else e-mail. Also seeds each item's operator channel with * `showOperatorChannels`. Only meaningful for operator options, not * signers. */ defaultChannel?: OperatorChannel; /** * The host has worked out that this person is under 18; the kit does not * compute age. On the `isPatient` option it drives platform's minor * patient warning under the signers heading, which reads the roster: the * patient signing alone (add or choose a parent or guardian), or one * signee signing while a second seat is free (have the other parent or * guardian sign too). The two "add" messages carry the "Add signatory" * link when the host passes `onAddSignee`. On any other option, a roster * row with this person selected shows a general reminder under their * name. Neither blocks the request. Only meaningful for signer options, not * operators. */ isMinor?: boolean; /** * This option is the patient's own record, not one of their registered * signees (a legal guardian, a parent). Read in required documents mode * and in `channelMode: 'batch'`, as on platform. Required documents mode: * a second signee needs a registered signee as the main signer, and the * patient is never offered as a second signee. The OTP waiver (the * batch's, or the main signer's row per signer) starts on when the patient * cannot receive SMS, and is forced on when the patient is the main signer. * Batch mode: the channel defaults to SMS when the patient signs and can * receive it, and offers a channel the patient OR the main signer can * receive (so pass each person's own channels, not a combined list). Omit * it and the person counts as a registered signee. Only meaningful for * signer options, not operators. */ isPatient?: boolean; /** * Where this person's request goes, exactly as the host will send it: * `phone` for SMS and WhatsApp, `email` for e-mail. Shown only in the * recipient check (`confirmRecipients`), never in a DOM hook. Platform: * the patient's first SMS-enabled phone (prefix and number) and e-mail; a * signee's phoneNumber and email; an operator's phone and email. For * signer and operator options alike. */ contact?: { phone?: string; email?: string; }; } /** * One operator offered in the operator pickers: the single-document form's * operator select and, in required documents mode, every item's picker. * Operators keep host order. "Assign later" comes after them in the request * form, and first in required documents mode, as on platform * (OperatorSignee.js). */ export interface OperatorOption extends SignerOption { /** * Shows the operator greyed out, with `reason` under the name. It cannot * be picked, and is never seeded as the first operator. A host-seeded * value stays as it is. */ disabled?: boolean; /** * Why the operator is greyed out, host-translated (e.g. "non ha un numero * di telefono"). Shown under the name and read on the picker trigger. * Ignored unless `disabled`. */ reason?: string; /** * The operator was deleted. Hidden, unless they are a picker's current * value (e.g. an item still assigned to them); once changed they are gone * from that picker. Never picked or seeded as the first operator. */ deleted?: boolean; } /** One patient-side signer row in the submission. */ export interface SignerRequest { signerId: string; /** In `channelMode: 'batch'` every row carries the batch channel. */ channel: SignatureChannel; /** * Only meaningful when `channel` is `'on-device'`. In batch mode it is set * on the main signer (the first row) only: the batch's "Do not require OTP * from the patient" (platform `doesNotRequireOtp`). In required documents * mode the main signer always carries it on this device. */ skipOtp?: boolean; } /** Payload handed to `onRequestSignature`. */ export interface SignatureRequestSubmission { /** * `null` when the counter-signing operator is left unassigned. In required * documents mode: the first assigned operator in list order (or `null`), * for hosts that still need one operator; `operatorAssignments` is the * full picture. */ operatorId: string | null; /** * How the operator's counter-sign request is delivered. `null` while the * operator is unassigned. Defaults to the operator's own system-defined * channel unless explicitly changed in the modal. Always `null` in * required documents mode: see `operatorChannels`. */ operatorChannel: OperatorChannel | null; /** 1..N signers; on-device signers sign sequentially in this order. */ signers: SignerRequest[]; /** * `channelMode: 'batch'` only: the one channel the whole roster goes out * on (platform `signMode`), the same value as every `signers[].channel`. * Absent per signer, and when the patient does not sign. */ signMode?: SignatureChannel; /** * Required documents mode with `showOperatorChannels` only: the operator's * channel per item id, for every item with an assigned operator (items on * Assign later have none). */ operatorChannels?: Record; /** * Required documents mode only: the operator per listed item id, for every * item that `requiresOperator`; `null` means "Assign later". Absent in the * single-document form. */ operatorAssignments?: Record; /** * Required documents mode only: the ids of the items in this batch (the * ones still to sign), in list order. Absent in the single-document form. */ requiredItemIds?: string[]; } /** One server-issued signing link for a signer on the Via QR channel. */ export interface SignatureQrCode { /** The roster signer this link belongs to (`SignerRequest.signerId`). */ signerId: string; /** The signing link, encoded byte-for-byte. The kit never builds it. */ url: string; /** * When the link stops working: a `Date` or an ISO 8601 string, formatted * in the active locale and the browser's time zone. Today shows the time * alone; any other day adds the date. An unparseable value hides the line. */ expiresAt: Date | string; } export interface SignatureRequestModalProps { /** Controlled open state. Reopening re-seeds the form from `initial*`. */ open: boolean; /** * Fires when the dialog requests a state change (Esc or the * footer Close button). `false` is the dismiss signal — hosts typically * persist a "dismissed this session" flag there. */ onOpenChange: (open: boolean) => void; /** * Operators offered as the counter-signing signer, in host order ("Assign * later" follows them, or leads in required documents mode). `disabled` * shows one greyed out with its `reason`; `deleted` hides one unless it * is a picker's current value. */ operators: OperatorOption[]; /** Patient-side people/roles offered in each signer row. */ signerOptions: SignerOption[]; /** * Enables the "Assign to me" shortcut targeting this operator ("Assign all * to me" in required documents mode, which sets every item's picker). */ currentOperatorId?: string; /** Offer an "Assign later" option in the operator select. Default `true`. */ allowAssignLater?: boolean; /** * Preselected operator; `null` preselects "Assign later" (honoured only * while `allowAssignLater` — otherwise the first operator is seeded). In * required documents mode it seeds every item picker whose item has no * `initialOperatorId` of its own. */ initialOperatorId?: string | null; /** * Preselected operator delivery channel. Defaults to the seeded operator's * system-defined `defaultChannel`. `'on-device'` is honoured only while a * seeded roster signer also signs on this device, unless `resolveChannels` * is passed: then the host decides. `'whatsapp'` needs a host rule that * offers it; without one the operator's default is used. */ initialOperatorChannel?: OperatorChannel; /** * Seed the signer roster. Defaults to the first option, on-device. Each * row's `channel` is read per signer; in batch mode only the first row's * `skipOtp` is read. */ initialSigners?: SignerRequest[]; /** * Cap on roster size. Default `2`. In required documents mode the roster * is the main signer plus at most one second signee, offered only when * the second signee rules allow it. */ maxSigners?: number; /** * How signers get their channel, in the request form and in required * documents mode. `'perSigner'` (default): a channel dropdown beside each * signer's name. `'batch'`: one "Signature channel" select under the * signers heading for the whole roster, with one OTP waiver; every * `SignerRequest.channel` carries it and the submission gains `signMode`. */ channelMode?: SignatureChannelMode; /** * `channelMode: 'batch'` only: the channel to start on. Omit it for * platform's default: e-mail, or SMS when the main signer `isPatient` and * can receive SMS. A channel neither the main signer nor the patient can * receive falls back to the first one on offer. Read when the dialog * opens only: in required documents mode, changing the main signer * returns the batch to platform's default. The channels in * `initialSigners` are not read in batch mode. Ignored per signer. */ initialBatchChannel?: SignatureChannel; /** * Whether an operator must counter-sign. `false` drops the operator * section entirely and submits `operatorId: null` — a host whose document * does not require a counter-signature must not be asked for one. Default * `true`. */ requireOperator?: boolean; /** * Whether the patient side signs at all. `false` drops the signer roster * and submits an empty `signers` array — for documents only an operator * signs. Default `true`. Setting BOTH this and `requireOperator` to false * leaves nothing to request, so the primary action stays disabled. */ requirePatient?: boolean; /** * Check the recipients before anything is sent, as platform does. When * the request goes to anyone by SMS, e-mail or WhatsApp, Request * signature first shows a step listing each of them with their channel * and `contact`: the assigned operator (request form only) and every * roster signer. Send hands over the submission; Back returns to the * form. On this device and Via QR need no check, so a request with no * remote recipient is sent at once. The step stays while `busy`. A new * `error` returns to the form; the step shows `error` too, so one that * repeats unchanged is still seen. Default `false`. */ confirmRecipients?: boolean; /** Disables all inputs and shows the primary action as loading. */ busy?: boolean; /** Consumer-translated error shown above the footer (e.g. server failure). */ error?: string; /** Override the dialog title. Defaults to the `ui.*` string. */ title?: string; /** Override the dialog description. Defaults to the `ui.*` string. */ description?: string; /** * Receives the completed submission. The host closes via `onOpenChange`, * except when a signer chose `'qr'`: then the dialog moves to its QR step * and waits, so keep it open and pass `qrCodes` once the server replies * (or `error` to send the user back to the form). */ onRequestSignature: (submission: SignatureRequestSubmission) => void; /** * Server-issued signing links, one per `'qr'` signer. While non-empty the * dialog shows the QR step instead of the form, one code at a time. A QR * signer with no entry gets an error slot. Clear it when the dialog closes, * and drop a server response that arrives after the close. */ qrCodes?: readonly SignatureQrCode[]; /** * Shows the QR step's waiting state without a submit, e.g. when the host * remounts the dialog mid-request. The dialog enters this state by itself * after a submit with a `'qr'` signer; `qrCodes` arriving ends it. A set * `error` always wins and shows the form, even while this stays `true`. */ qrPending?: boolean; /** * Fires when the user presses Done on the QR step, before * `onOpenChange(false)`. Close, Esc and any other dismissal do not fire it. */ onQrDone?: () => void; /** * The document's past signature requests. Passing it (even empty) adds a * signed versions table (Date, Signature type, Actions) at the top of the * form, on the same screen, as on platform. Rows show newest first by * `createdAt`; records with the same or an unparseable date keep the host * order. An empty array shows one line saying there are no signed * versions. Omit it (and `signaturesLoading`) for the plain form. */ signatures?: readonly SignatureRecord[]; /** * Shows the signed versions table's loading state (skeleton rows). Also * adds the table while `signatures` is still undefined. */ signaturesLoading?: boolean; /** * Show each row's legal level (simple or advanced). Default `true`. Pass * `false` where the level does not apply, e.g. for German practices. */ showLegalLevel?: boolean; /** Row action "View". The button shows only when this is passed. */ onViewSignature?: (id: string) => void; /** * Row action "Download". The button shows only when this is passed. It is * a menu (full or anonymised) when the record `hasAnonymisedVersion`, * otherwise a button that asks for `'full'`. */ onDownloadSignature?: (id: string, version: SignatureDownloadVersion) => void; /** * Row action "Share", a menu of link, e-mail and WhatsApp. The button * shows only when this is passed. The host does the sharing. */ onShareSignature?: (id: string, via: SignatureShareTarget) => void; /** * The share targets for rows whose record has no `shareTargets` of its * own. Default: link, e-mail and WhatsApp. An empty list hides Share. */ signatureShareTargets?: readonly SignatureShareTarget[]; /** * Row action "Delete". The button shows only when this is passed. It asks * for confirmation first; this fires only on confirm. The host deletes and * drops the record from `signatures`. */ onDeleteSignature?: (id: string) => void; /** * The host's channel rules, in place of the built-in one (the operator may * sign on this device only while a patient signer does). Called with the * choices after every channel change, and when the dialog opens; the * modal uses the channels it returns, clamped to what is offered. Also * called while rendering, with `changed: null`, to read the available * channels, so it must be pure. Omit it to keep the built-in rule. */ resolveChannels?: (draft: SignatureChannelDraft) => SignatureChannelResolution; /** * Required documents mode: every outstanding document and anamnesis * interview for one patient, signed in one batch. Passing it (even empty) * replaces the single operator row with one picker per item that * `requiresOperator`, seeded from the item's `initialOperatorId`, else the * modal's `initialOperatorId`. Fully signed items are left out; when none * is left the dialog shows the `'allSigned'` outcome. The roster applies * to the whole batch, each signer on their own channel, or on one channel * with `channelMode: 'batch'`. A second roster row (the second signee) is * offered only when every listed item is a document with * `canCarrySecondary`, the main signer's channel (the batch's, or their * row's) is on this device and the main signer is not `isPatient`. * `signatures`, `requireOperator` and `requirePatient` do not apply. The * submission gains `operatorAssignments` and `requiredItemIds`. */ requiredItems?: readonly RequiredSignatureItem[]; /** * Shows required documents mode loading (skeleton rows). Also enters the * mode while `requiredItems` is still undefined. */ requiredItemsLoading?: boolean; /** * Required documents mode: shows each item's operator channel (SMS, * e-mail, on this device) beside its operator picker, starting on the * chosen operator's `defaultChannel` and following a new operator, and * hides the note that each operator's settings decide. The submission * gains `operatorChannels`. Default `false`. */ showOperatorChannels?: boolean; /** * A final screen in place of any other view, with a Close action: * `'linkSent'` after a request sent by link, `'allSigned'` when nothing is * left to sign, `'creditsExhausted'` when the practice has no electronic * signature documents left. The host sets and clears it; reopening does * not clear it. */ outcome?: SignatureRequestOutcome; /** * "Buy more" on the `'creditsExhausted'` outcome. The button shows only * when this is passed; the host navigates to the purchase page. */ onBuyMoreSignatures?: () => void; /** * "Cancel" on the `'creditsExhausted'` outcome (platform's Annulla). The * button shows only when this is passed. It returns the dialog to the view * under the outcome (the form) by itself, then calls this so the host * clears `outcome`. Clear it here: the modal shows the outcome again only * when the prop changes, or on reopen. */ onOutcomeCancel?: () => void; /** * A link-style "Add signatory", as on platform. The host opens its own * create-signee flow and passes the new person in `signerOptions`. It * shows in the minor patient warning when that warning asks to add a * parent or guardian (see `isMinor`), in either form. Required documents * mode also shows it under the Patient signers heading while the patient * has no registered signee (no `signerOptions` entry without * `isPatient`), unless the warning already carries it. Hidden when * omitted. */ onAddSignee?: () => void; /** * The dialog's own loading state: the title, skeleton rows for the * operator and the signers, and a disabled Request signature. Open the * dialog at once and pass this while the host fetches what the form needs * (operators, signers, signed versions), instead of a spinner of its own. * The form seeds from the `initial*` props when loading ends, as it does * on open. An `outcome` still wins. Default `false`. * Pass a dedicated "data not ready" flag, never the modal's * request-in-flight state: a `true` then `false` toggle while the dialog * is open re-seeds the form and wipes the user's choices. Use `busy` * during a request. */ loading?: boolean; /** * `channelMode: 'batch'` only: where the one channel select sits. * `'below'` (default): under the Patient signers heading, with its own * label. `'inline'`: beside the main signer's name, in the same column as * the operator's channel. Any other signer shows the same channel beside * their name, read-only, and the OTP waiver and contact links sit under * the main signer. */ batchChannelPlacement?: 'below' | 'inline'; /** * What the button beside the Patient signers heading does. `'addRow'` * (default): "Add signer" adds a roster row, up to `maxSigners`. * `'createSignee'`: "Add signatory" calls `onAddSignee`, so the host opens * its own create-signee page, as platform's button does. It shows while * `onAddSignee` is passed (and `secondSigneeSlot` is not in effect: in * request-form mode the slot overrides this prop for the heading * button), and the minor patient warning then has no * button of its own. Every signer dropdown also ends with "New * signatory", which calls `onAddSignee`. */ signerAddAction?: 'addRow' | 'createSignee'; /** * Request form only, with `maxSigners` of 2 or more: platform's * secondary signee. While one can be seated (a registered signee is * the main signer and another is registered), "Add signer" beside the * heading opens a Second signee line offering registered signees only, * never the patient; its X takes the line away again. Every signer * dropdown ends with "New signatory", which calls `onAddSignee` (the * host's create-signee page). While the patient is the main signer a * visible line says why there is no second signee. A new main signer clears the second * signee, as on platform. A second signee who cannot receive the channel * shows a message under the row and blocks the request. Replaces "Add * signer". Pass `maxSigners: 1` when the document has no second * signature field. Default `false`. */ secondSigneeSlot?: boolean; /** * Request form with an operator and patient signers: the operator signs * on this device exactly when the patient side does, as platform's backend * does. The operator's On this device is always offered, and picking it * moves the main signer (or the batch) on this device too. While the * patient side signs on this device the operator does too, right after, * and their other channels are greyed out with the reason. When the * patient side moves to a remote channel the operator returns to their * own default. Applies on top of `resolveChannels` when the rule offers * the operator On this device. Default `false`. */ linkOnDevice?: boolean; /** * Extra attributes for the portaled dialog surface (e.g. `dir`/`lang` * when the host pins a locale below ``, or a test id). */ contentProps?: ComponentPropsWithoutRef<'div'>; } export declare const SignatureRequestModal: import("react").ForwardRefExoticComponent>; //# sourceMappingURL=signature-request.d.ts.map