mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 00:03:33 +10:00
🚧 place signatures and text signatures
This commit is contained in:
@ -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 (
|
||||
|
||||
@ -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} />
|
||||
) : (
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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
777
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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";
|
||||
|
||||
24
packages/lib/webstorage.ts
Normal file
24
packages/lib/webstorage.ts
Normal 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;
|
||||
}
|
||||
},
|
||||
};
|
||||
@ -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();
|
||||
|
||||
@ -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),
|
||||
|
||||
Reference in New Issue
Block a user