🚧 place signatures and text signatures

This commit is contained in:
Timur Ercan
2023-02-20 14:49:17 +01:00
parent 11eb557dbb
commit 44884ae648
10 changed files with 891 additions and 47 deletions

View File

@ -30,7 +30,7 @@ export default function PDFSigner(props: any) {
const signature = {
fieldId: dialogField.id,
type: dialogResult.type,
name: dialogResult.name,
typedSignature: dialogResult.typedSignature,
signatureImage: dialogResult.signatureImage,
};
@ -72,8 +72,6 @@ export default function PDFSigner(props: any) {
error: "Could not sign :/",
}
);
// goto signing done page
}
return (

View File

@ -57,7 +57,9 @@ export default function ReadOnlyField(props: FieldPropsType) {
hidden={!field?.signature}
className="font-qwigley text-5xl m-auto w-auto flex-row-reverse font-medium text-center"
>
{field?.signature?.type === "type" ? field?.signature.name : ""}
{field?.signature?.type === "type"
? field?.signature.typedSignature
: ""}
{field?.signature?.type === "draw" ? (
<img className="w-50 h-20" src={field?.signature?.signatureImage} />
) : (

View File

@ -6,8 +6,9 @@ import {
PencilIcon,
TrashIcon,
} from "@heroicons/react/24/outline";
import { Fragment, useState } from "react";
import { Fragment, useEffect, useState } from "react";
import SignatureCanvas from "react-signature-canvas";
import { localStorage } from "@documenso/lib";
const tabs = [
{ name: "Type", icon: LanguageIcon, current: true },
@ -16,10 +17,13 @@ const tabs = [
export default function SignatureDialog(props: any) {
const [currentTab, setCurrentTab] = useState(tabs[0]);
const [typedName, setTypedName] = useState("");
const [typedSignature, setTypedSignature] = useState("");
const [signatureEmpty, setSignatureEmpty] = useState(true);
let signCanvasRef: any | undefined;
let signCanvas: any | undefined;
useEffect(() => {
setTypedSignature(localStorage.getItem("typedSignature") || "");
});
return (
<>
@ -82,13 +86,16 @@ export default function SignatureDialog(props: any) {
</div>
{isCurrentTab("Type") ? (
<div>
<div className="my-8">
<div className="my-8 border-b border-gray-300 mb-3">
<input
value={typedName}
value={typedSignature}
onChange={(e) => {
setTypedName(e.target.value);
setTypedSignature(e.target.value);
}}
className="font-qw leading-none h-10 align-bottom font-qwigley mt-14 text-center block border-b w-full border-gray-300 focus:border-neon focus:ring-neon text-2xl"
className={classNames(
typedSignature ? "font-qwigley text-4xl" : "",
"leading-none h-10 align-bottom mt-14 text-center block w-full focus:border-neon focus:ring-neon text-2xl"
)}
placeholder="Kindly type your name"
/>
</div>
@ -101,11 +108,15 @@ export default function SignatureDialog(props: any) {
</Button>
<Button
className="ml-3"
disabled={!typedName}
disabled={!typedSignature}
onClick={() => {
localStorage.setItem(
"typedSignature",
typedSignature
);
props.onClose({
type: "type",
name: typedName,
typedSignature: typedSignature,
});
}}
>
@ -120,7 +131,7 @@ export default function SignatureDialog(props: any) {
<div className="">
<SignatureCanvas
ref={(ref) => {
signCanvas = ref;
signCanvasRef = ref;
}}
canvasProps={{
className:
@ -128,15 +139,15 @@ export default function SignatureDialog(props: any) {
}}
clearOnResize={true}
onEnd={() => {
setSignatureEmpty(signCanvas?.isEmpty());
setSignatureEmpty(signCanvasRef?.isEmpty());
}}
/>
<IconButton
className="block float-left"
icon={TrashIcon}
onClick={() => {
signCanvas?.clear();
setSignatureEmpty(signCanvas?.isEmpty());
signCanvasRef?.clear();
setSignatureEmpty(signCanvasRef?.isEmpty());
}}
></IconButton>
<div className="mt-10 float-right">
@ -152,7 +163,7 @@ export default function SignatureDialog(props: any) {
props.onClose({
type: "draw",
signatureImage:
signCanvas.toDataURL("image/png"),
signCanvasRef.toDataURL("image/png"),
});
}}
disabled={signatureEmpty}

View File

@ -45,6 +45,7 @@
"sass": "^1.57.1",
"short-uuid": "^4.2.2",
"string-to-color": "^2.2.2",
"text2png": "^2.3.0",
"typescript": "4.8.4"
},
"devDependencies": {

View File

@ -9,10 +9,12 @@ import { SigningStatus, DocumentStatus } from "@prisma/client";
import { getDocument } from "@documenso/lib/query";
import { Document as PrismaDocument } from "@prisma/client";
import { insertImageInPDF, insertTextInPDF } from "@documenso/pdf";
const text2png = require("text2png");
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
const existingUser = await getUserFromToken(req, res);
const { token: recipientToken } = req.query;
const { signatures: signatures }: { signatures: any[] } = req.body;
if (!recipientToken) {
return res.status(401).send("Missing recipient token.");
@ -33,8 +35,18 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
);
if (!document) res.status(404).end(`No document found.`);
// todo save signatures from body to db for later use
// todo insert if not exits
signatures.forEach(async (signature) => {
await prisma.signature.create({
data: {
recipientId: recipient.id,
fieldId: signature.fieldId,
signatureImageAsBase64: signature.signatureImage
? signature.signatureImage
: text2png(signature.typedSignature).toString("base64"),
},
});
});
await prisma.recipient.update({
where: {
@ -52,26 +64,41 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
},
});
await prisma.document.update({
where: {
id: recipient.documentId,
},
data: {
status: DocumentStatus.COMPLETED,
},
});
if (unsignedRecipients.length === 0) {
// todo if everybody signed insert images and create signature
await prisma.document.update({
where: {
id: recipient.documentId,
},
data: {
status: DocumentStatus.COMPLETED,
},
const signedFields = await prisma.field.findMany({
where: { documentId: document.id },
include: { Signature: true },
});
// todo rename .document to documentImageAsBase64 or sth. like that
let documentWithSignatureImages = document.document;
let signaturesInserted = 0;
signedFields.forEach(async (item) => {
if (!item.Signature) {
documentWithSignatureImages = document.document;
throw new Error("Invalid Signature in Field");
}
documentWithSignatureImages = await insertImageInPDF(
documentWithSignatureImages,
item.Signature ? item.Signature?.signatureImageAsBase64 : "",
item.positionX,
item.positionY,
item.page
);
signaturesInserted++;
if (signaturesInserted == signedFields.length) {
await prisma.document.update({
where: {
id: recipient.documentId,
},
data: {
status: DocumentStatus.COMPLETED,
document: documentWithSignatureImages,
},
});
}
// todo send notifications
});
// todo send notifications
}
return res.status(200).end();

777
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +1,4 @@
export { coloredConsole } from "./coloredConsole";
export { default as classNames } from "./classNames";
export { NEXT_PUBLIC_WEBAPP_URL } from "./constants";
export { localStorage } from "./webstorage";

View File

@ -0,0 +1,24 @@
// TODO: In case of an embed if localStorage is not available(third party), use localStorage of parent(first party) that contains the iframe.
export const localStorage = {
getItem(key: string) {
try {
// eslint-disable-next-line @calcom/eslint/avoid-web-storage
return window.localStorage.getItem(key);
} catch (e) {
// In case storage is restricted. Possible reasons
// 1. Third Party Context in Chrome Incognito mode.
return null;
}
},
setItem(key: string, value: string) {
try {
// eslint-disable-next-line @calcom/eslint/avoid-web-storage
window.localStorage.setItem(key, value);
} catch (e) {
// In case storage is restricted. Possible reasons
// 1. Third Party Context in Chrome Incognito mode.
// 2. Storage limit reached
return;
}
},
};

View File

@ -1,4 +1,4 @@
import { degrees, PDFDocument, PDFImage, rgb, StandardFonts } from "pdf-lib";
import { PDFDocument } from "pdf-lib";
export async function insertImageInPDF(
pdfAsBase64: string,
@ -12,12 +12,13 @@ export async function insertImageInPDF(
const pages = pdfDoc.getPages();
const pdfPage = pages[page];
const pngImage = await pdfDoc.embedPng(image);
const drawSize = { width: 213, height: 50 };
pdfPage.drawImage(pngImage, {
x: positionX,
y: positionY,
width: pngImage.width,
height: pngImage.height,
x: pdfPage.getWidth() - positionX - drawSize.width,
y: pdfPage.getHeight() - positionY - drawSize.height,
width: drawSize.width,
height: drawSize.height,
});
const pdfAsUint8Array = await pdfDoc.save();

View File

@ -1,4 +1,4 @@
import { degrees, PDFDocument, rgb, StandardFonts } from "pdf-lib";
import { PDFDocument, rgb, StandardFonts } from "pdf-lib";
export async function insertTextInPDF(
pdfAsBase64: string,
@ -13,10 +13,12 @@ export async function insertTextInPDF(
const helveticaFont = await pdfDoc.embedFont(StandardFonts.Helvetica);
const pages = pdfDoc.getPages();
const firstPage = pages[page];
firstPage.drawText(text, {
x: positionX,
y: positionY,
const pdfPage = pages[page];
const lineHeightEsimate = 25;
pdfPage.drawText(text, {
x: pdfPage.getWidth() - positionX,
y: pdfPage.getHeight() - positionY - lineHeightEsimate,
size: 25,
font: helveticaFont,
color: rgb(0, 0, 0),