Compare commits

..
Author SHA1 Message Date
prafull-opensignlabs b3fef4a52f Merge pull request #858 from nxglabs/staging
v2.25.0
2025-06-17 14:23:51 +00:00
62 changed files with 1706 additions and 2921 deletions
+259 -295
View File
File diff suppressed because it is too large Load Diff
+21 -21
View File
@@ -5,15 +5,15 @@
"dependencies": {
"@formkit/auto-animate": "^0.8.2",
"@imgly/background-removal": "^1.6.0",
"@lottiefiles/dotlottie-react": "^0.14.2",
"@lottiefiles/dotlottie-react": "^0.14.0",
"@pdf-lib/fontkit": "^1.1.1",
"@radix-ui/themes": "^3.2.1",
"@reduxjs/toolkit": "^2.8.2",
"axios": "^1.10.0",
"axios": "^1.9.0",
"date-fns-tz": "^3.2.0",
"file-saver": "^2.0.5",
"i18next": "^25.3.0",
"i18next-browser-languagedetector": "^8.2.0",
"i18next": "^23.16.8",
"i18next-browser-languagedetector": "^8.1.0",
"i18next-http-backend": "^3.0.2",
"jszip": "^3.10.1",
"jwt-decode": "^4.0.0",
@@ -36,24 +36,24 @@
"react-dom": "^18.3.1",
"react-gtm-module": "^2.0.11",
"react-helmet": "^6.1.0",
"react-i18next": "^15.5.3",
"react-i18next": "^15.5.2",
"react-konva": "^18.2.10",
"react-pdf": "^9.2.1",
"react-quill-new": "^3.4.6",
"react-redux": "^9.2.0",
"react-rnd": "^10.5.2",
"react-router": "^7.6.3",
"react-router": "^7.6.1",
"react-scrollbars-custom": "^4.1.1",
"react-select": "^5.10.1",
"react-signature-canvas": "^1.1.0-alpha.2",
"react-timezone-select": "^3.2.8",
"react-tooltip": "^5.29.1",
"react-tooltip": "^5.28.1",
"reactour": "^1.19.4",
"redux": "^5.0.1",
"regex-parser": "^2.3.1",
"serve": "^14.2.4",
"styled-components": "^5.3.11",
"web-vitals": "^5.0.3",
"web-vitals": "^5.0.2",
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz"
},
"scripts": {
@@ -94,38 +94,38 @@
}
},
"devDependencies": {
"@babel/core": "^7.27.7",
"@babel/core": "^7.27.4",
"@babel/preset-env": "^7.27.2",
"@babel/preset-react": "^7.27.1",
"@babel/runtime-corejs2": "^7.27.6",
"@babel/runtime-corejs2": "^7.27.4",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@types/react": "^18.3.23",
"@vitejs/plugin-react": "^4.6.0",
"@vitejs/plugin-react-swc": "^3.10.2",
"@vitest/ui": "^3.2.4",
"@vitejs/plugin-react": "^4.5.1",
"@vitejs/plugin-react-swc": "^3.10.1",
"@vitest/ui": "^3.2.0",
"autoprefixer": "^10.4.21",
"babel-loader": "^10.0.0",
"commitizen": "^4.3.1",
"concurrently": "^9.2.0",
"concurrently": "^9.1.2",
"css-loader": "^7.1.2",
"daisyui": "^4.12.24",
"dotenv": "^16.6.1",
"eslint": "^9.30.0",
"eslint-plugin-prettier": "^5.5.1",
"dotenv": "^16.5.0",
"eslint": "^9.28.0",
"eslint-plugin-prettier": "^5.4.1",
"eslint-plugin-react": "^7.37.5",
"jsdom": "^26.1.0",
"lint-staged": "^16.1.2",
"postcss": "^8.5.6",
"prettier": "^3.6.2",
"lint-staged": "^16.1.0",
"postcss": "^8.5.4",
"prettier": "^3.5.3",
"pretty-quick": "^4.2.2",
"rollup-plugin-node-polyfills": "^0.2.1",
"tailwindcss": "^3.4.17",
"vite": "^6.3.5",
"vite-plugin-svgr": "^4.3.0",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.2.4"
"vitest": "^3.2.0"
},
"engines": {
"node": "18 || 20 || 22"
@@ -761,8 +761,6 @@
"Rotation-alert": "Seite drehen",
"bulk-import": "Massenimport",
"contacts-file": "Kontaktdatei (xlsx, csv)",
"import-guideline": "Laden Sie eine CSV- oder Excel-Datei mit den Spalten Name, Email und optional Phone hoch. Es werden nur die ersten 100 Kontakte importiert.",
"download-sample": "Beispieldatei herunterladen",
"100-records-only": "Derzeit können Sie nur bis zu 100 Datensätze importieren.",
"csv-excel-support-only": "Laden Sie eine Datei in einem der folgenden Formate hoch: CSV, XLSX oder XLS.",
"contact-imported": "{{imported}} Kontakte wurden importiert. {{failed}} Kontakte konnten nicht importiert werden.",
@@ -1035,11 +1033,5 @@
"choose-one":"Wählen Sie eine aus",
"search-templates": "Vorlagen durchsuchen…",
"search-documents": "Dokumente suchen…",
"search-contacts": "Kontakte durchsuchen…",
"invalid-email-found": "Ungültige E-Mail gefunden: {{email}}",
"duplicate-email-found": "Doppelte E-Mail gefunden: {{email}}",
"vertical": "Vertikal",
"horizontal": "Horizontal",
"billing": "Abrechnung",
"console": "Konsole"
"search-contacts": "Kontakte durchsuchen…"
}
@@ -761,8 +761,6 @@
"Rotation-alert": "Rotate page",
"bulk-import": "Bulk import",
"contacts-file": "Contacts file (xlsx, csv)",
"import-guideline": "Upload a CSV or Excel file with columns Name, Email and optional Phone. Only the first 100 records will be imported.",
"download-sample": "Download sample file",
"100-records-only": "Currently you can only import up to 100 records.",
"csv-excel-support-only": "Upload a file in one of the following formats: CSV, XLSX or XLS.",
"contact-imported": "{{imported}} contacts were imported. {{failed}} contacts failed to import.",
@@ -1035,11 +1033,5 @@
"choose-one":"Choose One",
"search-templates": "Search templates…",
"search-documents": "Search documents…",
"search-contacts": "Search contacts…",
"invalid-email-found": "Invalid email found: {{email}}",
"duplicate-email-found": "Duplicate email found: {{email}}",
"vertical": "Vertical",
"horizontal": "Horizontal",
"billing": "Billing",
"console": "Console"
"search-contacts": "Search contacts…"
}
@@ -761,8 +761,6 @@
"Rotation-alert": "Girar página",
"bulk-import": "Importación masiva",
"contacts-file": "Archivo de contactos (xlsx, csv)",
"import-guideline": "Sube un archivo CSV o Excel con las columnas Name, Email y opcionalmente Phone. Solo se importarán los primeros 100 contactos.",
"download-sample": "Descargar archivo de ejemplo",
"100-records-only": "Currently you can only import up to 100 records.",
"csv-excel-support-only": "Cargue un archivo en uno de los siguientes formatos: CSV, XLSX o XLS.",
"contact-imported": "Se importaron {{imported}} contactos. {{failed}} contactos no pudieron importarse.",
@@ -1035,11 +1033,5 @@
"choose-one":"Elige uno",
"search-templates": "Buscar plantillas…",
"search-documents": "Buscar documentos…",
"search-contacts": "Buscar contactos…",
"invalid-email-found": "Correo electrónico no válido encontrado: {{email}}",
"duplicate-email-found": "Correo electrónico duplicado encontrado: {{email}}",
"vertical": "Vertical",
"horizontal": "Horizontal",
"billing": "Facturación",
"console": "Consola"
"search-contacts": "Buscar contactos…"
}
@@ -761,8 +761,6 @@
"Rotation-alert": "Faire pivoter la page",
"bulk-import": "Importation en masse",
"contacts-file": "Fichier de contacts (xlsx, csv)",
"import-guideline": "Téléchargez un fichier CSV ou Excel avec les colonnes Name, Email et éventuellement Phone. Seuls les 100 premiers contacts seront importés.",
"download-sample": "Télécharger un fichier d'exemple",
"100-records-only": "Actuellement, vous ne pouvez importer que 100 enregistrements.",
"csv-excel-support-only": "Veuillez télécharger un fichier dans l'un des formats suivants : CSV, XLSX ou XLS.",
"contact-imported": "{{imported}} contacts ont été importés. {{failed}} contacts n'ont pas pu être importés.",
@@ -1035,11 +1033,5 @@
"choose-one":"Choisissez-en un",
"search-templates": "Rechercher des modèles…",
"search-documents": "Rechercher des documents…",
"search-contacts": "Rechercher des contacts…",
"invalid-email-found": "Adresse e-mail invalide trouvée : {{email}}",
"duplicate-email-found": "Adresse e-mail en double trouvée : {{email}}",
"vertical": "Vertical",
"horizontal": "Horizontal",
"billing": "Facturation",
"console": "Console"
"search-contacts": "Rechercher des contacts…"
}
@@ -355,7 +355,7 @@
"widgets-name": {
"signature": "हस्ताक्षर",
"stamp": "मोहर",
"initials": "प्रारंभिक अक्षर",
"initials": "हस्ताक्षर",
"name": "नाम",
"job title": "पद",
"company": "कंपनी",
@@ -389,7 +389,7 @@
"Email-verified-alert-2": "ईमेल पहले से ही सत्यापित है।",
"upload-stamp-image": "मोहर छवि अपलोड करें",
"draw": "आरेखित करें",
"type": "टाइप",
"type": "प्रकार",
"color-type": {
"red": "लाल",
"blue": "नीला",
@@ -761,8 +761,6 @@
"Rotation-alert": "पृष्ठ घुमाएँ",
"bulk-import": "थोक आयात",
"contacts-file": "संपर्क फ़ाइल (xlsx, csv)",
"import-guideline": "नाम, ईमेल और वैकल्पिक फोन कॉलम वाली CSV या Excel फ़ाइल अपलोड करें। केवल पहले 100 संपर्क आयात किए जाएंगे.",
"download-sample": "उदाहरण फ़ाइल डाउनलोड करें",
"100-records-only": "वर्तमान में आप केवल 100 रिकॉर्ड तक आयात कर सकते हैं।",
"csv-excel-support-only": "निम्नलिखित प्रारूपों में से किसी एक में फ़ाइल अपलोड करें: CSV, XLSX या XLS।",
"contact-imported": "{{imported}} संपर्क आयात किए गए। {{failed}} संपर्क आयात करने में विफल रहे।",
@@ -1035,11 +1033,5 @@
"choose-one":"एक का चयन",
"search-templates": "टेम्पलेट खोजें…",
"search-documents": "दस्तावेज़ खोजें…",
"search-contacts": "संपर्क खोजें…",
"invalid-email-found": "अमान्य ईमेल पाया गया: {{email}}",
"duplicate-email-found": "डुप्लिकेट ईमेल पाया गया: {{email}}",
"vertical": "वर्टिकल",
"horizontal": "हॉरिज़ॉन्टल",
"billing": "बिलिंग",
"console": "कंसोल"
"search-contacts": "संपर्क खोजें…"
}
@@ -761,8 +761,6 @@
"Rotation-alert": "Ruota pagina",
"bulk-import": "Importazione massiva",
"contacts-file": "File contatti (xlsx, csv)",
"import-guideline": "Carica un file CSV o Excel con le colonne Name, Email e opzionale Phone. Verranno importati solo i primi 100 contatti.",
"download-sample": "Scarica file di esempio",
"100-records-only": "Attualmente puoi importare solo fino a 100 record.",
"csv-excel-support-only": "Carica un file nei seguenti formati: CSV, XLSX o XLS.",
"contact-imported": "{{imported}} contatti importati. {{failed}} contatti non sono stati importati.",
@@ -1035,11 +1033,5 @@
"choose-one":"Scegline uno",
"search-templates": "Cerca modelli…",
"search-documents": "Cerca documenti…",
"search-contacts": "Cerca contatti…",
"invalid-email-found": "Email non valida trovata: {{email}}",
"duplicate-email-found": "Email duplicata trovata: {{email}}",
"vertical": "Verticale",
"horizontal": "Orizzontale",
"billing": "Fatturazione",
"console": "Console"
"search-contacts": "Cerca contatti…"
}
-4
View File
@@ -1,4 +0,0 @@
Name,Email,Phone
John Doe,john@example.com,1234567890
Jane Smith,jane@example.com,9876543210
Foo Bar,foo@example.com,5555555555
1 Name Email Phone
2 John Doe john@example.com 1234567890
3 Jane Smith jane@example.com 9876543210
4 Foo Bar foo@example.com 5555555555
+5 -22
View File
@@ -15,8 +15,6 @@ const BulkSendUi = (props) => {
const [isSignatureExist, setIsSignatureExist] = useState();
const [isDisableBulkSend, setIsDisableBulkSend] = useState(false);
const [isLoader, setIsLoader] = useState(false);
const [signers, setSigners] = useState([]);
const [emails, setEmails] = useState([]);
useEffect(() => {
signatureExist();
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -49,14 +47,7 @@ const BulkSendUi = (props) => {
(() => {
if (props?.Placeholders?.length > 0) {
let users = [];
let emails = [];
props?.Placeholders?.forEach((element) => {
const signerEmail = element?.email || element?.signerPtr?.Email;
// only add when there's a non-empty signerEmail
if (signerEmail) {
emails = [...emails, signerEmail];
}
if (!element.signerObjId) {
users = [
...users,
@@ -69,10 +60,7 @@ const BulkSendUi = (props) => {
];
}
});
setEmails(emails);
setForms((prevForms) => [...prevForms, { Id: 1, fields: users }]);
const signer = props.item?.Signers?.filter((x) => x?.objectId);
setSigners(signer);
}
})();
// eslint-disable-next-line
@@ -94,16 +82,10 @@ const BulkSendUi = (props) => {
function validateEmails(data) {
for (const item of data) {
let email = "";
for (const field of item.fields) {
if (!emailRegex.test(field.email)) {
alert(t("invalid-email-found", { email: field.email }));
alert(`Invalid email found: ${field.email}`);
return false;
} else if (email === field.email || emails?.includes(field.email)) {
alert(t("duplicate-email-found", { email: field.email }));
return false;
} else {
email = field.email;
}
}
}
@@ -161,14 +143,15 @@ const BulkSendUi = (props) => {
Documents.push({
...props.item,
Placeholders: updatedPlaceholders,
Signers: signers ? [...signers, ...existSigner] : [...existSigner]
Signers: props.item.Signers
? [...props.item.Signers, ...existSigner]
: [...existSigner]
});
} else {
Documents.push({
...props.item,
Placeholders: updatedPlaceholders,
SignatureType: props.signatureType,
Signers: signers
SignatureType: props.signatureType
});
}
});
+17 -13
View File
@@ -21,7 +21,6 @@ const Header = ({ showSidebar, setIsMenu, isConsole }) => {
const image = localStorage.getItem("profileImg") || dp;
const [isOpen, setIsOpen] = useState(false);
const [applogo, setAppLogo] = useState("");
const [isDarkTheme, setIsDarkTheme] = useState();
const toggleDropdown = () => {
setIsOpen(!isOpen);
@@ -89,15 +88,22 @@ const Header = ({ showSidebar, setIsMenu, isConsole }) => {
useEffect(() => {
const updateThemeStatus = () => {
const isDarkTheme =
const updateLogoForTheme = () => {
const isDarkMode =
document.documentElement.getAttribute("data-theme") === "opensigndark";
setIsDarkTheme(isDarkTheme);
const logo = isDarkMode
? "/static/js/assets/images/logo-dark.png" // Path to the dark mode logo
: appInfo.applogo; // Use current logo for light mode
if (applogo !== logo) {
setAppLogo(logo);
}
};
updateThemeStatus();
// Set the logo immediately based on the current theme
updateLogoForTheme();
const observer = new MutationObserver(() => {
updateThemeStatus();
updateLogoForTheme();
});
observer.observe(document.documentElement, {
@@ -106,11 +112,11 @@ const Header = ({ showSidebar, setIsMenu, isConsole }) => {
});
return () => observer.disconnect();
}, []);
}, [applogo]);
return (
<div>
<div className="op-navbar bg-base-100 shadow touch-none">
<div className="op-navbar bg-base-100 shadow">
<div className="flex-none">
<button
className="op-btn op-btn-square op-btn-ghost focus:outline-none hover:bg-transparent op-btn-sm no-animation"
@@ -123,11 +129,9 @@ const Header = ({ showSidebar, setIsMenu, isConsole }) => {
<div className="h-[25px] md:h-[40px] w-auto overflow-hidden">
{applogo && (
<img
src={
isDarkTheme
? "/static/js/assets/images/logo-dark.png"
: applogo
}
className="object-contain h-full w-auto"
src={applogo}
alt="logo"
/>
)}
</div>
@@ -148,7 +148,7 @@ const DashboardCard = (props) => {
);
let arr = [];
for (const obj of listData) {
const isSigner = obj.Signers?.some(
const isSigner = obj.Signers.some(
(item) => item.UserId.objectId === currentUser.id
);
if (isSigner) {
@@ -134,7 +134,7 @@ function DashboardReport(props) {
const listData = res.data?.result.filter((x) => x.Signers.length > 0);
let arr = [];
for (const obj of listData) {
const isSigner = obj.Signers?.some(
const isSigner = obj.Signers.some(
(item) => item.UserId.objectId === currentUser
);
if (isSigner) {
@@ -405,7 +405,7 @@ function DriveBody(props) {
</tr>
)
) : listType === "list" && data.Type === "Folder" ? (
<div className="relative w-[100px] h-[100px] mx-2 my-3">
<div key={ind} className="relative w-[100px] h-[100px] mx-2 my-3">
<ContextMenu.Root>
<ContextMenu.Trigger className="flex flex-col justify-center items-center select-none-cls">
{/* folder */}
@@ -447,6 +447,7 @@ function DriveBody(props) {
)}
</div>
</ContextMenu.Trigger>
<ContextMenu.Portal>
<ContextMenu.Content
className="ContextMenuContent"
@@ -640,7 +641,7 @@ function DriveBody(props) {
) : (
<div className="flex flex-row flex-wrap items-center mt-1 pb-[20px] mx-[5px]">
{props.pdfData.map((data, ind) => {
return <React.Fragment key={ind}>{handleFolderData(data, ind, "list")}</React.Fragment>;
return <div key={ind}>{handleFolderData(data, ind, "list")}</div>;
})}
</div>
)}
@@ -12,9 +12,12 @@ function BorderResize(props) {
return (
<div
style={{ width: getHeight() || "14px", height: getHeight() || "14px" }}
style={{
width: getHeight() || "14px",
height: getHeight() || "14px"
}}
className={`${props.right ? `-right-[12px]` : "-right-[2px]"} ${
props.top ? `-bottom-[12px]` : "-bottom-[2px]"
props.top ? `-bottom-[12px]` : "-bottom-[2px] "
} absolute inline-block hover:cursor-sw-resize border-r-[3px] border-b-[3px] border-[#188ae2]`}
></div>
);
@@ -1,7 +1,6 @@
import { useState, useEffect, useRef } from "react";
const Cell = ({
isEnabled,
count,
h,
value,
@@ -16,11 +15,10 @@ const Cell = ({
hint
}) => (
<div
className={`${isEnabled ? "bg-white border-gray-400" : "select-none-cls pointer-events-none border-gray-500"} flex items-center justify-center border-[1px]`}
className="flex items-center justify-center border border-gray-800 bg-white"
style={{ flex: `0 0 ${100 / count}%`, height: h }}
>
<input
disabled={!isEnabled}
maxLength={1}
value={value}
readOnly={!editable}
@@ -29,7 +27,7 @@ const Cell = ({
onKeyDown={editable ? (e) => onKeyDown && onKeyDown(e, index) : undefined}
// trigger validation when leaving a cell
onBlur={editable ? (e) => onBlur && onBlur(e, index) : undefined}
className={`${isEnabled ? "placeholder-gray-300" : "placeholder-gray-500"} w-full text-center focus:outline-none bg-transparent`}
className="w-full text-center focus:outline-none bg-transparent placeholder-gray-300"
placeholder={hint}
style={{ fontFamily: "Arial, sans-serif", fontSize, color: fontColor }}
/>
@@ -37,7 +35,6 @@ const Cell = ({
);
export default function CellsWidget({
isEnabled,
count = 8,
height = 40,
value = "",
@@ -123,7 +120,6 @@ export default function CellsWidget({
{cells.map((val, i) => (
<Cell
key={i}
isEnabled={isEnabled}
count={cellCount}
h={height}
value={val}
@@ -389,8 +389,8 @@ function DropdownWidgetOption(props) {
checked={layout.toLowerCase() === data.toLowerCase()}
onChange={() => setLayout(data.toLowerCase())}
/>
<label className="text-[13px] font-500 mb-0">
{t(data)}
<label className="text-[13px] font-500 mb-0 capitalize">
{data}
</label>
</div>
))}
+24 -62
View File
@@ -2,10 +2,7 @@ import React, { useRef, useState } from "react";
import PrevNext from "./PrevNext";
import {
base64ToArrayBuffer,
decryptPdf,
deletePdfPage,
flattenPdf,
getFileAsArrayBuffer,
handleDownloadCertificate,
handleDownloadPdf,
handleRemoveWidgets,
@@ -86,42 +83,7 @@ function Header(props) {
return;
}
try {
let uploadedPdfBytes = await file.arrayBuffer();
try {
uploadedPdfBytes = await flattenPdf(uploadedPdfBytes);
} catch (err) {
if (err?.message?.includes("is encrypted")) {
try {
const pdfFile = await decryptPdf(file, "");
const pdfArrayBuffer = await getFileAsArrayBuffer(pdfFile);
uploadedPdfBytes = await flattenPdf(pdfArrayBuffer);
} catch (err) {
if (err?.response?.status === 401) {
const password = prompt(
`PDF "${file.name}" is password-protected. Enter password:`
);
if (password) {
try {
const pdfFile = await decryptPdf(file, password);
const pdfArrayBuffer = await getFileAsArrayBuffer(pdfFile);
uploadedPdfBytes = await flattenPdf(pdfArrayBuffer);
// Upload the file to Parse Server
} catch (err) {
console.error("Incorrect password or decryption failed", err);
alert("Incorrect password or decryption failed.");
}
} else {
alert("Please provided Password.");
}
} else {
console.log("Err ", err);
alert("error while uploading pdf.");
}
}
} else {
alert("error while uploading pdf.");
}
}
const uploadedPdfBytes = await file.arrayBuffer();
const uploadedPdfDoc = await PDFDocument.load(uploadedPdfBytes, {
ignoreEncryption: true
});
@@ -167,7 +129,7 @@ function Header(props) {
{isMobile && props?.isShowHeader ? (
<div
id="navbar"
className="stickyHead touch-none"
className="stickyHead"
style={{
width: window.innerWidth + "px"
}}
@@ -379,28 +341,28 @@ function Header(props) {
</span>
</div>
</DropdownMenu.Item>
<DropdownMenu.Item
className="DropdownMenuItem"
onClick={() => setIsDeletePage(true)}
>
<div className="flex flex-row">
<i className="fa-light fa-trash text-gray-500 2xl:text-[30px] mr-[3px]"></i>
<span className="font-[500]">
{t("delete-page")}
</span>
</div>
</DropdownMenu.Item>
<DropdownMenu.Item
className="DropdownMenuItem"
onClick={() => setIsReorderModal(true)}
>
<div className="flex flex-row">
<i className="fa-light fa-list-ol text-gray-500 2xl:text-[30px] mr-[3px]"></i>
<span className="font-[500]">
{t("reorder-pages")}
</span>
</div>
</DropdownMenu.Item>
<DropdownMenu.Item
className="DropdownMenuItem"
onClick={() => setIsDeletePage(true)}
>
<div className="flex flex-row">
<i className="fa-light fa-trash text-gray-500 2xl:text-[30px] mr-[3px]"></i>
<span className="font-[500]">
{t("delete-page")}
</span>
</div>
</DropdownMenu.Item>
<DropdownMenu.Item
className="DropdownMenuItem"
onClick={() => setIsReorderModal(true)}
>
<div className="flex flex-row">
<i className="fa-light fa-list-ol text-gray-500 2xl:text-[30px] mr-[3px]"></i>
<span className="font-[500]">
{t("reorder-pages")}
</span>
</div>
</DropdownMenu.Item>
<DropdownMenu.Item
className="DropdownMenuItem"
+2 -40
View File
@@ -1,11 +1,8 @@
import { useRef, useState } from "react";
import React, { useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import {
base64ToArrayBuffer,
decryptPdf,
deletePdfPage,
flattenPdf,
getFileAsArrayBuffer,
handleRemoveWidgets,
reorderPdfPages
} from "../../constant/Utils";
@@ -73,42 +70,7 @@ function PdfZoom(props) {
return;
}
try {
let uploadedPdfBytes = await file.arrayBuffer();
try {
uploadedPdfBytes = await flattenPdf(uploadedPdfBytes);
} catch (err) {
if (err?.message?.includes("is encrypted")) {
try {
const pdfFile = await decryptPdf(file, "");
const pdfArrayBuffer = await getFileAsArrayBuffer(pdfFile);
uploadedPdfBytes = await flattenPdf(pdfArrayBuffer);
} catch (err) {
if (err?.response?.status === 401) {
const password = prompt(
`PDF "${file.name}" is password-protected. Enter password:`
);
if (password) {
try {
const pdfFile = await decryptPdf(file, password);
const pdfArrayBuffer = await getFileAsArrayBuffer(pdfFile);
uploadedPdfBytes = await flattenPdf(pdfArrayBuffer);
// Upload the file to Parse Server
} catch (err) {
console.error("Incorrect password or decryption failed", err);
alert("Incorrect password or decryption failed.");
}
} else {
alert("Please provided Password.");
}
} else {
console.log("Err ", err);
alert("error while uploading pdf.");
}
}
} else {
alert("error while uploading pdf.");
}
}
const uploadedPdfBytes = await file.arrayBuffer();
const uploadedPdfDoc = await PDFDocument.load(uploadedPdfBytes, {
ignoreEncryption: true
});
@@ -14,8 +14,7 @@ import {
radioButtonWidget,
textInputWidget,
cellsWidget,
textWidget,
selectFormat
textWidget
} from "../../constant/Utils";
import PlaceholderType from "./PlaceholderType";
import moment from "moment";
@@ -26,6 +25,37 @@ import { useDispatch } from "react-redux";
import { setIsShowModal } from "../../redux/reducers/widgetSlice";
import { themeColor } from "../../constant/const";
const selectFormat = (data) => {
switch (data) {
case "L":
return "MM/dd/yyyy";
case "MM/DD/YYYY":
return "MM/dd/yyyy";
case "DD-MM-YYYY":
return "dd-MM-yyyy";
case "DD/MM/YYYY":
return "dd/MM/yyyy";
case "LL":
return "MMMM dd, yyyy";
case "DD MMM, YYYY":
return "dd MMM, yyyy";
case "YYYY-MM-DD":
return "yyyy-MM-dd";
case "MM-DD-YYYY":
return "MM-dd-yyyy";
case "MM.DD.YYYY":
return "MM.dd.yyyy";
case "MMM DD, YYYY":
return "MMM dd, yyyy";
case "MMMM DD, YYYY":
return "MMMM dd, yyyy";
case "DD MMMM, YYYY":
return "dd MMMM, yyyy";
default:
return "MM/dd/yyyy";
}
};
//function to get default format
const getDefaultFormat = (dateFormat) => dateFormat || "MM/dd/yyyy";
@@ -80,9 +110,7 @@ function Placeholder(props) {
"MMM DD, YYYY",
"LL",
"DD MMM, YYYY",
"DD MMMM, YYYY",
"DD.MM.YYYY",
"DD/MM/YYYY"
"DD MMMM, YYYY"
];
useEffect(() => {
@@ -104,18 +132,15 @@ function Placeholder(props) {
if (selectDate && selectDate.format === "dd-MM-yyyy") {
const [day, month, year] = selectDate.date.split("-");
date = new Date(`${year}-${month}-${day}`);
} else if (selectDate && selectDate.format === "dd.MM.yyyy") {
const [day, month, year] = selectDate.date.split(".");
date = new Date(`${year}.${month}.${day}`);
} else if (selectDate && selectDate.format === "dd/MM/yyyy") {
const [day, month, year] = selectDate.date.split("/");
date = new Date(`${year}/${month}/${day}`);
} else {
date = new Date(selectDate?.date);
}
const milliseconds = date.getTime();
const newDate = moment(milliseconds).format(data);
const dateObj = { date: newDate, format: selectFormat(data) };
const dateObj = {
date: newDate,
format: selectFormat(data)
};
updateDate.push(dateObj);
});
setDateFormat(updateDate);
@@ -366,12 +391,9 @@ function Placeholder(props) {
//function to save date and format on local array onchange date and onclick format
const handleSaveDate = (data, isDateChange) => {
const isSpecialDateFormat =
data?.format &&
["dd-MM-yyyy", "dd.MM.yyyy", "dd/MM/yyyy"].includes(data?.format);
let updateDate = data.date;
let date;
if (isSpecialDateFormat) {
if (data?.format === "dd-MM-yyyy") {
date = isDateChange
? moment(updateDate).format(changeDateToMomentFormat(data.format))
: updateDate;
@@ -718,7 +740,7 @@ function Placeholder(props) {
return "not-allowed";
}
} else {
return "move";
return "all-scroll";
}
};
@@ -837,16 +859,8 @@ function Placeholder(props) {
props.isNeedSign && props.data?.Id !== props?.uniqueId && "0.4",
background: handleBackground()
}}
onDrag={(_, d) => {
onDrag={() => {
props.handleTabDrag && props.handleTabDrag(props.pos.key);
props.showGuidelines &&
props.showGuidelines(
true,
d.x,
d.y,
d.node.offsetWidth,
d.node.offsetHeight
);
}}
size={{
width:
@@ -867,26 +881,8 @@ function Placeholder(props) {
calculateFont(props.pos.options?.fontSize, true)
}
maxHeight="auto"
onResizeStart={(e, dir, ref) => {
onResizeStart={() => {
props.setIsResize && props.setIsResize(true);
props.showGuidelines &&
props.showGuidelines(
true,
xPos(props.pos, props.isSignYourself),
yPos(props.pos, props.isSignYourself),
ref.offsetWidth,
ref.offsetHeight
);
}}
onResize={(e, dir, ref, delta, position) => {
props.showGuidelines &&
props.showGuidelines(
true,
position.x,
position.y,
ref.offsetWidth,
ref.offsetHeight
);
}}
onResizeStop={(e, direction, ref) => {
setTimeout(() => {
@@ -904,7 +900,6 @@ function Placeholder(props) {
props.data && props.data.Id,
props.isResize
);
props.showGuidelines && props.showGuidelines(false);
}}
onDragStop={(event, dragElement) => {
props.handleStop &&
@@ -914,9 +909,6 @@ function Placeholder(props) {
props.data?.Id,
props.pos?.key
);
props.isDragging &&
props.showGuidelines &&
props.showGuidelines(false);
}}
position={{
x: xPos(props.pos, props.isSignYourself),
@@ -953,24 +945,24 @@ function Placeholder(props) {
![radioButtonWidget, "checkbox"].includes(props.pos.type) &&
props.pos.key === props?.currWidgetsDetails?.key && <BorderResize />
)}
{/* 1- Show a border if props.pos.key === props?.currWidgetsDetails?.key, indicating the current user's selected widget.
2- If props.isShowBorder is true, display border for all widgets.
3- Use the combination of props?.isAlllowModify and !props?.assignedWidgetId.includes(props.pos.key) to determine when to show border:
3.1- When isAlllowModify is true, show border.
3.2- Do not display border for widgets already assigned (props.assignedWidgetId.includes(props.pos.key) is true).
{/* 1- Show a ouline if props.pos.key === props?.currWidgetsDetails?.key, indicating the current user's selected widget.
2- If props.isShowBorder is true, display ouline for all widgets.
3- Use the combination of props?.isAlllowModify and !props?.assignedWidgetId.includes(props.pos.key) to determine when to show ouline:
3.1- When isAlllowModify is true, show ouline.
3.2- Do not display ouline for widgets already assigned (props.assignedWidgetId.includes(props.pos.key) is true).
*/}
{props.pos.key === props?.currWidgetsDetails?.key &&
(props.isShowBorder ||
(props?.isAlllowModify &&
!props?.assignedWidgetId.includes(props.pos.key))) && (
<div
style={{ borderColor: themeColor }}
className="w-[calc(100%+21px)] h-[calc(100%+21px)] cursor-move absolute inline-block border-[1px] border-dashed"
></div>
)}
<div
className="flex items-stretch justify-center"
className={`${
props.pos.key === props?.currWidgetsDetails?.key &&
(props.isShowBorder ||
(props?.isAlllowModify &&
!props?.assignedWidgetId.includes(props.pos.key)))
? "outline-[0.3px] outline-dashed outline-offset-[10px]"
: ""
} flex items-stretch justify-center`}
style={{
outlineColor: themeColor,
left: xPos(props.pos, props.isSignYourself),
top: yPos(props.pos, props.isSignYourself),
width: "100%",
@@ -1,4 +1,4 @@
import React, { useEffect, useState, forwardRef } from "react";
import React, { useEffect, useState, forwardRef, useRef } from "react";
import {
getMonth,
getYear,
@@ -36,6 +36,7 @@ function PlaceholderType(props) {
const widgetData =
props.pos?.options?.response ?? props.pos?.options?.defaultValue ?? "";
const widgetTypeTranslation = t(`widgets-name.${props?.pos?.type}`);
const inputRef = useRef(null);
const [widgetValue, setwidgetValue] = useState();
const [selectedCheckbox, setSelectedCheckbox] = useState([]);
const [hint, setHint] = useState("");
@@ -157,7 +158,7 @@ function PlaceholderType(props) {
const checkBoxWrapperClass = `flex items-start ${
checkBoxLayout === "horizontal"
? `flex-row flex-wrap ${isMultipleCheckbox ? "gap-x-2" : ""}`
: `flex-col ${isMultipleCheckbox ? "gap-y-[3px]" : ""}`
: `flex-col ${isMultipleCheckbox ? "gap-y-[5px]" : ""}`
}`; // Using gap-y-1 for consistency, adjust if needed
return (
@@ -192,6 +193,7 @@ function PlaceholderType(props) {
case textInputWidget:
return props.isSignYourself || iswidgetEnable ? (
<textarea
ref={inputRef}
placeholder={hint || t("widgets-name.text")}
rows={1}
value={widgetValue}
@@ -224,7 +226,6 @@ function PlaceholderType(props) {
props.isPlaceholder || props.isSignYourself || props.isSelfSign;
return (
<CellsWidget
isEnabled={iswidgetEnable}
count={count}
height={height}
value={cells.join("")}
@@ -275,6 +276,7 @@ function PlaceholderType(props) {
return iswidgetEnable ? (
<textarea
readOnly
ref={inputRef}
placeholder={hint || widgetTypeTranslation}
rows={1}
value={widgetValue}
@@ -297,6 +299,7 @@ function PlaceholderType(props) {
return iswidgetEnable ? (
<textarea
readOnly
ref={inputRef}
placeholder={hint || widgetTypeTranslation}
rows={1}
value={widgetValue}
@@ -319,6 +322,7 @@ function PlaceholderType(props) {
return iswidgetEnable ? (
<textarea
readOnly
ref={inputRef}
placeholder={hint || widgetTypeTranslation}
rows={1}
value={widgetValue}
@@ -424,6 +428,7 @@ function PlaceholderType(props) {
return iswidgetEnable ? (
<textarea
readOnly
ref={inputRef}
placeholder={hint || widgetTypeTranslation}
rows={1}
value={widgetValue}
@@ -447,7 +452,7 @@ function PlaceholderType(props) {
const isOnlyOneBtn = props.pos.options?.values?.length > 0 ? true : false;
const radioWrapperClass = `flex items-start ${
radioLayout === "horizontal"
? `flex-row flex-wrap ${isOnlyOneBtn ? "gap-x-[10px]" : ""}`
? `flex-row flex-wrap ${isOnlyOneBtn ? "gap-x-2" : ""}`
: `flex-col ${isOnlyOneBtn ? "gap-y-[5px]" : ""}`
}`; // Using gap-y-1 for consistency, adjust if needed
return (
@@ -82,7 +82,7 @@ const RecipientList = (props) => {
};
return (
<>
{props.signersdata?.length > 0 &&
{props.signersdata.length > 0 &&
props.signersdata.map((obj, ind) => {
return (
<div
@@ -3,12 +3,7 @@ import { useTranslation } from "react-i18next";
import { Document, Page } from "react-pdf";
import { useSelector } from "react-redux";
import { PDFDocument } from "pdf-lib";
import {
base64ToArrayBuffer,
decryptPdf,
flattenPdf,
getFileAsArrayBuffer
} from "../../constant/Utils";
import { base64ToArrayBuffer } from "../../constant/Utils";
import { maxFileSize } from "../../constant/const";
function RenderAllPdfPage(props) {
@@ -92,42 +87,7 @@ function RenderAllPdfPage(props) {
return;
}
try {
let uploadedPdfBytes = await file.arrayBuffer();
try {
uploadedPdfBytes = await flattenPdf(uploadedPdfBytes);
} catch (err) {
if (err?.message?.includes("is encrypted")) {
try {
const pdfFile = await decryptPdf(file, "");
const pdfArrayBuffer = await getFileAsArrayBuffer(pdfFile);
uploadedPdfBytes = await flattenPdf(pdfArrayBuffer);
} catch (err) {
if (err?.response?.status === 401) {
const password = prompt(
`PDF "${file.name}" is password-protected. Enter password:`
);
if (password) {
try {
const pdfFile = await decryptPdf(file, password);
const pdfArrayBuffer = await getFileAsArrayBuffer(pdfFile);
uploadedPdfBytes = await flattenPdf(pdfArrayBuffer);
// Upload the file to Parse Server
} catch (err) {
console.error("Incorrect password or decryption failed", err);
alert("Incorrect password or decryption failed.");
}
} else {
alert("Please provided Password.");
}
} else {
console.log("Err ", err);
alert("error while uploading pdf.");
}
}
} else {
alert("error while uploading pdf.");
}
}
const uploadedPdfBytes = await file.arrayBuffer();
const uploadedPdfDoc = await PDFDocument.load(uploadedPdfBytes, {
ignoreEncryption: true
});
@@ -209,8 +169,8 @@ function RenderAllPdfPage(props) {
onChange={handleFileUpload}
/>
<i className="fa-light fa-plus text-gray-500"></i>
<span className="text-xs lg:text-sm text-base-content">
{t("add-pages")}
<span className="text-xs lg:text-sm text-base-content ">
Add pages
</span>
</button>
)}
+4 -66
View File
@@ -1,4 +1,4 @@
import React, { useState, useRef } from "react";
import React, { useState } from "react";
import RSC from "react-scrollbars-custom";
import { Document, Page } from "react-pdf";
import {
@@ -11,45 +11,13 @@ import {
import Placeholder from "./Placeholder";
import Alert from "../../primitives/Alert";
import { useTranslation } from "react-i18next";
import usePdfPinchZoom from "../../hook/usePdfPinchZoom";
function RenderPdf(props) {
const { t } = useTranslation();
const [scaledHeight, setScaledHeight] = useState();
const [guideline, setGuideline] = useState({
show: false,
x1: 0,
x2: 0,
y1: 0,
y2: 0
});
//check isGuestSigner is present in local if yes than handle login flow header in mobile view
const isGuestSigner = localStorage.getItem("isGuestSigner");
const pdfContainerRef = useRef(null);
// enable pinch to zoom only on actual pdf wrapper
usePdfPinchZoom(
pdfContainerRef,
props.scale,
props.setScale,
props.setZoomPercent
);
const handleGuideline = (isShow, x = 0, y = 0, width = 0, height = 0) => {
if (isShow) {
setGuideline({
show: true,
x1: x,
x2: x + width,
y1: y,
y2: y + height
});
} else {
setGuideline({ show: false, x1: 0, x2: 0, y1: 0, y2: 0 });
}
};
// handle signature block width and height according to screen
const posWidth = (pos, signYourself) => {
const containerScale = getContainerScale(
@@ -147,7 +115,6 @@ function RenderPdf(props) {
isSignYourself={false}
posWidth={posWidth}
posHeight={posHeight}
showGuidelines={handleGuideline}
isDragging={props.isDragging}
pdfDetails={props.pdfDetails}
unSignedWidgetId={props.unSignedWidgetId}
@@ -237,17 +204,14 @@ function RenderPdf(props) {
data-tut={isMobile ? "reactourForth" : undefined}
className={
isMobile
? `${isGuestSigner ? "30px" : ""} border-[0.1px] border-[#ebe8e8] overflow-x-auto relative`
: "relative"
? `${isGuestSigner ? "30px" : ""} border-[0.1px] border-[#ebe8e8] overflow-x-auto`
: ""
}
style={{
width:
props.containerWH?.width && props.containerWH?.width * props.scale
}}
ref={(node) => {
pdfContainerRef.current = node;
props.drop && props.drop(node);
}}
ref={props.drop}
id="container"
>
{props.pdfLoad !== false &&
@@ -294,7 +258,6 @@ function RenderPdf(props) {
isSignYourself={false}
posWidth={posWidth}
posHeight={posHeight}
showGuidelines={handleGuideline}
isDragging={props.isDragging}
setIsValidate={props.setIsValidate}
setIsRadio={props.setIsRadio}
@@ -362,7 +325,6 @@ function RenderPdf(props) {
isSignYourself={true}
posWidth={posWidth}
posHeight={posHeight}
showGuidelines={handleGuideline}
pdfDetails={props.pdfDetails[0]}
isDragging={props.isDragging}
setIsCheckbox={props.setIsCheckbox}
@@ -427,30 +389,6 @@ function RenderPdf(props) {
}}
/>
</Document>
{guideline.show && (
<>
{/* top guide */}
<div
className="absolute pointer-events-none z-[1000] left-0 w-full border-t-[1px] border-dashed border-[#3b82f6]"
style={{ top: guideline.y1 }}
/>
{/* bottom guide */}
<div
className="absolute pointer-events-none z-[1000] left-0 w-full border-t-[1px] border-dashed border-[#3b82f6]"
style={{ top: guideline.y2 }}
/>
{/* left guide */}
<div
className="absolute pointer-events-none z-[1000] top-0 h-full border-l-[1px] border-dashed border-[#3b82f6]"
style={{ left: guideline.x1 }}
/>
{/* right guide */}
<div
className="absolute pointer-events-none z-[1000] top-0 h-full border-l-[1px] border-dashed border-[#3b82f6]"
style={{ left: guideline.x2 }}
/>
</>
)}
</div>
</RSC>
</>
@@ -123,23 +123,6 @@ const WidgetNameModal = (props) => {
}
};
const handleChangeValidateInput = (e) => {
if (e) {
if (e.target.value === "ssn") {
setFormdata({
...formdata,
[e.target.name]: e.target.value,
hint: "xxx-xx-xxxx",
cellCount: 11
});
} else {
setFormdata({ ...formdata, [e.target.name]: e.target.value });
}
} else {
setFormdata({ ...formdata, textvalidate: "" });
}
};
const handledefaultChange = (e) => {
if (formdata.textvalidate) {
const regexObject = RegexParser(handleValidation(formdata.textvalidate));
@@ -165,7 +148,7 @@ const WidgetNameModal = (props) => {
//allow space in text regex
return "/^[a-zA-Z ]+$/";
case "ssn":
return "/^(?!000|666|9\\d{2})\\d{3}-(?!00)\\d{2}-(?!0000)\\d{4}$/";
return "^\\d{3}-\\d{2}-\\d{4}$";
default:
return type;
}
@@ -17,8 +17,7 @@ import {
textInputWidget,
cellsWidget,
textWidget,
years,
convertTextToImg
years
} from "../../constant/Utils";
import CellsWidget from "./CellsWidget";
import DatePicker from "react-datepicker";
@@ -68,11 +67,6 @@ const fontOptions = [
{ value: "Delicious Handrawn" }
// Add more font options as needed
];
const allColor = ["blue", "red", "black"];
const textInputcls =
"op-input op-input-bordered op-input-sm focus:outline-none text-base-content hover:border-base-content w-full text-xs";
const isTabCls = "bg-[#002864] text-white rounded-[15px] px-[10px] py-[4px]";
function WidgetsValueModal(props) {
const dispatch = useDispatch();
const saveSignCheckbox = useSelector(
@@ -173,7 +167,10 @@ function WidgetsValueModal(props) {
)
: new Date()
);
const allColor = ["blue", "red", "black"];
const textInputcls =
"op-input op-input-bordered op-input-sm focus:outline-none text-base-content hover:border-base-content w-full text-xs";
const isTabCls = "bg-[#002864] text-white rounded-[15px] px-[10px] py-[4px]";
useEffect(() => {
if (
["name", "email", "job title", "company", textInputWidget].includes(
@@ -190,8 +187,6 @@ function WidgetsValueModal(props) {
} else {
setHint(currWidgetsDetails?.type);
}
} else if (["signature", "initials"].includes(currWidgetsDetails?.type)) {
setIsTab(currWidgetsDetails?.signatureType);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currWidgetsDetails]); // Added currWidgetsDetails to dependency array for reset logic
@@ -350,6 +345,7 @@ function WidgetsValueModal(props) {
const placeholderIndex = signer.placeHolder.findIndex(
(x) => x.pageNumber === pageNumber
);
const updatedPlaceholders = onSaveSign(
type,
signer.placeHolder,
@@ -362,9 +358,7 @@ function WidgetsValueModal(props) {
typedSignature,
isAutoSign,
widgetsType,
isApplyAll,
fontSelect,
penColor
isApplyAll
);
return {
...signer,
@@ -385,9 +379,7 @@ function WidgetsValueModal(props) {
imgWH,
isDefaultSign,
isTypeText,
typedSignature,
false,
widgetsType
typedSignature
);
setXyPosition(getUpdatePosition);
}
@@ -413,9 +405,7 @@ function WidgetsValueModal(props) {
if (getIndex !== -1) {
setIsSignTypes(true);
const tab = signatureTypes?.[getIndex].name;
if (currWidgetsDetails?.signatureType === "type") {
setIsTab("type");
} else if (tab === "draw") {
if (tab === "draw") {
setIsTab("draw");
} else if (tab === "upload") {
setIsImageSelect(true);
@@ -629,7 +619,7 @@ function WidgetsValueModal(props) {
if (isTab === "type") {
setSignature("");
handleSaveSignature(
"type",
null,
false,
!currWidgetsDetails?.type === "initials" && textWidth > 150
? 150
@@ -704,43 +694,91 @@ function WidgetsValueModal(props) {
}
}
if (isTab === "type") {
//trim user name or typed name value to show in initial signature
const trimmedName = typedSignature
? typedSignature?.trim()
: currentUserName?.trim();
//get full name of user
const fullUserName = typedSignature || currentUserName;
const firstCharacter = trimmedName?.charAt(0);
const userName =
currWidgetsDetails?.type === "initials" ? firstCharacter : fullUserName;
currWidgetsDetails?.type === "initials"
? firstCharacter
: typedSignature;
const signatureValue = currWidgetsDetails?.typeSignature;
setTypedSignature(signatureValue || userName || "");
convertToImg(fontSelect, signatureValue || userName || "");
convertToImg(fontSelect, userName);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isTab]);
// function for convert input text value in image
const convertToImg = async (fontStyle, text, color) => {
const maxWidth = currWidgetsDetails?.Width;
const maxHeight = currWidgetsDetails?.Height;
const widgetDims = { Width: maxWidth, Height: maxHeight };
const fontFamily = fontStyle || fontSelect || "Fasthand";
// 1) Read the max widget dimensions:
const maxWidth = currWidgetsDetails.Width; // e.g. 150 px
const maxHeight = currWidgetsDetails.Height; // e.g. 40 px
// 2) Pick a “baseline” font size for measurement:
const baselineFontSizePx = 40;
const chosenFontFamily = fontStyle || fontSelect || "Fasthand";
const fillColor = color || penColor;
const dataUrl = convertTextToImg(fontFamily, text, fillColor, widgetDims);
// 3) Create a temporary <span> (hidden) to measure the text at 40px:
const span = document.createElement("span");
span.textContent = text;
span.style.font = `${baselineFontSizePx}px ${chosenFontFamily}`;
span.style.visibility = "hidden"; // keep it in the DOM so offsetWidth/Height works
span.style.whiteSpace = "nowrap"; // so we measure a single line
document.body.appendChild(span);
// Measured size at 40px:
const measuredWidth = span.offsetWidth;
const measuredHeight = span.offsetHeight;
document.body.removeChild(span);
// 4) Compute uniform scale so that 40pxsized text fits inside (maxWidth × maxHeight):
const scaleX = maxWidth / measuredWidth;
const scaleY = maxHeight / measuredHeight;
const scale = Math.min(scaleX, scaleY, 1); // never scale up beyond 1
// 5) Final text size in **CSS px**:
const finalFontSizePx = baselineFontSizePx * scale;
// 6) Create a <canvas> that is ALWAYS maxWidth × maxHeight in **CSS px**,
// but use devicePixelRatio for sharpness.
const pixelRatio = window.devicePixelRatio || 1;
const canvas = document.createElement("canvas");
// ★ Instead of using `finalTextWidth/Height`, force it to be the max box:
canvas.width = Math.ceil(maxWidth * pixelRatio);
canvas.height = Math.ceil(maxHeight * pixelRatio);
const ctx = canvas.getContext("2d");
ctx.scale(pixelRatio, pixelRatio);
// 7) Draw the text **centered** inside the full maxWidth×maxHeight box:
ctx.font = `${finalFontSizePx}px ${chosenFontFamily}`;
ctx.fillStyle = fillColor;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
// ★ Center = (maxWidth/2, maxHeight/2):
const centerX = maxWidth / 2;
const centerY = maxHeight / 2;
ctx.fillText(text, centerX, centerY);
// 8) Export to a PNG data-URL:
const dataUrl = canvas.toDataURL("image/png");
setSignature(dataUrl);
setTextWidth(maxWidth);
setTextHeight(maxHeight);
};
const PenColorComponent = () => {
const PenColorComponent = (props) => {
return (
<div className="flex flex-row items-center m-[5px] gap-3">
<span className="text-base-content">{t("options")}</span>
<span className="text-base-content">Options</span>
{allColor.map((data, key) => {
return (
<i
key={key}
onClick={() => {
convertToImg(fontSelect, typedSignature, data);
props?.convertToImg &&
props?.convertToImg(fontSelect, typedSignature, data);
setPenColor(allColor[key]);
}}
className={`border-b-[2px] cursor-pointer ${key === 0 && penColor === "blue" ? "border-blue-600" : key === 1 && penColor === "red" ? "border-red-500" : key === 2 && penColor === "black" ? "border-black" : "border-white"} text-[${data}] text-[16px] fa-light fa-pen-nib`}
@@ -751,6 +789,17 @@ function WidgetsValueModal(props) {
);
};
// `handleCancelBtn` function trigger when user click on cross button
const handleCancelBtn = () => {
setPenColor("blue");
setIsImageSelect(false);
setIsDefaultSign(false);
setImage(null);
setOriginalImage(null); // Add this
setRemoveBgEnabled(false); // Add this
handleTab();
};
// Effect to store original image and apply initial BG removal if enabled
useEffect(() => {
if (
@@ -853,7 +902,7 @@ function WidgetsValueModal(props) {
checked={isSavedSign}
onChange={(e) => setIsSavedSign(e.target.checked)}
/>
{t("save")} {t(`widgets-name.${currWidgetsDetails?.type}`)}
{t("save")} {currWidgetsDetails?.type}
</label>
);
//function for set checked and unchecked value of checkbox
@@ -999,28 +1048,10 @@ function WidgetsValueModal(props) {
currWidgetsDetails?.type !== "image" && (
<div className="text-base-content rounded-[4px] tabWidth">
<div className="ml-3 flex justify-start gap-4 text-[11px] md:text-base my-[3px]">
{currWidgetsDetails?.type !== "initials" &&
defaultSignImg &&
isTabEnabled("default") ? (
<div>
<span
onClick={() => {
setIsDefaultSign(true);
setIsImageSelect(true);
setIsTab("mysignature");
setImage();
}}
className={`${
isTab === "mysignature" && `${isTabCls}`
} ml-[2px] cursor-pointer`}
>
{t("my-signature")}
</span>
</div>
) : (
currWidgetsDetails?.type === "initials" &&
myInitial &&
isTabEnabled("default") && (
<>
{currWidgetsDetails?.type !== "initials" &&
defaultSignImg &&
isTabEnabled("default") ? (
<div>
<span
onClick={() => {
@@ -1031,64 +1062,84 @@ function WidgetsValueModal(props) {
}}
className={`${
isTab === "mysignature" && `${isTabCls}`
} ml-[2px] cursor-pointer`}
} ml-[2px] cursor-pointer`}
>
{t("my-initials")}
{t("my-signature")}
</span>
</div>
)
)}
{isTabEnabled("draw") && (
<div>
<span
onClick={() => {
setIsDefaultSign(false);
setIsImageSelect(false);
setIsTab("draw");
setImage();
setSignature("");
}}
className={`${
isTab === "draw" && `${isTabCls}`
} ml-[2px] cursor-pointer`}
>
{t("draw")}
</span>
</div>
)}
{isTabEnabled("upload") && (
<div>
<span
onClick={() => {
setIsDefaultSign(false);
setIsImageSelect(true);
setIsTab("uploadImage");
}}
className={`${
isTab === "uploadImage" && `${isTabCls}`
} ml-[2px] cursor-pointer`}
>
{t("upload-image")}
</span>
</div>
)}
{isTabEnabled("typed") && (
<div>
<span
onClick={() => {
setIsDefaultSign(false);
setIsImageSelect(false);
setIsTab("type");
setImage();
}}
className={`${
isTab === "type" && `${isTabCls}`
} ml-[2px] cursor-pointer`}
>
{t("type")}
</span>
</div>
)}
) : (
currWidgetsDetails?.type === "initials" &&
myInitial &&
isTabEnabled("default") && (
<div>
<span
onClick={() => {
setIsDefaultSign(true);
setIsImageSelect(true);
setIsTab("mysignature");
setImage();
}}
className={`${
isTab === "mysignature" && `${isTabCls}`
} ml-[2px] cursor-pointer`}
>
{t("my-initials")}
</span>
</div>
)
)}
{isTabEnabled("draw") && (
<div>
<span
onClick={() => {
setIsDefaultSign(false);
setIsImageSelect(false);
setIsTab("draw");
setImage();
setSignature("");
}}
className={`${
isTab === "draw" && `${isTabCls}`
} ml-[2px] cursor-pointer`}
>
{t("draw")}
</span>
</div>
)}
{isTabEnabled("upload") && (
<div>
<span
onClick={() => {
setIsDefaultSign(false);
setIsImageSelect(true);
setIsTab("uploadImage");
}}
className={`${
isTab === "uploadImage" && `${isTabCls}`
} ml-[2px] cursor-pointer`}
>
{t("upload-image")}
</span>
</div>
)}
{isTabEnabled("typed") && (
<div>
<span
onClick={() => {
setIsDefaultSign(false);
setIsImageSelect(false);
setIsTab("type");
setImage();
}}
className={`${
isTab === "type" && `${isTabCls}`
} ml-[2px] cursor-pointer`}
>
{t("type")}
</span>
</div>
)}
</>
</div>
</div>
)}
@@ -1123,7 +1174,7 @@ function WidgetsValueModal(props) {
</div>
</div>
{/* Standalone autoSignAll for "My Signature/Initials" (isDefaultSign) if conditions met */}
{uniqueId && (
{setIsAutoSign && uniqueId && (
<div className="flex justify-center my-2">
{autoSignAll}
</div>
@@ -1152,7 +1203,7 @@ function WidgetsValueModal(props) {
</div>
</div>
{/* Standalone autoSignAll for "My Signature/Initials" (isDefaultSign) if conditions met */}
{uniqueId && (
{setIsAutoSign && uniqueId && (
<div className="flex justify-center my-2">
{autoSignAll}
</div>
@@ -1177,8 +1228,8 @@ function WidgetsValueModal(props) {
ref={imageRef}
hidden
/>
<i className="fa-light fa-cloud-upload-alt uploadImgLogo text-base-content"></i>
<div className="text-[10px] text-base-content">{t("upload")}</div>
<i className="fa-light fa-cloud-upload-alt uploadImgLogo"></i>
<div className="text-[10px]">{t("upload")}</div>
</div>
</div>
) : (
@@ -1197,14 +1248,14 @@ function WidgetsValueModal(props) {
</div>
</div>
{/* Shared container for autoSignAll and removeBackground when image is displayed for upload/stamp/image */}
{(uniqueId ||
{((setIsAutoSign && uniqueId) ||
(image &&
(isImageSelect ||
["image", "stamp"].includes(
currWidgetsDetails?.type
)))) && (
<div className="flex justify-center items-center gap-x-2 my-2">
{uniqueId && autoSignAll}
{setIsAutoSign && uniqueId && autoSignAll}
{image &&
(isImageSelect ||
["image", "stamp"].includes(
@@ -1294,7 +1345,7 @@ function WidgetsValueModal(props) {
</div>
<div className="flex flex-row ml-1 mt-2 gap-x-3 text-base-content">
{/* Standalone autoSignAll for "Type" tab if conditions met */}
{uniqueId && (
{setIsAutoSign && uniqueId && (
<div className="flex justify-start my-1">
{autoSignAll}
</div>
@@ -1330,7 +1381,7 @@ function WidgetsValueModal(props) {
</div>
<div className="flex flex-row ml-1 mt-1 gap-x-3 text-base-content">
{/* Standalone autoSignAll for "Draw" tab if conditions met */}
{uniqueId && (
{setIsAutoSign && uniqueId && (
<div className="flex justify-start my-1">
{autoSignAll}
</div>
@@ -1421,7 +1472,6 @@ function WidgetsValueModal(props) {
case cellsWidget:
return (
<CellsWidget
isEnabled={true}
count={cellsValue.length}
height="100%"
value={cellsValue.join("")}
@@ -1712,10 +1762,6 @@ function WidgetsValueModal(props) {
regexValidation = /^[0-9\s]*$/;
validateExpression(regexValidation);
break;
case "ssn":
regexValidation = /^(?!000|666|9\d{2})\d{3}-(?!00)\d{2}-(?!0000)\d{4}$/;
validateExpression(regexValidation);
break;
default:
// Grab the current pattern (if it exists)
const pattern = currWidgetsDetails?.options?.validation?.pattern;
@@ -1727,7 +1773,6 @@ function WidgetsValueModal(props) {
break;
}
};
// console.log("currwidgetdetails ", currWidgetsDetails )
//function too use on click on next/finish button then update modal UI according to current widgets
const handleClickOnNext = (isFinishDoc) => {
if (
@@ -1827,11 +1872,6 @@ function WidgetsValueModal(props) {
}
};
const handleclose = () => {
// If validation is shown, clear the response and close the modal
if (isShowValidation) {
handleClear();
setIsShowValidation(false);
}
dispatch(setIsShowModal({}));
dispatch(setLastIndex(""));
if (currWidgetsDetails?.type === textWidget && uniqueId) {
@@ -1896,7 +1936,7 @@ function WidgetsValueModal(props) {
<>
<ModalUi
isOpen={true}
handleClose={() => handleclose()}
handleClose={() => !isShowValidation && handleclose()}
position="bottom"
>
<div className="h-[100%] p-[18px]">
@@ -18,16 +18,16 @@ const Menu = ({ item, isOpen, closeSidebar }) => {
className={({ isActive }) =>
`${
isActive ? " bg-base-300 text-base-content" : ""
} flex gap-x-5 items-center justify-start text-left p-3 text-base-content hover:text-base-content focus:bg-base-300 hover:bg-base-300 hover:no-underline focus:outline-none`
} flex items-center justify-start text-left p-3 lg:p-4 text-base-content hover:text-base-content focus:bg-base-300 hover:bg-base-300 hover:no-underline focus:outline-none`
}
onClick={closeSidebar}
tabIndex={isOpen ? 0 : -1}
role="menuitem"
>
<span className="w-[20px] h-[20px] flex justify-center">
<i className={`${item.icon} text-[20px]`} aria-hidden="true"></i>
<i className={`${item.icon} text-[18px]`} aria-hidden="true"></i>
</span>
<span className="flex items-center mb-0.5">
<span className="ml-3 lg:ml-4">
{t(`sidebar.${item.title}`, { appName: drivename })}
</span>
</NavLink>
@@ -12,16 +12,16 @@ const Submenu = ({ item, closeSidebar, toggleSubmenu, submenuOpen }) => {
<li role="none" className="my-0.5">
<button
onClick={() => toggleSubmenu(item.title)}
className="flex gap-x-5 items-center justify-start text-left p-3 text-base-content hover:text-base-content focus:bg-base-300 hover:bg-base-300 hover:no-underline focus:outline-none"
className="flex items-center justify-start text-left p-3 lg:p-4 text-base-content hover:text-base-content focus:bg-base-300 hover:bg-base-300 hover:no-underline focus:outline-none "
aria-expanded={submenuOpen}
aria-haspopup="true"
aria-controls={`submenu-${title}`}
>
<span className="w-[20px] h-[20px] flex justify-center">
<i className={`${icon} text-[20px]`}></i>
<i className={`${icon} text-[18px]`}></i>
</span>
<div className="flex justify-between items-center w-full">
<span className="flex items-center mb-0.5">
<span className="ml-3 lg:ml-4 text-start">
{t(`sidebar.${item.title}`, { appName })}
</span>
<i
@@ -46,20 +46,20 @@ const Submenu = ({ item, closeSidebar, toggleSubmenu, submenuOpen }) => {
}
className={({ isActive }) =>
`${
isActive ? "bg-base-300 text-base-content " : ""
} pl-4 flex items-center gap-x-5 py-2 text-sm cursor-pointer text-base-content hover:text-base-content focus:bg-base-300 hover:bg-base-300 hover:no-underline focus:outline-none`
isActive ? "bg-base-300 text-base-content" : ""
} flex items-center justify-start text-left pl-6 md:pl-8 py-2 text-sm cursor-pointer text-base-content hover:text-base-content focus:bg-base-300 hover:bg-base-300 hover:no-underline focus:outline-none`
}
onClick={closeSidebar}
role="menuitem"
tabIndex={submenuOpen ? 0 : -1}
>
<span className="w-[18px] h-[18px] flex justify-center">
<span className="w-[15px] h-[15px] flex justify-center">
<i
className={`${childItem.icon} text-[18px]`}
aria-hidden="true"
></i>
</span>
<span className="mb-0.5">
<span className="ml-3 lg:ml-4">
{t(`sidebar.${item.title}-Children.${childItem.title}`, {
appName: drivename
})}
+83 -322
View File
@@ -98,27 +98,6 @@ export const getSecureUrl = async (url) => {
}
};
/**
* Removes a trailing path segment from a URL string, if present.
*
* @param {string} url - The original URL.
* @param {string} segment - The segment to strip off (default: "app").
* @returns {string} - The URL with the trailing segment removed, or unmodified if it didnt match.
*/
export function removeTrailingSegment(url, segment = "app") {
// Normalize a trailing slash (e.g. “/app/” → “/app”)
const normalized = url.endsWith("/") ? url.slice(0, -1) : url;
const lastSlash = normalized.lastIndexOf("/");
const lastPart = normalized.slice(lastSlash + 1);
if (lastPart === segment) {
return normalized.slice(0, lastSlash);
}
return normalized;
}
export const color = [
"#93a3db",
"#e6c3db",
@@ -241,13 +220,6 @@ export const handleImageResize = (
signerId,
showResize
) => {
// Compute widget dimensions only once
const { offsetWidth, offsetHeight } = ref;
const factor = scale * containerScale || 1;
const widgetWidth = offsetWidth / factor;
const widgetHeight = offsetHeight / factor;
const widgetDims = { Width: widgetWidth, Height: widgetHeight };
const filterSignerPos = signerPos.filter((data) => data.Id === signerId);
if (filterSignerPos.length > 0) {
const getPlaceHolder = filterSignerPos[0].placeHolder;
@@ -259,28 +231,12 @@ export const handleImageResize = (
const getPosData = getXYdata;
const addSignPos = getPosData.map((url) => {
if (url.key === key) {
// Base fields for every resized signature
const base = {
return {
...url,
Width: widgetWidth,
Height: widgetHeight,
Width: ref.offsetWidth / (containerScale * scale),
Height: ref.offsetHeight / (containerScale * scale),
IsResize: showResize ? true : false
};
// If it's a “type” signature, regenerate the image and options
if (url.typeSignature && url.signatureType === "type") {
const signImg = convertTextToImg(
url.typeFont ?? "Fasthand",
url.typeSignature,
url.fontColor ?? "blue",
widgetDims
);
return {
...base,
SignUrl: signImg,
options: { ...url.options, response: signImg }
};
}
return base;
}
return url;
});
@@ -303,7 +259,6 @@ export const handleImageResize = (
}
}
};
export const widgets = [
{ type: "signature", icon: "fa-light fa-pen-nib", iconSize: "20px" },
{ type: "stamp", icon: "fa-light fa-stamp", iconSize: "19px" },
@@ -360,8 +315,6 @@ export const selectFormat = (data) => {
return "MMMM dd, yyyy";
case "DD MMMM, YYYY":
return "dd MMMM, yyyy";
case "DD.MM.YYYY":
return "dd.MM.yyyy";
default:
return "MM/dd/yyyy";
}
@@ -389,8 +342,6 @@ export const changeDateToMomentFormat = (format) => {
return "MMM DD, YYYY";
case "dd MMMM, yyyy":
return "DD MMMM, YYYY";
case "dd.MM.yyyy":
return"DD.MM.YYYY";
default:
return "L";
}
@@ -618,6 +569,7 @@ export async function getBase64FromIMG(url) {
reader.readAsDataURL(blob);
reader.onloadend = function () {
const pdfBase = this.result;
resolve(pdfBase);
};
});
@@ -659,57 +611,27 @@ export const handleSignYourselfImageResize = (
containerScale,
scale
) => {
// Guard against bad index
if (!xyPosition[index]) {
console.log(`ImageResize invalid index ${index}`);
return;
}
// Compute widget dimensions only once
const { offsetWidth, offsetHeight } = ref;
const factor = scale * containerScale || 1;
const widgetWidth = offsetWidth / factor;
const widgetHeight = offsetHeight / factor;
const widgetDims = { Width: widgetWidth, Height: widgetHeight };
// Single pass to update only the targeted index/key
const updated = xyPosition.map((item, idx) => {
if (idx !== index) return item;
return {
...item,
pos: item.pos.map((url) => {
if (url.key !== key) return url;
// Base fields for every resized signature
const base = {
...url,
Width: widgetWidth,
Height: widgetHeight,
IsResize: true
};
// If it's a “type” signature, regenerate the image and options
if (url.typeSignature && url.signatureType === "type") {
const signImg = convertTextToImg(
url.typeFont ?? "Fasthand",
url.typeSignature,
url.fontColor ?? "blue",
widgetDims
);
return {
...base,
SignUrl: signImg,
options: { ...url.options, response: signImg }
};
}
return base;
})
};
const getXYdata = xyPosition[index].pos;
const getPosData = getXYdata;
const addSign = getPosData.map((url) => {
if (url.key === key) {
return {
...url,
Width: ref.offsetWidth / (scale * containerScale),
Height: ref.offsetHeight / (scale * containerScale),
IsResize: true
};
}
return url;
});
setXyPosition(updated);
const newUpdateUrl = xyPosition.map((obj, ind) => {
if (ind === index) {
return { ...obj, pos: addSign };
}
return obj;
});
setXyPosition(newUpdateUrl);
};
//function for call cloud function signPdf and generate digital signature
@@ -1262,62 +1184,6 @@ export const embedDocId = async (pdfOriginalWH, pdfDoc, documentId) => {
}
};
// function for convert input text value in image
export function convertTextToImg(fontStyle, text, color, widgetDims) {
// read your widget dimensions:
const maxWidth = widgetDims.Width;
const maxHeight = widgetDims.Height;
const baselineFontSizePx = maxHeight;
const fontFamily = fontStyle || "Fasthand";
const fillColor = color || "blue";
// 1. Use a temporary canvas to measure actual text size
const tempCanvas = document.createElement("canvas");
const tempCtx = tempCanvas.getContext("2d");
tempCtx.font = `${baselineFontSizePx}px ${fontFamily}`;
const metrics = tempCtx.measureText(text);
const actualHeight =
metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;
const actualWidth = metrics.width;
// const renderscale = 3;
// 2. Scale font size so text height fills canvas
const scaleX = maxWidth / actualWidth;
const scaleY = maxHeight / actualHeight;
const scale = Math.min(scaleX, scaleY, 1);
// final font-size
// const finalFontSizePx = baselineFontSizePx * scale * renderscale;
const finalFontSizePx = baselineFontSizePx * scale;
// 3. Create the final canvas
const pxRatio = window.devicePixelRatio * 2 || 3;
const canvas = document.createElement("canvas");
canvas.width = Math.ceil(maxWidth * pxRatio);
canvas.height = Math.ceil(maxHeight * pxRatio);
const ctx = canvas.getContext("2d");
ctx.scale(pxRatio, pxRatio);
// 4. Setup text with final font
ctx.font = `${finalFontSizePx}px ${fontFamily}`;
ctx.fillStyle = fillColor;
ctx.textAlign = "center";
ctx.textBaseline = "alphabetic";
const newMetrics = ctx.measureText(text);
const ascent = newMetrics.actualBoundingBoxAscent;
const descent = newMetrics.actualBoundingBoxDescent;
// 5. Align text vertically so it touches top and bottom
const y = maxHeight / 2 + (ascent - descent) / 2;
ctx.clearRect(0, 0, maxWidth, maxHeight);
ctx.fillText(text, maxWidth / 2, y);
// 6. Output image
const dataUrl = canvas.toDataURL("image/png");
return dataUrl;
}
//function for save button to save signature or image url
export function onSaveSign(
type,
@@ -1331,14 +1197,10 @@ export function onSaveSign(
typedSignature,
isAutoSign,
widgetsType,
isApplyAll,
typeFont,
fontColor
isApplyAll
) {
let getIMGWH, posWidth, posHeight;
let getXYdata = xyPosition[index].pos;
const isSignOrInitials =
widgetsType && ["signature", "initials"].includes(widgetsType);
const updateXYData = getXYdata.map((position) => {
if (position.key === signKey) {
if (isDefaultSign === "initials") {
@@ -1365,22 +1227,14 @@ export function onSaveSign(
}
posWidth = getIMGWH ? getIMGWH.newWidth : 150;
posHeight = getIMGWH ? getIMGWH.newHeight : 60;
const widgetDims = { Width: posWidth, Height: posHeight };
const signImg = typedSignature
? convertTextToImg(typeFont, typedSignature, fontColor, widgetDims)
: signatureImg;
return {
...position,
Width: posWidth,
Height: posHeight,
SignUrl: signImg,
...(isSignOrInitials && { signatureType: type || "" }),
options: { ...position.options, response: signImg },
...(typedSignature && {
typeSignature: typedSignature,
typeFont: typeFont ?? "Fasthand",
fontColor: fontColor ?? "blue"
})
SignUrl: signatureImg,
...(type && { signatureType: type }),
options: { ...position.options, response: signatureImg },
...(typedSignature && { typeSignature: typedSignature })
};
}
return position;
@@ -1396,29 +1250,20 @@ export function onSaveSign(
if (isApplyAll || isAutoSign) {
const updatedArray = updateXYposition.map((page) => ({
...page,
pos: page.pos.map((item) => {
if (item.type === widgetsType) {
const widgetDims = { Width: item.Width, Height: item.Height };
const signImg = typedSignature
? convertTextToImg(typeFont, typedSignature, fontColor, widgetDims)
: signatureImg;
return {
...item,
// Only copy signature content, keep existing dimensions
// Width: posWidth,
// Height: posHeight,
SignUrl: signImg,
...(isSignOrInitials && { signatureType: type || "" }),
options: { ...item.options, response: signImg },
...(typedSignature && {
typeSignature: typedSignature,
typeFont: typeFont || "Fasthand",
fontColor: fontColor || "blue"
})
};
}
return item; // Otherwise, keep it unchanged
})
pos: page.pos.map(
(item) =>
item.type === widgetsType
? {
...item,
Width: posWidth,
Height: posHeight,
SignUrl: signatureImg,
...(type && { signatureType: type }),
options: { ...item.options, response: signatureImg },
...(typedSignature && { typeSignature: typedSignature })
}
: item // Otherwise, keep it unchanged
)
}));
return updatedArray;
} else {
@@ -1452,8 +1297,8 @@ export const handleCopySignUrl = (
if (x.key === currentPos.key) {
return {
...x,
// Width: existSignPosition.Width,
// Height: existSignPosition.Height,
Width: existSignPosition.Width,
Height: existSignPosition.Height,
SignUrl: existSignPosition.SignUrl,
signatureType: existSignPosition.signatureType,
options: { ...x.options, response: existSignPosition.SignUrl },
@@ -1485,6 +1330,7 @@ export const calculateImgAspectRatio = (imgWH, pos) => {
const aspectRatio = imgWH.width / imgWH.height;
newWidth = aspectRatio * placeholderHeight;
newHeight = placeholderHeight;
return { newHeight, newWidth };
};
@@ -1537,8 +1383,8 @@ export function onSaveImage(
item.type === widgetsType && item.type !== "image"
? {
...item,
// Width: getIMGWH.newWidth,
// Height: getIMGWH.newHeight,
Width: getIMGWH.newWidth,
Height: getIMGWH.newHeight,
SignUrl: image.src,
ImageType: image.imgType,
options: {
@@ -1685,7 +1531,13 @@ const getWidgetsFontColor = (type) => {
}
};
//function for embed multiple signature using pdf-lib
export const multiSignEmbed = async (widgets, pdfDoc, signyourself, scale) => {
export const multiSignEmbed = async (
pdfOriginalWH,
widgets,
pdfDoc,
signyourself,
scale
) => {
// `fontBytes` is used to embed custom font in pdf
const fontBytes = await fileasbytes(
"https://cdn.opensignlabs.com/webfonts/times.ttf"
@@ -1694,9 +1546,15 @@ export const multiSignEmbed = async (widgets, pdfDoc, signyourself, scale) => {
const font = await pdfDoc.embedFont(fontBytes, { subset: true });
let hasError = false;
for (let item of widgets) {
//pdfOriginalWH contained all pdf's pages width and height
//'getSize' is used to get particular pdf's page width and height
const getSize = pdfOriginalWH.find(
(page) => page?.pageNumber === item?.pageNumber
);
if (hasError) break; // Stop the outer loop if an error occurred
const typeExist = item.pos.some((data) => data?.type);
let updateItem;
if (typeExist) {
if (signyourself) {
updateItem = item.pos;
@@ -1722,21 +1580,6 @@ export const multiSignEmbed = async (widgets, pdfDoc, signyourself, scale) => {
const pages = pdfDoc.getPages();
const form = pdfDoc.getForm();
const page = pages[pageNo - 1];
// `page.getCropBox()` returns the visible area of the PDF page.
// It provides an object with the following properties:
// - x: the x-coordinate of the lower-left corner of the visible area
// - y: the y-coordinate of the lower-left corner of the visible area
// - width: the width of the visible area (x1 - x0)
// - height: the height of the visible area (y1 - y0)
// If the page does not explicitly define a CropBox, this method returns
// the MediaBox instead, which represents the full physical size of the page.
//
// Example CropBox: [0, 7.92, 612, 799.92]
// => x = 0, y = 7.92, width = 612 - 0 = 612, height = 799.92 - 7.92 = 792
// This is equivalent to `react-pdf`'s `page.view` array: [x0, y0, x1, y1]
const { _, y, width, height } = page.getCropBox();
const originalHeight = height + y;
const getSize = { height: originalHeight, width: width };
const images = await Promise.all(
widgetsPositionArr.map(async (widget) => {
// `SignUrl` this is wrong nomenclature and maintain for older code in this options we save base64 of signature image from sign pad
@@ -1837,8 +1680,8 @@ export const multiSignEmbed = async (widgets, pdfDoc, signyourself, scale) => {
let currentY = yPos(position) + 2;
// Size and spacing settings
const checkboxSize = fontSize - 1; // checkbox diameter
const checkboxTextGapFromLeft = fontSize + 3.4; // gap between box and its label
const verticalGap = fontSize + 3.4; // gap between two rows (vertical layout)
const checkboxTextGapFromLeft = fontSize + 5; // gap between box and its label
const verticalGap = fontSize + 3.2; // gap between two rows (vertical layout)
let horizontalGap = 0; // will compute after drawing each label
if (position?.options?.values.length > 0) {
position.options.values.forEach((item, ind) => {
@@ -1902,10 +1745,8 @@ export const multiSignEmbed = async (widgets, pdfDoc, signyourself, scale) => {
// Measure the width of this label text at `fontSize`
const textWidth = font.widthOfTextAtSize(item, fontSize);
// Next checkbox should come after: [box] + gap + [label text] + extra 10pt padding
const gap = position.options?.isHideLabel
? checkboxTextGapFromLeft - 5
: checkboxTextGapFromLeft + textWidth;
horizontalGap = checkboxSize + gap;
horizontalGap =
checkboxSize + checkboxTextGapFromLeft + textWidth;
}
});
}
@@ -2050,13 +1891,13 @@ export const multiSignEmbed = async (widgets, pdfDoc, signyourself, scale) => {
const radioGroup = form.createRadioGroup(radioRandomId);
//getting radio buttons options text font size
const optionsFontSize = fontSize; // font size for option text
const radioTextGapFromLeft = fontSize + 3; // gap between circle and its label
const radioTextGapFromLeft = fontSize + 6; // gap between circle and its label
const radioSize = fontSize; // circle diameter (square of width×height)
// Initial “cursor” positions (from your existing helpers)
let currentX = xPos(position) + 2;
let currentY = yPos(position) + 3;
let currentY = yPos(position);
// Vertical gap between two radiorows
const verticalGap = fontSize + 3;
const verticalGap = fontSize + 6;
// Well compute horizontalGap on the fly—after drawing each label
// Initialize to zero (will be set after first option is placed)
let horizontalGap = 0;
@@ -2112,10 +1953,7 @@ export const multiSignEmbed = async (widgets, pdfDoc, signyourself, scale) => {
const textWidth = font.widthOfTextAtSize(item, optionsFontSize);
// radioSize = the circle. radioTextGapFromLeft = gap between circle and label.
// Add a small extra padding (e.g. 10pt) before placing next circle.
const gap = position?.options?.isHideLabel
? radioTextGapFromLeft - 6
: radioTextGapFromLeft + textWidth;
horizontalGap = radioSize + gap;
horizontalGap = radioSize + radioTextGapFromLeft + textWidth;
}
});
}
@@ -2414,10 +2252,7 @@ export const getAppLogo = async () => {
});
if (tenant) {
localStorage.setItem("appname", "OpenSign™");
return {
logo: tenant?.logo,
user: tenant?.user
};
return { logo: tenant?.logo, user: tenant?.user };
}
} catch (err) {
console.log("err in getlogo ", err);
@@ -2869,9 +2704,9 @@ function getWidgetPosition(page, image, sizeRatio, getSize) {
let pageWidth;
// pageHeight;
if ([90, 270].includes(page.getRotation().angle)) {
pageWidth = getSize?.height || page.getHeight();
pageWidth = page.getHeight();
} else {
pageWidth = getSize?.width || page.getWidth();
pageWidth = page.getWidth();
}
// eslint-disable-next-line
if (!image?.hasOwnProperty("vpWidth")) {
@@ -3008,9 +2843,8 @@ export const convertBase64ToFile = async (
}
};
export const onClickZoomIn = (scale, zoomPercent, setScale, setZoomPercent) => {
const newPercent = zoomPercent + 10;
setZoomPercent(newPercent);
setScale(1 + newPercent / 100);
setScale(scale + 0.1 * scale);
setZoomPercent(zoomPercent + 10);
};
export const onClickZoomOut = (
zoomPercent,
@@ -3019,9 +2853,12 @@ export const onClickZoomOut = (
setScale
) => {
if (zoomPercent > 0) {
const newPercent = Math.max(0, zoomPercent - 10);
setZoomPercent(newPercent);
setScale(1 + newPercent / 100);
if (zoomPercent === 10) {
setScale(1);
} else {
setScale(scale - 0.1 * scale);
}
setZoomPercent(zoomPercent - 10);
}
};
@@ -3467,13 +3304,8 @@ export const getOriginalWH = async (pdf) => {
for (let index = 0; index < totalPages; index++) {
try {
const getPage = await pdf.getPage(index + 1);
const scale = 1;
//getting extra height which is removed from viewPort height (letter type pdf have originial h-799.92 but using getPage.getViewport getting height-792 something diffrence-7.92px)
//getPage?.view[1] store diffrence of height (Page starts 7.92 from bottom)
const y0 = getPage?.view[1] || 0;
let { width, height } = getPage.getViewport({ scale });
//adding it on viewPort height then get original height ex- 792+7.92 = 799.92 orignial height
height = height + y0;
const width = getPage?.view[2];
const height = getPage?.view[3];
pdfWHObj.push({ pageNumber: index + 1, width, height });
} catch (e) {
console.log(`Error getting page ${index + 1} of PDF: ${e.message}`);
@@ -3483,7 +3315,7 @@ export const getOriginalWH = async (pdf) => {
};
//function is used to check required and optional widgets and ensure required widget should be response
export const handleCheckResponse = (checkUser, setminRequiredCount) => {
export const handleCheckResponse = (checkUser,setminRequiredCount) => {
let checkboxExist,
showAlert = false,
widgetKey,
@@ -3582,74 +3414,3 @@ export const handleCheckResponse = (checkUser, setminRequiredCount) => {
showAlert
};
};
/**
* decryptPdf
* @param {File} file - The password-protected PDF file to decrypt.
* @param {string | undefined} password - The password used to unlock the PDF.
* @returns {Promise<File>} - A Promise that resolves to a decrypted PDF as a File object.
*
*/
export const decryptPdf = async (file, password) => {
const name = generatePdfName(16);
const baseApi = localStorage.getItem("baseUrl") || "";
const url = removeTrailingSegment(baseApi) + "/decryptpdf?ts=" + Date.now();
let formData = new FormData();
formData.append("file", file);
formData.append("password", password);
const config = {
headers: { "content-type": "multipart/form-data" },
responseType: "blob"
};
const response = await axios.post(url, formData, config);
const pdfBlob = new Blob([response.data], { type: "application/pdf" });
return new File([pdfBlob], name, { type: "application/pdf" });
};
/**
* Convert a Base64 string to a File object.
*
* @param {string} base64String - The Base64 string, with or without the data URI prefix.
* e.g. "data:image/png;base64,iVBORw0KGgoAAAANS…" or "iVBORw0KGgoAAAANS…"
* @param {string} filename - Desired filename for the File object, e.g. "photo.png"
* @returns {File} - The resulting File object
*/
export function base64ToFile(base64String, filename) {
// Separate out the mime-type and the actual Base64 payload
const [header, payload] = base64String.includes(",")
? base64String.split(",")
: [null, base64String];
// Determine the MIME type (fallback to application/octet-stream)
const mimeMatch = header?.match(/data:(.*?);base64/);
const mime = mimeMatch ? mimeMatch[1] : "application/octet-stream";
// Decode Base64 to raw binary data held in a string
const binaryString = atob(payload);
// Create an ArrayBuffer and a view (as unsigned 8-bit)
const len = binaryString.length;
const u8arr = new Uint8Array(len);
for (let i = 0; i < len; i++) {
u8arr[i] = binaryString.charCodeAt(i);
}
// Create a File object (Blob subclass) with the binary data
return new File([u8arr], filename, { type: mime });
}
/**
* Reads the given File object and returns its contents as an ArrayBuffer.
*
* @param {File} file
* The File instance to be read.
* @returns {Promise<ArrayBuffer>}
* A promise that resolves with the files binary data as an ArrayBuffer,
* or rejects with an error if the read fails.
*/
export function getFileAsArrayBuffer(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => resolve(e.target.result);
reader.onerror = (e) => reject(e.target.error);
reader.readAsArrayBuffer(file);
});
}
-110
View File
@@ -1,110 +0,0 @@
import { useEffect, useRef } from "react";
export default function usePdfPinchZoom(ref, scale, setScale, setZoomPercent) {
const initialDistanceRef = useRef(null);
const startScaleRef = useRef(scale);
const currentScaleRef = useRef(scale);
const pinchScaleRef = useRef(null); // stores relative scale during a gesture
const frameRef = useRef(null);
// keep a reference to the latest scale value
useEffect(() => {
const element = ref.current;
currentScaleRef.current = scale;
// once React commits the new scale after a pinch we clear the transform
if (element && !pinchScaleRef.current) {
element.style.transform = "";
}
}, [scale, ref]);
useEffect(() => {
const element = ref.current;
if (!element) return;
const getDistance = (touches) => {
const [touch1, touch2] = touches;
const dx = touch1.clientX - touch2.clientX;
const dy = touch1.clientY - touch2.clientY;
return Math.hypot(dx, dy);
};
const handleTouchStart = (e) => {
if (e.touches.length === 2) {
initialDistanceRef.current = getDistance(e.touches);
startScaleRef.current = currentScaleRef.current;
element.style.transformOrigin = "0 0";
}
};
const handleTouchMove = (e) => {
if (e.touches.length === 2 && initialDistanceRef.current) {
e.preventDefault();
const newDistance = getDistance(e.touches);
const pinchScale = newDistance / initialDistanceRef.current;
const newScale = Math.max(1, startScaleRef.current * pinchScale);
// store relative scale so it can be applied on top of current width
pinchScaleRef.current = newScale / startScaleRef.current;
if (!frameRef.current) {
frameRef.current = requestAnimationFrame(() => {
element.style.transform = `scale(${pinchScaleRef.current})`;
frameRef.current = null;
});
}
}
};
const handleTouchEnd = () => {
if (pinchScaleRef.current) {
let relativeScale = pinchScaleRef.current;
const inlineTransform = element.style.transform;
if (inlineTransform && inlineTransform.startsWith("scale(")) {
const value = parseFloat(inlineTransform.slice(6, -1));
if (!isNaN(value)) {
relativeScale = value;
}
}
const finalScale = Math.max(1, startScaleRef.current * relativeScale);
const percent = parseFloat(((finalScale - 1) * 100).toFixed(2));
const unchanged =
Math.abs(finalScale - currentScaleRef.current) < 0.001;
pinchScaleRef.current = null;
if (frameRef.current) {
cancelAnimationFrame(frameRef.current);
frameRef.current = null;
}
if (unchanged) {
// if scale didn't actually change, reset transform right away
element.style.transform = "";
} else {
// keep transform applied until state update reflects the new scale
element.style.transform = `scale(${relativeScale})`;
}
setScale(finalScale);
setZoomPercent(percent);
// ensure refs reflect the latest value before next gesture starts
currentScaleRef.current = finalScale;
startScaleRef.current = finalScale;
}
initialDistanceRef.current = null;
};
element.addEventListener("touchstart", handleTouchStart, {
passive: false
});
element.addEventListener("touchmove", handleTouchMove, { passive: false });
element.addEventListener("touchend", handleTouchEnd);
element.addEventListener("touchcancel", handleTouchEnd);
return () => {
element.removeEventListener("touchstart", handleTouchStart);
element.removeEventListener("touchmove", handleTouchMove);
element.removeEventListener("touchend", handleTouchEnd);
element.removeEventListener("touchcancel", handleTouchEnd);
};
}, [ref, setScale, setZoomPercent]);
}
+52 -83
View File
@@ -12,12 +12,9 @@ import {
flattenPdf,
generatePdfName,
generateTitleFromFilename,
getFileName,
getSecureUrl,
toDataUrl,
decryptPdf,
base64ToFile,
getFileAsArrayBuffer,
removeTrailingSegment
toDataUrl
} from "../constant/Utils";
import { PDFDocument } from "pdf-lib";
import axios from "axios";
@@ -118,6 +115,15 @@ const Forms = (props) => {
}));
};
function getFileAsArrayBuffer(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => resolve(e.target.result);
reader.onerror = (e) => reject(e.target.error);
reader.readAsArrayBuffer(file);
});
}
// `removeFile` is used to reset progress, percentage and remove file if exists
const removeFile = (e) => {
setfileload(false);
@@ -130,13 +136,12 @@ const Forms = (props) => {
setpercentage(0);
try {
const files = Array.from(e.target.files);
const filesNameArr = files.map((f) => f.name);
setSelectedFiles(filesNameArr);
setSelectedFiles(files.map((f) => f.name));
if (!files.length) {
alert(t("file-alert-2"));
return;
}
// setFormData((prev) => ({ ...prev, file: files[0] }));
setFormData((prev) => ({ ...prev, file: files[0] }));
const totalMb = Math.round(
files.reduce((sum, f) => sum + f.size, 0) / Math.pow(1024, 2)
);
@@ -150,74 +155,10 @@ const Forms = (props) => {
const pdfBuffers = [];
for (const file of files) {
setFormData((prev) => ({ ...prev, file: file }));
if (file.type === "application/pdf") {
try {
const buffer = await getFileAsArrayBuffer(file);
const flat = await flattenPdf(buffer);
pdfBuffers.push(flat);
} catch (err) {
if (err?.message?.includes("is encrypted")) {
try {
setIsDecrypting(true);
const pdfFile = await decryptPdf(file, "");
setIsDecrypting(false);
setfileload(true);
const res = await getFileAsArrayBuffer(pdfFile);
const flatPdf = await flattenPdf(res);
// Upload the file to Parse Server
pdfBuffers.push(flatPdf);
} catch (err) {
removeFile(e);
if (err?.response?.status === 401) {
// setIsPassword(true);
const password = prompt(
`PDF "${file.name}" is password-protected. Enter password:`
);
if (password) {
try {
const pdfFile = await decryptPdf(file, password);
setIsDecrypting(false);
setfileload(true);
const res = await getFileAsArrayBuffer(pdfFile);
const flatPdf = await flattenPdf(res);
// Upload the file to Parse Server
pdfBuffers.push(flatPdf);
} catch (err) {
console.error(
"Incorrect password or decryption failed",
err
);
setSelectedFiles(
filesNameArr.filter((f) => f !== file.name)
);
setIsDecrypting(false);
setfileload(false);
removeFile(e);
alert(`Incorrect password for file: ${file.name}`);
}
} else {
console.error("password not provided");
setSelectedFiles(
filesNameArr.filter((f) => f !== file.name)
);
setIsDecrypting(false);
setfileload(false);
removeFile(e);
}
} else {
console.log("Error uploading file: ", err?.response);
setIsDecrypting(false);
e.target.value = "";
removeFile(e);
}
}
} else {
console.log("err ", err);
removeFile(e);
}
}
const buffer = await getFileAsArrayBuffer(file);
const flat = await flattenPdf(buffer);
pdfBuffers.push(flat);
} else if (file.type.includes("image/")) {
const image = await toDataUrl(file);
const pdfDoc = await PDFDocument.create();
@@ -241,7 +182,7 @@ const Forms = (props) => {
) {
try {
const baseApi = localStorage.getItem("baseUrl") || "";
const url = removeTrailingSegment(baseApi) + "/docxtopdf";
const url = baseApi.replace("/app", "") + "docxtopdf";
let fd = new FormData();
fd.append("file", file);
setfileload(true);
@@ -299,12 +240,17 @@ const Forms = (props) => {
const pdfBytes = await merged.save({ useObjectStreams: false });
const name = generatePdfName(16);
const pdfName = `${name}.pdf`;
const fileAdapterId = extUserData?.TenantId?.ActiveFileAdapter || "";
let uploadedUrl = "";
const parseFile = new Parse.File(
pdfName,
[...pdfBytes],
"application/pdf"
);
if (fileAdapterId) {
const base64 = await merged.saveAsBase64({ useObjectStreams: false });
const fileBase64 = base64.split(",").pop();
const fileRes = await saveToCustomFile(fileBase64, pdfName, fileAdapterId);
if (fileRes.url) {
uploadedUrl = fileRes.url;
}
} else {
const parseFile = new Parse.File(pdfName, [...pdfBytes], "application/pdf");
const response = await parseFile.save({
progress: (progressValue, loaded, total, { type }) => {
if (type === "upload" && progressValue !== null) {
@@ -319,12 +265,14 @@ const Forms = (props) => {
uploadedUrl = fileRes.url;
}
}
}
if (uploadedUrl) {
const tenantId = localStorage.getItem("TenantId");
SaveFileSize(pdfBytes.byteLength, uploadedUrl, tenantId);
setFileUpload(uploadedUrl);
setfileload(false);
const title = generateTitleFromFilename(filesNameArr?.[0]);
const title = generateTitleFromFilename(files[0].name);
setFormData((obj) => ({ ...obj, Name: title }));
removeFile(e);
} else {
@@ -434,6 +382,9 @@ const Forms = (props) => {
className: "contracts_Users",
objectId: ExtCls[0].objectId
});
if (extUserData?.TenantId?.ActiveFileAdapter) {
object.set("FileAdapterId", extUserData?.TenantId?.ActiveFileAdapter);
}
const res = await object.save();
if (res) {
setSigners([]);
@@ -558,7 +509,25 @@ const Forms = (props) => {
try {
const size = formData?.file?.size;
const name = generatePdfName(16);
const pdfFile = await decryptPdf(formData?.file, formData?.password);
const baseApi = localStorage.getItem("baseUrl") || "";
const url = baseApi.replace("/app", "") + "decryptpdf";
let Data = new FormData();
Data.append("file", formData?.file);
Data.append("password", formData.password);
const config = {
headers: {
"content-type": "multipart/form-data"
// sessiontoken: Parse.User.current().getSessionToken()
},
responseType: "blob"
};
const response = await axios.post(url, Data, config);
const pdfBlob = new Blob([response.data], {
type: "application/pdf"
});
const pdfFile = new File([pdfBlob], name, {
type: "application/pdf"
});
setIsDecrypting(false);
const res = await getFileAsArrayBuffer(pdfFile);
const flatPdf = await flattenPdf(res);
@@ -693,6 +693,7 @@ function PdfRequestFiles(
}
//embed all widgets in document
const pdfBytes = await multiSignEmbed(
pdfOriginalWH,
widgets,
pdfDoc,
isSignYourSelfFlow,
+13 -10
View File
@@ -863,6 +863,7 @@ function PlaceHolderSign() {
try {
//pdfOriginalWH contained all pdf's pages width,height & pagenumber in array format
const pdfBase64 = await multiSignEmbed(
pdfOriginalWH,
placeholder,
pdfDoc,
isSignYourSelfFlow,
@@ -1607,7 +1608,13 @@ function PlaceHolderSign() {
parseInt(defaultdata?.cellCount || 5)
),
validation:
{},
isSubscribe && inputype
? {
type: inputype,
pattern:
inputype === "regex" ? defaultdata.textvalidate : ""
}
: {},
fontSize:
fontSize || currWidgetsDetails?.options?.fontSize || 12,
fontColor:
@@ -1744,14 +1751,14 @@ function PlaceHolderSign() {
objectId: data.objectId
};
const updatePlaceHolder = signerPos.map((x) => {
if (x.Id === id || x.signerObjId === id) {
if (x.Id === id) {
return { ...x, signerPtr: signerPtr, signerObjId: data.objectId };
}
return { ...x };
});
setSignerPos(updatePlaceHolder);
const updateSigner = signersdata.map((x) => {
if (x.Id === id || x.objectId === id) {
if (x.Id === id) {
return { ...x, ...data, className: "contracts_Contactbook" };
}
return { ...x };
@@ -1766,9 +1773,7 @@ function PlaceHolderSign() {
}
}
setSignersData(updateSigner);
const index = signersdata.findIndex(
(x) => x.Id === id || x.objectId === id
);
const index = signersdata.findIndex((x) => x.Id === id);
setIsSelectId(index);
}
if (isNewContact.status) {
@@ -1932,9 +1937,7 @@ function PlaceHolderSign() {
};
//`handleInputChange` function to get signers list from dropdown
const handleInputChange = (item, id) => {
const signerExist = signersdata?.some(
(x) => x.objectId && x.objectId === item.value
);
const signerExist = forms.some((x) => x.value === item.value);
if (signerExist) {
alert(t("already-exist-signer"));
} else {
@@ -2633,7 +2636,7 @@ function PlaceHolderSign() {
title={t("document-alert")}
showClose={false}
>
<div className="h-[100%] p-[20px] text-base-content">
<div className="h-[100%] p-[20px]">
<p>{isAlreadyPlace.message}</p>
<div className="h-[1px] w-full my-[15px] bg-[#9f9f9f]"></div>
<button
+3 -70
View File
@@ -56,10 +56,6 @@ const Preferences = () => {
const [dateFormat, setDateFormat] = useState("MM/DD/YYYY");
const [is12HourTime, setIs12HourTime] = useState(false);
const [isLTVEnabled, setIsLTVEnabled] = useState(false);
const [isDefaultMail, setIsDefaultMail] = useState({
requestMail: false,
completionMail: false
});
useEffect(() => {
fetchSignType();
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -231,7 +227,6 @@ const Preferences = () => {
setRequestSubject(
`{{sender_name}} has requested you to sign {{document_title}}`
);
setIsDefaultMail((prev) => ({ ...prev, requestMail: true }));
}
setDefaultReqHtml({
body: defaultRequestBody,
@@ -246,7 +241,6 @@ const Preferences = () => {
setCompletionSubject(
`Document {{document_title}} has been signed by all parties`
);
setIsDefaultMail((prev) => ({ ...prev, completionMail: true }));
}
setDefaultCompHtml({
body: defaultCompletionBody,
@@ -324,45 +318,13 @@ const Preferences = () => {
};
//function to use reset form
const handleReset = async (request, completion) => {
let extUser =
localStorage.getItem("Extand_Class") &&
JSON.parse(localStorage.getItem("Extand_Class"))?.[0];
handleModifyMail(request);
if (request && !isDefaultMail?.requestMail) {
const handleReset = (request, completion) => {
if (request) {
setRequestBody(defaultReqHtml?.body);
setRequestSubject(defaultReqHtml?.subject);
try {
await Parse.Cloud.run("updatetenant", {
tenantId: tenantId,
details: { RequestBody: "", RequestSubject: "" }
});
if (extUser && extUser?.objectId) {
extUser.TenantId.RequestBody = "";
extUser.TenantId.RequestSubject = "";
const _extUser = JSON.parse(JSON.stringify(extUser));
localStorage.setItem("Extand_Class", JSON.stringify([_extUser]));
}
} catch (err) {
console.log("Err in reseting request mail", err);
}
} else if (completion && !isDefaultMail?.completionMail) {
} else if (completion) {
setCompletionSubject(defaultCompHtml?.subject);
SetCompletionBody(defaultCompHtml?.body);
try {
await Parse.Cloud.run("updatetenant", {
tenantId: tenantId,
details: { CompletionBody: "", CompletionSubject: "" }
});
if (extUser && extUser?.objectId) {
extUser.TenantId.CompletionBody = "";
extUser.TenantId.CompletionSubject = "";
const _extUser = JSON.parse(JSON.stringify(extUser));
localStorage.setItem("Extand_Class", JSON.stringify([_extUser]));
}
} catch (err) {
console.log("Err in reseting completion mail", err);
}
}
};
//function for handle ontext change and save again text in delta
@@ -382,11 +344,6 @@ const Preferences = () => {
const handleTourInput = () => setIsTourEnabled(!isTourEnabled);
const handleSendinOrderInput = () => setSendinOrder(!sendinOrder);
const tabName = (ind) => tab.find((t, i) => i === ind)?.name;
const handleModifyMail = (mode) => {
mode === "request"
? setIsDefaultMail((p) => ({ ...p, requestMail: !p?.requestMail }))
: setIsDefaultMail((p) => ({ ...p, completionMail: !p?.completionMail }));
};
return (
<React.Fragment>
<Title title={t("Preferences")} />
@@ -780,18 +737,6 @@ const Preferences = () => {
{t("request-email")}
</h1>
<div className="relative mt-2 mb-4">
{
isDefaultMail?.requestMail && (
<div className="absolute backdrop-blur-[2px] flex w-full h-full justify-center items-center bg-black/10 rounded-box select-none z-20">
<button
onClick={() => handleModifyMail("request")}
className="op-btn op-btn-primary shadow-lg"
>
Modify
</button>
</div>
)
}
<form
onSubmit={handleSaveRequestEmail}
className="p-3 border-[1px] border-base-content rounded-box"
@@ -856,18 +801,6 @@ const Preferences = () => {
{t("completion-email")}
</h1>
<div className="relative my-2">
{
isDefaultMail?.completionMail && (
<div className="absolute backdrop-blur-[2px] flex w-full h-full justify-center items-center bg-black/10 rounded-box select-none z-20">
<button
onClick={() => handleModifyMail("completion")}
className="op-btn op-btn-primary shadow-lg"
>
Modify
</button>
</div>
)
}
<form
onSubmit={handleSaveCompletionEmail}
className="p-3 border-[1px] border-base-content rounded-box"
+1 -1
View File
@@ -180,7 +180,7 @@ const Report = () => {
const listData = res.data?.result.filter((x) => x.Signers.length > 0);
let arr = [];
for (const obj of listData) {
const isSigner = obj?.Signers?.some(
const isSigner = obj.Signers.some(
(item) => item.UserId.objectId === currentUser
);
if (isSigner) {
@@ -648,6 +648,7 @@ function SignYourSelf() {
}
//embed all widgets in document
const pdfBytes = await multiSignEmbed(
pdfOriginalWH,
xyPosition,
pdfDoc,
isSignYourSelfFlow,
@@ -833,6 +833,7 @@ const TemplatePlaceholder = () => {
try {
//pdfOriginalWH contained all pdf's pages width,height & pagenumber in array format
const pdfBase64 = await multiSignEmbed(
pdfOriginalWH,
placeholder,
pdfDoc,
isSignYourSelfFlow,
@@ -91,7 +91,6 @@ const ReportTable = (props) => {
const [contact, setContact] = useState({ Name: "", Email: "", Phone: "" });
const [isSuccess, setIsSuccess] = useState({});
const [templateId, setTemplateId] = useState("");
const [sortOrder, setSortOrder] = useState("asc");
const isTemplateReport = props.ReportName === "Templates";
const recordsPerPage = 5;
const startIndex = (currentPage - 1) * props.docPerPage;
@@ -438,22 +437,7 @@ const ReportTable = (props) => {
// Get current list
const indexOfLastDoc = currentPage * props.docPerPage;
const indexOfFirstDoc = indexOfLastDoc - props.docPerPage;
const sortedList = React.useMemo(() => {
if (props.ReportName === "Contactbook") {
const contacts = [...props.List];
contacts.sort((a, b) => {
const nameA = a?.Name?.toLowerCase() || "";
const nameB = b?.Name?.toLowerCase() || "";
if (nameA < nameB) return sortOrder === "asc" ? -1 : 1;
if (nameA > nameB) return sortOrder === "asc" ? 1 : -1;
return 0;
});
return contacts;
}
return props.List;
}, [props.List, sortOrder, props.ReportName]);
const currentList = sortedList?.slice(indexOfFirstDoc, indexOfLastDoc);
const currentList = props.List?.slice(indexOfFirstDoc, indexOfLastDoc);
// Change page
const paginateFront = () => {
@@ -473,10 +457,6 @@ const ReportTable = (props) => {
setIsContactform(!isContactform);
};
const toggleSortOrder = () => {
setSortOrder(sortOrder === "asc" ? "desc" : "asc");
};
const handleUserData = (data) => {
props.setList((prevData) => [data, ...prevData]);
};
@@ -1556,17 +1536,6 @@ const ReportTable = (props) => {
required
className="op-file-input op-file-input-bordered op-file-input-sm focus:outline-none hover:border-base-content w-full text-xs"
/>
<p className="mt-1 ml-2 text-[11px] text-gray-600">
{t("import-guideline")}{" "}
<a
href="/sample_contacts.csv"
target="_blank"
rel="noopener noreferrer"
className="text-primary underline"
>
{t("download-sample")}
</a>
</p>
</div>
<div className="text-md m-2">
<div className="flex flex-col md:flex-row gap-1">
@@ -1655,24 +1624,7 @@ const ReportTable = (props) => {
<tr className="border-y-[1px]">
{props.heading?.map((item, index) => (
<React.Fragment key={index}>
<th className="p-2">
{t(`report-heading.${item}`)}
{props.ReportName === "Contactbook" && item === "Name" && (
<button
type="button"
onClick={toggleSortOrder}
className="ml-1"
>
<i
className={
sortOrder === "asc"
? "fa-light fa-arrow-down-a-z"
: "fa-light fa-arrow-up-a-z"
}
></i>
</button>
)}
</th>
<th className="p-2">{t(`report-heading.${item}`)}</th>
</React.Fragment>
))}
{props.actions?.length > 0 && (
@@ -1865,7 +1817,7 @@ const ReportTable = (props) => {
{/* template report */}
{isOption[item.objectId] &&
act.action === "option" && (
<ul className="absolute -right-1 top-auto z-[70] w-52 op-dropdown-content op-menu op-menu-sm shadow-black/20 shadow bg-base-100 text-base-content rounded-box">
<ul className="absolute -right-1 top-auto z-[70] w-52 op-dropdown-content op-menu shadow-black/20 shadow bg-base-100 text-base-content rounded-box">
{act.subaction?.map((subact) => (
<li
key={subact.btnId}
@@ -1956,7 +1908,7 @@ const ReportTable = (props) => {
{/* doc report */}
{isOption[item.objectId] &&
act.action === "option" && (
<ul className="absolute -right-1 top-auto z-[70] w-max op-dropdown-content op-menu op-menu-sm shadow-black/20 shadow bg-base-100 text-base-content rounded-box">
<ul className="absolute -right-1 top-auto z-[70] w-max op-dropdown-content op-menu shadow-black/20 shadow bg-base-100 text-base-content rounded-box">
{act.subaction?.map(
(subact) =>
!restrictBtn(
+2 -3
View File
@@ -1,4 +1,4 @@
import { defineConfig, splitVendorChunkPlugin } from "vite";
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import svgr from "vite-plugin-svgr";
import { resolve } from "path";
@@ -12,8 +12,7 @@ export default defineConfig(({ command, mode }) => {
return {
plugins: [
react(),
svgr(), // Transform SVGs into React components
splitVendorChunkPlugin()
svgr() // Transform SVGs into React components
],
resolve: {
alias: {
-3
View File
@@ -7,7 +7,6 @@ import axios from 'axios';
dotenv.config();
export const cloudServerUrl = 'http://localhost:8080/app';
export const serverAppId = process.env.APP_ID || 'opensign';
export const appName = 'OpenSign™';
export const MAX_NAME_LENGTH = 250;
@@ -289,8 +288,6 @@ export const selectFormat = data => {
return 'MMMM dd, yyyy';
case 'DD MMMM, YYYY':
return 'dd MMMM, yyyy';
case 'DD.MM.YYYY':
return 'dd.MM.yyyy';
default:
return 'MM/dd/yyyy';
}
@@ -15,18 +15,15 @@ export const upload = multer({ storage });
export default async function decryptpdf(req, res) {
const inputPath = req.file.path;
const outputPath = './exports/out.pdf';
const password = req.body.password || '';
try {
const file = fs.readFileSync(inputPath);
const pdf = await Coherentpdf.fromMemory(file, password);
await Coherentpdf.decryptPdf(pdf, password);
// Get decrypted buffer directly from memory (no file I/O)
const buffer = await Coherentpdf.toMemory(pdf, false, false);
res.set({
'Content-Type': 'application/pdf',
'Content-Disposition': 'inline; filename="decrypted.pdf"',
'Content-Length': buffer.length,
});
await Coherentpdf.toFile(pdf, outputPath, false, false);
const buffer = fs.readFileSync(outputPath);
res.set('Content-Type', 'application/pdf');
res.send(buffer);
fs.unlink(inputPath, () => {});
} catch (err) {
@@ -3,7 +3,7 @@ import axios from 'axios';
import multer from 'multer';
import libre from 'libreoffice-convert';
import util from 'node:util';
import { cloudServerUrl, getSecureUrl, serverAppId } from '../../Utils.js';
import { cloudServerUrl, getSecureUrl } from '../../Utils.js';
libre.convertAsync = util.promisify(libre.convert);
@@ -29,7 +29,7 @@ function generatePdfName(length) {
export default async function docxtopdf(req, res) {
const serverUrl = cloudServerUrl;
const appId = serverAppId;
const appId = process.env.APP_ID;
const masterKey = process.env.MASTER_KEY;
const inputPath = req.file.path;
const name = generatePdfName(16);
@@ -3,7 +3,7 @@ import multer from 'multer';
import multerS3 from 'multer-s3';
import aws from 'aws-sdk';
import dotenv from 'dotenv';
import { cloudServerUrl, serverAppId, useLocal } from '../../Utils.js';
import { cloudServerUrl, useLocal } from '../../Utils.js';
dotenv.config();
function sanitizeFileName(fileName) {
@@ -50,7 +50,7 @@ async function uploadFile(req, res) {
const DO_SPACE = process.env.DO_SPACE;
const parseBaseUrl = cloudServerUrl; //process.env.SERVER_URL;
const parseAppId = serverAppId;
const parseAppId = process.env.APP_ID;
let fileStorage;
if (useLocal === 'true') {
fileStorage = multer.diskStorage({
@@ -1,7 +1,7 @@
import axios from 'axios';
import { cloudServerUrl, serverAppId } from '../../Utils.js';
import { cloudServerUrl } from '../../Utils.js';
const serverUrl = cloudServerUrl; //process.env.SERVER_URL;
const APPID = serverAppId;
const APPID = process.env.APP_ID;
const masterKEY = process.env.MASTER_KEY;
async function addTeamAndOrg(extUser) {
try {
@@ -1,10 +1,10 @@
import axios from 'axios';
import { cloudServerUrl, serverAppId } from '../../Utils.js';
import { cloudServerUrl } from '../../Utils.js';
async function AuthLoginAsMail(request) {
try {
//function for login user using user objectId without touching user's password
const serverUrl = cloudServerUrl; //process.env.SERVER_URL;
const APPID = serverAppId;
const APPID = process.env.APP_ID;
const masterKEY = process.env.MASTER_KEY;
let otpN = request.params.otp;
@@ -92,7 +92,7 @@ async function AuthLoginAsMail(request) {
} catch (err) {
console.log('err in Auth');
console.log(err);
return 'Result not found';
return 'Result not found', err;
}
}
export default AuthLoginAsMail;
@@ -1,5 +1,5 @@
import axios from 'axios';
import { appName, cloudServerUrl, serverAppId } from '../../Utils.js';
import { appName, cloudServerUrl } from '../../Utils.js';
export default async function forwardDoc(request) {
try {
@@ -58,7 +58,7 @@ export default async function forwardDoc(request) {
mailRes = await axios.post(`${cloudServerUrl}/functions/sendmailv3`, params, {
headers: {
'Content-Type': 'application/json',
'X-Parse-Application-Id': serverAppId,
'X-Parse-Application-Id': process.env.APP_ID,
'X-Parse-Master-Key': process.env.MASTER_KEY,
},
});
@@ -1,5 +1,5 @@
import axios from 'axios';
import { cloudServerUrl, serverAppId } from '../../Utils.js';
import { cloudServerUrl } from '../../Utils.js';
export default async function GetTemplate(request) {
const serverUrl = cloudServerUrl; //process.env.SERVER_URL;
@@ -10,7 +10,7 @@ export default async function GetTemplate(request) {
if (sessiontoken) {
const userRes = await axios.get(serverUrl + '/users/me', {
headers: {
'X-Parse-Application-Id': serverAppId,
'X-Parse-Application-Id': process.env.APP_ID,
'X-Parse-Session-Token': sessiontoken,
},
});
@@ -1,6 +1,6 @@
import axios from 'axios';
import { cloudServerUrl, serverAppId } from '../../Utils.js';
const appId = serverAppId;
import { cloudServerUrl } from '../../Utils.js';
const appId = process.env.APP_ID;
const masterkey = process.env.MASTER_KEY;
export default async function createBatchContact(req) {
if (!req?.user) {
@@ -1,7 +1,7 @@
import axios from 'axios';
import { cloudServerUrl, mailTemplate, replaceMailVaribles, serverAppId } from '../../Utils.js';
import { cloudServerUrl, mailTemplate, replaceMailVaribles } from '../../Utils.js';
const serverUrl = cloudServerUrl; //process.env.SERVER_URL;
const appId = serverAppId;
const appId = process.env.APP_ID;
async function deductcount(docsCount, extUserId) {
try {
const extCls = new Parse.Object('contracts_Users');
@@ -84,7 +84,7 @@ async function sendMail(document, publicUrl) {
};
let params = {
extUserId: document.ExtUserPtr.objectId,
recipient: existSigner?.Email || signerMail[i].email,
recipient: objectId ? existSigner?.Email : signerMail[i].email,
subject: replaceVar?.subject ? replaceVar?.subject : mailTemplate(mailparam).subject,
from: document.ExtUserPtr.Email,
replyto: senderEmail || '',
@@ -1,5 +1,5 @@
import axios from 'axios';
import { cloudServerUrl, serverAppId } from '../../Utils.js';
import { cloudServerUrl } from '../../Utils.js';
export default async function getDocument(request) {
const serverUrl = cloudServerUrl; //process.env.SERVER_URL;
const docId = request.params.docId;
@@ -30,7 +30,7 @@ export default async function getDocument(request) {
try {
const userRes = await axios.get(serverUrl + '/users/me', {
headers: {
'X-Parse-Application-Id': serverAppId,
'X-Parse-Application-Id': process.env.APP_ID,
'X-Parse-Session-Token': sessiontoken,
},
});
@@ -1,8 +1,8 @@
import axios from 'axios';
import { cloudServerUrl, serverAppId } from '../../Utils.js';
import { cloudServerUrl } from '../../Utils.js';
export default async function getDrive(request) {
const serverUrl = cloudServerUrl; //process.env.SERVER_URL;
const appId = serverAppId;
const appId = process.env.APP_ID;
const limit = request.params.limit;
const skip = request.params.skip;
const docId = request.params.docId;
@@ -1,4 +1,4 @@
import { cloudServerUrl, serverAppId } from '../../Utils.js';
import { cloudServerUrl } from '../../Utils.js';
import reportJson from './reportsJson.js';
import axios from 'axios';
@@ -14,7 +14,7 @@ export default async function getReport(request) {
const searchTerm = request.params.searchTerm || '';
const serverUrl = cloudServerUrl; //process.env.SERVER_URL;
const appId = serverAppId;
const appId = process.env.APP_ID;
const masterKey = process.env.MASTER_KEY;
const sessionToken = request.headers['sessiontoken'] || request.headers['x-parse-session-token'];
try {
@@ -1,5 +1,25 @@
// `saveRoleContact` is used to save user in contracts_Guest role and create contact
const saveRoleContact = async contact => {
try {
const Role = new Parse.Query(Parse.Role);
const guestRole = await Role.equalTo('name', 'contracts_Guest').first();
if (guestRole) {
// Check if the user is already in the role
const relation = guestRole.relation('users');
const usersInRoleQuery = relation.query();
usersInRoleQuery.equalTo('objectId', contact.UserId.objectId);
const usersInRole = await usersInRoleQuery.find();
if (usersInRole.length > 0) {
console.log('User already added to Guest role.');
} else {
relation.add({ __type: 'Pointer', className: '_User', id: contact.UserId.objectId });
await guestRole.save(null, { useMasterKey: true });
// console.log('User added to Guest role successfully.');
}
}
} catch (err) {
console.log('err in role save', err);
}
const contactQuery = new Parse.Object('contracts_Contactbook');
contactQuery.set('Name', contact.Name);
contactQuery.set('Email', contact.Email);
@@ -7,7 +7,6 @@ import {
saveFileUsage,
getSecureUrl,
appName,
serverAppId,
} from '../../../Utils.js';
import GenerateCertificate from './GenerateCertificate.js';
import { pdflibAddPlaceholder } from '@signpdf/placeholder-pdf-lib';
@@ -16,7 +15,7 @@ import { SignPdf } from '@signpdf/signpdf';
import { P12Signer } from '@signpdf/signer-p12';
const serverUrl = cloudServerUrl; // process.env.SERVER_URL;
const APPID = serverAppId;
const APPID = process.env.APP_ID;
const masterKEY = process.env.MASTER_KEY;
const eSignName = 'OpenSign';
const eSigncontact = 'hello@opensignlabs.com';
@@ -59,7 +59,6 @@ export default async function saveAsTemplate(request) {
options: {
...p.options,
status: 'required',
...(p?.options?.defaultValue ? { defaultValue: '' } : {}),
},
})),
}));
@@ -74,28 +73,10 @@ export default async function saveAsTemplate(request) {
};
templateCls.set('Placeholders', [placeHolders]);
} else {
const placeHolders = _docRes?.Placeholders.map((signer, signerIndex) => ({
// copy everything else, then overwrite these fields:
...signer,
signerObjId: '',
signerPtr: {},
Role: `Role ${signerIndex + 1}`,
email: '',
// rebuild placeHolder/pages
placeHolder: (signer.placeHolder || []).map(page => ({
...page,
pos: (page.pos || []).map(widget => {
// if there is a defaultValue in options, zero it out
if (widget.options && widget.options.defaultValue !== undefined) {
return { ...widget, options: { ...widget.options, defaultValue: '' } }; // reset only the value
}
// otherwise, return the widget unchanged
return widget;
}),
})),
}));
const placeHolders = _docRes?.Placeholders?.map((x, i) => {
const email = x.email ? { email: '' } : {};
return { ...x, signerObjId: '', signerPtr: {}, Role: 'Role ' + (i + 1), ...email };
});
templateCls.set('Placeholders', placeHolders);
}
}
@@ -38,6 +38,7 @@ const makeEmail = async (
replyto,
testPdf
) => {
const publicUrl = new URL(process.env.SERVER_URL);
const htmlContent = html;
const boundary = 'boundary_' + Date.now().toString(16);
const bccHeader = bcc && bcc.length > 0 ? `BCC: ${bcc.join(',')}\n` : ''; // Construct BCC header if provided
@@ -8,15 +8,12 @@ export default async function updateTenant(request) {
if (!request.user) {
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'unauthorized');
}
const validKeys = ['CompletionBody', 'CompletionSubject', 'RequestBody', 'RequestSubject'];
try {
const tenant = new Parse.Object('partners_Tenant');
tenant.id = tenantId;
// Update tenant details
Object.keys(details).forEach(key => {
if (validKeys.includes(key)) {
tenant.set(key, details?.[key]);
}
tenant.set(key, details?.[key]);
});
const tenantRes = await tenant.save(null, { useMasterKey: true });
@@ -1,7 +1,7 @@
import axios from 'axios';
import { cloudServerUrl, serverAppId } from '../../Utils.js';
import { cloudServerUrl } from '../../Utils.js';
const serverUrl = cloudServerUrl; //process.env.SERVER_URL;
const APPID = serverAppId;
const APPID = process.env.APP_ID;
const masterKEY = process.env.MASTER_KEY;
async function saveUser(userDetails) {
@@ -1,19 +0,0 @@
/**
*
* @param {Parse} Parse
*/
exports.up = async Parse => {
const schema = new Parse.Schema('contracts_Subscriptions');
schema.addNumber('CreditAlertLevel');
return schema.update();
};
/**
*
* @param {Parse} Parse
*/
exports.down = async Parse => {
const schema = new Parse.Schema('contracts_Subscriptions');
schema.deleteField('CreditAlertLevel');
return schema.update();
};
+5 -4
View File
@@ -15,7 +15,7 @@ import AWS from 'aws-sdk';
import { app as customRoute } from './cloud/customRoute/customApp.js';
import { exec } from 'child_process';
import { createTransport } from 'nodemailer';
import { appName, cloudServerUrl, serverAppId, smtpenable, smtpsecure, useLocal } from './Utils.js';
import { appName, cloudServerUrl, smtpenable, smtpsecure, useLocal } from './Utils.js';
import { SSOAuth } from './auth/authadapter.js';
import createContactIndex from './migrationdb/createContactIndex.js';
import { validateSignedLocalUrl } from './cloud/parsefunction/getSignedUrl.js';
@@ -97,7 +97,7 @@ export const config = {
cloud: function () {
import('./cloud/main.js');
},
appId: serverAppId,
appId: process.env.APP_ID || 'opensign',
logLevel: ['error'],
maxLimit: 500,
maxUploadSize: '30mb',
@@ -232,9 +232,10 @@ if (!process.env.TESTING) {
const isWindows = process.platform === 'win32';
// console.log('isWindows', isWindows);
createContactIndex();
const migrate = isWindows
? `set APPLICATION_ID=${serverAppId}&& set SERVER_URL=${cloudServerUrl}&& set MASTER_KEY=${process.env.MASTER_KEY}&& npx parse-dbtool migrate`
: `APPLICATION_ID=${serverAppId} SERVER_URL=${cloudServerUrl} MASTER_KEY=${process.env.MASTER_KEY} npx parse-dbtool migrate`;
? `set APPLICATION_ID=${process.env.APP_ID}&& set SERVER_URL=${cloudServerUrl}&& set MASTER_KEY=${process.env.MASTER_KEY}&& npx parse-dbtool migrate`
: `APPLICATION_ID=${process.env.APP_ID} SERVER_URL=${cloudServerUrl} MASTER_KEY=${process.env.MASTER_KEY} npx parse-dbtool migrate`;
exec(migrate, (error, stdout, stderr) => {
if (error) {
console.error(`Error: ${error.message}`);
+863 -1303
View File
File diff suppressed because it is too large Load Diff
+12 -12
View File
@@ -18,51 +18,51 @@
"watch": "nodemon index.js"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.840.0",
"@aws-sdk/s3-request-presigner": "^3.840.0",
"@aws-sdk/client-s3": "^3.828.0",
"@aws-sdk/s3-request-presigner": "^3.828.0",
"@parse/fs-files-adapter": "^3.0.0",
"@parse/s3-files-adapter": "^4.2.0",
"@parse/s3-files-adapter": "^4.1.1",
"@pdf-lib/fontkit": "^1.1.1",
"@signpdf/placeholder-pdf-lib": "^3.2.6",
"@signpdf/signer-p12": "^3.2.4",
"@signpdf/signpdf": "^3.2.5",
"aws-sdk": "^2.1692.0",
"axios": "^1.10.0",
"axios": "^1.9.0",
"coherentpdf": "^2.5.5",
"cors": "^2.8.5",
"date-fns-tz": "^3.2.0",
"dotenv": "^16.6.1",
"dotenv": "^16.5.0",
"express": "^5.1.0",
"form-data": "^4.0.3",
"generate-api-key": "^1.0.2",
"googleapis": "^150.0.1",
"libreoffice-convert": "^1.6.1",
"mailgun.js": "^12.0.3",
"mailgun.js": "^12.0.2",
"mongodb": "^6.17.0",
"multer": "^2.0.1",
"multer-s3": "^3.0.1",
"node-forge": "^1.3.1",
"nodemailer": "^7.0.4",
"nodemailer": "^7.0.3",
"parse": "^6.1.1",
"parse-dbtool": "^1.2.0",
"parse-server": "^8.2.1",
"parse-server-api-mail-adapter": "^4.1.0",
"pdf-lib": "^1.17.1",
"posthog-node": "^5.1.1",
"posthog-node": "^5.0.0",
"qrcode": "^1.5.4",
"rate-limiter-flexible": "^7.1.1",
"speakeasy": "^2.0.0",
"ws": "^8.18.3"
"ws": "^8.18.2"
},
"type": "module",
"devDependencies": {
"@babel/eslint-parser": "^7.27.5",
"eslint": "^9.29.0",
"eslint": "^9.28.0",
"jasmine": "^5.8.0",
"mongodb-runner": "^5.9.2",
"mongodb-runner": "^5.8.3",
"nodemon": "^3.1.10",
"nyc": "^17.1.0",
"prettier": "^3.6.2"
"prettier": "^3.5.3"
},
"overrides": {
"ws": "$ws",