Compare commits

..
Author SHA1 Message Date
RaktimaNXG b2afe37942 feat: implement functionality to delete a PDF page while editing the document 2024-11-25 13:06:31 +05:30
AmolandGitHub 41c3b4746d Update CONTRIBUTING.md 2024-11-22 11:07:44 +05:30
AmolandGitHub f52d013b06 Update LICENSE 2024-11-21 18:36:03 +05:30
AmolandGitHub 0045603cb0 Create LICENSE 2024-11-21 18:35:11 +05:30
nxglabs 4235bc077d refactor: add comment for optional phone field 2024-11-21 17:53:08 +05:30
prafull-opensignlabsandGitHub 58b3cc6ba2 Merge pull request #1481 from OpenSignLabs/feat_status
fix: cannot read undefined objectId in reports
2024-11-21 11:04:02 +05:30
prafull-opensignlabs 97f5d09306 refactor: remove console log 2024-11-21 10:57:36 +05:30
prafull-opensignlabs fa98972b27 fix: cannot read undefined objectId in reports 2024-11-21 10:48:23 +05:30
AmolandGitHub 76848b7e09 Merge pull request #1479 from OpenSignLabs/feat_status
refactor: made phone field optional for signer/contact
2024-11-20 19:45:37 +05:30
prafull-opensignlabs c3faaa65b9 refactor: made phone field optional for signer/contact 2024-11-20 19:39:41 +05:30
AmolandGitHub e819513650 Merge pull request #1478 from OpenSignLabs/feat_status
feat: show status and signers list in need your sign and in-progress report
2024-11-20 19:21:20 +05:30
prafull-opensignlabs dd0e2f0e58 feat: show status and signers list in need your sign and in-progress report 2024-11-20 19:19:43 +05:30
prafull-opensignlabsandGitHub fa08efeac6 Merge pull request #1476 from OpenSignLabs/raktima-patch-12
fix: resolved text widget data embedding issue after sending the document
2024-11-20 16:31:28 +05:30
RaktimaandGitHub f3ba02496c Update PlaceHolderSign.js 2024-11-20 16:13:04 +05:30
RaktimaNXG 5cbbb00f66 fix: resolved text widget data embedding issue after sending the document 2024-11-20 15:34:02 +05:30
AndrewandGitHub 7050df8b4c Merge pull request #1474 from OpenSignLabs/uncompatible_pdf 2024-11-19 22:28:58 +05:30
prafull-opensignlabs 4fb27f4896 refactor: add date format in label for expiry date 2024-11-19 19:18:39 +05:30
AndrewandGitHub 145aa5da97 Merge pull request #1473 from OpenSignLabs/uncompatible_pdf 2024-11-19 18:20:59 +05:30
prafull-opensignlabs 1a455415f1 feat: add extend document expiry date functionality 2024-11-19 18:10:59 +05:30
prafull-opensignlabsandGitHub 44ef1fe825 Merge pull request #1472 from OpenSignLabs/uncompatible_pdf
feat: add column for expiry date in in-progress and expired report
2024-11-19 13:07:52 +05:30
prafull-opensignlabs cb07e376d5 feat: add column for expiry date in in-progress and expired report 2024-11-18 19:28:12 +05:30
AndrewandGitHub 9b472a6b8b Merge pull request #1471 from OpenSignLabs/uncompatible_pdf 2024-11-18 18:23:52 +05:30
prafull-opensignlabs 9840fe9412 fix: notify on signatures not working in public template 2024-11-18 18:22:39 +05:30
prafull-opensignlabsandGitHub d1a99d5211 Merge pull request #1418 from hanicker/patch-1
Specify signatureVersion for AWS S3
2024-11-18 17:28:29 +05:30
prafull-opensignlabsandGitHub 5f7736882b Merge pull request #1470 from OpenSignLabs/uncompatible_pdf
fix: schema mismatch issue while saving preferences
2024-11-18 15:41:39 +05:30
prafull-opensignlabs d8095e077a fix: schema mismatch issue while saving preferences 2024-11-18 13:55:54 +05:30
AndrewandGitHub c8fae00ce5 Merge pull request #1469 from OpenSignLabs/uncompatible_pdf 2024-11-18 12:54:00 +05:30
prafull-opensignlabs 7c6a03a2d2 fix: notify on signature not working in quicksend and publictemplate 2024-11-18 12:52:17 +05:30
AndrewandGitHub 40c21321dd Merge pull request #1461 from OpenSignLabs/dependabot/npm_and_yarn/apps/OpenSignServer/npm_and_yarn-c3cc913ac2 2024-11-16 23:54:23 +05:30
AndrewandGitHub 476dbb555d Merge pull request #1462 from OpenSignLabs/dependabot/npm_and_yarn/apps/OpenSign/npm_and_yarn-c3cc913ac2 2024-11-16 23:54:10 +05:30
dependabot[bot]andGitHub 19b4bfb9ef build(deps): bump cross-spawn
Bumps the npm_and_yarn group in /apps/OpenSign with 1 update: [cross-spawn](https://github.com/moxystudio/node-cross-spawn).


Updates `cross-spawn` from 7.0.3 to 7.0.5
- [Changelog](https://github.com/moxystudio/node-cross-spawn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/moxystudio/node-cross-spawn/compare/v7.0.3...v7.0.5)

---
updated-dependencies:
- dependency-name: cross-spawn
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-16 17:06:34 +00:00
dependabot[bot]andGitHub e9a85162e7 build(deps): bump cross-spawn
Bumps the npm_and_yarn group in /apps/OpenSignServer with 1 update: [cross-spawn](https://github.com/moxystudio/node-cross-spawn).


Updates `cross-spawn` from 7.0.3 to 7.0.5
- [Changelog](https://github.com/moxystudio/node-cross-spawn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/moxystudio/node-cross-spawn/compare/v7.0.3...v7.0.5)

---
updated-dependencies:
- dependency-name: cross-spawn
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-16 17:05:07 +00:00
ZathielandGitHub 960c297995 Merge pull request #1460 from OpenSignLabs/dependabot/npm_and_yarn/apps/OpenSign/npm_and_yarn-7503158af6
build(deps): bump @eslint/plugin-kit from 0.2.0 to 0.2.3 in /apps/OpenSign in the npm_and_yarn group
2024-11-16 18:04:55 +01:00
ZathielandGitHub 50e525f448 Merge pull request #1459 from OpenSignLabs/dependabot/npm_and_yarn/apps/OpenSignServer/npm_and_yarn-7503158af6
build(deps): bump @eslint/plugin-kit from 0.2.0 to 0.2.3 in /apps/OpenSignServer in the npm_and_yarn group
2024-11-16 18:03:46 +01:00
dependabot[bot]andGitHub c86f048498 build(deps): bump @eslint/plugin-kit
Bumps the npm_and_yarn group in /apps/OpenSign with 1 update: [@eslint/plugin-kit](https://github.com/eslint/rewrite).


Updates `@eslint/plugin-kit` from 0.2.0 to 0.2.3
- [Release notes](https://github.com/eslint/rewrite/releases)
- [Changelog](https://github.com/eslint/rewrite/blob/main/release-please-config.json)
- [Commits](https://github.com/eslint/rewrite/compare/core-v0.2.0...plugin-kit-v0.2.3)

---
updated-dependencies:
- dependency-name: "@eslint/plugin-kit"
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-15 21:09:03 +00:00
dependabot[bot]andGitHub 39fd2f697f build(deps): bump @eslint/plugin-kit
Bumps the npm_and_yarn group in /apps/OpenSignServer with 1 update: [@eslint/plugin-kit](https://github.com/eslint/rewrite).


Updates `@eslint/plugin-kit` from 0.2.0 to 0.2.3
- [Release notes](https://github.com/eslint/rewrite/releases)
- [Changelog](https://github.com/eslint/rewrite/blob/main/release-please-config.json)
- [Commits](https://github.com/eslint/rewrite/compare/core-v0.2.0...plugin-kit-v0.2.3)

---
updated-dependencies:
- dependency-name: "@eslint/plugin-kit"
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-15 21:08:08 +00:00
prafull-opensignlabsandGitHub 3f2cf3ddf4 Merge pull request #1458 from OpenSignLabs/uncompatible_pdf
feat: send email notification to document owner when signer sign document
2024-11-14 19:26:38 +05:30
prafull-opensignlabs 5a53a70dcf feat: send email notification to document owner when signer sign document 2024-11-14 19:02:16 +05:30
prafull-opensignlabsandGitHub 66323f9347 Merge pull request #1456 from OpenSignLabs/uncompatible_pdf
fix: otp auth must be disable in certificate if OTP verification is off
2024-11-13 16:22:16 +05:30
prafull-opensignlabs c65f17a216 fix: otp auth must be disable in certificate if OTP verfication is off 2024-11-13 16:20:17 +05:30
prafull-opensignlabsandGitHub 9bf93fb231 Merge pull request #1454 from OpenSignLabs/uncompatible_pdf
fix: incompatible pdf error while signing pdf
2024-11-13 14:13:43 +05:30
prafull-opensignlabs f418e02270 fix: uncompatible pdf error while signing pdf 2024-11-13 14:11:11 +05:30
prafull-opensignlabsandGitHub 403254a5c6 Merge pull request #1451 from OpenSignLabs/validation
fix: server get crash in self-hosting
2024-11-13 10:09:14 +05:30
prafull-opensignlabs c893d96ad3 fix: server get crash in self-hosting 2024-11-13 00:33:46 +05:30
AndrewandGitHub 3971118af8 Merge pull request #1449 from OpenSignLabs/validation 2024-11-12 17:04:04 +05:30
prafull-opensignlabs 4db0d93f41 refactor: update news from header 2024-11-12 16:24:50 +05:30
Nicola M.andGitHub 124fbfd798 Update getSignedUrl.js 2024-11-05 12:17:46 +01:00
Nicola M.andGitHub 893e433805 Update uploadFiletoS3.js
set signatureVersion
2024-11-05 12:12:31 +01:00
54 changed files with 3219 additions and 3567 deletions
+2 -2
View File
@@ -77,6 +77,6 @@ Engage with the community on our [Gitter Channel](https://gitter.im/OpenSignLabs
## License
By contributing, you agree that your contributions will be licensed under its [MIT License](LICENSE).
By contributing, you agree that your contributions will be licensed under its [AGPL-V3 License](LICENSE).
---
---
+6 -1
View File
@@ -1,4 +1,9 @@
GNU AFFERO GENERAL PUBLIC LICENSE
* All content that resides under the "apps/OpenSignServer/cloud/customRoute" directory of this repository, if that directory exists, is licensed under the license defined in "apps/OpenSignServer/cloud/customRoute".
* All third party components incorporated into the OpenSign Software are licensed under the original license provided by the owner of the applicable component.
* Content outside of the above mentioned directories or restrictions above is available under the "GNU AFFERO GENERAL PUBLIC LICENSE" as defined below -
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
+1067 -254
View File
File diff suppressed because it is too large Load Diff
+9 -9
View File
@@ -6,7 +6,7 @@
"@formkit/auto-animate": "^0.8.2",
"@lottiefiles/dotlottie-react": "^0.9.3",
"@pdf-lib/fontkit": "^1.1.1",
"@radix-ui/themes": "^3.1.4",
"@radix-ui/themes": "^3.1.6",
"@react-pdf/renderer": "^4.0.0",
"@reduxjs/toolkit": "^2.3.0",
"axios": "^1.7.7",
@@ -34,13 +34,13 @@
"react-dom": "^18.2.0",
"react-gtm-module": "^2.0.11",
"react-helmet": "^6.1.0",
"react-i18next": "^15.0.3",
"react-i18next": "^15.1.1",
"react-konva": "^18.2.10",
"react-pdf": "^9.1.1",
"react-quill-new": "^3.3.3",
"react-redux": "^9.1.2",
"react-rnd": "^10.4.13",
"react-router-dom": "^6.27.0",
"react-router-dom": "^6.28.0",
"react-scripts": "5.0.1",
"react-scrollbars-custom": "^4.1.1",
"react-select": "^5.8.3",
@@ -54,7 +54,7 @@
"regex-parser": "^2.3.0",
"serve": "^14.2.4",
"styled-components": "^5.3.0",
"web-vitals": "^4.2.3",
"web-vitals": "^4.2.4",
"ws": "^8.18.0"
},
"scripts": {
@@ -103,17 +103,17 @@
"babel-loader": "^9.2.1",
"clean-webpack-plugin": "^4.0.0",
"commitizen": "^4.3.1",
"concurrently": "^9.0.1",
"concurrently": "^9.1.0",
"css-loader": "^7.1.2",
"daisyui": "^4.12.14",
"dotenv": "^16.4.5",
"dotenv-webpack": "^8.1.0",
"eslint": "^9.13.0",
"eslint": "^9.14.0",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-react": "^7.37.1",
"eslint-plugin-react": "^7.37.2",
"lint-staged": "^15.2.10",
"mini-css-extract-plugin": "^2.9.1",
"postcss": "^8.4.48",
"mini-css-extract-plugin": "^2.9.2",
"postcss": "^8.4.49",
"prettier": "^3.3.3",
"pretty-quick": "^4.0.0",
"tailwindcss": "^3.4.14",
@@ -1,5 +1,5 @@
{
"header-news": "New feature: Public profiles are now live! Easily create and share public templates",
"header-news": "New feature: Teams plan users can now integrate their own AWS S3 buckets for file storage",
"header-news-btn": "Setup Now",
"create-account": "Create Account",
"login": "Login",
@@ -134,7 +134,8 @@
"option": "Option",
"Embed": "Embed",
"Copy TemplateId":"Copy TemplateId",
"Copy Public URL":"Copy Public URL"
"Copy Public URL":"Copy Public URL",
"extend-expiry-date": "Extend expiry date"
},
"report-heading": {
"Sr.No": "Sr.No",
@@ -155,7 +156,8 @@
"Status": "Status",
"created-date": "Created Date",
"Type": "Type",
"Logs": "Logs"
"Logs": "Logs",
"Expiry-date": "Expiry Date"
},
"report-help": {
"Draft Documents": "These are documents you have started but have not finalized for sending.",
@@ -266,7 +268,8 @@
"make-template-public-alert": "Are you sure you want to make this template public?",
"make-template-private-alert": "Are you sure you want to make this template private? This will remove it from your public profile.",
"public-role": "Public role",
"public-url": "Public profile needed",
"public-url": "Public profile",
"embed-template": "Embed Template",
"public-url-copy": "Heres your public URL: ",
"public-url-copy-mssg":"Copy it or share it with the signer, and you will be able to see all your publicly set templates.",
"add-public-url-alert": "Please complete your OpenSign public profile setup. Once done, youll be able to make templates public. This process only needs to be completed once.",
@@ -512,7 +515,7 @@
"error": "Error",
"invalid-document": "Error: invalid document!",
"document-not-found": "Document not Found!",
"document-signed-alert-1": "You have successfully signed the document. You can download or print a copy of the partially signed document. A copy of the digitally signed document will be sent to the owner over email once it is signed by all signers.",
"document-signed-alert-1": "You have successfully signed the document. You can download or print a copy of the partially signed document. A copy of the digitally signed document & completion certificate will be sent to the owner & all signers over email once it is signed by all signers.",
"encrypted-pdf-not-support": "Currently encrypted pdf files are not supported.",
"document-signed-alert-4": " Congratulations! 🎉 This document has been successfully signed by all participants!",
"document-signed-alert-5": "This document has been signed by all Signers.",
@@ -652,6 +655,7 @@
"angular-npm-mssg-1": "To integrate OpenSign into your Angular project, simply run the following command:",
"quota-mail-info-head":"Monthly request signatures email limit",
"quota-mail-info": "You can send upto 15 signature request emails every month. Upgrade now to send unlimited signing requests directly.",
"quota-mail-reset": "Your signature request email credits will be reset on",
"quota-mail": "You've reached your limit of 15 signature request emails for this month. Upgrade now to continue sending emails directly.",
"quota-mail-tip":"Tip: You can still sign <1>unlimited documents</1> by manually sharing the signing request links.",
"quota-mail-head":"Quota Reached",
@@ -674,14 +678,14 @@
"document-logs":"Document logs",
"server-down": "Unable to connect to the OpenSign server. If you are self-hosting OpenSign, please ensure that all the steps in the documentation have been followed correctly. If you're running OpenSign locally, you might be accessing it through an incorrect port number.",
"admin-exists": "Admin already exists. Please login to the application using admin credentials in order to manage users.",
"enable-tour":"Enable Tour",
"istourenabled-help":{
"enable-tour":"Enable Tour",
"istourenabled-help":{
"p1":"This will enable a guided tour for signers, providing instructions during the signing process.",
"p2":"This will disable the guided tour, ensuring a faster, uninterrupted signing experience.",
"p3":"Tip: If your signers are familiar with OpenSign and frequently use it, you may want to disable the guided tour for a more seamless experience. Choose the option that best fits your document signing needs."
},
"form-title-1":"Document Flow Setting",
"form-title-2":"Security Settings",
"form-title-1":"Document Flow Setting",
"form-title-2":"Security Settings",
"public-tour-message":"The template needs to be public before you can generate a shareable link.",
"add-user-template": "You need to add a role before you can add fields for it.",
"pdf-uncompatible": "This pdf is not compatible with opensign, please contact <support@opensignlabs.com>",
@@ -697,5 +701,17 @@
"l2": "Enter your name and choose from various font styles to create a signature.",
"l3": "Upload an existing digital version of your signature.",
"l4": "Use a pre-set signature style saved under 'My Signature' in your settings."
}
},
"notify-on-signatures": "Notify on signatures",
"notify-on-signatures-help": {
"p1": "The document creator will receive an email notification whenever a signer signs the document.",
"note": "Note: Notification email will not be sent for the last signer, as the document creator will receive a completion email instead."
},
"expiry-date": "Expiry Date",
"expiry-date-updated": "The expiry date has been successfully extended to {{newexpirydate}}",
"expiry-date-error": "Please provide a new expiry date that is later than the current one",
"delete-page":"Delete page",
"delete-alert":"Can not delete single page",
"delete-alert-2":"Are you sure want to delete this page ?",
"delete-note":"Note - once delete this page you can not undo"
}
@@ -1,5 +1,5 @@
{
"header-news": "Nueva funcionalidad: ¡Los perfiles públicos ahora están disponibles! Crea y comparte plantillas públicas fácilmente",
"header-news": "Nueva característica: los usuarios del plan Teams ahora pueden integrar sus propios depósitos de AWS S3 para el almacenamiento de archivos",
"header-news-btn": "Configurar ahora",
"create-account": "Crear cuenta",
"login": "Iniciar sesión",
@@ -134,7 +134,8 @@
"option": "Opción",
"Embed": "Incrustar",
"Copy TemplateId": "Copiar ID de plantilla",
"Copy Public URL": "Copiar URL pública"
"Copy Public URL": "Copiar URL pública",
"extend-expiry-date": "Ampliar fecha de caducidad"
},
"report-heading": {
"Sr.No": "Nº",
@@ -155,7 +156,8 @@
"Status": "Estado",
"created-date": "Fecha de creación",
"Type": "Tipo",
"Logs": "Registros"
"Logs": "Registros",
"Expiry-date": "Date d'expiration"
},
"report-help": {
"Draft Documents": "Estos son documentos que has iniciado pero no has finalizado para su envío.",
@@ -266,7 +268,8 @@
"make-template-public-alert": "¿En definitiva quieres convertir esta plantilla en pública?",
"make-template-private-alert": "¿En definitiva quieres convertir esta plantilla en privada? Esto lo removerá de tu perfil público.",
"public-role": "Rol público",
"public-url": "Se requiere perfil público",
"public-url": "Perfil publico",
"embed-template": "Insertar plantilla",
"public-url-copy": "Aquí está tu URL pública: ",
"public-url-copy-mssg": "Cópiala o compártela con el firmante y podrás ver todas tus plantillas públicas.",
"add-public-url-alert": "Por favor, completa la configuración de tu perfil público OpenSign. Una vez terminado, podrás crear plantillas públicas. Este proceso solo necesita ser completado una vez.",
@@ -512,7 +515,7 @@
"error": "Error",
"invalid-document": "Error: ¡Documento inválido!",
"document-not-found": "¡Documento no encontrado!",
"document-signed-alert-1": "Has firmado el documento exitosamente. Puedes descargar o imprimir una copia del documento parcialmente firmado. Una copia del documento firmado digitalmente será enviada al propietario mediante correo electrónico una vez haya sido firmado por todos los firmantes.",
"document-signed-alert-1": "Ha firmado exitosamente el documento. Puede descargar o imprimir una copia del documento parcialmente firmado. Se enviará una copia del documento firmado digitalmente y del certificado de finalización al propietario y a todos los firmantes por correo electrónico una vez que todos los firmantes lo hayan firmado.",
"encrypted-pdf-not-support": "Los archivos PDF encriptados no están soportados actualmente.",
"document-signed-alert-4": " ¡Felicitaciones! 🎉 ¡Este documento ha sido firmado exitosamente por todos los participantes!",
"document-signed-alert-5": "Este documento ha sido firmado por todos los firmantes.",
@@ -652,6 +655,7 @@
"angular-npm-mssg-1": "Para integrar OpenSign a tu proyecto Angular, simplemente ejecuta los siguientes comandos:",
"quota-mail-info-head": "Límite mensual de solicitudes de firma por correo",
"quota-mail-info": "Puedes enviar hasta 15 solicitudes de firma por correo cada mes. Mejora ahora para enviar solicitudes de firma sin límite.",
"quota-mail-reset": "Los créditos de tu correo electrónico de solicitud de firma se restablecerán el",
"quota-mail": "Has alcanzado tu límite de 15 solicitudes de firma por correo para este mes. Mejora ahora para continuar enviando correos.",
"quota-mail-tip": "Consejo: Todavía puedes solciitar la firma de <1>documentos ilimitados</1> compartiendo manualmente los enlaces de solicitud de firma.",
"quota-mail-head": "Cuota alcanzada",
@@ -678,24 +682,36 @@
"add-user-template": "Es necesario agregar un rol antes de poder agregarle campos.",
"pdf-uncompatible": "Este pdf no es compatible con opensign, comuníquese con <support@opensignlabs.com>",
"enable-tour": "Habilitar recorrido",
"istourenabled-help":{
"p1": "Esto permitirá realizar una visita guiada para los firmantes, brindando instrucciones durante el proceso de firma.",
"p2": "Esto desactivará la visita guiada, lo que garantizará una experiencia de firma más rápida e ininterrumpida.",
"p3": "Consejo: si sus firmantes están familiarizados con OpenSign y lo utilizan con frecuencia, es posible que desee desactivar la visita guiada para disfrutar de una experiencia más fluida. Elija la opción que mejor se adapte a sus necesidades de firma de documentos"
"istourenabled-help":{
"p1": "Esto permitirá realizar una visita guiada para los firmantes, brindando instrucciones durante el proceso de firma.",
"p2": "Esto desactivará la visita guiada, lo que garantizará una experiencia de firma más rápida e ininterrumpida.",
"p3": "Consejo: si sus firmantes están familiarizados con OpenSign y lo utilizan con frecuencia, es posible que desee desactivar la visita guiada para disfrutar de una experiencia más fluida. Elija la opción que mejor se adapte a sus necesidades de firma de documentos"
},
"form-title-1": "Configuración del flujo de documentos",
"form-title-2": "Configuración de seguridad",
"text-field-tour":"Los campos de tipo 'Texto' deben completarse con anticipación antes de enviar el documento. Si necesita que los firmantes proporcionen información, utilice el campo 'Entrada de texto'",
"attach-signer-tour" :"Debe adjuntar un firmante a cada función. Puede hacerlo haciendo clic en este icono. Una vez que seleccione un Firmante, se adjuntará a todos los campos asociados con ese rol que aparecen en el mismo color.",
"allowed-signature-types": "Tipos de firma permitidos",
"at-least-one-signature-type": "Se debe habilitar al menos un tipo de firma.",
"expect-default-one-signature-type": "Habilite un tipo de firma adicional aparte del predeterminado.",
"add-signer-note": "Nota - Este contacto se agregará a su lista de contactos",
"allowed-signature-types-help": {
"form-title-1": "Configuración del flujo de documentos",
"form-title-2": "Configuración de seguridad",
"text-field-tour":"Los campos de tipo 'Texto' deben completarse con anticipación antes de enviar el documento. Si necesita que los firmantes proporcionen información, utilice el campo 'Entrada de texto'",
"attach-signer-tour" :"Debe adjuntar un firmante a cada función. Puede hacerlo haciendo clic en este icono. Una vez que seleccione un Firmante, se adjuntará a todos los campos asociados con ese rol que aparecen en el mismo color.",
"allowed-signature-types": "Tipos de firma permitidos",
"at-least-one-signature-type": "Se debe habilitar al menos un tipo de firma.",
"expect-default-one-signature-type": "Habilite un tipo de firma adicional aparte del predeterminado.",
"add-signer-note": "Nota - Este contacto se agregará a su lista de contactos",
"allowed-signature-types-help": {
"p1": "Esta preferencia de firma controla qué opciones de firma están disponibles para sus firmantes. Solo los tipos de firma que seleccione aparecerán en el widget de firma cuando se firme un documento. Por ejemplo, si elige sólo la opción 'Draw' en las preferencias, su firmante verá sólo la opción 'Draw' en el widget de firma, mientras que las otras tres opciones no estarán disponibles.",
"l1": "Cree una firma manuscrita directamente en la pantalla usando su dedo o un lápiz.",
"l2": "Ingrese su nombre y elija entre varios estilos de fuente para crear una firma.",
"l3": "Cargue una versión digital existente de su firma.",
"l4": "Utilice un estilo de firma preestablecido guardado en 'Mi firma' en su configuración."
}
},
"notify-on-signatures": "Notificar sobre firmas",
"notify-on-signatures-help": {
"p1": "El creador del documento recibirá una notificación por correo electrónico cada vez que un firmante firme el documento.",
"note": "Nota: No se enviará un correo electrónico de notificación al último firmante, ya que el creador del documento recibirá un correo electrónico de finalización."
},
"expiry-date": "Date d'expiration",
"expiry-date-updated": "La fecha de vencimiento se ha extendido con éxito hasta el {{newexpirydate}}",
"expiry-date-error": "Proporcione una nueva fecha de vencimiento que sea posterior a la actual",
"delete-page": "eliminar página",
"delete-alert": "No se puede eliminar una sola página",
"delete-alert-2": "¿Está seguro de que desea eliminar esta página?",
"delete-note": "Nota: una vez que elimines esta página, no podrás deshacerla"
}
@@ -1,5 +1,5 @@
{
"header-news": "Nouvelle fonctionnalité : les profils publics sont désormais en ligne ! Créez et partagez facilement des modèles publics",
"header-news": "Nouvelle fonctionnalité : les utilisateurs du forfait Teams peuvent désormais intégrer leurs propres compartiments AWS S3 pour le stockage de fichiers",
"header-news-btn": "Configurer maintenant",
"create-account": "Créer un compte",
"login": "Se Connecter",
@@ -138,7 +138,8 @@
"Status": "Statut",
"created-date": "Date de création",
"Type": "Saisir",
"Logs": "Journaux"
"Logs": "Journaux",
"Expiry-date": "Fecha de caducidad"
},
"btnLabel": {
"sign": "Signer",
@@ -154,7 +155,8 @@
"option": "Option",
"Embed": "Intégrer",
"Copy TemplateId": "Copier l'ID du modèle",
"Copy Public URL":"Copier l'URL publique"
"Copy Public URL":"Copier l'URL publique",
"extend-expiry-date": "Prolonger la date d'expiration"
},
"report-help": {
"Draft Documents": "Il s'agit de documents que vous avez commencés mais que vous n'avez pas finalisés pour envoi.",
@@ -265,7 +267,8 @@
"make-template-public-alert": "Êtes-vous sûr de vouloir rendre ce modèle public ?",
"make-template-private-alert": "Êtes-vous sûr de vouloir rendre ce modèle privé ? Cela le supprimera de votre profil public.",
"public-role": "Rôle public",
"public-url": "Profil public requis",
"public-url": "Profil public",
"embed-template": "Intégrer le modèle",
"public-url-copy": "Voici votre URL publique : ",
"public-url-copy-mssg": "copiez-la ou partagez-le avec le signataire et vous pouvoir voir tous vos paramètres publics modèles.",
"add-public-url-alert": "Veuillez terminer la configuration de votre profil public OpenSign. Une fois cela fait, vous pourrez rendre les modèles publics. Ce processus ne doit être effectué quune seule fois.",
@@ -511,7 +514,7 @@
"error": "Erreur",
"invalid-document": "Erreur : document invalide !",
"document-not-found": "Document introuvable !",
"document-signed-alert-1": "Vous avez signé le document avec succès. Vous pouvez télécharger ou imprimer une copie du document partiellement signé. Une copie du document signé numériquement sera envoyée au propriétaire par e-mail une fois qu'il sera signé par tous les signataires.",
"document-signed-alert-1": "Vous avez signé le document avec succès. Vous pouvez télécharger ou imprimer une copie du document partiellement signé. Une copie du document signé numériquement et du certificat d'achèvement sera envoyée au propriétaire et à tous les signataires par e-mail une fois qu'il aura été signé par tous les signataires.",
"encrypted-pdf-not-support": "Les fichiers PDF actuellement cryptés ne sont pas pris en charge. ",
"document-signed-alert-4": "Toutes nos félicitations! 🎉 Ce document a été signé avec succès par tous les participants !",
"document-signed-alert-5": "Ce document a été signé par tous les signataires.",
@@ -651,6 +654,7 @@
"angular-npm-mssg-1" :"Pour intégrer OpenSign dans votre projet Angular, exécutez simplement la commande suivante :",
"quota-mail-info-head":"Limite mensuelle d'e-mails de signatures de demandes",
"quota-mail-info": "Vous pouvez envoyer jusqu'à 15 e-mails de demande de signature chaque mois. Mettez à niveau maintenant pour envoyer directement des demandes de signature illimitées.",
"quota-mail-reset": "Les crédits de votre email de demande de signature seront réinitialisés le",
"quota-mail": "Vous avez atteint votre limite de 15 e-mails de demande de signature pour ce mois. Mettez à niveau maintenant pour continuer à envoyer des e-mails directement.",
"quota-mail-tip-tip":"Astuce : Vous pouvez toujours signer un nombre <1>illimité de documents</1> en partageant manuellement les liens de demande de signature.",
"quota-mail-head":"Quota atteint",
@@ -696,5 +700,17 @@
"l2": "Entrez votre nom et choisissez parmi différents styles de police pour créer une signature.",
"l3": "Téléchargez une version numérique existante de votre signature.",
"l4": "Utilisez un style de signature prédéfini enregistré sous 'Ma signature' dans vos paramètres."
}
},
"notify-on-signatures": "Notifier les signatures",
"notify-on-signatures-help": {
"p1": "Le créateur du document recevra une notification par e-mail chaque fois qu'un signataire signera le document.",
"note": "Remarque : L'e-mail de notification ne sera pas envoyé pour le dernier signataire, car le créateur du document recevra à la place un e-mail de fin."
},
"expiry-date": "Fecha de caducidad",
"expiry-date-updated": "La fecha de vencimiento se ha extendido con éxito hasta el {{newexpirydate}}",
"expiry-date-error": "Veuillez fournir une nouvelle date d'expiration postérieure à la date actuelle",
"delete-page": "supprimer la page",
"delete-alert": "Impossible de supprimer une seule page",
"delete-alert-2": "Etes-vous sûr de vouloir supprimer cette page ?",
"delete-note": "Remarque : une fois cette page supprimée, vous ne pouvez plus l'annuler"
}
-5
View File
@@ -35,7 +35,6 @@ const GenerateToken = lazy(() => import("./pages/GenerateToken"));
const Webhook = lazy(() => import("./pages/Webhook"));
const AddAdmin = lazy(() => import("./pages/AddAdmin"));
const UpdateExistUserAdmin = lazy(() => import("./pages/UpdateExistUserAdmin"));
const DraftTemplate = lazy(() => import("./pages/DraftTemplate"));
const Preferences = lazy(() => import("./pages/Preferences"));
pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/legacy/build/pdf.worker.min.mjs`;
@@ -206,10 +205,6 @@ function App() {
/>
</Route>
<Route path="/sso" element={<SSOVerify />} />
<Route
path="/drafttemplate/:jwttoken"
element={<DraftTemplate />}
/>
<Route path="*" element={<PageNotFound />} />
</Routes>
</BrowserRouter>
@@ -274,6 +274,7 @@ const AddSigner = (props) => {
onChange={(e) => setPhone(e.target.value)}
disabled={addYourself}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
placeholder={t("phone-optional")}
/>
</div>
<div className="mt-4 flex gap-x-2 justify-start">
+2 -2
View File
@@ -431,13 +431,13 @@ const AddUser = (props) => {
{t("phone")}
{/* <span className="text-[red] text-[13px]"> *</span> */}
</label>
{/* added hint for phone number being optional */}
<input
type="text"
name="phone"
placeholder="optional"
placeholder={t("phone-optional")}
value={formdata.phone}
onChange={(e) => handleChange(e)}
// required
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
/>
</div>
+24 -3
View File
@@ -6,6 +6,7 @@ import Parse from "parse";
import { useWindowSize } from "../hook/useWindowSize";
import {
checkIsSubscribed,
formatDate,
getAppLogo,
openInNewTab,
saveLanguageInLocal
@@ -33,6 +34,7 @@ const Header = ({ showSidebar, setIsMenu }) => {
);
const [emailUsed, setEmailUsed] = useState(0);
const [isModal, setIsModal] = useState(false);
const [emailResetDate, setEmailResetDate] = useState("");
const [showNotification, setShowNotification] = useState(
(!dismissedVersion || dismissedVersion !== current_notification_version) &&
true
@@ -64,6 +66,14 @@ const Header = ({ showSidebar, setIsMenu }) => {
const extUser = await Parse.Cloud.run("getUserDetails");
const MonthlyFreeEmails = extUser?.get("MonthlyFreeEmails") || 0;
setEmailUsed(MonthlyFreeEmails);
if (extUser) {
const _extUser = JSON.parse(JSON.stringify(extUser));
if (_extUser?.LastEmailCountReset?.iso) {
const resetDate = formatDate(_extUser?.LastEmailCountReset?.iso);
setEmailResetDate(resetDate);
}
localStorage.setItem("Extand_Class", JSON.stringify([_extUser]));
}
} catch (err) {
console.log("err in while fetching monthlyfreeEmails", err);
}
@@ -140,15 +150,22 @@ const Header = ({ showSidebar, setIsMenu }) => {
);
setShowNotification(false);
};
const handleSetupNowNavigation = () => {
const url = isStaging
? "https://staging-console.opensignlabs.com/fileadapter"
: "https://console.opensignlabs.com/fileadapter";
openInNewTab(url);
};
return (
<div>
{isEnableSubscription && showNotification && (
<div className="flex justify-between items-center shadow py-1 bg-[#CAE4FA] p-1">
<div className="flex justify-between items-center shadow py-1 bg-[#CAE4FA] p-1">
<div className="text-center text-[14px] ml-auto">
{t("header-news")}
<span
className="cursor-pointer font-medium underline text-blue-800"
onClick={() => navigate("/profile")}
onClick={() => handleSetupNowNavigation()}
>
{" " + t("header-news-btn") + "."}
</span>
@@ -291,7 +308,11 @@ const Header = ({ showSidebar, setIsMenu }) => {
</div>
</div>
<ModalUi isOpen={isModal}>
<QuotaCard isPaidInfo={true} handlClose={handleMailUsed} />
<QuotaCard
isPaidInfo={true}
handlClose={handleMailUsed}
emailResetDate={emailResetDate}
/>
</ModalUi>
</div>
);
+5 -3
View File
@@ -6,9 +6,9 @@ function RotateAlert(props) {
const { t } = useTranslation();
return (
<ModalUi
isOpen={props.isRotate}
isOpen={props.showRotateAlert}
title={t("Rotation-alert")}
handleClose={() => props.setIsRotate({ status: false, degree: 0 })}
handleClose={() => props.setShowRotateAlert({ status: false, degree: 0 })}
>
{" "}
<div className="p-[20px] h-full">
@@ -16,7 +16,9 @@ function RotateAlert(props) {
<div className="h-[1px] w-full my-[15px] bg-[#9f9f9f]"></div>
<div className="flex gap-1">
<button
onClick={() => props.setIsRotate({ status: false, degree: 0 })}
onClick={() =>
props.setShowRotateAlert({ status: false, degree: 0 })
}
type="button"
className="op-btn op-btn-ghost shadow-md"
>
+141 -73
View File
@@ -17,7 +17,11 @@ const EditTemplate = ({ template, onSuccess, jwttoken }) => {
IsEnableOTP: template?.IsEnableOTP ? `${template?.IsEnableOTP}` : "false",
IsTourEnabled: template?.IsTourEnabled
? `${template?.IsTourEnabled}`
: "false"
: "false",
NotifyOnSignatures:
template?.NotifyOnSignatures !== undefined
? template?.NotifyOnSignatures
: false
});
const [isSubscribe, setIsSubscribe] = useState(false);
useEffect(() => {
@@ -64,6 +68,11 @@ const EditTemplate = ({ template, onSuccess, jwttoken }) => {
AutomaticReminders: !formData.AutomaticReminders
}));
};
// `handleNotifySignChange` is trigger when user change radio of notify on signatures
const handleNotifySignChange = (value) => {
setFormData((obj) => ({ ...obj, NotifyOnSignatures: value }));
};
return (
<div className="max-h-[300px] md:max-h-[400px] overflow-y-scroll p-[10px]">
<div className="text-base-content">
@@ -120,27 +129,29 @@ const EditTemplate = ({ template, onSuccess, jwttoken }) => {
</div>
<div className="mb-[0.35rem]">
<label className="text-[13px]">{t("send-in-order")}</label>
<div className="flex items-center gap-[8px] ml-[8px] mb-[5px]">
<input
type="radio"
value={"true"}
className="op-radio op-radio-xs"
name="SendinOrder"
checked={formData.SendinOrder === "true"}
onChange={handleStrInput}
/>
<div className="text-[12px]">{t("yes")}</div>
</div>
<div className="flex items-center gap-[8px] ml-[8px] mb-[5px]">
<input
type="radio"
value={"false"}
name="SendinOrder"
className="op-radio op-radio-xs"
checked={formData.SendinOrder === "false"}
onChange={handleStrInput}
/>
<div className="text-[12px]">{t("no")}</div>
<div className="flex flex-col md:flex-row md:gap-4">
<div className="flex items-center gap-[8px] ml-[8px] mb-[5px]">
<input
type="radio"
value={"true"}
className="op-radio op-radio-xs"
name="SendinOrder"
checked={formData.SendinOrder === "true"}
onChange={handleStrInput}
/>
<div className="text-[12px]">{t("yes")}</div>
</div>
<div className="flex items-center gap-[8px] ml-[8px] mb-[5px]">
<input
type="radio"
value={"false"}
name="SendinOrder"
className="op-radio op-radio-xs"
checked={formData.SendinOrder === "false"}
onChange={handleStrInput}
/>
<div className="text-[12px]">{t("no")}</div>
</div>
</div>
</div>
{isEnableSubscription && (
@@ -156,7 +167,7 @@ const EditTemplate = ({ template, onSuccess, jwttoken }) => {
<label
className={`${
isSubscribe
? "cursor-pointer "
? "cursor-pointer"
: "pointer-events-none opacity-50"
} relative block items-center mb-0`}
>
@@ -221,39 +232,40 @@ const EditTemplate = ({ template, onSuccess, jwttoken }) => {
</div>
</Tooltip>
</label>
<div
className={`${
isSubscribe ? "" : "pointer-events-none opacity-50"
} flex items-center gap-2 ml-2 mb-1 `}
>
<input
type="radio"
value={"true"}
className="op-radio op-radio-xs"
name="IsEnableOTP"
checked={formData.IsEnableOTP === "true"}
onChange={handleStrInput}
/>
<div className="text-center">{t("yes")}</div>
</div>
<div
className={`${
isSubscribe ? "" : "pointer-events-none opacity-50"
} flex items-center gap-2 ml-2 mb-1 `}
>
<input
type="radio"
value={"false"}
name="IsEnableOTP"
className="op-radio op-radio-xs"
checked={formData.IsEnableOTP === "false"}
onChange={handleStrInput}
/>
<div className="text-center">{t("no")}</div>
<div className="flex flex-col md:flex-row md:gap-4">
<div
className={`${
isSubscribe ? "" : "pointer-events-none opacity-50"
} flex items-center gap-2 ml-2 mb-1`}
>
<input
type="radio"
value={"true"}
className="op-radio op-radio-xs"
name="IsEnableOTP"
checked={formData.IsEnableOTP === "true"}
onChange={handleStrInput}
/>
<div className="text-center">{t("yes")}</div>
</div>
<div
className={`${
isSubscribe ? "" : "pointer-events-none opacity-50"
} flex items-center gap-2 ml-2 mb-1 `}
>
<input
type="radio"
value={"false"}
name="IsEnableOTP"
className="op-radio op-radio-xs"
checked={formData.IsEnableOTP === "false"}
onChange={handleStrInput}
/>
<div className="text-center">{t("no")}</div>
</div>
</div>
</div>
)}
<div className="text-xs mt-2">
<label className="block">
<span>
@@ -283,27 +295,83 @@ const EditTemplate = ({ template, onSuccess, jwttoken }) => {
</div>
</Tooltip>
</label>
<div className={` flex items-center gap-2 ml-2 mb-1 `}>
<input
type="radio"
value={"true"}
className="op-radio op-radio-xs"
name="IsTourEnabled"
checked={formData.IsTourEnabled === "true"}
onChange={handleStrInput}
/>
<div className="text-center">{t("yes")}</div>
<div className="flex flex-col md:flex-row md:gap-4">
<div className="flex items-center gap-2 ml-2 mb-1">
<input
type="radio"
value={"true"}
className="op-radio op-radio-xs"
name="IsTourEnabled"
checked={formData.IsTourEnabled === "true"}
onChange={handleStrInput}
/>
<div className="text-center">{t("yes")}</div>
</div>
<div className="flex items-center gap-2 ml-2 mb-1">
<input
type="radio"
value={"false"}
name="IsTourEnabled"
className="op-radio op-radio-xs"
checked={formData.IsTourEnabled === "false"}
onChange={handleStrInput}
/>
<div className="text-center">{t("no")}</div>
</div>
</div>
<div className={` flex items-center gap-2 ml-2 mb-1 `}>
<input
type="radio"
value={"false"}
name="IsTourEnabled"
className="op-radio op-radio-xs"
checked={formData.IsTourEnabled === "false"}
onChange={handleStrInput}
/>
<div className="text-center">{t("no")}</div>
</div>
<div className="text-xs mt-3">
<label
className={`${
isSubscribe || !isEnableSubscription ? "" : "text-gray-300"
} block`}
>
{t("notify-on-signatures")}
<a data-tooltip-id="nos-tooltip" className="ml-1">
<sup>
<i className="fa-light fa-question rounded-full border-[#33bbff] text-[#33bbff] text-[13px] border-[1px] py-[1.5px] px-[4px]"></i>
</sup>
</a>
{!isSubscribe && isEnableSubscription && <Upgrade />}
<Tooltip id="nos-tooltip" className="z-[999]">
<div className="max-w-[200px] md:max-w-[450px] text-[11px]">
<p className="font-bold">{t("notify-on-signatures")}</p>
<p>{t("notify-on-signatures-help.p1")}</p>
<p>{t("notify-on-signatures-help.note")}</p>
</div>
</Tooltip>
</label>
<div className="flex flex-col md:flex-row md:gap-4">
<div
className={`${
isSubscribe || !isEnableSubscription
? ""
: "pointer-events-none opacity-50"
} flex items-center gap-2 ml-2 mb-1`}
>
<input
className="mr-[2px] op-radio op-radio-xs"
type="radio"
onChange={() => handleNotifySignChange(true)}
checked={formData.NotifyOnSignatures === true}
/>
<div className="text-center">{t("yes")}</div>
</div>
<div
className={`${
isSubscribe || !isEnableSubscription
? ""
: "pointer-events-none opacity-50"
} flex items-center gap-2 ml-2 mb-1`}
>
<input
className="mr-[2px] op-radio op-radio-xs"
type="radio"
onChange={() => handleNotifySignChange(false)}
checked={formData.NotifyOnSignatures === false}
/>
<div className="text-center">{t("no")}</div>
</div>
</div>
</div>
<div className="mt-[1rem] flex justify-start">
+10 -12
View File
@@ -18,17 +18,16 @@ function EmbedTab(props) {
{
id: 0,
title: "Installation",
codeString: `
npm install @opensign/react`
codeString: `npm install @opensign/react`
},
{
id: 1,
title: "Usage",
codeString: `
import React from "react";
import React from "react";
import Opensign from "@opensign/react";
export function App() {
export default function App() {
return (
<div className="app">
<Opensign
@@ -40,7 +39,6 @@ export function App() {
);
}
`
}
];
@@ -49,15 +47,14 @@ export function App() {
{
id: 0,
title: "Installation",
codeString: `
npm install @opensign/angular`
codeString: `npm install @opensign/angular`
},
{
id: 1,
title: "Usage",
codeString: `
import { Component } from '@angular/core';
import {OpensignComponent} from "@opensign/angular"
import { OpensignComponent } from "@opensign/angular"
@Component({
selector:'app-root',
@@ -68,17 +65,15 @@ import {OpensignComponent} from "@opensign/angular"
(onLoadError)="handleError($event)"
></opensign>\`,
})
export class AppComponent{
export class AppComponent {
handleLoad() {
console.log("success");
}
handleError(error: string) {
console.log(error);
}
}
`
}
];
@@ -98,6 +93,9 @@ export class AppComponent{
return (
<div className="mt-4 border-t-[1px]">
<h3 className="text-base-content font-bold text-lg pt-[15px] pb-[5px]">
{t("embed-template")}
</h3>
<div className="flex justify-center items-center mt-2">
<div role="tablist" className="op-tabs op-tabs-bordered">
{tabName.map((tabData, ind) => (
@@ -107,7 +105,7 @@ export class AppComponent{
role="tab"
className={`${
activeTab === ind ? "op-tab-active" : ""
} op-tab flex items-center`}
} op-tab flex items-center pb-10 md:pb-0`}
>
<i
className={`${tabData.icon}`}
+59 -1
View File
@@ -1,8 +1,10 @@
import React, { useState } from "react";
import PrevNext from "./PrevNext";
import {
deletePdfPage,
handleDownloadCertificate,
handleDownloadPdf,
handleRemoveWidgets,
handleToPrint
} from "../../constant/Utils";
import "../../styles/signature.css";
@@ -41,7 +43,13 @@ function Header({
isDisableRotate,
templateId,
setIsDownloadModal,
disabledBackBtn
disabledBackBtn,
setPdfArrayBuffer,
setPdfBase64Url,
pdfArrayBuffer,
setIsUploadPdf,
setSignerPos,
userId
}) {
const { t } = useTranslation();
const filterPrefill =
@@ -49,6 +57,7 @@ function Header({
const isMobile = window.innerWidth < 767;
const [isDownloading, setIsDownloading] = useState("");
const isGuestSigner = localStorage.getItem("isGuestSigner");
const [isDeletePage, setIsDeletePage] = useState(false);
const enabledBackBtn = disabledBackBtn === true ? false : true;
//function for show decline alert
@@ -56,6 +65,18 @@ function Header({
const currentDecline = { currnt: "Sure", isDeclined: true };
setIsDecline(currentDecline);
};
const handleDetelePage = async () => {
setIsUploadPdf(true);
const pdfupdatedData = await deletePdfPage(pdfArrayBuffer, pageNumber);
if (pdfupdatedData?.totalPages === 1) {
alert(t("delete-alert"));
} else {
setPdfBase64Url(pdfupdatedData.base64);
setPdfArrayBuffer(pdfupdatedData.arrayBuffer);
setIsDeletePage(false);
handleRemoveWidgets(setSignerPos, signerPos, pageNumber, userId);
}
};
return (
<div className="flex py-[5px]">
{isMobile && isShowHeader ? (
@@ -255,6 +276,17 @@ function Header({
</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>
</>
)}
@@ -553,6 +585,32 @@ function Header({
<p>{t("generate-certificate-alert")}</p>
</div>
</ModalUi>
<ModalUi
isOpen={isDeletePage}
title={t("validation-alert")}
handleClose={() => setIsDeletePage(false)}
>
<div className="h-[100%] p-[20px]">
<p>{t("delete-alert-2")}</p>
<p className="pt-3 font-medium">{t("delete-note")}</p>
{/* <p>{t("deletepage-alert")}</p> */}
<div className="h-[1px] bg-[#9f9f9f] w-full my-[15px]"></div>
<button
onClick={() => handleDetelePage()}
type="button"
className="op-btn op-btn-primary"
>
{t("yes")}
</button>
<button
onClick={() => setIsDeletePage(false)}
type="button"
className="op-btn op-btn-ghost"
>
{t("no")}
</button>
</div>
</ModalUi>
</div>
);
}
+101 -33
View File
@@ -1,47 +1,115 @@
import React from "react";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { deletePdfPage, handleRemoveWidgets } from "../../constant/Utils";
import ModalUi from "../../primitives/ModalUi";
function PdfZoom(props) {
const { t } = useTranslation();
const [isDeletePage, setIsDeletePage] = useState(false);
const handleDetelePage = async () => {
props.setIsUploadPdf(true);
try {
const pdfupdatedData = await deletePdfPage(
props.pdfArrayBuffer,
props.pageNumber
);
if (pdfupdatedData?.totalPages === 1) {
alert(t("delete-alert"));
} else {
props.setPdfBase64Url(pdfupdatedData.base64);
props.setPdfArrayBuffer(pdfupdatedData.arrayBuffer);
setIsDeletePage(false);
handleRemoveWidgets(
props.setSignerPos,
props.signerPos,
props.pageNumber,
props.userId
);
}
} catch (e) {
console.log("error in delete pdf page", e);
}
};
return (
<span className="hidden md:flex flex-col gap-1 text-center md:w-[5%] mt-[43px] 2xl:mt-[63px]">
<span
className="bg-gray-50 px-[4px] 2xl:px-[15px] 2xl:py-[10px] cursor-pointer"
onClick={() => props.clickOnZoomIn()}
title={t("zoom-in")}
>
<i className="fa-light fa-magnifying-glass-plus text-gray-500 2xl:text-[30px]"></i>
</span>
{!props?.isDisableRotate && (
<>
<>
<span className="hidden md:flex flex-col gap-1 text-center md:w-[5%] mt-[43px] 2xl:mt-[63px]">
<span
className="bg-gray-50 px-[4px] 2xl:px-[15px] 2xl:py-[10px] cursor-pointer"
onClick={() => props.clickOnZoomIn()}
title={t("zoom-in")}
>
<i className="fa-light fa-magnifying-glass-plus text-gray-500 2xl:text-[30px]"></i>
</span>
{!props.isDisableDelete && (
<span
className="bg-gray-50 px-[4px] 2xl:px-[15px] 2xl:py-[10px] cursor-pointer"
onClick={() => props.handleRotationFun(90)}
title={t("rotate-right")}
onClick={() => setIsDeletePage(true)}
title={t("delete-page")}
>
<i className="fa-light fa-rotate-right text-gray-500 2xl:text-[30px]"></i>
<i className="fa-light fa-trash text-gray-500 2xl:text-[30px]"></i>
</span>
<span
className="bg-gray-50 px-[4px] 2xl:px-[15px] 2xl:py-[10px] cursor-pointer"
title={t("rotate-left")}
onClick={() => props.handleRotationFun(-90)}
>
<i className="fa-light fa-rotate-left text-gray-500 2xl:text-[30px]"></i>
</span>
</>
)}
<span
className="bg-gray-50 px-[4px] 2xl:px-[15px] 2xl:py-[10px]"
onClick={() => props.clickOnZoomOut()}
style={{
cursor: props.zoomPercent > 0 ? "pointer" : "default"
}}
title={t("zoom-out")}
>
<i className="fa-light fa-magnifying-glass-minus text-gray-500 2xl:text-[30px]"></i>
)}
{!props?.isDisableRotate && (
<>
<span
className="bg-gray-50 px-[4px] 2xl:px-[15px] 2xl:py-[10px] cursor-pointer"
onClick={() => props.handleRotationFun(90)}
title={t("rotate-right")}
>
<i className="fa-light fa-rotate-right text-gray-500 2xl:text-[30px]"></i>
</span>
<span
className="bg-gray-50 px-[4px] 2xl:px-[15px] 2xl:py-[10px] cursor-pointer"
title={t("rotate-left")}
onClick={() => props.handleRotationFun(-90)}
>
<i className="fa-light fa-rotate-left text-gray-500 2xl:text-[30px]"></i>
</span>
</>
)}
<span
className="bg-gray-50 px-[4px] 2xl:px-[15px] 2xl:py-[10px]"
onClick={() => props.clickOnZoomOut()}
style={{
cursor: props.zoomPercent > 0 ? "pointer" : "default"
}}
title={t("zoom-out")}
>
<i className="fa-light fa-magnifying-glass-minus text-gray-500 2xl:text-[30px]"></i>
</span>
</span>
</span>
<ModalUi
isOpen={isDeletePage}
title={t("validation-alert")}
handleClose={() => setIsDeletePage(false)}
>
<div className="h-[100%] p-[20px]">
<p>Are you sure want to delete this page ?</p>
<p className="pt-3 font-medium">
Note - once delete this page you can not undo
</p>
{/* <p>{t("deletepage-alert")}</p> */}
<div className="h-[1px] bg-[#9f9f9f] w-full my-[15px]"></div>
<button
onClick={() => handleDetelePage()}
type="button"
className="op-btn op-btn-primary"
>
{t("yes")}
</button>
<button
onClick={() => setIsDeletePage(false)}
type="button"
className="op-btn op-btn-ghost"
>
{t("no")}
</button>
</div>
</ModalUi>
</>
);
}
@@ -13,7 +13,7 @@ function RenderAllPdfPage({
signerPos,
signerObjectId,
containerWH,
pdfRotateBase64
pdfBase64Url
}) {
const { t } = useTranslation();
const [signPageNumber, setSignPageNumber] = useState([]);
@@ -67,7 +67,7 @@ function RenderAllPdfPage({
)
);
};
const pdfDataBase64 = `data:application/pdf;base64,${pdfRotateBase64}`;
const pdfDataBase64 = `data:application/pdf;base64,${pdfBase64Url}`;
return (
<div ref={pageContainer} className="hidden w-[20%] bg-base-100 md:block">
<div className="mx-2 pr-2 pt-2 pb-1 text-[15px] text-base-content font-semibold border-b-[1px] border-base-300">
@@ -80,7 +80,7 @@ function RenderAllPdfPage({
<Document
loading={t("loading-doc")}
onLoadSuccess={onDocumentLoad}
file={(pdfRotateBase64 && pdfDataBase64) || signPdfUrl}
file={(pdfBase64Url && pdfDataBase64) || signPdfUrl}
>
{Array.from(new Array(allPages), (el, index) => (
<div
@@ -61,7 +61,7 @@ function RenderPdf({
setIsSelectId,
ispublicTemplate,
handleUserDetails,
pdfRotateBase64,
pdfBase64Url,
fontSize,
setFontSize,
fontColor,
@@ -256,8 +256,7 @@ function RenderPdf({
);
}
};
const pdfDataBase64 = `data:application/pdf;base64,${pdfRotateBase64}`;
const pdfDataBase64 = `data:application/pdf;base64,${pdfBase64Url}`;
return (
<>
{successEmail && (
@@ -377,10 +376,11 @@ function RenderPdf({
return (
<React.Fragment key={ind}>
{data.pageNumber === pageNumber &&
data.pos.map((pos) => {
data.pos.map((pos, id) => {
return (
pos && (
<Placeholder
key={id}
pos={pos}
setIsPageCopy={setIsPageCopy}
setSignKey={setSignKey}
@@ -443,7 +443,7 @@ function RenderPdf({
}
}}
file={
(pdfRotateBase64 && pdfDataBase64) ||
(pdfBase64Url && pdfDataBase64) ||
pdfDetails[0]?.SignedUrl ||
pdfDetails[0].URL
}
@@ -640,9 +640,8 @@ function RenderPdf({
setSelectWidgetId("");
}
}}
// ref={pdfRef}
file={
(pdfRotateBase64 && pdfDataBase64) ||
(pdfBase64Url && pdfDataBase64) ||
pdfDetails[0]?.SignedUrl ||
pdfDetails[0].URL
}
@@ -7,10 +7,7 @@ const DROPBOX_SCRIPT_ID = "dropboxjs";
export default function DropboxChooser({ children, onSuccess, onCancel }) {
useDropScript(DROPBOX_SDK_URL, {
attrs: {
id: DROPBOX_SCRIPT_ID,
"data-app-key": DROPBOX_APP_KEY
}
attrs: { id: DROPBOX_SCRIPT_ID, "data-app-key": DROPBOX_APP_KEY }
});
const options = useMemo(
+70 -37
View File
@@ -705,6 +705,9 @@ export const createDocument = async (
const SignatureType = Doc?.SignatureType
? { SignatureType: Doc?.SignatureType }
: {};
const NotifyOnSignatures = Doc?.NotifyOnSignatures
? { NotifyOnSignatures: Doc?.NotifyOnSignatures }
: {};
const data = {
Name: Doc.Name,
URL: pdfUrl,
@@ -730,7 +733,8 @@ export const createDocument = async (
IsEnableOTP: Doc?.IsEnableOTP || false,
IsTourEnabled: Doc?.IsTourEnabled || false,
FileAdapterId: Doc?.FileAdapterId || "",
...SignatureType
...SignatureType,
...NotifyOnSignatures
};
try {
@@ -1453,7 +1457,6 @@ export const multiSignEmbed = async (widgets, pdfDoc, signyourself, scale) => {
if (position?.options?.values.length > 0) {
position?.options?.values.forEach((item, ind) => {
const checkboxRandomId = "checkbox" + randomId();
if (
position?.options?.response &&
position?.options?.response?.length > 0
@@ -1505,8 +1508,6 @@ export const multiSignEmbed = async (widgets, pdfDoc, signyourself, scale) => {
});
}
} else if (widgetTypeExist) {
const fontSize = position?.options?.fontSize || 12;
parseInt(fontSize);
let textContent;
if (position?.options?.response) {
textContent = position.options?.response;
@@ -1541,7 +1542,6 @@ export const multiSignEmbed = async (widgets, pdfDoc, signyourself, scale) => {
// Function to break text into lines based on when user go next line on press enter button
const breakTextIntoLines = (textContent, width) => {
const lines = [];
for (const word of textContent.split("\n")) {
const lineWidth = font.widthOfTextAtSize(`${word}`, fontSize);
//checking string length to container width
@@ -1583,7 +1583,6 @@ export const multiSignEmbed = async (widgets, pdfDoc, signyourself, scale) => {
y += 18; // Adjust the line height as needed
}
} else if (position.type === "dropdown") {
const fontsize = parseInt(position?.options?.fontSize) || 12;
const dropdownRandomId = "dropdown" + randomId();
const dropdown = form.createDropdown(dropdownRandomId);
dropdown.addOptions(position?.options?.values);
@@ -1598,10 +1597,10 @@ export const multiSignEmbed = async (widgets, pdfDoc, signyourself, scale) => {
// - `FontSize` is the size you want to set (e.g., 12)
// - `Tf` specifies the font and size
// - `0 g` sets the text color to black
const defaultAppearance = `/Helv ${fontsize} Tf 0 g`;
const defaultAppearance = `/Helv ${fontSize} Tf 0 g`;
// Set the default appearance for the dropdown field
dropdown.acroField.setDefaultAppearance(defaultAppearance);
dropdown.setFontSize(fontsize);
dropdown.setFontSize(fontSize);
const dropdownObj = {
x: xPos(position),
y: yPos(position),
@@ -2426,23 +2425,9 @@ export const getDefaultSignature = async (objectId) => {
};
//function to rotate pdf page
export async function rotatePdfPage(
url,
rotateDegree,
pageNumber,
pdfRotateBase64
) {
let file;
//condition to handle pdf base64 in arrayBuffer format
if (pdfRotateBase64) {
file = base64ToArrayBuffer(pdfRotateBase64);
} else {
//condition to handle pdf url in arrayBuffer format
file = await convertPdfArrayBuffer(url);
}
export async function rotatePdfPage(rotateDegree, pageNumber, pdfArrayBuffer) {
// Load the existing PDF
const pdfDoc = await PDFDocument.load(file);
const pdfDoc = await PDFDocument.load(pdfArrayBuffer);
// Get the page according to page number
const page = pdfDoc.getPage(pageNumber);
//get current page rotation angle
@@ -2508,21 +2493,35 @@ export const handleRemoveWidgets = (
setSignerPos,
signerPos,
pageNumber,
userId,
setIsRotate
) => {
const updatedSignerPos = signerPos.map((placeholderObj) => {
return {
...placeholderObj,
placeHolder: placeholderObj?.placeHolder?.filter(
(data) => data?.pageNumber !== pageNumber
)
};
});
setSignerPos(updatedSignerPos);
setIsRotate({
status: false,
degree: 0
});
const isSigners = signerPos.some((data) => data.signerPtr);
if (isSigners) {
const updatedSignerPos = signerPos.map((placeholderObj) => {
if (placeholderObj.Id === userId) {
return {
...placeholderObj,
placeHolder: placeholderObj?.placeHolder?.filter(
(data) => data?.pageNumber !== pageNumber
)
};
} else {
return placeholderObj;
}
});
setSignerPos(updatedSignerPos);
setIsRotate &&
setIsRotate({
status: false,
degree: 0
});
} else {
const updatedSignerPos = signerPos.filter(
(data) => data?.pageNumber !== pageNumber
);
setSignerPos(updatedSignerPos);
}
};
//function to show warning when user rotate page and there are some already widgets on that page
export const handleRotateWarning = (signerPos, pageNumber) => {
@@ -2597,3 +2596,37 @@ export async function handleSignatureType(tenantSignTypes, signatureType) {
}
return updatedSignatureType;
}
// `formatDate` is used to format date to dd-mmm-yyy
export const formatDate = (date) => {
// Create a Date object
const newDate = new Date(date);
// Format the date
const formattedDate = newDate.toLocaleDateString("en-GB", {
day: "2-digit",
month: "short",
year: "numeric"
});
const format = formattedDate.replaceAll(/ /g, "-");
return format;
};
export const deletePdfPage = async (pdfArrayBuffer, pageNumber) => {
// Load the existing PDF
const pdfDoc = await PDFDocument.load(pdfArrayBuffer);
// Get the total number of pages
const totalPages = pdfDoc.getPageCount();
// Ensure the page index is valid
if (totalPages > 1) {
//Remove the specified page
pdfDoc.removePage(pageNumber - 1);
// Save the modified PDF
const modifiedPdfBytes = await pdfDoc.saveAsBase64({
useObjectStreams: false
});
const arrayBuffer = base64ToArrayBuffer(modifiedPdfBytes);
return { arrayBuffer: arrayBuffer, base64: modifiedPdfBytes };
} else {
return { totalPages: 1 };
}
};
+24 -21
View File
@@ -2,28 +2,12 @@ import { isEnableSubscription } from "../constant/const";
export default function reportJson(id) {
// console.log("json ", json);
const head = ["Sr.No", "Title", "Note", "Folder", "File", "Owner", "Signers"];
const declineHead = [
"Sr.No",
"Title",
"Reason",
"Folder",
"File",
"Owner",
"Signers"
];
const iphead = [
"Sr.No",
"Title",
"Note",
"Folder",
"File",
"Status",
"Signers"
];
const contactbook = ["Sr.No", "Name", "Email", "Phone"];
const head = ["Title", "Note", "Folder", "File", "Owner", "Signers"];
const declineHead = ["Title", "Reason", "Folder", "File", "Owner", "Signers"];
const iphead = ["Title", "Note", "Folder", "File", "Signers"];
const contactbook = ["Name", "Email", "Phone"];
const dashboardReportHead = ["Title", "File", "Owner", "Signers"];
const templateReport = ["Sr.No", "Title", "File", "Owner", "Signers"];
const templateReport = ["Title", "File", "Owner", "Signers"];
const templateSubAction = isEnableSubscription
? [
{
@@ -228,6 +212,25 @@ export default function reportJson(id) {
btnIcon: "fa-light fa-trash",
redirectUrl: "",
action: "delete"
},
{
btnId: "3234",
hoverLabel: "option",
btnColor: "",
textColor: "black",
btnIcon: "fa-light fa-ellipsis-vertical fa-lg",
action: "option",
subaction: [
{
btnId: "2199",
btnLabel: "extend-expiry-date",
hoverLabel: "Extend expiry date",
btnColor: "op-btn-primary",
btnIcon: "fa-light fa-hourglass-end",
redirectUrl: "",
action: "extendexpiry"
}
]
}
],
helpMsg:
File diff suppressed because it is too large Load Diff
+90 -27
View File
@@ -68,7 +68,8 @@ const Forms = (props) => {
remindOnceInEvery: 5,
autoreminder: false,
IsEnableOTP: "false",
IsTourEnabled: "false"
IsTourEnabled: "false",
NotifyOnSignatures: ""
});
const [fileupload, setFileUpload] = useState("");
const [fileload, setfileload] = useState(false);
@@ -102,21 +103,25 @@ const Forms = (props) => {
if (isEnableSubscription) {
const subscribe = await checkIsSubscribed();
setIsSubscribe(subscribe.isValid);
if (subscribe.isValid && extUserData.NotifyOnSignatures !== undefined) {
setFormData((obj) => ({
...obj,
NotifyOnSignatures: extUserData.NotifyOnSignatures
}));
} else if (subscribe.isValid) {
setFormData((obj) => ({ ...obj, NotifyOnSignatures: true }));
}
} else {
setIsSubscribe(true);
setFormData((obj) => ({ ...obj, NotifyOnSignatures: true }));
}
};
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.onload = (e) => resolve(e.target.result);
reader.onerror = (e) => reject(e.target.error);
reader.readAsArrayBuffer(file);
});
}
@@ -525,7 +530,6 @@ const Forms = (props) => {
}
}
};
const dropboxCancel = async () => {};
const handleSubmit = async (e) => {
e.preventDefault();
e.stopPropagation();
@@ -555,8 +559,14 @@ const Forms = (props) => {
const IsEnableOTP =
formData?.IsEnableOTP === "false" ? false : true;
object.set("IsEnableOTP", IsEnableOTP);
if (isSubscribe && formData.NotifyOnSignatures !== undefined) {
object.set("NotifyOnSignatures", formData.NotifyOnSignatures);
}
} else {
object.set("IsEnableOTP", false);
if (formData.NotifyOnSignatures !== undefined) {
object.set("NotifyOnSignatures", formData.NotifyOnSignatures);
}
}
}
object.set("URL", fileupload);
@@ -639,7 +649,7 @@ const Forms = (props) => {
setIsReset(true);
setSigners([]);
setFolder({ ObjectId: "", Name: "" });
setFormData({
let obj = {
Name: "",
Description: "",
Note:
@@ -654,7 +664,8 @@ const Forms = (props) => {
autoreminder: false,
IsEnableOTP: "false",
IsTourEnabled: "true"
});
};
setFormData(obj);
setFileUpload("");
setpercentage(0);
setTimeout(() => setIsReset(false), 50);
@@ -772,6 +783,12 @@ const Forms = (props) => {
const handleAutoReminder = () => {
setFormData((prev) => ({ ...prev, autoreminder: !formData.autoreminder }));
};
// `handleNotifySignChange` is trigger when user change radio of notify on signatures
const handleNotifySignChange = (value) => {
setFormData((obj) => ({ ...obj, NotifyOnSignatures: value }));
};
return (
<div className="shadow-md rounded-box my-[2px] p-3 bg-base-100 text-base-content">
<Title title={props?.title} />
@@ -865,10 +882,7 @@ const Forms = (props) => {
</div>
</div>
{process.env.REACT_APP_DROPBOX_API_KEY && (
<DropboxChooser
onSuccess={dropboxSuccess}
onCancel={dropboxCancel}
/>
<DropboxChooser onSuccess={dropboxSuccess} />
)}
</div>
) : (
@@ -890,10 +904,7 @@ const Forms = (props) => {
required
/>
{process.env.REACT_APP_DROPBOX_API_KEY && (
<DropboxChooser
onSuccess={dropboxSuccess}
onCancel={dropboxCancel}
/>
<DropboxChooser onSuccess={dropboxSuccess} />
)}
</div>
)}
@@ -1215,7 +1226,7 @@ const Forms = (props) => {
<label
className={`${
isSubscribe
? "cursor-pointer "
? "cursor-pointer"
: "pointer-events-none opacity-50"
} relative block items-center mb-0 mt-1.5`}
>
@@ -1251,7 +1262,7 @@ const Forms = (props) => {
)}
<span className="font-[400] mt-2">{t("form-title-2")}</span>
{isEnableSubscription && (
<div className="text-xs mt-4">
<div className="text-xs mt-3">
<label className="block">
<span className={isSubscribe ? "" : " text-gray-300"}>
{t("isenable-otp")}{" "}
@@ -1262,7 +1273,7 @@ const Forms = (props) => {
<sup>
<i className="fa-light fa-question rounded-full border-[#33bbff] text-[#33bbff] text-[13px] border-[1px] py-[1.5px] px-[4px]"></i>
</sup>
</a>{" "}
</a>
{!isSubscribe && isEnableSubscription && (
<Upgrade />
)}
@@ -1340,7 +1351,7 @@ const Forms = (props) => {
<sup>
<i className="fa-light fa-question rounded-full border-[#33bbff] text-[#33bbff] text-[13px] border-[1px] py-[1.5px] px-[4px]"></i>
</sup>
</a>{" "}
</a>
</span>
<Tooltip id="istourenabled-tooltip" className="z-[999]">
<div className="max-w-[200px] md:max-w-[450px]">
@@ -1364,7 +1375,7 @@ const Forms = (props) => {
</Tooltip>
</label>
<div className="flex flex-col md:flex-row md:gap-4">
<div className={` flex items-center gap-2 ml-2 mb-1 `}>
<div className="flex items-center gap-2 ml-2 mb-1">
<input
type="radio"
value={"true"}
@@ -1375,7 +1386,7 @@ const Forms = (props) => {
/>
<div className="text-center">{t("yes")}</div>
</div>
<div className={` flex items-center gap-2 ml-2 mb-1 `}>
<div className="flex items-center gap-2 ml-2 mb-1">
<input
type="radio"
value={"false"}
@@ -1388,6 +1399,58 @@ const Forms = (props) => {
</div>
</div>
</div>
<div className="text-xs mt-3">
<label
className={`${
isSubscribe ? "" : "text-gray-300"
} block`}
>
{t("notify-on-signatures")}
<a data-tooltip-id="nos-tooltip" className="ml-1">
<sup>
<i className="fa-light fa-question rounded-full border-[#33bbff] text-[#33bbff] text-[13px] border-[1px] py-[1.5px] px-[4px]"></i>
</sup>
</a>
{!isSubscribe && isEnableSubscription && <Upgrade />}
<Tooltip id="nos-tooltip" className="z-[999]">
<div className="max-w-[200px] md:max-w-[450px] text-[11px]">
<p className="font-bold">
{t("notify-on-signatures")}
</p>
<p>{t("notify-on-signatures-help.p1")}</p>
<p>{t("notify-on-signatures-help.note")}</p>
</div>
</Tooltip>
</label>
<div className="flex flex-col md:flex-row md:gap-4">
<div
className={`${
isSubscribe ? "" : "pointer-events-none opacity-50"
} flex items-center gap-2 ml-2 mb-1`}
>
<input
className="mr-[2px] op-radio op-radio-xs"
type="radio"
onChange={() => handleNotifySignChange(true)}
checked={formData.NotifyOnSignatures === true}
/>
<div className="text-center">{t("yes")}</div>
</div>
<div
className={`${
isSubscribe ? "" : "pointer-events-none opacity-50"
} flex items-center gap-2 ml-2 mb-1`}
>
<input
className="mr-[2px] op-radio op-radio-xs"
type="radio"
onChange={() => handleNotifySignChange(false)}
checked={formData.NotifyOnSignatures === false}
/>
<div className="text-center">{t("no")}</div>
</div>
</div>
</div>
</div>
)}
</div>
+25 -17
View File
@@ -1,7 +1,7 @@
import React, { useState, useEffect } from "react";
import { useNavigate, useParams } from "react-router-dom";
import axios from "axios";
import { isEnableSubscription } from "../constant/const";
import { emailRegex, isEnableSubscription } from "../constant/const";
import {
contractUsers,
getAppLogo,
@@ -189,23 +189,30 @@ function GuestLogin() {
};
const handleUserData = async (e) => {
e.preventDefault();
const params = { ...contact, docId: documentId };
try {
setLoading(true);
const linkContactRes = await Parse.Cloud.run("linkcontacttodoc", params);
setContactId(linkContactRes.contactId);
const IsEnableOTP = await navigateToDoc(
documentId,
linkContactRes.contactId
);
if (!IsEnableOTP) {
setEnterOtp(true);
await SendOtp();
if (!emailRegex.test(contact.email)) {
alert("Please enter a valid email address.");
} else {
const params = { ...contact, docId: documentId };
try {
setLoading(true);
const linkContactRes = await Parse.Cloud.run(
"linkcontacttodoc",
params
);
setContactId(linkContactRes.contactId);
const IsEnableOTP = await navigateToDoc(
documentId,
linkContactRes.contactId
);
if (!IsEnableOTP) {
setEnterOtp(true);
await SendOtp();
}
} catch (err) {
setLoading(false);
alert(t("something-went-wrong-mssg"));
console.log("Err in link ext contact", err);
}
} catch (err) {
setLoading(false);
alert(t("something-went-wrong-mssg"));
console.log("Err in link ext contact", err);
}
};
const handleInputChange = (e) => {
@@ -348,6 +355,7 @@ function GuestLogin() {
onChange={handleInputChange}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
disabled={loading}
placeholder={t("phone-optional")}
/>
</div>
<div className="mt-2 flex justify-start">
+51 -13
View File
@@ -41,7 +41,8 @@ import {
fetchUrl,
signatureTypes,
handleSignatureType,
getTenantDetails
getTenantDetails,
getBase64FromUrl
} from "../constant/Utils";
import Header from "../components/pdf/PdfHeader";
import RenderPdf from "../components/pdf/RenderPdf";
@@ -59,6 +60,7 @@ import LoaderWithMsg from "../primitives/LoaderWithMsg";
import DownloadPdfZip from "../primitives/DownloadPdfZip";
import Loader from "../primitives/Loader";
import PdfDeclineModal from "../primitives/PdfDeclineModal";
import { serverUrl_fn } from "../constant/appinfo";
function PdfRequestFiles(props) {
const { t } = useTranslation();
@@ -145,6 +147,7 @@ function PdfRequestFiles(props) {
const divRef = useRef(null);
const [isDownloadModal, setIsDownloadModal] = useState(false);
const [signatureType, setSignatureType] = useState([]);
const [pdfBase64Url, setPdfBase64Url] = useState("");
const isMobile = window.innerWidth < 767;
let isGuestSignFlow = false;
@@ -322,10 +325,9 @@ function PdfRequestFiles(props) {
documentData[0] &&
(documentData[0]?.SignedUrl || documentData[0]?.URL);
if (url) {
//convert document url in array buffer format to use embed widgets in pdf using pdf-lib
const arrayBuffer = await convertPdfArrayBuffer(url);
if (arrayBuffer === "Error") {
setHandleError(t("something-went-wrong-mssg"));
const base64Pdf = await getBase64FromUrl(url);
if (base64Pdf) {
setPdfBase64Url(base64Pdf);
}
} else {
setHandleError(t("something-went-wrong-mssg"));
@@ -442,10 +444,9 @@ function PdfRequestFiles(props) {
documentData[0] &&
(documentData[0]?.SignedUrl || documentData[0]?.URL);
if (url) {
//convert document url in array buffer format to use embed widgets in pdf using pdf-lib
const arrayBuffer = await convertPdfArrayBuffer(url);
if (arrayBuffer === "Error") {
setHandleError(t("something-went-wrong-mssg"));
const base64Pdf = await getBase64FromUrl(url);
if (base64Pdf) {
setPdfBase64Url(base64Pdf);
}
} else {
setHandleError(t("something-went-wrong-mssg"));
@@ -1752,6 +1753,42 @@ function PdfRequestFiles(props) {
</div>
);
};
const handleExpiry = async (expiryDate) => {
setIsUiLoading(true);
const doc = pdfDetails?.[0];
const oldExpiryDate = new Date(doc?.ExpiryDate?.iso);
const newExpiryDate = new Date(expiryDate);
if (newExpiryDate > oldExpiryDate) {
const updateExpiryDate = new Date(expiryDate).toISOString();
const expiryIsoFormat = { iso: updateExpiryDate, __type: "Date" };
try {
const serverUrl = serverUrl_fn();
const url = serverUrl + `/classes/contracts_Document/`;
const body = { ExpiryDate: expiryIsoFormat };
const res = await axios.put(url + doc.objectId, body, {
headers: {
"Content-Type": "application/json",
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
"X-Parse-Session-Token": localStorage.getItem("accesstoken")
}
});
if (res.data && res.data.updatedAt) {
setIsExpired(false);
let doc = pdfDetails?.[0];
doc.ExpiryDate = expiryIsoFormat;
setPdfDetails([doc]);
}
} catch (err) {
console.log("err", err);
} finally {
setIsUiLoading(false);
}
} else {
setIsUiLoading(false);
alert(t("expiry-date-error"));
}
};
return (
<DndProvider backend={HTML5Backend}>
<Title title={props.templateId ? "Public Sign" : "Request Sign"} />
@@ -1838,10 +1875,12 @@ function PdfRequestFiles(props) {
{/* this modal is used for show expired alert */}
<PdfDeclineModal
show={isExpired}
doc={pdfDetails?.[0]}
headMsg={t("expired-doc-title")}
bodyMssg={t("expired-on-mssg", { expiredDate })}
isDownloadBtn={true}
handleDownloadBtn={handleDownloadBtn}
handleExpiry={handleExpiry}
/>
{!isEmailVerified && (
<VerifyEmail
@@ -2024,15 +2063,12 @@ function PdfRequestFiles(props) {
<RenderAllPdfPage
signerPos={signerPos}
signerObjectId={signerObjectId}
signPdfUrl={
pdfDetails[0] &&
(pdfDetails[0]?.SignedUrl || pdfDetails[0]?.URL)
}
allPages={allPages}
setAllPages={setAllPages}
setPageNumber={setPageNumber}
pageNumber={pageNumber}
containerWH={containerWH}
pdfBase64Url={pdfBase64Url}
/>
{/* pdf render view */}
<div className=" w-full md:w-[57%] flex mr-4">
@@ -2040,6 +2076,7 @@ function PdfRequestFiles(props) {
clickOnZoomIn={clickOnZoomIn}
clickOnZoomOut={clickOnZoomOut}
isDisableRotate={true}
isDisableDelete={true}
/>
<div className=" w-full md:w-[95%] ">
{/* this modal is used show this document is already sign */}
@@ -2233,6 +2270,7 @@ function PdfRequestFiles(props) {
uniqueId={uniqueId}
ispublicTemplate={isPublicTemplate}
handleUserDetails={handleUserDetails}
pdfBase64Url={pdfBase64Url}
/>
)}
</div>
+96 -46
View File
@@ -40,7 +40,9 @@ import {
handleRemoveWidgets,
handleRotateWarning,
signatureTypes,
handleSignatureType
handleSignatureType,
formatDate,
getBase64FromUrl
} from "../constant/Utils";
import RenderPdf from "../components/pdf/RenderPdf";
import { useNavigate } from "react-router-dom";
@@ -139,7 +141,7 @@ function PlaceHolderSign() {
const [pdfArrayBuffer, setPdfArrayBuffer] = useState("");
const isHeader = useSelector((state) => state.showHeader);
const [activeMailAdapter, setActiveMailAdapter] = useState("");
const [isRotate, setIsRotate] = useState({
const [showRotateAlert, setShowRotateAlert] = useState({
status: false,
degree: 0
});
@@ -151,10 +153,12 @@ function PlaceHolderSign() {
const [isCustomize, setIsCustomize] = useState(false);
const [zoomPercent, setZoomPercent] = useState(0);
const [scale, setScale] = useState(1);
const [pdfRotateBase64, setPdfRotatese64] = useState("");
const [pdfBase64Url, setPdfBase64Url] = useState("");
const [planCode, setPlanCode] = useState("");
const [unSignedWidgetId, setUnSignedWidgetId] = useState("");
const [signatureType, setSignatureType] = useState(signatureTypes);
const [emailResetDate, setEmailResetDate] = useState("");
const [isUploadPdf, setIsUploadPdf] = useState(false);
const isMobile = window.innerWidth < 767;
const [, drop] = useDrop({
accept: "BOX",
@@ -264,11 +268,19 @@ function PlaceHolderSign() {
const url = documentData[0] && documentData[0]?.URL;
//convert document url in array buffer format to use embed widgets in pdf using pdf-lib
const arrayBuffer = await convertPdfArrayBuffer(url);
const base64Pdf = await getBase64FromUrl(url);
if (arrayBuffer === "Error") {
setHandleError(t("something-went-wrong-mssg"));
} else {
setPdfBase64Url(base64Pdf);
setPdfArrayBuffer(arrayBuffer);
}
if (documentData[0]?.ExtUserPtr?.LastEmailCountReset?.iso) {
const resetDate = formatDate(
documentData[0]?.ExtUserPtr?.LastEmailCountReset?.iso
);
setEmailResetDate(resetDate);
}
setExtUserId(documentData[0]?.ExtUserPtr?.objectId);
if (isEnableSubscription) {
checkIsSubscribed(documentData[0]?.ExtUserPtr?.Email);
@@ -580,22 +592,33 @@ function PlaceHolderSign() {
}
setSignerPos(updatesignerPos);
} else {
let updatesignerPos;
//if condition when widget type is prefill label text widget
if (dragTypeValue === textWidget) {
const prefileTextWidget = {
signerPtr: {},
signerObjId: "",
blockColor: "#f58f8c",
placeHolder: [placeHolder],
Role: "prefill",
Id: key
};
signerPos.push(prefileTextWidget);
setSignerPos(signerPos);
//check text widgets data (prefill) already exist then and want to add text widget on new page
//create new page entry with old data and update placeholder
if (filterSignerPos) {
const addPrefillData =
filterSignerPos && filterSignerPos?.placeHolder;
addPrefillData.push(placeHolder);
const updatePrefillPos = signerPos.map((x) =>
x.Role === "prefill" ? { ...x, placeHolder: addPrefillData } : x
);
setSignerPos(updatePrefillPos);
} //else condition if user do not have any text widget data
else {
const prefillTextWidget = {
signerPtr: {},
signerObjId: "",
blockColor: "#f58f8c",
placeHolder: [placeHolder],
Role: "prefill",
Id: key
};
setSignerPos((prev) => [...prev, prefillTextWidget]);
}
} else {
//else condition to add placeholder widgets on multiple page first time
updatesignerPos = signerPos.map((x) =>
const updatesignerPos = signerPos.map((x) =>
x.Id === uniqueId && x?.placeHolder
? { ...x, placeHolder: [...x.placeHolder, placeHolder] }
: x.Id === uniqueId
@@ -770,15 +793,17 @@ function PlaceHolderSign() {
signerupdate.push(newUpdatePos[0]);
setSignerPos(signerupdate);
} else {
const updatedData = signerPos.map((item) => {
if (item.Id === Id) {
// Create a copy of the item object and delete the placeHolder field
const updatedItem = { ...item };
delete updatedItem.placeHolder;
return updatedItem;
}
return item;
});
const updatedData = signerPos
.filter((item) => !(item.Id === Id && item.Role === "prefill")) // Remove prefill object
.map((item) => {
if (item.Id === Id && item.Role !== "prefill") {
// Create a copy of the item object and delete the placeHolder field
const updatedItem = { ...item };
delete updatedItem.placeHolder;
return updatedItem;
}
return item;
});
setSignerPos(updatedData);
}
}
@@ -838,15 +863,12 @@ function PlaceHolderSign() {
} catch (e) {
console.log("error to convertBase64ToFile in placeholder flow", e);
}
} else if (pdfRotateBase64) {
} else if (pdfBase64Url) {
try {
const pdfUrl = await convertBase64ToFile(
pdfDetails[0].Name,
pdfRotateBase64
pdfBase64Url
);
const tenantId = localStorage.getItem("TenantId");
const buffer = atob(pdfRotateBase64);
SaveFileSize(buffer.length, pdfUrl, tenantId);
return pdfUrl;
} catch (e) {
console.log("error to convertBase64ToFile in placeholder flow", e);
@@ -919,7 +941,6 @@ function PlaceHolderSign() {
}
}
};
useEffect(() => {
const timer = setTimeout(() => {
if (signerPos?.length > 0 && !pdfDetails?.[0]?.IsCompleted) {
@@ -928,7 +949,7 @@ function PlaceHolderSign() {
}, 2000);
return () => clearTimeout(timer);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [signerPos, signersdata, signatureType]);
}, [signerPos, signersdata, signatureType, pdfBase64Url]);
// `autosavedetails` is used to save doc details after every 2 sec when changes are happern in placeholder like drag-drop widgets, remove signers
const autosavedetails = async () => {
@@ -942,12 +963,20 @@ function PlaceHolderSign() {
}
return acc;
}, []);
let pdfUrl;
if (isUploadPdf) {
pdfUrl = await convertBase64ToFile(pdfDetails[0].Name, pdfBase64Url);
}
try {
const docCls = new Parse.Object("contracts_Document");
docCls.id = documentId;
docCls.set("Placeholders", signerPos);
docCls.set("Signers", signers);
docCls.set("SignatureType", signatureType);
if (pdfUrl) {
docCls.set("URL", pdfUrl);
}
await docCls.save();
} catch (e) {
console.log("error", e);
@@ -1720,30 +1749,36 @@ function PlaceHolderSign() {
};
//`handleRotationFun` function is used to roatate pdf particular page
const handleRotationFun = async (rotateDegree) => {
const isRotate = handleRotateWarning(signerPos, pageNumber);
if (isRotate) {
setIsRotate({ status: true, degree: rotateDegree });
const rotatePlaceholderExist = handleRotateWarning(signerPos, pageNumber);
//show rotation alert if widgets already exist
if (rotatePlaceholderExist) {
setShowRotateAlert({ status: true, degree: rotateDegree });
} else {
setIsUploadPdf(true);
const urlDetails = await rotatePdfPage(
pdfDetails[0].URL,
rotateDegree,
pageNumber - 1,
pdfRotateBase64
pdfBase64Url
);
setPdfArrayBuffer && setPdfArrayBuffer(urlDetails.arrayBuffer);
setPdfRotatese64(urlDetails.base64);
setPdfBase64Url(urlDetails.base64);
}
};
const handleRemovePlaceholder = async () => {
handleRemoveWidgets(setSignerPos, signerPos, pageNumber, setIsRotate);
handleRemoveWidgets(
setSignerPos,
signerPos,
pageNumber,
uniqueId,
setShowRotateAlert
);
const urlDetails = await rotatePdfPage(
pdfDetails[0].URL,
isRotate.degree,
showRotateAlert.degree,
pageNumber - 1,
pdfRotateBase64
pdfArrayBuffer
);
setPdfArrayBuffer && setPdfArrayBuffer(urlDetails.arrayBuffer);
setPdfRotatese64(urlDetails.base64);
setPdfBase64Url(urlDetails.base64);
};
return (
<>
@@ -1800,7 +1835,7 @@ function PlaceHolderSign() {
setPageNumber={setPageNumber}
setSignBtnPosition={setSignBtnPosition}
pageNumber={pageNumber}
pdfRotateBase64={pdfRotateBase64}
pdfBase64Url={pdfBase64Url}
/>
{/* pdf render view */}
<div className=" w-full md:w-[57%] flex mr-4">
@@ -1808,6 +1843,14 @@ function PlaceHolderSign() {
clickOnZoomIn={clickOnZoomIn}
clickOnZoomOut={clickOnZoomOut}
handleRotationFun={handleRotationFun}
pdfArrayBuffer={pdfArrayBuffer}
pageNumber={pageNumber}
setPdfBase64Url={setPdfBase64Url}
setPdfArrayBuffer={setPdfArrayBuffer}
setIsUploadPdf={setIsUploadPdf}
setSignerPos={setSignerPos}
signerPos={signerPos}
userId={uniqueId}
/>
<div className=" w-full md:w-[95%] ">
{/* this modal is used show alert set placeholder for all signers before send mail */}
@@ -1926,6 +1969,7 @@ function PlaceHolderSign() {
) : mailStatus === "quotareached" ? (
<div className="flex flex-col gap-y-3">
<QuotaCard
emailResetDate={emailResetDate}
handleClose={() => {
setIsSend(false);
setSignerPos([]);
@@ -2071,6 +2115,12 @@ function PlaceHolderSign() {
handleRotationFun={handleRotationFun}
clickOnZoomIn={clickOnZoomIn}
clickOnZoomOut={clickOnZoomOut}
setIsUploadPdf={setIsUploadPdf}
pdfArrayBuffer={pdfArrayBuffer}
setPdfArrayBuffer={setPdfArrayBuffer}
setPdfBase64Url={setPdfBase64Url}
setSignerPos={setSignerPos}
userId={uniqueId}
/>
<div
@@ -2119,7 +2169,7 @@ function PlaceHolderSign() {
setScale={setScale}
scale={scale}
setIsSelectId={setIsSelectId}
pdfRotateBase64={pdfRotateBase64}
pdfBase64Url={pdfBase64Url}
fontSize={fontSize}
setFontSize={setFontSize}
fontColor={fontColor}
@@ -2264,8 +2314,8 @@ function PlaceHolderSign() {
setFontColor={setFontColor}
/>
<RotateAlert
isRotate={isRotate.status}
setIsRotate={setIsRotate}
showRotateAlert={showRotateAlert.status}
setShowRotateAlert={setShowRotateAlert}
handleRemoveWidgets={handleRemovePlaceholder}
/>
</DndProvider>
+116 -20
View File
@@ -5,12 +5,15 @@ import { useTranslation } from "react-i18next";
import Loader from "../primitives/Loader";
import Tooltip from "../primitives/Tooltip";
import {
checkIsSubscribed,
getTenantDetails,
handleSignatureType,
signatureTypes
} from "../constant/Utils";
import Parse from "parse";
import { Tooltip as ReactTooltip } from "react-tooltip";
import { isEnableSubscription } from "../constant/const";
import Upgrade from "../primitives/Upgrade";
const Preferences = () => {
const { t } = useTranslation();
const [isalert, setIsAlert] = useState({ type: "success", msg: "" });
@@ -18,11 +21,19 @@ const Preferences = () => {
const [isLoader, setIsLoader] = useState(false);
const [signatureType, setSignatureType] = useState([]);
const [errMsg, setErrMsg] = useState("");
const [isNotifyOnSignatures, setIsNotifyOnSignatures] = useState();
const [isSubscribe, setIsSubscribe] = useState(false);
useEffect(() => {
fetchSignType();
}, []);
const fetchSignType = async () => {
if (isEnableSubscription) {
const subscribe = await checkIsSubscribed();
setIsSubscribe(subscribe.isValid);
} else {
setIsSubscribe(true);
}
setIsTopLoader(true);
try {
const user = JSON.parse(
@@ -36,6 +47,11 @@ const Preferences = () => {
const getUser = await Parse.Cloud.run("getUserDetails");
if (getUser) {
const _getUser = JSON.parse(JSON.stringify(getUser));
const notifyOnSignatures =
_getUser?.NotifyOnSignatures !== undefined
? _getUser?.NotifyOnSignatures
: "";
setIsNotifyOnSignatures(notifyOnSignatures);
if (tenantSignTypes?.length > 0) {
const signatureType = _getUser?.SignatureType || signatureTypes;
const updatedSignatureType = await handleSignatureType(
@@ -69,39 +85,61 @@ const Preferences = () => {
// `handleSave` is used save updated value signature type
const handleSave = async () => {
setIsLoader(true);
const enabledSignTypes = signatureType?.filter((x) => x.enabled);
const isDefaultSignTypeOnly =
enabledSignTypes?.length === 1 && enabledSignTypes[0]?.name === "default";
if (enabledSignTypes.length === 0) {
setIsAlert({
type: "danger",
msg: t("at-least-one-signature-type")
});
} else if (isDefaultSignTypeOnly) {
setIsAlert({
type: "danger",
msg: t("expect-default-one-more-signature-type")
});
} else {
if (signatureType.length > 0 || isNotifyOnSignatures) {
let params = {};
if (signatureType.length > 0) {
const enabledSignTypes = signatureType?.filter((x) => x.enabled);
const isDefaultSignTypeOnly =
enabledSignTypes?.length === 1 &&
enabledSignTypes[0]?.name === "default";
if (enabledSignTypes.length === 0) {
setIsAlert({
type: "danger",
msg: t("at-least-one-signature-type")
});
} else if (isDefaultSignTypeOnly) {
setIsAlert({
type: "danger",
msg: t("expect-default-one-more-signature-type")
});
} else {
params = { ...params, SignatureType: signatureType };
}
}
if (isNotifyOnSignatures) {
params = { ...params, NotifyOnSignatures: isNotifyOnSignatures };
}
try {
const updateRes = await Parse.Cloud.run("updatesignaturetype", {
SignatureType: signatureType
});
const updateRes = await Parse.Cloud.run("updatepreferences", params);
if (updateRes) {
setIsAlert({ type: "success", msg: "Saved successfully." });
let extUser =
localStorage.getItem("Extand_Class") &&
JSON.parse(localStorage.getItem("Extand_Class"))?.[0];
if (extUser && extUser?.objectId) {
extUser.NotifyOnSignatures = isNotifyOnSignatures;
const _extUser = JSON.parse(JSON.stringify(extUser));
localStorage.setItem("Extand_Class", JSON.stringify([_extUser]));
}
}
} catch (err) {
console.log("Error updating signature type", err);
setIsAlert({ type: "danger", msg: err.message });
}
setTimeout(() => setIsAlert({ type: "success", msg: "" }), 1500);
setIsLoader(false);
}
setTimeout(() => setIsAlert({ type: "success", msg: "" }), 1500);
setIsLoader(false);
};
// `handleNotifySignChange` is trigger when user change radio of notify on signatures
const handleNotifySignChange = (value) => {
setIsNotifyOnSignatures(value);
};
return (
<React.Fragment>
<Title title={"API Token"} />
<Title title={t("Preferences")} />
{isalert.msg && <Alert type={isalert.type}>{isalert.msg}</Alert>}
{isTopLoader ? (
<div className="flex justify-center items-center h-screen">
@@ -200,6 +238,64 @@ const Preferences = () => {
</div>
))}
</div>
</div>
<div className="mb-[0.75rem]">
<label
className={`${
isSubscribe ? "" : "text-gray-300"
} text-[14px] mb-[0.7rem] font-medium`}
>
{t("notify-on-signatures")}
<a data-tooltip-id="nos-tooltip" className="ml-1">
<sup>
<i className="fa-light fa-question rounded-full border-[#33bbff] text-[#33bbff] text-[13px] border-[1px] py-[1.5px] px-[4px]"></i>
</sup>
</a>
<ReactTooltip id="nos-tooltip" className="z-[999]">
<div className="max-w-[200px] md:max-w-[450px] text-[11px]">
<p className="font-bold">{t("notify-on-signatures")}</p>
<p>{t("notify-on-signatures-help.p1")}</p>
<p>{t("notify-on-signatures-help.note")}</p>
</div>
</ReactTooltip>
{!isSubscribe && isEnableSubscription && <Upgrade />}
</label>
<div className="flex flex-col md:flex-row md:gap-4">
<div
className={`${
isSubscribe ? "" : "pointer-events-none opacity-50"
} flex items-center gap-2 ml-2 mb-1`}
>
<input
className="mr-[2px] op-radio op-radio-xs"
type="radio"
onChange={() => handleNotifySignChange(true)}
checked={isNotifyOnSignatures === true}
/>
<div className="text-[13px] font-medium cursor-default capitalize">
{t("yes")}
</div>
</div>
<div className="flex flex-col md:flex-row md:gap-4">
<div
className={`${
isSubscribe ? "" : "pointer-events-none opacity-50"
} flex items-center gap-2 ml-2 mb-1`}
>
<input
className="mr-[2px] op-radio op-radio-xs"
type="radio"
onChange={() => handleNotifySignChange(false)}
checked={isNotifyOnSignatures === false}
/>
<div className="text-[13px] font-medium cursor-default capitalize">
{t("no")}
</div>
</div>
</div>
</div>
</div>
<div className="mb-[0.75rem]">
<button
className="op-btn op-btn-primary"
onClick={handleSave}
+44 -40
View File
@@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import Parse from "parse";
import { appInfo } from "../constant/appinfo";
import { isEnableSubscription } from "../constant/const";
import { emailRegex, isEnableSubscription } from "../constant/const";
import { fetchSubscription } from "../constant/Utils";
import { useDispatch } from "react-redux";
import { showTenant } from "../redux/reducers/ShowTenant";
@@ -88,54 +88,58 @@ const SSOVerify = () => {
// `handleSubmitbtn` is used to create a user in the extended class
const handleSubmitbtn = async (e) => {
e.preventDefault();
setIsLoader(true);
let phone;
if (userDetails?.phone) {
phone = validateInput(userDetails?.phone);
if (!emailRegex.test(userDetails.email)) {
alert("Please enter a valid email address.");
} else {
phone = true;
}
if (userDetails.Destination && userDetails.Company && phone) {
const payload = { sessionToken: localStorage.getItem("accesstoken") };
if (payload && payload.sessionToken) {
const params = {
userDetails: {
name: userDetails.name,
email: userDetails.email,
phone: userDetails?.phone || "",
role: "contracts_User",
company: userDetails.Company,
jobTitle: userDetails.Destination
}
};
try {
const userSignUp = await Parse.Cloud.run("usersignup", params);
if (userSignUp && userSignUp.sessionToken) {
const LocalUserDetails = params.userDetails;
localStorage.setItem(
"userDetails",
JSON.stringify(LocalUserDetails)
);
await thirdpartyLoginfn(userSignUp.sessionToken);
setIsLoader(false);
} else {
alert(userSignUp.message);
setIsLoader(true);
let phone;
if (userDetails?.phone) {
phone = validateInput(userDetails?.phone);
} else {
phone = true;
}
if (userDetails.Destination && userDetails.Company && phone) {
const payload = { sessionToken: localStorage.getItem("accesstoken") };
if (payload && payload.sessionToken) {
const params = {
userDetails: {
name: userDetails.name,
email: userDetails.email,
phone: userDetails?.phone || "",
role: "contracts_User",
company: userDetails.Company,
jobTitle: userDetails.Destination
}
};
try {
const userSignUp = await Parse.Cloud.run("usersignup", params);
if (userSignUp && userSignUp.sessionToken) {
const LocalUserDetails = params.userDetails;
localStorage.setItem(
"userDetails",
JSON.stringify(LocalUserDetails)
);
await thirdpartyLoginfn(userSignUp.sessionToken);
setIsLoader(false);
} else {
alert(userSignUp.message);
setIsLoader(false);
}
} catch (err) {
console.log("error in usersignup", err);
localStorage.removeItem("accesstoken");
alert(t("something-went-wrong-mssg"));
setIsLoader(false);
}
} catch (err) {
console.log("error in usersignup", err);
} else {
localStorage.removeItem("accesstoken");
alert(t("something-went-wrong-mssg"));
alert(t("server-error"));
setIsLoader(false);
}
} else {
localStorage.removeItem("accesstoken");
alert(t("server-error"));
alert(t("filed-required-correctly"));
setIsLoader(false);
}
} else {
alert(t("filed-required-correctly"));
setIsLoader(false);
}
};
// `thirdpartyLoginfn` is used to save necessary parameters locally for the logged-in user
+40 -12
View File
@@ -37,7 +37,9 @@ import {
onClickZoomIn,
onClickZoomOut,
rotatePdfPage,
signatureTypes
signatureTypes,
getBase64FromUrl,
convertBase64ToFile
} from "../constant/Utils";
import { useParams } from "react-router-dom";
import Tour from "reactour";
@@ -129,11 +131,12 @@ function SignYourSelf() {
const [zoomPercent, setZoomPercent] = useState(0);
const isHeader = useSelector((state) => state.showHeader);
const [scale, setScale] = useState(1);
const [pdfRotateBase64, setPdfRotatese64] = useState("");
const [pdfBase64Url, setPdfBase64Url] = useState("");
const [isRotate, setIsRotate] = useState({ status: false, degree: 0 });
const [isSubscribe, setIsSubscribe] = useState({ plan: "", isValid: true });
const [isDownloadModal, setIsDownloadModal] = useState(false);
const [isResize, setIsResize] = useState(false);
const [isUploadPdf, setIsUploadPdf] = useState(false);
const divRef = useRef(null);
const nodeRef = useRef(null);
const [, drop] = useDrop({
@@ -221,13 +224,17 @@ function SignYourSelf() {
? documentData[0]?.Placeholders
: [];
setXyPostion(placeholders);
const url = documentData[0] && documentData[0]?.URL;
const url =
documentData[0] &&
(documentData[0]?.SignedUrl || documentData[0]?.URL);
if (url) {
//convert document url in array buffer format to use embed widgets in pdf using pdf-lib
const arrayBuffer = await convertPdfArrayBuffer(url);
const base64Pdf = await getBase64FromUrl(url);
if (arrayBuffer === "Error") {
setHandleError(t("something-went-wrong-mssg"));
} else {
setPdfBase64Url(base64Pdf);
setPdfArrayBuffer(arrayBuffer);
}
} else {
@@ -562,15 +569,23 @@ function SignYourSelf() {
}, 2000);
return () => clearTimeout(timer);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [xyPostion]);
}, [xyPostion, pdfBase64Url]);
// `autosavedetails` is used to save doc details after every 2 sec when changes are happern in placeholder like drag-drop widgets, remove signers
const autosavedetails = async () => {
try {
const docCls = new Parse.Object("contracts_Document");
docCls.id = documentId;
let pdfUrl;
if (isUploadPdf) {
pdfUrl = await convertBase64ToFile(pdfDetails[0].Name, pdfBase64Url);
}
console.log("isUploadPdf", isUploadPdf);
docCls.set("Placeholders", xyPostion);
docCls.set("IsSignyourself", true);
if (pdfUrl) {
docCls.set("URL", pdfUrl);
}
await docCls.save();
} catch (e) {
console.log("error", e);
@@ -765,7 +780,7 @@ function SignYourSelf() {
const signedpdf = JSON.parse(JSON.stringify(resSignPdf));
setPdfUrl(signedpdf);
getDocumentDetails(false);
setPdfRotatese64("");
setPdfBase64Url("");
}
};
@@ -1127,14 +1142,14 @@ function SignYourSelf() {
if (isPlaceholder) {
setIsRotate({ status: true, degree: rotateDegree });
} else {
setIsUploadPdf(true);
const urlDetails = await rotatePdfPage(
pdfUrl || pdfDetails[0].URL,
rotateDegree,
pageNumber - 1,
pdfRotateBase64
pdfBase64Url
);
setPdfArrayBuffer && setPdfArrayBuffer(urlDetails.arrayBuffer);
setPdfRotatese64(urlDetails.base64);
setPdfBase64Url(urlDetails.base64);
}
};
@@ -1158,10 +1173,10 @@ function SignYourSelf() {
pdfUrl || pdfDetails[0].URL,
isRotate.degree,
pageNumber - 1,
pdfRotateBase64
pdfArrayBuffer
);
setPdfArrayBuffer && setPdfArrayBuffer(urlDetails.arrayBuffer);
setPdfRotatese64(urlDetails.base64);
setPdfBase64Url(urlDetails.base64);
setXyPostion(updatePlaceholder);
setIsRotate({ status: false, degree: 0 });
};
@@ -1228,13 +1243,20 @@ function SignYourSelf() {
setSignBtnPosition={setSignBtnPosition}
pageNumber={pageNumber}
containerWH={containerWH}
pdfRotateBase64={pdfRotateBase64}
pdfBase64Url={pdfBase64Url}
/>
<div className=" w-full md:w-[57%] flex mr-4">
<PdfZoom
clickOnZoomIn={clickOnZoomIn}
clickOnZoomOut={clickOnZoomOut}
handleRotationFun={handleRotationFun}
pdfArrayBuffer={pdfArrayBuffer}
pageNumber={pageNumber}
setPdfBase64Url={setPdfBase64Url}
setPdfArrayBuffer={setPdfArrayBuffer}
setIsUploadPdf={setIsUploadPdf}
setSignerPos={setXyPostion}
signerPos={xyPostion}
/>
<div className="w-full md:w-[95%]">
<ModalUi
@@ -1348,6 +1370,12 @@ function SignYourSelf() {
clickOnZoomOut={clickOnZoomOut}
widgetsDetails={xyPostion}
setIsDownloadModal={setIsDownloadModal}
setIsUploadPdf={setIsUploadPdf}
pdfArrayBuffer={pdfArrayBuffer}
setPdfArrayBuffer={setPdfArrayBuffer}
setPdfBase64Url={setPdfBase64Url}
setSignerPos={setXyPostion}
signerPos={xyPostion}
/>
<div ref={divRef} data-tut="reactourSecond" className="h-full">
{containerWH?.width && containerWH?.height && (
@@ -1388,7 +1416,7 @@ function SignYourSelf() {
handleTextSettingModal={handleTextSettingModal}
setScale={setScale}
scale={scale}
pdfRotateBase64={pdfRotateBase64}
pdfBase64Url={pdfBase64Url}
fontSize={fontSize}
setFontSize={setFontSize}
fontColor={fontColor}
+55 -16
View File
@@ -33,7 +33,9 @@ import {
color,
signatureTypes,
getTenantDetails,
handleSignatureType
handleSignatureType,
getBase64FromUrl,
convertPdfArrayBuffer
} from "../constant/Utils";
import RenderPdf from "../components/pdf/RenderPdf";
import "../styles/AddUser.css";
@@ -102,7 +104,8 @@ const TemplatePlaceholder = () => {
const [isNameModal, setIsNameModal] = useState(false);
const [isTextSetting, setIsTextSetting] = useState(false);
const [pdfLoad, setPdfLoad] = useState(false);
const [pdfRotateBase64, setPdfRotatese64] = useState("");
const [pdfBase64Url, setPdfBase64Url] = useState("");
const [isUploadPdf, setIsUploadPdf] = useState(false);
const isMobile = window.innerWidth < 767;
const [, drop] = useDrop({
accept: "BOX",
@@ -139,6 +142,7 @@ const TemplatePlaceholder = () => {
const [zoomPercent, setZoomPercent] = useState(0);
const [scale, setScale] = useState(1);
const [signatureType, setSignatureType] = useState([]);
const [pdfArrayBuffer, setPdfArrayBuffer] = useState("");
useEffect(() => {
fetchTemplate();
@@ -247,6 +251,17 @@ const TemplatePlaceholder = () => {
tenantSignTypes,
docSignTypes
);
const url = documentData[0] && documentData[0]?.URL;
if (url) {
const arrayBuffer = await convertPdfArrayBuffer(url);
const base64Pdf = await getBase64FromUrl(url);
if (arrayBuffer === "Error") {
setHandleError(t("something-went-wrong-mssg"));
} else {
setPdfBase64Url(base64Pdf);
setPdfArrayBuffer(arrayBuffer);
}
}
setSignatureType(updatedSignatureType);
const updatedPdfDetails = [...documentData];
updatedPdfDetails[0].SignatureType = updatedSignatureType;
@@ -709,12 +724,19 @@ const TemplatePlaceholder = () => {
}
});
}
let pdfUrl;
if (isUploadPdf) {
pdfUrl = await convertBase64ToFile(pdfDetails[0].Name, pdfBase64Url);
}
try {
const templateCls = new Parse.Object("contracts_Template");
templateCls.id = templateId;
templateCls.set("Placeholders", signerPos);
templateCls.set("Signers", signers);
templateCls.set("SignatureType", signatureType);
if (pdfUrl) {
templateCls.set("URL", pdfUrl);
}
await templateCls.save();
} catch (err) {
console.log("error in autosave template", err);
@@ -739,12 +761,9 @@ const TemplatePlaceholder = () => {
});
}
let pdfUrl = pdfDetails[0]?.URL;
if (pdfRotateBase64) {
if (pdfBase64Url) {
try {
pdfUrl = await convertBase64ToFile(
pdfDetails[0].Name,
pdfRotateBase64
);
pdfUrl = await convertBase64ToFile(pdfDetails[0].Name, pdfBase64Url);
} catch (e) {
console.log("error to convertBase64ToFile in placeholder flow", e);
}
@@ -763,7 +782,11 @@ const TemplatePlaceholder = () => {
IsEnableOTP: pdfDetails[0]?.IsEnableOTP === true ? true : false,
IsTourEnabled: pdfDetails[0]?.IsTourEnabled === true ? true : false,
URL: pdfUrl,
SignatureType: signatureType
SignatureType: signatureType,
NotifyOnSignatures:
pdfDetails[0]?.NotifyOnSignatures !== undefined
? pdfDetails[0]?.NotifyOnSignatures
: false
};
const updateTemplate = new Parse.Object("contracts_Template");
updateTemplate.id = templateId;
@@ -896,9 +919,9 @@ const TemplatePlaceholder = () => {
setIsCreateDocModal(false);
setIsCreateDoc(true);
let pdfUrl = pdfDetails[0]?.URL;
if (pdfRotateBase64) {
if (pdfBase64Url) {
try {
pdfUrl = await convertBase64ToFile(pdfDetails[0].Name, pdfRotateBase64);
pdfUrl = await convertBase64ToFile(pdfDetails[0].Name, pdfBase64Url);
} catch (e) {
console.log("error to convertBase64ToFile in placeholder flow", e);
}
@@ -1323,24 +1346,31 @@ const TemplatePlaceholder = () => {
if (isRotate) {
setIsRotate({ status: true, degree: rotateDegree });
} else {
setIsUploadPdf(false);
const urlDetails = await rotatePdfPage(
pdfDetails[0].URL,
rotateDegree,
pageNumber - 1,
pdfRotateBase64
pdfArrayBuffer
);
setPdfRotatese64(urlDetails.base64);
setPdfBase64Url(urlDetails.base64);
}
};
const handleRemovePlaceholder = async () => {
handleRemoveWidgets(setSignerPos, signerPos, pageNumber, setIsRotate);
handleRemoveWidgets(
setSignerPos,
signerPos,
pageNumber,
uniqueId,
setIsRotate
);
const urlDetails = await rotatePdfPage(
pdfDetails[0].URL,
isRotate.degree,
pageNumber - 1,
pdfRotateBase64
pdfBase64Url
);
setPdfRotatese64(urlDetails.base64);
setPdfBase64Url(urlDetails.base64);
};
return (
@@ -1385,6 +1415,7 @@ const TemplatePlaceholder = () => {
setPageNumber={setPageNumber}
setSignBtnPosition={setSignBtnPosition}
pageNumber={pageNumber}
pdfBase64Url={pdfBase64Url}
/>
{/* pdf render view */}
@@ -1393,6 +1424,13 @@ const TemplatePlaceholder = () => {
clickOnZoomIn={clickOnZoomIn}
clickOnZoomOut={clickOnZoomOut}
handleRotationFun={handleRotationFun}
pdfArrayBuffer={pdfArrayBuffer}
pageNumber={pageNumber}
setPdfBase64Url={setPdfBase64Url}
setPdfArrayBuffer={setPdfArrayBuffer}
setIsUploadPdf={setIsUploadPdf}
setSignerPos={setSignerPos}
signerPos={signerPos}
/>
<div className="w-full md:w-[95%]">
{/* this modal is used show alert set placeholder for all signers before send mail */}
@@ -1538,6 +1576,7 @@ const TemplatePlaceholder = () => {
handleRotationFun={handleRotationFun}
clickOnZoomIn={clickOnZoomIn}
clickOnZoomOut={clickOnZoomOut}
setSignerPos={setSignerPos}
/>
<div
ref={divRef}
@@ -1582,7 +1621,7 @@ const TemplatePlaceholder = () => {
setScale={setScale}
scale={scale}
setIsSelectId={setIsSelectId}
pdfRotateBase64={pdfRotateBase64}
pdfBase64Url={pdfBase64Url}
fontSize={fontSize}
setFontSize={setFontSize}
fontColor={fontColor}
@@ -197,6 +197,7 @@ const AddContact = (props) => {
onChange={(e) => setPhone(e.target.value)}
disabled={addYourself}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
placeholder={t("phone-optional")}
/>
</div>
+222 -90
View File
@@ -14,6 +14,7 @@ import {
checkIsSubscribed,
copytoData,
fetchUrl,
formatDate,
getSignedUrl,
getTenantDetails,
handleSignatureType,
@@ -81,6 +82,7 @@ const ReportTable = (props) => {
const [isEmbed, setIsEmbed] = useState(false);
const [isPublicTour, setIsPublicTour] = useState();
const [signatureType, setSignatureType] = useState([]);
const [expiryDate, setExpiryDate] = useState("");
const Extand_Class = localStorage.getItem("Extand_Class");
const extClass = Extand_Class && JSON.parse(Extand_Class);
const startIndex = (currentPage - 1) * props.docPerPage;
@@ -288,6 +290,10 @@ const ReportTable = (props) => {
updatedSignatureType.length > 0
? { SignatureType: updatedSignatureType }
: {};
const NotifyOnSignatures =
Doc.NotifyOnSignatures !== undefined
? { NotifyOnSignatures: Doc.NotifyOnSignatures }
: {};
let placeholdersArr = [];
if (Doc.Placeholders?.length > 0) {
placeholdersArr = Doc.Placeholders;
@@ -315,7 +321,8 @@ const ReportTable = (props) => {
RemindOnceInEvery: Doc?.RemindOnceInEvery || 5,
IsEnableOTP: Doc?.IsEnableOTP || false,
FileAdapterId: Doc?.FileAdapterId || "",
...SignatureType
...SignatureType,
...NotifyOnSignatures
};
try {
const res = await axios.post(
@@ -428,6 +435,8 @@ const ReportTable = (props) => {
} else {
setIsPublicTour({ [item.objectId]: true });
}
} else if (act.action) {
setIsModal({ [`extendexpiry_${item.objectId}`]: true });
}
};
// Get current list
@@ -658,15 +667,12 @@ const ReportTable = (props) => {
}
const closeTour = async () => {
// console.log("closeTour");
setIsTour(false);
if (props.isDontShow) {
const serverUrl = localStorage.getItem("baseUrl");
const appId = localStorage.getItem("parseAppId");
const json = JSON.parse(localStorage.getItem("Extand_Class"));
const extUserId = json && json.length > 0 && json[0].objectId;
// console.log("extUserId ", extUserId)
let updatedTourStatus = [];
if (tourStatusArr.length > 0) {
updatedTourStatus = [...tourStatusArr];
@@ -684,14 +690,8 @@ const ReportTable = (props) => {
await axios.put(
serverUrl + "classes/contracts_Users/" + extUserId,
{
TourStatus: updatedTourStatus
},
{
headers: {
"X-Parse-Application-Id": appId
}
}
{ TourStatus: updatedTourStatus },
{ headers: { "X-Parse-Application-Id": appId } }
);
}
};
@@ -1205,6 +1205,129 @@ const ReportTable = (props) => {
const closePublicTour = () => {
setIsPublicTour();
};
const handleUpdateExpiry = async (e, item) => {
e.preventDefault();
e.stopPropagation();
if (expiryDate) {
const oldExpiryDate = new Date(item?.ExpiryDate?.iso);
const newExpiryDate = new Date(expiryDate);
if (newExpiryDate > oldExpiryDate) {
setActLoader({ [`${item.objectId}`]: true });
const updateExpiryDate = new Date(expiryDate).toISOString();
const expiryIsoFormat = { iso: updateExpiryDate, __type: "Date" };
try {
const serverUrl = serverUrl_fn();
const cls = "contracts_Document";
const url = serverUrl + `/classes/${cls}/`;
const body = { ExpiryDate: expiryIsoFormat };
const res = await axios.put(url + item.objectId, body, {
headers: {
"Content-Type": "application/json",
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
"X-Parse-Session-Token": localStorage.getItem("accesstoken")
}
});
if (res.data && res.data.updatedAt) {
setIsAlert(true);
setAlertMsg({
type: "success",
message: t("expiry-date-updated", {
newexpirydate: new Date(expiryDate)?.toLocaleDateString()
})
});
if (props.ReportName === "Expired Documents") {
const upldatedList = props.List.filter(
(x) => x.objectId !== item.objectId
);
props.setList(upldatedList);
}
}
} catch (err) {
console.log("err", err);
setIsAlert(true);
setAlertMsg({
type: "danger",
message: t("something-went-wrong-mssg")
});
} finally {
setActLoader({});
setExpiryDate();
setTimeout(() => setIsAlert(false), 2000);
setIsModal({});
}
} else {
setIsAlert(true);
setAlertMsg({ type: "danger", message: t("expiry-date-error") });
setTimeout(() => setIsAlert(false), 2000);
}
} else {
setIsAlert(true);
setAlertMsg({ type: "danger", message: t("expiry-date-error") });
setTimeout(() => setIsAlert(false), 2000);
}
};
// `formatStatusRow` is used to format status row
const formatStatusRow = (item) => {
const signers = item?.Placeholders?.map((x, i) => {
const matchSigner = item?.AuditTrail?.find(
(audit) => audit?.UserPtr?.objectId === x.signerObjId
);
if (matchSigner) {
return {
id: i,
Email: matchSigner.UserPtr.Email,
Activity: matchSigner?.Activity?.toUpperCase() || "-",
SignedOn: matchSigner?.SignedOn
? new Date(matchSigner?.SignedOn)?.toUTCString()
: "-",
ViewedOn: matchSigner?.ViewedOn
? new Date(matchSigner?.ViewedOn)?.toUTCString()
: "-"
};
} else {
return {
id: i,
Email: x?.signerPtr?.Email || x?.email,
Activity: "SENT",
SignedOn: "-",
ViewedOn: "-"
};
}
});
return signers?.map((x, i) => (
<div
key={i}
className="text-sm font-medium flex flex-row gap-2 items-center"
>
<button
onClick={() => setIsModal({ [`${item.objectId}_${i}`]: true })}
className={`${
x.Activity === "SIGNED"
? "op-border-primary op-text-primary"
: x.Activity === "VIEWED"
? "border-green-400 text-green-400"
: "border-black text-black"
} focus:outline-none border-2 w-[60px] h-[30px] text-[11px] rounded-full`}
>
{x?.Activity?.toUpperCase() || "-"}
</button>
<div className="py-2 font-bold text-[12px]">{x?.Email || "-"}</div>
{isModal[`${item.objectId}_${i}`] && (
<ModalUi
isOpen
title={t("document-logs")}
handleClose={() => setIsModal({})}
>
<div className="pl-3 first:mt-2 border-t-[1px] border-gray-600 text-[12px] py-2">
<p className="font-bold"> {x?.Email}</p>
<p>Viewed on: {x?.ViewedOn}</p>
<p>Signed on: {x?.SignedOn}</p>
</div>
</ModalUi>
)}
</div>
));
};
return (
<div className="relative">
{Object.keys(actLoader)?.length > 0 && (
@@ -1284,18 +1407,18 @@ const ReportTable = (props) => {
}`}
>
<table className="op-table border-collapse w-full mb-4">
<thead className="text-[14px]">
<thead className="text-[14px] text-center">
<tr className="border-y-[1px]">
{props.heading?.map((item, index) => (
<React.Fragment key={index}>
<th className="px-4 py-2">{t(`report-heading.${item}`)}</th>
<th className="p-2">{t(`report-heading.${item}`)}</th>
</React.Fragment>
))}
{props.ReportName === "Templates" && isEnableSubscription && (
<th className="px-4 py-2">{t("public")}</th>
<th className="p-2">{t("public")}</th>
)}
{props.actions?.length > 0 && (
<th className="px-4 py-2 text-transparent pointer-events-none">
<th className="p-2 text-transparent pointer-events-none">
{t("action")}
</th>
)}
@@ -1308,15 +1431,17 @@ const ReportTable = (props) => {
props.ReportName === "Contactbook" ? (
<tr className="border-y-[1px]" key={index}>
{props.heading.includes("Sr.No") && (
<th className="px-4 py-2">
{startIndex + index + 1}
</th>
<th className="p-2">{startIndex + index + 1}</th>
)}
<td className="px-4 py-2 font-semibold">
{item?.Name}{" "}
</td>
<td className="px-4 py-2 ">{item?.Email || "-"}</td>
<td className="px-4 py-2">{item?.Phone || "-"}</td>
<td className="p-2 text-center">
{item?.Email || "-"}
</td>
<td className="p-2 text-center">
{item?.Phone || "-"}
</td>
<td className="px-3 py-2">
<div className="text-base-content min-w-max flex flex-row gap-x-2 gap-y-1 justify-start items-center">
{props.actions?.length > 0 &&
@@ -1373,34 +1498,39 @@ const ReportTable = (props) => {
key={index}
>
{props.heading.includes("Sr.No") && (
<th className="px-4 py-2">
<th className="px-2 py-2">
{startIndex + index + 1}
</th>
)}
<td className="px-4 py-2 font-semibold min-w-56 max-w-56">
{item?.Name}{" "}
<td className="p-2 min-w-56 max-w-56 flex flex-col">
<div className="font-semibold">{item?.Name}</div>
{item?.ExpiryDate?.iso && (
<div className="text-gray-500">
Expire: {formatDate(item?.ExpiryDate?.iso)}
</div>
)}
</td>
{props?.heading?.includes("Reason") && (
<td className="px-4 py-2">
<td className="p-2 text-center">
{item?.DeclineReason?.length > 25
? item?.DeclineReason?.slice(0, 25) + "..."
: item?.DeclineReason || "-"}
</td>
)}
{props.heading.includes("Note") && (
<td className="px-4 py-2">
{item?.Note?.length > 25
? item?.Note?.slice(0, 25) + "..."
: item?.Note || "-"}
<td className="p-2 text-center">
<p className="truncate w-[100px]">
{item?.Note || "-"}
</p>
</td>
)}
{props.heading.includes("Folder") && (
<td className="px-4 py-2">
<td className="p-2 text-center">
{item?.Folder?.Name ||
t("sidebar.OpenSign™ Drive")}
</td>
)}
<td className="px-4 py-2">
<td className="p-2 text-center">
<button
onClick={() => handleDownload(item)}
className="op-link op-link-primary"
@@ -1409,73 +1539,40 @@ const ReportTable = (props) => {
{item?.URL ? t("download") : "-"}
</button>
</td>
{props.ReportName === "In-progress documents" ? (
<td className="px-4 py-2">
<button
onClick={() =>
item?.AuditTrail?.length > 0 &&
setIsModal({ [item?.objectId]: true })
}
className={`${
item?.AuditTrail?.length
? "border-green-400"
: "cursor-default op-border-primary"
} focus:outline-none w-[60px] border-[2px] text-[12px] rounded-full md:self-center`}
>
{item?.AuditTrail?.length ? "VIEWED" : "SENT"}
</button>
{isModal[item.objectId] && (
<ModalUi
isOpen
title={t("document-logs")}
handleClose={() => setIsModal({})}
>
{item?.AuditTrail?.map((x, i) => (
<div
key={i}
className="pl-3 first:mt-2 text-sm font-medium flex flex-col md:flex-row items-start md:gap-4 border-t-[1px] border-gray-600"
>
<div className="py-2 break-all font-bold md:text-[12px] md:col-span-2 w-full md:w-[210px]">
{x?.UserPtr?.Email || "-"}
</div>
<button className="px-2 cursor-default border-[2px] text-[12px] border-green-400 rounded-full md:self-center">
{x?.Activity?.toUpperCase() || "-"}
</button>
<div className=" text-[12px] py-2">
{x?.Activity === "Signed"
? new Date(x?.SignedOn)?.toUTCString()
: new Date(
x?.ViewedOn
)?.toUTCString() || "-"}
</div>
</div>
))}
</ModalUi>
)}
</td>
) : (
<td className="px-4 py-2">
{props.heading.includes("Owner") && (
<td className="p-2 text-center">
{formatRow(item?.ExtUserPtr)}
</td>
)}
<td className="px-4 py-2">
{!item?.IsSignyourself && item?.Placeholders ? (
<button
onClick={() => handleViewSigners(item)}
className="op-link op-link-primary"
>
{t("view")}
</button>
) : (
"-"
)}
</td>
{props.heading.includes("Signers") &&
["In-progress documents", "Need your sign"].includes(
props.ReportName
) ? (
<td className="px-1 py-2">
{!item?.IsSignyourself && item?.Placeholders && (
<>{formatStatusRow(item)}</>
)}
</td>
) : (
<td className="p-2 text-center">
{!item?.IsSignyourself && item?.Placeholders ? (
<button
onClick={() => handleViewSigners(item)}
className="op-link op-link-primary"
>
{t("view")}
</button>
) : (
"-"
)}
</td>
)}
{props.ReportName === "Templates" &&
isEnableSubscription && (
<td className=" pl-[20px] py-2">
{props.ReportName === "Templates" && (
<div
className="flex flex-row "
className="flex flex-row"
data-tut="IsPublic"
>
<label className="cursor-pointer relative inline-flex items-center mb-0">
@@ -1758,6 +1855,41 @@ const ReportTable = (props) => {
)
)}
</div>
{isModal["extendexpiry_" + item.objectId] && (
<ModalUi
isOpen
title={t("btnLabel.extend-expiry-date")}
reduceWidth={"md:max-w-[450px]"}
handleClose={() => setIsModal({})}
>
<form
className="px-4 py-2 flex flex-col"
onSubmit={(e) => handleUpdateExpiry(e, item)}
>
<label className="mr-2">
{t("expiry-date")} {"(dd-mm-yyyy)"}
</label>
<input
type="date"
className="rounded-full mb-2 bg-base-300 w-full px-4 py-2 text-black border-2 hover:border-spacing-2"
defaultValue={
item?.ExpiryDate?.iso?.split("T")?.[0]
}
onChange={(e) => {
setExpiryDate(e.target.value);
}}
/>
<div className="flex justify-start mb-1">
<button
type="submit"
className="op-btn op-btn-primary"
>
{t("update")}
</button>
</div>
</form>
</ModalUi>
)}
{isShareWith[item.objectId] && (
<div className="op-modal op-modal-open">
<div className="max-h-90 bg-base-100 w-[95%] md:max-w-[500px] rounded-box relative">
+74 -11
View File
@@ -5,6 +5,29 @@ import { useTranslation } from "react-i18next";
function CustomModal(props) {
const { t } = useTranslation();
const [reason, setReason] = useState("");
const [isExtendExpiry, setIsExtendExpiry] = useState(false);
const [expiryDate, setExpiryDate] = useState("");
const localuser = localStorage.getItem(
`Parse/${localStorage.getItem("parseAppId")}/currentUser`
);
const currentUser = JSON.parse(localuser);
const isCreator = props?.doc
? props?.doc?.CreatedBy?.objectId === currentUser?.objectId &&
localStorage.getItem("_user_role") !== "Guest"
: false;
const handleExtendBtn = () => setIsExtendExpiry(!isExtendExpiry);
const handleUpdateExpiry = async (e) => {
e.preventDefault();
e.stopPropagation();
if (expiryDate) {
props.handleExpiry && props.handleExpiry(expiryDate);
} else {
alert(t("expiry-date-error"));
}
};
return (
props.show && (
<dialog className="op-modal op-modal-open absolute z-[448]">
@@ -12,17 +35,29 @@ function CustomModal(props) {
<h3 className="text-base-content font-bold text-lg pt-[15px] px-[20px]">
{props?.headMsg && props?.headMsg}
</h3>
<div className="p-[10px] px-[20px]">
<p className="text-[15px]">{props.bodyMssg && props.bodyMssg}</p>
</div>
{props.isDownloadBtn && (
<div className="flex justify-start w-full ml-[20px] mb-3 mt-1">
<button
className="op-btn op-btn-primary "
onClick={() => props.handleDownloadBtn()}
>
Download
</button>
{!isExtendExpiry && (
<div className="p-[10px] px-[20px]">
<p className="text-[15px]">{props.bodyMssg && props.bodyMssg}</p>
</div>
)}
{!isExtendExpiry && (
<div className="flex flex-row items-center">
{isCreator && (
<button
className="op-btn op-btn-primary px-6 ml-[20px] mb-3 mt-1"
onClick={() => handleExtendBtn()}
>
Extend
</button>
)}
{props.isDownloadBtn && (
<button
className="op-btn op-btn-primary ml-[10px] mb-3 mt-1"
onClick={() => props.handleDownloadBtn()}
>
Download
</button>
)}
</div>
)}
{props.footerMessage && (
@@ -60,6 +95,34 @@ function CustomModal(props) {
</div>
</>
)}
{isExtendExpiry && (
<form className="mx-3 mb-3" onSubmit={handleUpdateExpiry}>
<label className="ml-2 mt-2">
{t("expiry-date")} {"(dd-mm-yyyy)"}
</label>
<input
type="date"
className="rounded-full bg-base-300 w-full px-4 py-2 text-black border-2 hover:border-spacing-2"
defaultValue={props?.doc?.ExpiryDate?.iso?.split("T")?.[0]}
onChange={(e) => setExpiryDate(e.target.value)}
/>
<div className="flex flex-row items-center mt-2">
<button type="submit" className="op-btn op-btn-primary mr-2">
{t("update")}
</button>
<button
type="button"
className="op-btn op-btn-secondary"
onClick={() => {
setExpiryDate("");
setIsExtendExpiry(false);
}}
>
{t("cancel")}
</button>
</div>
</form>
)}
</div>
</dialog>
)
+17 -2
View File
@@ -2,7 +2,7 @@ import React from "react";
import { Trans, useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
const QuotaCard = ({ isPaidInfo, handlClose }) => {
const QuotaCard = ({ isPaidInfo, handlClose, emailResetDate }) => {
const { t } = useTranslation();
const navigate = useNavigate();
return isPaidInfo ? (
@@ -17,6 +17,11 @@ const QuotaCard = ({ isPaidInfo, handlClose }) => {
<div className="op-card-body">
<h2 className="op-card-title">{t("quota-mail-info-head")}</h2>
<p>{t("quota-mail-info")}</p>
{emailResetDate && (
<p>
{t("quota-mail-reset")} {emailResetDate}
</p>
)}
<p>
<Trans i18nKey={"quota-mail-tip"}>
Tip: You can still sign <strong>unlimited documents</strong> by
@@ -39,7 +44,17 @@ const QuotaCard = ({ isPaidInfo, handlClose }) => {
<div className="op-card op-bg-primary text-primary-content w-full shadow-lg">
<div className="op-card-body">
<p>{t("quota-mail")}</p>
<p>{t("quota-mail-tip")}</p>
{emailResetDate && (
<p>
{t("quota-mail-reset")} {emailResetDate}
</p>
)}
<div>
<Trans i18nKey={"quota-mail-tip"}>
Tip: You can still sign <strong>unlimited documents</strong> by
manually sharing the signing request links.
</Trans>
</div>
<div className="op-card-actions justify-end">
<button
onClick={() => navigate("/subscription")}
@@ -1,5 +1,5 @@
{
"header-news": "New feature: Public profiles are now live! Easily create and share public templates",
"header-news": "New feature: Teams plan users can now integrate their own AWS S3 buckets for file storage",
"header-news-btn": "Setup Now",
"create-account": "Create Account",
"login": "Login",
@@ -134,7 +134,8 @@
"option": "Option",
"Embed": "Embed",
"Copy TemplateId": "Copy TemplateId",
"Copy Public URL": "Copy Public URL"
"Copy Public URL": "Copy Public URL",
"extend-expiry-date": "Extend expiry date"
},
"report-heading": {
"Sr.No": "Sr.No",
@@ -155,7 +156,8 @@
"Status": "Status",
"created-date": "Created Date",
"Type": "Type",
"Logs": "Logs"
"Logs": "Logs",
"Expiry-date": "Expiry Date"
},
"report-help": {
"Draft Documents": "These are documents you have started but have not finalized for sending.",
@@ -266,7 +268,8 @@
"make-template-public-alert": "Are you sure you want to make this template public?",
"make-template-private-alert": "Are you sure you want to make this template private? This will remove it from your public profile.",
"public-role": "Public role",
"public-url": "Public profile needed",
"public-url": "Public profile",
"embed-template": "Embed Template",
"public-url-copy": "Heres your public URL: ",
"public-url-copy-mssg": "Copy it or share it with the signer, and you will be able to see all your publicly set templates.",
"add-public-url-alert": "Please complete your OpenSign public profile setup. Once done, youll be able to make templates public. This process only needs to be completed once.",
@@ -512,7 +515,7 @@
"error": "Error",
"invalid-document": "Error: invalid document!",
"document-not-found": "Document not Found!",
"document-signed-alert-1": "You have successfully signed the document. You can download or print a copy of the partially signed document. A copy of the digitally signed document will be sent to the owner over email once it is signed by all signers.",
"document-signed-alert-1": "You have successfully signed the document. You can download or print a copy of the partially signed document. A copy of the digitally signed document & completion certificate will be sent to the owner & all signers over email once it is signed by all signers.",
"encrypted-pdf-not-support": "Currently encrypted pdf files are not supported.",
"document-signed-alert-4": " Congratulations! 🎉 This document has been successfully signed by all participants!",
"document-signed-alert-5": "This document has been signed by all Signers.",
@@ -652,6 +655,7 @@
"angular-npm-mssg-1": "To integrate OpenSign into your Angular project, simply run the following command:",
"quota-mail-info-head": "Monthly request signatures email limit",
"quota-mail-info": "You can send upto 15 signature request emails every month. Upgrade now to send unlimited signing requests directly.",
"quota-mail-reset": "Your signature request email credits will be reset on",
"quota-mail": "You've reached your limit of 15 signature request emails for this month. Upgrade now to continue sending emails directly.",
"quota-mail-tip": "Tip: You can still sign <1>unlimited documents</1> by manually sharing the signing request links.",
"quota-mail-head": "Quota Reached",
@@ -697,5 +701,17 @@
"l2": "Enter your name and choose from various font styles to create a signature.",
"l3": "Upload an existing digital version of your signature.",
"l4": "Use a pre-set signature style saved under 'My Signature' in your settings."
}
},
"notify-on-signatures": "Notify on signatures",
"notify-on-signatures-help": {
"p1": "The document creator will receive an email notification whenever a signer signs the document.",
"note": "Note: Notification email will not be sent for the last signer, as the document creator will receive a completion email instead."
},
"expiry-date": "Expiry date",
"expiry-date-updated": "The expiry date has been successfully extended to {{newexpirydate}}",
"expiry-date-error": "Please provide a new expiry date that is later than the current one",
"delete-page": "Delete page",
"delete-alert": "Can not delete single page",
"delete-alert-2": "Are you sure want to delete this page ?",
"delete-note": "Note - once delete this page you can not undo"
}
@@ -1,5 +1,5 @@
{
"header-news": "Nueva funcionalidad: ¡Los perfiles públicos ahora están disponibles! Crea y comparte plantillas públicas fácilmente",
"header-news": "Nueva característica: los usuarios del plan Teams ahora pueden integrar sus propios depósitos de AWS S3 para el almacenamiento de archivos",
"header-news-btn": "Configurar ahora",
"create-account": "Crear cuenta",
"login": "Iniciar sesión",
@@ -134,7 +134,8 @@
"option": "Opción",
"Embed": "Incrustar",
"Copy TemplateId": "Copiar ID de plantilla",
"Copy Public URL": "Copiar URL pública"
"Copy Public URL": "Copiar URL pública",
"Expiry-date": "Date d'expiration"
},
"report-heading": {
"Sr.No": "Nº",
@@ -155,7 +156,8 @@
"Status": "Estado",
"created-date": "Fecha de creación",
"Type": "Tipo",
"Logs": "Registros"
"Logs": "Registros",
"Expiry-date": "Date d'expiration"
},
"report-help": {
"Draft Documents": "Estos son documentos que has iniciado pero no has finalizado para su envío.",
@@ -266,7 +268,8 @@
"make-template-public-alert": "¿En definitiva quieres convertir esta plantilla en pública?",
"make-template-private-alert": "¿En definitiva quieres convertir esta plantilla en privada? Esto lo removerá de tu perfil público.",
"public-role": "Rol público",
"public-url": "Se requiere perfil público",
"public-url": "Perfil publico",
"embed-template": "Insertar plantilla",
"public-url-copy": "Aquí está tu URL pública: ",
"public-url-copy-mssg": "Cópiala o compártela con el firmante y podrás ver todas tus plantillas públicas.",
"add-public-url-alert": "Por favor, completa la configuración de tu perfil público OpenSign. Una vez terminado, podrás crear plantillas públicas. Este proceso solo necesita ser completado una vez.",
@@ -512,7 +515,7 @@
"error": "Error",
"invalid-document": "Error: ¡Documento inválido!",
"document-not-found": "¡Documento no encontrado!",
"document-signed-alert-1": "Has firmado el documento exitosamente. Puedes descargar o imprimir una copia del documento parcialmente firmado. Una copia del documento firmado digitalmente será enviada al propietario mediante correo electrónico una vez haya sido firmado por todos los firmantes.",
"document-signed-alert-1": "Ha firmado exitosamente el documento. Puede descargar o imprimir una copia del documento parcialmente firmado. Se enviará una copia del documento firmado digitalmente y del certificado de finalización al propietario y a todos los firmantes por correo electrónico una vez que todos los firmantes lo hayan firmado.",
"encrypted-pdf-not-support": "Los archivos PDF encriptados no están soportados actualmente.",
"document-signed-alert-4": " ¡Felicitaciones! 🎉 ¡Este documento ha sido firmado exitosamente por todos los participantes!",
"document-signed-alert-5": "Este documento ha sido firmado por todos los firmantes.",
@@ -652,6 +655,7 @@
"angular-npm-mssg-1": "Para integrar OpenSign a tu proyecto Angular, simplemente ejecuta los siguientes comandos:",
"quota-mail-info-head": "Límite mensual de solicitudes de firma por correo",
"quota-mail-info": "Puedes enviar hasta 15 solicitudes de firma por correo cada mes. Mejora ahora para enviar solicitudes de firma sin límite.",
"quota-mail-reset": "Los créditos de tu correo electrónico de solicitud de firma se restablecerán el",
"quota-mail": "Has alcanzado tu límite de 15 solicitudes de firma por correo para este mes. Mejora ahora para continuar enviando correos.",
"quota-mail-tip": "Consejo: Todavía puedes solciitar la firma de <1>documentos ilimitados</1> compartiendo manualmente los enlaces de solicitud de firma.",
"quota-mail-head": "Cuota alcanzada",
@@ -697,5 +701,17 @@
"l2": "Ingrese su nombre y elija entre varios estilos de fuente para crear una firma.",
"l3": "Cargue una versión digital existente de su firma.",
"l4": "Utilice un estilo de firma preestablecido guardado en 'Mi firma' en su configuración."
}
},
"notify-on-signatures": "Notificar sobre firmas",
"notify-on-signatures-help": {
"p1": "El creador del documento recibirá una notificación por correo electrónico cada vez que un firmante firme el documento.",
"note": "Nota: No se enviará un correo electrónico de notificación al último firmante, ya que el creador del documento recibirá un correo electrónico de finalización."
},
"expiry-date": "Date d'expiration",
"expiry-date-updated": "La fecha de vencimiento se ha extendido con éxito hasta el {{newexpirydate}}",
"expiry-date-error": "Proporcione una nueva fecha de vencimiento que sea posterior a la actual",
"delete-page": "eliminar página",
"delete-alert": "No se puede eliminar una sola página",
"delete-alert-2": "¿Está seguro de que desea eliminar esta página?",
"delete-note": "Nota: una vez que elimines esta página, no podrás deshacerla"
}
@@ -1,5 +1,5 @@
{
"header-news": "Nouvelle fonctionnalité : les profils publics sont désormais en ligne ! Créez et partagez facilement des modèles publics",
"header-news": "Nouvelle fonctionnalité : les utilisateurs du forfait Teams peuvent désormais intégrer leurs propres compartiments AWS S3 pour le stockage de fichiers",
"header-news-btn": "Configurer maintenant",
"create-account": "Créer un compte",
"login": "Se Connecter",
@@ -138,7 +138,8 @@
"Status": "Statut",
"created-date": "Date de création",
"Type": "Saisir",
"Logs": "Journaux"
"Logs": "Journaux",
"Expiry-date": "Fecha de caducidad"
},
"btnLabel": {
"sign": "Signer",
@@ -154,7 +155,8 @@
"option": "Option",
"Embed": "Intégrer",
"Copy TemplateId": "Copier l'ID du modèle",
"Copy Public URL": "Copier l'URL publique"
"Copy Public URL": "Copier l'URL publique",
"extend-expiry-date": "Prolonger la date d'expiration"
},
"report-help": {
"Draft Documents": "Il s'agit de documents que vous avez commencés mais que vous n'avez pas finalisés pour envoi.",
@@ -265,7 +267,8 @@
"make-template-public-alert": "Êtes-vous sûr de vouloir rendre ce modèle public ?",
"make-template-private-alert": "Êtes-vous sûr de vouloir rendre ce modèle privé ? Cela le supprimera de votre profil public.",
"public-role": "Rôle public",
"public-url": "Profil public requis",
"public-url": "Profil public",
"embed-template": "Intégrer le modèle",
"public-url-copy": "Voici votre URL publique : ",
"public-url-copy-mssg": "copiez-la ou partagez-le avec le signataire et vous pouvoir voir tous vos paramètres publics modèles.",
"add-public-url-alert": "Veuillez terminer la configuration de votre profil public OpenSign. Une fois cela fait, vous pourrez rendre les modèles publics. Ce processus ne doit être effectué quune seule fois.",
@@ -511,7 +514,7 @@
"error": "Erreur",
"invalid-document": "Erreur : document invalide !",
"document-not-found": "Document introuvable !",
"document-signed-alert-1": "Vous avez signé le document avec succès. Vous pouvez télécharger ou imprimer une copie du document partiellement signé. Une copie du document signé numériquement sera envoyée au propriétaire par e-mail une fois qu'il sera signé par tous les signataires.",
"document-signed-alert-1": "Vous avez signé le document avec succès. Vous pouvez télécharger ou imprimer une copie du document partiellement signé. Une copie du document signé numériquement et du certificat d'achèvement sera envoyée au propriétaire et à tous les signataires par e-mail une fois qu'il aura été signé par tous les signataires.",
"encrypted-pdf-not-support": "Les fichiers PDF actuellement cryptés ne sont pas pris en charge. ",
"document-signed-alert-4": "Toutes nos félicitations! 🎉 Ce document a été signé avec succès par tous les participants !",
"document-signed-alert-5": "Ce document a été signé par tous les signataires.",
@@ -651,6 +654,7 @@
"angular-npm-mssg-1": "Pour intégrer OpenSign dans votre projet Angular, exécutez simplement la commande suivante :",
"quota-mail-info-head": "Limite mensuelle d'e-mails de signatures de demandes",
"quota-mail-info": "Vous pouvez envoyer jusqu'à 15 e-mails de demande de signature chaque mois. Mettez à niveau maintenant pour envoyer directement des demandes de signature illimitées.",
"quota-mail-reset": "Les crédits de votre email de demande de signature seront réinitialisés le",
"quota-mail": "Vous avez atteint votre limite de 15 e-mails de demande de signature pour ce mois. Mettez à niveau maintenant pour continuer à envoyer des e-mails directement.",
"quota-mail-tip-tip": "Astuce : Vous pouvez toujours signer un nombre <1>illimité de documents</1> en partageant manuellement les liens de demande de signature.",
"quota-mail-head": "Quota atteint",
@@ -696,5 +700,17 @@
"l2": "Entrez votre nom et choisissez parmi différents styles de police pour créer une signature.",
"l3": "Téléchargez une version numérique existante de votre signature.",
"l4": "Utilisez un style de signature prédéfini enregistré sous 'Ma signature' dans vos paramètres."
}
},
"notify-on-signatures": "Notifier les signatures",
"notify-on-signatures-help": {
"p1": "Le créateur du document recevra une notification par e-mail chaque fois qu'un signataire signera le document.",
"note": "Remarque : L'e-mail de notification ne sera pas envoyé pour le dernier signataire, car le créateur du document recevra à la place un e-mail de fin."
},
"expiry-date": "Fecha de caducidad",
"expiry-date-updated": "La date d'expiration a été prolongée avec succès jusqu'au {{newexpirydate}}",
"expiry-date-error": "Veuillez fournir une nouvelle date d'expiration postérieure à la date actuelle",
"delete-page": "supprimer la page",
"delete-alert": "Impossible de supprimer une seule page",
"delete-alert-2": "Etes-vous sûr de vouloir supprimer cette page ?",
"delete-note": "Remarque : une fois cette page supprimée, vous ne pouvez plus l'annuler"
}
@@ -0,0 +1,35 @@
The OpenSign Enterprise license (the “Enterprise License”)
Copyright (c) 2020-2024 Qik Innovations Private Limited.
With regard to the OpenSign Software:
This software and associated documentation files (the "Software") may only be
used in production, if you (and any entity that you represent) have agreed to,
and are in compliance with, the OpenSign Subscription Terms of Service(the “Enterprise Terms”), or other
agreement governing the use of the Software, as agreed by you and OpenSign,
and otherwise have a valid OpenSign Enterprise license for the
correct number of user seats. Subject to the foregoing sentence, you are free to
modify this Software and publish patches to the Software. You agree that OpenSign
and/or its licensors (as applicable) retain all right, title and interest in and
to all such modifications and/or patches, and all such modifications and/or
patches may only be used, copied, modified, displayed, distributed, or otherwise
exploited with a valid OpenSign Enterprise license for the correct
number of user seats. Notwithstanding the foregoing, you may copy and modify
the Software for development and testing purposes, without requiring a
subscription. You agree that OpenSign and/or its licensors (as applicable) retain
all right, title and interest in and to all such modifications. You are not
granted any other rights beyond what is expressly stated herein. Subject to the
foregoing, it is forbidden to copy, merge, publish, distribute, sublicense,
and/or sell the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
For all third party components incorporated into the OpenSign Software, those
components are licensed under the original license provided by the owner of the
applicable component.
@@ -29,7 +29,6 @@ import createFolder from './routes/createFolder.js';
import updateFolder from './routes/updateFolder.js';
import getFolderList from './routes/getFolderList.js';
import deleteFolder from './routes/deleteFolder.js';
import draftTemplate from './routes/draftTemplate.js';
dotenv.config();
const storage = multer.memoryStorage();
const upload = multer({ storage: storage });
@@ -74,9 +73,6 @@ app.delete('/document/:document_id', deleteDocument);
// get all types of documents on the basis of doctype
app.get('/documentlist/:doctype', getDocumentList);
// create draft template with file base64
app.post('/drafttemplate', draftTemplate);
// create Template with co-ordinate
app.post('/createtemplate', createTemplatewithCoordinate);
@@ -1,238 +0,0 @@
import axios from 'axios';
import {
color,
customAPIurl,
saveFileUsage,
formatWidgetOptions,
sanitizeFileName,
} from '../../../../Utils.js';
import uploadFileToS3 from '../../../parsefunction/uploadFiletoS3.js';
import jwt from 'jsonwebtoken';
const randomId = () => Math.floor(1000 + Math.random() * 9000);
export default async function draftTemplate(request, response) {
const name = request.body.title;
const note = request.body.note;
const description = request.body.description;
const signers = request.body.signers;
const folderId = request.body.folderId;
const base64File = request.body.file;
const fileData = request.files?.[0] ? request.files[0].buffer : null;
const SendinOrder = request.body.sendInOrder || true;
const isEnableOTP = request.body?.enableOTP === true ? true : false;
const isTourEnabled = request.body?.enableTour || false;
const isPublic = request.body?.isPublic;
const protocol = customAPIurl();
const baseUrl = new URL(process.env.PUBLIC_URL).origin;
try {
const reqToken = request.headers['x-api-token'];
if (!reqToken) {
return response.status(400).json({ error: 'Please Provide API Token' });
}
const tokenQuery = new Parse.Query('appToken');
tokenQuery.equalTo('token', reqToken);
tokenQuery.include('userId');
const token = await tokenQuery.first({ useMasterKey: true });
if (token !== undefined) {
// Valid Token then proceed request
const parseUser = JSON.parse(JSON.stringify(token));
const userPtr = {
__type: 'Pointer',
className: '_User',
objectId: parseUser.userId.objectId,
};
const contractsUser = new Parse.Query('contracts_Users');
contractsUser.equalTo('UserId', userPtr);
contractsUser.include('TenantId');
const extUser = await contractsUser.first({ useMasterKey: true });
const parseExtUser = JSON.parse(JSON.stringify(extUser));
let fileUrl;
if (request.files?.[0]) {
const base64 = fileData?.toString('base64');
const file = new Parse.File(request.files?.[0]?.originalname, {
base64: base64,
});
await file.save({ useMasterKey: true });
fileUrl = file.url();
const buffer = Buffer.from(base64, 'base64');
saveFileUsage(buffer.length, fileUrl, parseUser.userId.objectId);
} else {
const filename = sanitizeFileName(`${name}.pdf`);
let adapter = {};
const ActiveFileAdapter = parseExtUser?.TenantId?.ActiveFileAdapter || '';
if (ActiveFileAdapter) {
adapter =
parseExtUser?.TenantId?.FileAdapters?.find(x => (x.id = ActiveFileAdapter)) || {};
}
if (adapter?.id) {
const filedata = Buffer.from(base64File, 'base64');
// `uploadFileToS3` is used to save document in user's file storage
fileUrl = await uploadFileToS3(filedata, filename, 'application/pdf', adapter);
} else {
const file = new Parse.File(filename, { base64: base64File }, 'application/pdf');
await file.save({ useMasterKey: true });
fileUrl = file.url();
}
const buffer = Buffer.from(base64File, 'base64');
saveFileUsage(buffer.length, fileUrl, parseUser.userId.objectId);
}
const extUserPtr = {
__type: 'Pointer',
className: 'contracts_Users',
objectId: extUser.id,
};
const object = new Parse.Object('contracts_Template');
object.set('Name', name);
if (note) {
object.set('Note', note);
}
if (description) {
object.set('Description', description);
}
if (SendinOrder) {
object.set('SendinOrder', SendinOrder);
}
object.set('URL', fileUrl);
object.set('CreatedBy', userPtr);
object.set('ExtUserPtr', extUserPtr);
object.set('IsEnableOTP', isEnableOTP);
object.set('IsTourEnabled', isTourEnabled);
const Public = isPublic !== undefined ? isPublic : false;
if (isPublic !== undefined) {
object.set('IsPublic', Public);
}
let contact = [];
if (signers && signers.length > 0) {
let parseSigners;
if (base64File) {
parseSigners = signers;
} else {
parseSigners = JSON.parse(signers);
}
let createContactUrl = protocol + '/v1/createcontact';
for (const [index, element] of parseSigners.entries()) {
if (element?.name && element?.email) {
const body = {
name: element?.name,
email: element?.email,
phone: element?.phone || '',
};
try {
const res = await axios.post(createContactUrl, body, {
headers: { 'Content-Type': 'application/json', 'x-api-token': reqToken },
});
// console.log('res ', res.data);
const contactPtr = {
__type: 'Pointer',
className: 'contracts_Contactbook',
objectId: res.data?.objectId,
};
const newObj = { ...element, contactPtr: contactPtr, index: index };
contact.push(newObj);
} catch (err) {
// console.log('err ', err.response);
if (err?.response?.data?.objectId) {
const contactPtr = {
__type: 'Pointer',
className: 'contracts_Contactbook',
objectId: err.response.data?.objectId,
};
const newObj = { ...element, contactPtr: contactPtr, index: index };
contact.push(newObj);
}
}
} else {
const newObj = { ...element, contactPtr: {}, index: index };
contact.push(newObj);
}
}
const updatedSigners = contact?.filter(x => x.contactPtr && x.contactPtr.objectId);
if (updatedSigners && updatedSigners.length > 0) {
object.set(
'Signers',
updatedSigners?.map(x => x.contactPtr)
);
}
let updatePlaceholders = [];
for (const signer of contact) {
const placeHolder = [];
if (signer.widgets?.length > 0) {
for (const widget of signer.widgets) {
const pageNumber = widget.page;
const page = placeHolder.find(page => page.pageNumber === pageNumber);
const options = formatWidgetOptions(widget.type, widget.options);
const widgetData = {
isStamp: widget.type === 'stamp' || widget.type === 'image',
key: randomId(),
isDrag: false,
scale: 1,
isMobile: false,
zIndex: 1,
type: widget.type === 'textbox' ? 'text input' : widget.type,
options: options,
Width: widget.w,
Height: widget.h,
xPosition: widget.x,
yPosition: widget.y,
};
if (page) {
page.pos.push(widgetData);
} else {
placeHolder.push({ pageNumber, pos: [widgetData] });
}
}
}
updatePlaceholders.push({
signerObjId: signer?.contactPtr?.objectId || '',
signerPtr: signer?.contactPtr,
Role: signer.role,
Id: randomId(),
blockColor: color[signer?.index],
placeHolder,
});
}
if (updatePlaceholders?.length > 0) {
object.set('Placeholders', updatePlaceholders);
}
}
if (folderId) {
object.set('Folder', {
__type: 'Pointer',
className: 'contracts_Template',
objectId: folderId,
});
}
if (parseExtUser?.TenantId?.ActiveFileAdapter) {
object.set('FileAdapterId', parseExtUser?.TenantId?.ActiveFileAdapter);
}
const newACL = new Parse.ACL();
newACL.setPublicReadAccess(false);
newACL.setPublicWriteAccess(false);
newACL.setReadAccess(userPtr.objectId, true);
newACL.setWriteAccess(userPtr.objectId, true);
object.setACL(newACL);
const res = await object.save(null, { useMasterKey: true });
if (request.posthog) {
request.posthog?.capture({
distinctId: parseUser.userId.email,
event: 'api_draft_template',
properties: { response_code: 200 },
});
}
const JwtToken = jwt.sign(
{ user_email: parseUser.userId.email, template_id: res.id },
reqToken
);
return response.json({ objectId: res.id, url: `${baseUrl}/drafttemplate/${JwtToken}` });
} else {
return response.status(405).json({ error: 'Invalid API Token!' });
}
} catch (err) {
console.log('err ', err);
return response.status(400).json({ error: 'Something went wrong, please try again later!' });
}
}
+2
View File
@@ -84,6 +84,7 @@ import updateTourStatus from './parsefunction/updateTourStatus.js';
import saveTemplate from './parsefunction/saveTemplate.js';
import updateToPublicTemplate from './parsefunction/updateToPublicTemplate.js';
import updateSignatureType from './parsefunction/updatesignaturetype.js';
import updatePreferences from './parsefunction/updatePreferences.js';
// This afterSave function triggers after an object is added or updated in the specified class, allowing for post-processing logic.
Parse.Cloud.afterSave('contracts_Document', DocumentAftersave);
@@ -180,3 +181,4 @@ Parse.Cloud.define('isuserincontactbook', isUserInContactBook);
Parse.Cloud.define('updatetourstatus', updateTourStatus);
Parse.Cloud.define('updatetopublictemplate', updateToPublicTemplate);
Parse.Cloud.define('updatesignaturetype', updateSignatureType);
Parse.Cloud.define('updatepreferences', updatePreferences);
@@ -74,6 +74,9 @@ const createDocumentFromTemplate = async (template, existContact, index) => {
if (template?.SignatureType?.length > 0) {
object.set('SignatureType', template?.SignatureType);
}
if (template?.NotifyOnSignatures) {
object.set('NotifyOnSignatures', template?.NotifyOnSignatures);
}
let signers = template?.Signers || [];
const signerobj = {
__type: 'Pointer',
@@ -252,6 +252,7 @@ async function batchQuery(userId, Documents, Ip, parseConfig) {
IsTourEnabled: x?.IsTourEnabled || false,
FileAdapterId: x?.FileAdapterId || '',
...(x?.SignatureType ? { SignatureType: x?.SignatureType } : {}),
...(x?.NotifyOnSignatures ? { NotifyOnSignatures: x?.NotifyOnSignatures } : {}),
},
};
});
@@ -8,7 +8,7 @@ export default function getPresignedUrl(url, adapter) {
AWS.config.update({ credentials: credentials, region: adapter?.region || process.env.DO_REGION });
const spacesEndpoint = adapter?.endpoint || new AWS.Endpoint(process.env.DO_ENDPOINT);
const s3 = new AWS.S3({ endpoint: spacesEndpoint });
const s3 = new AWS.S3({ endpoint: spacesEndpoint, signatureVersion: "v4" });
// Create a new URL object
const parsedUrl = new URL(url);
@@ -458,17 +458,17 @@ export default async function GenerateCertificate(docDetails) {
});
currentPage.drawText('Viewed on :', {
x: half,
x: half + 55,
y: yPosition2,
size: text,
size: timeText,
font: timesRomanFont,
color: textKeyColor,
});
currentPage.drawText(`${new Date(x.ViewedOn).toUTCString()}`, {
x: half + 75,
x: half + 112,
y: yPosition2,
size: text,
size: timeText,
font: timesRomanFont,
color: textValueColor,
});
@@ -490,17 +490,17 @@ export default async function GenerateCertificate(docDetails) {
});
currentPage.drawText('Signed on :', {
x: half,
y: yPosition3,
size: text,
x: half + 55,
y: yPosition3 + 5,
size: timeText,
font: timesRomanFont,
color: textKeyColor,
});
currentPage.drawText(`${new Date(x.SignedOn).toUTCString()}`, {
x: half + 70,
y: yPosition3,
size: text,
x: half + 108,
y: yPosition3 + 5,
size: timeText,
font: timesRomanFont,
color: textValueColor,
});
@@ -520,22 +520,23 @@ export default async function GenerateCertificate(docDetails) {
font: timesRomanFont,
color: textValueColor,
});
currentPage.drawText('Security level :', {
x: half,
y: yPosition4,
size: text,
font: timesRomanFont,
color: textKeyColor,
});
currentPage.drawText(`Email, OTP Auth`, {
x: half + 90,
y: yPosition4,
size: text,
font: timesRomanFont,
color: textValueColor,
});
if (IsEnableOTP) {
currentPage.drawText('Security level :', {
x: half + 55,
y: yPosition4 + 10,
size: timeText,
font: timesRomanFont,
color: textKeyColor,
});
currentPage.drawText(`Email, OTP Auth`, {
x: half + 125,
y: yPosition4 + 10,
size: timeText,
font: timesRomanFont,
color: textValueColor,
});
}
currentPage.drawText('Signature :', {
x: 30,
y: yPosition5,
@@ -95,6 +95,53 @@ async function updateDoc(docId, url, userId, ipAddress, data, className, sign) {
}
}
// `sendNotifyMail` is used to send notification mail of signer signed the document
async function sendNotifyMail(doc, signUser, mailProvider) {
try {
const auditTrailCount = doc?.AuditTrail?.filter(x => x.Activity === 'Signed')?.length || 0;
const signersCount = doc?.Placeholders?.length;
const remaingsign = signersCount - auditTrailCount;
if (remaingsign > 1 && doc?.NotifyOnSignatures) {
const sender = doc.ExtUserPtr;
const pdfName = doc.Name;
const creatorName = doc.ExtUserPtr.Name;
const creatorEmail = doc.ExtUserPtr.Email;
const signerName = signUser.Name;
const signerEmail = signUser.Email;
const mailLogo = 'https://qikinnovation.ams3.digitaloceanspaces.com/logo.png';
const viewDocUrl = `${process.env.PUBLIC_URL}/recipientSignPdf/${doc.objectId}`;
const subject = `Document "${pdfName}" has been signed by ${signerName}`;
const body =
"<html><head><meta http-equiv='Content-Type' content='text/html; charset=UTF-8' /></head><body><div style='background-color:#f5f5f5;padding:20px'><div style='box-shadow:rgba(0, 0, 0, 0.1) 0px 4px 12px;background-color:white;'>" +
`<div><img src=${mailLogo} height='50' style='padding:20px' /></div><div style='padding:2px;font-family:system-ui;background-color:#47a3ad;'>` +
`<p style='font-size:20px;font-weight:400;color:white;padding-left:20px'>Document signed by ${signerName}</p>` +
`</div><div style='padding:20px;font-family:system-ui;font-size:14px'><p>Dear ${creatorName},</p>` +
`<p>${pdfName} has been signed by ${signerName} "${signerEmail}" successfully</p>` +
`<p><a href=${viewDocUrl} target=_blank>View Document</a></p>` +
`</div></div><div><p>This is an automated email from OpenSign™. For any queries regarding this email, please contact the sender ${creatorEmail} directly. If you think this email is inappropriate or spam, you may file a complaint with OpenSign™ <a href=www.opensignlabs.com target=_blank>here</a>.</p></div></div></body></html>`;
const params = {
extUserId: sender.objectId,
from: 'OpenSign™',
recipient: creatorEmail,
subject: subject,
pdfName: pdfName,
html: body,
mailProvider: mailProvider,
};
await axios.post(serverUrl + '/functions/sendmailv3', params, {
headers: {
'Content-Type': 'application/json',
'X-Parse-Application-Id': APPID,
'X-Parse-Master-Key': masterKEY,
},
});
}
} catch (err) {
console.log('err in sendnotifymail', err);
}
}
// `sendCompletedMail` is used to send copy of completed document mail
async function sendCompletedMail(obj) {
const url = obj.doc?.SignedUrl;
@@ -384,6 +431,7 @@ async function PDF(req) {
isCompleted = true;
}
const randomNumber = Math.floor(Math.random() * 5000);
// below regex is used to replace all word with "_" except A to Z, a to z, numbers
const docName = _resDoc?.Name?.replace(/[^a-zA-Z0-9._-]/g, '_')?.toLowerCase();
const name = `signed_${docName}_${randomNumber}.pdf`;
const filePath = `./exports/${name}`;
@@ -446,6 +494,7 @@ async function PDF(req) {
sign // sign base64
);
sendDoctoWebhook(_resDoc, data.imageUrl, 'signed', signUser);
sendNotifyMail(_resDoc, signUser, mailProvider);
saveFileUsage(pdfSize, data.imageUrl, _resDoc?.CreatedBy?.objectId);
if (updatedDoc && updatedDoc.isCompleted) {
const doc = { ..._resDoc, AuditTrail: updatedDoc.AuditTrail, SignedUrl: data.imageUrl };
@@ -76,6 +76,7 @@ export default function reportJson(id, userId) {
'Placeholders',
'SignedUrl',
'FileAdapterId',
'ExpiryDate',
],
};
// In progess report
@@ -211,6 +212,7 @@ export default function reportJson(id, userId) {
'Placeholders',
'SignedUrl',
'FileAdapterId',
'ExpiryDate',
],
};
// Recently sent for signatures report show on dashboard
@@ -292,6 +294,7 @@ export default function reportJson(id, userId) {
'Placeholders',
'SignedUrl',
'FileAdapterId',
'ExpiryDate',
],
};
// Drafts report show on dashboard
@@ -362,6 +365,8 @@ export default function reportJson(id, userId) {
'SharedWith.Name',
'SendinOrder',
'FileAdapterId',
'SignatureType',
'NotifyOnSignatures',
],
};
default:
@@ -37,7 +37,8 @@ const makeEmail = async (to, from, subject, html, url, pdfName) => {
let Pdf = fs.createWriteStream('test.pdf');
const writeToLocalDisk = () => {
return new Promise((resolve, reject) => {
const isSecure = new URL(url)?.protocol === 'https:';
const isSecure =
new URL(url)?.protocol === 'https:' && new URL(url)?.hostname !== 'localhost';
if (useLocal !== 'true' || isSecure) {
https.get(url, async function (response) {
response.pipe(Pdf);
@@ -33,7 +33,9 @@ async function sendMailProvider(req, plan, monthchange) {
let Pdf = fs.createWriteStream('test.pdf');
const writeToLocalDisk = () => {
return new Promise((resolve, reject) => {
const isSecure = new URL(req.params.url)?.protocol === 'https:';
const isSecure =
new URL(req.params.url)?.protocol === 'https:' &&
new URL(req.params.url)?.hostname !== 'localhost';
if (useLocal !== 'true' || isSecure) {
https.get(req.params.url, async function (response) {
response.pipe(Pdf);
@@ -200,7 +202,9 @@ async function sendcustomsmtp(extRes, req) {
let Pdf = fs.createWriteStream('test.pdf');
const writeToLocalDisk = () => {
return new Promise((resolve, reject) => {
const isSecure = new URL(req.params.url)?.protocol === 'https:';
const isSecure =
new URL(req.params.url)?.protocol === 'https:' &&
new URL(req.params.url)?.hostname !== 'localhost';
if (useLocal !== 'true' || isSecure) {
https.get(req.params.url, async function (response) {
response.pipe(Pdf);
@@ -0,0 +1,58 @@
export default async function updatePreferences(request) {
if (!request?.user) {
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'User is not authenticated.');
}
const SignatureType = request.params.SignatureType || [];
const NotifyOnSignatures = request.params.NotifyOnSignatures;
if (SignatureType?.length > 0 || NotifyOnSignatures !== undefined) {
try {
const orgQuery = new Parse.Query('contracts_Users');
orgQuery.equalTo('UserId', {
__type: 'Pointer',
className: '_User',
objectId: request.user.id,
});
const resUser = await orgQuery.first({ useMasterKey: true });
if (resUser) {
const newOrg = new Parse.Object('contracts_Users');
newOrg.id = resUser.id;
if (NotifyOnSignatures !== undefined) {
newOrg.set('NotifyOnSignatures', NotifyOnSignatures);
}
if (SignatureType.length > 0) {
const enabledSignTypes = SignatureType?.filter(x => x.enabled);
const isDefaultSignTypeOnly =
enabledSignTypes?.length === 1 && enabledSignTypes[0]?.name === 'default';
if (enabledSignTypes.length === 0) {
throw new Parse.Error(
Parse.Error.INVALID_QUERY,
'At least one signature type should be enabled.'
);
} else if (isDefaultSignTypeOnly) {
throw new Parse.Error(
Parse.Error.INVALID_QUERY,
'At least one signature type other than the default should be enabled.'
);
} else {
newOrg.set('SignatureType', SignatureType);
}
}
const updateUserRes = await newOrg.save(null, { useMasterKey: true });
if (updateUserRes) {
const _updateUserRes = JSON.parse(JSON.stringify(updateUserRes));
return _updateUserRes;
}
} else {
throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'Premission denied.');
}
} catch (err) {
console.log('err in addorganization', err);
const code = err?.code || 400;
const msg = err?.message || 'Something went wrong.';
throw new Parse.Error(code, msg);
}
} else {
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Please provide parameters.');
}
}
@@ -14,6 +14,7 @@ async function uploadFileToS3(buffer, fileName, mimeType, adapter) {
client = new S3Client({
region: adapter?.region,
credentials: { accessKeyId: adapter?.accessKeyId, secretAccessKey: adapter?.secretAccessKey },
signatureVersion: 'v4'
});
}
const prefixId = crypto.randomBytes(16).toString('hex');
@@ -0,0 +1,35 @@
/**
*
* @param {Parse} Parse
*/
exports.up = async Parse => {
const docSchema = new Parse.Schema('contracts_Document');
docSchema.addBoolean('NotifyOnSignatures');
await docSchema.update();
const templateSchema = new Parse.Schema('contracts_Template');
templateSchema.addBoolean('NotifyOnSignatures');
await templateSchema.update();
const extUserSchema = new Parse.Schema('contracts_Users');
extUserSchema.addBoolean('NotifyOnSignatures');
return extUserSchema.update();
};
/**
*
* @param {Parse} Parse
*/
exports.down = async Parse => {
const docSchema = new Parse.Schema('contracts_Document');
docSchema.deleteField('NotifyOnSignatures');
await docSchema.update();
const templateSchema = new Parse.Schema('contracts_Template');
templateSchema.deleteField('NotifyOnSignatures');
await templateSchema.update();
const extUserSchema = new Parse.Schema('contracts_Users');
docSchema.deleteField('NotifyOnSignatures');
return extUserSchema.update();
};
+480 -575
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -18,15 +18,15 @@
"watch": "nodemon index.js"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.685.0",
"@aws-sdk/s3-request-presigner": "^3.688.0",
"@aws-sdk/client-s3": "^3.691.0",
"@aws-sdk/s3-request-presigner": "^3.691.0",
"@parse/fs-files-adapter": "^3.0.0",
"@parse/s3-files-adapter": "^3.0.0",
"@pdf-lib/fontkit": "^1.1.1",
"@signpdf/placeholder-pdf-lib": "^3.2.4",
"@signpdf/signer-p12": "^3.2.4",
"@signpdf/signpdf": "^3.2.4",
"aws-sdk": "^2.1691.0",
"aws-sdk": "^2.1692.0",
"axios": "^1.7.7",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
@@ -35,7 +35,7 @@
"generate-api-key": "^1.0.2",
"googleapis": "^144.0.0",
"mailgun.js": "^10.2.3",
"mongoose": "^8.8.0",
"mongoose": "^8.8.1",
"multer": "^1.4.5-lts.1",
"multer-s3": "^3.0.1",
"node-forge": "^1.3.1",