diff --git a/.vscode/settings.json b/.vscode/settings.json index 97d5d1948..82aa3c1a3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,7 +1,7 @@ { "typescript.tsdk": "node_modules/typescript/lib", "editor.codeActionsOnSave": { - "source.fixAll.eslint": true + "source.fixAll.eslint": "explicit" }, "eslint.validate": ["typescript", "typescriptreact", "javascript", "javascriptreact"], "javascript.preferences.importModuleSpecifier": "non-relative", diff --git a/apps/marketing/src/components/(marketing)/widget.tsx b/apps/marketing/src/components/(marketing)/widget.tsx index 7fd4aaa49..24dff7235 100644 --- a/apps/marketing/src/components/(marketing)/widget.tsx +++ b/apps/marketing/src/components/(marketing)/widget.tsx @@ -1,6 +1,7 @@ 'use client'; -import { HTMLAttributes, KeyboardEvent, useMemo, useState } from 'react'; +import { useMemo, useState } from 'react'; +import type { HTMLAttributes, KeyboardEvent } from 'react'; import { zodResolver } from '@hookform/resolvers/zod'; import { AnimatePresence, motion } from 'framer-motion'; diff --git a/apps/web/src/app/(signing)/sign/[token]/form.tsx b/apps/web/src/app/(signing)/sign/[token]/form.tsx index 29cd77995..1db419b58 100644 --- a/apps/web/src/app/(signing)/sign/[token]/form.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/form.tsx @@ -4,8 +4,10 @@ import { useMemo, useState } from 'react'; import { useRouter } from 'next/navigation'; +import { zodResolver } from '@hookform/resolvers/zod'; import { useSession } from 'next-auth/react'; import { useForm } from 'react-hook-form'; +import { z } from 'zod'; import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics'; import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields'; @@ -28,6 +30,19 @@ export type SigningFormProps = { fields: Field[]; }; +const ZSigningpadSchema = z.union([ + z.object({ + signatureDataUrl: z.string().min(1), + signatureText: z.null().or(z.string().max(0)), + }), + z.object({ + signatureDataUrl: z.null().or(z.string().max(0)), + signatureText: z.string().trim().min(1), + }), +]); + +export type TSigningpadSchema = z.infer; + export const SigningForm = ({ document, recipient, fields }: SigningFormProps) => { const router = useRouter(); const analytics = useAnalytics(); @@ -40,9 +55,22 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) = trpc.recipient.completeDocumentWithToken.useMutation(); const { + register, handleSubmit, + setValue, + watch, formState: { isSubmitting }, - } = useForm(); + } = useForm({ + mode: 'onChange', + defaultValues: { + signatureDataUrl: signature || null, + signatureText: '', + }, + resolver: zodResolver(ZSigningpadSchema), + }); + + const signatureDataUrl = watch('signatureDataUrl'); + const signatureText = watch('signatureText'); const uninsertedFields = useMemo(() => { return sortFieldsByPosition(fields.filter((field) => !field.inserted)); @@ -118,15 +146,69 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) =
- - - { - setSignature(value); - }} - /> + + +
+ {!signatureText && signature && ( + { + setSignature(value); + }} + /> + )} + + {signatureText && ( +

+ {signatureText} +

+ )} +
+ +
e.stopPropagation()} + > + { + if (e.target.value !== '') { + setValue('signatureDataUrl', null); + } + + setValue('signatureText', e.target.value); + }, + + onBlur: (e) => { + if (e.target.value === '') { + return setValue('signatureText', ''); + } + + setSignature(e.target.value.trimStart()); + }, + })} + /> + + {/*
+ +
*/} +
diff --git a/apps/web/src/app/(signing)/sign/[token]/sign-dialog.tsx b/apps/web/src/app/(signing)/sign/[token]/sign-dialog.tsx index 0ce750a39..8dd9fc6e8 100644 --- a/apps/web/src/app/(signing)/sign/[token]/sign-dialog.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/sign-dialog.tsx @@ -1,6 +1,6 @@ import { useState } from 'react'; -import { Document, Field } from '@documenso/prisma/client'; +import type { Document, Field } from '@documenso/prisma/client'; import { Button } from '@documenso/ui/primitives/button'; import { Dialog, diff --git a/apps/web/src/app/(signing)/sign/[token]/signature-field.tsx b/apps/web/src/app/(signing)/sign/[token]/signature-field.tsx index ec3e45fe5..6c2c8d6bd 100644 --- a/apps/web/src/app/(signing)/sign/[token]/signature-field.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/signature-field.tsx @@ -86,7 +86,7 @@ export const SignatureField = ({ field, recipient }: SignatureFieldProps) => { token: recipient.token, fieldId: field.id, value, - isBase64: true, + isBase64: typeof value === 'string' && value.startsWith('data:image/png;base64,'), }); if (source === 'local' && !providedSignature) { diff --git a/apps/web/src/app/(signing)/sign/[token]/signing-field-container.tsx b/apps/web/src/app/(signing)/sign/[token]/signing-field-container.tsx index 046e5b3df..485384a3e 100644 --- a/apps/web/src/app/(signing)/sign/[token]/signing-field-container.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/signing-field-container.tsx @@ -2,7 +2,7 @@ import React from 'react'; -import { FieldWithSignature } from '@documenso/prisma/types/field-with-signature'; +import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature'; import { FieldRootContainer } from '@documenso/ui/components/field/field'; export type SignatureFieldProps = { diff --git a/packages/lib/constants/pdf.ts b/packages/lib/constants/pdf.ts index eba72ab56..3afd58ea0 100644 --- a/packages/lib/constants/pdf.ts +++ b/packages/lib/constants/pdf.ts @@ -1,7 +1,7 @@ import { APP_BASE_URL } from './app'; export const DEFAULT_STANDARD_FONT_SIZE = 15; -export const DEFAULT_HANDWRITING_FONT_SIZE = 50; +export const DEFAULT_HANDWRITING_FONT_SIZE = 30; export const MIN_STANDARD_FONT_SIZE = 8; export const MIN_HANDWRITING_FONT_SIZE = 20; diff --git a/packages/lib/server-only/document/seal-document.ts b/packages/lib/server-only/document/seal-document.ts index 5fa4b1a00..a48785fca 100644 --- a/packages/lib/server-only/document/seal-document.ts +++ b/packages/lib/server-only/document/seal-document.ts @@ -69,6 +69,8 @@ export const sealDocument = async ({ documentId, sendEmail = true }: SealDocumen const doc = await PDFDocument.load(pdfData); + console.log('Fields to insert into PDF: ', fields); + for (const field of fields) { await insertFieldInPDF(doc, field); } diff --git a/packages/lib/server-only/pdf/insert-field-in-pdf.ts b/packages/lib/server-only/pdf/insert-field-in-pdf.ts index dde46ba6b..53ebbaf6c 100644 --- a/packages/lib/server-only/pdf/insert-field-in-pdf.ts +++ b/packages/lib/server-only/pdf/insert-field-in-pdf.ts @@ -46,8 +46,12 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu await pdf.embedFont(fontCaveat); } + const CUSTOM_TEXT = field.customText || field.Signature?.typedSignature || ''; + const isInsertingImage = - isSignatureField && typeof field.Signature?.signatureImageAsBase64 === 'string'; + isSignatureField && + typeof field.Signature?.signatureImageAsBase64 === 'string' && + field.Signature?.signatureImageAsBase64.startsWith('data:image/png;base64,'); if (isSignatureField && isInsertingImage) { const image = await pdf.embedPng(field.Signature?.signatureImageAsBase64 ?? ''); @@ -73,13 +77,13 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu height: imageHeight, }); } else { - let textWidth = font.widthOfTextAtSize(field.customText, fontSize); + let textWidth = font.widthOfTextAtSize(CUSTOM_TEXT, fontSize); const textHeight = font.heightAtSize(fontSize); const scalingFactor = Math.min(fieldWidth / textWidth, fieldHeight / textHeight, 1); fontSize = Math.max(Math.min(fontSize * scalingFactor, maxFontSize), minFontSize); - textWidth = font.widthOfTextAtSize(field.customText, fontSize); + textWidth = font.widthOfTextAtSize(CUSTOM_TEXT, fontSize); const textX = fieldX + (fieldWidth - textWidth) / 2; let textY = fieldY + (fieldHeight - textHeight) / 2; @@ -87,7 +91,7 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu // Invert the Y axis since PDFs use a bottom-left coordinate system textY = pageHeight - textY - textHeight; - page.drawText(field.customText, { + page.drawText(CUSTOM_TEXT, { x: textX, y: textY, size: fontSize, diff --git a/packages/ui/primitives/signature-pad/signature-pad.tsx b/packages/ui/primitives/signature-pad/signature-pad.tsx index 3497418d7..d11a6d81c 100644 --- a/packages/ui/primitives/signature-pad/signature-pad.tsx +++ b/packages/ui/primitives/signature-pad/signature-pad.tsx @@ -15,12 +15,14 @@ const DPI = 2; export type SignaturePadProps = Omit, 'onChange'> & { onChange?: (_signatureDataUrl: string | null) => void; containerClassName?: string; + clearSignatureClassName?: string; }; export const SignaturePad = ({ className, containerClassName, defaultValue, + clearSignatureClassName, onChange, ...props }: SignaturePadProps) => { @@ -217,7 +219,7 @@ export const SignaturePad = ({ {...props} /> -
+