mirror of
https://github.com/OpenSignLabs/OpenSign.git
synced 2026-08-20 06:35:54 +02:00
@@ -73,12 +73,28 @@ Welcome to OpenSign, the premier open source docusign alternative - document e-s
|
||||
|
||||
---
|
||||
|
||||
### Installation
|
||||
### Deploy
|
||||
|
||||
Note: The default MongoDB instance used in deployment is not persistant and will be cleared on every restart. To retain your data, configure and supply your own MongoDB connection URL.
|
||||
|
||||
#### DigitalOcean
|
||||
[](https://cloud.digitalocean.com/apps/new?repo=https://github.com/OpenSignLabs/Deploy-OpenSign-to-Digital-Ocean/tree/main&refcode=30db1c901ab0)
|
||||
|
||||
#### Docker
|
||||
The simplest way to install OpenSign on your own server is using official docker images by running the following command -
|
||||
|
||||
**Command for linux/MacOS**
|
||||
```
|
||||
export HOST_URL=https://opensign.yourdomain.com && curl --remote-name-all https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/docker-compose.yml https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/Caddyfile https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/.env.local_dev && mv .env.local_dev .env.prod && docker compose up --force-recreate
|
||||
```
|
||||
**Command for Windows (Powershell)**
|
||||
```
|
||||
$env:HOST_URL="https://opensign.yourdomain.com"; Invoke-WebRequest -Uri https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/docker-compose.yml -OutFile docker-compose.yml; Invoke-WebRequest -Uri https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/Caddyfile -OutFile Caddyfile; Invoke-WebRequest -Uri https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/.env.local_dev -OutFile .env.local_dev; Rename-Item -Path .env.local_dev -NewName .env.prod; docker compose up --force-recreate
|
||||
```
|
||||
**Command for Windows (CMD/Terminal)**
|
||||
```
|
||||
set HOST_URL=https://opensign.yourdomain.com && curl -O https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/docker-compose.yml && curl -O https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/Caddyfile && curl -O https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/.env.local_dev && rename .env.local_dev .env.prod && docker compose up --force-recreate
|
||||
```
|
||||
Make sure that you have `Docker` and `git` installed before you run this command -
|
||||
|
||||
Please refer to the [Installation Guide](https://docs.opensignlabs.com/docs/self-host/docker/run-locally/) for detailed instructions on how to install OpenSign on your system.
|
||||
|
||||
Generated
+923
-192
File diff suppressed because it is too large
Load Diff
+20
-18
@@ -4,11 +4,11 @@
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@formkit/auto-animate": "^0.8.2",
|
||||
"@lottiefiles/dotlottie-react": "^0.13.5",
|
||||
"@imgly/background-removal": "^1.6.0",
|
||||
"@lottiefiles/dotlottie-react": "^0.14.0",
|
||||
"@pdf-lib/fontkit": "^1.1.1",
|
||||
"@radix-ui/themes": "^3.2.1",
|
||||
"@reduxjs/toolkit": "^2.8.2",
|
||||
"@imgly/background-removal": "^1.6.0",
|
||||
"axios": "^1.9.0",
|
||||
"date-fns-tz": "^3.2.0",
|
||||
"file-saver": "^2.0.5",
|
||||
@@ -19,15 +19,15 @@
|
||||
"jwt-decode": "^4.0.0",
|
||||
"moment": "^2.30.1",
|
||||
"parse": "^6.1.1",
|
||||
"pkijs": "^3.0.8",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"pkijs": "^3.0.8",
|
||||
"print-js": "^1.6.0",
|
||||
"prismjs": "^1.30.0",
|
||||
"radix-ui": "^1.4.2",
|
||||
"react": "^18.3.1",
|
||||
"react-bootstrap": "^2.10.10",
|
||||
"react-confetti": "^6.4.0",
|
||||
"react-datepicker": "^8.3.0",
|
||||
"react-datepicker": "^8.4.0",
|
||||
"react-dnd": "^16.0.1",
|
||||
"react-dnd-html5-backend": "^16.0.1",
|
||||
"react-dnd-multi-backend": "^9.0.0",
|
||||
@@ -35,13 +35,13 @@
|
||||
"react-dom": "^18.3.1",
|
||||
"react-gtm-module": "^2.0.11",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-i18next": "^15.5.1",
|
||||
"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.0",
|
||||
"react-router": "^7.6.1",
|
||||
"react-scrollbars-custom": "^4.1.1",
|
||||
"react-select": "^5.10.1",
|
||||
"react-signature-canvas": "^1.1.0-alpha.2",
|
||||
@@ -52,7 +52,7 @@
|
||||
"regex-parser": "^2.3.1",
|
||||
"serve": "^14.2.4",
|
||||
"styled-components": "^5.3.11",
|
||||
"web-vitals": "^5.0.1",
|
||||
"web-vitals": "^5.0.2",
|
||||
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz"
|
||||
},
|
||||
"scripts": {
|
||||
@@ -93,16 +93,17 @@
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.27.1",
|
||||
"@babel/core": "^7.27.4",
|
||||
"@babel/preset-env": "^7.27.2",
|
||||
"@babel/preset-react": "^7.27.1",
|
||||
"@babel/runtime-corejs2": "^7.27.1",
|
||||
"@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.22",
|
||||
"@vitejs/plugin-react": "^4.4.1",
|
||||
"@vitejs/plugin-react-swc": "^3.9.0",
|
||||
"@types/react": "^18.3.23",
|
||||
"@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",
|
||||
@@ -110,19 +111,20 @@
|
||||
"css-loader": "^7.1.2",
|
||||
"daisyui": "^4.12.24",
|
||||
"dotenv": "^16.5.0",
|
||||
"eslint": "^9.27.0",
|
||||
"eslint-plugin-prettier": "^5.4.0",
|
||||
"eslint": "^9.28.0",
|
||||
"eslint-plugin-prettier": "^5.4.1",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"lint-staged": "^16.0.0",
|
||||
"postcss": "^8.5.3",
|
||||
"jsdom": "^26.1.0",
|
||||
"lint-staged": "^16.1.0",
|
||||
"postcss": "^8.5.4",
|
||||
"prettier": "^3.5.3",
|
||||
"pretty-quick": "^4.1.1",
|
||||
"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.1.4"
|
||||
"vitest": "^3.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || 22"
|
||||
|
||||
@@ -1022,5 +1022,8 @@
|
||||
"no-signer-info-in-pkcs7": "Keine Signaturinformationen in PKCS#7",
|
||||
"could-not-parse-signer-info": "Signaturinformationen konnten nicht analysiert werden",
|
||||
"not-calculated": "Nicht berechnet",
|
||||
"not-found-in-signature": "Nicht in Signatur gefunden"
|
||||
"not-found-in-signature": "Nicht in Signatur gefunden",
|
||||
"readonly-textinput-error": "Schreibgeschütztes Text-Widget muss einen Standardwert haben oder optional sein.",
|
||||
"readonly-dropdown-error": "Schreibgeschütztes Dropdown-Widget muss einen Standardwert haben oder optional sein.",
|
||||
"readonly-radiobtn-error": "Schreibgeschütztes Optionsfeld-Widget muss einen Standardwert haben oder optional sein."
|
||||
}
|
||||
|
||||
@@ -1022,5 +1022,8 @@
|
||||
"no-signer-info-in-pkcs7": "No signer info in PKCS#7",
|
||||
"could-not-parse-signer-info": "Could not parse signer info",
|
||||
"not-calculated": "Not calculated",
|
||||
"not-found-in-signature": "Not found in signature"
|
||||
"not-found-in-signature": "Not found in signature",
|
||||
"readonly-textinput-error": "Read-only text widget must have a default value or you can make it optional.",
|
||||
"readonly-dropdown-error": "Read-only dropdown widget must have a default value or you can make it optional.",
|
||||
"readonly-radiobtn-error": "Read-only radio button widget must have a default value or you can make it optional."
|
||||
}
|
||||
@@ -1022,5 +1022,8 @@
|
||||
"no-signer-info-in-pkcs7": "No hay información del firmante en PKCS#7",
|
||||
"could-not-parse-signer-info": "No se pudo analizar la información del firmante",
|
||||
"not-calculated": "No calculado",
|
||||
"not-found-in-signature": "No encontrado en la firma"
|
||||
"not-found-in-signature": "No encontrado en la firma",
|
||||
"readonly-textinput-error": "El widget de texto de solo lectura debe tener un valor predeterminado o puede hacerse opcional.",
|
||||
"readonly-dropdown-error": "El widget desplegable de solo lectura debe tener un valor predeterminado o puede hacerse opcional.",
|
||||
"readonly-radiobtn-error": "El widget de botón de opción de solo lectura debe tener un valor predeterminado o puede hacerse opcional."
|
||||
}
|
||||
|
||||
@@ -1022,5 +1022,8 @@
|
||||
"no-signer-info-in-pkcs7": "Aucune information sur le signataire dans PKCS#7",
|
||||
"could-not-parse-signer-info": "Impossible d'analyser les informations sur le signataire",
|
||||
"not-calculated": "Non calculé",
|
||||
"not-found-in-signature": "Introuvable dans la signature"
|
||||
"not-found-in-signature": "Introuvable dans la signature",
|
||||
"readonly-textinput-error": "Le widget de texte en lecture seule doit avoir une valeur par défaut ou être rendu optionnel.",
|
||||
"readonly-dropdown-error": "Le widget déroulant en lecture seule doit avoir une valeur par défaut ou être rendu optionnel.",
|
||||
"readonly-radiobtn-error": "Le widget bouton radio en lecture seule doit avoir une valeur par défaut ou être rendu optionnel."
|
||||
}
|
||||
|
||||
@@ -1022,5 +1022,8 @@
|
||||
"no-signer-info-in-pkcs7": "PKCS#7 में कोई हस्ताक्षरकर्ता जानकारी नहीं है",
|
||||
"could-not-parse-signer-info": "हस्ताक्षरकर्ता जानकारी पार्स नहीं की जा सकी",
|
||||
"not-calculated": "गणना नहीं की गई",
|
||||
"not-found-in-signature": "हस्ताक्षर में नहीं मिला"
|
||||
"not-found-in-signature": "हस्ताक्षर में नहीं मिला",
|
||||
"readonly-textinput-error": "रीड-ओनली टेक्स्ट विजेट में एक डिफ़ॉल्ट मान होना चाहिए या आप इसे वैकल्पिक बना सकते हैं।",
|
||||
"readonly-dropdown-error": "रीड-ओनली ड्रॉपडाउन विजेट में एक डिफ़ॉल्ट मान होना चाहिए या आप इसे वैकल्पिक बना सकते हैं।",
|
||||
"readonly-radiobtn-error": "रीड-ओनली रेडियो बटन विजेट में एक डिफ़ॉल्ट मान होना चाहिए या आप इसे वैकल्पिक बना सकते हैं।"
|
||||
}
|
||||
|
||||
@@ -1022,5 +1022,8 @@
|
||||
"no-signer-info-in-pkcs7": "Nessuna informazione firmatario in PKCS#7",
|
||||
"could-not-parse-signer-info": "Impossibile analizzare le informazioni del firmatario",
|
||||
"not-calculated": "Non calcolato",
|
||||
"not-found-in-signature": "Non trovato nella firma"
|
||||
"not-found-in-signature": "Non trovato nella firma",
|
||||
"readonly-textinput-error": "Il widget di testo di sola lettura deve avere un valore predefinito oppure può essere reso opzionale.",
|
||||
"readonly-dropdown-error": "Il widget a discesa di sola lettura deve avere un valore predefinito oppure può essere reso opzionale.",
|
||||
"readonly-radiobtn-error": "Il widget pulsante di opzione di sola lettura deve avere un valore predefinito oppure può essere reso opzionale."
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ const AddAdmin = lazy(() => import("./pages/AddAdmin"));
|
||||
const UpdateExistUserAdmin = lazy(() => import("./pages/UpdateExistUserAdmin"));
|
||||
const Preferences = lazy(() => import("./pages/Preferences"));
|
||||
const Login = lazy(() => import("./pages/Login"));
|
||||
|
||||
const VerifyDocument = lazy(() => import("./pages/VerifyDocument"));
|
||||
pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/legacy/build/pdf.worker.min.mjs`;
|
||||
const AppLoader = () => {
|
||||
return (
|
||||
@@ -166,6 +166,10 @@ function App() {
|
||||
element={<PdfRequestFiles />}
|
||||
/>
|
||||
<Route path="/users" element={<UserList />} />
|
||||
<Route
|
||||
path="/verify-document"
|
||||
element={<LazyPage Page={VerifyDocument} />}
|
||||
/>
|
||||
<Route
|
||||
path="/preferences"
|
||||
element={<LazyPage Page={Preferences} />}
|
||||
|
||||
@@ -183,6 +183,17 @@ const Header = ({ showSidebar, setIsMenu, isConsole }) => {
|
||||
{t("change-password")}
|
||||
</span>
|
||||
</li>
|
||||
<li
|
||||
onClick={() => {
|
||||
setIsOpen(false);
|
||||
navigate("/verify-document");
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
<i className="fa-light fa-check-square"></i>{" "}
|
||||
{t("verify-document")}
|
||||
</span>
|
||||
</li>
|
||||
</>
|
||||
)}
|
||||
<li onClick={closeDropdown}>
|
||||
|
||||
@@ -12,19 +12,32 @@ function AgreementSign(props) {
|
||||
<div className="op-modal op-modal-open absolute z-[448]">
|
||||
<div className="w-[95%] md:w-[60%] lg:w-[40%] op-modal-box overflow-y-auto hide-scrollbar text-sm p-4">
|
||||
<div className="flex flex-row items-center">
|
||||
<input
|
||||
data-tut="IsAgree"
|
||||
className="mr-3 op-checkbox op-checkbox-m"
|
||||
type="checkbox"
|
||||
value={isChecked}
|
||||
onChange={(e) => {
|
||||
setIsChecked(e.target.checked);
|
||||
if (e.target.checked) {
|
||||
props.setIsAgreeTour(false);
|
||||
}
|
||||
props.showFirstWidget();
|
||||
}}
|
||||
/>
|
||||
<label className="inline-flex justify-center items-center cursor-pointer mb-0">
|
||||
{/* 1) This div becomes the “fake” checkbox */}
|
||||
<div
|
||||
data-tut="IsAgree"
|
||||
className={`w-6 h-6 border-2 mr-3 rounded-full flex text-center items-center justify-center ${isChecked ? "op-border-primary" : "border-red-500"}`}
|
||||
>
|
||||
{isChecked ? (
|
||||
<span className="op-text-primary text-sm font-bold">✓</span>
|
||||
) : (
|
||||
<span className="text-red-500 text-sm font-bold">X</span>
|
||||
)}
|
||||
</div>
|
||||
{/* 2) Visually hide the native checkbox but keep it in the DOM */}
|
||||
<input
|
||||
className="sr-only"
|
||||
type="checkbox"
|
||||
checked={isChecked}
|
||||
onChange={(e) => {
|
||||
setIsChecked(e.target.checked);
|
||||
if (e.target.checked) {
|
||||
props.setIsAgreeTour(false);
|
||||
}
|
||||
props.showFirstWidget();
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<div className="text-[11px] md:text-base">
|
||||
<span>{t("agree-p1")}</span>
|
||||
<span
|
||||
|
||||
@@ -103,6 +103,22 @@ function DropdownWidgetOption(props) {
|
||||
? defaultCheckbox
|
||||
: defaultValue;
|
||||
|
||||
const isDropdownOrRadio =
|
||||
props?.type === "dropdown" || props?.type === radioButtonWidget;
|
||||
const readOnlyWithoutValue =
|
||||
isReadOnly && !defaultValue && status !== "optional";
|
||||
|
||||
// If it’s a dropdown and it’s read-only without a value (nor marked optional), stop here.
|
||||
if (isDropdownOrRadio && readOnlyWithoutValue) {
|
||||
alert(
|
||||
props?.type === "dropdown"
|
||||
? t("readonly-dropdown-error")
|
||||
: t("readonly-radiobtn-error")
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise (either not a dropdown, or a valid dropdown), do the save + reset exactly once.
|
||||
props.handleSaveWidgetsOptions(
|
||||
dropdownName,
|
||||
dropdownOptionList,
|
||||
@@ -228,7 +244,7 @@ function DropdownWidgetOption(props) {
|
||||
</select>
|
||||
</>
|
||||
)}
|
||||
{props.type !== "checkbox" && props.type !== radioButtonWidget && (
|
||||
{props.type !== "checkbox" && (
|
||||
<>
|
||||
<div className="flex flex-row gap-[10px] mt-[0.5rem]">
|
||||
{statusArr.map((data, ind) => {
|
||||
|
||||
@@ -241,6 +241,7 @@ function WidgetComponent(props) {
|
||||
handleDivClick={props.handleDivClick}
|
||||
handleMouseLeave={props.handleMouseLeave}
|
||||
signRef={signRef}
|
||||
addPositionOfSignature={props.addPositionOfSignature}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -83,6 +83,19 @@ const WidgetNameModal = (props) => {
|
||||
props.handleData(data, props.defaultdata?.type);
|
||||
}
|
||||
} else {
|
||||
const isTextInput = props.defaultdata?.type === textInputWidget;
|
||||
const { isReadOnly, defaultValue, status } = formdata;
|
||||
// If it’s a text‐input widget, enforce that read-only fields have
|
||||
// either a defaultValue or an "optional" status.
|
||||
if (isTextInput) {
|
||||
const readOnlyWithoutValue =
|
||||
isReadOnly && !defaultValue && status !== "optional";
|
||||
|
||||
if (readOnlyWithoutValue) {
|
||||
alert(t("readonly-textinput-error"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
props.handleData(formdata);
|
||||
}
|
||||
setFormdata({
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
onSaveSign,
|
||||
radioButtonWidget,
|
||||
selectCheckbox,
|
||||
signatureTypes,
|
||||
textInputWidget,
|
||||
textWidget,
|
||||
years
|
||||
@@ -85,7 +84,8 @@ function WidgetsValueModal(props) {
|
||||
setXyPosition,
|
||||
isSave,
|
||||
setUniqueId,
|
||||
tempSignerId
|
||||
tempSignerId,
|
||||
signatureTypes
|
||||
} = props;
|
||||
const [penColor, setPenColor] = useState("blue");
|
||||
const [isOptional, setIsOptional] = useState(true);
|
||||
@@ -400,7 +400,7 @@ function WidgetsValueModal(props) {
|
||||
|
||||
if (getIndex !== -1) {
|
||||
setIsSignTypes(true);
|
||||
const tab = signatureTypes[getIndex].name;
|
||||
const tab = signatureTypes?.[getIndex].name;
|
||||
if (tab === "draw") {
|
||||
setIsTab("draw");
|
||||
setSignatureType("draw");
|
||||
@@ -427,7 +427,7 @@ function WidgetsValueModal(props) {
|
||||
}
|
||||
}
|
||||
function isTabEnabled(tabName) {
|
||||
const isEnabled = signatureTypes.find((x) => x.name === tabName)?.enabled;
|
||||
const isEnabled = signatureTypes?.find((x) => x.name === tabName)?.enabled;
|
||||
return isEnabled;
|
||||
}
|
||||
|
||||
@@ -683,51 +683,64 @@ function WidgetsValueModal(props) {
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isTab]);
|
||||
//function for convert input text value in image
|
||||
// function for convert input text value in image
|
||||
const convertToImg = async (fontStyle, text, color) => {
|
||||
//get text content to convert in image
|
||||
const textContent = text;
|
||||
const fontfamily = fontStyle
|
||||
? fontStyle
|
||||
: fontSelect
|
||||
? fontSelect
|
||||
: "Fasthand";
|
||||
const fontSizeValue = "40px";
|
||||
//creating span for getting text content width
|
||||
// 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;
|
||||
|
||||
// 3) Create a temporary <span> (hidden) to measure the text at 40px:
|
||||
const span = document.createElement("span");
|
||||
span.textContent = textContent;
|
||||
span.style.font = `${fontSizeValue} ${fontfamily}`; // here put your text size and font family
|
||||
span.style.color = color ? color : penColor;
|
||||
span.style.display = "hidden";
|
||||
document.body.appendChild(span); // Replace 'container' with the ID of the container element
|
||||
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);
|
||||
|
||||
//create canvas to render text in canvas and convert in image
|
||||
const canvasElement = document.createElement("canvas");
|
||||
// Draw the text content on the canvas
|
||||
const ctx = canvasElement.getContext("2d");
|
||||
// Measured size at 40px:
|
||||
const measuredWidth = span.offsetWidth;
|
||||
const measuredHeight = span.offsetHeight;
|
||||
document.body.removeChild(span);
|
||||
|
||||
// 4) Compute uniform scale so that 40px‐sized 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 addExtraWidth = currWidgetsDetails?.type === "initials" ? 10 : 50;
|
||||
const width = span.offsetWidth + addExtraWidth;
|
||||
const height = span.offsetHeight;
|
||||
setTextWidth(width);
|
||||
setTextHeight(height);
|
||||
const font = span.style["font"];
|
||||
// Set the canvas dimensions to match the span
|
||||
canvasElement.width = width * pixelRatio;
|
||||
canvasElement.height = height * pixelRatio;
|
||||
const canvas = document.createElement("canvas");
|
||||
|
||||
// You can customize text styles if needed
|
||||
ctx.font = font;
|
||||
ctx.fillStyle = color ? color : penColor; // Set the text color
|
||||
// ★ 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";
|
||||
ctx.scale(pixelRatio, pixelRatio);
|
||||
// Draw the content of the span onto the canvas
|
||||
ctx.fillText(span.textContent, width / 2, height / 2); // Adjust the x,y-coordinate as needed
|
||||
//remove span tag
|
||||
document.body.removeChild(span);
|
||||
// Convert the canvas to image data
|
||||
const dataUrl = canvasElement.toDataURL("image/png");
|
||||
|
||||
// ★ 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);
|
||||
};
|
||||
const PenColorComponent = (props) => {
|
||||
@@ -1671,8 +1684,12 @@ function WidgetsValueModal(props) {
|
||||
validateExpression(regexValidation);
|
||||
break;
|
||||
default:
|
||||
regexValidation =
|
||||
currWidgetsDetails?.options?.validation?.pattern || "";
|
||||
// Grab the current pattern (if it exists)
|
||||
const pattern = currWidgetsDetails?.options?.validation?.pattern;
|
||||
// Removed `backwordSupportPattern` (/^[a-zA-Z0-9s]+$/) — it blocked spaces and special characters.
|
||||
const backwordSupportPattern =
|
||||
pattern && pattern === "/^[a-zA-Z0-9s]+$/" ? "" : pattern; // If it matches exactly '/^[a-zA-Z0-9s]+$/', clear it
|
||||
regexValidation = backwordSupportPattern || "";
|
||||
validateExpression(regexValidation);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -683,10 +683,14 @@ export const signPdfFun = async (
|
||||
};
|
||||
|
||||
export const randomId = () => {
|
||||
const randomBytes = crypto.getRandomValues(new Uint16Array(1));
|
||||
const randomValue = randomBytes[0];
|
||||
const randomDigit = 1000 + (randomValue % 9000);
|
||||
return randomDigit;
|
||||
// 1. Grab a cryptographically-secure 32-bit random value
|
||||
const randomBytes = crypto.getRandomValues(new Uint32Array(1));
|
||||
const raw = randomBytes[0]; // 0 … 4 294 967 295
|
||||
|
||||
// 2. Collapse into a 90 000 000-wide band (0…89 999 999), then shift to 10 000 000…99 999 999
|
||||
const eightDigit = 10_000_000 + (raw % 90_000_000);
|
||||
|
||||
return eightDigit;
|
||||
};
|
||||
|
||||
export const createDocument = async (
|
||||
@@ -1552,7 +1556,7 @@ export const multiSignEmbed = async (
|
||||
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
|
||||
//'getSize' is used to get particular pdf's page width and height
|
||||
const getSize = pdfOriginalWH.find(
|
||||
(page) => page?.pageNumber === item?.pageNumber
|
||||
);
|
||||
|
||||
@@ -455,7 +455,7 @@ const Forms = (props) => {
|
||||
setBcc([]);
|
||||
setFolder({ ObjectId: "", Name: "" });
|
||||
const notifySign =
|
||||
extUserData?.NotifyOnSignatures
|
||||
extUserData?.NotifyOnSignatures !== undefined
|
||||
? extUserData?.NotifyOnSignatures
|
||||
: true;
|
||||
setFormData({
|
||||
@@ -534,7 +534,7 @@ const Forms = (props) => {
|
||||
setBcc([]);
|
||||
setFolder({ ObjectId: "", Name: "" });
|
||||
const notifySign =
|
||||
extUserData?.NotifyOnSignatures
|
||||
extUserData?.NotifyOnSignatures !== undefined
|
||||
? extUserData?.NotifyOnSignatures
|
||||
: true;
|
||||
let obj = {
|
||||
|
||||
@@ -821,7 +821,7 @@ function PdfRequestFiles(
|
||||
await embedDocId(pdfOriginalWH, pdfDoc, docId);
|
||||
}
|
||||
}
|
||||
//embed all widgets in document
|
||||
//embed all widgets in document
|
||||
const pdfBytes = await multiSignEmbed(
|
||||
pdfOriginalWH,
|
||||
widgets,
|
||||
@@ -865,7 +865,10 @@ function PdfRequestFiles(
|
||||
const user = usermail?.Email
|
||||
? usermail
|
||||
: pdfDetails?.[0]?.Signers[newIndex];
|
||||
if (sendmail !== "false" && sendInOrder) {
|
||||
if (
|
||||
sendmail !== "false" &&
|
||||
sendInOrder
|
||||
) {
|
||||
const requestBody =
|
||||
updatedDoc.updatedPdfDetails?.[0]?.RequestBody;
|
||||
const requestSubject =
|
||||
@@ -1564,11 +1567,15 @@ function PdfRequestFiles(
|
||||
const widgetValue = widgetDataValue(dragTypeValue, parseUser);
|
||||
//adding and updating drop position in array when user drop signature button in div
|
||||
if (item === "onclick") {
|
||||
// `getBoundingClientRect()` is used to get accurate measurement width, height of the Pdf div
|
||||
const divWidth = divRef.current.getBoundingClientRect().width;
|
||||
const divHeight = divRef.current.getBoundingClientRect().height;
|
||||
// `getBoundingClientRect()` is used to get accurate measurement height of the div
|
||||
// Compute the pixel‐space center within the PDF viewport:
|
||||
const centerX_Pixels = divWidth / 2 - widgetWidth / 2;
|
||||
const xPosition_Final = centerX_Pixels / (containerScale * scale);
|
||||
dropObj = {
|
||||
//onclick put placeholder center on pdf
|
||||
xPosition: widgetWidth / 4 + containerWH.width / 2,
|
||||
xPosition: xPosition_Final,
|
||||
yPosition: widgetHeight + divHeight / 2,
|
||||
isStamp:
|
||||
(dragTypeValue === "stamp" || dragTypeValue === "image") && true,
|
||||
@@ -2259,6 +2266,7 @@ function PdfRequestFiles(
|
||||
index={pageNumber}
|
||||
setUniqueId={setUniqueId}
|
||||
tempSignerId={tempSignerId}
|
||||
signatureTypes={signatureType}
|
||||
/>
|
||||
)}
|
||||
<DownloadPdfZip
|
||||
|
||||
@@ -193,7 +193,7 @@ function PlaceHolderSign() {
|
||||
);
|
||||
if (user) {
|
||||
try {
|
||||
const defaultRequestBody = `<p>Hi {{receiver_name}},</p><br><p>We hope this email finds you well. {{sender_name}} has requested you to review and sign {{document_title}}.</p><p>Your signature is crucial to proceed with the next steps as it signifies your agreement and authorization.</p><br><p>{{signing_url}}</p><br><p>If you have any questions or need further clarification regarding the document or the signing process, please contact the sender.</p><br><p>Thanks</p><p> Team ${appName}</p><br>`;
|
||||
const defaultRequestBody = `<p>Hi {{receiver_name}},</p><br><p>We hope this email finds you well. {{sender_name}} has requested you to review and sign {{document_title}}.</p><p>Your signature is crucial to proceed with the next steps as it signifies your agreement and authorization.</p><br><p><a href='{{signing_url}}' rel='noopener noreferrer' target='_blank'>Sign here</a></p><br><br><p>If you have any questions or need further clarification regarding the document or the signing process, please contact the sender.</p><br><p>Thanks</p><p> Team ${appName}</p><br>`;
|
||||
const defaultSubject = `{{sender_name}} has requested you to sign {{document_title}}`;
|
||||
setDefaultBody(defaultRequestBody);
|
||||
setDefaultSubject(defaultSubject);
|
||||
@@ -524,11 +524,15 @@ function PlaceHolderSign() {
|
||||
defaultWidthHeight(dragTypeValue).height * containerScale;
|
||||
//adding and updating drop position in array when user drop signature button in div
|
||||
if (item === "onclick") {
|
||||
// `getBoundingClientRect()` is used to get accurate measurement width, height of the Pdf div
|
||||
const divWidth = divRef.current.getBoundingClientRect().width;
|
||||
const divHeight = divRef.current.getBoundingClientRect().height;
|
||||
// `getBoundingClientRect()` is used to get accurate measurement height of the div
|
||||
// Compute the pixel‐space center within the PDF viewport:
|
||||
const centerX_Pixels = divWidth / 2 - widgetWidth / 2;
|
||||
const xPosition_Final = centerX_Pixels / (containerScale * scale);
|
||||
dropObj = {
|
||||
//onclick put placeholder center on pdf
|
||||
xPosition: widgetWidth / 4 + containerWH.width / 2,
|
||||
xPosition: xPosition_Final,
|
||||
yPosition: widgetHeight + divHeight / 2,
|
||||
isStamp:
|
||||
(dragTypeValue === "stamp" || dragTypeValue === "image") && true,
|
||||
@@ -1448,6 +1452,7 @@ function PlaceHolderSign() {
|
||||
...position.options,
|
||||
name: dropdownName,
|
||||
values: dropdownOptions,
|
||||
status: status,
|
||||
isReadOnly: isReadOnly || false,
|
||||
isHideLabel: isHideLabel || false,
|
||||
defaultValue: defaultValue,
|
||||
@@ -2589,6 +2594,7 @@ function PlaceHolderSign() {
|
||||
isSave={true}
|
||||
tempSignerId={tempSignerId}
|
||||
setUniqueId={setUniqueId}
|
||||
signatureTypes={signatureType}
|
||||
/>
|
||||
)}
|
||||
<ModalUi
|
||||
|
||||
@@ -218,7 +218,7 @@ const Preferences = () => {
|
||||
setIsLoader(true);
|
||||
const updateRes = tenantRes;
|
||||
setTenantId(updateRes?.objectId);
|
||||
const defaultRequestBody = `<p>Hi {{receiver_name}},</p><br><p>We hope this email finds you well. {{sender_name}} has requested you to review and sign {{document_title}}.</p><p>Your signature is crucial to proceed with the next steps as it signifies your agreement and authorization.</p><br><p>{{signing_url}}</p><br><p>If you have any questions or need further clarification regarding the document or the signing process, please contact the sender.</p><br><p>Thanks</p><p> Team ${appName}</p><br>`;
|
||||
const defaultRequestBody = `<p>Hi {{receiver_name}},</p><br><p>We hope this email finds you well. {{sender_name}} has requested you to review and sign {{document_title}}.</p><p>Your signature is crucial to proceed with the next steps as it signifies your agreement and authorization.</p><br><p><a href='{{signing_url}}' rel='noopener noreferrer' target='_blank'>Sign here</a></p><br><br><p>If you have any questions or need further clarification regarding the document or the signing process, please contact the sender.</p><br><p>Thanks</p><p> Team ${appName}</p><br>`;
|
||||
if (updateRes?.RequestBody) {
|
||||
setRequestBody(updateRes?.RequestBody);
|
||||
setRequestSubject(updateRes?.RequestSubject);
|
||||
|
||||
@@ -37,7 +37,8 @@ import {
|
||||
generatePdfName,
|
||||
handleRemoveWidgets,
|
||||
addWidgetSelfsignOptions,
|
||||
getOriginalWH
|
||||
getOriginalWH,
|
||||
signatureTypes
|
||||
} from "../constant/Utils";
|
||||
import { useParams } from "react-router";
|
||||
import Tour from "../primitives/Tour";
|
||||
@@ -365,15 +366,19 @@ function SignYourSelf() {
|
||||
);
|
||||
//adding and updating drop position in array when user drop signature button in div
|
||||
if (item === "onclick") {
|
||||
// `getBoundingClientRect()` is used to get accurate measurement height of the div
|
||||
// `getBoundingClientRect()` is used to get accurate measurement width, height of the Pdf div
|
||||
const divHeight = divRef.current.getBoundingClientRect().height;
|
||||
const divWidth = divRef.current.getBoundingClientRect().width;
|
||||
const getWidth = widgetTypeExist
|
||||
? calculateInitialWidthHeight(widgetValue).getWidth
|
||||
: defaultWidthHeight(dragTypeValue).width;
|
||||
const getHeight = defaultWidthHeight(dragTypeValue).height;
|
||||
|
||||
// Compute the pixel‐space center within the PDF viewport:
|
||||
const centerX_Pixels = divWidth / 2 - getWidth / 2;
|
||||
const xPosition_Final = centerX_Pixels / (containerScale * scale);
|
||||
dropObj = {
|
||||
xPosition: getWidth / 2 + containerWH.width / 2,
|
||||
xPosition: xPosition_Final,
|
||||
yPosition: getHeight + divHeight / 2,
|
||||
isStamp:
|
||||
(dragTypeValue === "stamp" || dragTypeValue === "image") && true,
|
||||
@@ -1330,6 +1335,7 @@ function SignYourSelf() {
|
||||
currWidgetsDetails={currWidgetsDetails}
|
||||
index={index}
|
||||
isSave={true}
|
||||
signatureTypes={signatureTypes}
|
||||
/>
|
||||
)}
|
||||
<RotateAlert
|
||||
|
||||
@@ -376,11 +376,15 @@ const TemplatePlaceholder = () => {
|
||||
filterSignerPos;
|
||||
let placeHolder;
|
||||
if (item === "onclick") {
|
||||
// `getBoundingClientRect()` is used to get accurate measurement height of the div
|
||||
// `getBoundingClientRect()` is used to get accurate measurement width, height of the Pdf div
|
||||
const divWidth = divRef.current.getBoundingClientRect().width;
|
||||
const divHeight = divRef.current.getBoundingClientRect().height;
|
||||
// Compute the pixel‐space center within the PDF viewport:
|
||||
const centerX_Pixels = divWidth / 2 - widgetWidth / 2;
|
||||
const xPosition_Final = centerX_Pixels / (containerScale * scale);
|
||||
dropObj = {
|
||||
//onclick put placeholder center on pdf
|
||||
xPosition: widgetWidth / 4 + containerWH.width / 2,
|
||||
xPosition: xPosition_Final,
|
||||
yPosition: widgetHeight + divHeight / 2,
|
||||
isStamp:
|
||||
(dragTypeValue === "stamp" || dragTypeValue === "image") && true,
|
||||
|
||||
@@ -763,7 +763,7 @@ const ReportTable = (props) => {
|
||||
const body =
|
||||
doc?.RequestBody ||
|
||||
doc?.ExtUserPtr?.TenantId?.RequestBody ||
|
||||
`<html><head><meta http-equiv='Content-Type' content='text/html; charset=UTF-8' /></head><body><p>Hi {{receiver_name}},</p><br><p>We hope this email finds you well. {{sender_name}} has requested you to review and sign <b>"{{document_title}}"</b>.</p><p>Your signature is crucial to proceed with the next steps as it signifies your agreement and authorization.</p><br><p>{{signing_url}}</p><br><p>If you have any questions or need further clarification regarding the document or the signing process, please contact the sender.</p><br><p>Thanks</p><p> Team ${appName}</p><br></body> </html>`;
|
||||
`<html><head><meta http-equiv='Content-Type' content='text/html; charset=UTF-8' /></head><body><p>Hi {{receiver_name}},</p><br><p>We hope this email finds you well. {{sender_name}} has requested you to review and sign <b>"{{document_title}}"</b>.</p><p>Your signature is crucial to proceed with the next steps as it signifies your agreement and authorization.</p><br><p><a href='{{signing_url}}' rel='noopener noreferrer' target='_blank'>Sign here</a></p><br><br><p>If you have any questions or need further clarification regarding the document or the signing process, please contact the sender.</p><br><p>Thanks</p><p> Team ${appName}</p><br></body> </html>`;
|
||||
const res = replaceMailVaribles(subject, body, variables);
|
||||
setMail((prev) => ({ ...prev, subject: res.subject, body: res.body }));
|
||||
setIsNextStep({ [user.Id]: true });
|
||||
|
||||
Generated
+2781
-3832
File diff suppressed because it is too large
Load Diff
@@ -18,10 +18,10 @@
|
||||
"watch": "nodemon index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.812.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.812.0",
|
||||
"@aws-sdk/client-s3": "^3.824.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.824.0",
|
||||
"@parse/fs-files-adapter": "^3.0.0",
|
||||
"@parse/s3-files-adapter": "^4.1.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",
|
||||
@@ -34,19 +34,19 @@
|
||||
"express": "^5.1.0",
|
||||
"form-data": "^4.0.2",
|
||||
"generate-api-key": "^1.0.2",
|
||||
"googleapis": "^148.0.0",
|
||||
"mailgun.js": "^12.0.1",
|
||||
"mongodb": "^6.16.0",
|
||||
"multer": "^2.0.0",
|
||||
"googleapis": "^149.0.0",
|
||||
"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.3",
|
||||
"parse": "^6.1.1",
|
||||
"parse-dbtool": "^1.2.0",
|
||||
"parse-server": "^8.2.0",
|
||||
"parse-server": "^8.2.1",
|
||||
"parse-server-api-mail-adapter": "^4.1.0",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"posthog-node": "^4.17.1",
|
||||
"posthog-node": "^4.18.0",
|
||||
"qrcode": "^1.5.4",
|
||||
"rate-limiter-flexible": "^7.1.1",
|
||||
"speakeasy": "^2.0.0",
|
||||
@@ -54,8 +54,8 @@
|
||||
},
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "^7.27.1",
|
||||
"eslint": "^9.27.0",
|
||||
"@babel/eslint-parser": "^7.27.5",
|
||||
"eslint": "^9.28.0",
|
||||
"jasmine": "^5.7.1",
|
||||
"mongodb-runner": "^5.8.3",
|
||||
"nodemon": "^3.1.10",
|
||||
|
||||
Reference in New Issue
Block a user