{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "signature-dialog", "type": "registry:block", "title": "Signature Dialog", "description": "Direct browser A1 signing dialog with saved-password retry handling, progress rows, and signed-PDF download preview.", "registryDependencies": [ "badge", "button", "checkbox", "dialog", "input", "label" ], "dependencies": [ "@signature-kit/react", "@signature-kit/i18n", "@signature-kit/pdf", "@signature-kit/cms", "@signature-kit/signatures", "@tanstack/react-form@1.33.2", "effect@4.0.0-beta.86", "lucide-react" ], "files": [ { "path": "registry/default/signature-dialog/signature-dialog.tsx", "type": "registry:component", "target": "@components/signature-kit/signature-dialog.tsx", "content": "\"use client\";\n\nimport type { A1SignerInput, A1SignerRow } from \"@signature-kit/react/config\";\nimport { useA1Signer } from \"@signature-kit/react/a1\";\nimport { usePdfObjectUrl } from \"@signature-kit/react/browser-pdf\";\nimport { cmsErrorMessages } from \"@signature-kit/cms/config\";\nimport { errorMessage } from \"@signature-kit/i18n\";\nimport {\n DEFAULT_PDF_ANCHOR_STAMP_SIZE,\n type PdfStampSize,\n type PdfTextAnchorMatcher,\n pdfErrorMessages,\n pdfTextAnchorMatchersFromProps,\n} from \"@signature-kit/pdf/config\";\nimport { SignatureKitErrorCodeValue, signatureKitErrorMessages } from \"@signature-kit/signatures\";\nimport { CheckCircle2, Download, Loader2, PenLine } from \"lucide-react\";\nimport * as React from \"react\";\nimport { Effect } from \"effect\";\nimport { useForm, useSelector } from \"@tanstack/react-form\";\n\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport { Checkbox } from \"@/components/ui/checkbox\";\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n DialogTrigger,\n} from \"@/components/ui/dialog\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { cn } from \"@/lib/utils\";\n\nexport type SignatureDialogSignedRow = Extract;\n\ntype SignatureDialogDocument = A1SignerInput[\"documents\"][number];\nexport type SignatureDialogProps = {\n readonly pfx: Uint8Array;\n readonly buildDocuments: () =>\n | ReadonlyArray\n | Promise>;\n readonly signing: A1SignerInput[\"signing\"];\n readonly stamp?: A1SignerInput[\"stamp\"];\n readonly anchorTokens?: ReadonlyArray;\n readonly anchorDigits?: ReadonlyArray;\n readonly anchorStampSize?: PdfStampSize;\n readonly onSigned: (rows: ReadonlyArray) => void | Promise;\n readonly getSavedPassword?: () => string | null;\n readonly onSavePassword?: (password: string | null) => void | Promise;\n readonly onWrongPassword?: () => void | Promise;\n readonly locale?: \"en-US\" | \"pt-BR\";\n readonly title?: string;\n readonly description?: string;\n readonly triggerLabel?: string;\n readonly submitLabel?: string;\n readonly success?: (context: {\n readonly rows: ReadonlyArray;\n readonly firstPdfUrl: string | null;\n }) => React.ReactNode;\n readonly className?: string;\n};\n\ntype SignatureDialogActionState =\n | { readonly status: \"idle\" }\n | { readonly status: \"running\" }\n | { readonly status: \"failed\"; readonly error: unknown };\n\nconst catalogs = [pdfErrorMessages, cmsErrorMessages, signatureKitErrorMessages];\nconst emptyAnchorValues: ReadonlyArray = [];\n\ntype SignatureDialogRun = {\n readonly id: number;\n readonly controller: AbortController;\n};\n\ntype SignatureDialogExecution =\n | { readonly status: \"cancelled\" }\n | { readonly status: \"needs-password\" }\n | { readonly status: \"completed\" }\n | { readonly status: \"failed\"; readonly error: unknown };\n\nconst withAnchorFallback = (\n document: SignatureDialogDocument,\n matchers: ReadonlyArray,\n stampSize: PdfStampSize,\n): SignatureDialogDocument =>\n document.anchors !== undefined || matchers.length === 0\n ? document\n : { ...document, anchors: { matchers, stampSize } };\n\nconst isWrongPasswordRow = (row: A1SignerRow): boolean =>\n row.status === \"failed\" && row.error.code === SignatureKitErrorCodeValue.wrongPassword;\n\nconst signedRows = (rows: ReadonlyArray): ReadonlyArray =>\n rows.filter((row): row is SignatureDialogSignedRow => row.status === \"signed\");\n\nexport function SignatureDialog({\n pfx,\n buildDocuments,\n signing,\n stamp,\n anchorTokens = emptyAnchorValues,\n anchorDigits = emptyAnchorValues,\n anchorStampSize,\n onSigned,\n getSavedPassword,\n onSavePassword,\n onWrongPassword,\n locale = \"en-US\",\n title = \"Sign documents\",\n description = \"Sign selected PDFs locally with your A1 certificate. The password stays in this browser.\",\n triggerLabel = \"Sign with A1\",\n submitLabel = \"Sign now\",\n success,\n className,\n}: SignatureDialogProps) {\n const signer = useA1Signer();\n const clearSigner = signer.clear;\n const [open, setOpen] = React.useState(false);\n const [needsPassword, setNeedsPassword] = React.useState(false);\n const [actionState, setActionState] = React.useState({\n status: \"idle\",\n });\n const mounted = React.useRef(true);\n const nextRunId = React.useRef(0);\n const activeRun = React.useRef(null);\n const lastSignedRows = signedRows(signer.rows);\n const firstPdfUrl = usePdfObjectUrl(lastSignedRows[0]?.signedPdf ?? null);\n const form = useForm({\n defaultValues: { password: \"\", rememberPassword: false },\n });\n const password = useSelector(form.store, (state) => state.values.password);\n const rememberPassword = useSelector(form.store, (state) => state.values.rememberPassword);\n\n React.useEffect(() => {\n mounted.current = true;\n return () => {\n mounted.current = false;\n const run = activeRun.current;\n if (run !== null) {\n run.controller.abort();\n activeRun.current = null;\n clearSigner();\n }\n };\n }, [clearSigner]);\n\n const isOwner = (run: SignatureDialogRun): boolean =>\n mounted.current && activeRun.current === run && !run.controller.signal.aborted;\n\n const cancelRun = (): boolean => {\n const run = activeRun.current;\n if (run === null) return false;\n if (mounted.current) setActionState({ status: \"idle\" });\n run.controller.abort();\n activeRun.current = null;\n clearSigner();\n return true;\n };\n const formatRowError = (row: A1SignerRow): string =>\n row.status === \"failed\" ? errorMessage(row.error, { locale, catalogs }) : \"\";\n const signerError = signer.error === null ? \"\" : errorMessage(signer.error, { locale, catalogs });\n const actionError =\n actionState.status === \"failed\" ? errorMessage(actionState.error, { locale, catalogs }) : \"\";\n const dialogError = actionError.length > 0 ? actionError : signerError;\n const actionRunning = actionState.status === \"running\";\n const actionFailed = actionState.status === \"failed\";\n\n const run = (passwordToUse: string | undefined, passwordToSave: string | null | undefined) => {\n if (!cancelRun()) clearSigner();\n const currentRun: SignatureDialogRun = {\n id: nextRunId.current + 1,\n controller: new AbortController(),\n };\n nextRunId.current = currentRun.id;\n activeRun.current = currentRun;\n setActionState({ status: \"running\" });\n\n const program = Effect.result(\n Effect.tryPromise({\n try: async (signal) => {\n if (!isOwner(currentRun) || signal.aborted) return { status: \"cancelled\" };\n\n if (passwordToSave !== undefined) {\n await Promise.resolve(onSavePassword?.(passwordToSave));\n if (!isOwner(currentRun) || signal.aborted) return { status: \"cancelled\" };\n }\n\n const resolvedPassword = passwordToUse ?? getSavedPassword?.() ?? \"\";\n if (resolvedPassword.length === 0) {\n if (!isOwner(currentRun) || signal.aborted) return { status: \"cancelled\" };\n return { status: \"needs-password\" };\n }\n\n const matchers = pdfTextAnchorMatchersFromProps(anchorTokens, anchorDigits);\n const anchorSize = anchorStampSize ?? stamp?.stampSize ?? DEFAULT_PDF_ANCHOR_STAMP_SIZE;\n const documents = (await buildDocuments()).map((document) =>\n withAnchorFallback(document, matchers, anchorSize),\n );\n if (!isOwner(currentRun) || signal.aborted) return { status: \"cancelled\" };\n\n const result = await signer.sign({\n documents,\n credentials: { pfx, password: resolvedPassword },\n signing,\n ...(stamp === undefined ? {} : { stamp }),\n });\n if (!isOwner(currentRun) || signal.aborted) return { status: \"cancelled\" };\n\n if (!result.ok) {\n if (result.error.code !== SignatureKitErrorCodeValue.wrongPassword) {\n return { status: \"completed\" };\n }\n\n const outcomes = await Promise.allSettled([\n onWrongPassword?.(),\n onSavePassword?.(null),\n ]);\n if (!isOwner(currentRun) || signal.aborted) return { status: \"cancelled\" };\n const rejectedOutcome = outcomes.find(\n (outcome): outcome is PromiseRejectedResult => outcome.status === \"rejected\",\n );\n return rejectedOutcome === undefined\n ? { status: \"needs-password\" }\n : { status: \"failed\", error: rejectedOutcome.reason };\n }\n\n if (result.rows.some(isWrongPasswordRow)) {\n const outcomes = await Promise.allSettled([\n onWrongPassword?.(),\n onSavePassword?.(null),\n ]);\n if (!isOwner(currentRun) || signal.aborted) return { status: \"cancelled\" };\n const rejectedOutcome = outcomes.find(\n (outcome): outcome is PromiseRejectedResult => outcome.status === \"rejected\",\n );\n return rejectedOutcome === undefined\n ? { status: \"needs-password\" }\n : { status: \"failed\", error: rejectedOutcome.reason };\n }\n\n const signed = signedRows(result.rows);\n if (signed.length > 0) {\n if (!isOwner(currentRun) || signal.aborted) return { status: \"cancelled\" };\n await Promise.resolve(onSigned(signed));\n if (!isOwner(currentRun) || signal.aborted) return { status: \"cancelled\" };\n }\n return { status: \"completed\" };\n },\n catch: (error) => error,\n }),\n );\n\n void Effect.runPromise(Effect.exit(program), { signal: currentRun.controller.signal }).then(\n (exit) => {\n if (!isOwner(currentRun)) return;\n activeRun.current = null;\n if (exit._tag === \"Failure\") {\n setActionState({ status: \"failed\", error: exit.cause });\n return;\n }\n\n if (exit.value._tag === \"Failure\") {\n setActionState({ status: \"failed\", error: exit.value.failure });\n return;\n }\n\n if (exit.value.success.status === \"needs-password\") setNeedsPassword(true);\n if (exit.value.success.status === \"failed\") {\n setNeedsPassword(true);\n setActionState({ status: \"failed\", error: exit.value.success.error });\n return;\n }\n setActionState({ status: \"idle\" });\n },\n () => undefined,\n );\n };\n\n const handleOpenChange = (nextOpen: boolean) => {\n if (!nextOpen) cancelRun();\n setOpen(nextOpen);\n };\n\n const start = () => {\n setNeedsPassword(false);\n run(undefined, undefined);\n };\n\n const submitPassword = (event: React.FormEvent) => {\n event.preventDefault();\n run(password, rememberPassword ? password : null);\n };\n\n return (\n \n \n \n \n {triggerLabel}\n \n \n \n \n {title}\n {description}\n \n {dialogError.length > 0 ? (\n

\n {dialogError}\n

\n ) : null}\n\n {needsPassword ? (\n
\n \n \n {(field) => (\n
\n \n field.handleChange(event.currentTarget.value)}\n />\n
\n )}\n
\n \n {(field) => (\n \n )}\n \n \n \n ) : null}\n\n
\n {signer.rows.map((row) => (\n \n
\n

{row.name}

\n {row.status === \"failed\" ? (\n

{formatRowError(row)}

\n ) : null}\n
\n \n {row.status}\n \n
\n ))}\n \n\n {!actionRunning && !actionFailed && signer.error === null && lastSignedRows.length > 0 ? (\n
\n {success?.({ rows: lastSignedRows, firstPdfUrl }) ?? (\n
\n
\n \n {lastSignedRows.length} signed document(s)\n
\n {firstPdfUrl !== null ? (\n \n ) : null}\n
\n )}\n
\n ) : null}\n\n \n \n \n
\n
\n );\n}\n" } ] }