mirror of
https://github.com/documenso/documenso.git
synced 2026-08-25 07:42:28 +10:00
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:
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user