diff --git a/apps/marketing/package.json b/apps/marketing/package.json
index 52d0d5de8..907f74698 100644
--- a/apps/marketing/package.json
+++ b/apps/marketing/package.json
@@ -1,11 +1,11 @@
{
"name": "@documenso/marketing",
- "version": "1.7.2-rc.0",
+ "version": "1.7.2-rc.1",
"private": true,
"license": "AGPL-3.0",
"scripts": {
"dev": "next dev -p 3001",
- "build": "turbo run translate:extract && turbo run translate:compile && next build",
+ "build": "npm run translate:extract --prefix ../../ && turbo run translate:compile && next build",
"start": "next start -p 3001",
"lint": "next lint",
"lint:fix": "next lint --fix",
diff --git a/apps/web/package.json b/apps/web/package.json
index 235755674..314f39723 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -1,11 +1,11 @@
{
"name": "@documenso/web",
- "version": "1.7.2-rc.0",
+ "version": "1.7.2-rc.1",
"private": true,
"license": "AGPL-3.0",
"scripts": {
"dev": "next dev -p 3000",
- "build": "turbo run translate:extract && turbo run translate:compile && next build",
+ "build": "npm run translate:extract --prefix ../../ && turbo run translate:compile && next build",
"start": "next start",
"lint": "next lint",
"e2e:prepare": "next build && next start",
diff --git a/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx b/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx
index 9c37ab7b7..571ca535f 100644
--- a/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx
+++ b/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx
@@ -112,6 +112,24 @@ export const EditDocumentForm = ({
},
});
+ const { mutateAsync: updateTypedSignature } =
+ trpc.document.updateTypedSignatureSettings.useMutation({
+ ...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
+ onSuccess: (newData) => {
+ utils.document.getDocumentWithDetailsById.setData(
+ {
+ id: initialDocument.id,
+ teamId: team?.id,
+ },
+ (oldData) => ({
+ ...(oldData || initialDocument),
+ ...newData,
+ id: Number(newData.id),
+ }),
+ );
+ },
+ });
+
const { mutateAsync: addSigners } = trpc.recipient.addSigners.useMutation({
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
onSuccess: (newRecipients) => {
@@ -258,6 +276,11 @@ export const EditDocumentForm = ({
fields: data.fields,
});
+ await updateTypedSignature({
+ documentId: document.id,
+ typedSignatureEnabled: data.typedSignatureEnabled,
+ });
+
// Clear all field data from localStorage
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
@@ -387,6 +410,7 @@ export const EditDocumentForm = ({
fields={fields}
onSubmit={onAddFieldsFormSubmit}
isDocumentPdfLoaded={isDocumentPdfLoaded}
+ typedSignatureEnabled={document.documentMeta?.typedSignatureEnabled}
teamId={team?.id}
/>
diff --git a/apps/web/src/app/(dashboard)/templates/data-table-templates.tsx b/apps/web/src/app/(dashboard)/templates/data-table-templates.tsx
index 3b2a384be..0181a5ea7 100644
--- a/apps/web/src/app/(dashboard)/templates/data-table-templates.tsx
+++ b/apps/web/src/app/(dashboard)/templates/data-table-templates.tsx
@@ -144,6 +144,7 @@ export const TemplatesDataTable = ({
diff --git a/apps/web/src/app/(dashboard)/templates/template-direct-link-dialog.tsx b/apps/web/src/app/(dashboard)/templates/template-direct-link-dialog.tsx
index 2a5a92992..90cd2c5d4 100644
--- a/apps/web/src/app/(dashboard)/templates/template-direct-link-dialog.tsx
+++ b/apps/web/src/app/(dashboard)/templates/template-direct-link-dialog.tsx
@@ -434,12 +434,14 @@ export const TemplateDirectLinkDialog = ({
diff --git a/apps/web/src/app/(dashboard)/templates/use-template-dialog.tsx b/apps/web/src/app/(dashboard)/templates/use-template-dialog.tsx
index f43b91ffe..cbf5b5e4a 100644
--- a/apps/web/src/app/(dashboard)/templates/use-template-dialog.tsx
+++ b/apps/web/src/app/(dashboard)/templates/use-template-dialog.tsx
@@ -15,7 +15,9 @@ import {
} from '@documenso/lib/constants/template';
import { AppError } from '@documenso/lib/errors/app-error';
import type { Recipient } from '@documenso/prisma/client';
+import { DocumentSigningOrder } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react';
+import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import { Checkbox } from '@documenso/ui/primitives/checkbox';
import {
@@ -51,6 +53,7 @@ const ZAddRecipientsForNewDocumentSchema = z
id: z.number(),
email: z.string().email(),
name: z.string(),
+ signingOrder: z.number().optional(),
}),
),
})
@@ -86,6 +89,7 @@ type TAddRecipientsForNewDocumentSchema = z.infer
{
- const isRecipientEmailPlaceholder = recipient.email.match(
- TEMPLATE_RECIPIENT_EMAIL_PLACEHOLDER_REGEX,
- );
+ recipients: recipients
+ .sort((a, b) => (a.signingOrder || 0) - (b.signingOrder || 0))
+ .map((recipient) => {
+ const isRecipientEmailPlaceholder = recipient.email.match(
+ TEMPLATE_RECIPIENT_EMAIL_PLACEHOLDER_REGEX,
+ );
- const isRecipientNamePlaceholder = recipient.name.match(
- TEMPLATE_RECIPIENT_NAME_PLACEHOLDER_REGEX,
- );
+ const isRecipientNamePlaceholder = recipient.name.match(
+ TEMPLATE_RECIPIENT_NAME_PLACEHOLDER_REGEX,
+ );
- return {
- id: recipient.id,
- name: !isRecipientNamePlaceholder ? recipient.name : '',
- email: !isRecipientEmailPlaceholder ? recipient.email : '',
- };
- }),
+ return {
+ id: recipient.id,
+ name: !isRecipientNamePlaceholder ? recipient.name : '',
+ email: !isRecipientEmailPlaceholder ? recipient.email : '',
+ signingOrder: recipient.signingOrder ?? undefined,
+ };
+ }),
},
});
@@ -203,6 +211,33 @@ export function UseTemplateDialog({
{formRecipients.map((recipient, index) => (
+ {templateSigningOrder === DocumentSigningOrder.SEQUENTIAL && (
+ (
+
+
+
+
+
+
+ )}
+ />
+ )}
+
{
setSignature(value);
}}
+ allowTypedSignature={document.documentMeta?.typedSignatureEnabled}
/>
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 990dfe057..b564ea11c 100644
--- a/apps/web/src/app/(signing)/sign/[token]/signature-field.tsx
+++ b/apps/web/src/app/(signing)/sign/[token]/signature-field.tsx
@@ -31,12 +31,12 @@ import { useRequiredSigningContext } from './provider';
import { SigningFieldContainer } from './signing-field-container';
type SignatureFieldState = 'empty' | 'signed-image' | 'signed-text';
-
export type SignatureFieldProps = {
field: FieldWithSignature;
recipient: Recipient;
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise | void;
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise | void;
+ typedSignatureEnabled?: boolean;
};
export const SignatureField = ({
@@ -44,6 +44,7 @@ export const SignatureField = ({
recipient,
onSignField,
onUnsignField,
+ typedSignatureEnabled,
}: SignatureFieldProps) => {
const router = useRouter();
@@ -92,14 +93,12 @@ export const SignatureField = ({
return true;
};
-
/**
* When the user clicks the sign button in the dialog where they enter their signature.
*/
const onDialogSignClick = () => {
setShowSignatureModal(false);
setProvidedSignature(localSignature);
-
if (!localSignature) {
return;
}
@@ -109,7 +108,6 @@ export const SignatureField = ({
actionTarget: field.type,
});
};
-
const onSign = async (authOptions?: TRecipientActionAuth, signature?: string) => {
try {
const value = signature || providedSignature;
@@ -231,11 +229,11 @@ export const SignatureField = ({
id="signature"
className="border-border mt-2 h-44 w-full rounded-md border"
onChange={(value) => setLocalSignature(value)}
+ allowTypedSignature={typedSignatureEnabled}
/>
-
-
@@ -1059,8 +1093,9 @@ export const AddFieldsFormPartial = ({
{
previousStep();
remove();
diff --git a/packages/ui/primitives/document-flow/add-fields.types.ts b/packages/ui/primitives/document-flow/add-fields.types.ts
index 7309250a8..4d9c89e73 100644
--- a/packages/ui/primitives/document-flow/add-fields.types.ts
+++ b/packages/ui/primitives/document-flow/add-fields.types.ts
@@ -18,6 +18,7 @@ export const ZAddFieldsFormSchema = z.object({
fieldMeta: ZFieldMetaSchema,
}),
),
+ typedSignatureEnabled: z.boolean(),
});
export type TAddFieldsFormSchema = z.infer;
diff --git a/packages/ui/primitives/signature-pad/signature-pad.tsx b/packages/ui/primitives/signature-pad/signature-pad.tsx
index 6859d21ec..82e6b52b3 100644
--- a/packages/ui/primitives/signature-pad/signature-pad.tsx
+++ b/packages/ui/primitives/signature-pad/signature-pad.tsx
@@ -3,12 +3,15 @@
import type { HTMLAttributes, MouseEvent, PointerEvent, TouchEvent } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
+import { Caveat } from 'next/font/google';
+
import { Trans } from '@lingui/macro';
import { Undo2 } from 'lucide-react';
import type { StrokeOptions } from 'perfect-freehand';
import { getStroke } from 'perfect-freehand';
import { unsafe_useEffectOnce } from '@documenso/lib/client-only/hooks/use-effect-once';
+import { Input } from '@documenso/ui/primitives/input';
import {
Select,
SelectContent,
@@ -21,12 +24,20 @@ import { cn } from '../../lib/utils';
import { getSvgPathFromStroke } from './helper';
import { Point } from './point';
+const fontCaveat = Caveat({
+ weight: ['500'],
+ subsets: ['latin'],
+ display: 'swap',
+ variable: '--font-caveat',
+});
+
const DPI = 2;
export type SignaturePadProps = Omit, 'onChange'> & {
onChange?: (_signatureDataUrl: string | null) => void;
containerClassName?: string;
disabled?: boolean;
+ allowTypedSignature?: boolean;
};
export const SignaturePad = ({
@@ -35,6 +46,7 @@ export const SignaturePad = ({
defaultValue,
onChange,
disabled = false,
+ allowTypedSignature,
...props
}: SignaturePadProps) => {
const $el = useRef(null);
@@ -44,6 +56,7 @@ export const SignaturePad = ({
const [lines, setLines] = useState([]);
const [currentLine, setCurrentLine] = useState([]);
const [selectedColor, setSelectedColor] = useState('black');
+ const [typedSignature, setTypedSignature] = useState('');
const perfectFreehandOptions = useMemo(() => {
const size = $el.current ? Math.min($el.current.height, $el.current.width) * 0.03 : 10;
@@ -181,34 +194,107 @@ export const SignaturePad = ({
onChange?.(null);
+ setTypedSignature('');
setLines([]);
setCurrentLine([]);
};
+ const renderTypedSignature = () => {
+ if ($el.current && typedSignature) {
+ const ctx = $el.current.getContext('2d');
+
+ if (ctx) {
+ const canvasWidth = $el.current.width;
+ const canvasHeight = $el.current.height;
+
+ ctx.clearRect(0, 0, canvasWidth, canvasHeight);
+ ctx.textAlign = 'center';
+ ctx.textBaseline = 'middle';
+ ctx.fillStyle = selectedColor;
+
+ // Calculate the desired width (25ch)
+ const desiredWidth = canvasWidth * 0.85; // 85% of canvas width
+
+ // Start with a base font size
+ let fontSize = 18;
+ ctx.font = `${fontSize}px ${fontCaveat.style.fontFamily}`;
+
+ // Measure 10 characters and calculate scale factor
+ const characterWidth = ctx.measureText('m'.repeat(10)).width;
+ const scaleFactor = desiredWidth / characterWidth;
+
+ // Apply scale factor to font size
+ fontSize = fontSize * scaleFactor;
+
+ // Adjust font size if it exceeds canvas width
+ ctx.font = `${fontSize}px ${fontCaveat.style.fontFamily}`;
+
+ const textWidth = ctx.measureText(typedSignature).width;
+
+ if (textWidth > desiredWidth) {
+ fontSize = fontSize * (desiredWidth / textWidth);
+ }
+
+ // Set final font and render text
+ ctx.font = `${fontSize}px ${fontCaveat.style.fontFamily}`;
+ ctx.fillText(typedSignature, canvasWidth / 2, canvasHeight / 2);
+ }
+ }
+ };
+
+ const handleTypedSignatureChange = (event: React.ChangeEvent) => {
+ const newValue = event.target.value;
+ setTypedSignature(newValue);
+
+ if (newValue.trim() !== '') {
+ onChange?.($el.current?.toDataURL() || null);
+ } else {
+ onChange?.(null);
+ }
+ };
+
+ useEffect(() => {
+ if (typedSignature.trim() !== '') {
+ renderTypedSignature();
+ onChange?.($el.current?.toDataURL() || null);
+ } else {
+ onClearClick();
+ }
+ }, [typedSignature, selectedColor]);
+
const onUndoClick = () => {
- if (lines.length === 0) {
+ if (lines.length === 0 && typedSignature.length === 0) {
return;
}
- const newLines = lines.slice(0, -1);
- setLines(newLines);
+ if (typedSignature.length > 0) {
+ const newTypedSignature = typedSignature.slice(0, -1);
+ setTypedSignature(newTypedSignature);
+ // You might want to call onChange here as well
+ // onChange?.(newTypedSignature);
+ } else {
+ const newLines = lines.slice(0, -1);
+ setLines(newLines);
- // Clear the canvas
- if ($el.current) {
- const ctx = $el.current.getContext('2d');
- const { width, height } = $el.current;
- ctx?.clearRect(0, 0, width, height);
+ // Clear and redraw the canvas
+ if ($el.current) {
+ const ctx = $el.current.getContext('2d');
+ const { width, height } = $el.current;
+ ctx?.clearRect(0, 0, width, height);
- if (typeof defaultValue === 'string' && $imageData.current) {
- ctx?.putImageData($imageData.current, 0, 0);
+ if (typeof defaultValue === 'string' && $imageData.current) {
+ ctx?.putImageData($imageData.current, 0, 0);
+ }
+
+ newLines.forEach((line) => {
+ const pathData = new Path2D(
+ getSvgPathFromStroke(getStroke(line, perfectFreehandOptions)),
+ );
+ ctx?.fill(pathData);
+ });
+
+ onChange?.($el.current.toDataURL());
}
-
- newLines.forEach((line) => {
- const pathData = new Path2D(getSvgPathFromStroke(getStroke(line, perfectFreehandOptions)));
- ctx?.fill(pathData);
- });
-
- onChange?.($el.current.toDataURL());
}
};
@@ -263,6 +349,21 @@ export const SignaturePad = ({
{...props}
/>
+ {allowTypedSignature && (
+ 0 || typedSignature.length > 0,
+ })}
+ >
+
+
+ )}
+
- {lines.length > 0 && (
+ {(lines.length > 0 || typedSignature.length > 0) && (
onUndoClick()}
+ onClick={onUndoClick}
>
Undo
diff --git a/turbo.json b/turbo.json
index 0004b30d1..753c11911 100644
--- a/turbo.json
+++ b/turbo.json
@@ -34,9 +34,6 @@
"dependsOn": ["^build"],
"cache": false
},
- "translate:extract": {
- "cache": false
- },
"translate:compile": {
"cache": false
}