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 93477 additions and 47027 deletions
@@ -1,16 +1,11 @@
import { useEffect, useId, useLayoutEffect, useMemo, useState } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type { DocumentMeta, EnvelopeItem } from '@prisma/client';
import { type Field, RecipientRole, SigningStatus } from '@prisma/client';
import { LucideChevronDown, LucideChevronUp } from 'lucide-react';
import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn';
import { APP_I18N_OPTIONS } from '@documenso/lib/constants/i18n';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { ZSignDocumentEmbedDataSchema } from '@documenso/lib/types/embed-document-sign-schema';
import { isFieldUnsignedAndRequired } from '@documenso/lib/utils/advanced-fields-helpers';
import { validateFieldsInserted } from '@documenso/lib/utils/fields';
import { getDocumentDataUrlForPdfViewer } from '@documenso/lib/utils/envelope-download';
import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields';
import { dynamicActivate } from '@documenso/lib/utils/i18n';
import { isSignatureFieldType } from '@documenso/prisma/guards/is-signature-field';
import type { RecipientWithFields } from '@documenso/prisma/types/recipient-with-fields';
import { trpc } from '@documenso/trpc/react';
@@ -23,15 +18,21 @@ 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 { RadioGroup, RadioGroupItem } from '@documenso/ui/primitives/radio-group';
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 type { DocumentMeta, EnvelopeItem } from '@prisma/client';
import { type Field, RecipientRole, SigningStatus } from '@prisma/client';
import { LucideChevronDown, LucideChevronUp } from 'lucide-react';
import { useEffect, useId, useLayoutEffect, useMemo, useState } from 'react';
import { BrandingLogo } from '~/components/general/branding-logo';
import PDFViewerLazy from '~/components/general/pdf-viewer/pdf-viewer-lazy';
import { injectCss } from '~/utils/css-vars';
import { ZSignDocumentEmbedDataSchema } from '../../types/embed-document-sign-schema';
import { DocumentSigningAttachmentsPopover } from '../general/document-signing/document-signing-attachments-popover';
import { useRequiredDocumentSigningContext } from '../general/document-signing/document-signing-provider';
import { DocumentSigningRecipientProvider } from '../general/document-signing/document-signing-recipient-provider';
@@ -45,7 +46,7 @@ export type EmbedSignDocumentV1ClientPageProps = {
token: string;
documentId: number;
envelopeId: string;
envelopeItems: Pick<EnvelopeItem, 'id' | 'envelopeId'>[];
envelopeItems: (Pick<EnvelopeItem, 'id' | 'envelopeId'> & { documentData: { id: string } })[];
recipient: RecipientWithFields;
fields: Field[];
completedFields: DocumentField[];
@@ -73,24 +74,21 @@ export const EmbedSignDocumentV1ClientPage = ({
const { _ } = useLingui();
const { toast } = useToast();
const { fullName, email, signature, setFullName, setSignature } =
useRequiredDocumentSigningContext();
const { fullName, email, signature, setFullName, setEmail, setSignature } = useRequiredDocumentSigningContext();
const [hasFinishedInit, setHasFinishedInit] = useState(false);
const [hasDocumentLoaded, setHasDocumentLoaded] = useState(false);
const [hasCompletedDocument, setHasCompletedDocument] = useState(isCompleted);
const [hasRejectedDocument, setHasRejectedDocument] = useState(
recipient.signingStatus === SigningStatus.REJECTED,
);
const [hasRejectedDocument, setHasRejectedDocument] = useState(recipient.signingStatus === SigningStatus.REJECTED);
const [selectedSignerId, setSelectedSignerId] = useState<number | null>(
allRecipients.length > 0 ? allRecipients[0].id : null,
);
const [isExpanded, setIsExpanded] = useState(false);
const [isNameLocked, setIsNameLocked] = useState(false);
const [isEmailLocked, setIsEmailLocked] = useState(!!email);
const [showPendingFieldTooltip, setShowPendingFieldTooltip] = useState(false);
const [_showOtherRecipientsCompletedFields, setShowOtherRecipientsCompletedFields] =
useState(false);
const [_showOtherRecipientsCompletedFields, setShowOtherRecipientsCompletedFields] = useState(false);
const [allowDocumentRejection, setAllowDocumentRejection] = useState(false);
@@ -100,21 +98,16 @@ export const EmbedSignDocumentV1ClientPage = ({
const [throttledOnCompleteClick, isThrottled] = useThrottleFn(() => void onCompleteClick(), 500);
const [pendingFields, _completedFields] = [
fields.filter(
(field) => field.recipientId === recipient.id && isFieldUnsignedAndRequired(field),
sortFieldsByPosition(
fields.filter((field) => field.recipientId === recipient.id && isFieldUnsignedAndRequired(field)),
),
fields.filter((field) => field.inserted),
];
const highestPendingPageNumber = Math.max(...pendingFields.map((field) => field.page));
const { mutateAsync: completeDocumentWithToken, isPending: isSubmitting } =
trpc.recipient.completeDocumentWithToken.useMutation();
const fieldsRequiringValidation = useMemo(
() => fields.filter(isFieldUnsignedAndRequired),
[fields],
);
const fieldsRequiringValidation = useMemo(() => fields.filter(isFieldUnsignedAndRequired), [fields]);
const hasSignatureField = fields.some((field) => isSignatureFieldType(field.type));
@@ -171,9 +164,7 @@ export const EmbedSignDocumentV1ClientPage = ({
toast({
title: _(msg`Something went wrong`),
description: _(
msg`We were unable to submit this document at this time. Please try again later.`,
),
description: _(msg`We were unable to submit this document at this time. Please try again later.`),
variant: 'destructive',
});
}
@@ -204,13 +195,19 @@ export const EmbedSignDocumentV1ClientPage = ({
try {
const data = ZSignDocumentEmbedDataSchema.parse(JSON.parse(decodeURIComponent(atob(hash))));
if (!isCompleted && data.name) {
if (!isCompleted && data.name && !fullName) {
setFullName(data.name);
}
// Since a recipient can be provided a name we can lock it without requiring
// a to be provided by the parent application, unlike direct templates.
setIsNameLocked(!!data.lockName);
if (!isCompleted && data.email && !email) {
setEmail(data.email);
setIsEmailLocked(!!data.lockEmail);
}
setAllowDocumentRejection(!!data.allowDocumentRejection);
setShowOtherRecipientsCompletedFields(!!data.showOtherRecipientsCompletedFields);
@@ -224,12 +221,19 @@ export const EmbedSignDocumentV1ClientPage = ({
cssVars: data.cssVars,
});
}
if (data.language && data.language !== APP_I18N_OPTIONS.sourceLang) {
void dynamicActivate(data.language).finally(() => {
setHasFinishedInit(true);
});
} else {
setHasFinishedInit(true);
}
} catch (err) {
console.error(err);
setHasFinishedInit(true);
}
setHasFinishedInit(true);
// !: While the two setters are stable we still want to ensure we're avoiding
// !: re-renders.
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -276,11 +280,7 @@ export const EmbedSignDocumentV1ClientPage = ({
<DocumentSigningAttachmentsPopover envelopeId={envelopeId} token={token} />
{allowDocumentRejection && (
<DocumentSigningRejectDialog
documentId={documentId}
token={token}
onRejected={onDocumentRejected}
/>
<DocumentSigningRejectDialog documentId={documentId} token={token} onRejected={onDocumentRejected} />
)}
</div>
@@ -288,9 +288,15 @@ export const EmbedSignDocumentV1ClientPage = ({
{/* Viewer */}
<div className="embed--DocumentViewer flex-1">
<PDFViewerLazy
envelopeItem={envelopeItems[0]}
token={token}
version="signed"
data={getDocumentDataUrlForPdfViewer({
envelopeId: envelopeItems[0]?.envelopeId,
envelopeItemId: envelopeItems[0]?.id,
documentDataId: envelopeItems[0]?.documentData.id,
version: 'current',
token: token,
presignToken: undefined,
})}
scrollParentRef="window"
onDocumentLoad={() => setHasDocumentLoaded(true)}
/>
</div>
@@ -298,19 +304,15 @@ export const EmbedSignDocumentV1ClientPage = ({
{/* Widget */}
<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-4 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-4 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">
{isAssistantMode ? (
<Trans>Assist with signing</Trans>
) : (
<Trans>Sign document</Trans>
)}
<h3 className="font-semibold text-foreground text-xl md:text-2xl">
{isAssistantMode ? <Trans>Assist with signing</Trans> : <Trans>Sign document</Trans>}
</h3>
{isExpanded ? (
@@ -334,9 +336,7 @@ export const EmbedSignDocumentV1ClientPage = ({
variant="default"
size="sm"
className="md:hidden"
disabled={
isThrottled || (!isAssistantMode && hasSignatureField && !signatureValid)
}
disabled={isThrottled || (!isAssistantMode && hasSignatureField && !signatureValid)}
loading={isSubmitting}
onClick={() => throttledOnCompleteClick()}
>
@@ -347,7 +347,7 @@ export const EmbedSignDocumentV1ClientPage = ({
</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">
{isAssistantMode ? (
<Trans>Help complete the document for other signers.</Trans>
) : (
@@ -355,7 +355,7 @@ export const EmbedSignDocumentV1ClientPage = ({
)}
</p>
<hr className="mb-8 mt-4 border-border" />
<hr className="mt-4 mb-8 border-border" />
</div>
{/* Form */}
@@ -396,15 +396,13 @@ export const EmbedSignDocumentV1ClientPage = ({
{r.name}
{r.id === recipient.id && (
<span className="ml-2 text-muted-foreground">
{_(msg`(You)`)}
</span>
<span className="ml-2 text-muted-foreground">{_(msg`(You)`)}</span>
)}
</Label>
<p className="text-xs text-muted-foreground">{r.email}</p>
<p className="text-muted-foreground text-xs">{r.email}</p>
</div>
</div>
<div className="text-xs leading-[inherit] text-muted-foreground">
<div className="text-muted-foreground text-xs leading-[inherit]">
{r.fields.length} {r.fields.length === 1 ? 'field' : 'fields'}
</div>
</div>
@@ -442,7 +440,8 @@ export const EmbedSignDocumentV1ClientPage = ({
id="email"
className="mt-2 bg-background"
value={email}
disabled
onChange={(e) => setEmail(e.target.value)}
disabled={isEmailLocked}
/>
</div>
@@ -491,15 +490,13 @@ export const EmbedSignDocumentV1ClientPage = ({
</div>
</div>
<ElementVisible
target={`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${highestPendingPageNumber}"]`}
>
{showPendingFieldTooltip && pendingFields.length > 0 && (
{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 */}
<EmbedDocumentFields fields={fields} metadata={metadata} />
@@ -509,8 +506,10 @@ export const EmbedSignDocumentV1ClientPage = ({
</div>
{!hidePoweredBy && (
<div className="fixed bottom-0 left-0 z-40 rounded-tr bg-primary px-2 py-1 text-xs font-medium text-primary-foreground opacity-60 hover:opacity-100">
<span>Powered by</span>
<div className="fixed bottom-0 left-0 z-40 rounded-tr bg-primary px-2 py-1 font-medium text-primary-foreground text-xs opacity-60 hover:opacity-100">
<span>
<Trans>Powered by</Trans>
</span>
<BrandingLogo className="ml-2 inline-block h-[14px]" />
</div>
)}