diff --git a/apps/web/src/app/(recipient)/d/[token]/sign-direct-template.tsx b/apps/web/src/app/(recipient)/d/[token]/sign-direct-template.tsx index a0d92e595..cda99c458 100644 --- a/apps/web/src/app/(recipient)/d/[token]/sign-direct-template.tsx +++ b/apps/web/src/app/(recipient)/d/[token]/sign-direct-template.tsx @@ -90,7 +90,7 @@ export const SignDirectTemplateForm = ({ const tempField: DirectTemplateLocalField = { ...field, - customText: value.value, + customText: value.value ?? '', inserted: true, signedValue: value, }; @@ -101,8 +101,8 @@ export const SignDirectTemplateForm = ({ created: new Date(), recipientId: 1, fieldId: 1, - signatureImageAsBase64: value.value.startsWith('data:') ? value.value : null, - typedSignature: value.value.startsWith('data:') ? null : value.value, + signatureImageAsBase64: value.value?.startsWith('data:') ? value.value : null, + typedSignature: value.value && !value.value.startsWith('data:') ? value.value : null, } satisfies Signature; } diff --git a/apps/web/src/app/embed/direct/[[...url]]/client.tsx b/apps/web/src/app/embed/direct/[[...url]]/client.tsx index dbefcc084..7f446cdf7 100644 --- a/apps/web/src/app/embed/direct/[[...url]]/client.tsx +++ b/apps/web/src/app/embed/direct/[[...url]]/client.tsx @@ -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 { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer'; 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 type { DocumentMeta, Recipient, Signature, TemplateMeta } 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(() => fields); const [pendingFields, _completedFields] = [ - localFields.filter((field) => !field.inserted), + localFields.filter((field) => isFieldUnsignedAndRequired(field)), localFields.filter((field) => field.inserted), ]; @@ -112,7 +116,7 @@ export const EmbedDirectTemplateClientPage = ({ const newField: DirectTemplateLocalField = structuredClone({ ...field, - customText: payload.value, + customText: payload.value ?? '', inserted: true, signedValue: payload, }); @@ -123,8 +127,10 @@ export const EmbedDirectTemplateClientPage = ({ created: new Date(), recipientId: 1, fieldId: 1, - signatureImageAsBase64: payload.value.startsWith('data:') ? payload.value : null, - typedSignature: payload.value.startsWith('data:') ? null : payload.value, + signatureImageAsBase64: + payload.value && payload.value.startsWith('data:') ? payload.value : null, + typedSignature: + payload.value && !payload.value.startsWith('data:') ? payload.value : null, } satisfies Signature; } @@ -182,7 +188,7 @@ export const EmbedDirectTemplateClientPage = ({ }; const onNextFieldClick = () => { - validateFieldsInserted(localFields); + validateFieldsInserted(pendingFields); setShowPendingFieldTooltip(true); setIsExpanded(false); @@ -194,7 +200,7 @@ export const EmbedDirectTemplateClientPage = ({ return; } - const valid = validateFieldsInserted(localFields); + const valid = validateFieldsInserted(pendingFields); if (!valid) { setShowPendingFieldTooltip(true); @@ -207,12 +213,6 @@ export const EmbedDirectTemplateClientPage = ({ directTemplateExternalId = decodeURIComponent(directTemplateExternalId); } - localFields.forEach((field) => { - if (!field.signedValue) { - throw new Error('Invalid configuration'); - } - }); - const { documentId, token: documentToken, @@ -223,13 +223,11 @@ export const EmbedDirectTemplateClientPage = ({ directRecipientName: fullName, directRecipientEmail: email, templateUpdatedAt: updatedAt, - signedFieldValues: localFields.map((field) => { - if (!field.signedValue) { - throw new Error('Invalid configuration'); - } - - return field.signedValue; - }), + signedFieldValues: localFields + .filter((field) => { + return field.signedValue && (isRequiredField(field) || field.inserted); + }) + .map((field) => field.signedValue!), }); if (window.parent) { diff --git a/apps/web/src/app/embed/sign/[[...url]]/client.tsx b/apps/web/src/app/embed/sign/[[...url]]/client.tsx index e5a556077..d60511909 100644 --- a/apps/web/src/app/embed/sign/[[...url]]/client.tsx +++ b/apps/web/src/app/embed/sign/[[...url]]/client.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useEffect, useId, useLayoutEffect, useState } from 'react'; +import { useEffect, useId, useLayoutEffect, useMemo, useState } from 'react'; import { Trans, msg } from '@lingui/macro'; 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 { 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 type { DocumentMeta, TemplateMeta } from '@documenso/prisma/client'; import { @@ -102,19 +103,26 @@ export const EmbedSignDocumentClientPage = ({ const [throttledOnCompleteClick, isThrottled] = useThrottleFn(() => void onCompleteClick(), 500); 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), ]; const { mutateAsync: completeDocumentWithToken, isPending: isSubmitting } = trpc.recipient.completeDocumentWithToken.useMutation(); + const fieldsRequiringValidation = useMemo( + () => fields.filter(isFieldUnsignedAndRequired), + [fields], + ); + const hasSignatureField = fields.some((field) => field.type === FieldType.SIGNATURE); const assistantSignersId = useId(); const onNextFieldClick = () => { - validateFieldsInserted(fields); + validateFieldsInserted(fieldsRequiringValidation); setShowPendingFieldTooltip(true); setIsExpanded(false); @@ -126,7 +134,7 @@ export const EmbedSignDocumentClientPage = ({ return; } - const valid = validateFieldsInserted(fields); + const valid = validateFieldsInserted(fieldsRequiringValidation); if (!valid) { setShowPendingFieldTooltip(true); diff --git a/packages/lib/server-only/template/create-document-from-direct-template.ts b/packages/lib/server-only/template/create-document-from-direct-template.ts index b3e187688..9c74d0d53 100644 --- a/packages/lib/server-only/template/create-document-from-direct-template.ts +++ b/packages/lib/server-only/template/create-document-from-direct-template.ts @@ -37,6 +37,7 @@ import { mapDocumentToWebhookDocumentPayload, } from '../../types/webhook-payload'; import type { ApiRequestMetadata } from '../../universal/extract-request-metadata'; +import { isRequiredField } from '../../utils/advanced-fields-helpers'; import type { CreateDocumentAuditLogDataResponse } from '../../utils/document-audit-logs'; import { createDocumentAuditLogData } from '../../utils/document-audit-logs'; import { @@ -175,20 +176,28 @@ export const createDocumentFromDirectTemplate = async ({ const metaSigningOrder = template.templateMeta?.signingOrder || DocumentSigningOrder.PARALLEL; // 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( - directTemplateRecipient.fields.map(async (templateField) => { + fieldsToProcess.map(async (templateField) => { const signedFieldValue = signedFieldValues.find( (value) => value.fieldId === templateField.id, ); - if (!signedFieldValue) { + if (isRequiredField(templateField) && !signedFieldValue) { throw new AppError(AppErrorCode.INVALID_BODY, { message: 'Invalid, missing or changed fields', }); } if (templateField.type === FieldType.NAME && directRecipientName === undefined) { - directRecipientName === signedFieldValue.value; + directRecipientName === signedFieldValue?.value; } const derivedRecipientActionAuth = await validateFieldAuth({ @@ -199,9 +208,18 @@ export const createDocumentFromDirectTemplate = async ({ }, field: templateField, userId: user?.id, - authOptions: signedFieldValue.authOptions, + authOptions: signedFieldValue?.authOptions, }); + if (!signedFieldValue) { + return { + templateField, + customText: '', + derivedRecipientActionAuth, + signature: null, + }; + } + const { value, isBase64 } = signedFieldValue; const isSignatureField = @@ -379,7 +397,7 @@ export const createDocumentFromDirectTemplate = async ({ positionY: templateField.positionY, width: templateField.width, height: templateField.height, - customText, + customText: customText ?? '', inserted: true, fieldMeta: templateField.fieldMeta || Prisma.JsonNull, })), diff --git a/packages/trpc/server/field-router/router.ts b/packages/trpc/server/field-router/router.ts index df17aef88..0ab4d6b21 100644 --- a/packages/trpc/server/field-router/router.ts +++ b/packages/trpc/server/field-router/router.ts @@ -452,7 +452,7 @@ export const fieldRouter = router({ return await signFieldWithToken({ token, fieldId, - value, + value: value ?? '', isBase64, userId: ctx.user?.id, authOptions, diff --git a/packages/trpc/server/field-router/schema.ts b/packages/trpc/server/field-router/schema.ts index 373d4a693..9297d6234 100644 --- a/packages/trpc/server/field-router/schema.ts +++ b/packages/trpc/server/field-router/schema.ts @@ -153,7 +153,7 @@ export const ZSetFieldsForTemplateResponseSchema = z.object({ export const ZSignFieldWithTokenMutationSchema = z.object({ token: z.string(), fieldId: z.number(), - value: z.string().trim(), + value: z.string().trim().optional(), isBase64: z.boolean().optional(), authOptions: ZRecipientActionAuthSchema.optional(), });