chore: merge main, resolve biome formatting conflicts

Merge origin/main into feat/external-2fa-codes. Resolve formatting
conflicts caused by biome rollout; preserve both feature streams:
PR's external 2FA token + signing-session 2FA proof additions plus
main's RateLimit/RecipientExpired/signingReminders/date-auto-insert.

In complete-document-with-token.ts, drop the duplicate early
field-fetching block introduced when main moved that logic later
with date auto-insert support; keep the EXTERNAL_TWO_FACTOR_AUTH
check using derivedRecipientActionAuth.
This commit is contained in:
ephraimduncan
2026-05-12 11:46:11 +00:00
parent 9194884fbe
commit 138d663c25
1959 changed files with 93488 additions and 47038 deletions
@@ -1,14 +1,13 @@
import { Trans } from '@lingui/react/macro';
import { ReadStatus, RecipientRole, SigningStatus } from '@prisma/client';
import { ArrowRight, EyeIcon, XCircle } from 'lucide-react';
import { match } from 'ts-pattern';
import type { DocumentAndSender } from '@documenso/lib/server-only/document/get-document-by-token';
import type { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
import { Badge } from '@documenso/ui/primitives/badge';
import { Button } from '@documenso/ui/primitives/button';
import { Progress } from '@documenso/ui/primitives/progress';
import { Trans } from '@lingui/react/macro';
import { ReadStatus, RecipientRole, SigningStatus } from '@prisma/client';
import { ArrowRight, EyeIcon, XCircle } from 'lucide-react';
import { match } from 'ts-pattern';
// Get the return type from getRecipientByToken
type RecipientWithFields = Awaited<ReturnType<typeof getRecipientByToken>>;
@@ -25,9 +24,7 @@ interface MultiSignDocumentListProps {
export function MultiSignDocumentList({ envelopes, onDocumentSelect }: MultiSignDocumentListProps) {
// Calculate progress
const completedDocuments = envelopes.filter(
(envelope) => envelope.recipient.signingStatus === SigningStatus.SIGNED,
);
const completedDocuments = envelopes.filter((envelope) => envelope.recipient.signingStatus === SigningStatus.SIGNED);
const totalDocuments = envelopes.length;
const progressPercentage = (completedDocuments.length / totalDocuments) * 100;
@@ -41,9 +38,7 @@ export function MultiSignDocumentList({ envelopes, onDocumentSelect }: MultiSign
const allDocumentsCompleted = completedDocuments.length === totalDocuments;
const hasAssistantOrCcRecipient = envelopes.some(
(envelope) =>
envelope.recipient.role === RecipientRole.ASSISTANT ||
envelope.recipient.role === RecipientRole.CC,
(envelope) => envelope.recipient.role === RecipientRole.ASSISTANT || envelope.recipient.role === RecipientRole.CC,
);
function handleView(doc: DocumentEnvelope['document']) {
@@ -60,27 +55,25 @@ export function MultiSignDocumentList({ envelopes, onDocumentSelect }: MultiSign
return (
<div className="mx-auto mt-16 flex w-full max-w-lg flex-col md:mt-16 md:rounded-2xl md:border md:px-8 md:py-16 md:shadow-lg">
<div className="flex items-center justify-center">
<XCircle className="text-destructive h-16 w-16 md:h-24 md:w-24" strokeWidth={1.2} />
<XCircle className="h-16 w-16 text-destructive md:h-24 md:w-24" strokeWidth={1.2} />
</div>
<h2 className="mt-12 text-xl font-bold md:text-2xl">
<h2 className="mt-12 font-bold text-xl md:text-2xl">
<Trans>It looks like we ran into an issue!</Trans>
</h2>
<p className="text-muted-foreground mt-6">
<p className="mt-6 text-muted-foreground">
<Trans>
One of the documents in the current bundle has a signing role that is not compatible
with the current signing experience.
One of the documents in the current bundle has a signing role that is not compatible with the current
signing experience.
</Trans>
</p>
<p className="text-muted-foreground mt-2">
<Trans>
Assistants and Copy roles are currently not compatible with the multi-sign experience.
</Trans>
<p className="mt-2 text-muted-foreground">
<Trans>Assistants and Copy roles are currently not compatible with the multi-sign experience.</Trans>
</p>
<p className="text-muted-foreground mt-2">
<p className="mt-2 text-muted-foreground">
<Trans>Please contact the site owner for further assistance.</Trans>
</p>
</div>
@@ -88,22 +81,22 @@ export function MultiSignDocumentList({ envelopes, onDocumentSelect }: MultiSign
}
return (
<div className="bg-background mx-auto w-full max-w-lg md:my-12 md:rounded-2xl md:border md:p-8 md:shadow-lg">
<h2 className="text-foreground mb-1 text-lg font-semibold">
<div className="mx-auto w-full max-w-lg bg-background md:my-12 md:rounded-2xl md:border md:p-8 md:shadow-lg">
<h2 className="mb-1 font-semibold text-foreground text-lg">
<Trans>Sign Documents</Trans>
</h2>
<p className="text-muted-foreground text-sm">
<Trans>
You have been requested to sign the following documents. Review each document carefully
and complete the signing process.
You have been requested to sign the following documents. Review each document carefully and complete the
signing process.
</Trans>
</p>
{/* Progress Section */}
<div className="mt-6">
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground font-medium">
<span className="font-medium text-muted-foreground">
<Trans>Progress</Trans>
</span>
<span className="text-muted-foreground">
@@ -118,13 +111,8 @@ export function MultiSignDocumentList({ envelopes, onDocumentSelect }: MultiSign
<div className="mt-6 flex flex-col gap-4">
{envelopes.map((envelope) => (
<div
key={envelope.document.id}
className="border-border flex items-center gap-4 rounded-lg border px-4 py-2"
>
<span className="text-foreground flex-1 truncate text-sm font-medium">
{envelope.document.title}
</span>
<div key={envelope.document.id} className="flex items-center gap-4 rounded-lg border border-border px-4 py-2">
<span className="flex-1 truncate font-medium text-foreground text-sm">{envelope.document.title}</span>
{match(envelope.recipient)
.with({ signingStatus: SigningStatus.SIGNED }, () => (
@@ -144,12 +132,7 @@ export function MultiSignDocumentList({ envelopes, onDocumentSelect }: MultiSign
))
.otherwise(() => null)}
<Button
className="-mr-2"
variant="outline"
size="sm"
onClick={() => handleView(envelope.document)}
>
<Button className="-mr-2" variant="outline" size="sm" onClick={() => handleView(envelope.document)}>
<EyeIcon className="mr-1 h-4 w-4" />
<Trans>View</Trans>
</Button>
@@ -1,14 +1,7 @@
import { useState } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { DocumentStatus, SigningStatus } from '@prisma/client';
import { Loader, LucideChevronDown, LucideChevronUp, X } from 'lucide-react';
import { P, match } from 'ts-pattern';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { getDocumentDataUrlForPdfViewer } from '@documenso/lib/utils/envelope-download';
import { sortFieldsByPosition } from '@documenso/lib/utils/fields';
import { isSignatureFieldType } from '@documenso/prisma/guards/is-signature-field';
import { trpc } from '@documenso/trpc/react';
import type {
@@ -22,9 +15,17 @@ import { Button } from '@documenso/ui/primitives/button';
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
import { Input } from '@documenso/ui/primitives/input';
import { Label } from '@documenso/ui/primitives/label';
import { PDFViewerLazy } from '@documenso/ui/primitives/pdf-viewer/lazy';
import { SignaturePadDialog } from '@documenso/ui/primitives/signature-pad/signature-pad-dialog';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { DocumentStatus, SigningStatus } from '@prisma/client';
import { Loader, LucideChevronDown, LucideChevronUp, X } from 'lucide-react';
import { useState } from 'react';
import { match, P } from 'ts-pattern';
import PDFViewerLazy from '~/components/general/pdf-viewer/pdf-viewer-lazy';
import { useRequiredDocumentSigningContext } from '../../general/document-signing/document-signing-provider';
import { DocumentSigningRejectDialog } from '../../general/document-signing/document-signing-reject-dialog';
@@ -35,12 +36,7 @@ interface MultiSignDocumentSigningViewProps {
recipientId: number;
onBack: () => void;
onDocumentCompleted?: (data: { token: string; documentId: number; recipientId: number }) => void;
onDocumentRejected?: (data: {
token: string;
documentId: number;
recipientId: number;
reason: string;
}) => void;
onDocumentRejected?: (data: { token: string; documentId: number; recipientId: number; reason: string }) => void;
onDocumentError?: () => void;
onDocumentReady?: () => void;
isNameLocked?: boolean;
@@ -61,8 +57,7 @@ export const MultiSignDocumentSigningView = ({
const { _ } = useLingui();
const { toast } = useToast();
const { fullName, email, signature, setFullName, setSignature } =
useRequiredDocumentSigningContext();
const { fullName, email, signature, setFullName, setSignature } = useRequiredDocumentSigningContext();
const [hasDocumentLoaded, setHasDocumentLoaded] = useState(false);
@@ -78,23 +73,19 @@ export const MultiSignDocumentSigningView = ({
);
const { mutateAsync: signFieldWithToken } = trpc.field.signFieldWithToken.useMutation();
const { mutateAsync: removeSignedFieldWithToken } =
trpc.field.removeSignedFieldWithToken.useMutation();
const { mutateAsync: removeSignedFieldWithToken } = trpc.field.removeSignedFieldWithToken.useMutation();
const { mutateAsync: completeDocumentWithToken } =
trpc.recipient.completeDocumentWithToken.useMutation();
const { mutateAsync: completeDocumentWithToken } = trpc.recipient.completeDocumentWithToken.useMutation();
const hasSignatureField = document?.fields.some((field) => isSignatureFieldType(field.type));
const [pendingFields, completedFields] = [
document?.fields.filter((field) => field.recipient.signingStatus !== SigningStatus.SIGNED) ??
[],
document?.fields.filter((field) => field.recipient.signingStatus === SigningStatus.SIGNED) ??
[],
sortFieldsByPosition(
document?.fields.filter((field) => field.recipient.signingStatus !== SigningStatus.SIGNED) ?? [],
),
document?.fields.filter((field) => field.recipient.signingStatus === SigningStatus.SIGNED) ?? [],
];
const highestPendingPageNumber = Math.max(...pendingFields.map((field) => field.page));
const uninsertedFields = document?.fields.filter((field) => !field.inserted) ?? [];
const onSignField = async (payload: TSignFieldWithTokenMutationSchema) => {
@@ -178,14 +169,14 @@ export const MultiSignDocumentSigningView = ({
};
return (
<div className="min-h-screen overflow-hidden bg-background">
<div id="document-field-portal-root" className="relative h-full w-full overflow-y-auto p-8">
<div className="min-h-screen bg-background">
<div className="relative h-full w-full p-8">
{match({ isLoading, document })
.with({ isLoading: true }, () => (
<div className="flex min-h-[400px] w-full items-center justify-center">
<div className="flex flex-col items-center gap-4">
<Loader className="h-8 w-8 animate-spin text-primary" />
<p className="text-sm text-muted-foreground">
<p className="text-muted-foreground text-sm">
<Trans>Loading document...</Trans>
</p>
</div>
@@ -193,7 +184,7 @@ export const MultiSignDocumentSigningView = ({
))
.with({ isLoading: false, document: undefined }, () => (
<div className="flex min-h-[400px] w-full items-center justify-center">
<p className="text-sm text-muted-foreground">
<p className="text-muted-foreground text-sm">
<Trans>Failed to load document</Trans>
</p>
</div>
@@ -202,7 +193,7 @@ export const MultiSignDocumentSigningView = ({
<>
<div className="mx-auto flex w-full max-w-screen-xl items-baseline justify-between">
<div className="flex items-center gap-4">
<h1 className="text-2xl font-semibold">{document.title}</h1>
<h1 className="font-semibold text-2xl">{document.title}</h1>
</div>
<Button variant="ghost" size="sm" onClick={onBack} className="p-2">
@@ -211,12 +202,8 @@ export const MultiSignDocumentSigningView = ({
</div>
{allowDocumentRejection && (
<div className="embed--Actions mb-4 mt-8 flex w-full flex-row-reverse items-baseline justify-between">
<DocumentSigningRejectDialog
documentId={document.id}
token={token}
onRejected={onRejected}
/>
<div className="embed--Actions mt-8 mb-4 flex w-full flex-row-reverse items-baseline justify-between">
<DocumentSigningRejectDialog documentId={document.id} token={token} onRejected={onRejected} />
</div>
)}
@@ -227,9 +214,15 @@ export const MultiSignDocumentSigningView = ({
})}
>
<PDFViewerLazy
envelopeItem={document.envelopeItems[0]}
token={token}
version="signed"
data={getDocumentDataUrlForPdfViewer({
envelopeId: document.envelopeId,
envelopeItemId: document.envelopeItems[0]?.id,
documentDataId: document.documentData.id,
version: 'current',
token,
presignToken: undefined,
})}
scrollParentRef="window"
onDocumentLoad={() => {
setHasDocumentLoaded(true);
onDocumentReady?.();
@@ -241,14 +234,14 @@ export const MultiSignDocumentSigningView = ({
{document.status !== DocumentStatus.COMPLETED && (
<div
key={isExpanded ? 'expanded' : 'collapsed'}
className="embed--DocumentWidgetContainer group/document-widget fixed bottom-8 left-0 z-50 h-fit max-h-[calc(100dvh-2rem)] w-full flex-shrink-0 px-6 md:sticky md:bottom-[unset] md:top-0 md:z-auto md:w-[350px] md:px-0"
className="embed--DocumentWidgetContainer group/document-widget fixed bottom-8 left-0 z-50 h-fit max-h-[calc(100dvh-2rem)] w-full flex-shrink-0 px-6 md:sticky md:top-0 md:bottom-[unset] md:z-auto md:w-[350px] md:px-0"
data-expanded={isExpanded || undefined}
>
<div className="embed--DocumentWidget flex w-full flex-col rounded-xl border border-border bg-widget px-4 py-4 md:py-6">
{/* Header */}
<div className="embed--DocumentWidgetHeader">
<div className="flex items-center justify-between gap-x-2">
<h3 className="text-xl font-semibold text-foreground md:text-2xl">
<h3 className="font-semibold text-foreground text-xl md:text-2xl">
<Trans>Sign document</Trans>
</h3>
@@ -269,11 +262,11 @@ export const MultiSignDocumentSigningView = ({
</div>
<div className="embed--DocumentWidgetContent hidden group-data-[expanded]/document-widget:block md:block">
<p className="mt-2 text-sm text-muted-foreground">
<p className="mt-2 text-muted-foreground text-sm">
<Trans>Sign the document to complete the process.</Trans>
</p>
<hr className="mb-8 mt-4 border-border" />
<hr className="mt-4 mb-8 border-border" />
</div>
{/* Form */}
@@ -301,13 +294,7 @@ export const MultiSignDocumentSigningView = ({
<Trans>Email</Trans>
</Label>
<Input
type="email"
id="email"
className="mt-2 bg-background"
value={email}
disabled
/>
<Input type="email" id="email" className="mt-2 bg-background" value={email} disabled />
</div>
{hasSignatureField && (
@@ -323,15 +310,9 @@ export const MultiSignDocumentSigningView = ({
fullName={fullName}
value={signature ?? ''}
onChange={(v) => setSignature(v ?? '')}
typedSignatureEnabled={
document.documentMeta?.typedSignatureEnabled
}
uploadSignatureEnabled={
document.documentMeta?.uploadSignatureEnabled
}
drawSignatureEnabled={
document.documentMeta?.drawSignatureEnabled
}
typedSignatureEnabled={document.documentMeta?.typedSignatureEnabled}
uploadSignatureEnabled={document.documentMeta?.uploadSignatureEnabled}
drawSignatureEnabled={document.documentMeta?.drawSignatureEnabled}
/>
</div>
)}
@@ -348,11 +329,7 @@ export const MultiSignDocumentSigningView = ({
<Trans>Next</Trans>
</Button>
) : (
<Button
className="col-span-2"
loading={isSubmitting}
onClick={onDocumentComplete}
>
<Button className="col-span-2" loading={isSubmitting} onClick={onDocumentComplete}>
<Trans>Complete</Trans>
</Button>
)}
@@ -360,41 +337,30 @@ export const MultiSignDocumentSigningView = ({
</div>
</div>
)}
</div>
{hasDocumentLoaded && (
<ElementVisible
target={`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${highestPendingPageNumber}"]`}
>
{showPendingFieldTooltip && pendingFields.length > 0 && (
<FieldToolTip
key={pendingFields[0].id}
field={pendingFields[0]}
color="warning"
>
{hasDocumentLoaded && showPendingFieldTooltip && pendingFields.length > 0 && (
<ElementVisible target={`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${pendingFields[0].page}"]`}>
<FieldToolTip key={pendingFields[0].id} field={pendingFields[0]} color="warning">
<Trans>Click to insert field</Trans>
</FieldToolTip>
)}
</ElementVisible>
)}
</ElementVisible>
)}
{/* Fields */}
{hasDocumentLoaded && (
<EmbedDocumentFields
fields={pendingFields}
metadata={document.documentMeta}
onSignField={onSignField}
onUnsignField={onUnsignField}
/>
)}
{/* Fields */}
{hasDocumentLoaded && (
<EmbedDocumentFields
fields={pendingFields}
metadata={document.documentMeta}
onSignField={onSignField}
onUnsignField={onUnsignField}
/>
)}
{/* Completed fields */}
{document.status !== DocumentStatus.COMPLETED && (
<DocumentReadOnlyFields
documentMeta={document.documentMeta ?? undefined}
fields={completedFields}
/>
)}
{/* Completed fields */}
{document.status !== DocumentStatus.COMPLETED && (
<DocumentReadOnlyFields documentMeta={document.documentMeta ?? undefined} fields={completedFields} />
)}
</div>
</>
))
.otherwise(() => null)}