UNPKG

@foreverrbum/ethsign

Version:

This package will allow you to electronically sign documents within your application

644 lines (584 loc) 23.5 kB
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) } }