fix: prevent accidental signatures (#1515)

![CleanShot 2024-12-06 at 03 30
39](https://github.com/user-attachments/assets/d47dc820-f19d-43b7-a60d-914fc9ab24b8)

![CleanShot 2024-12-06 at 03 32
34](https://github.com/user-attachments/assets/0db98735-8c91-469b-873c-adb19d0fff7b)
This commit is contained in:
Ephraim Duncan
2024-12-08 03:17:58 +00:00
committed by GitHub
parent a88ae1cc1e
commit dd162205fa
23 changed files with 443 additions and 292 deletions

View File

@ -221,7 +221,7 @@ export const DocumentPageView = async ({ params, team }: DocumentPageViewProps)
<DocumentPageViewDropdown document={documentWithRecipients} team={team} />
</div>
<p className="text-muted-foreground mt-2 px-4 text-sm ">
<p className="text-muted-foreground mt-2 px-4 text-sm">
{match(document.status)
.with(DocumentStatus.COMPLETED, () => (
<Trans>This document has been signed by all recipients</Trans>

View File

@ -1,7 +1,6 @@
import { useMemo, useState } from 'react';
import { Trans } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { DateTime } from 'luxon';
import { match } from 'ts-pattern';
@ -72,9 +71,8 @@ export const SignDirectTemplateForm = ({
template,
onSubmit,
}: SignDirectTemplateFormProps) => {
const { _ } = useLingui();
const { fullName, signature, setFullName, setSignature } = useRequiredSigningContext();
const { fullName, signature, signatureValid, setFullName, setSignature } =
useRequiredSigningContext();
const [localFields, setLocalFields] = useState<DirectTemplateLocalField[]>(directRecipientFields);
const [validateUninsertedFields, setValidateUninsertedFields] = useState(false);
@ -135,6 +133,8 @@ export const SignDirectTemplateForm = ({
);
};
const hasSignatureField = localFields.some((field) => field.type === FieldType.SIGNATURE);
const uninsertedFields = useMemo(() => {
return sortFieldsByPosition(localFields.filter((field) => !field.inserted));
}, [localFields]);
@ -147,6 +147,10 @@ export const SignDirectTemplateForm = ({
const handleSubmit = async () => {
setValidateUninsertedFields(true);
if (hasSignatureField && !signatureValid) {
return;
}
const isFieldsValid = validateFieldsInserted(localFields);
if (!isFieldsValid) {

View File

@ -12,7 +12,7 @@ import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import type { DocumentAndSender } from '@documenso/lib/server-only/document/get-document-by-token';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields';
import { type Field, type Recipient, RecipientRole } from '@documenso/prisma/client';
import { type Field, FieldType, type Recipient, RecipientRole } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react';
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
import { cn } from '@documenso/ui/lib/utils';
@ -44,7 +44,8 @@ export const SigningForm = ({
const analytics = useAnalytics();
const { data: session } = useSession();
const { fullName, signature, setFullName, setSignature } = useRequiredSigningContext();
const { fullName, signature, setFullName, setSignature, signatureValid, setSignatureValid } =
useRequiredSigningContext();
const [validateUninsertedFields, setValidateUninsertedFields] = useState(false);
@ -56,6 +57,8 @@ export const SigningForm = ({
// Keep the loading state going if successful since the redirect may take some time.
const isSubmitting = formState.isSubmitting || formState.isSubmitSuccessful;
const hasSignatureField = fields.some((field) => field.type === FieldType.SIGNATURE);
const uninsertedFields = useMemo(() => {
return sortFieldsByPosition(fields.filter((field) => !field.inserted));
}, [fields]);
@ -68,6 +71,10 @@ export const SigningForm = ({
const onFormSubmit = async () => {
setValidateUninsertedFields(true);
if (hasSignatureField && !signatureValid) {
return;
}
const isFieldsValid = validateFieldsInserted(fields);
if (!isFieldsValid) {
@ -142,7 +149,7 @@ export const SigningForm = ({
<div className="flex flex-col gap-4 md:flex-row">
<Button
type="button"
className="dark:bg-muted dark:hover:bg-muted/80 w-full bg-black/5 hover:bg-black/10"
className="dark:bg-muted dark:hover:bg-muted/80 w-full bg-black/5 hover:bg-black/10"
variant="secondary"
size="lg"
disabled={typeof window !== 'undefined' && window.history.length <= 1}
@ -198,20 +205,33 @@ export const SigningForm = ({
className="h-44 w-full"
disabled={isSubmitting}
defaultValue={signature ?? undefined}
onValidityChange={(isValid) => {
setSignatureValid(isValid);
}}
onChange={(value) => {
setSignature(value);
if (signatureValid) {
setSignature(value);
}
}}
allowTypedSignature={document.documentMeta?.typedSignatureEnabled}
/>
</CardContent>
</Card>
{hasSignatureField && !signatureValid && (
<div className="text-destructive mt-2 text-sm">
<Trans>
Signature is too small. Please provide a more complete signature.
</Trans>
</div>
)}
</div>
</div>
<div className="flex flex-col gap-4 md:flex-row">
<Button
type="button"
className="dark:bg-muted dark:hover:bg-muted/80 w-full bg-black/5 hover:bg-black/10"
className="dark:bg-muted dark:hover:bg-muted/80 w-full bg-black/5 hover:bg-black/10"
variant="secondary"
size="lg"
disabled={typeof window !== 'undefined' && window.history.length <= 1}

View File

@ -9,6 +9,8 @@ export type SigningContextValue = {
setEmail: (_value: string) => void;
signature: string | null;
setSignature: (_value: string | null) => void;
signatureValid: boolean;
setSignatureValid: (_valid: boolean) => void;
};
const SigningContext = createContext<SigningContextValue | null>(null);
@ -43,6 +45,7 @@ export const SigningProvider = ({
const [fullName, setFullName] = useState(initialFullName || '');
const [email, setEmail] = useState(initialEmail || '');
const [signature, setSignature] = useState(initialSignature || null);
const [signatureValid, setSignatureValid] = useState(true);
useEffect(() => {
if (initialSignature) {
@ -59,6 +62,8 @@ export const SigningProvider = ({
setEmail,
signature,
setSignature,
signatureValid,
setSignatureValid,
}}
>
{children}

View File

@ -55,8 +55,12 @@ export const SignatureField = ({
const containerRef = useRef<HTMLDivElement>(null);
const [fontSize, setFontSize] = useState(2);
const { signature: providedSignature, setSignature: setProvidedSignature } =
useRequiredSigningContext();
const {
signature: providedSignature,
setSignature: setProvidedSignature,
signatureValid,
setSignatureValid,
} = useRequiredSigningContext();
const { executeActionAuthProcedure } = useRequiredDocumentAuthContext();
@ -90,7 +94,7 @@ export const SignatureField = ({
}, [field.inserted, signature?.signatureImageAsBase64]);
const onPreSign = () => {
if (!providedSignature) {
if (!providedSignature || !signatureValid) {
setShowSignatureModal(true);
return false;
}
@ -117,7 +121,7 @@ export const SignatureField = ({
try {
const value = signature || providedSignature;
if (!value) {
if (!value || (signature && !signatureValid)) {
setShowSignatureModal(true);
return;
}
@ -282,12 +286,23 @@ export const SignatureField = ({
<Trans>Signature</Trans>
</Label>
<SignaturePad
id="signature"
className="border-border mt-2 h-44 w-full rounded-md border"
onChange={(value) => setLocalSignature(value)}
allowTypedSignature={typedSignatureEnabled}
/>
<div className="border-border mt-2 rounded-md border">
<SignaturePad
id="signature"
className="h-44 w-full"
onChange={(value) => setLocalSignature(value)}
allowTypedSignature={typedSignatureEnabled}
onValidityChange={(isValid) => {
setSignatureValid(isValid);
}}
/>
</div>
{!signatureValid && (
<div className="text-destructive mt-2 text-sm">
<Trans>Signature is too small. Please provide a more complete signature.</Trans>
</div>
)}
</div>
<SigningDisclosure />
@ -307,7 +322,7 @@ export const SignatureField = ({
<Button
type="button"
className="flex-1"
disabled={!localSignature}
disabled={!localSignature || !signatureValid}
onClick={() => onDialogSignClick()}
>
<Trans>Sign</Trans>

View File

@ -67,8 +67,16 @@ export const EmbedDirectTemplateClientPage = ({
const searchParams = useSearchParams();
const { fullName, email, signature, setFullName, setEmail, setSignature } =
useRequiredSigningContext();
const {
fullName,
email,
signature,
signatureValid,
setFullName,
setEmail,
setSignature,
setSignatureValid,
} = useRequiredSigningContext();
const [hasFinishedInit, setHasFinishedInit] = useState(false);
const [hasDocumentLoaded, setHasDocumentLoaded] = useState(false);
@ -90,6 +98,8 @@ export const EmbedDirectTemplateClientPage = ({
localFields.filter((field) => field.inserted),
];
const hasSignatureField = localFields.some((field) => field.type === FieldType.SIGNATURE);
const { mutateAsync: createDocumentFromDirectTemplate, isLoading: isSubmitting } =
trpc.template.createDocumentFromDirectTemplate.useMutation();
@ -180,6 +190,10 @@ export const EmbedDirectTemplateClientPage = ({
const onCompleteClick = async () => {
try {
if (hasSignatureField && !signatureValid) {
return;
}
const valid = validateFieldsInserted(localFields);
if (!valid) {
@ -417,6 +431,9 @@ export const EmbedDirectTemplateClientPage = ({
onChange={(value) => {
setSignature(value);
}}
onValidityChange={(isValid) => {
setSignatureValid(isValid);
}}
allowTypedSignature={Boolean(
metadata &&
'typedSignatureEnabled' in metadata &&
@ -425,6 +442,14 @@ export const EmbedDirectTemplateClientPage = ({
/>
</CardContent>
</Card>
{hasSignatureField && !signatureValid && (
<div className="text-destructive mt-2 text-sm">
<Trans>
Signature is too small. Please provide a more complete signature.
</Trans>
</div>
)}
</div>
</div>
</div>

View File

@ -10,7 +10,7 @@ import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn'
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { validateFieldsInserted } from '@documenso/lib/utils/fields';
import type { DocumentMeta, Recipient, TemplateMeta } from '@documenso/prisma/client';
import { type DocumentData, type Field } from '@documenso/prisma/client';
import { type DocumentData, type Field, FieldType } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react';
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
import { Button } from '@documenso/ui/primitives/button';
@ -57,7 +57,15 @@ export const EmbedSignDocumentClientPage = ({
const { _ } = useLingui();
const { toast } = useToast();
const { fullName, email, signature, setFullName, setSignature } = useRequiredSigningContext();
const {
fullName,
email,
signature,
signatureValid,
setFullName,
setSignature,
setSignatureValid,
} = useRequiredSigningContext();
const [hasFinishedInit, setHasFinishedInit] = useState(false);
const [hasDocumentLoaded, setHasDocumentLoaded] = useState(false);
@ -79,6 +87,8 @@ export const EmbedSignDocumentClientPage = ({
const { mutateAsync: completeDocumentWithToken, isLoading: isSubmitting } =
trpc.recipient.completeDocumentWithToken.useMutation();
const hasSignatureField = fields.some((field) => field.type === FieldType.SIGNATURE);
const onNextFieldClick = () => {
validateFieldsInserted(fields);
@ -88,6 +98,10 @@ export const EmbedSignDocumentClientPage = ({
const onCompleteClick = async () => {
try {
if (hasSignatureField && !signatureValid) {
return;
}
const valid = validateFieldsInserted(fields);
if (!valid) {
@ -296,6 +310,9 @@ export const EmbedSignDocumentClientPage = ({
onChange={(value) => {
setSignature(value);
}}
onValidityChange={(isValid) => {
setSignatureValid(isValid);
}}
allowTypedSignature={Boolean(
metadata &&
'typedSignatureEnabled' in metadata &&
@ -304,6 +321,14 @@ export const EmbedSignDocumentClientPage = ({
/>
</CardContent>
</Card>
{hasSignatureField && !signatureValid && (
<div className="text-destructive mt-2 text-sm">
<Trans>
Signature is too small. Please provide a more complete signature.
</Trans>
</div>
)}
</div>
</div>
</div>
@ -318,7 +343,7 @@ export const EmbedSignDocumentClientPage = ({
) : (
<Button
className="col-start-2"
disabled={isThrottled}
disabled={isThrottled || (hasSignatureField && !signatureValid)}
loading={isSubmitting}
onClick={() => throttledOnCompleteClick()}
>