Merge pull request #734 from OpenSignLabs/staging

v1.7.0
This commit is contained in:
Amol
2024-05-14 01:49:55 +05:30
committed by GitHub
20 changed files with 826 additions and 263 deletions
+13 -13
View File
@@ -33,7 +33,7 @@
"react-helmet": "^6.1.0",
"react-konva": "^18.2.10",
"react-modal": "^3.16.1",
"react-pdf": "^7.7.1",
"react-pdf": "^7.7.3",
"react-quill": "^2.0.0",
"react-redux": "^9.1.0",
"react-rnd": "^10.4.10",
@@ -9883,9 +9883,9 @@
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
},
"node_modules/ejs": {
"version": "3.1.9",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz",
"integrity": "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==",
"version": "3.1.10",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
"integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==",
"dependencies": {
"jake": "^10.8.5"
},
@@ -19821,9 +19821,9 @@
}
},
"node_modules/react-pdf": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-7.7.1.tgz",
"integrity": "sha512-cbbf/PuRtGcPPw+HLhMI1f6NSka8OJgg+j/yPWTe95Owf0fK6gmVY7OXpTxMeh92O3T3K3EzfE0ML0eXPGwR5g==",
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-7.7.3.tgz",
"integrity": "sha512-a2VfDl8hiGjugpqezBTUzJHYLNB7IS7a2t7GD52xMI9xHg8LdVaTMsnM9ZlNmKadnStT/tvX5IfV0yLn+JvYmw==",
"dependencies": {
"clsx": "^2.0.0",
"dequal": "^2.0.3",
@@ -22168,9 +22168,9 @@
}
},
"node_modules/tar": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz",
"integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==",
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
"integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
"optional": true,
"dependencies": {
"chownr": "^2.0.0",
@@ -23188,9 +23188,9 @@
}
},
"node_modules/webpack-dev-middleware": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz",
"integrity": "sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA==",
"version": "5.3.4",
"resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz",
"integrity": "sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==",
"dependencies": {
"colorette": "^2.0.10",
"memfs": "^3.4.3",
+1 -1
View File
@@ -28,7 +28,7 @@
"react-helmet": "^6.1.0",
"react-konva": "^18.2.10",
"react-modal": "^3.16.1",
"react-pdf": "^7.7.1",
"react-pdf": "^7.7.3",
"react-quill": "^2.0.0",
"react-redux": "^9.1.0",
"react-rnd": "^10.4.10",
@@ -1,4 +1,4 @@
import React from "react";
import React, { useState } from "react";
import RSC from "react-scrollbars-custom";
import { Document, Page } from "react-pdf";
import { themeColor } from "../../constant/const";
@@ -9,13 +9,54 @@ function RenderAllPdfPage({
setAllPages,
setPageNumber,
setSignBtnPosition,
pageNumber
pageNumber,
signerPos,
signerObjectId
}) {
const [signPageNumber, setSignPageNumber] = useState([]);
const [bookmarkColor, setBookmarkColor] = useState("");
//set all number of pages after load pdf
function onDocumentLoad({ numPages }) {
setAllPages(numPages);
//check if signerPos array exist then save page number exist in signerPos array to show bookmark icon
if (signerPos) {
const checkUser = signerPos.filter(
(data) => data.signerObjId === signerObjectId
);
setBookmarkColor(checkUser[0]?.blockColor);
let pageNumberArr = [];
if (checkUser?.length > 0) {
checkUser[0]?.placeHolder?.map((data) => {
pageNumberArr.push(data?.pageNumber);
});
setSignPageNumber(pageNumberArr);
}
}
}
//'function `addSignatureBookmark` is used to display the page where the user's signature is located.
const addSignatureBookmark = (index) => {
const ispageNumber = signPageNumber.includes(index + 1);
return (
ispageNumber && (
<div
style={{
position: "absolute",
zIndex: 2,
top: -12,
right: -7,
transform: "translate(50% -50%)"
}}
>
<i
style={{ color: bookmarkColor || "red" }}
className="fa-solid fa-bookmark"
></i>
</div>
)
);
};
return (
<div className="showPages">
<div
@@ -66,7 +107,8 @@ function RenderAllPdfPage({
display: "flex",
justifyContent: "center",
alignItems: "center",
contain: "content"
position: "relative"
}}
onClick={() => {
setPageNumber(index + 1);
@@ -75,15 +117,25 @@ function RenderAllPdfPage({
}
}}
>
<Page
key={`page_${index + 1}`}
pageNumber={index + 1}
width={100}
height={100}
scale={1}
renderAnnotationLayer={false}
renderTextLayer={false}
/>
{signerPos && addSignatureBookmark(index)}
<div
style={{
position: "relative",
zIndex: 1,
overflow: "hidden"
}}
>
<Page
key={`page_${index + 1}`}
pageNumber={index + 1}
width={100}
height={100}
scale={1}
renderAnnotationLayer={false}
renderTextLayer={false}
/>
</div>
</div>
))}
</Document>
@@ -169,32 +169,40 @@ const SelectFolder = ({ required, onSuccess, folderCls, isReset }) => {
{required && <span className="text-red-500 text-[13px]">*</span>}
</label>
</div>
<div className="relative rounded px-[20px] py-[20px] bg-white border border-gray-200 shadow flex max-w-sm gap-8 items-center">
<div>
<i
className="far fa-folder-open text-[40px] text-[#33bbff]"
style={{ fontSize: "40px" }}
aria-hidden="true"
></i>
</div>
<div className="font-semibold ">
<div className="flex items-center gap-2">
<p>
{selectFolder && selectFolder.Name
? selectFolder.Name
: "OpenSign™ Drive"}
</p>
<div className="text-black text-sm" onClick={() => SetIsOpen(true)}>
<i
className="fa fa-pencil"
title="Select Folder"
aria-hidden="true"
></i>
</div>
<div className="relative max-w-sm">
<div
onClick={() => SetIsOpen(true)}
className=" cursor-pointer rounded px-[20px] py-[20px] bg-white border border-gray-200 shadow flex max-w-sm gap-8 items-center"
>
<div>
<i
className="far fa-folder-open text-[40px] text-[#33bbff]"
style={{ fontSize: "40px" }}
aria-hidden="true"
></i>
</div>
<div className="font-semibold ">
<div className="flex items-center gap-2">
<p>
{selectFolder && selectFolder.Name
? selectFolder.Name
: "OpenSign™ Drive"}
</p>
<div
className="text-black text-sm"
// onClick={() => SetIsOpen(true)}
>
<i
className="fa fa-pencil cursor-pointer"
title="Select Folder"
aria-hidden="true"
></i>
</div>
</div>
<p className="text-[10px] text-gray-400">
{selectFolder && selectFolder.Name ? `(${folderPath})` : ""}
</p>
</div>
<p className="text-[10px] text-gray-400">
{selectFolder && selectFolder.Name ? `(${folderPath})` : ""}
</p>
</div>
<div className="absolute top-2 right-1 cursor-pointer">
<Tooltip
+68 -7
View File
@@ -16,6 +16,7 @@ import { PDFDocument } from "pdf-lib";
import axios from "axios";
import { isEnableSubscription, submitBtn } from "../constant/const";
import ModalUi from "../primitives/ModalUi";
import { Tooltip } from "react-tooltip";
// `Form` render all type of Form on this basis of their provided in path
function Form() {
@@ -707,12 +708,13 @@ const Forms = (props) => {
required
/>
</div>
<SelectFolder
onSuccess={handleFolder}
folderCls={props.Cls}
isReset={isReset}
/>
{props.title !== "New Template" && (
<SelectFolder
onSuccess={handleFolder}
folderCls={props.Cls}
isReset={isReset}
/>
)}
{props.title === "Request Signatures" && (
<div className="text-xs mt-2">
<label className="block">
@@ -731,7 +733,66 @@ const Forms = (props) => {
)}
{props.title !== "Sign Yourself" && (
<div className="text-xs mt-2">
<label className="block">Send In Order</label>
<label className="block">
Send In Order
<a data-tooltip-id="sendInOrder-tooltip" className="ml-1">
<sup>
<i
className="fa-solid fa-question rounded-full"
style={{
borderColor: "#33bbff",
color: "#33bbff",
fontSize: 13,
borderWidth: 1.5,
padding: "1.5px 4px"
}}
></i>
</sup>
</a>
<Tooltip id="sendInOrder-tooltip" className="z-50">
<div className="max-w-[200px] md:max-w-[450px]">
<p className="font-bold">Send in Order</p>
<p>
Choose how you want the signing requests to be sent to
the document signers:
</p>
<p className="p-[5px]">
<ol className="list-disc">
<li>
<span className="font-bold">Yes:</span>
<span>
{" "}
Selecting this option will send the signing
request to the first signer initially. Once the
first signer completes their part, the next signer
in the sequence will receive the request. This
process continues until all signers have signed
the document. This method ensures that the
document is signed in a specific order.
</span>
</li>
<li>
<span className="font-bold">No: </span>
<span>
Selecting this option will send the signing links
to all signers simultaneously. Every signer can
sign the document at their convenience, regardless
of whether other signers have completed their
signatures. This method is faster but does not
enforce any signing order among the participants.
</span>
</li>
</ol>
</p>
<p>
Select the option that best suits the needs of your
document processing.
</p>
</div>
</Tooltip>
</label>
<div className="flex items-center gap-2 ml-2 mb-1">
<input
type="radio"
+63 -78
View File
@@ -472,36 +472,8 @@ function PdfRequestFiles() {
});
};
// const checkSendInOrder = () => {
// if (sendInOrder) {
// const index = pdfDetails?.[0].Signers.findIndex(
// (x) => x.Email === jsonSender.email
// );
// const newIndex = index - 1;
// if (newIndex !== -1) {
// const user = pdfDetails?.[0].Signers[newIndex];
// const isPrevUserSigned =
// pdfDetails?.[0].AuditTrail &&
// pdfDetails?.[0].AuditTrail.some(
// (x) =>
// x.UserPtr.objectId === user.objectId && x.Activity === "Signed"
// );
// if (isPrevUserSigned) {
// return true;
// } else {
// return false;
// }
// } else {
// return true;
// }
// } else {
// return true;
// }
// };
//function for embed signature or image url in pdf
async function embedWidgetsData() {
// const validateSigning = checkSendInOrder();
// if (validateSigning) {
try {
const checkUser = signerPos.filter(
(data) => data.signerObjId === signerObjectId
@@ -512,83 +484,99 @@ function PdfRequestFiles() {
showAlert = false,
widgetKey,
radioExist,
requiredCheckbox;
requiredCheckbox,
pageNumber; // `pageNumber` is used to check on which page user did not fill widget's data then change current pageNumber and show tour message on that page
for (let i = 0; i < checkUser[0].placeHolder.length; i++) {
for (let j = 0; j < checkUser[0].placeHolder[i].pos.length; j++) {
//get current page
const updatePage = checkUser[0].placeHolder[i]?.pageNumber;
//checking checbox type widget
checkboxExist =
checkUser[0].placeHolder[i].pos[j].type === "checkbox";
//checking radio button type widget
radioExist =
checkUser[0].placeHolder[i].pos[j].type === radioButtonWidget;
//condition to check checkbox widget exist or not
if (checkboxExist) {
//get all required type checkbox
requiredCheckbox = checkUser[0].placeHolder[i].pos.filter(
(position) =>
!position.options?.isReadOnly && position.type === "checkbox"
);
//if required type checkbox data exit then check user checked all checkbox or some checkbox remain to check
//also validate to minimum and maximum required checkbox
if (requiredCheckbox && requiredCheckbox.length > 0) {
for (let i = 0; i < requiredCheckbox.length; i++) {
//get minimum required count if exit
const minCount =
requiredCheckbox[i].options?.validation?.minRequiredCount;
const parseMin = minCount && parseInt(minCount);
//get maximum required count if exit
const maxCount =
requiredCheckbox[i].options?.validation?.maxRequiredCount;
const parseMax = maxCount && parseInt(maxCount);
//in `response` variable is used to get how many checkbox checked by user
const response =
requiredCheckbox[i].options?.response?.length;
//in `defaultValue` variable is used to get how many checkbox checked by default
const defaultValue =
requiredCheckbox[i].options?.defaultValue?.length;
if (parseMin === 0 && parseMax === 0) {
if (!showAlert) {
showAlert = false;
setminRequiredCount(null);
}
} else if (parseMin === 0 && parseMax > 0) {
if (!showAlert) {
showAlert = false;
setminRequiredCount(null);
}
} else if (!response) {
if (!defaultValue) {
if (!showAlert) {
showAlert = true;
widgetKey = requiredCheckbox[i].key;
setminRequiredCount(parseMin);
}
}
} else if (parseMin > 0 && parseMin > response) {
//condition to check parseMin and parseMax greater than 0 then consider it as a required check box
if (
parseMin > 0 &&
parseMax > 0 &&
!response &&
!defaultValue &&
!showAlert
) {
showAlert = true;
widgetKey = requiredCheckbox[i].key;
pageNumber = updatePage;
setminRequiredCount(parseMin);
}
//else condition to validate minimum required checkbox
else if (parseMin > 0 && (parseMin > response || !response)) {
if (!showAlert) {
showAlert = true;
widgetKey = requiredCheckbox[i].key;
pageNumber = updatePage;
setminRequiredCount(parseMin);
}
}
}
}
} else if (radioExist) {
}
//condition to check radio widget exist or not
else if (radioExist) {
//get all required type radio button
requiredRadio = checkUser[0].placeHolder[i].pos.filter(
(position) =>
!position.options?.isReadOnly &&
position.type === radioButtonWidget
);
//if required type radio data exit then check user checked all radio button or some radio remain to check
if (requiredRadio && requiredRadio?.length > 0) {
let checkSigned;
for (let i = 0; i < requiredRadio?.length; i++) {
checkSigned = requiredRadio[i]?.options.response;
checkSigned = requiredRadio[i]?.options?.response;
if (!checkSigned) {
let checkDefaultSigned =
requiredRadio[i]?.options.defaultValue;
if (!checkDefaultSigned) {
if (!showAlert) {
showAlert = true;
widgetKey = requiredRadio[i].key;
setminRequiredCount(null);
}
requiredRadio[i]?.options?.defaultValue;
if (!checkDefaultSigned && !showAlert) {
showAlert = true;
widgetKey = requiredRadio[i].key;
pageNumber = updatePage;
setminRequiredCount(null);
}
}
}
}
} else {
}
//else condition to check all type widget data fill or not except checkbox and radio button
else {
//get all required type widgets except checkbox and radio
const requiredWidgets = checkUser[0].placeHolder[i].pos.filter(
(position) =>
position.options?.status === "required" &&
@@ -601,16 +589,14 @@ function PdfRequestFiles() {
checkSigned = requiredWidgets[i]?.options?.response;
if (!checkSigned) {
const checkSignUrl = requiredWidgets[i]?.pos?.SignUrl;
let checkDefaultSigned =
requiredWidgets[i]?.options?.defaultValue;
if (!checkSignUrl) {
if (!checkDefaultSigned) {
if (!showAlert) {
showAlert = true;
widgetKey = requiredWidgets[i].key;
setminRequiredCount(null);
}
let checkDefaultSigned =
requiredWidgets[i]?.options?.defaultValue;
if (!checkDefaultSigned && !showAlert) {
showAlert = true;
widgetKey = requiredWidgets[i].key;
pageNumber = updatePage;
setminRequiredCount(null);
}
}
}
@@ -618,16 +604,22 @@ function PdfRequestFiles() {
}
}
}
//when showAlert is true then break the loop and show alert to fill required data in widgets
if (showAlert) {
break;
}
}
if (checkboxExist && requiredCheckbox && showAlert) {
setUnSignedWidgetId(widgetKey);
setPageNumber(pageNumber);
setWidgetsTour(true);
} else if (radioExist && showAlert) {
setUnSignedWidgetId(widgetKey);
setPageNumber(pageNumber);
setWidgetsTour(true);
} else if (showAlert) {
setUnSignedWidgetId(widgetKey);
setPageNumber(pageNumber);
setWidgetsTour(true);
} else {
setIsUiLoading(true);
@@ -882,15 +874,6 @@ function PdfRequestFiles() {
alertMessage: "something went wrong, please try again later."
});
}
// }
// else {
// setIsAlert({
// isShow: true,
// alertMessage:
// "Please wait for your turn to sign this document, as it has been set up by the creator to be signed in a specific order; you'll be notified when it's your turn."
// });
// }
}
//function for update TourStatus
@@ -1349,6 +1332,8 @@ function PdfRequestFiles() {
</ModalUi>
{/* this component used to render all pdf pages in left side */}
<RenderAllPdfPage
signerPos={signerPos}
signerObjectId={signerObjectId}
signPdfUrl={
pdfDetails[0] &&
(pdfDetails[0]?.SignedUrl || pdfDetails[0]?.URL)
+4 -1
View File
@@ -409,7 +409,10 @@ function PlaceHolderSign() {
setActiveMailAdapter(res[0]?.active_mail_adapter);
setSignerUserId(res[0].objectId);
const tourstatus = res[0].TourStatus && res[0].TourStatus;
if (tourstatus && tourstatus.length > 0) {
const alreadyPlaceholder = documentData[0]?.SignedUrl;
if (alreadyPlaceholder) {
setCheckTourStatus(true);
} else if (tourstatus && tourstatus.length > 0) {
setTourStatus(tourstatus);
const checkTourRecipients = tourstatus.filter(
(data) => data.placeholder
+112 -113
View File
@@ -233,29 +233,6 @@ function SignYourSelf() {
setSignBtnPosition([]);
}
}
if (!isCompleted) {
//check current user email verified or not
const currentUser = JSON.parse(JSON.stringify(Parse.User.current()));
let isEmailVerified;
isEmailVerified = currentUser?.emailVerified;
if (isEmailVerified) {
setIsEmailVerified(isEmailVerified);
} else {
try {
const userQuery = new Parse.Query(Parse.User);
const user = await userQuery.get(currentUser.objectId, {
sessionToken: localStorage.getItem("accesstoken")
});
if (user) {
isEmailVerified = user?.get("emailVerified");
setIsEmailVerified(isEmailVerified);
}
} catch (e) {
setHandleError("Error: Something went wrong!");
}
}
}
} else if (
documentData === "Error: Something went wrong!" ||
(documentData.result && documentData.result.error)
@@ -603,104 +580,126 @@ function SignYourSelf() {
};
//function for send placeholder's co-ordinate(x,y) position embed signature url or stamp url
async function embedWidgetsData() {
let showAlert = false,
isSignatureExist = false;
try {
for (let i = 0; i < xyPostion?.length; i++) {
const requiredWidgets = xyPostion[i].pos.filter(
(position) => position.type !== "checkbox"
);
if (requiredWidgets && requiredWidgets?.length > 0) {
let checkSigned;
for (let i = 0; i < requiredWidgets?.length; i++) {
checkSigned = requiredWidgets[i]?.options?.response;
if (!checkSigned) {
const checkSignUrl = requiredWidgets[i]?.pos?.SignUrl;
let checkDefaultSigned =
requiredWidgets[i]?.options?.defaultValue;
if (!checkSignUrl) {
if (!checkDefaultSigned) {
if (!showAlert) {
showAlert = true;
//check current user email is verified or not
const currentUser = JSON.parse(JSON.stringify(Parse.User.current()));
let isEmailVerified;
isEmailVerified = currentUser?.emailVerified;
if (isEmailVerified) {
setIsEmailVerified(isEmailVerified);
} else {
try {
const userQuery = new Parse.Query(Parse.User);
const user = await userQuery.get(currentUser.objectId, {
sessionToken: localStorage.getItem("accesstoken")
});
if (user) {
isEmailVerified = user?.get("emailVerified");
setIsEmailVerified(isEmailVerified);
}
} catch (e) {
setHandleError("Error: Something went wrong!");
}
}
if (isEmailVerified) {
let showAlert = false,
isSignatureExist = false;
try {
for (let i = 0; i < xyPostion?.length; i++) {
const requiredWidgets = xyPostion[i].pos.filter(
(position) => position.type !== "checkbox"
);
if (requiredWidgets && requiredWidgets?.length > 0) {
let checkSigned;
for (let i = 0; i < requiredWidgets?.length; i++) {
checkSigned = requiredWidgets[i]?.options?.response;
if (!checkSigned) {
const checkSignUrl = requiredWidgets[i]?.pos?.SignUrl;
let checkDefaultSigned =
requiredWidgets[i]?.options?.defaultValue;
if (!checkSignUrl) {
if (!checkDefaultSigned) {
if (!showAlert) {
showAlert = true;
}
}
}
}
}
}
}
//condition to check exist signature widget or not
if (!isSignatureExist) {
isSignatureExist = xyPostion[i].pos.some(
(data) => data?.type === "signature"
);
}
}
if (xyPostion.length === 0 || !isSignatureExist) {
setIsAlert({
header: "Fields required",
isShow: true,
alertMessage:
"Please ensure there's at least one signature widget added"
});
return;
} else if (showAlert) {
setIsAlert({
isShow: true,
alertMessage:
"Please ensure all field is accurately filled and meets all requirements."
});
return;
} else {
setIsCeleb(true);
setTimeout(() => {
setIsCeleb(false);
}, 3000);
setIsUiLoading(true);
const existingPdfBytes = pdfArrayBuffer;
// Load a PDFDocument from the existing PDF bytes
try {
const pdfDoc = await PDFDocument.load(existingPdfBytes);
const isSignYourSelfFlow = true;
const extUserPtr = pdfDetails[0].ExtUserPtr;
const HeaderDocId = extUserPtr?.HeaderDocId;
//embed document's object id to all pages in pdf document
if (!HeaderDocId) {
await embedDocId(pdfDoc, documentId, allPages);
}
//embed multi signature in pdf
const pdfBytes = await multiSignEmbed(
xyPostion,
pdfDoc,
pdfOriginalWidth,
isSignYourSelfFlow,
containerWH
);
// console.log("pdf", pdfBytes);
//function for call to embed signature in pdf and get digital signature pdf
await signPdfFun(pdfBytes, documentId);
} catch (err) {
setIsUiLoading(false);
if (err && err.message.includes("is encrypted.")) {
setIsAlert({
isShow: true,
alertMessage: `Currently encrypted pdf files are not supported.`
});
} else {
console.log("err in signing", err);
setIsAlert({
isShow: true,
alertMessage: `Something went wrong.`
});
//condition to check exist signature widget or not
if (!isSignatureExist) {
isSignatureExist = xyPostion[i].pos.some(
(data) => data?.type === "signature"
);
}
}
if (xyPostion.length === 0 || !isSignatureExist) {
setIsAlert({
header: "Fields required",
isShow: true,
alertMessage:
"Please ensure there's at least one signature widget added"
});
return;
} else if (showAlert) {
setIsAlert({
isShow: true,
alertMessage:
"Please ensure all field is accurately filled and meets all requirements."
});
return;
} else {
setIsCeleb(true);
setTimeout(() => {
setIsCeleb(false);
}, 3000);
setIsUiLoading(true);
const existingPdfBytes = pdfArrayBuffer;
// Load a PDFDocument from the existing PDF bytes
try {
const pdfDoc = await PDFDocument.load(existingPdfBytes);
const isSignYourSelfFlow = true;
const extUserPtr = pdfDetails[0].ExtUserPtr;
const HeaderDocId = extUserPtr?.HeaderDocId;
//embed document's object id to all pages in pdf document
if (!HeaderDocId) {
await embedDocId(pdfDoc, documentId, allPages);
}
//embed multi signature in pdf
const pdfBytes = await multiSignEmbed(
xyPostion,
pdfDoc,
pdfOriginalWidth,
isSignYourSelfFlow,
containerWH
);
// console.log("pdf", pdfBytes);
//function for call to embed signature in pdf and get digital signature pdf
await signPdfFun(pdfBytes, documentId);
} catch (err) {
setIsUiLoading(false);
if (err && err.message.includes("is encrypted.")) {
setIsAlert({
isShow: true,
alertMessage: `Currently encrypted pdf files are not supported.`
});
} else {
console.log("err in signing", err);
setIsAlert({
isShow: true,
alertMessage: `Something went wrong.`
});
}
}
}
} catch (err) {
console.log("err in embedselfsign ", err);
setIsUiLoading(false);
setIsAlert({
isShow: true,
alertMessage: "something went wrong, please try again later."
});
}
} catch (err) {
console.log("err in embedselfsign ", err);
setIsUiLoading(false);
setIsAlert({
isShow: true,
alertMessage: "something went wrong, please try again later."
});
}
}
//function for get digital signature
-1
View File
@@ -404,7 +404,6 @@ function UserProfile() {
url={
"https://docs.opensignlabs.com/docs/help/Settings/disabledocumentid"
}
isSubscribe={isSubscribe}
/>{" "}
{!isSubscribe && isEnableSubscription && <Upgrade />}
</span>{" "}
+2 -10
View File
@@ -1,17 +1,9 @@
import React from "react";
import { Tooltip as ReactTooltip } from "react-tooltip";
import { openInNewTab } from "../constant/Utils";
import { isEnableSubscription } from "../constant/const";
const Tooltip = ({ id, message, url, iconColor, isSubscribe }) =>
const Tooltip = ({ id, message, url, iconColor }) =>
url ? (
<button
onClick={() => openInNewTab(url)}
className={
isSubscribe || !isEnableSubscription
? "text-center"
: "text-center opacity-20 pointer-events-none"
}
>
<button onClick={() => openInNewTab(url)} className={"text-center"}>
<sup>
<i
className="fa-solid fa-question rounded-full"
@@ -17,7 +17,6 @@ import getTemplatetList from './routes/getTemplateList.js';
import updateTemplate from './routes/updateTemplate.js';
import createContact from './routes/createContact.js';
import multer from 'multer';
// import fs from 'node:fs';
import updateDocument from './routes/updateDocument.js';
import deleteDocument from './routes/deleteDocument.js';
import createDocumentWithTemplate from './routes/CreateDocumentWithTemplate.js';
@@ -26,6 +25,11 @@ import deleteWebhook from './routes/deleteWebhook.js';
import getWebhook from './routes/getWebhook.js';
import createDocumentwithCoordinate from './routes/createDocumentwithCoordinate.js';
import createTemplatewithCoordinate from './routes/createTemplatewithCoordinate.js';
import resendMail from './routes/resendMail.js';
import getFolder from './routes/getFolder.js';
import createFolder from './routes/createFolder.js';
import updateFolder from './routes/updateFolder.js';
import getFolderList from './routes/getFolderList.js';
dotenv.config();
const storage = multer.memoryStorage();
const upload = multer({ storage: storage });
@@ -102,3 +106,18 @@ app.post('/webhook', saveWebhook);
// set and update webhook
app.delete('/webhook', deleteWebhook);
// resend mail
app.post('/resendmail', resendMail);
// create folder
app.post('/createfolder', createFolder);
// update folder
app.put('/folder/:folder_id', updateFolder);
// get folder
app.get('/folder/:folder_id', getFolder);
// get folder list
app.get('/folderlist', getFolderList);
@@ -55,6 +55,7 @@ export default async function createDocumentWithTemplate(request, response) {
const email_subject = request.body.email_subject;
const email_body = request.body.email_body;
const sendInOrder = request.body.sendInOrder || false;
const TimeToCompleteDays = request.body.timeToCompleteDays || 15;
try {
const reqToken = request.headers['x-api-token'];
@@ -178,6 +179,9 @@ export default async function createDocumentWithTemplate(request, response) {
}
object.set('URL', template.URL);
object.set('SignedUrl', template.URL);
if (TimeToCompleteDays) {
object.set('TimeToCompleteDays', TimeToCompleteDays);
}
object.set('CreatedBy', template.CreatedBy);
object.set('ExtUserPtr', {
__type: 'Pointer',
@@ -66,6 +66,7 @@ export default async function createDocumentwithCoordinate(request, response) {
const email_subject = request.body.email_subject;
const email_body = request.body.email_body;
const sendInOrder = request.body.sendInOrder || false;
const TimeToCompleteDays = request.body.timeToCompleteDays || 15;
// console.log('fileData ', fileData);
const protocol = customAPIurl();
const baseUrl = new URL(process.env.SERVER_URL);
@@ -142,6 +143,9 @@ export default async function createDocumentwithCoordinate(request, response) {
object.set('SignedUrl', fileUrl);
object.set('CreatedBy', userPtr);
object.set('ExtUserPtr', extUserPtr);
if (TimeToCompleteDays) {
object.set('TimeToCompleteDays', TimeToCompleteDays);
}
object.set('IsSendMail', send_email);
let contact = [];
if (signers && signers.length > 0) {
@@ -0,0 +1,73 @@
export default async function createFolder(request, response) {
const apiToken = request.headers['x-api-token'];
const folderName = request.body.folderName;
const parentFolderId = request.body.parentFolderId;
if (!apiToken) {
return response.status(400).json({ error: 'Please Provide API Token' });
}
try {
const tokenQuery = new Parse.Query('appToken');
tokenQuery.equalTo('token', apiToken);
tokenQuery.include('userId');
const token = await tokenQuery.first({ useMasterKey: true });
if (token !== undefined) {
// Valid Token then proceed request
const parseUser = JSON.parse(JSON.stringify(token));
const userPtr = {
__type: 'Pointer',
className: '_User',
objectId: parseUser.userId.objectId,
};
const userQuery = new Parse.Query('contracts_Users');
userQuery.equalTo('UserId', userPtr);
const extUser = await userQuery.first({ useMasterKey: true });
const folderCls = new Parse.Object('contracts_Document');
folderCls.set('Name', folderName);
folderCls.set('CreatedBy', userPtr);
folderCls.set('Type', 'Folder');
folderCls.set('ExtUserPtr', {
__type: 'Pointer',
className: 'contracts_Users',
objectId: extUser.id,
});
if (parentFolderId) {
folderCls.set('Folder', {
__type: 'Pointer',
className: 'contracts_Document',
objectId: parentFolderId,
});
}
const acl = new Parse.ACL();
acl.setReadAccess(parseUser.userId.objectId, true); // Allow read access to the userPtr
acl.setWriteAccess(parseUser.userId.objectId, true); // Allow write access to the userPtr
// Set the ACL to the document
folderCls.setACL(acl);
const _resFolder = await folderCls.save(null, { useMasterKey: true });
if (_resFolder) {
if (request.posthog) {
request.posthog?.capture({
distinctId: parseUser.userId.email,
event: 'api_create_folder',
properties: { response_code: 200 },
});
}
const parseRes = JSON.parse(JSON.stringify(_resFolder));
return response.json({
objectId: parseRes.objectId,
folderName: parseRes.Name,
parentFolderId: parseRes?.Folder?.objectId || '',
createdAt: parseRes.createdAt,
updatedAt: parseRes.updatedAt,
});
}
} else {
return response.status(405).json({ error: 'Invalid API Token!' });
}
} catch (err) {
console.log('err ', err);
return response.status(400).json({ error: 'Something went wrong, please try again later!' });
}
}
@@ -40,6 +40,7 @@ export default async function getDocument(request, response) {
note: document.Note || '',
folder: { objectId: document?.Folder?.objectId, name: document?.Folder?.Name } || '',
file: document?.SignedUrl || document.URL,
certificate: document?.CertificateUrl || '',
owner: document?.ExtUserPtr?.Name,
signers:
document?.Placeholders?.map(y => ({
@@ -77,7 +77,7 @@ export default async function getDocumentList(request, response) {
title: x.Name,
note: x.Note || '',
folder: { objectId: x?.Folder?.objectId, name: x?.Folder?.Name } || '',
file: x?.SignedUrl || x.URL,
// file: x?.SignedUrl || x.URL,
owner: x?.ExtUserPtr?.Name,
signers:
x?.Placeholders?.map(y => ({
@@ -0,0 +1,64 @@
export default async function getFolder(request, response) {
const apiToken = request.headers['x-api-token'];
const folder_id = request.params.folder_id;
if (!apiToken) {
return response.status(400).json({ error: 'Please Provide API Token' });
}
try {
const tokenQuery = new Parse.Query('appToken');
tokenQuery.equalTo('token', apiToken);
tokenQuery.include('userId');
const token = await tokenQuery.first({ useMasterKey: true });
if (token !== undefined) {
// Valid Token then proceed request
const parseUser = JSON.parse(JSON.stringify(token));
const userPtr = {
__type: 'Pointer',
className: '_User',
objectId: parseUser.userId.objectId,
};
const folderCls = new Parse.Query('contracts_Document');
folderCls.equalTo('CreatedBy', userPtr);
folderCls.equalTo('objectId', folder_id);
folderCls.equalTo('Type', 'Folder');
folderCls.notEqualTo('IsArchive', true);
folderCls.descending('createdAt');
folderCls.include('Folder');
const res = await folderCls.first({ useMasterKey: true });
if (res) {
const parseRes = JSON.parse(JSON.stringify(res));
if (request.posthog) {
request.posthog?.capture({
distinctId: parseUser.userId.email,
event: 'api_get_folder',
properties: { response_code: 200 },
});
}
return response.json({
objectId: parseRes.objectId,
folderName: parseRes.Name,
parentFolderId: parseRes?.Folder?.objectId || '',
parentFolderName: parseRes?.Folder?.Name || '',
createdAt: parseRes.createdAt,
updatedAt: parseRes.updatedAt,
});
} else {
if (request.posthog) {
request.posthog?.capture({
distinctId: parseUser.userId.email,
event: 'api_get_folder',
properties: { response_code: 404 },
});
}
return response.status(404).json({ error: 'folder not found.' });
}
} else {
return response.status(405).json({ error: 'Invalid API Token!' });
}
} catch (err) {
console.log('err ', err);
return response.status(400).json({ error: 'Something went wrong, please try again later!' });
}
}
@@ -0,0 +1,76 @@
export default async function getFolderList(request, response) {
const apiToken = request.headers['x-api-token'];
const parentFolderId = request.query?.parentFolderId || '';
if (!apiToken) {
return response.status(400).json({ error: 'Please Provide API Token' });
}
try {
const tokenQuery = new Parse.Query('appToken');
tokenQuery.equalTo('token', apiToken);
tokenQuery.include('userId');
const token = await tokenQuery.first({ useMasterKey: true });
if (token !== undefined) {
// Valid Token then proceed request
const parseUser = JSON.parse(JSON.stringify(token));
const userPtr = {
__type: 'Pointer',
className: '_User',
objectId: parseUser.userId.objectId,
};
const limit = request?.query?.limit ? parseInt(request.query.limit) : 100;
const skip = request?.query?.skip ? parseInt(request.query.skip) : 0;
const folderCls = new Parse.Query('contracts_Document');
folderCls.equalTo('CreatedBy', userPtr);
folderCls.equalTo('Type', 'Folder');
folderCls.notEqualTo('IsArchive', true);
folderCls.descending('createdAt');
folderCls.include('Folder');
if (parentFolderId) {
folderCls.equalTo('Folder', {
__type: 'Pointer',
className: 'contracts_Document',
objectId: parentFolderId,
});
} else {
folderCls.doesNotExist('Folder');
}
folderCls.descending('createdAt');
folderCls.limit(limit);
folderCls.skip(skip);
const res = await folderCls.find({ useMasterKey: true });
if (res && res.length > 0) {
const parseRes = JSON.parse(JSON.stringify(res));
const folderlist = parseRes.map(x => ({
objectId: x.objectId,
folderName: x.Name,
parentFolderId: x?.Folder?.objectId || '',
parentFolderName: x?.Folder?.Name || '',
createdAt: x.createdAt,
updatedAt: x.updatedAt,
}));
if (request.posthog) {
request.posthog?.capture({
distinctId: parseUser.userId.email,
event: 'api_get_Folder_list',
properties: { response_code: 200 },
});
}
return response.json({ result: folderlist });
} else {
if (request.posthog) {
request.posthog?.capture({
distinctId: parseUser.userId.email,
event: 'api_get_Folder_list',
properties: { response_code: 200 },
});
}
return response.json({ result: [] });
}
} else {
return response.status(405).json({ error: 'Invalid API Token!' });
}
} catch (err) {
console.log('err ', err);
return response.status(400).json({ error: 'Something went wrong, please try again later!' });
}
}
@@ -0,0 +1,156 @@
import axios from 'axios';
import { replaceMailVaribles } from '../../../../Utils.js';
export default async function resendMail(request, response) {
try {
const reqToken = request.headers['x-api-token'];
const docId = request.body.document_id;
const userMail = request.body.email;
const email_subject = request.body.email_subject;
const email_body = request.body.email_body;
const baseUrl = new URL(process.env.SERVER_URL);
if (!reqToken) {
return response.status(400).json({ error: 'Please Provide API Token' });
}
const tokenQuery = new Parse.Query('appToken');
tokenQuery.equalTo('token', reqToken);
tokenQuery.include('userId');
const token = await tokenQuery.first({ useMasterKey: true });
if (token !== undefined) {
// Valid Token then proceed request
const parseUser = JSON.parse(JSON.stringify(token));
const userPtr = {
__type: 'Pointer',
className: '_User',
objectId: parseUser.userId.objectId,
};
const docQuery = new Parse.Query('contracts_Document');
docQuery.equalTo('objectId', docId);
docQuery.equalTo('CreatedBy', userPtr);
docQuery.include('Signers,ExtUserPtr');
docQuery.notEqualTo('IsCompleted', true);
docQuery.notEqualTo('IsDeclined', true);
docQuery.notEqualTo('IsArchive', true);
docQuery.lessThanOrEqualTo('ExpiryDate', new Date());
docQuery.exists('SignedUrl');
const resDoc = await docQuery.first({ useMasterKey: true });
if (resDoc) {
const _resDoc = resDoc.toJSON();
const contact = _resDoc.Signers.find(x => x.Email === userMail);
if (contact) {
try {
const imgPng = 'https://qikinnovation.ams3.digitaloceanspaces.com/logo.png';
let url = `${process.env.SERVER_URL}/functions/sendmailv3/`;
const headers = {
'Content-Type': 'application/json',
'X-Parse-Application-Id': process.env.APP_ID,
'X-Parse-Master-Key': process.env.MASTER_KEY,
};
const objectId = contact.objectId;
const hostUrl = baseUrl.origin;
const title = _resDoc.Name;
const receiverMail = contact.Email;
//encode this url value `${response.id}/${receiverMail}/${objectId}` to base64 using `btoa` function
const encodeBase64 = btoa(`${_resDoc.objectId}/${receiverMail}/${objectId}`);
let signPdf = `${hostUrl}/login/${encodeBase64}`;
const openSignUrl = 'https://www.opensignlabs.com/contact-us';
const orgName = _resDoc.ExtUserPtr.Company ? _resDoc.ExtUserPtr.Company : '';
const newDate = new Date(_resDoc.ExpiryDate.iso);
newDate.setDate(newDate.getDate() + 15);
const sender = _resDoc.ExtUserPtr.Email;
const localExpireDate = newDate.toLocaleDateString('en-US', {
day: 'numeric',
month: 'long',
year: 'numeric',
});
const themeBGcolor = '#47a3ad';
const email_html =
"<html><head><meta http-equiv='Content-Type' content='text/html; charset=UTF-8' /> </head> <body> <div style='background-color: #f5f5f5; padding: 20px'> <div style=' box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 12px;background: white;padding-bottom: 20px;'> <div style='padding:10px 10px 0 10px'><img src=" +
imgPng +
" height='50' style='padding:20px; width:170px; height:40px;' /></div> <div style='padding:2px; font-family: system-ui;background-color:" +
themeBGcolor +
";'><p style='font-size: 20px;font-weight: 400;color: white;padding-left: 20px;' > Digital Signature Request</p></div><div><p style='padding: 20px;font-family: system-ui;font-size: 14px; margin-bottom: 10px;'> " +
_resDoc.ExtUserPtr.Name +
' has requested you to review and sign <strong> ' +
title +
"</strong>.</p><div style='padding: 5px 0px 5px 25px;display: flex;flex-direction: row;justify-content: space-around;'><table> <tr> <td style='font-weight:bold;font-family:sans-serif;font-size:15px'>Sender</td> <td> </td> <td style='color:#626363;font-weight:bold'>" +
sender +
"</td></tr><tr><td style='font-weight:bold;font-family:sans-serif;font-size:15px'>Organization</td> <td> </td><td style='color:#626363;font-weight:bold'> " +
orgName +
"</td></tr> <tr> <td style='font-weight:bold;font-family:sans-serif;font-size:15px'>Expires on</td><td> </td> <td style='color:#626363;font-weight:bold'>" +
localExpireDate +
"</td></tr><tr> <td></td> <td> </td></tr></table> </div> <div style='margin-left:70px'><a href=" +
signPdf +
"> <button style='padding: 12px 12px 12px 12px;background-color: #d46b0f;color: white; border: 0px;box-shadow: rgba(0, 0, 0, 0.05) 0px 6px 24px 0px,rgba(0, 0, 0, 0.08) 0px 0px 0px 1px;font-weight:bold;margin-top:30px'>Sign here</button></a> </div> <div style='display: flex; justify-content: center;margin-top: 10px;'> </div></div></div><div><p> This is an automated email from OpenSign™. For any queries regarding this email, please contact the sender " +
sender +
' directly.If you think this email is inappropriate or spam, you may file a complaint with OpenSign™ <a href=' +
openSignUrl +
' target=_blank>here</a>.</p> </div></div></body> </html>';
let replaceVar;
const variables = {
document_title: title,
sender_name: _resDoc.ExtUserPtr.Name,
sender_mail: _resDoc.ExtUserPtr.Email,
sender_phone: _resDoc.ExtUserPtr.Phone,
receiver_name: contact.Name,
receiver_email: contact.Email,
receiver_phone: contact.Phone,
expiry_date: localExpireDate,
company_name: orgName,
signing_url: signPdf,
};
if (email_subject && email_body) {
replaceVar = replaceMailVaribles(email_subject, email_body, variables);
} else if (email_subject) {
replaceVar = replaceMailVaribles(email_subject, '', variables);
replaceVar = { subject: replaceVar.subject, body: email_html };
} else if (email_body) {
replaceVar = replaceMailVaribles(
`${_resDoc.ExtUserPtr.Name} has requested you to sign "${title}"`,
email_body,
variables
);
} else {
replaceVar = {
subject: `${_resDoc.ExtUserPtr.Name} has requested you to sign "${title}"`,
body: email_html,
};
}
const subject = replaceVar.subject;
const html = replaceVar.body;
let params = {
recipient: contact.Email,
subject: subject,
from: sender,
html: html,
extUserId: _resDoc.ExtUserPtr.objectId,
};
const res = await axios.post(url, params, { headers: headers });
if (res.data.result && res.data.result.status === 'success') {
return response.json({ result: 'mail sent successfully.' });
}
} catch (error) {
console.log('error', error);
return response.status(400).json({ error: error.message });
}
} else {
return response.status(404).json({ error: 'user not found.' });
}
} else {
return response.status(404).json({ error: 'document not found.' });
}
} else {
return response.status(405).json({ error: 'Invalid API Token.' });
}
} catch (err) {
console.log('err in resendmail', err);
return response.status(400).json({ error: err.message || 'Something went wrong.' });
}
}
@@ -0,0 +1,67 @@
export default async function updateFolder(request, response) {
const apiToken = request.headers['x-api-token'];
const folderId = request.params.folder_id;
const name = request.body.foldername;
const parentFolderId = request.body.parentFolderId;
if (!apiToken) {
return response.status(400).json({ error: 'Please Provide API Token' });
}
try {
const tokenQuery = new Parse.Query('appToken');
tokenQuery.equalTo('token', apiToken);
tokenQuery.include('userId');
const token = await tokenQuery.first({ useMasterKey: true });
if (token !== undefined) {
// Valid Token then proceed request
const parseUser = JSON.parse(JSON.stringify(token));
const userPtr = {
__type: 'Pointer',
className: '_User',
objectId: parseUser.userId.objectId,
};
const folderCls = new Parse.Query('contracts_Document');
folderCls.equalTo('CreatedBy', userPtr);
folderCls.equalTo('objectId', folderId);
folderCls.equalTo('Type', 'Folder');
folderCls.notEqualTo('IsArchive', true);
const res = await folderCls.first({ useMasterKey: true });
if (res) {
const updateFolder = new Parse.Object('contracts_Document');
updateFolder.id = res.id;
updateFolder.set('Name', name);
if (parentFolderId) {
updateFolder.set('Folder', {
__type: 'Pointer',
className: 'contracts_Document',
objectId: parentFolderId,
});
}
const updateRes = await updateFolder.save(null, { useMasterKey: true });
if (request.posthog) {
request.posthog?.capture({
distinctId: parseUser.userId.email,
event: 'api_update_folder',
properties: { response_code: 200 },
});
}
return response.json({ objectId: updateRes.id, updatedAt: updateRes.updatedAt });
} else {
if (request.posthog) {
request.posthog?.capture({
distinctId: parseUser.userId.email,
event: 'api_update_folder',
properties: { response_code: 404 },
});
}
return response.status(404).json({ error: 'folder not found.' });
}
} else {
return response.status(405).json({ error: 'Invalid API Token!' });
}
} catch (err) {
console.log('err ', err);
return response.status(400).json({ error: 'Something went wrong, please try again later!' });
}
}