@foreverrbum/ethsign
Version:
This package will allow you to electronically sign documents within your application
644 lines (584 loc) • 23.5 kB
JavaScript
import _ from 'lodash';
import { truncate } from './dashboard';
import { isMobile } from './window';
export const addField = (type, point = {}, name = '', value = '', flag = {}) => {
// instance.setToolMode('AnnotationCreateTriangle');
const { docViewer, Annotations } = instance;
const annotManager = docViewer.getAnnotationManager();
const doc = docViewer.getDocument();
const displayMode = docViewer.getDisplayModeManager().getDisplayMode();
const page = displayMode.getSelectedPages(point, point);
if (!!point.x && page.first == null) {
return; //don't add field to an invalid page location
}
const page_idx =
page.first !== null ? page.first : docViewer.getCurrentPage();
const page_info = doc.getPageInfo(page_idx);
const page_point = displayMode.windowToPage(point, page_idx);
const zoom = docViewer.getZoom();
var textAnnot = new Annotations.FreeTextAnnotation();
textAnnot.PageNumber = page_idx;
const rotation = docViewer.getCompleteRotation(page_idx) * 90;
textAnnot.Rotation = rotation;
if (rotation === 270 || rotation === 90) {
textAnnot.Width = 20.0;
textAnnot.Height = 140.0;
} else {
textAnnot.Width = 140.0;
textAnnot.Height = 20.0;
}
textAnnot.X = (page_point.x || page_info.width / 2) - textAnnot.Width / 2;
textAnnot.Y = (page_point.y || page_info.height / 2) - textAnnot.Height / 2;
textAnnot.LockedContents = true;
textAnnot.setPadding(new Annotations.Rect(0, 0, 0, 0));
textAnnot.custom = {
type,
value,
flag,
name: `0x345...345 Signs Here`,
};
// set the type of annot
textAnnot.setContents(textAnnot.custom.name);
textAnnot.FontSize = '' + 15.0 + 'px';
textAnnot.TextAlign = 'center';
textAnnot.autoSizeProperties = { expandHeight: true };
textAnnot.TextColor = new Annotations.Color(0, 0, 0);
textAnnot.StrokeThickness = 1;
textAnnot.StrokeColor = new Annotations.Color(211, 211, 211, 0);
textAnnot.TextAlign = 'center';
textAnnot.Author = annotManager.getCurrentUser();
const page_matrix = doc.getPageMatrix(page_idx);
textAnnot.fitText(page_info, page_matrix, rotation);
const pageMatrix = doc.getPageMatrix(page_idx);
const annotRect = textAnnot.getRect;
const canvas = document.createElement('canvas');
var ctx = canvas.getContext("2d");
ctx.setLineDash([5, 3]); /*dashes are 5px and spaces are 3px*/
ctx.beginPath();
ctx.moveTo(0,100);
ctx.lineTo(3000, 100);
ctx.stroke();
// textAnnot.draw(ctx, pageMatrix)
annotManager.deselectAllAnnotations();
annotManager.addAnnotation(textAnnot, true);
annotManager.redrawAnnotation(textAnnot);
annotManager.selectAnnotation(textAnnot);
};
export const redrawSignHereWidget = async (annots, instance, ethAccount, ethAlias, ethAvatar) => {
const {annotManager, docViewer} = instance;
const page_idx = docViewer.getCurrentPage();
const rotation = docViewer.getCompleteRotation(page_idx)*-90;
const signer = {
rotation: rotation,
address: ethAccount,
alias: ethAlias,
avatar: ethAvatar,
}
let annotsToDraw = []
await Promise.all(
annots.map(async (annot, index) => {
const {inputAnnot} = await replace(instance, annot, index, signer)
// draw the annotation the viewer
annotManager.addAnnotation(inputAnnot);
annotsToDraw.push(inputAnnot);
})
)
await annotManager.drawAnnotationsFromList(annotsToDraw);
}
export const replace = async (instance, annot, index, signer) => {
//annot is the original signature field
let field, inputAnnot;
const {Annotations, annotManager} = instance;
try{
field = annot.getField()
}catch(e){
field = new Annotations.Forms.Field(
"EthSign Signature Field" + Date.now() + index,
{
type: 'Sig'
},
);
}
inputAnnot = new Annotations.SignatureWidgetAnnotation(field, {
appearance: '_DEFAULT',
appearances: {
_DEFAULT: {
Normal: {
data:
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAYdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjEuMWMqnEsAAAANSURBVBhXY/j//z8DAAj8Av6IXwbgAAAAAElFTkSuQmCC',
offset: {
x: 100,
y: 100,
},
},
},
},
});
inputAnnot.Author = annotManager.getCurrentUser();
if (signer){
inputAnnot.Index = index
inputAnnot.Signer = signer
}else{
inputAnnot.Signer = annot.Signer;
inputAnnot.Index = annot.Index
}
const {rotation} = inputAnnot.Signer
inputAnnot.createSignHereElement = () => {
const div = document.createElement('div');
div.style.height = "100%";
div.style.width = "100%";
div.style.cursor = 'pointer';
div.style.border = '1px dashed #E98234';
div.style.transform = `rotate(${rotation}deg)`
let inlineImg = `<img src="assets/sign_here.svg" style="width:inherit; height:100%;"></img>`;
div.innerHTML = inlineImg;
return div;
}
// set position
inputAnnot.PageNumber = annot.getPageNumber();
if ( rotation === -90 || rotation === -270) {
inputAnnot.X = annot.getX() - annot.getHeight()/2 + annot.getWidth()/2;
inputAnnot.Y = annot.getY() + annot.getHeight()/2 - annot.getWidth()/2;
inputAnnot.Width = annot.getHeight();
inputAnnot.Height = annot.getWidth();
} else {
inputAnnot.X = annot.getX();
inputAnnot.Y = annot.getY();
inputAnnot.Width = annot.getWidth();
inputAnnot.Height = annot.getHeight();
}
return ({inputAnnot,field});
}
export const applyFields = async (sigfields, instance, ethAccount, signatureData) => {
const { docViewer } = instance;
const annotManager = docViewer.getAnnotationManager();
const fieldManager = annotManager.getFieldManager();
const annotationsList = sigfields;
// const annotationsList = annotManager.getAnnotationsList();
const annotsToDelete = [];
const annotsToDraw = [];
await Promise.all(
annotationsList.map(async (annot, index) => {
annot.ReadOnly = true
try{
const {address} = annot.Signer
const sigDataIdx = _.findIndex(signatureData, {signer:address})
if(sigDataIdx>-1){
const signed = signatureData[sigDataIdx]?.fieldSigned[annot.Index]
if (signed == true ){
annotsToDelete.push(annot);
} else if (address.toLowerCase() == ethAccount) {
// if (typeof annot.custom !== 'undefined') {
// create a form field based on the type of annotation
const {inputAnnot, field} = await replace(instance, annot, index)
// delete original annotation
annotsToDelete.push(annot);
// draw the annotation the viewer
annotManager.addAnnotation(inputAnnot);
fieldManager.addField(field);
annotsToDraw.push(inputAnnot);
}
}
}catch(err){
console.log(err)
// errors might be caused by contracts made before we implemented the indexing feature
}
}),
);
// delete old annotations
annotManager.deleteAnnotations(annotsToDelete, null, true);
// refresh viewer
await annotManager.drawAnnotationsFromList(annotsToDraw);
};
export const isSignersWithFields = (signers, annotList) => {
var fields = _.remove(annotList, function(n) {
return n.Subject == "SignatureField" ;
});
// setting indexes of fields
let addresses = [];
fields.map((field)=>{
const index = _.indexOf(addresses, field.Signer.address);
if (index == -1){ //address isn't added yet
addresses.push(field.Signer.address)
}
})
_.pullAll(signers, addresses)
return(signers.length==0)
}
export const styleWidgets = async (instance, widgets) => {
const { annotManager, Annotations} = instance;
const {Color} = Annotations;
await Promise.all(
widgets.map(async (widget, index) => {
widget.font.strokeColor = new Color(255,0,0)
annotManager.redrawAnnotation(widget)
})
)
}
export const getxfdfString = async (instance, annotList) => {
if(!instance) {
return {
sigFieldXFDF: '',
annotationsXFDF: '',
addresses: [],
count: 0
}
}
const {annotManager} = instance
var fields = _.remove(annotList, function(n) {
return n.Subject == "SignatureField" ;
});
// setting indexes of fields
let addresses = [];
let count = []
fields.map((field)=>{
const index = _.indexOf(addresses, field.Signer.address);
if (index == -1){ //address isn't added yet
addresses.push(field.Signer.address)
count.push(1)
field.Index = 0;
}else{
field.Index = count[index];
count[index] = count[index] + 1
}
})
const xfdfStringSigFields = await annotManager.exportAnnotations({ annotList: fields });
const xfdfStringAnnots = await annotManager.exportAnnotations({ annotList: annotList });
return {
sigFieldsXFDF: xfdfStringSigFields,
annotationsXFDF: xfdfStringAnnots,
addresses: addresses,
count: count
}
}
export const updateXFDF = async (instance, annotList, handleAnnotations, updateStoredData) => {
const {annotManager} = instance
const xfdfStringAnnots = await annotManager.exportAnnotations({ annotList: annotList });
handleAnnotations(xfdfStringAnnots)
updateStoredData(null, xfdfStringAnnots)
}
export const getStringFromDecryptedArr = async (decryptedArr, formatMessage) => {
const blob = new Blob([decryptedArr], { type: 'text/plain' });
const reader = new window.FileReader();
return new Promise((resolve, reject) => {
reader.onload = async (fileEvent) => {
const xfdf = fileEvent.target.result
resolve(xfdf)
}
reader.onerror = () => {
reject(formatMessage({id: 'OOPS_SOMETHING_WENT_WRONG'}));
}
reader.readAsText(blob)
})
}
const areFieldsFilled = (field) => {
const { value } = field;
let filled = true;
if (field.children.length>0){
field.children.forEach((childField)=>{
filled = filled && areFieldsFilled(childField)
});
} else{
filled = value!=="";
}
return filled;
}
export const getFields = (annotManager) => {
let fields = []
annotManager.getFieldManager().forEachField((field)=>{
fields.push(field)
});
return fields;
}
// This fx is for debugging purposes only
const displayFieldNameAndValue = (field) => {
// insert this in code
// annotManager.getFieldManager().forEachField((field)=>{
// displayFieldNameAndValue(field)
// });
const { name, value } = field;
if(field.children.length==0){
console.log(`Name: ${name}, Value: ${value}`);
}
// Check children fields
field.children.forEach(displayFieldNameAndValue);
}
export const handleDocChanges = async (instance, handleFieldsByPage, handleNumOfSigned, handleNewChanges, savedAnnotations, handleFieldChanges, handleAllFieldsFilled, ethAccount) => {
const {annotManager} = instance
annotManager.on('annotationChanged', async (annotations, action, { imported }) => {
const annotList = annotManager.getAnnotationsList();
const signatures = annotations.filter(a => a.Subject=="Signature")
if(!imported){
const annotationList = [...annotManager.getAnnotationsList()]
_.pullAll(annotationList, savedAnnotations);
handleNewChanges(annotationList.length > 0)
}
if(signatures.length>0){
if(action=="add"){
signatures.map(sig => {
// get widget of the added signature annotation first and then get the field of the widget to set the field value
const field = annotList.filter(a => a.annot == sig)[0].getField();
field.setValue('signed')
})
updateSignatureFinder(annotManager, handleFieldsByPage, handleNumOfSigned, ethAccount)
}
}
});
if(handleFieldChanges){
annotManager.on('fieldChanged', (field, value) => {
handleFieldChanges(true)
if(handleAllFieldsFilled){
handleAllFieldsFilled(areAllFieldsFilled(annotManager))
}
});
}
}
export const areAllFieldsFilled = (annotManager) =>{
const fieldManager = annotManager.getFieldManager();
let filled = true;
fieldManager.forEachField((field)=>{
filled = filled && areFieldsFilled(field)
});
return filled;
}
export const checkAndUpdateAnnotList = async (file, filename, handleWebviewer, viewer, ethAlias, ethAccount, storedAnnotations, signers ) => {
// invisible webviewer to generate annotList
return new Promise((resolve, reject) => {
WebViewer(
{
licenseKey: '7m4ej8GFJuRlJGHs8B5l',
path: '/webviewer/lib',
disabledElements: [
'header',
'toolsHeader',
]
},
viewer.current,
).then( (instance) => {
handleWebviewer(instance);
if (file != null) {
instance.loadDocument(file, { filename: filename.name+filename.ext })
}
const { Annotations, Tools, annotManager, docViewer, CoreControls } = instance;
instance.openElements(['leftPanel']);
annotManager.setCurrentUser(ethAlias? ethAlias:ethAccount)
// new code
signers.map((signer, index) => {
class SigFieldTxtAnnotation extends Annotations.CustomAnnotation {
constructor() {
super('SigFieldTxt'); // provide the custom XFDF element name
const page_idx = docViewer.getCurrentPage();
const rotation = docViewer.getCompleteRotation(page_idx)*-90;
this.PageRotation = rotation;
// TODO: add index in Sending.js
this.Subject = "SignatureField"
this.Signer = {
address: signer.address,
rotation: rotation,
alias: signer.alias,
avatar: signer.avatar
};
this.Index = 0;
this.Address = signer.alias? truncate(signer.alias) : truncate(signer.address);
this.Signed = false;
this.vertices = [];
const numVertices = 4;
for (let i = 0; i < numVertices; ++i) {
this.vertices.push(new CoreControls.Math.Point());
}
this.selectionModel = SigFieldSelectionModel;
}
serialize(element, pageMatrix) {
this.setCustomData('vertices', this.vertices);
const el = super.serialize(element, pageMatrix);
// create an attribute to save the vertices list
el.setAttribute('Signer', JSON.stringify(this.Signer));
el.setAttribute('Index', this.Index)
el.setAttribute('Address', this.Address)
el.setAttribute('Signed', this.Signed)
return el;
}
deserialize(element, pageMatrix) {
super.deserialize(element, pageMatrix);
this.Signer = JSON.parse(element.getAttribute('Signer'));
this.Index = element.getAttribute('Index')
this.Address = element.getAttribute('Address')
this.Signed = element.getAttribute('Signed')
const storedVertices = this.getCustomData('vertices');
this.vertices = storedVertices.map(v => new CoreControls.Math.Point(v.x, v.y)); }
draw(ctx, pageMatrix) {
this.setStyles(ctx, pageMatrix);
ctx.beginPath();
ctx.setLineDash([5,3]);
ctx.moveTo(this.vertices[0].x, this.vertices[0].y);
ctx.lineTo(this.vertices[1].x, this.vertices[1].y);
ctx.lineTo(this.vertices[2].x, this.vertices[2].y);
ctx.lineTo(this.vertices[3].x, this.vertices[3].y);
ctx.closePath();
ctx.stroke();
ctx.fillStyle = "black";
const zoom = docViewer.getZoom();
const textAnnot = this.Address+" Signs Here"
ctx.save()
ctx.textBaseline = 'middle';
ctx.textAlign = 'center';
ctx.rotate(this.PageRotation/180*Math.PI);
if (this.PageRotation == -90 || this.PageRotation == -270){
const textHeight = this.Height/11;
ctx.font = `${textHeight}px san-serif`;
if(this.PageRotation == -90){
ctx.fillText(textAnnot, -this.vertices[0].y-((this.Height/2)), this.vertices[0].x+(this.Width/2));
}else{
ctx.fillText(textAnnot, this.vertices[0].y+(this.Height/2), -this.vertices[0].x-(this.Width/2));
}
}
else{
const textHeight = this.Width/11;
ctx.font = `${textHeight}px san-serif`;
if(this.PageRotation == - 180){
ctx.fillText(textAnnot, -this.vertices[0].x+((this.Width/2)-this.Width), -this.vertices[0].y+(this.Height/2)-this.Height);
}
else{
ctx.fillText(textAnnot, this.vertices[0].x+(this.Width/2), this.vertices[0].y+this.Height/2);
}
}
ctx.stroke();
}
resize(rect) {
const annotRect = this.getRect();
const deltaX = rect.x1 - annotRect.x1;
const deltaY = rect.y1 - annotRect.y1;
this.vertices = this.vertices.map((vertex) => {
vertex.translate(deltaX, deltaY);
return vertex;
});
this.setRect(rect);
}
}
class SigFieldSelectionModel extends Annotations.SelectionModel {
constructor(annotation, canModify) {
super(annotation, canModify);
if (canModify) {
const controlHandles = this.getControlHandles();
controlHandles.push(new SigFieldControlHandle(annotation, 0));
controlHandles.push(new SigFieldControlHandle(annotation, 1));
controlHandles.push(new SigFieldControlHandle(annotation, 2));
controlHandles.push(new SigFieldControlHandle(annotation, 3));
}
}
}
// this is necessary to set the elementName before instantiation
SigFieldTxtAnnotation.prototype.elementName = 'SigFieldTxt';
SigFieldTxtAnnotation.SerializationType = Annotations.CustomAnnotation.SerializationTypes.CUSTOM;
// register the annotation type so that it can be saved to XFDF files
annotManager.registerAnnotationType(SigFieldTxtAnnotation.prototype.elementName, SigFieldTxtAnnotation);
class SigFieldTxtCreateTool extends Tools.GenericAnnotationCreateTool {
constructor(docViewer) {
// TriangleAnnotation is the class (function) for our annotation we defined previously
super(docViewer, SigFieldTxtAnnotation);
}
mouseMove(e) {
super.mouseMove(e);
if (this.annotation) {
this.annotation.vertices[0].x = this.annotation.X;
this.annotation.vertices[0].y = this.annotation.Y;
this.annotation.vertices[1].x = this.annotation.X + this.annotation.Width;
this.annotation.vertices[1].y = this.annotation.Y;
this.annotation.vertices[2].x = this.annotation.X + this.annotation.Width;
this.annotation.vertices[2].y = this.annotation.Y + this.annotation.Height;
this.annotation.vertices[3].x = this.annotation.X;
this.annotation.vertices[3].y = this.annotation.Y + this.annotation.Height;
// update the annotation appearance
annotManager.redrawAnnotation(this.annotation);
}
}
};
const SigFieldTxtToolName = 'AnnotationCreateSigFieldTxt' + index;
const SigFieldTxtTool = new SigFieldTxtCreateTool(docViewer);
instance.registerTool({
toolName: SigFieldTxtToolName,
toolObject: SigFieldTxtTool,
buttonImage: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">' +
'<path d="M12 7.77L18.39 18H5.61L12 7.77M12 4L2 20h20L12 4z"/>' +
'<path fill="none" d="M0 0h24v24H0V0z"/>' +
'</svg>',
buttonName: 'SigFieldTxtToolButton',
tooltip: 'SigFieldTxt'
}, SigFieldTxtAnnotation);
instance.setHeaderItems((header) => {
header.getHeader('toolbarGroup-Shapes').get('freeHandToolGroupButton').insertBefore({
type: 'toolButton',
toolName: SigFieldTxtToolName
});
});
});
docViewer.on('documentLoaded', () => {
instance.annotManager.importAnnotations(storedAnnotations)
.then(async (importedAnnotations) => {
resolve(importedAnnotations)
});
});
});
})
}
export const resetStoredData = (handleStoredData) => {
handleStoredData({
createProgress: 0,
progress: 0,
originalFilename: '',
filename:'',
signers: null,
indicatorProgress: 0,
storageHash: null,
storageProvider: 'FL',
annotations: null,
documentKey: null,
password: '',
expiryBlock: 0,
withPassword: true,
withExpiry: false
})
}
export const getDefaultTool = () => {
return (isMobile()) ? 'Pan' : 'AnnotationEdit';
}
export const getShortenedName = (doc) => {
if(doc.name.length + doc.ext.length >= 32) {
doc.name = doc.name.slice(0, 31 - doc.ext.length);
// Our file name can only be 32 characters long WITH NULL TERMINATOR, including extension. Do calculation below and slice it.
}
return doc;
}
export const updateSignatureFinder = (annotManager, handleFieldsByPage, handleNumOfSigned, ethAccount) => {
const mySigFields = [...annotManager.getAnnotationsList().filter((annot) => (annot.Subject == "Widget" && annot.Signer != null) || (annot.Subject == "Signature" && annot.getCustomData('Signer').address == ethAccount))]
if (mySigFields!=null){
const temp_fieldsByPage = []
let temp_numOfSigned = 0
mySigFields.map((annot, idx)=>{
const signed = (annot.Subject == "Widget" && annot.annot != null) || (annot.Subject == "Signature" && annot.getCustomData('Signer') != null)
const pageNumberAdded = _.findIndex(temp_fieldsByPage, {'page':annot.PageNumber})
if (pageNumberAdded == -1){ // page number has not been added yet
if (signed == true){
temp_numOfSigned = temp_numOfSigned + 1;
}
temp_fieldsByPage.push({
page: annot.PageNumber,
annots: [annot],
length: 1,
signed: signed
})
}else{ // page number is already added
// currently length is number of not signed fields
temp_fieldsByPage[pageNumberAdded].annots.push(annot);
temp_fieldsByPage[pageNumberAdded].length = temp_fieldsByPage[pageNumberAdded].length + 1;
if (signed == true){
temp_numOfSigned = temp_numOfSigned + 1;
}
temp_fieldsByPage[pageNumberAdded].signed = temp_fieldsByPage[pageNumberAdded].signed && signed
}
})
const newFields = _.orderBy(temp_fieldsByPage, ['page'], ['asc']);
handleNumOfSigned(temp_numOfSigned)
handleFieldsByPage(newFields)
}
}