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

View File

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

View File

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

View File

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

View File

@ -9,10 +9,12 @@ import { SigningStatus, DocumentStatus } from "@prisma/client";
import { getDocument } from "@documenso/lib/query"; import { getDocument } from "@documenso/lib/query";
import { Document as PrismaDocument } from "@prisma/client"; import { Document as PrismaDocument } from "@prisma/client";
import { insertImageInPDF, insertTextInPDF } from "@documenso/pdf"; import { insertImageInPDF, insertTextInPDF } from "@documenso/pdf";
const text2png = require("text2png");
async function postHandler(req: NextApiRequest, res: NextApiResponse) { async function postHandler(req: NextApiRequest, res: NextApiResponse) {
const existingUser = await getUserFromToken(req, res); const existingUser = await getUserFromToken(req, res);
const { token: recipientToken } = req.query; const { token: recipientToken } = req.query;
const { signatures: signatures }: { signatures: any[] } = req.body;
if (!recipientToken) { if (!recipientToken) {
return res.status(401).send("Missing recipient token."); 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.`); if (!document) res.status(404).end(`No document found.`);
// todo insert if not exits
// todo save signatures from body to db for later use 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({ await prisma.recipient.update({
where: { 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) { if (unsignedRecipients.length === 0) {
// todo if everybody signed insert images and create signature // todo if everybody signed insert images and create signature
await prisma.document.update({ const signedFields = await prisma.field.findMany({
where: { where: { documentId: document.id },
id: recipient.documentId, include: { Signature: true },
}, });
data: { // todo rename .document to documentImageAsBase64 or sth. like that
status: DocumentStatus.COMPLETED, 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(); 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 { coloredConsole } from "./coloredConsole";
export { default as classNames } from "./classNames"; export { default as classNames } from "./classNames";
export { NEXT_PUBLIC_WEBAPP_URL } from "./constants"; 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( export async function insertImageInPDF(
pdfAsBase64: string, pdfAsBase64: string,
@ -12,12 +12,13 @@ export async function insertImageInPDF(
const pages = pdfDoc.getPages(); const pages = pdfDoc.getPages();
const pdfPage = pages[page]; const pdfPage = pages[page];
const pngImage = await pdfDoc.embedPng(image); const pngImage = await pdfDoc.embedPng(image);
const drawSize = { width: 213, height: 50 };
pdfPage.drawImage(pngImage, { pdfPage.drawImage(pngImage, {
x: positionX, x: pdfPage.getWidth() - positionX - drawSize.width,
y: positionY, y: pdfPage.getHeight() - positionY - drawSize.height,
width: pngImage.width, width: drawSize.width,
height: pngImage.height, height: drawSize.height,
}); });
const pdfAsUint8Array = await pdfDoc.save(); 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( export async function insertTextInPDF(
pdfAsBase64: string, pdfAsBase64: string,
@ -13,10 +13,12 @@ export async function insertTextInPDF(
const helveticaFont = await pdfDoc.embedFont(StandardFonts.Helvetica); const helveticaFont = await pdfDoc.embedFont(StandardFonts.Helvetica);
const pages = pdfDoc.getPages(); const pages = pdfDoc.getPages();
const firstPage = pages[page]; const pdfPage = pages[page];
firstPage.drawText(text, { const lineHeightEsimate = 25;
x: positionX,
y: positionY, pdfPage.drawText(text, {
x: pdfPage.getWidth() - positionX,
y: pdfPage.getHeight() - positionY - lineHeightEsimate,
size: 25, size: 25,
font: helveticaFont, font: helveticaFont,
color: rgb(0, 0, 0), color: rgb(0, 0, 0),