Files
OpenSign/apps/OpenSign/src/constant/Utils.js
T
2026-06-30 07:52:10 +00:00

4937 lines
157 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import axios from "axios";
import moment from "moment";
import {
PDFDocument,
rgb,
degrees,
PDFName,
StandardFonts,
PDFArray,
PDFDict
} from "pdf-lib";
import Parse from "parse";
import { appInfo } from "./appinfo";
import { saveAs } from "file-saver";
import printModule from "print-js";
import fontkit from "@pdf-lib/fontkit";
import { SCALE_STEPS, themeColor } from "./const";
import { format, toZonedTime } from "date-fns-tz";
import i18n from "../i18n";
import {
applyNumberFormulasToPages,
buildDownloadFilename,
addPreferenceOpt
} from "../utils";
export const fontsizeArr = [2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28];
export const fontColorArr = ["red", "black", "blue", "yellow"];
export const isMobile = window.innerWidth < 767;
export const isTab = 767 < window.innerWidth < 1023;
export const isHighResolution = window.innerWidth > 1023;
export const isTabAndMobile = window.innerWidth < 1023;
export const textInputWidget = "text input";
export const drawWidget = "draw";
export const textWidget = "text";
export const radioButtonWidget = "radio button";
export const cellsWidget = "cells";
const duplicateAutoApplyWidgetTypes = [
"name",
"company",
"job title",
"email",
textInputWidget
];
const normalizeDuplicateWidgetType = (type) => {
if (typeof type !== "string") {
return type;
}
const normalizedType = type.trim().toLowerCase();
// Backward compatibility for legacy payloads.
if (normalizedType === "textbox") {
return textInputWidget;
}
return normalizedType;
};
const normalizeDuplicateWidgetName = (name) =>
typeof name === "string" ? name.trim().toLowerCase() : "";
const isDuplicateAutoApplyWidget = (widget) =>
duplicateAutoApplyWidgetTypes.includes(
normalizeDuplicateWidgetType(widget?.type)
);
const getDuplicateAutoApplyName = (widget) => {
if (!isDuplicateAutoApplyWidget(widget)) {
return "";
}
return normalizeDuplicateWidgetName(widget?.options?.name);
};
const getWidgetResponseValue = (widget) =>
widget?.options?.response ?? widget?.options?.defaultValue;
const getDuplicateResponseMap = (widgets = []) => {
const responseByName = new Map();
widgets.forEach((widget) => {
const widgetName = getDuplicateAutoApplyName(widget);
const response = getWidgetResponseValue(widget);
if (
widgetName &&
response !== undefined &&
response !== "" &&
!responseByName.has(widgetName)
) {
responseByName.set(widgetName, response);
}
});
return responseByName;
};
const applyDuplicateResponsesToWidgets = (
widgets = [],
responseByName = getDuplicateResponseMap(widgets)
) => {
if (responseByName.size === 0) {
return widgets;
}
return widgets.map((widget) => {
const widgetName = getDuplicateAutoApplyName(widget);
if (!widgetName || !responseByName.has(widgetName)) {
return widget;
}
return {
...widget,
options: {
...widget.options,
response: responseByName.get(widgetName),
defaultValue: ""
}
};
});
};
const applyDuplicateResponsesToPages = (pages = []) => {
const responseByName = getDuplicateResponseMap(
pages.flatMap((page) => page?.pos || [])
);
return pages.map((page) => ({
...page,
pos: applyDuplicateResponsesToWidgets(page?.pos || [], responseByName)
}));
};
export function getEnv() {
return window?.RUNTIME_ENV || {};
}
const appName = "OpenSign™";
export const defaultMailBody = `<p>Hi {{receiver_name}},</p><br><p>We hope this email finds you well. {{sender_name}}&nbsp;has requested you to review and sign&nbsp;{{document_title}}.</p><p>Your signature is crucial to proceed with the next steps as it signifies your agreement and authorization.</p><br><p><a href='{{signing_url}}' rel='noopener noreferrer' target='_blank'>Sign here</a></p><br><br><p>If you have any questions or need further clarification regarding the document or the signing process, please contact the sender.</p><br><p>Thanks</p><p> Team ${appName}</p><br>`;
export const defaultMailSubject = `{{sender_name}} has requested you to sign {{document_title}}`;
export const nonPresentMaskCss = (base) => ({
...base,
width: "0px",
height: "0px"
});
export const randomId = (digit = 8) => {
// 1. Grab a cryptographically-secure 32-bit random value
// Use crypto for stronger randomness
const randomBytes = crypto.getRandomValues(new Uint32Array(1));
const raw = randomBytes[0]; // 0 … 4,294,967,295
// Calculate the min and max for the given digit length
const min = Math.pow(10, digit - 1); // e.g., digit=3 → 100
const max = Math.pow(10, digit) - 1; // e.g., digit=3 → 999
const range = max - min + 1;
// Collapse random value into the range and shift
return min + (raw % range);
};
//function for create list of year for date widget
export const range = (start, end, step) => {
const range = [];
for (let i = start; i <= end; i += step) {
range.push(i);
}
return range;
};
//function for get year
export const getYear = (date) => {
const newYear = new Date(date).getFullYear();
return newYear;
};
export const years = range(1950, getYear(new Date()) + 16, 1);
export const months = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December"
];
export const fileasbytes = async (filepath) => {
const response = await fetch(filepath); // Adjust the path accordingly
const arrayBuffer = await response.arrayBuffer();
return new Uint8Array(arrayBuffer);
};
export const openInNewTab = (url, target) => {
if (target) {
window.open(url, target, "noopener,noreferrer");
} else {
window.open(url, "_blank", "noopener,noreferrer");
}
};
export const getUserCountry = async () => {
try {
const res = await fetch("https://ipapi.co/json/");
const data = await res.json();
return data?.country_code;
} catch (err) {
console.log("Error fetching country", err);
return "";
}
};
// `getSecureUrl` is used to return local secure url if local files
export const getSecureUrl = async (url) => {
const fileUrl = new URL(url)?.pathname?.includes("/files/");
if (fileUrl) {
try {
const fileRes = await Parse.Cloud.run("fileupload", { url: url });
if (fileRes.url) {
return { url: fileRes.url };
} else {
return { url: "" };
}
} catch (err) {
console.log("err while fileupload ", err);
return { url: "" };
}
} else {
return { url: url };
}
};
// `generateId` generates a random alphanumeric ID of specified length.
export function generateId(length) {
const characters =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let result = "";
const charactersLength = characters.length;
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
/**
* Removes a trailing path segment from a URL string, if present.
*
* @param {string} url - The original URL.
* @param {string} segment - The segment to strip off (default: "app").
* @returns {string} - The URL with the trailing segment removed, or unmodified if it didnt match.
*/
export function removeTrailingSegment(url, segment = "app") {
// Normalize a trailing slash (e.g. “/app/” → “/app”)
const normalized = url.endsWith("/") ? url.slice(0, -1) : url;
const lastSlash = normalized.lastIndexOf("/");
const lastPart = normalized.slice(lastSlash + 1);
if (lastPart === segment) {
return normalized.slice(0, lastSlash);
}
return normalized;
}
export const color = [
"#93a3db",
"#e6c3db",
"#c0e3bc",
"#bce3db",
"#b8ccdb",
"#ceb8db",
"#ffccff",
"#99ffcc",
"#cc99ff",
"#ffcc99",
"#66ccff",
"#ffffcc"
];
export const nameColor = [
"#304fbf",
"#7d5270",
"#5f825b",
"#578077",
"#576e80",
"#6d527d",
"#cc00cc",
"#006666",
"#cc00ff",
"#ff9900",
"#336699",
"#cc9900"
];
export const toDataUrl = (file) => {
return new Promise((resolve) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onloadend = (e) => {
resolve(e.target.result);
};
});
};
//function for getting document details for getDrive cloud function
export const getDrive = async (documentId, skip = 0, limit = 50) => {
const data = {
docId: documentId && documentId,
limit: limit,
skip: skip
};
const driveDeatils = await axios
.post(`${localStorage.getItem("baseUrl")}functions/getDrive`, data, {
headers: {
"Content-Type": "application/json",
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
sessiontoken: localStorage.getItem("accesstoken")
}
})
.then((Listdata) => {
const json = Listdata.data;
if (json && json.result.error) {
return json;
} else if (json && json.result) {
const data = json.result;
return data;
} else {
return [];
}
})
.catch((err) => {
console.log("Err in getDrive cloud function", err);
return "Error: Something went wrong!";
});
return driveDeatils;
};
// `pdfNewWidthFun` function is used to calculate pdf width to render in middle container
export const pdfNewWidthFun = (divRef) => {
const pdfWidth = divRef.current.offsetWidth;
return pdfWidth;
};
//`contractUsers` function is used to get contract_User details
export const contractUsers = async () => {
try {
const url = `${localStorage.getItem("baseUrl")}functions/getUserDetails`;
const parseAppId = localStorage.getItem("parseAppId");
const accesstoken = localStorage.getItem("accesstoken");
const token = { "X-Parse-Session-Token": accesstoken };
const headers = {
headers: {
"Content-Type": "application/json",
"X-Parse-Application-Id": parseAppId,
...token
}
};
const userDetails = await axios.post(url, {}, headers);
let data = [];
if (userDetails?.data?.result) {
const json = JSON.parse(JSON.stringify(userDetails.data.result));
data.push(json);
}
return data;
} catch (err) {
console.log("Err in getUserDetails cloud function", err);
return "Error: Something went wrong!";
}
};
//function for resize image and update width and height for mulitisigners
export const handleWidgetResize = (
ref,
key,
signerPos,
setSignerPos,
pageNumber,
containerScale,
signerId,
showResize
) => {
// Compute widget dimensions only once
const { offsetWidth, offsetHeight } = ref;
const factor = containerScale || 1;
const widgetWidth = offsetWidth / factor;
const widgetHeight = offsetHeight / factor;
const filterSignerPos = signerPos.filter((data) => data.Id === signerId);
if (filterSignerPos.length > 0) {
const getPlaceHolder = filterSignerPos[0].placeHolder;
const getPageNumer = getPlaceHolder.filter(
(data) => data.pageNumber === pageNumber
);
if (getPageNumer.length > 0) {
const getXYdata = getPageNumer[0].pos;
const getPosData = getXYdata;
const addSignPos = getPosData.map((url) => {
if (url.key === key) {
// For rotated widgets (90°/270°), the visual dimensions are swapped.
// Store the original-orientation dimensions in the data model.
const rotation = url.options?.rotation || 0;
const isRotSwapped = [90, 270].includes(rotation);
const storedWidth = isRotSwapped ? widgetHeight : widgetWidth;
const storedHeight = isRotSwapped ? widgetWidth : widgetHeight;
const widgetDims = { Width: storedWidth, Height: storedHeight };
// Base fields for every resized signature
const base = {
...url,
Width: storedWidth,
Height: storedHeight,
IsResize: showResize ? true : false
};
// If it's a “type” signature, regenerate the image and options
if (url.typeSignature && url.signatureType === "type") {
const signImg = convertTextToImg(
url.typeFont ?? "Fasthand",
url.typeSignature,
url.fontColor ?? "blue",
widgetDims
);
return {
...base,
SignUrl: signImg,
options: { ...url.options, response: signImg }
};
}
return base;
}
return url;
});
const newUpdateSignPos = getPlaceHolder.map((obj) => {
if (obj.pageNumber === pageNumber) {
return { ...obj, pos: addSignPos };
}
return obj;
});
const newUpdateSigner = signerPos.map((obj) => {
if (obj.Id === signerId) {
return { ...obj, placeHolder: newUpdateSignPos };
}
return obj;
});
setSignerPos(newUpdateSigner);
}
}
};
export const widgets = [
{ type: "signature", icon: "fa-light fa-pen-nib", iconSize: "20px" },
{ type: "stamp", icon: "fa-light fa-stamp", iconSize: "19px" },
{ type: "initials", icon: "fa-light fa-signature", iconSize: "15px" },
{ type: textInputWidget, icon: "fa-light fa-font", iconSize: "21px" },
{ type: "name", icon: "fa-light fa-user", iconSize: "21px" },
{ type: "job title", icon: "fa-light fa-address-card", iconSize: "17px" },
{ type: "company", icon: "fa-light fa-building", iconSize: "25px" },
{ type: "email", icon: "fa-light fa-envelope", iconSize: "20px" },
{ type: "date", icon: "fa-light fa-calendar-days", iconSize: "20px" },
{ type: textWidget, icon: "fa-light fa-text-width", iconSize: "20px" },
{ type: cellsWidget, icon: "fa-light fa-table-cells", iconSize: "20px" },
{ type: "checkbox", icon: "fa-light fa-square-check", iconSize: "22px" },
{
type: "dropdown",
icon: "fa-light fa-circle-chevron-down",
iconSize: "19px"
},
{ type: radioButtonWidget, icon: "fa-light fa-circle-dot", iconSize: "20px" },
{ type: "image", icon: "fa-light fa-image", iconSize: "20px" },
{ type: drawWidget, icon: "fa-light fa-pen-nib", iconSize: "20px" }
];
export const getDate = (dateformat) => {
const format = dateformat || "MM/DD/YYYY";
const date = new Date();
const milliseconds = date.getTime();
const newDate = moment(milliseconds).format(format);
return newDate;
};
export const selectFormat = (data) => {
switch (data) {
case "L":
return "MM/dd/yyyy";
case "MM/DD/YYYY":
return "MM/dd/yyyy";
case "DD-MM-YYYY":
return "dd-MM-yyyy";
case "DD/MM/YYYY":
return "dd/MM/yyyy";
case "LL":
return "MMMM dd, yyyy";
case "DD MMM, YYYY":
return "dd MMM, yyyy";
case "YYYY-MM-DD":
return "yyyy-MM-dd";
case "MM-DD-YYYY":
return "MM-dd-yyyy";
case "MM.DD.YYYY":
return "MM.dd.yyyy";
case "MMM DD, YYYY":
return "MMM dd, yyyy";
case "MMMM DD, YYYY":
return "MMMM dd, yyyy";
case "DD MMMM, YYYY":
return "dd MMMM, yyyy";
case "DD.MM.YYYY":
return "dd.MM.yyyy";
case "DD-MMM-YYYY":
return "dd-MMM-yyyy";
default:
return "MM/dd/yyyy";
}
};
export const changeDateToMomentFormat = (format) => {
switch (format) {
case "MM/dd/yyyy":
return "L";
case "dd-MM-yyyy":
return "DD-MM-YYYY";
case "dd/MM/yyyy":
return "DD/MM/YYYY";
case "MMMM dd, yyyy":
return "LL";
case "dd MMM, yyyy":
return "DD MMM, YYYY";
case "yyyy-MM-dd":
return "YYYY-MM-DD";
case "MM-dd-yyyy":
return "MM-DD-YYYY";
case "MM.dd.yyyy":
return "MM.DD.YYYY";
case "MMM dd, yyyy":
return "MMM DD, YYYY";
case "dd MMMM, yyyy":
return "DD MMMM, YYYY";
case "dd.MM.yyyy":
return "DD.MM.YYYY";
case "dd-MMM-yyyy":
return "DD-MMM-YYYY";
default:
return "L";
}
};
export const getSignerPages = (xyPosition = [], currentKey, signerId) => {
if (!Array.isArray(xyPosition)) {
return [];
}
const hasSigners = xyPosition.some((item) =>
Array.isArray(item?.placeHolder)
);
if (hasSigners) {
const candidates =
signerId !== undefined && signerId !== null && signerId !== ""
? xyPosition.filter(
(item) => item.Id === signerId || item.signerObjId === signerId
)
: xyPosition;
for (const signer of candidates) {
const pages = signer?.placeHolder || [];
const ownsWidget = pages.some((page) =>
(page?.pos || []).some((widget) => widget?.key === currentKey)
);
if (ownsWidget) {
return pages;
}
}
return [];
}
return xyPosition;
};
export const addWidgetOptions = (
type,
signer,
placeholder,
role,
widgetValue
) => {
let defaultOpt;
const id = generateId(6);
if (placeholder) {
const countSameWidget = placeholder?.reduce((count, page) => {
return count + page.pos.filter((item) => item.type === type).length;
}, 0);
const count = countSameWidget + 1;
defaultOpt = { name: `${type}-${id}-${count}`, status: "required" };
} else {
defaultOpt = { name: `${type}-${id}-1`, status: "required" };
}
switch (type) {
case "signature":
return defaultOpt;
case "stamp":
return defaultOpt;
case "checkbox":
return { ...defaultOpt, isReadOnly: false, isHideLabel: false };
case textInputWidget:
return {
...defaultOpt,
isReadOnly: false
};
case cellsWidget:
return {
...defaultOpt,
cellCount: 5,
defaultValue: "",
validation: { type: "", pattern: "" },
isReadOnly: false
};
case "initials":
return defaultOpt;
case "name":
return {
...defaultOpt,
defaultValue: widgetValue ? widgetValue : ""
};
case "company":
return {
...defaultOpt,
defaultValue: widgetValue ? widgetValue : ""
};
case "job title":
return {
...defaultOpt,
defaultValue: widgetValue ? widgetValue : ""
};
case "date": {
const dateFormat = signer?.DateFormat
? selectFormat(signer?.DateFormat)
: "MM/dd/yyyy";
const options = addPreferenceOpt(signer, "date", role);
return {
...defaultOpt,
response: options?.response || "",
isReadOnly: options?.isReadOnly || false,
validation: {
format: options?.format || dateFormat,
type: "date-format"
}
};
}
case "image":
return defaultOpt;
case "email":
return {
...defaultOpt,
validation: { type: "email", pattern: "" },
defaultValue: widgetValue ? widgetValue : ""
};
case "dropdown":
return defaultOpt;
case radioButtonWidget:
return {
...defaultOpt,
values: [],
isReadOnly: false
};
case textWidget:
return defaultOpt;
case drawWidget:
return defaultOpt;
default:
return {};
}
};
export const addWidgetSelfsignOptions = (
type,
getWidgetValue,
owner,
placeholder,
isSignyourself
) => {
let defaultOpt;
//condition to handle widgets name field
const id = generateId(6);
if (placeholder) {
const countSameWidget = placeholder?.reduce((count, page) => {
return count + page.pos.filter((item) => item.type === type).length;
}, 0);
const count = countSameWidget + 1;
defaultOpt = { name: `${type}-${id}-${count}`, status: "required" };
} else {
defaultOpt = { name: `${type}-${id}-1`, status: "required" };
}
switch (type) {
case "signature":
return defaultOpt;
case "stamp":
return defaultOpt;
case "checkbox":
return defaultOpt;
case textWidget:
return defaultOpt;
case cellsWidget:
return {
...defaultOpt,
cellCount: 5,
defaultValue: "",
validation: { type: "", pattern: "" },
isReadOnly: false
};
case "initials":
return defaultOpt;
case "name":
return {
...defaultOpt,
defaultValue: getWidgetValue(type),
validation: { type: "text", pattern: "" }
};
case "company":
return {
...defaultOpt,
defaultValue: getWidgetValue(type),
validation: { type: "text", pattern: "" }
};
case "job title":
return {
...defaultOpt,
defaultValue: getWidgetValue(type),
validation: { type: "text", pattern: "" }
};
case "date": {
const dateFormat = owner?.DateFormat
? selectFormat(owner?.DateFormat)
: "MM/dd/yyyy";
const options = addPreferenceOpt(owner, "date", "", isSignyourself);
return {
...defaultOpt,
response: options?.response || "",
...(!isSignyourself
? { isReadOnly: options?.isReadOnly || false }
: {}),
validation: {
format: options?.format || dateFormat,
type: "date-format"
}
};
}
case "image":
return defaultOpt;
case "email":
return {
...defaultOpt,
defaultValue: getWidgetValue(type),
validation: { type: "email", pattern: "" }
};
default:
return {};
}
};
export const defaultWidthHeight = (type) => {
switch (type) {
case "signature":
return { width: 150, height: 60 };
case "stamp":
return { width: 150, height: 60 };
case "checkbox":
return { width: 15, height: 19 };
case textInputWidget:
return { width: 150, height: 19 };
case cellsWidget:
return { width: 112, height: 22 };
case "dropdown":
return { width: 120, height: 22 };
case "initials":
return { width: 50, height: 50 };
case "name":
return { width: 150, height: 19 };
case "company":
return { width: 150, height: 19 };
case "job title":
return { width: 150, height: 19 };
case "date":
return { width: 100, height: 20 };
case "image":
return { width: 70, height: 70 };
case "email":
return { width: 150, height: 19 };
case radioButtonWidget:
return { width: 5, height: 10 };
case textWidget:
return { width: 150, height: 19 };
case drawWidget:
return { width: 150, height: 60 };
default:
return { width: 150, height: 60 };
}
};
//convert url to base64
export async function getBase64FromUrl(url, autosign) {
const data = await fetch(url);
const blob = await data.blob();
return new Promise((resolve) => {
const reader = new FileReader();
reader.readAsDataURL(blob);
reader.onloadend = function () {
const pdfBase = this.result;
if (autosign) {
resolve(pdfBase);
} else {
const suffixbase64 = pdfBase.split(",").pop();
resolve(suffixbase64);
}
};
});
}
export async function getBase64FromIMG(url) {
const data = await fetch(url);
const blob = await data.blob();
return new Promise((resolve) => {
const reader = new FileReader();
reader.readAsDataURL(blob);
reader.onloadend = function () {
const pdfBase = this.result;
resolve(pdfBase);
};
});
}
//function for convert signature png base64 url to jpeg base64
export const convertPNGtoJPEG = (base64Data) => {
return new Promise((resolve, reject) => {
const canvas = document.createElement("canvas");
const img = new Image();
img.src = base64Data;
img.onload = () => {
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext("2d");
ctx.fillStyle = "#ffffff"; // white color
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0);
// Convert to JPEG by using the canvas.toDataURL() method
const jpegBase64Data = canvas.toDataURL("image/jpeg");
resolve(jpegBase64Data);
};
img.onerror = (error) => {
reject(error);
};
});
};
//function for resize image and update width and height for sign-yourself
export const handleSignYourselfWidgetResize = (
ref,
key,
xyPosition,
setXyPosition,
index,
containerScale
) => {
// Guard against bad index
if (!xyPosition[index]) {
console.log(`ImageResize invalid index ${index}`);
return;
}
// Compute widget dimensions only once
const { offsetWidth, offsetHeight } = ref;
const factor = containerScale || 1;
const widgetWidth = offsetWidth / factor;
const widgetHeight = offsetHeight / factor;
// Single pass to update only the targeted index/key
const updated = xyPosition.map((item, idx) => {
if (idx !== index) return item;
return {
...item,
pos: item.pos.map((url) => {
if (url.key !== key) return url;
// For rotated widgets (90°/270°), the visual dimensions are swapped.
// Store the original-orientation dimensions in the data model.
const rotation = url.options?.rotation || 0;
const isRotSwapped = [90, 270].includes(rotation);
const storedWidth = isRotSwapped ? widgetHeight : widgetWidth;
const storedHeight = isRotSwapped ? widgetWidth : widgetHeight;
const widgetDims = { Width: storedWidth, Height: storedHeight };
// Base fields for every resized signature
const base = {
...url,
Width: storedWidth,
Height: storedHeight,
IsResize: true
};
// If it's a “type” signature, regenerate the image and options
if (url.typeSignature && url.signatureType === "type") {
const signImg = convertTextToImg(
url.typeFont ?? "Fasthand",
url.typeSignature,
url.fontColor ?? "blue",
widgetDims
);
return {
...base,
SignUrl: signImg,
options: { ...url.options, response: signImg }
};
}
return base;
})
};
});
setXyPosition(updated);
};
//function for call cloud function signPdf and generate digital signature
export const signPdfFun = async (
base64Url,
documentId,
signerObjectId,
objectId,
widgets,
activity
) => {
let isCustomCompletionMail = false;
try {
//get tenant details
const tenantDetails = await getTenantDetails(objectId);
if (tenantDetails && tenantDetails === "user does not exist!") {
return { status: "error", message: "User does not exist." };
} else {
if (tenantDetails?.CompletionBody && tenantDetails?.CompletionSubject) {
isCustomCompletionMail = true;
}
}
// below for loop is used to get first signature of user to send if to signpdf
// for adding it in completion certificate
let getSignature;
for (let item of widgets) {
if (!getSignature) {
const typeExist = item.pos.some((data) => data?.type);
if (typeExist) {
getSignature = item.pos.find((data) => data?.type === "signature");
} else {
getSignature = item.pos.find((data) => !data.isStamp);
}
}
}
let base64Sign = getSignature?.SignUrl;
let suffixbase64 = "";
if (base64Sign) {
//check https type signature (default signature exist) then convert in base64
const isUrl = base64Sign.includes("https");
if (isUrl) {
try {
base64Sign = await fetchImageBase64(base64Sign);
} catch (e) {
console.log("error", e);
return { status: "error", message: "something went wrong." };
}
}
//change image width and height to 300/120 in png base64
const imagebase64 = await changeImageWH(base64Sign);
//remove suffiix of base64 (without type)
suffixbase64 = imagebase64 && imagebase64.split(",").pop();
}
const params = {
pdfFile: base64Url,
docId: documentId,
userId: signerObjectId,
isCustomCompletionMail: isCustomCompletionMail,
signature: suffixbase64,
activity: activity || "Signed"
};
const resSignPdf = await Parse.Cloud.run("signPdf", params);
if (resSignPdf) {
const signedPdf = JSON.parse(JSON.stringify(resSignPdf));
return signedPdf;
}
} catch (e) {
console.log("Err in signPdf cloud function ", e.message);
if (e && e?.message?.includes("is encrypted.")) {
return {
status: "error",
message: "Currently encrypted pdf files are not supported."
};
} else if (e?.message?.includes("password")) {
return { status: "error", message: "PFX file password is invalid." };
} else if (
e?.code === 119 ||
e?.message?.toLowerCase?.().includes("strict signing order")
) {
return { status: "error", message: e.message };
} else {
return { status: "error", message: "something went wrong." };
}
}
};
export const createDocument = async (
template,
placeholders,
signerData,
SignedUrl,
isSendDoc
) => {
if (template && template.length > 0) {
const Doc = template[0];
const date = new Date();
const isoDate = date.toISOString();
let extUserId = Doc.ExtUserPtr.objectId;
let creatorId = Doc.CreatedBy.objectId;
const Extand_Class = localStorage.getItem("Extand_Class");
const extClass = Extand_Class && JSON.parse(Extand_Class);
if (extClass && extClass.length > 0) {
if (Doc.ExtUserPtr?.objectId !== extClass?.[0]?.objectId) {
if (extClass && extClass.length > 0) {
extUserId = extClass[0].objectId;
creatorId = extClass[0]?.UserId.objectId;
}
}
}
let placeholdersArr = [];
if (placeholders?.length > 0) {
placeholdersArr = placeholders;
}
let signers = [];
if (signerData?.length > 0) {
signerData.forEach((x) => {
if (x.objectId) {
const obj = {
__type: "Pointer",
className: "contracts_Contactbook",
objectId: x.objectId
};
signers.push(obj);
}
});
}
const useNameAsSender = extClass?.[0]?.UseNameAsSender === true;
const senderName =
Doc?.SenderName || (useNameAsSender ? extClass?.[0]?.Name || "" : "");
const senderMail =
Doc?.SenderMail || (useNameAsSender ? extClass?.[0]?.Email || "" : "");
const SenderName = senderName ? { SenderName: senderName } : {};
const SenderMail = senderMail ? { SenderMail: senderMail } : {};
const SignatureType = Doc?.SignatureType
? { SignatureType: Doc?.SignatureType }
: {};
const NotifyOnSignatures =
Doc?.NotifyOnSignatures !== undefined
? { NotifyOnSignatures: Doc?.NotifyOnSignatures }
: {};
const Bcc = Doc?.Bcc?.length > 0 ? { Bcc: Doc?.Bcc } : {};
const Cc = Doc?.Cc?.length > 0 ? { Cc: Doc?.Cc } : {};
const RedirectUrl = Doc?.RedirectUrl
? { RedirectUrl: Doc?.RedirectUrl }
: {};
const PenColors =
Doc?.PenColors?.length > 0 ? { PenColors: Doc?.PenColors } : {};
const TemplateId = Doc?.objectId
? {
TemplateId: {
__type: "Pointer",
className: "contracts_Template",
objectId: Doc?.objectId
}
}
: {};
const data = {
Name: Doc.Name,
URL: SignedUrl ? SignedUrl : Doc?.URL,
SignedUrl: isSendDoc ? SignedUrl : Doc.SignedUrl,
SentToOthers: Doc.SentToOthers,
Description: Doc.Description,
Note: Doc.Note,
Placeholders: placeholdersArr,
ExtUserPtr: {
__type: "Pointer",
className: "contracts_Users",
objectId: extUserId
},
CreatedBy: { __type: "Pointer", className: "_User", objectId: creatorId },
Signers: signers,
SendinOrder: Doc?.SendinOrder || false,
AutomaticReminders: Doc?.AutomaticReminders || false,
RemindOnceInEvery: parseInt(Doc?.RemindOnceInEvery || 5),
IsEnableOTP: Doc?.IsEnableOTP || false,
SendInOrderStrict: Doc?.SendInOrderStrict || false,
IsTourEnabled: Doc?.IsTourEnabled || false,
AllowModifications: Doc?.AllowModifications || false,
TimeToCompleteDays: parseInt(Doc?.TimeToCompleteDays) || 15,
DocSentAt: { __type: "Date", iso: isoDate },
...SignatureType,
...NotifyOnSignatures,
...SenderName,
...SenderMail,
...Bcc,
...Cc,
...RedirectUrl,
...TemplateId,
...PenColors
};
const remindOnceInEvery = Doc?.RemindOnceInEvery;
const TimeToCompleteDays = parseInt(Doc?.TimeToCompleteDays);
const reminderCount = TimeToCompleteDays / remindOnceInEvery;
const AutomaticReminders = Doc.autoreminder;
if (AutomaticReminders && reminderCount > 15) {
return { status: "error", id: "only-15-reminder-allowed" };
}
const url = `${localStorage.getItem("baseUrl")}functions/createdocumentfromapp`;
const token = {
"X-Parse-Session-Token": localStorage.getItem("accesstoken")
};
try {
const res = await axios.post(
url,
{ document: data },
{
headers: {
"Content-Type": "application/json",
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
...token
}
}
);
if (res) {
const result = res.data.result;
return { status: "success", id: result.objectId, data: result };
}
} catch (err) {
const message =
err?.response?.data?.error || err?.message || "something went wrong.";
console.error("error in create document:", message);
return { status: "error", id: "something-went-wrong-mssg" };
}
}
};
export const getFirstLetter = (name) => {
const firstLetter = name?.charAt(0);
return firstLetter;
};
export const darkenColor = (color, factor) => {
// Remove '#' from the color code and parse it to get RGB values
const hex = color.replace("#", "");
const r = parseInt(hex.substring(0, 2), 16);
const g = parseInt(hex.substring(2, 4), 16);
const b = parseInt(hex.substring(4, 6), 16);
// Darken the color by reducing each RGB component
const darkerR = Math.floor(r * (1 - factor));
const darkerG = Math.floor(g * (1 - factor));
const darkerB = Math.floor(b * (1 - factor));
// Convert the darkened RGB components back to hex
return `#${((darkerR << 16) | (darkerG << 8) | darkerB)
.toString(16)
.padStart(6, "0")}`;
};
export const addZIndex = (signerPos, key, setZIndex) => {
return signerPos.map((item) => {
if (item.placeHolder && item.placeHolder.length > 0) {
// If there is a nested array, recursively add the field to the last object
return {
...item,
placeHolder: addZIndex(item.placeHolder, key, setZIndex)
};
} else if (item.pos && item.pos.length > 0) {
// If there is no nested array, add the new field
return {
...item,
pos: addZIndex(item.pos, key, setZIndex)
// Adjust this line to add the desired field
};
} else {
if (item.key === key) {
setZIndex(item.zIndex);
return {
...item,
zIndex: item.zIndex ? item.zIndex + 1 : 1
};
} else {
return { ...item };
}
}
});
};
//function for save widgets value on onchange function
export const onChangeInput = (
value,
currentPosition,
xyPosition,
index,
setXyPosition,
userId,
initial,
dateFormat,
fontSize,
fontColor,
dateDetails,
textWidgetHeight
) => {
const isDuplicateValueUpdate = !dateFormat && !textWidgetHeight;
const shouldAutoApplyDuplicateWidget = (position) => {
const currentName = getDuplicateAutoApplyName(currentPosition);
const positionName = getDuplicateAutoApplyName(position);
return currentName && positionName && currentName === positionName;
};
const applyDuplicateWidgetValue = (position) => {
if (
position?.options?.response === value &&
position?.options?.defaultValue === ""
) {
return position;
}
return {
...position,
options: {
...position.options,
response: value,
defaultValue: ""
}
};
};
const isSigners = xyPosition.some(
(data) => data.signerPtr || data.Role === "prefill"
);
let filterSignerPos;
if (isSigners) {
if (userId) {
filterSignerPos = xyPosition.filter((data) => data.Id === userId);
}
const getPlaceHolder = filterSignerPos[0]?.placeHolder;
if (initial) {
const xyData = addInitialData(xyPosition, setXyPosition, value, userId);
setXyPosition(xyData);
} else {
const getPageNumer = getPlaceHolder.filter(
(data) => data.pageNumber === index
);
if (getPageNumer.length > 0) {
const updatePositionValue = (position) => {
if (position.key === currentPosition.key) {
if (dateFormat) {
return {
...position,
options: {
...position.options,
response: value,
fontSize: fontSize ? fontSize : position.options?.fontSize,
fontColor: fontColor
? fontColor
: position.options?.fontColor,
isReadOnly:
dateDetails && dateDetails?.isReadOnly !== "undefined"
? dateDetails?.isReadOnly
: position.options?.isReadOnly,
status:
dateDetails && dateDetails?.status !== "undefined"
? dateDetails?.status
: position.options?.status,
name:
dateDetails && dateDetails?.name !== "undefined"
? dateDetails?.name
: position.options?.name,
validation: {
type: "date-format",
format: dateFormat // This indicates the required date format explicitly.
},
hint: dateDetails?.hint || position.options?.hint || ""
}
};
} else if (
(currentPosition?.type === "text" ||
currentPosition?.type === textInputWidget) &&
textWidgetHeight &&
!value
) {
return { ...position, Height: textWidgetHeight };
} else {
return applyDuplicateWidgetValue(position);
}
}
if (
isDuplicateValueUpdate &&
shouldAutoApplyDuplicateWidget(position)
) {
return applyDuplicateWidgetValue(position);
}
return position;
};
setXyPosition((prevPosition) => {
const sourcePosition = Array.isArray(prevPosition)
? prevPosition
: xyPosition;
return sourcePosition.map((obj) => {
if (obj.Id !== userId) {
return obj;
}
const updatedPlaceHolder = (obj.placeHolder || []).map((page) => ({
...page,
pos: page.pos.map(updatePositionValue)
}));
return {
...obj,
placeHolder: applyNumberFormulasToPages(updatedPlaceHolder)
};
});
});
}
}
} else {
const updatePositionValue = (positionData) => {
if (positionData.key === currentPosition.key) {
if (dateFormat) {
return {
...positionData,
options: {
...positionData.options,
response: value,
fontSize: fontSize,
fontColor: fontColor,
validation: {
type: "date-format",
format: dateFormat // This indicates the required date format explicitly.
}
}
};
} else if (
currentPosition?.type === "text" &&
textWidgetHeight &&
!value
) {
return { ...positionData, Height: textWidgetHeight };
} else {
return applyDuplicateWidgetValue(positionData);
}
}
if (
isDuplicateValueUpdate &&
shouldAutoApplyDuplicateWidget(positionData)
) {
return applyDuplicateWidgetValue(positionData);
}
return positionData;
};
setXyPosition((prevPosition) => {
const sourcePosition = Array.isArray(prevPosition)
? prevPosition
: xyPosition;
const updatePlaceholder = sourcePosition.map((obj) => ({
...obj,
pos: obj.pos.map(updatePositionValue)
}));
return applyNumberFormulasToPages(updatePlaceholder);
});
}
};
//function to increase height of text area on press enter
export const onChangeHeightOfTextArea = (
height,
widgetType,
signKey,
xyPosition,
index,
setXyPosition,
userId
) => {
const isSigners = xyPosition.some((data) => data.signerPtr);
let filterSignerPos;
if (isSigners) {
if (userId) {
filterSignerPos = xyPosition.filter((data) => data.Id === userId);
}
const getPlaceHolder = filterSignerPos[0]?.placeHolder;
const getPageNumer = getPlaceHolder.filter(
(data) => data.pageNumber === index
);
if (getPageNumer.length > 0) {
const getXYdata = getPageNumer[0].pos;
const getPosData = getXYdata;
const addSignPos = getPosData.map((position) => {
if (position.key === signKey) {
return {
...position,
Height: position.Height
? position.Height + height
: defaultWidthHeight(widgetType).height + height
};
}
return position;
});
const newUpdateSignPos = getPlaceHolder.map((obj) => {
if (obj.pageNumber === index) {
return { ...obj, pos: addSignPos };
}
return obj;
});
const newUpdateSigner = xyPosition.map((obj) => {
if (obj.Id === userId) {
return { ...obj, placeHolder: newUpdateSignPos };
}
return obj;
});
setXyPosition(newUpdateSigner);
}
} else {
let getXYdata = xyPosition[index].pos;
const updatePosition = getXYdata.map((position) => {
if (position.key === signKey) {
return {
...position,
Height: position.Height
? position.Height + height
: defaultWidthHeight(widgetType).height + height
};
}
return position;
});
const updatePlaceholder = xyPosition.map((obj, ind) => {
if (ind === index) {
return { ...obj, pos: updatePosition };
}
return obj;
});
setXyPosition(updatePlaceholder);
}
};
//calculate width and height
export const calculateInitialWidthHeight = (widgetData) => {
const intialText = widgetData;
const span = document.createElement("span");
span.textContent = intialText;
span.style.font = `12px`; // here put your text size and font family
span.style.display = "hidden";
document.body.appendChild(span);
const width = span.offsetWidth;
const height = span.offsetHeight;
document.body.removeChild(span);
return { getWidth: width, getHeight: height };
};
export const widgetDataValue = (type, value) => {
switch (type) {
case "name":
return value?.Name || value?.name;
case "company":
return value?.Company || value?.company;
case "job title":
return value?.JobTitle || value?.jobTitle;
case "email":
return value?.Email || value?.email;
default:
return "";
}
};
export const addInitialData = (signerPos, setXyPosition, value, userId) => {
return signerPos.map((item) => {
if (item.placeHolder && item.placeHolder.length > 0) {
// If there is a nested array, recursively add the field to the last object
if (item.Id === userId) {
return {
...item,
placeHolder: addInitialData(
item.placeHolder,
setXyPosition,
value,
userId
)
};
} else {
return item;
}
} else if (item.pos && item.pos.length > 0) {
// If there is no nested array, add the new field
return {
...item,
pos: addInitialData(item.pos, setXyPosition, value, userId)
// Adjust this line to add the desired field
};
} else {
const widgetData = widgetDataValue(item.type, value);
if (["name", "company", "job title", "email"].includes(item.type)) {
return {
...item,
options: {
...item.options,
defaultValue: item?.options?.defaultValue || widgetData
}
};
} else {
return item;
}
}
});
};
//function for embed document id
export const embedDocId = async (pdfOriginalWH, pdfDoc, documentId) => {
const appName = "OpenSign™";
// `fontBytes` is used to embed custom font in pdf
const fontBytes = await fileasbytes(
"https://cdn.opensignlabs.com/webfonts/times.ttf"
);
pdfDoc.registerFontkit(fontkit);
const font = await pdfDoc.embedFont(fontBytes, { subset: true });
//pdfOriginalWH contained all pdf's pages width and height
for (let i = 0; i < pdfOriginalWH?.length; i++) {
const fontSize = 10;
const textContent = documentId && `${appName} DocumentId: ${documentId} `;
const pages = pdfDoc.getPages();
const page = pages[i];
const getSize = pdfOriginalWH[i];
try {
const getObj = compensateRotation(
page.getRotation().angle,
10,
5,
1,
getSize,
fontSize,
rgb(0.5, 0.5, 0.5),
font,
page
);
page.drawText(textContent, getObj);
} catch (err) {
console.log("Err in embed docId on page", i + 1, err?.message);
}
}
};
// function for convert input text value in image
export function convertTextToImg(fontStyle, text, color, widgetDims) {
// read your widget dimensions:
const maxWidth = widgetDims.Width;
const maxHeight = widgetDims.Height;
const baselineFontSizePx = maxHeight;
const fontFamily = fontStyle || "Fasthand";
const fillColor = color || "blue";
// 1. Use a temporary canvas to measure actual text size
const tempCanvas = document.createElement("canvas");
const tempCtx = tempCanvas.getContext("2d");
tempCtx.font = `${baselineFontSizePx}px ${fontFamily}`;
const metrics = tempCtx.measureText(text);
const actualHeight =
metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;
const actualWidth = metrics.width;
// const renderscale = 3;
// 2. Scale font size so text height fills canvas
const scaleX = maxWidth / actualWidth;
const scaleY = maxHeight / actualHeight;
const scale = Math.min(scaleX, scaleY, 1);
// final font-size
// const finalFontSizePx = baselineFontSizePx * scale * renderscale;
const finalFontSizePx = baselineFontSizePx * scale;
// 3. Create the final canvas
const pxRatio = window.devicePixelRatio * 2 || 3;
const canvas = document.createElement("canvas");
canvas.width = Math.ceil(maxWidth * pxRatio);
canvas.height = Math.ceil(maxHeight * pxRatio);
const ctx = canvas.getContext("2d");
ctx.scale(pxRatio, pxRatio);
// 4. Setup text with final font
ctx.font = `${finalFontSizePx}px ${fontFamily}`;
ctx.fillStyle = fillColor;
ctx.textAlign = "center";
ctx.textBaseline = "alphabetic";
const newMetrics = ctx.measureText(text);
const ascent = newMetrics.actualBoundingBoxAscent;
const descent = newMetrics.actualBoundingBoxDescent;
// 5. Align text vertically so it touches top and bottom
const y = maxHeight / 2 + (ascent - descent) / 2;
ctx.clearRect(0, 0, maxWidth, maxHeight);
ctx.fillText(text, maxWidth / 2, y);
// 6. Output image
const dataUrl = canvas.toDataURL("image/png");
return dataUrl;
}
// `onSaveSign` trigger on save button to save for signature, initials widget for type: draw, typed, default
export function onSaveSign(
type,
xyPosition,
index,
signKey,
signatureImg,
updatedImgWH,
isTypeText,
typedSignature,
isAutoSign,
widgetsType,
typeFont,
fontColor
) {
let getIMGWH, posWidth, posHeight;
let getXYdata = xyPosition[index].pos;
const isSignOrInitials =
widgetsType && ["signature", "initials"].includes(widgetsType);
const updateXYData = getXYdata.map((position) => {
if (position.key === signKey) {
if (isTypeText) {
getIMGWH = {
newWidth: updatedImgWH.width,
newHeight: updatedImgWH.height
};
}
posWidth = getIMGWH ? getIMGWH.newWidth : 150;
posHeight = getIMGWH ? getIMGWH.newHeight : 60;
const widgetDims = { Width: posWidth, Height: posHeight };
const signImg = typedSignature
? convertTextToImg(typeFont, typedSignature, fontColor, widgetDims)
: signatureImg;
if (widgetsType === drawWidget) {
return {
...position,
options: { ...position.options, response: signImg }
};
} else {
return {
...position,
...(type === "type" ? { Width: posWidth } : {}),
...(type === "type" ? { Height: posHeight } : {}),
SignUrl: signImg,
...(isSignOrInitials && { signatureType: type || "" }),
options: { ...position.options, response: signImg },
...(typedSignature && {
typeSignature: typedSignature,
typeFont: typeFont ?? "Fasthand",
fontColor: fontColor ?? "blue"
})
};
}
}
return position;
});
const updateXYposition = xyPosition.map((obj, ind) => {
if (ind === index) {
return { ...obj, pos: updateXYData };
}
return obj;
});
//condition when draw/upload signature/initials then apply it all related to widgets (draw, typed signature or default signature)
if (isAutoSign && widgetsType !== drawWidget) {
const updatedArray = updateXYposition.map((page) => ({
...page,
pos: page.pos.map((item) => {
if (item.type === widgetsType) {
const widgetDims = { Width: item.Width, Height: item.Height };
const signImg = typedSignature
? convertTextToImg(typeFont, typedSignature, fontColor, widgetDims)
: signatureImg;
return {
...item,
SignUrl: signImg,
...(isSignOrInitials && { signatureType: type || "" }),
options: { ...item.options, response: signImg },
...(typedSignature && {
typeSignature: typedSignature,
typeFont: typeFont || "Fasthand",
fontColor: fontColor || "blue"
})
};
}
return item; // Otherwise, keep it unchanged
})
}));
return updatedArray;
} else {
return updateXYposition;
}
}
export function clearResponse(widgetKey, placeholder = [], index) {
if (!Array.isArray(placeholder) || !placeholder[index]?.pos) {
return placeholder;
}
const getXYdata = placeholder[index]?.pos;
const updateXYData = getXYdata.map((widget) => {
if (widget?.key !== widgetKey) return widget;
return {
...widget,
options: { ...widget.options, response: "" },
SignUrl: ""
};
});
const updatePlaceholder = placeholder.map((p, ind) => {
if (ind !== index) return p;
return { ...p, pos: updateXYData };
});
return updatePlaceholder;
}
/**
* Scales and centers a base64encoded image into a fixedsize widget
* and returns a new base64 PNG.
*
* @param {string} base64Image A data-URL (e.g. "data:image/png;base64,…")
* @param {{ Width: number, Height: number }} widgetDims
* @returns {Promise<string>} A Promise that resolves to a data-URL of the new image
*/
export async function convertBase64ToImg(base64Image, widgetDims) {
const { Width: maxWidth, Height: maxHeight } = widgetDims;
// Load the image off-DOM
const img = new Image();
img.src = base64Image;
await new Promise((resolve, reject) => {
img.onload = resolve;
img.onerror = reject;
});
// 2. Compute scale to fit within widget (preserving aspect ratio)
const { naturalWidth: imgW, naturalHeight: imgH } = img;
const scale = Math.min(maxWidth / imgW, maxHeight / imgH, 1);
const drawW = imgW * scale;
const drawH = imgH * scale;
// 3. Prepare a high-DPI canvas
const pxRatio = (window.devicePixelRatio || 1) * 2;
const canvas = document.createElement("canvas");
canvas.width = Math.ceil(maxWidth * pxRatio);
canvas.height = Math.ceil(maxHeight * pxRatio);
const ctx = canvas.getContext("2d");
ctx.scale(pxRatio, pxRatio);
ctx.clearRect(0, 0, maxWidth, maxHeight);
// 4. Center the image in the widget rectangle
const x = (maxWidth - drawW) / 2;
const y = (maxHeight - drawH) / 2;
ctx.drawImage(img, x, y, drawW, drawH);
// 5. Return new base64 in same format as input
// const quality =
// inputMime.includes("jpeg") || inputMime.includes("jpg") ? 0.9 : undefined;
// return canvas.toDataURL(inputMime, quality);
// 5. Always return PNG (lossless, no quality param needed)
return canvas.toDataURL("image/png");
}
//function to use After setting the signature URL for the first signature widget, clicking on subsequent
//signature widgets should automatically apply and display the signature. apply for initial,signature and stamp widget
export const handleCopySignUrl = (
currentPos,
existSignPosition,
setXyPosition,
xyPosition,
pageNumber,
signerObjectId
) => {
//get current signer details
const currentSigner = xyPosition.filter(
(data) => data.signerObjId === signerObjectId
);
//get current signer placeholder details
const placeholderPosition = currentSigner[0].placeHolder;
//get current pagenumber position
const getcurrentPagePosition = placeholderPosition.find(
(data) => data.pageNumber === pageNumber
);
let getXYdata = getcurrentPagePosition.pos;
const updatePos = getXYdata.map((x) => {
//update widgets sign url details
if (x.key === currentPos.key) {
return {
...x,
// Width: existSignPosition.Width,
// Height: existSignPosition.Height,
SignUrl: existSignPosition.SignUrl,
signatureType: existSignPosition.signatureType,
options: { ...x.options, response: existSignPosition.SignUrl },
...(existSignPosition.typedSignature && {
typeSignature: existSignPosition.typedSignature
})
};
}
return x;
});
const updateXYposition = placeholderPosition.map((obj) => {
if (obj.pageNumber === pageNumber) {
return { ...obj, pos: updatePos };
}
return obj;
});
const newUpdateSigner = xyPosition.map((obj) => {
if (obj.signerObjId === signerObjectId) {
return { ...obj, placeHolder: updateXYposition };
}
return obj;
});
setXyPosition(newUpdateSigner);
};
export const calculateImgAspectRatio = (imgWH, pos) => {
let newWidth, newHeight;
const placeholderHeight = pos && pos.Height ? pos.Height : 60;
const aspectRatio = imgWH.width / imgWH.height;
newWidth = aspectRatio * placeholderHeight;
newHeight = placeholderHeight;
return { newHeight, newWidth };
};
// `onSaveImage` trigger for upload image through stamp, image, signature, initials widgets
export function onSaveImage(
signatureType,
xyPosition,
index,
signKey,
image,
isAutoSign,
widgetsType,
imgUrl,
defaultStampImg,
defaultStampType
) {
let widgetName;
const isSignOrInitials =
widgetsType && ["signature", "initials"].includes(widgetsType);
//get current page position
const getXYData = xyPosition[index].pos;
const updateXYData = getXYData.map((position) => {
if (position.key === signKey) {
widgetName = position?.options?.name;
return {
...position,
SignUrl: imgUrl || image?.src || defaultStampImg,
ImageType: image?.imgType || defaultStampType,
...(isSignOrInitials && { signatureType: signatureType || "" }),
options: {
...position.options,
response: imgUrl || image?.src || defaultStampImg
}
};
}
return position;
});
const updateXYposition = xyPosition.map((obj, ind) => {
if (ind === index) {
return { ...obj, pos: updateXYData };
}
return obj;
});
// condition when user apply auto sign feature for any type upload image (signature,initials,stamp,image)
if (isAutoSign) {
const updatedArray = updateXYposition.map((page) => ({
...page,
pos: page.pos.map(
(item) =>
// below condition is check if image widget name same then apply to auto sign other wise not
// and for other widget like signature,stamp,initials apply auto sign for all remaining widgets
item.type === widgetsType &&
(item.type !== "image" || item.options.name === widgetName)
? {
...item,
SignUrl: image?.src || defaultStampImg,
ImageType: image?.imgType || defaultStampType,
options: {
...item.options,
response: image?.src || defaultStampImg
}
}
: item // Otherwise, keep it unchanged
)
}));
return updatedArray;
} else {
return updateXYposition;
}
}
//function for select image and upload image
export const onImageSelect = (setImage, file) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onloadend = function (e) {
let width, height;
const image = new Image();
image.src = e.target.result;
image.onload = function () {
width = image.width;
height = image.height;
const aspectRatio = 460 / 184;
const imgR = width / height;
if (imgR > aspectRatio) {
width = 460;
height = 460 / imgR;
} else {
width = 184 * imgR;
height = 184;
}
};
image.src = reader.result;
setImage({ src: image.src, imgType: file.type });
};
};
export const compressedFileSize = (file, setImage) => {
// Create a new FileReader instance to read the uploaded file
const reader = new FileReader();
// Event listener triggered when the file is read successfully
reader.onload = (e) => {
// Create a new Image instance to manipulate the image data
const img = new Image();
// Set the image source to the file's data URL (base64 string)
img.src = e.target.result;
// Event listener triggered when the image is fully loaded
img.onload = () => {
// Create a canvas element to resize and compress the image
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d"); // Get the drawing context of the canvas
// Set the canvas dimensions, ensuring they don't exceed 1920x1920
canvas.width = Math.min(img.width, 1920); // Limit width to 1920px
canvas.height = Math.min(img.height, 1920); // Limit height to 1920px
// Draw the image onto the canvas with the specified dimensions
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
// Get the original file type (default to "image/png" if not recognized)
const fileType = "image/png"; //file.type || "image/png"
// Compress the image and convert it into a Blob
canvas.toBlob(
(blob) => {
// Create a new File object from the compressed Blob
const compressedFile = new File([blob], file.name, {
type: fileType, // Set the file type to JPEG
lastModified: Date.now() // Update the last modified timestamp
});
// Pass the compressed file to a custom function for further processing
onImageSelect(setImage, compressedFile);
},
fileType, // Output format for the compressed image
0.3 // Compression quality (30%)
);
};
};
// Read the file as a data URL (base64 string) to be processed
reader.readAsDataURL(file);
};
//convert https url to base64
export const fetchImageBase64 = async (imageUrl) => {
try {
const response = await fetch(imageUrl);
const blob = await response.blob();
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(blob);
reader.onloadend = () => {
const base64data = reader.result;
resolve(base64data);
};
reader.onerror = (error) => {
reject(error);
};
});
} catch (error) {
throw new Error("Error converting URL to base64:", error);
}
};
//function for select image and upload image
export const changeImageWH = async (base64Image) => {
const newWidth = 300;
const newHeight = 120;
return new Promise((resolve, reject) => {
const img = new Image();
img.src = base64Image;
img.onload = async () => {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
canvas.width = newWidth;
canvas.height = newHeight;
ctx.drawImage(img, 0, 0, newWidth, newHeight);
const resizedBase64 = canvas.toDataURL("image/png", 1.0);
resolve(resizedBase64);
};
img.onerror = (error) => {
reject(error);
};
});
};
const getWidgetsFontColor = (type) => {
switch (type) {
case "red":
return rgb(1, 0, 0);
case "black":
return rgb(0, 0, 0);
case "blue":
return rgb(0, 0, 1);
case "yellow":
return rgb(0.9, 1, 0);
default:
return rgb(0, 0, 0);
}
};
export const isBase64 = (str) => {
const base64Pattern =
/^data:([a-zA-Z0-9]+\/[a-zA-Z0-9-.+]+)?;base64,[A-Za-z0-9+/=]+$/;
return base64Pattern.test(str);
};
// Break a long word into smaller parts character-by-character
const forceBreakLongWord = (word, width, font, fontSize) => {
const parts = [];
let current = "";
for (const char of word) {
const lineWidth = font.widthOfTextAtSize(current + char, fontSize);
if (lineWidth <= width) {
current += char;
} else {
parts.push(current);
current = char;
}
}
if (current) parts.push(current);
return parts;
};
export const isEmptyValue = (val) =>
val === null ||
val === undefined ||
(typeof val === "string" && val.trim() === "") ||
(Array.isArray(val) && val.length === 0);
//function for embed all type widgets in document using pdf-lib
export const embedWidgetsToDoc = async (
widgets,
pdfDoc,
signyourself,
scale,
prefillImg
) => {
// `fontBytes` is used to embed custom font in pdf
const fontBytes = await fileasbytes(
"https://cdn.opensignlabs.com/webfonts/times.ttf"
);
pdfDoc.registerFontkit(fontkit);
const font = await pdfDoc.embedFont(fontBytes, { subset: true });
let hasError = false;
for (let item of widgets) {
if (hasError) break; // Stop the outer loop if an error occurred
const typeExist = item.pos.some((data) => data?.type);
let updateItem;
if (typeExist) {
if (signyourself) {
updateItem = item.pos;
} else {
// Checking required and optional widget types
// For both required and optional widgets, handle signurl, defaultValue, and response as the widget's data
// If the widget type is checkbox or radio (whether required or optional), we don't need to validate its value.
// Instead, add an empty checkbox/radio, or if a value exists, mark the checkbox/radio as checked.
updateItem = item.pos.filter(
(data) =>
data?.options?.SignUrl ||
!isEmptyValue(data?.options?.defaultValue) ||
!isEmptyValue(data?.options?.response) ||
data?.type === "checkbox" ||
data?.type === radioButtonWidget
);
}
} else {
updateItem = item.pos;
}
const ImgTypeWidget = [
"signature",
"stamp",
"initials",
"image",
drawWidget
];
const pageNo = item.pageNumber;
const widgetsPositionArr = updateItem;
const pages = pdfDoc.getPages();
const form = pdfDoc.getForm();
const page = pages[pageNo - 1];
// `page.getCropBox()` returns the visible area of the PDF page.
// It provides an object with the following properties:
// - x: the x-coordinate of the lower-left corner of the visible area
// - y: the y-coordinate of the lower-left corner of the visible area
// - width: the width of the visible area (x1 - x0)
// - height: the height of the visible area (y1 - y0)
// If the page does not explicitly define a CropBox, this method returns
// the MediaBox instead, which represents the full physical size of the page.
//
// Example CropBox: [0, 7.92, 612, 799.92]
// => x = 0, y = 7.92, width = 612 - 0 = 612, height = 799.92 - 7.92 = 792
// This is equivalent to `react-pdf`'s `page.view` array: [x0, y0, x1, y1]
const { _, y, width, height } = page.getCropBox();
const originalHeight = height + y;
let images;
const getSize = { height: originalHeight, width: width };
try {
images = await Promise.all(
widgetsPositionArr.map(async (widget) => {
// `SignUrl` this is wrong nomenclature and maintain for older code in this options we save base64 of signature image from sign pad
let signbase64 = widget?.options?.response;
const widgetDims = { Width: widget.Width, Height: widget.Height };
if (signbase64 && ImgTypeWidget.includes(widget.type)) {
if (!isBase64(signbase64) && prefillImg) {
const imgData = prefillImg?.find((x) => x?.id === widget?.key);
signbase64 = imgData?.base64;
}
// let arr = signbase64.split(",");
// const mime = arr[0].match(/:(.*?);/)[1];
const signatureImg = await convertBase64ToImg(
signbase64,
widgetDims
);
const mime = signatureImg?.split(",")?.[0]?.match(/:(.*?);/)?.[1];
const res = await fetch(signatureImg);
const arrayBuffer = await res.arrayBuffer();
const obj = { mimetype: mime, arrayBuffer: arrayBuffer };
return obj;
}
})
);
} catch (e) {
console.log("error in image", e);
}
for (let [id, position] of widgetsPositionArr.entries()) {
const isTextTypeWidget = [
textWidget,
textInputWidget,
cellsWidget,
"name",
"company",
"job title",
"date",
"email"
].includes(position.type);
if (hasError) break; // Stop the inner loop if an error occurred
try {
let img;
if (ImgTypeWidget.includes(position.type)) {
if (images[id].mimetype === "image/png") {
img = await pdfDoc.embedPng(images[id].arrayBuffer);
} else {
img = await pdfDoc.embedJpg(images[id].arrayBuffer);
}
} else if (!position.type) {
// to handle old widget when only stamp and signature are exists
if (images[id].mimetype === "image/png") {
img = await pdfDoc.embedPng(images[id].arrayBuffer);
} else {
img = await pdfDoc.embedJpg(images[id].arrayBuffer);
}
}
let widgetWidth, widgetHeight;
widgetWidth = placeholderWidth(position);
widgetHeight = placeholderHeight(position);
const xPos = (position) => {
const resizePos = position.xPosition;
//first two condition handle to old data already saved from mobile view which scale point diffrent
if (isMobile && position.isMobile) {
//if pos.isMobile false -- placeholder saved from desktop view then handle position in mobile view divided by scale
const x = resizePos * (position.scale / scale);
return x * scale;
} else if (position.isMobile && position.scale) {
const x = resizePos * position.scale;
return x;
} else {
return resizePos;
}
};
const yPos = (position) => {
const resizePos = position.yPosition;
if (position.isMobile && position.scale) {
if (position.IsResize) {
const y = resizePos * position.scale;
return y;
} else {
const y = resizePos * position.scale;
return y;
}
} else {
const yPosition = isTextTypeWidget ? resizePos + 6 : resizePos;
return yPosition;
}
};
const color = position?.options?.fontColor;
const updateColorInRgb = getWidgetsFontColor(color);
const fontSize = parseInt(position?.options?.fontSize || 12);
if (position.type === "checkbox") {
// Determine layout mode: 'vertical' (default) or 'horizontal'
// const isHorizontal = position.layout === "horizontal";
const isHorizontal =
position?.options?.layout === "horizontal" ? true : false;
// Initial “cursor” positions
let currentX = xPos(position);
let currentY = yPos(position) + 2;
// Size and spacing settings
const checkboxSize = fontSize - 1; // checkbox diameter
const checkboxTextGapFromLeft = fontSize + 3.4; // gap between box and its label
const verticalGap = fontSize + 5.5; // gap between two options (vertical layout)
let horizontalGap = 0; // will compute after drawing each label
if (position?.options?.values?.length > 0) {
position.options.values.forEach((item, ind) => {
// 1. Advance the “cursor” on second+ iteration
if (ind > 0) {
if (isHorizontal) {
currentX += horizontalGap;
} else {
currentY += verticalGap;
}
}
// 2. Determine whether this checkbox should be checked
let isCheck = false;
if (
position.options.response &&
position.options.response?.length > 0
) {
isCheck = position.options.response.includes(ind);
} else if (position.options.defaultValue) {
isCheck = position.options.defaultValue.includes(ind);
}
// 3. Draw the label (if labels are not hidden)
if (!position.options.isHideLabel) {
const labelX = currentX + checkboxTextGapFromLeft;
const labelY = currentY - 3;
const optionsPosition = compensateRotation(
page.getRotation().angle,
labelX,
labelY,
1,
getSize,
fontSize,
updateColorInRgb,
font,
page
);
page.drawText(item, optionsPosition);
}
// 4. Create and place the actual checkbox
const checkboxRandomId = "checkbox" + randomId();
const checkbox = form.createCheckBox(checkboxRandomId);
let checkboxObj = {
x: currentX,
y: currentY,
width: checkboxSize,
height: checkboxSize
};
checkboxObj = getWidgetPosition(page, checkboxObj, 1, getSize);
checkbox.addToPage(page, checkboxObj);
// 5. Check or uncheck as needed, then make readonly
if (isCheck) {
checkbox.check();
} else {
checkbox.uncheck();
}
checkbox.enableReadOnly();
// 6. If horizontal layout, compute how far to shift next checkboxcircle
if (isHorizontal) {
// Measure the width of this label text at `fontSize`
const textWidth = font.widthOfTextAtSize(item, fontSize);
// Next checkbox should come after: [box] + gap + [label text] + extra 10pt padding
const gap = position.options?.isHideLabel
? checkboxTextGapFromLeft - 5
: checkboxTextGapFromLeft + textWidth;
horizontalGap = checkboxSize + gap;
}
});
}
} else if (isTextTypeWidget) {
let textContent = "";
if (!isEmptyValue(position?.options?.response)) {
if (
position.type === "date" &&
position.options?.response === "today"
) {
const getFormat = changeDateToMomentFormat(
position?.options?.validation?.format
);
textContent = moment().format(getFormat);
} else {
textContent = position.options?.response;
}
} else if (!isEmptyValue(position?.options?.defaultValue)) {
textContent = position?.options?.defaultValue?.toString();
}
if (position.type === cellsWidget) {
const cellCount =
position?.options?.cellCount || textContent.length || 1;
const charWidth = widgetWidth / cellCount;
const y = yPos(position) - 4;
for (let i = 0; i < cellCount; i++) {
const ch = textContent[i] || "";
const charX =
xPos(position) +
charWidth * i +
(charWidth - font.widthOfTextAtSize(ch, fontSize)) / 2;
const textPosition = compensateRotation(
page.getRotation().angle,
charX,
y,
1,
getSize,
fontSize,
updateColorInRgb,
font,
page
);
if (ch) page.drawText(ch, textPosition);
}
} else {
const fixedWidth = widgetWidth; // Set your fixed
textContent = textContent?.toString();
const isNewOnEnterLineExist = textContent?.includes("\n");
// Function to break text into lines based on the fixed width
const NewbreakTextIntoLines = (textContent, width) => {
const lines = [];
let currentLine = "";
const words = textContent?.split(" ");
for (let word of words) {
const testLine = currentLine ? currentLine + " " + word : word;
const testWidth = font.widthOfTextAtSize(testLine, fontSize);
if (testWidth <= width) {
currentLine = testLine;
} else {
// If single long word
if (font.widthOfTextAtSize(word, fontSize) > width) {
// Break a long word into smaller parts character-by-character
const brokenParts = forceBreakLongWord(
word,
width,
font,
fontSize
);
// push currentLine before breaking word
if (currentLine.trim()) lines.push(currentLine.trim());
// push ALL broken parts in order
brokenParts.forEach((p) => lines.push(p));
currentLine = "";
} else {
// push current line and start new
if (currentLine.trim()) lines.push(currentLine.trim());
currentLine = word;
}
}
}
if (currentLine.trim()) lines.push(currentLine.trim());
return lines;
};
// Function to break text into lines based on when user go next line on press enter button
const breakTextIntoLines = (textContent, width) => {
const finalLines = [];
const paragraphs = textContent.split("\n"); // preserve user order
for (const para of paragraphs) {
const lineWidth = font.widthOfTextAtSize(para, fontSize);
//checking string length to container width
//if string length is less then container width it means user press enter button
if (lineWidth <= width) {
// user forced newline
finalLines.push(para);
} else {
// auto wrap paragraph
finalLines.push(...NewbreakTextIntoLines(para, width));
}
}
return finalLines;
};
//check if text content have `\n` string it means user press enter to go next line and handle condition
//else auto adjust text content according to container width
const lines = isNewOnEnterLineExist
? breakTextIntoLines(textContent, fixedWidth)
: NewbreakTextIntoLines(textContent, fixedWidth);
// Set initial y-coordinate for the first line
let x = xPos(position);
let y = yPos(position) - 4;
// Embed each line on the page
for (const line of lines) {
const textPosition = compensateRotation(
page.getRotation().angle,
x,
y,
1,
getSize,
fontSize,
updateColorInRgb,
font,
page
);
page.drawText(line, textPosition);
y += 18; // Adjust the line height as needed
}
}
} else if (position.type === "dropdown") {
const dropdownRandomId = "dropdown" + randomId();
const dropdown = form.createDropdown(dropdownRandomId);
dropdown.addOptions(position?.options?.values);
if (position?.options?.response) {
dropdown.select(position.options?.response);
} else if (position?.options?.defaultValue) {
dropdown.select(position?.options?.defaultValue);
}
// Define the default appearance string
// Example format: `/FontName FontSize Tf 0 g` where:
// - `/FontName` is the name of the font (e.g., `/Helv` for Helvetica)
// - `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`;
// Set the default appearance for the dropdown field
dropdown.acroField.setDefaultAppearance(defaultAppearance);
dropdown.setFontSize(fontSize);
const dropdownObj = {
x: xPos(position),
y: yPos(position),
width: widgetWidth,
height: widgetHeight
};
const dropdownOption = getWidgetPosition(
page,
dropdownObj,
1,
getSize
);
const dropdownSelected = {
...dropdownOption,
font: font,
textColor: updateColorInRgb
};
dropdown.defaultUpdateAppearances(font);
dropdown.addToPage(page, dropdownSelected);
dropdown.enableReadOnly();
} else if (position.type === radioButtonWidget) {
const radioRandomId = "radio" + randomId();
const radioGroup = form.createRadioGroup(radioRandomId);
//getting radio buttons options text font size
const optionsFontSize = fontSize; // font size for option text
const radioTextGapFromLeft = fontSize + 3; // gap between circle and its label
const radioSize = fontSize; // circle diameter (square of width×height)
// Initial “cursor” positions (from your existing helpers)
let currentX = xPos(position) + 2;
let currentY = yPos(position) + 3;
// Vertical gap between two options
const verticalGap = fontSize + 5;
// Well compute horizontalGap on the fly—after drawing each label
// Initialize to zero (will be set after first option is placed)
let horizontalGap = 0;
// Determine layout mode: 'vertical' or 'horizontal'.
// (You mentioned “add one variable called layout” here we read it from position.layout.)
const isHorizontal =
position?.options?.layout === "horizontal" ? true : false;
// Loop through each option in the group
if (position?.options?.values?.length > 0) {
position.options.values.forEach((item, ind) => {
// 1. Advance cursor on second+ iteration
if (ind > 0) {
if (isHorizontal) {
// Move to the right by horizontalGap
currentX += horizontalGap;
} else {
// Move down by verticalGap (vertical stacking)
currentY += verticalGap;
}
}
// 2. Draw the label text (if not hidden)
if (!position?.options?.isHideLabel) {
// Compute where to draw the text (just to the right of the circle)
const labelX = currentX + radioTextGapFromLeft;
const labelY = currentY - 2;
const optionsPosition = compensateRotation(
page.getRotation().angle,
labelX,
labelY,
1,
getSize,
optionsFontSize,
updateColorInRgb,
font,
page
);
page.drawText(item, optionsPosition);
}
// 3. Place the radiocircle itself at (currentX, currentY)
let radioObj = {
x: currentX,
y: currentY,
width: radioSize,
height: radioSize
};
radioObj = getWidgetPosition(page, radioObj, 1, getSize);
radioGroup.addOptionToPage(item, page, radioObj);
// 4. If horizontal layout, re-compute horizontalGap for next iteration:
if (isHorizontal) {
// Measure how wide the label text is, so we know how far to shift next circle
const textWidth = font.widthOfTextAtSize(item, optionsFontSize);
// radioSize = the circle. radioTextGapFromLeft = gap between circle and label.
// Add a small extra padding (e.g. 10pt) before placing next circle.
const gap = position?.options?.isHideLabel
? radioTextGapFromLeft - 6
: radioTextGapFromLeft + textWidth;
horizontalGap = radioSize + gap;
}
});
}
const isOptionExist = position?.options?.values?.some(
(x) =>
x === position?.options?.response ||
position?.options?.defaultValue
);
if (isOptionExist) {
// 5. Preselect a value if provided
if (position?.options?.response) {
radioGroup.select(position.options?.response);
} else if (position?.options?.defaultValue) {
radioGroup.select(position?.options?.defaultValue);
}
}
// 6. Set to readonly (if required)
radioGroup.enableReadOnly();
} else {
const widgetRotation = position?.options?.rotation || 0;
const isSwapped = [90, 270].includes(widgetRotation);
// Use visual (swapped) dimensions to compute the correct position
const signature = {
x: xPos(position),
y: yPos(position),
width: isSwapped ? widgetHeight : widgetWidth,
height: isSwapped ? widgetWidth : widgetHeight
};
const imageOptions = getWidgetPosition(page, signature, 1, getSize);
if (widgetRotation) {
const pageRotation = page.getRotation().angle;
const combinedRotation = degrees(
(imageOptions.rotate?.angle || pageRotation) - widgetRotation
);
const visualWidth = imageOptions.width;
const visualHeight = imageOptions.height;
if (isSwapped) {
// Override with original (unswapped) dimensions for the actual image
imageOptions.width = visualHeight;
imageOptions.height = visualWidth;
}
imageOptions.rotate = combinedRotation;
// Adjust position to compensate for pdf-lib rotation pivot (bottom-left corner)
if (widgetRotation === 90) {
imageOptions.y += visualHeight;
} else if (widgetRotation === 180) {
imageOptions.x += imageOptions.width;
imageOptions.y += imageOptions.height;
} else if (widgetRotation === 270) {
imageOptions.x += visualWidth;
}
}
page.drawImage(img, imageOptions);
}
} catch (err) {
console.log("Err in embed widget on page ", pageNo, err);
hasError = true; // Set the flag to stop both loops
break; // Exit inner loop
}
}
form?.flatten();
}
if (!hasError) {
const pdfBytes = await pdfDoc.saveAsBase64({ useObjectStreams: false });
return pdfBytes;
} else {
return {
error:
"This pdf is not compatible with opensign please contact <support@opensignlabs.com>"
};
}
};
// function for validating URLs
export function urlValidator(url) {
try {
const newUrl = new URL(url);
return newUrl.protocol === "http:" || newUrl.protocol === "https:";
} catch (err) {
return false;
}
}
//calculate placeholder width to embed in pdf
export const placeholderWidth = (pos) => {
const defaultWidth = defaultWidthHeight(pos.type).width;
const posWidth = pos.Width || defaultWidth;
//condition to handle old data saved from mobile view to get widthh
if (pos.isMobile && pos.scale) {
return pos.IsResize ? posWidth : posWidth * pos.scale;
} else {
return posWidth;
}
};
//calculate placeholder height to embed in pdf
export const placeholderHeight = (pos) => {
const posHeight = pos.Height;
const defaultHeight = defaultWidthHeight(pos.type).height;
const posUpdateHeight = posHeight || defaultHeight;
//condition to handle old data saved from mobile view to get height
if (pos.isMobile && pos.scale) {
return pos.IsResize ? posUpdateHeight : posUpdateHeight * pos.scale;
} else {
return posUpdateHeight;
}
};
//function for getting contracts_contactbook details
export const contactBook = async (objectId) => {
try {
const url = `${localStorage.getItem("baseUrl")}classes/contracts_Contactbook?where={"objectId":"${objectId}"}`;
const headers = {
"Content-Type": "application/json",
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
"X-Parse-Session-Token": localStorage.getItem("accesstoken")
};
const axiosRes = await axios.get(url, { headers });
const result = axiosRes?.data?.results;
return result;
} catch (error) {
console.error("contracts_Contactbook error", err);
return "Error: Something went wrong!";
}
};
//function for getting document details from contract_Documents class
export const contractDocument = async (documentId, include) => {
const data = { docId: documentId, include: include };
const token = { sessionToken: localStorage.getItem("accesstoken") };
const documentDeatils = await axios
.post(`${localStorage.getItem("baseUrl")}functions/getDocument`, data, {
headers: {
"Content-Type": "application/json",
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
...token
}
})
.then((Listdata) => {
const json = Listdata.data;
let data = [];
if (json && json.result.error) {
return json;
} else if (json && json.result) {
data.push(json.result);
return data;
} else {
return [];
}
})
.catch((err) => {
console.log("Err in getDocument cloud function ", err);
return "Error: Something went wrong!";
});
return documentDeatils;
};
//function for add default signature or image for all requested location
export const addDefaultSignatureImg = (xyPosition, defaultSignImg, type) => {
let xyDefaultPos = [];
for (let i = 0; i < xyPosition.length; i++) {
const getXYdata = xyPosition[i].pos;
const getPageNo = xyPosition[i].pageNumber;
const getPosData = getXYdata;
const addSign = getPosData.map((position) => {
if (position.type) {
if (position?.type === type) {
return {
...position,
SignUrl: defaultSignImg,
ImageType: "default",
options: { ...position.options, response: defaultSignImg }
};
}
} else if (position && !position.isStamp) {
return {
...position,
SignUrl: defaultSignImg,
ImageType: "default",
options: { ...position.options, response: defaultSignImg }
};
}
return position;
});
const newXypos = { pageNumber: getPageNo, pos: addSign };
xyDefaultPos.push(newXypos);
}
return xyDefaultPos;
};
//function for get month
export const getMonth = (date) => {
const newMonth = new Date(date).getMonth();
return newMonth;
};
//function to create/copy widget next to already dropped widget
export const handleCopyNextToWidget = (
newId,
position,
xyPosition,
index,
setXyPosition,
pdfOriginalWH,
userId
) => {
let filterSignerPos;
//Get page dimensions
const page = pdfOriginalWH?.find((x) => x?.pageNumber === index);
const pageWidth = page?.width;
const pageHeight = page?.height;
// Widget dimensions (fallback safe values)
const widgetWidth = position?.width || 150;
const widgetHeight = position?.height || 60;
const gap = 10;
//Default copy (slightly right + down)
let newX = parseInt(position.xPosition) + gap;
let newY = parseInt(position.yPosition) + gap;
// Prevent RIGHT overflow
if (pageWidth && newX + widgetWidth > pageWidth) {
newX = parseInt(position.xPosition) - gap;
}
if (newX < 0) newX = 0;
// Prevent BOTTOM overflow
if (pageHeight && newY + widgetHeight > pageHeight) {
newY = pageHeight - widgetHeight - gap;
}
if (newY < 0) newY = 0;
const widgetName = `${position?.type}${randomId(2)}`;
const newposition = {
...position,
xPosition: newX,
yPosition: newY,
key: newId,
options: { ...position?.options, name: widgetName }
};
//Your existing update logic
if (userId) {
filterSignerPos = xyPosition.find((data) => data.Id === userId);
const getPlaceHolder = filterSignerPos?.placeHolder;
const getPageNumer = getPlaceHolder?.filter(
(data) => data.pageNumber === index
);
const getXYdata = getPageNumer?.[0]?.pos || [];
getXYdata.push(newposition);
if (getPageNumer && getPageNumer.length > 0) {
const newUpdateSignPos = getPlaceHolder.map((obj) => {
if (obj.pageNumber === index) {
return { ...obj, pos: getXYdata };
}
return obj;
});
const newUpdateSigner = xyPosition.map((obj) => {
if (obj.Id === userId) {
return { ...obj, placeHolder: newUpdateSignPos };
}
return obj;
});
setXyPosition(newUpdateSigner);
}
} else {
const getPageNumer = xyPosition?.find((data) => data.pageNumber === index);
const getXYdata = getPageNumer?.pos;
getXYdata.push(newposition);
const updatePlaceholder = xyPosition.map((obj, ind) => {
if (obj?.pageNumber === index) {
return { ...obj, pos: getXYdata };
}
return obj;
});
setXyPosition(updatePlaceholder);
}
};
export const getFileName = (fileUrl) => {
if (fileUrl) {
const url = new URL(fileUrl);
const filename = url.pathname.substring(url.pathname.indexOf("_") + 1);
return filename || "";
} else {
return "";
}
};
//fetch tenant app logo from `partners_Tenant` class by domain name
export const getAppLogo = async () => {
const domain = window.location.host;
try {
const tenant = await Parse.Cloud.run("getlogobydomain", {
domain: domain
});
if (tenant) {
const resolvedFavicon =
tenant?.favicon || tenant?.logo || appInfo.fev_Icon;
localStorage.setItem("appname", "OpenSign™");
localStorage.setItem("favicon", appInfo.fev_Icon);
return {
logo: tenant?.logo,
favicon: resolvedFavicon,
user: tenant?.user
};
}
} catch (err) {
console.log("err in getlogo ", err);
localStorage.setItem("favicon", appInfo.fev_Icon);
if (err?.message?.includes("valid JSON")) {
return { logo: appInfo.applogo, user: "exist", error: "invalid_json" };
} else {
return { logo: appInfo.applogo, user: "exist" };
}
}
};
export const getTenantDetails = async (objectId, contactId) => {
try {
const url = `${localStorage.getItem("baseUrl")}functions/gettenant`;
const parseAppId = localStorage.getItem("parseAppId");
const accesstoken = localStorage.getItem("accesstoken");
const token = { "X-Parse-Session-Token": accesstoken };
const data = { userId: objectId, contactId: contactId };
const res = await axios.post(url, data, {
headers: {
"Content-Type": "application/json",
"X-Parse-Application-Id": parseAppId,
...token
}
});
if (res.data.result) {
const updateRes = JSON.parse(JSON.stringify(res.data.result));
return updateRes;
} else {
return "";
}
} catch (err) {
console.log("err in gettenant", err);
return "user does not exist!";
}
};
//function to convert variable string name to variable value of email body and subject
export function replaceMailVaribles(subject, body, variables) {
let replacedSubject = subject;
let replacedBody = body;
for (const variable in variables) {
const regex = new RegExp(`{{${variable}}}`, "g");
if (subject) {
replacedSubject = replacedSubject.replace(regex, variables[variable]);
}
if (body) {
replacedBody = replacedBody.replace(regex, variables[variable]);
}
}
const result = {
subject: replacedSubject,
body: replacedBody
};
return result;
}
export const copytoData = (url) => {
// navigator.clipboard.writeText(text);
if (navigator.clipboard) {
navigator.clipboard.writeText(url);
} else {
// Fallback for browsers that don't support navigator.clipboard
const textArea = document.createElement("textarea");
textArea.value = url;
document.body.appendChild(textArea);
textArea.select();
document.execCommand("copy");
document.body.removeChild(textArea);
}
};
export const convertPdfArrayBuffer = async (url) => {
try {
const response = await fetch(url);
// Check if the response was successful (status 200)
if (!response.ok) {
return "Error";
}
// Convert the response to ArrayBuffer
const arrayBuffer = await response.arrayBuffer();
return arrayBuffer;
} catch (error) {
console.error("Error fetching data:", error);
return "Error";
}
};
//`handleSendOTP` function is used to send otp on user's email using `SendOTPMailV1` cloud function
export const handleSendOTP = async (email) => {
try {
let url = `${localStorage.getItem("baseUrl")}functions/SendOTPMailV1`;
const headers = {
"Content-Type": "application/json",
"X-Parse-Application-Id": localStorage.getItem("parseAppId")
};
const body = {
email: email
};
await axios.post(url, body, { headers: headers });
} catch (error) {
alert(error.message);
}
};
export const fetchUrl = async (url, fileName) => {
try {
const response = await fetch(url);
if (!response.ok) {
alert(i18n.t("something-went-wrong-mssg"));
throw new Error("Network response was not ok");
}
const blob = await response.blob();
saveAs(blob, fileName);
} catch (error) {
alert(i18n.t("something-went-wrong-mssg"));
console.error("Error downloading the file:", error);
}
};
export const getSignedUrl = async (pdfUrl, docId, templateId) => {
//use only axios here due to public template sign
const token = {
"X-Parse-Session-Token": localStorage.getItem("accesstoken")
};
const axiosRes = await axios.post(
`${localStorage.getItem("baseUrl")}/functions/getsignedurl`,
{
url: pdfUrl,
docId: docId || "",
templateId: templateId || ""
},
{
headers: {
"content-type": "Application/json",
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
...token
}
}
);
const url = axiosRes.data.result;
return url;
};
//download base64 type pdf
export const fetchBase64 = async (pdfBase64, pdfName) => {
// Create a Blob from the Base64 string
const byteCharacters = atob(pdfBase64);
const byteNumbers = new Array(byteCharacters.length)
.fill(0)
.map((_, i) => byteCharacters.charCodeAt(i));
const byteArray = new Uint8Array(byteNumbers);
const blob = new Blob([byteArray], { type: "application/pdf" });
// Create a link element
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = pdfName;
// Programmatically click the link to trigger the download
document.body.appendChild(link);
link.click();
// Clean up
document.body.removeChild(link);
URL.revokeObjectURL(link.href);
};
//handle download signed pdf
export const handleDownloadPdf = async (
pdfDetails,
setIsDownloading,
pdfBase64
) => {
const pdfName =
pdfDetails?.[0]?.Name?.length > 100
? pdfDetails?.[0]?.Name?.slice(0, 100)
: pdfDetails?.[0]?.Name || "Document";
const isCompleted = pdfDetails?.[0]?.IsCompleted || false;
const formatId = pdfDetails?.[0]?.ExtUserPtr?.DownloadFilenameFormat;
const docName = buildDownloadFilename(formatId, {
docName: pdfName,
email: pdfDetails?.[0]?.ExtUserPtr?.Email,
isSigned: isCompleted
});
if (pdfBase64) {
await fetchBase64(pdfBase64, docName);
setIsDownloading && setIsDownloading("");
} else {
const pdfUrl = pdfDetails?.[0]?.SignedUrl || pdfDetails?.[0]?.URL;
setIsDownloading && setIsDownloading("pdf");
const docId = pdfDetails?.[0]?.objectId || "";
try {
const url = await getSignedUrl(pdfUrl, docId);
await fetchUrl(url, docName);
setIsDownloading && setIsDownloading("");
} catch (err) {
console.log("err in getsignedurl", err);
setIsDownloading("");
alert(i18n.t("something-went-wrong-mssg"));
}
}
};
export function fileNameWithUnderscore(pdfName) {
// Replace spaces with underscore
return pdfName.replace(/ /g, "_");
}
//function for print digital sign pdf
export const handleToPrint = async (event, setIsDownloading, pdfDetails) => {
event.preventDefault();
setIsDownloading("pdf");
const pdfUrl = pdfDetails?.[0]?.SignedUrl || pdfDetails?.[0]?.URL;
const docId = pdfDetails?.[0]?.objectId || "";
try {
// const url = await Parse.Cloud.run("getsignedurl", { url: pdfUrl });
//`localStorage.getItem("baseUrl")` is also use in public-profile flow for public-sign
//if we give this `appInfo.baseUrl` as a base url then in public-profile it will create base url of it's window.location.origin ex- opensign.me which is not base url
const axiosRes = await axios.post(
`${localStorage.getItem("baseUrl")}/functions/getsignedurl`,
{
url: pdfUrl,
docId: docId
},
{
headers: {
"content-type": "Application/json",
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
"X-Parse-Session-Token": localStorage.getItem("accesstoken")
}
}
);
const url = axiosRes.data.result;
const pdf = await getBase64FromUrl(url);
const isAndroidDevice = navigator.userAgent.match(/Android/i);
const isAppleDevice =
(/iPad|iPhone|iPod/.test(navigator.platform) ||
(navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1)) &&
!window.MSStream;
if (isAndroidDevice || isAppleDevice) {
const byteArray = Uint8Array.from(
atob(pdf)
.split("")
.map((char) => char.charCodeAt(0))
);
const blob = new Blob([byteArray], { type: "application/pdf" });
const blobUrl = URL.createObjectURL(blob);
window.open(blobUrl, "_blank");
setIsDownloading("");
} else {
printModule({ printable: pdf, type: "pdf", base64: true });
setIsDownloading("");
}
} catch (err) {
setIsDownloading("");
console.log("err in getsignedurl", err);
alert(i18n.t("something-went-wrong-mssg"));
}
};
const downloadCertificate = async (certificate, isZip, asBlob) => {
try {
const appName = "OpenSign™";
const certificateUrl = certificate;
if (isZip) {
return certificateUrl;
} else {
if (asBlob) {
const fetchCertificate = await fetch(certificateUrl);
// Convert the response into a Blob
const blob = await fetchCertificate.blob();
saveAs(blob, `Certificate_signed_by_${appName}.pdf`);
return;
}
saveAs(certificateUrl, `Certificate_signed_by_${appName}.pdf`);
}
} catch (err) {
console.error("download certificate err", err);
}
};
//handle download signed pdf
export const handleDownloadCertificate = async (
pdfDetails,
setIsDownloading,
isZip
) => {
const baseUrl = `${localStorage.getItem("baseUrl")}functions`;
const parseAppId = localStorage.getItem("parseAppId");
const sessionToken = localStorage.getItem("accesstoken");
const docId = pdfDetails?.[0]?.objectId;
const initialCertificateUrl = pdfDetails?.[0]?.CertificateUrl;
const headers = {
"Content-Type": "application/json",
"X-Parse-Application-Id": parseAppId
};
if (initialCertificateUrl) {
return await downloadCertificate(initialCertificateUrl, isZip);
} else {
setIsDownloading("certificate");
try {
const data = { docId: docId };
const docDetails = await axios.post(`${baseUrl}/getDocument`, data, {
headers: { ...headers, sessionToken }
});
const cert = docDetails?.data?.result?.CertificateUrl;
if (cert) {
const certificateUrl = await downloadCertificate(cert, isZip);
setIsDownloading("");
return certificateUrl;
} else {
const generateRes = await axios.post(
`${baseUrl}/generatecertificate`,
data,
{ headers }
);
const certificate = generateRes?.data?.result?.CertificateUrl;
if (certificate) {
try {
const certificateUrl = await downloadCertificate(
certificate,
isZip,
true
);
setIsDownloading("");
return certificateUrl;
} catch (err) {
console.error("download certificate err", err);
setIsDownloading("certificate_err");
}
} else {
setIsDownloading("certificate_err");
}
}
} catch (err) {
setIsDownloading("certificate_err");
console.error("download certificate err", err);
alert(i18n.t("something-went-wrong-mssg"));
}
}
return null;
};
// Function to escape special characters in the search string
export function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // Escape special characters
}
export async function findContact(value) {
try {
const baseURL = localStorage.getItem("baseUrl");
const url = `${baseURL}functions/getsigners`;
const token = {
"X-Parse-Session-Token": localStorage.getItem("accesstoken")
};
const headers = {
"Content-Type": "application/json",
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
...token
};
const axiosRes = await axios.post(url, { search: value }, { headers });
const contactRes = axiosRes?.data?.result || [];
if (contactRes) {
const res = JSON.parse(JSON.stringify(contactRes));
return res;
}
} catch (error) {
console.error("Error fetching suggestions:", error);
}
}
// `compensateRotation` is used to calculate x and y position of widget on portait, landscape pdf for pdf-lib
function compensateRotation(
pageRotation,
x,
y,
scale,
dimensions,
fontSize,
updateColorInRgb,
font,
page
) {
// Ensure pageRotation is between 0 and 360 degrees
pageRotation = ((pageRotation % 360) + 360) % 360;
let rotationRads = (pageRotation * Math.PI) / 180;
// Coordinates are from bottom-left
let coordsFromBottomLeft = { x: x / scale };
if (pageRotation === 90 || pageRotation === 270) {
coordsFromBottomLeft.y = dimensions.width - (y + fontSize) / scale;
} else {
coordsFromBottomLeft.y = dimensions.height - (y + fontSize) / scale;
}
let drawX = null;
let drawY = null;
if (pageRotation === 90) {
drawX =
coordsFromBottomLeft.x * Math.cos(rotationRads) -
coordsFromBottomLeft.y * Math.sin(rotationRads) +
dimensions.width;
drawY =
coordsFromBottomLeft.x * Math.sin(rotationRads) +
coordsFromBottomLeft.y * Math.cos(rotationRads);
} else if (pageRotation === 180) {
drawX =
coordsFromBottomLeft.x * Math.cos(rotationRads) -
coordsFromBottomLeft.y * Math.sin(rotationRads) +
dimensions.width;
drawY =
coordsFromBottomLeft.x * Math.sin(rotationRads) +
coordsFromBottomLeft.y * Math.cos(rotationRads) +
dimensions.height;
} else if (pageRotation === 270) {
drawX =
coordsFromBottomLeft.x * Math.cos(rotationRads) -
coordsFromBottomLeft.y * Math.sin(rotationRads);
drawY =
coordsFromBottomLeft.x * Math.sin(rotationRads) +
coordsFromBottomLeft.y * Math.cos(rotationRads) +
dimensions.height;
} else if (pageRotation === 0 || pageRotation === 360) {
// No rotation or full rotation
drawX = coordsFromBottomLeft.x;
drawY = coordsFromBottomLeft.y;
}
if (font) {
return {
x: drawX,
y: drawY,
font,
color: updateColorInRgb,
size: fontSize,
rotate: page.getRotation()
};
} else {
return { x: drawX, y: drawY };
}
}
// `getWidgetPosition` is used to calulcate position of image type widget like x, y, width, height for pdf-lib
function getWidgetPosition(page, image, sizeRatio, getSize) {
let pageWidth;
// pageHeight;
if ([90, 270].includes(page.getRotation().angle)) {
pageWidth = getSize?.height || page.getHeight();
} else {
pageWidth = getSize?.width || page.getWidth();
}
// eslint-disable-next-line
if (!image?.hasOwnProperty("vpWidth")) {
image["vpWidth"] = pageWidth;
}
const pageRatio = pageWidth / (image.vpWidth * sizeRatio);
const imageWidth = image.width * sizeRatio * pageRatio;
const imageHeight = image.height * sizeRatio * pageRatio;
const imageX = image.x * sizeRatio * pageRatio;
const imageYFromTop = image.y * sizeRatio * pageRatio;
const correction = compensateRotation(
page.getRotation().angle,
imageX,
imageYFromTop,
1,
getSize,
imageHeight
);
return {
width: imageWidth,
height: imageHeight,
x: correction.x,
y: correction.y,
rotate: page.getRotation()
};
}
//function to use calculate pdf rendering scale in the container
export const getContainerScale = (pdfOriginalWH, pageNumber, containerWH) => {
const getPdfPageWidth = pdfOriginalWH.find(
(data) => data.pageNumber === pageNumber
);
const containerScale = containerWH?.width / getPdfPageWidth?.width || 1;
return containerScale;
};
//function to get current laguage and set it in local
export const saveLanguageInLocal = (i18n) => {
const detectedLanguage = i18n.language || "en";
localStorage.setItem("i18nextLng", detectedLanguage);
};
// function to get default signature of current user from `contracts_Signature` class
export const getDefaultSignature = async (objectId) => {
try {
if (objectId) {
const result = await Parse.Cloud.run("getdefaultsignature", {
userId: objectId
});
if (result) {
const res = JSON.parse(JSON.stringify(result));
const defaultSignature = res?.ImageURL
? await getBase64FromUrl(res?.ImageURL, true)
: "";
const defaultInitial = res?.Initials
? await getBase64FromUrl(res?.Initials, true)
: "";
const defaultStamp = res?.Stamp
? await getBase64FromUrl(res?.Stamp, true)
: "";
return {
status: "success",
res: {
id: result?.id,
defaultSignature: defaultSignature,
defaultInitial: defaultInitial,
defaultStamp: defaultStamp
}
};
}
} else {
return { status: "error" };
}
} catch (err) {
console.log(
"Error: error in fetch data in contracts_Signature",
err?.message || err
);
return { status: "error" };
}
};
//function to rotate pdf page
export async function rotatePdfPage(rotateDegree, pageNumber, pdfArrayBuffer) {
// Load the existing PDF
const pdfDoc = await PDFDocument.load(pdfArrayBuffer);
// Get the page according to page number
const page = pdfDoc.getPage(pageNumber);
//get current page rotation angle
const currentRotation = page.getRotation().angle;
// Apply the rotation in the counterclockwise direction
let newRotation = (currentRotation + rotateDegree) % 360;
// Adjust for negative angles to keep within 0-359 range
if (newRotation < 0) {
newRotation += 360;
}
page.setRotation(degrees(newRotation));
const pdfbase64 = await pdfDoc.saveAsBase64({ useObjectStreams: false });
//convert base64 to arraybuffer is used in pdf-lib
//pdfbase64 is used to show pdf rotated format
const arrayBuffer = base64ToArrayBuffer(pdfbase64);
//`base64` is used to show pdf
return { arrayBuffer: arrayBuffer, base64: pdfbase64 };
}
export function base64ToArrayBuffer(base64) {
// Decode the base64 string to a binary string
const binaryString = atob(base64);
// Create a new ArrayBuffer with the same length as the binary string
const len = binaryString.length;
const bytes = new Uint8Array(len);
// Convert the binary string to a byte array
for (let i = 0; i < len; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
// Return the ArrayBuffer
return bytes.buffer;
}
export function removeBase64Prefix(base64String) {
return base64String.replace(/^data:.*;base64,/, "");
}
export const convertBase64ToFile = async (pdfName, pdfBase64, imgType) => {
let base64Str = pdfBase64;
const mime = imgType && imgType?.split("/")?.pop();
let fileName =
fileNameWithUnderscore(pdfName) + (imgType ? `.${mime}` : ".pdf");
try {
fileName = imgType ? pdfName : fileName;
const pdfFile = new Parse.File(fileName, { base64: base64Str });
// Save the Parse File if needed
const pdfData = await pdfFile.save();
const pdfUrl = pdfData.url();
const fileRes = await getSecureUrl(pdfUrl);
if (fileRes?.url) {
return fileRes.url;
}
} catch (e) {
console.log("error in convertbase64tofile", e);
}
};
// Shared ref that holds a pending scroll restore job for button zoom.
// RenderPdf reads this in its scale-change useEffect (same pattern as pinch).
export const pendingButtonZoomScrollRef = { current: null };
/**
* Call this with the scroll container ref so the zoom utils can snapshot
* the current scroll position BEFORE setScale triggers a re-render.
*
* Anchors zoom to the visible center of the viewport, so the page content
* under your eyes stays in place rather than jumping to the top.
*/
const captureScrollForButtonZoom = (scrollContainerRef, scale, newScale) => {
const el = scrollContainerRef?.current;
if (!el) return;
const scaleRatio = newScale / scale;
// Anchor point = center of the visible scroll container
const midRelX = el.clientWidth / 2;
const midRelY = el.clientHeight / 2;
const docX = el.scrollLeft + midRelX;
const docY = el.scrollTop + midRelY;
const newScrollLeft = Math.max(0, docX * scaleRatio - midRelX);
const newScrollTop = Math.max(0, docY * scaleRatio - midRelY);
pendingButtonZoomScrollRef.current = { newScrollLeft, newScrollTop };
};
export const onClickZoomIn = (scale, setScale, scrollContainerRef) => {
// Find last step smaller than current scale
const nextScale = SCALE_STEPS.find((s) => s > scale);
if (!nextScale) return;
captureScrollForButtonZoom(scrollContainerRef, scale, nextScale);
setScale(nextScale);
};
export const onClickZoomOut = (scale, setScale, scrollContainerRef) => {
// Find last step smaller than current scale
const prevScale = [...SCALE_STEPS].reverse().find((s) => s < scale);
if (!prevScale || prevScale < 1.0) return;
captureScrollForButtonZoom(scrollContainerRef, scale, prevScale);
setScale(prevScale);
};
//function to use remove widgets from current page when user want to rotate page
export const handleRemoveWidgets = (
setSignerPos,
signerPos,
pageNumber,
setIsRotate
) => {
const isSigners = signerPos.some((data) => data.signerPtr);
//placeholder,template,draftTemplate flow
if (isSigners) {
const updatedSignerPos = signerPos.map((placeholderObj) => {
return {
...placeholderObj,
placeHolder: placeholderObj?.placeHolder?.filter(
(data) => data?.pageNumber !== pageNumber
)
};
});
if (setIsRotate) {
setSignerPos(updatedSignerPos);
setIsRotate({ status: false, degree: 0 });
} else {
//after deleting pdf page we need to update page number of widgets
//For example, consider a PDF with 3 pages where widgets are placed on the 2nd page.
//If we delete the 1st page, the total number of pages will be reduced to 2. In this case,
// the widgets need to be updated to reflect the new page numbering and should now appear on the 1st page.
const updatePageNumber = updatedSignerPos?.map((placeholderObj) => {
return {
...placeholderObj,
placeHolder: placeholderObj?.placeHolder?.map((data) => {
if (data.pageNumber > pageNumber) {
return { ...data, pageNumber: data.pageNumber - 1 };
} else {
return data;
}
})
};
});
setSignerPos(updatePageNumber);
}
} else {
//signyourself flow
const updatedSignerPos = signerPos?.filter(
(data) => data?.pageNumber !== pageNumber
);
if (setIsRotate) {
setSignerPos(updatedSignerPos);
setIsRotate({ status: false, degree: 0 });
} else {
//after deleting pdf page we need to update page number of widgets
//For example, consider a PDF with 3 pages where widgets are placed on the 2nd page.
//If we delete the 1st page, the total number of pages will be reduced to 2. In this case,
// the widgets need to be updated to reflect the new page numbering and should now appear on the 1st page.
const updatePageNumber = updatedSignerPos?.map((data) => {
if (data.pageNumber > pageNumber) {
return { ...data, pageNumber: data.pageNumber - 1 };
} else {
return data;
}
});
setSignerPos(updatePageNumber);
}
}
};
//function to show warning when user rotate page and there are some already widgets on that page
export const handleRotateWarning = (signerPos, pageNumber) => {
const placeholderExist = signerPos?.some((placeholderObj) =>
placeholderObj?.placeHolder?.some((data) => data?.pageNumber === pageNumber)
);
return placeholderExist ? true : false;
};
// `generateTitleFromFilename` to generate Title of document from file name
export function generateTitleFromFilename(filename) {
try {
// Step 1: Trim whitespace
let title = filename.trim();
// Step 2: Remove the file extension (everything after the last '.')
const lastDotIndex = title.lastIndexOf(".");
if (lastDotIndex > 0) {
title = title.substring(0, lastDotIndex);
}
// Step 3: Replace special characters (except Unicode letters, digits, spaces, and hyphens)
title = title.replace(/[^\p{L}\p{N}\s-]/gu, " ");
// Step 4: Replace multiple spaces with a single space
title = title.replace(/\s+/g, " ");
// Step 5: Capitalize first letter of each word (Title Case), handling Unicode characters
title = title.replace(
/\p{L}\S*/gu,
(txt) => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase()
);
// Step 6: Restrict length of title (optional, let's say 100 characters)
if (title.length > 100) {
title = title.substring(0, 100).trim();
}
// Step 7: Handle empty or invalid title by falling back to "Untitled Document"
if (!title || title.length === 0) {
return "Untitled Document";
}
return title;
} catch (error) {
// Handle unexpected errors gracefully by returning a default title
console.error("Error generating title from filename:", error);
return "Untitled Document";
}
}
export const signatureTypes = [
{ name: "draw", enabled: true },
{ name: "typed", enabled: true },
{ name: "upload", enabled: true },
{ name: "default", enabled: true }
];
// `handleSignatureType` is used to return update signature types as per tenant or user
export async function handleSignatureType(tenantSignTypes, signatureType) {
const docSignTypes = signatureType || signatureTypes;
let updatedSignatureType = signatureType || signatureTypes;
if (tenantSignTypes?.length > 0) {
updatedSignatureType = tenantSignTypes?.map((item) => {
const match = docSignTypes.find((data) => data.name === item.name);
return match ? { ...item, enabled: match.enabled } : item;
});
}
return updatedSignatureType;
}
// `formatDateToDdMmmYyyy` is used to format date to dd-mmm-yyyy
export const formatDateToDdMmmYyyy = (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) => {
try {
// 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,
remainingPages: totalPages - 1
};
} else {
return { totalPages: 1 };
}
} catch (err) {
console.log("Err while deleting page", err);
}
};
export const reorderPdfPages = async (pdfArrayBuffer, orderArr) => {
try {
const pdfDoc = await PDFDocument.load(pdfArrayBuffer);
const newPdf = await PDFDocument.create();
const pages = await newPdf.copyPages(
pdfDoc,
orderArr.map((n) => n - 1)
);
pages.forEach((p) => newPdf.addPage(p));
const pdfBase64 = await newPdf.saveAsBase64({ useObjectStreams: false });
const arrayBuffer = base64ToArrayBuffer(pdfBase64);
return { arrayBuffer, base64: pdfBase64, totalPages: orderArr.length };
} catch (err) {
console.log("Err while reordering pages", err);
}
};
// `generatePdfName` is used to generate file name
export function generatePdfName(length) {
const characters =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let result = "";
const charactersLength = characters.length;
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
// Format date and time for the selected timezone
export const formatTimeInTimezone = (date, timezone) => {
const nyDate = timezone && toZonedTime(date, timezone);
const generatedDate = timezone
? format(nyDate, "EEE, dd MMM yyyy HH:mm:ss zzz", { timeZone: timezone })
: new Date(date).toUTCString();
return generatedDate;
};
// `usertimezone` is used to get timezone of current user
export const usertimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
export const handleHeighlightWidget = (
getCurrentSignerPos,
key,
pageNumber
) => {
const placeholder = getCurrentSignerPos.placeHolder;
// Find the highest zIndex value
const highestZIndex = placeholder
.flatMap((item) => item.pos.map((position) => position.zIndex))
.reduce((max, zIndex) => (zIndex > max ? zIndex : max), -Infinity); //-Infinity represents the smallest possible number
// Update the zIndex of the current signer
const updateZindex = placeholder.map((data) => {
if (data.pageNumber === pageNumber) {
return {
...data,
pos: data.pos.map((position) => {
if (position.key === key) {
return { ...position, zIndex: highestZIndex + 1 };
}
return position;
})
};
}
return data;
});
return updateZindex;
};
/**
* FlattenPdf renders field values as static content and removes the interactive
* form layer. Signatures are stripped entirely. Non-widget annotations (links,
* comments, stamps) are preserved.
* @param {string | Uint8Array | ArrayBuffer} pdfFile - pdf file.
* @returns {Promise<Uint8Array>} flatPdf - PDF file in Uint8Array
*/
export const flattenPdf = async (pdfFile) => {
const pdfDoc = await PDFDocument.load(pdfFile, { ignoreEncryption: true });
let form;
try {
form = pdfDoc.getForm();
} catch {
// No form, nothing to flatten
return await pdfDoc.save({ useObjectStreams: false });
}
const pages = pdfDoc.getPages();
const helvetica = await pdfDoc.embedFont(StandardFonts.Helvetica);
const helveticaBold = await pdfDoc.embedFont(StandardFonts.HelveticaBold);
const zapf = await pdfDoc.embedFont(StandardFonts.ZapfDingbats);
const fields = form.getFields();
for (const field of fields) {
const type = field.constructor.name;
if (type === "PDFSignature") {
continue;
}
const widgets = _safeGetWidgets(field);
for (const widget of widgets) {
const rect = _getWidgetRect(widget, pdfDoc);
const page = _getWidgetPage(pdfDoc, pages, widget);
if (!rect || !page) continue;
_drawWidgetBox(page, rect);
if (type === "PDFTextField") {
_drawTextField(page, field, rect, helvetica);
} else if (type === "PDFCheckBox") {
_drawCheckBox(page, field, rect, zapf);
} else if (type === "PDFRadioGroup") {
_drawRadioGroup(page, field, widget, rect);
} else if (type === "PDFDropdown") {
_drawDropdown(page, field, rect, helvetica);
} else if (type === "PDFOptionList") {
_drawOptionList(page, field, rect, helvetica);
} else if (type === "PDFButton") {
// Push buttons are interactive controls, not meaningful data fields.
}
}
}
if (fields.length > 0) {
_removeWidgetAnnotations(pdfDoc);
}
return await pdfDoc.save({ useObjectStreams: false });
};
/* ---- flattenPdf private helpers ---- */
function _safeGetWidgets(field) {
try {
return field.acroField?.getWidgets?.() || [];
} catch {
return [];
}
}
function _getWidgetRect(widget, pdfDoc) {
try {
const r = widget.getRectangle?.();
if (
r &&
isFinite(r.x) &&
isFinite(r.y) &&
isFinite(r.width) &&
isFinite(r.height)
) {
return r;
}
} catch {
/* fall through to manual extraction */
}
try {
const rectArr = widget.dict?.lookup?.(PDFName.of("Rect"));
if (!rectArr || typeof rectArr.size !== "function" || rectArr.size() !== 4)
return null;
const x1 = _numberFromPdfObject(rectArr.get(0));
const y1 = _numberFromPdfObject(rectArr.get(1));
const x2 = _numberFromPdfObject(rectArr.get(2));
const y2 = _numberFromPdfObject(rectArr.get(3));
if ([x1, y1, x2, y2].some((v) => !isFinite(v))) return null;
return {
x: Math.min(x1, x2),
y: Math.min(y1, y2),
width: Math.abs(x2 - x1),
height: Math.abs(y2 - y1)
};
} catch {
return null;
}
}
function _numberFromPdfObject(obj) {
if (!obj) return NaN;
if (typeof obj.asNumber === "function") return obj.asNumber();
if (typeof obj.numberValue === "function") return obj.numberValue();
return Number(obj?.value ?? obj);
}
function _getWidgetPage(pdfDoc, pages, widget) {
try {
const pRef = widget.P?.();
if (pRef) {
for (const page of pages) {
if (page.ref === pRef) return page;
}
}
} catch {
/* fall through */
}
try {
const pRef = widget.dict?.get?.(PDFName.of("P"));
if (pRef) {
for (const page of pages) {
if (page.ref === pRef) return page;
}
}
} catch {
/* fall through */
}
// Fallback: search page annots
try {
for (const page of pages) {
const annots = page.node.lookupMaybe(PDFName.of("Annots"), PDFArray);
if (!annots) continue;
for (let i = 0; i < annots.size(); i++) {
const ref = annots.get(i);
if (ref === widget.ref) return page;
}
}
} catch {
/* fall through */
}
return null;
}
function _drawWidgetBox(page, rect) {
try {
page.drawRectangle({
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
borderWidth: 0.6,
borderColor: rgb(0.65, 0.65, 0.65)
});
} catch {
/* best effort */
}
}
function _drawTextField(page, field, rect, font) {
let text = "";
try {
text = field.getText?.() ?? "";
} catch {
text = "";
}
text = String(text ?? "");
if (!text) return;
let multiline = false;
try {
multiline = field.isMultiline?.() ?? false;
} catch {
/* ignore */
}
let comb = false;
try {
comb = field.isCombed?.() ?? false;
} catch {
/* ignore */
}
if (comb) {
_drawCombText(page, text, rect, font);
return;
}
if (multiline || text.includes("\n")) {
_drawMultilineText(page, text, rect, font);
return;
}
const fontSize = _fitSingleLineFontSize(text, rect, font);
const baselineY = rect.y + Math.max(2, (rect.height - fontSize) / 2);
page.drawText(text, {
x: rect.x + 2,
y: baselineY,
size: fontSize,
font,
color: rgb(0, 0, 0),
maxWidth: Math.max(1, rect.width - 4)
});
}
function _drawMultilineText(page, text, rect, font) {
const lines = String(text).replace(/\r/g, "").split("\n");
const fontSize = Math.max(
8,
Math.min(11, rect.height / Math.max(lines.length + 0.5, 2))
);
const lineHeight = fontSize + 1.5;
let y = rect.y + rect.height - fontSize - 2;
for (const line of lines) {
if (y < rect.y + 1) break;
page.drawText(line, {
x: rect.x + 2,
y,
size: fontSize,
font,
color: rgb(0, 0, 0),
maxWidth: Math.max(1, rect.width - 4),
lineHeight
});
y -= lineHeight;
}
}
function _drawCombText(page, text, rect, font) {
const chars = String(text).split("");
const count = Math.max(chars.length, 1);
const cellWidth = rect.width / count;
const fontSize = Math.max(8, Math.min(12, rect.height - 4));
chars.forEach((ch, i) => {
const textWidth = font.widthOfTextAtSize(ch, fontSize);
const x = rect.x + i * cellWidth + (cellWidth - textWidth) / 2;
const y = rect.y + Math.max(2, (rect.height - fontSize) / 2);
page.drawText(ch, {
x,
y,
size: fontSize,
font,
color: rgb(0, 0, 0)
});
if (i < count - 1) {
try {
page.drawLine({
start: { x: rect.x + (i + 1) * cellWidth, y: rect.y },
end: {
x: rect.x + (i + 1) * cellWidth,
y: rect.y + rect.height
},
thickness: 0.4,
color: rgb(0.75, 0.75, 0.75)
});
} catch {
/* best effort */
}
}
});
}
function _fitSingleLineFontSize(text, rect, font) {
let size = Math.min(12, rect.height - 4);
size = Math.max(size, 6);
while (size > 6) {
const width = font.widthOfTextAtSize(text, size);
if (width <= rect.width - 4) return size;
size -= 0.5;
}
return 6;
}
function _drawCheckBox(page, field, rect, zapf) {
let checked = false;
try {
checked = field.isChecked();
} catch {
checked = false;
}
if (!checked) return;
const size = Math.max(8, Math.min(rect.width, rect.height) - 4);
page.drawText("\u2714", {
x: rect.x + Math.max(1, (rect.width - size * 0.7) / 2),
y: rect.y + Math.max(1, (rect.height - size) / 2),
size,
font: zapf,
color: rgb(0, 0, 0)
});
}
function _drawRadioGroup(page, field, widget, rect) {
let selected = null;
try {
selected = field.getSelected();
} catch {
selected = null;
}
if (!selected) return;
let widgetOnValue = null;
try {
widgetOnValue = widget.getOnValue?.();
} catch {
/* ignore */
}
if (!widgetOnValue) {
try {
const ap = widget.dict?.lookupMaybe?.(PDFName.of("AP"), PDFDict);
const n = ap?.lookupMaybe?.(PDFName.of("N"), PDFDict);
if (n) {
const keys = n.keys();
for (const k of keys) {
const name = k?.decodeText?.() ?? k?.encodedName ?? String(k);
if (name !== "/Off" && name !== "Off") {
widgetOnValue = name.replace(/^\//, "");
break;
}
}
}
} catch {
/* ignore */
}
}
const selectedStr = String(selected).replace(/^\//, "");
const onStr = String(widgetOnValue ?? "").replace(/^\//, "");
if (!onStr || selectedStr !== onStr) return;
// Circle outline
try {
page.drawEllipse({
x: rect.x + rect.width / 2,
y: rect.y + rect.height / 2,
xScale: rect.width / 2 - 1,
yScale: rect.height / 2 - 1,
borderWidth: 0.8,
borderColor: rgb(0, 0, 0)
});
} catch {
/* best effort */
}
// Inner filled dot (drawn as a filled ellipse instead of a text glyph
// so we avoid WinAnsi encoding issues with bullet characters)
try {
const r = Math.min(rect.width, rect.height) / 4;
page.drawEllipse({
x: rect.x + rect.width / 2,
y: rect.y + rect.height / 2,
xScale: r,
yScale: r,
color: rgb(0, 0, 0)
});
} catch {
/* best effort */
}
}
function _drawDropdown(page, field, rect, font) {
let text = "";
try {
const selected = field.getSelected?.();
if (Array.isArray(selected)) {
text = selected.join(", ");
} else {
text = selected ?? "";
}
} catch {
text = "";
}
text = String(text ?? "");
if (!text) return;
const fontSize = _fitSingleLineFontSize(text, rect, font);
page.drawText(text, {
x: rect.x + 2,
y: rect.y + Math.max(2, (rect.height - fontSize) / 2),
size: fontSize,
font,
color: rgb(0, 0, 0),
maxWidth: Math.max(1, rect.width - 12)
});
}
function _drawOptionList(page, field, rect, font) {
let selected = [];
try {
selected = field.getSelected?.() || [];
} catch {
selected = [];
}
if (!Array.isArray(selected)) {
selected = [selected].filter(Boolean);
}
if (!selected.length) return;
const lines = selected.map((v) => String(v));
const fontSize = Math.max(
8,
Math.min(11, rect.height / Math.max(lines.length + 0.5, 2))
);
const lineHeight = fontSize + 1.5;
let y = rect.y + rect.height - fontSize - 2;
for (const line of lines) {
if (y < rect.y + 1) break;
page.drawText(line, {
x: rect.x + 2,
y,
size: fontSize,
font,
color: rgb(0, 0, 0),
maxWidth: Math.max(1, rect.width - 4)
});
y -= lineHeight;
}
}
/** Remove only Widget annotations; preserve links, stamps, comments, etc. */
function _removeWidgetAnnotations(pdfDoc) {
for (const page of pdfDoc.getPages()) {
try {
const annotationsRef = page.node.get(PDFName.of("Annots"));
if (!annotationsRef) continue;
const annotations = pdfDoc.context.lookup(annotationsRef);
if (!annotations || !annotations.asArray) continue;
const filtered = annotations.asArray().filter((annotRef) => {
try {
const annot = pdfDoc.context.lookup(annotRef);
const subtype = annot?.get(PDFName.of("Subtype"));
return subtype?.toString() !== "/Widget";
} catch {
return true;
}
});
if (filtered.length === 0) {
page.node.delete(PDFName.of("Annots"));
} else {
page.node.set(PDFName.of("Annots"), pdfDoc.context.obj(filtered));
}
} catch {
/* best effort */
}
}
try {
pdfDoc.catalog.delete(PDFName.of("AcroForm"));
} catch {
/* best effort */
}
}
export const mailTemplate = (param) => {
const appName = "OpenSign™";
const logo = `<div style='padding:10px'><img src='https://qikinnovation.ams3.digitaloceanspaces.com/logo.png' height='50' /></div>`;
const subject = `${param.senderName} has requested you to sign "${param.title}"`;
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='background:white;padding-bottom:20px'>" +
logo +
`<div style='padding:2px;font-family:system-ui;background-color:${themeColor}'><p style='font-size:20px;font-weight:400;color:white;padding-left:20px'>Digital Signature Request</p></div><div><p style='padding:20px;font-size:14px;margin-bottom:10px'>` +
param.senderName +
" has requested you to review and sign <strong>" +
param.title +
"</strong>.</p><div style='padding: 5px 0px 5px 25px;display:flex;flex-direction:row;justify-content:space-around'><table><tr><td style='font-weight:bold;font-family:sans-serif;font-size:15px'>Sender</td><td></td><td style='color:#626363;font-weight:bold'>" +
param.senderMail +
"</td></tr><tr><td style='font-weight:bold;font-family:sans-serif;font-size:15px'>Organization</td><td></td><td style='color:#626363;font-weight:bold'> " +
param.organization +
"</td></tr><tr><td style='font-weight:bold;font-family:sans-serif;font-size:15px'>Expires on</td><td></td><td style='color:#626363;font-weight:bold'>" +
param.localExpireDate +
"</td></tr><tr><td style='font-weight:bold;font-family:sans-serif;font-size:15px'>Note</td><td></td><td style='color:#626363;font-weight:bold'>" +
param.note +
"</td></tr><tr><td></td><td></td></tr></table></div> <div style='margin-left:70px'><a target=_blank href=" +
param.signingUrl +
"><button style='padding:12px;background-color:#d46b0f;color:white;border:0px;font-weight:bold;margin-top:30px'>Sign here</button></a></div><div style='display:flex;justify-content:center;margin-top:10px'></div></div></div><div><p> This is an automated email from " +
appName +
". For any queries regarding this email, please contact the sender " +
param.senderMail +
" directly.</p></div></div></body></html> ";
return { subject, body };
};
export function formatDateTime(date, dateFormat, timeZone, is12Hour) {
const zonedDate = toZonedTime(date, timeZone); // Convert date to the given timezone
const timeFormat = is12Hour ? "hh:mm:ss a" : "HH:mm:ss";
return dateFormat
? format(
zonedDate,
`${selectFormat(dateFormat)}, ${timeFormat} 'GMT' XXX`,
{ timeZone }
)
: formatTimeInTimezone(date, timeZone);
}
export const updateDateWidgetsRes = (
documentData,
signerId,
journey,
isRemovePrefill
) => {
const contactUser = documentData?.Signers?.find(
(data) => data.objectId === signerId
);
const extUser =
localStorage.getItem("Extand_Class") || JSON.stringify([contactUser]);
let placeHolders = documentData?.Placeholders;
if (isRemovePrefill) {
placeHolders = placeHolders?.filter((x) => x.Role !== "prefill");
}
const userDetails = extUser ? JSON.parse(extUser)[0] : contactUser;
return placeHolders?.map((item) => {
if (item?.signerObjId === signerId || item?.Id === signerId) {
// Sort page number placeholders
const placeHolder = Array.isArray(item.placeHolder)
? [...item.placeHolder]
: [];
const sortedPlaceHolder = placeHolder
.sort((a, b) => a.pageNumber - b.pageNumber)
.map((ph) => {
const sortedWidgets = [...ph.pos]
.sort((a, b) => {
// Sort widgets by Y position (top to bottom) and X position (left to right)
// Treat widgets within 5px Y difference as belonging to the same row
const Y_TOLERANCE = 5;
const yDiff = a.yPosition - b.yPosition;
if (Math.abs(yDiff) <= Y_TOLERANCE) {
return a.xPosition - b.xPosition; // Same row → sort by X
}
return yDiff; // Different rows → sort by Y
})
.map((widget) => {
// Update widget values if needed
if (
["name", "email", "job title", "company"].includes(
widget.type
) &&
!widget.options.defaultValue &&
!widget.options.response &&
userDetails
) {
return {
...widget,
options: {
...widget.options,
response: widgetDataValue(widget.type, userDetails)
}
};
}
return widget;
});
let widgetsWithResponses = sortedWidgets;
return { ...ph, pos: widgetsWithResponses };
});
return {
...item,
placeHolder: applyDuplicateResponsesToPages(sortedPlaceHolder)
};
}
return item;
});
};
//function for show checked checkbox
export const selectCheckbox = (ind, selectedCheckbox) => {
if (selectedCheckbox && selectedCheckbox?.length > 0) {
const isCheck = selectedCheckbox?.some((data) => data === ind);
return isCheck || false;
}
};
export const checkRegularExpress = (validateType, setValidatePlaceholder) => {
switch (validateType) {
case "email":
setValidatePlaceholder("demo@gmail.com");
break;
case "number":
setValidatePlaceholder("12345");
break;
case "text":
setValidatePlaceholder("please enter text");
break;
case "ssn":
setValidatePlaceholder("123-45-6789");
break;
default:
setValidatePlaceholder("please enter value");
}
};
//function to use unlink signer from widgets
export const handleUnlinkSigner = (
signerPos,
setSignerPos,
signersdata,
setSignersData,
uniqueId
) => {
//remove existing signer's details from 'signerPos' array
const updatePlaceHolder = signerPos.map((x) => {
if (x.Id === uniqueId) {
return { ...x, signerPtr: {}, signerObjId: "" };
}
return { ...x };
});
setSignerPos(updatePlaceHolder);
//remove existing signer's details from 'signersdata' array and keep role and id
const updateSigner = signersdata.map((item) => {
if (item.Id == uniqueId) {
return { Role: item.Role, Id: item.Id, blockColor: item.blockColor };
}
return item;
});
setSignersData(updateSigner);
};
//function is used to get pdf original width and height
export const getOriginalWH = async (pdf) => {
let pdfWHObj = [];
//get total page number
const totalPages = pdf?.numPages;
//according to page number get all pdf's pages width and height
for (let index = 0; index < totalPages; index++) {
try {
const getPage = await pdf.getPage(index + 1);
const scale = 1;
//getting extra height which is removed from viewPort height (letter type pdf have originial h-799.92 but using getPage.getViewport getting height-792 something diffrence-7.92px)
//getPage?.view[1] store diffrence of height (Page starts 7.92 from bottom)
const y0 = getPage?.view[1] || 0;
let { width, height } = getPage.getViewport({ scale });
//adding it on viewPort height then get original height ex- 792+7.92 = 799.92 orignial height
height = height + y0;
pdfWHObj.push({ pageNumber: index + 1, width, height });
} catch (e) {
console.log(`Error getting page ${index + 1} of PDF: ${e.message}`);
}
}
return pdfWHObj;
};
//function is used to check required and optional widgets and ensure required widget should be response
export const handleCheckResponse = (checkUser, setminRequiredCount) => {
let checkboxExist,
showAlert = false,
widgetKey,
requiredCheckbox,
tourPageNumber; // `pageNumber` is used to check on which page user did not fill widget's data then change current pageNumber and show tour message on that page
for (let i = 0; i < checkUser[0].placeHolder.length; i++) {
for (let j = 0; j < checkUser[0].placeHolder[i].pos.length; j++) {
//get current page
const updatePage = checkUser[0].placeHolder[i]?.pageNumber;
//checking checbox type widget
checkboxExist = checkUser[0].placeHolder[i].pos[j].type === "checkbox";
//condition to check checkbox widget exist or not
if (checkboxExist) {
//get all required type checkbox
requiredCheckbox = checkUser[0].placeHolder[i].pos.filter(
(position) => {
return (
!position.options?.isReadOnly && position.type === "checkbox"
);
}
);
//if required type checkbox data exit then check user checked all checkbox or some checkbox remain to check
//also validate to minimum and maximum required checkbox
if (requiredCheckbox && requiredCheckbox.length > 0) {
for (let i = 0; i < requiredCheckbox.length; i++) {
//get minimum required count if exit
const minCount =
requiredCheckbox[i].options?.validation?.minRequiredCount;
const parseMin = minCount && parseInt(minCount);
//get maximum required count if exit
const maxCount =
requiredCheckbox[i].options?.validation?.maxRequiredCount;
const parseMax = maxCount && parseInt(maxCount);
//in `response` variable is used to get how many checkbox checked by user
const response = requiredCheckbox[i].options?.response?.length;
//in `defaultValue` variable is used to get how many checkbox checked by default
const defaultValue =
requiredCheckbox[i].options?.defaultValue?.length;
const checkboxValue = response ? response : defaultValue;
//condition to check parseMin and parseMax greater than 0 then consider it as a required check box
if (
parseMin > 0 &&
parseMax > 0 &&
!response &&
!defaultValue &&
!showAlert
) {
showAlert = true;
widgetKey = requiredCheckbox[i].key;
tourPageNumber = updatePage;
setminRequiredCount(parseMin);
}
//else condition to validate minimum required checkbox
else if (
parseMin > 0 &&
(parseMin > checkboxValue || !checkboxValue)
) {
if (!showAlert) {
showAlert = true;
widgetKey = requiredCheckbox[i].key;
tourPageNumber = updatePage;
setminRequiredCount(parseMin);
}
}
}
}
}
//else condition to check all type widget data fill or not except checkbox
else {
//get all required type widgets except checkbox and radio
const requiredWidgets = checkUser[0].placeHolder[i].pos.filter(
(position) => {
return (
position.type === "signature" ||
(position.options?.status === "required" &&
position.type !== "checkbox")
);
}
);
if (requiredWidgets && requiredWidgets?.length > 0) {
let checkSigned;
for (let i = 0; i < requiredWidgets?.length; i++) {
checkSigned = requiredWidgets[i]?.options?.response;
if (isEmptyValue(checkSigned)) {
let checkDefaultSigned =
requiredWidgets[i]?.options?.defaultValue;
if (isEmptyValue(checkDefaultSigned) && !showAlert) {
showAlert = true;
widgetKey = requiredWidgets[i].key;
tourPageNumber = updatePage;
setminRequiredCount(null);
}
}
}
}
}
}
//when showAlert is true then break the loop and show alert to fill required data in widgets
if (showAlert) {
break;
}
}
return { tourPageNumber, widgetKey, showAlert };
};
/**
* decryptPdf
* @param {File} file - The password-protected PDF file to decrypt.
* @param {string | undefined} password - The password used to unlock the PDF.
* @returns {Promise<File>} - A Promise that resolves to a decrypted PDF as a File object.
*
*/
export const decryptPdf = async (file, password) => {
const name = generatePdfName(16);
const baseApi = localStorage.getItem("baseUrl") || "";
const url = removeTrailingSegment(baseApi) + "/decryptpdf?ts=" + Date.now();
let formData = new FormData();
formData.append("file", file);
formData.append("password", password);
const config = {
headers: { "content-type": "multipart/form-data" },
responseType: "blob"
};
const response = await axios.post(url, formData, config);
const pdfBlob = new Blob([response.data], { type: "application/pdf" });
return new File([pdfBlob], name, { type: "application/pdf" });
};
/**
* Convert a Base64 string to a File object.
*
* @param {string} base64String - The Base64 string, with or without the data URI prefix.
* e.g. "data:image/png;base64,iVBORw0KGgoAAAANS…" or "iVBORw0KGgoAAAANS…"
* @param {string} filename - Desired filename for the File object, e.g. "photo.png"
* @returns {File} - The resulting File object
*/
export function base64ToFile(base64String, filename) {
// Separate out the mime-type and the actual Base64 payload
const [header, payload] = base64String.includes(",")
? base64String.split(",")
: [null, base64String];
// Determine the MIME type (fallback to application/octet-stream)
const mimeMatch = header?.match(/data:(.*?);base64/);
const mime = mimeMatch ? mimeMatch[1] : "application/octet-stream";
// Decode Base64 to raw binary data held in a string
const binaryString = atob(payload);
// Create an ArrayBuffer and a view (as unsigned 8-bit)
const len = binaryString.length;
const u8arr = new Uint8Array(len);
for (let i = 0; i < len; i++) {
u8arr[i] = binaryString.charCodeAt(i);
}
// Create a File object (Blob subclass) with the binary data
return new File([u8arr], filename, { type: mime });
}
/**
* Reads the given File object and returns its contents as an ArrayBuffer.
*
* @param {File} file
* The File instance to be read.
* @returns {Promise<ArrayBuffer>}
* A promise that resolves with the files binary data as an ArrayBuffer,
* or rejects with an error if the read fails.
*/
export function getFileAsArrayBuffer(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => resolve(e.target.result);
reader.onerror = (e) => reject(e.target.error);
reader.readAsArrayBuffer(file);
});
}
export const sendEmailToSigners = async (
pdfDetails,
signersdata,
customizeMail,
defaultMail,
isCustomize
) => {
let htmlReqBody;
const owner = pdfDetails?.[0]?.ExtUserPtr;
let sendMail;
const getDocumentExpDate = pdfDetails?.[0]?.ExpiryDate?.iso;
const getTemplateExpDate = new Date(pdfDetails[0]?.createdAt);
getTemplateExpDate.setDate(
getTemplateExpDate.getDate() + (pdfDetails[0]?.TimeToCompleteDays || 15)
);
const expireDate = getDocumentExpDate
? getDocumentExpDate
: getTemplateExpDate;
const newDate = new Date(expireDate);
const localExpireDate = newDate.toLocaleDateString("en-US", {
day: "numeric",
month: "long",
year: "numeric"
});
let senderEmail =
pdfDetails?.[0]?.SenderMail || pdfDetails?.[0]?.ExtUserPtr?.Email;
let senderPhone = pdfDetails?.[0]?.ExtUserPtr?.Phone;
let signerMail = signersdata.slice();
if (pdfDetails?.[0]?.SendinOrder && pdfDetails?.[0]?.SendinOrder === true) {
signerMail = signerMail[0] ? [signerMail[0]] : [];
}
for (let i = 0; i < signerMail.length; i++) {
try {
let url = `${localStorage.getItem("baseUrl")}functions/sendmailv3`;
const headers = {
"Content-Type": "application/json",
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
sessionToken: localStorage.getItem("accesstoken")
};
const objectId = signerMail[i].objectId;
const hostUrl = window.location.origin;
//encode this url value `${pdfDetails?.[0].objectId}/${signerMail[i].Email}/${objectId}` to base64 using `btoa` function
const encodeBase64 = btoa(
`${pdfDetails[0]?.objectId}/${signerMail[i].Email}/${objectId}`
);
let signPdf = `${hostUrl}/login/${encodeBase64}`;
const orgName = pdfDetails[0]?.ExtUserPtr.Company
? pdfDetails[0].ExtUserPtr.Company
: "";
const useNameAsSender =
pdfDetails?.[0]?.ExtUserPtr?.UseNameAsSender === true;
const senderName =
pdfDetails?.[0]?.SenderName || pdfDetails?.[0]?.ExtUserPtr?.Name;
const from =
pdfDetails?.[0]?.SenderName || useNameAsSender
? pdfDetails?.[0]?.ExtUserPtr?.Name || ""
: senderEmail;
const documentName = `${pdfDetails?.[0].Name}`;
let replaceVar;
if (customizeMail && isCustomize) {
const replacedRequestBody = customizeMail?.body.replace(/"/g, "'");
htmlReqBody =
"<html><head><meta http-equiv='Content-Type' content='text/html; charset=UTF-8' /></head><body>" +
replacedRequestBody +
"</body> </html>";
const variables = {
document_title: documentName,
note: pdfDetails?.[0]?.Note,
sender_name: senderName,
sender_mail: senderEmail,
sender_phone: senderPhone || "",
receiver_name: signerMail[i]?.Name || "",
receiver_email: signerMail[i].Email,
receiver_phone: signerMail[i]?.Phone || "",
expiry_date: localExpireDate,
company_name: orgName,
signing_url: signPdf
};
replaceVar = replaceMailVaribles(
customizeMail.subject,
htmlReqBody,
variables
);
} else if (defaultMail?.body && defaultMail?.subject) {
const mailBody = defaultMail?.body;
const mailSubject = defaultMail.subject;
const replacedRequestBody = mailBody.replace(/"/g, "'");
const htmlReqBody =
"<html><head><meta http-equiv='Content-Type' content='text/html; charset=UTF-8' /></head><body>" +
replacedRequestBody +
"</body> </html>";
const variables = {
document_title: documentName,
note: pdfDetails?.[0]?.Note,
sender_name: senderName,
sender_mail: senderEmail,
sender_phone: senderPhone || "",
receiver_name: signerMail[i]?.Name || "",
receiver_email: signerMail[i].Email,
receiver_phone: signerMail[i]?.Phone || "",
expiry_date: localExpireDate,
company_name: orgName,
signing_url: signPdf
};
replaceVar = replaceMailVaribles(mailSubject, htmlReqBody, variables);
}
const mailparam = {
senderName: senderName,
note: pdfDetails?.[0]?.Note || "",
senderMail: senderEmail,
title: documentName,
organization: orgName,
localExpireDate: localExpireDate,
signingUrl: signPdf
};
// Pick a role-appropriate default template (viewers get a "view"
// template instead of the "sign" template).
const defaultTemplate = mailTemplate(mailparam);
let params = {
extUserId: owner?.objectId,
recipient: signerMail[i].Email,
subject: replaceVar?.subject
? replaceVar?.subject
: defaultTemplate.subject,
replyto: senderEmail,
from: from,
html: replaceVar?.body ? replaceVar?.body : defaultTemplate.body
};
sendMail = await axios.post(url, params, { headers: headers });
} catch (error) {
console.log("error", error);
}
}
if (sendMail?.data?.result?.status === "success") {
const sessiontoken = localStorage.getItem("accesstoken");
if (pdfDetails[0]?.objectId && sessiontoken) {
try {
let data;
if (customizeMail && isCustomize) {
data = {
RequestBody: customizeMail?.body,
RequestSubject: customizeMail.subject,
SendMail: true
};
} else if (defaultMail?.body && defaultMail?.subject) {
data = {
RequestBody: defaultMail?.body,
RequestSubject: defaultMail?.subject,
SendMail: true
};
} else {
data = { SendMail: true };
}
const docUrl = `${localStorage.getItem("baseUrl")}classes/contracts_Document`;
await axios.put(`${docUrl}/${pdfDetails[0]?.objectId}`, data, {
headers: {
"Content-Type": "application/json",
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
"X-Parse-Session-Token": sessiontoken
}
});
} catch (error) {
const err = error?.response?.data?.error || error?.message;
console.error("Error while updating doc: ", err);
}
}
return { status: "success" };
} else {
return { status: sendMail?.data?.result?.status };
}
};
/**
* Converts a JPEG/JPG File/Blob into a PNG File.
*
* @param {string} base64Image - A data-URL (e.g. "data:image/jpeg;base64,…")
* @param {string} filename - Desired filename (e.g. "input").
* @returns {Promise<File>} - A promise that resolves with a new PNG File.
*/
export function convertJpegToPng(base64Image, filename) {
if (base64Image) {
const arr = base64Image.split(",");
const mimeMatch = arr[0].match(/:(.*?);/);
if (!mimeMatch) throw new Error("Invalid dataURL");
const mime = mimeMatch[1];
const type = mime.split("/")[1];
if (type === "png") {
return base64Image;
} else {
const bstr = atob(arr[1]);
let n = bstr.length;
const u8arr = new Uint8Array(n);
while (n--) u8arr[n] = bstr.charCodeAt(n);
const inputFile = new File([u8arr], `${filename}.${type}`, {
type: mime
});
return new Promise((resolve, reject) => {
// ensure its JPEG/JPG (you can remove this check if not needed)
if (!/image\/jpe?g/.test(inputFile.type)) {
return reject(new Error("Input must be a JPEG or JPG image"));
}
// Read the file as a data URL
const reader = new FileReader();
reader.onerror = () => reject(new Error("Failed to read the file"));
reader.onload = () => {
const img = new Image();
img.onerror = () => reject(new Error("Failed to load image"));
img.onload = () => {
// draw image onto a canvas
const canvas = document.createElement("canvas");
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0);
// directly get PNG as base64 data URL
try {
const pngDataUrl = canvas.toDataURL("image/png");
resolve(pngDataUrl);
} catch (err) {
reject(new Error("Failed to convert canvas to PNG"));
}
};
img.src = reader.result;
};
reader.readAsDataURL(inputFile);
});
}
}
}
//function is used to get assigned signer's email
export const getSignerEmail = (data, signers) => {
const getEmail =
signers?.length > 0 &&
signers.find((x) => x.objectId === data.signerObjId)?.Email;
return getEmail;
};
//function is used to delete widgets
export const handleDeleteWidget = (key, Id, pageNumber, signerPos) => {
// Find the signer by ID
const signer = signerPos.find((item) => item.Id === Id);
if (!signer || !signer.placeHolder) return signerPos;
const placeHolder = signer.placeHolder;
// Find the page inside placeHolder
const currentPage = placeHolder.find((p) => p.pageNumber === pageNumber);
if (!currentPage) return signerPos;
// Remove widget from the selected page
const updatedPos = currentPage.pos.filter((w) => w.key !== key);
// -------------------------------------------------------
// CASE A → After deletion, page still has widgets then keep that widgets
// -------------------------------------------------------
if (updatedPos.length > 0) {
const updatedPlaceHolder = placeHolder.map((p) =>
p.pageNumber === pageNumber ? { ...p, pos: updatedPos } : p
);
return signerPos.map((s) =>
s.Id === Id ? { ...s, placeHolder: updatedPlaceHolder } : s
);
}
// -------------------------------------------------------
// CASE B → current page becomes empty → remove current page entirely and keep another pages which have widgets
// -------------------------------------------------------
const remainPages = placeHolder.filter((p) => p.pageNumber !== pageNumber);
//`remainPages` keep another pages's data which have widgets
if (remainPages.length > 0) {
const index = signerPos.findIndex((s) => s.Id === Id);
if (index === -1) return signerPos;
const newSignerPos = [...signerPos];
newSignerPos[index] = {
...newSignerPos[index],
placeHolder: remainPages
};
return newSignerPos;
}
// -------------------------------------------------------
// CASE C → No pages have widgets after delete widgets
// Remove placeHolder field for particular signers, EXCEPT prefill role
// -------------------------------------------------------
const updatedData = signerPos
.map((s) => {
if (s.Id === Id && s.Role === "prefill") return null; // remove whole signer
if (s.Id === Id) {
const updated = { ...s };
delete updated.placeHolder;
return updated;
}
return s;
})
.filter(Boolean); // remove nulls (deleted items)
return updatedData;
};
//function to convert formatted date to new Date() format
export const getDefaultDate = (dateStr, format) => {
//get valid date format for moment to convert formatted date to new Date() format
const formats = changeDateToMomentFormat(format);
const parsedDate = moment(dateStr, formats);
let date;
if (parsedDate.isValid()) {
date = new Date(parsedDate.toISOString());
return date;
} else if (dateStr === "today") {
date = new Date();
return date;
}
};
//function to get default format
export const getDefaultFormat = (dateFormat) => dateFormat || "MM/dd/yyyy";
//function to handle widget background color
export const handleBackground = (data, isNeedSign, uniqueId) => {
if (data) {
if (isNeedSign) {
if (data?.Id === uniqueId) {
return data?.blockColor + "b0";
} else {
return "#dedddc";
}
} else {
return data?.blockColor + "b0";
}
} else {
return "rgba(203, 233, 237, 0.69)";
}
};
export function getBase64MimeType(base64) {
const match = base64.match(/^data:(image\/[a-zA-Z0-9+.-]+);base64,/);
return match ? match[1] : null;
}