fix: optional fields in embeds (#1691)

Fix optional fields blocking the signature process in embeds
This commit is contained in:
Catalin Pit
2025-03-08 01:21:29 +02:00
committed by GitHub
parent 27b81bc807
commit 5dfb17c216
6 changed files with 57 additions and 33 deletions

View File

@ -90,7 +90,7 @@ export const SignDirectTemplateForm = ({
const tempField: DirectTemplateLocalField = { const tempField: DirectTemplateLocalField = {
...field, ...field,
customText: value.value, customText: value.value ?? '',
inserted: true, inserted: true,
signedValue: value, signedValue: value,
}; };
@ -101,8 +101,8 @@ export const SignDirectTemplateForm = ({
created: new Date(), created: new Date(),
recipientId: 1, recipientId: 1,
fieldId: 1, fieldId: 1,
signatureImageAsBase64: value.value.startsWith('data:') ? value.value : null, signatureImageAsBase64: value.value?.startsWith('data:') ? value.value : null,
typedSignature: value.value.startsWith('data:') ? null : value.value, typedSignature: value.value && !value.value.startsWith('data:') ? value.value : null,
} satisfies Signature; } satisfies Signature;
} }

View File

@ -13,6 +13,10 @@ import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn'
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats'; import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer'; import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones'; import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones';
import {
isFieldUnsignedAndRequired,
isRequiredField,
} from '@documenso/lib/utils/advanced-fields-helpers';
import { validateFieldsInserted } from '@documenso/lib/utils/fields'; import { validateFieldsInserted } from '@documenso/lib/utils/fields';
import type { DocumentMeta, Recipient, Signature, TemplateMeta } from '@documenso/prisma/client'; import type { DocumentMeta, Recipient, Signature, TemplateMeta } from '@documenso/prisma/client';
import { type DocumentData, type Field, FieldType } from '@documenso/prisma/client'; import { type DocumentData, type Field, FieldType } from '@documenso/prisma/client';
@ -94,7 +98,7 @@ export const EmbedDirectTemplateClientPage = ({
const [localFields, setLocalFields] = useState<DirectTemplateLocalField[]>(() => fields); const [localFields, setLocalFields] = useState<DirectTemplateLocalField[]>(() => fields);
const [pendingFields, _completedFields] = [ const [pendingFields, _completedFields] = [
localFields.filter((field) => !field.inserted), localFields.filter((field) => isFieldUnsignedAndRequired(field)),
localFields.filter((field) => field.inserted), localFields.filter((field) => field.inserted),
]; ];
@ -112,7 +116,7 @@ export const EmbedDirectTemplateClientPage = ({
const newField: DirectTemplateLocalField = structuredClone({ const newField: DirectTemplateLocalField = structuredClone({
...field, ...field,
customText: payload.value, customText: payload.value ?? '',
inserted: true, inserted: true,
signedValue: payload, signedValue: payload,
}); });
@ -123,8 +127,10 @@ export const EmbedDirectTemplateClientPage = ({
created: new Date(), created: new Date(),
recipientId: 1, recipientId: 1,
fieldId: 1, fieldId: 1,
signatureImageAsBase64: payload.value.startsWith('data:') ? payload.value : null, signatureImageAsBase64:
typedSignature: payload.value.startsWith('data:') ? null : payload.value, payload.value && payload.value.startsWith('data:') ? payload.value : null,
typedSignature:
payload.value && !payload.value.startsWith('data:') ? payload.value : null,
} satisfies Signature; } satisfies Signature;
} }
@ -182,7 +188,7 @@ export const EmbedDirectTemplateClientPage = ({
}; };
const onNextFieldClick = () => { const onNextFieldClick = () => {
validateFieldsInserted(localFields); validateFieldsInserted(pendingFields);
setShowPendingFieldTooltip(true); setShowPendingFieldTooltip(true);
setIsExpanded(false); setIsExpanded(false);
@ -194,7 +200,7 @@ export const EmbedDirectTemplateClientPage = ({
return; return;
} }
const valid = validateFieldsInserted(localFields); const valid = validateFieldsInserted(pendingFields);
if (!valid) { if (!valid) {
setShowPendingFieldTooltip(true); setShowPendingFieldTooltip(true);
@ -207,12 +213,6 @@ export const EmbedDirectTemplateClientPage = ({
directTemplateExternalId = decodeURIComponent(directTemplateExternalId); directTemplateExternalId = decodeURIComponent(directTemplateExternalId);
} }
localFields.forEach((field) => {
if (!field.signedValue) {
throw new Error('Invalid configuration');
}
});
const { const {
documentId, documentId,
token: documentToken, token: documentToken,
@ -223,13 +223,11 @@ export const EmbedDirectTemplateClientPage = ({
directRecipientName: fullName, directRecipientName: fullName,
directRecipientEmail: email, directRecipientEmail: email,
templateUpdatedAt: updatedAt, templateUpdatedAt: updatedAt,
signedFieldValues: localFields.map((field) => { signedFieldValues: localFields
if (!field.signedValue) { .filter((field) => {
throw new Error('Invalid configuration'); return field.signedValue && (isRequiredField(field) || field.inserted);
} })
.map((field) => field.signedValue!),
return field.signedValue;
}),
}); });
if (window.parent) { if (window.parent) {

View File

@ -1,6 +1,6 @@
'use client'; 'use client';
import { useEffect, useId, useLayoutEffect, useState } from 'react'; import { useEffect, useId, useLayoutEffect, useMemo, useState } from 'react';
import { Trans, msg } from '@lingui/macro'; import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
@ -8,6 +8,7 @@ import { LucideChevronDown, LucideChevronUp } from 'lucide-react';
import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn'; import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer'; import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { isFieldUnsignedAndRequired } from '@documenso/lib/utils/advanced-fields-helpers';
import { validateFieldsInserted } from '@documenso/lib/utils/fields'; import { validateFieldsInserted } from '@documenso/lib/utils/fields';
import type { DocumentMeta, TemplateMeta } from '@documenso/prisma/client'; import type { DocumentMeta, TemplateMeta } from '@documenso/prisma/client';
import { import {
@ -102,19 +103,26 @@ export const EmbedSignDocumentClientPage = ({
const [throttledOnCompleteClick, isThrottled] = useThrottleFn(() => void onCompleteClick(), 500); const [throttledOnCompleteClick, isThrottled] = useThrottleFn(() => void onCompleteClick(), 500);
const [pendingFields, _completedFields] = [ const [pendingFields, _completedFields] = [
fields.filter((field) => field.recipientId === recipient.id && !field.inserted), fields.filter(
(field) => field.recipientId === recipient.id && isFieldUnsignedAndRequired(field),
),
fields.filter((field) => field.inserted), fields.filter((field) => field.inserted),
]; ];
const { mutateAsync: completeDocumentWithToken, isPending: isSubmitting } = const { mutateAsync: completeDocumentWithToken, isPending: isSubmitting } =
trpc.recipient.completeDocumentWithToken.useMutation(); trpc.recipient.completeDocumentWithToken.useMutation();
const fieldsRequiringValidation = useMemo(
() => fields.filter(isFieldUnsignedAndRequired),
[fields],
);
const hasSignatureField = fields.some((field) => field.type === FieldType.SIGNATURE); const hasSignatureField = fields.some((field) => field.type === FieldType.SIGNATURE);
const assistantSignersId = useId(); const assistantSignersId = useId();
const onNextFieldClick = () => { const onNextFieldClick = () => {
validateFieldsInserted(fields); validateFieldsInserted(fieldsRequiringValidation);
setShowPendingFieldTooltip(true); setShowPendingFieldTooltip(true);
setIsExpanded(false); setIsExpanded(false);
@ -126,7 +134,7 @@ export const EmbedSignDocumentClientPage = ({
return; return;
} }
const valid = validateFieldsInserted(fields); const valid = validateFieldsInserted(fieldsRequiringValidation);
if (!valid) { if (!valid) {
setShowPendingFieldTooltip(true); setShowPendingFieldTooltip(true);

View File

@ -37,6 +37,7 @@ import {
mapDocumentToWebhookDocumentPayload, mapDocumentToWebhookDocumentPayload,
} from '../../types/webhook-payload'; } from '../../types/webhook-payload';
import type { ApiRequestMetadata } from '../../universal/extract-request-metadata'; import type { ApiRequestMetadata } from '../../universal/extract-request-metadata';
import { isRequiredField } from '../../utils/advanced-fields-helpers';
import type { CreateDocumentAuditLogDataResponse } from '../../utils/document-audit-logs'; import type { CreateDocumentAuditLogDataResponse } from '../../utils/document-audit-logs';
import { createDocumentAuditLogData } from '../../utils/document-audit-logs'; import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
import { import {
@ -175,20 +176,28 @@ export const createDocumentFromDirectTemplate = async ({
const metaSigningOrder = template.templateMeta?.signingOrder || DocumentSigningOrder.PARALLEL; const metaSigningOrder = template.templateMeta?.signingOrder || DocumentSigningOrder.PARALLEL;
// Associate, validate and map to a query every direct template recipient field with the provided fields. // Associate, validate and map to a query every direct template recipient field with the provided fields.
// Only process fields that are either required or have been signed by the user
const fieldsToProcess = directTemplateRecipient.fields.filter((templateField) => {
const signedFieldValue = signedFieldValues.find((value) => value.fieldId === templateField.id);
// Include if it's required or has a signed value
return isRequiredField(templateField) || signedFieldValue !== undefined;
});
const createDirectRecipientFieldArgs = await Promise.all( const createDirectRecipientFieldArgs = await Promise.all(
directTemplateRecipient.fields.map(async (templateField) => { fieldsToProcess.map(async (templateField) => {
const signedFieldValue = signedFieldValues.find( const signedFieldValue = signedFieldValues.find(
(value) => value.fieldId === templateField.id, (value) => value.fieldId === templateField.id,
); );
if (!signedFieldValue) { if (isRequiredField(templateField) && !signedFieldValue) {
throw new AppError(AppErrorCode.INVALID_BODY, { throw new AppError(AppErrorCode.INVALID_BODY, {
message: 'Invalid, missing or changed fields', message: 'Invalid, missing or changed fields',
}); });
} }
if (templateField.type === FieldType.NAME && directRecipientName === undefined) { if (templateField.type === FieldType.NAME && directRecipientName === undefined) {
directRecipientName === signedFieldValue.value; directRecipientName === signedFieldValue?.value;
} }
const derivedRecipientActionAuth = await validateFieldAuth({ const derivedRecipientActionAuth = await validateFieldAuth({
@ -199,9 +208,18 @@ export const createDocumentFromDirectTemplate = async ({
}, },
field: templateField, field: templateField,
userId: user?.id, userId: user?.id,
authOptions: signedFieldValue.authOptions, authOptions: signedFieldValue?.authOptions,
}); });
if (!signedFieldValue) {
return {
templateField,
customText: '',
derivedRecipientActionAuth,
signature: null,
};
}
const { value, isBase64 } = signedFieldValue; const { value, isBase64 } = signedFieldValue;
const isSignatureField = const isSignatureField =
@ -379,7 +397,7 @@ export const createDocumentFromDirectTemplate = async ({
positionY: templateField.positionY, positionY: templateField.positionY,
width: templateField.width, width: templateField.width,
height: templateField.height, height: templateField.height,
customText, customText: customText ?? '',
inserted: true, inserted: true,
fieldMeta: templateField.fieldMeta || Prisma.JsonNull, fieldMeta: templateField.fieldMeta || Prisma.JsonNull,
})), })),

View File

@ -452,7 +452,7 @@ export const fieldRouter = router({
return await signFieldWithToken({ return await signFieldWithToken({
token, token,
fieldId, fieldId,
value, value: value ?? '',
isBase64, isBase64,
userId: ctx.user?.id, userId: ctx.user?.id,
authOptions, authOptions,

View File

@ -153,7 +153,7 @@ export const ZSetFieldsForTemplateResponseSchema = z.object({
export const ZSignFieldWithTokenMutationSchema = z.object({ export const ZSignFieldWithTokenMutationSchema = z.object({
token: z.string(), token: z.string(),
fieldId: z.number(), fieldId: z.number(),
value: z.string().trim(), value: z.string().trim().optional(),
isBase64: z.boolean().optional(), isBase64: z.boolean().optional(),
authOptions: ZRecipientActionAuthSchema.optional(), authOptions: ZRecipientActionAuthSchema.optional(),
}); });