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