UNPKG

@foreverrbum/ethsign

Version:

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

372 lines (341 loc) 15.4 kB
import React, { useEffect, useRef, useState } from 'react'; import WebViewer from '@pdftron/webviewer'; import { withRouter } from 'react-router-dom'; import { createAndSign, makeSignaturesReadOnly } from '../helpers/sign'; import { useIntl } from 'react-intl'; import { getCommentArrs } from '../helpers/download'; import { applyFields, getStringFromDecryptedArr, redrawSignHereWidget, styleWidgets, handleDocChanges, areAllFieldsFilled, getShortenedName, updateSignatureFinder} from '../helpers/pdf'; import SignatureFinder from './PDF/SignatureFinder'; import Alert from './Alert'; import ProgressBarIndicator from './Dashboard/ProgressBarIndicator'; import TitleBar from './Sign/TitleBar'; import { signAndSaveComments } from '../helpers/sign'; import _ from 'lodash'; const Sign = (props) => { const [instance, setInstance] = useState(null); const [fileArr, handleFileArr] = useState(null); const [mySigFields, handleMySigFields] = useState(null); // all fields const [doc, handleDoc] = useState(null); const { newDocument, allFieldsRequiredBeforeSave, callback , errorCallback, bar, initialSigners, handleActivePage, provider, contract, location, ethAlias, ethAccount, ethAvatar, handleData, networkId, reloadContractDetails } = props; const viewer = useRef(null); const [loaded, handleLoaded] = useState(false); const [updatedSignatures, handleUpdatedSignatures] = useState(false); const [signed, handleSigned] = useState(null) const [saving, handleSaving] = useState(false) const [signing, handleSigning] = useState(false) const [signer, handleSigner] = useState(null) const [idx, handleIdx] = useState(null); const [docPage, handleDocPage] = useState(1) const [saveStatus, handleSaveStatus] = useState(null); const [savedAnnotations, handleSavedAnnotations] = useState([]) const [newChanges, handleNewChanges] = useState(false) const [fieldChanges, handleFieldChanges] = useState(false) const [allFieldsFilled, handleAllFieldsFilled] = useState(false) const [signProgress, handleSignProgress] = useState(0); const [password, handlePassword] = useState(''); const [fieldsByPage, handleFieldsByPage] = useState(null) const [numOfSigned, handleNumOfSigned] = useState(0) const { formatMessage, locale } = useIntl(); const signContract = async () => { if(!saving && ( newChanges || fieldChanges)) { handleSaving(true); const {annotManager} = instance; const signaturesToSave = annotManager.getAnnotationsList().filter( annot => annot.Subject == "Widget" && annot.annot != null && annot.annot.getCustomData('Signer')=='' ) handleSigning(signaturesToSave.length>0) const user = ethAlias ? ethAlias : ethAccount handleSignProgress(0) if (newDocument){ //TODO: create document first and then sign /* * progress (aggregated): * 1 - aggregateHandleDocumentUpload completed * 2 - aggregateNewBasicDocumentAndSetStorage completed * 3 - aggregateAddSignersAndNumOfSigFieldsForDocument completed * 4 - (optional) completed self-signing * 5 - Feedback survey completed * 6 - Notification completed for successful creation */ await createAndSign(provider, ethAccount, instance, signaturesToSave, formatMessage, contract, handleSaveStatus, password, doc.name, initialSigners, bar, handleSignProgress, user, networkId, callback, errorCallback, handleSavedAnnotations, handleNewChanges ) }else{ await signAndSaveComments(annotManager, signaturesToSave, provider, ethAccount, contract, doc, password, user, async () => { await handleData(doc.documentKey, idx, 1) }, null, formatMessage, handleSaveStatus, handleSavedAnnotations, handleNewChanges, signProgress, handleSignProgress) } handleSaving(false); if(reloadContractDetails){ reloadContractDetails(doc.documentKey, idx); } } } useEffect(() => { (async () => { if(handleActivePage){ handleActivePage('sign'); } let temp_doc, temp_password; if (!newDocument && location.state !== undefined && location.state.doc && location.state.idx != null && location.state.fileArr) { temp_doc = location.state.doc temp_password = location.state.password handleDoc(temp_doc) handleIdx(location.state.idx) handleFileArr(location.state.fileArr) } else if (!newDocument) { props.history.push({ pathname: '/contracts' }) } // set signed and signer WebViewer( { initialDoc: newDocument? newDocument.url:'', licenseKey: 'Buildblock Tech Pte. Ltd.:OEM:EthSign::B+:AMS(20220926):60A5E4AD0457F80A7360B13AC982537860615F858748CDEA9BF51DE6240C48AE4AB4B6F5C7', path: '/webviewer/lib', disabledElements: [ 'header', 'toolsHeader', ] }, viewer.current, ).then((instance) => { if(locale=="zh"){ instance.setLanguage('zh_cn'); } setInstance(instance); const {docViewer, annotManager, Annotations} = instance instance.openElements(['notesPanel']); annotManager.setCurrentUser(ethAlias? ethAlias:ethAccount) 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; this.Subject = "SignatureField" this.Listable = false; } get isSigned() { return this.Signed; } set isSigned(signed) { this.Signed = signed; } serialize(element, pageMatrix) { 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') } draw(ctx, pageMatrix) { // the setStyles function is a function on markup annotations that sets up // certain properties for us on the canvas for the annotation's stroke thickness. this.setStyles(ctx, pageMatrix); // first we need to translate to the annotation's x/y coordinates so that it's // drawn in the correct location ctx.translate(this.X, this.Y); ctx.beginPath(); ctx.setLineDash([5,3]); ctx.rect(0,0,this.Width, this.Height); ctx.stroke(); ctx.fillStyle = "black"; const zoom = docViewer.getZoom(); const textAnnot = this.Address+" Signs Here" // context.translate(newx, newy); 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.Height/2)-this.Height, this.Width/2); }else{ ctx.fillText(textAnnot, (this.Height/2), (this.Width/2)-this.Width); } } else{ const textHeight = this.Width/11; ctx.font = `${textHeight}px san-serif`; if(this.PageRotation == - 180){ ctx.fillText(textAnnot, (this.Width/2)-this.Width, (this.Height/2)-this.Height); } else{ ctx.fillText(textAnnot, (this.Width/2), this.Height/2); } } ctx.stroke(); } } // 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); // not show signatures in notesPanel // instance.setCustomNoteFilter(annot => (!(annot instanceof instance.Annotations.FreeHandAnnotation) )) if (allFieldsRequiredBeforeSave){ handleAllFieldsFilled(areAllFieldsFilled(annotManager)) } if(newDocument){ const {name, ext} = getShortenedName(newDocument) handleDoc({name:name+ext}) docViewer.on('annotationsLoaded', async () => { const annotationList = annotManager.getAnnotationsList() const fieldManager = annotManager.getFieldManager(); let widgetAnnots = annotationList.filter(a => a instanceof Annotations.WidgetAnnotation) const signatureWidgetAnnots = widgetAnnots.filter( annot => annot instanceof Annotations.SignatureWidgetAnnotation ); // customize look of Sign Here field await redrawSignHereWidget(signatureWidgetAnnots, instance, ethAccount, ethAlias, ethAvatar ) annotManager.deleteAnnotations(signatureWidgetAnnots) // _.pull(annotationList, signatureWidgetAnnots) // customize look of the rest of the fields // TODO: style fields with #366DCC strokeColor and FDF4ED bg // await styleWidgets(instance, widgetAnnots) handleDocChanges(instance, handleFieldsByPage, handleNumOfSigned, handleNewChanges, savedAnnotations, handleFieldChanges, handleAllFieldsFilled, ethAccount); }); }else{ docViewer.on('annotationsLoaded', async () => { await Promise.all([ new Promise(async (resolve, reject) => { if(location.state.sigArr){ const xfdf = await getStringFromDecryptedArr(location.state.sigArr, formatMessage) annotManager.importAnnotations(xfdf) .then(async (sigfields) => { await applyFields(sigfields, instance, ethAccount, location.state.doc.signatureData) handleMySigFields(sigfields) resolve(); }); }else{ resolve(); } }), new Promise(async (resolve, reject) => { await getCommentArrs(location.state.commentData, temp_password, formatMessage).then(async (data) => { await loadComments(data, instance); resolve(); }) }) ]) // TODO: loading state // here, the loading state should be set to false and the user will now be allowed to edit the document updateSignatureFinder(annotManager, handleFieldsByPage, handleNumOfSigned, ethAccount); handleDocChanges(instance, handleFieldsByPage, handleNumOfSigned, handleNewChanges, savedAnnotations, handleFieldChanges, handleAllFieldsFilled, ethAccount) }); } docViewer.on('pageNumberUpdated', (pageNumber) => { handleDocPage(pageNumber); }); }); if (temp_doc){ const index = _.findIndex(temp_doc.signers, {address: ethAccount}); if (index > -1) { handleSigner(true) handleSigned(temp_doc.signers[index].fullySigned) } else { handleSigner(false) } } handleLoaded(true) })(); }, []); useEffect(() => { (async () => { if (!newDocument && fileArr != null && fileArr != false && instance != null) { const blob = new Blob([fileArr], { type: 'application/pdf' }); instance.loadDocument(blob, { filename: doc.name }); } })(); }, [fileArr, instance]); const loadComments = async (commentArrs, instance) => { const {annotManager} = instance; const allImportedAnnots = []; await Promise.all( await commentArrs.map(async (commentArr, id)=>{ const xfdf = await getStringFromDecryptedArr(commentArr, formatMessage) annotManager.importAnnotations(xfdf) .then(async (importedAnnotations) => { var signatures = importedAnnotations.filter((annot) => annot.Subject == "Signature") makeSignaturesReadOnly(signatures) if(signatures.length>0){ handleUpdatedSignatures(!updatedSignatures) } allImportedAnnots.push(importedAnnotations) }); }) ) handleSavedAnnotations(allImportedAnnots) return true; } return ( <> {saveStatus && <Alert title="Saving Document" message={saveStatus} type='bottom-right' customComponent={ <ProgressBarIndicator signStyle={true} value={signProgress} max={newDocument? 4: (signing? 2:1) } /> } /> } {!newDocument && <TitleBar newChanges={newChanges} doc={doc} saving={saving} allFieldsFilled={allFieldsFilled} fieldChanges={fieldChanges} allFieldsRequiredBeforeSave={allFieldsRequiredBeforeSave} bar={{ button1: formatMessage({id: "BACK"}), button1Action: ()=>{props.history.push({pathname: '/contracts'})}, button2: formatMessage({id: "SAVE"}), button2Action: ()=>{signContract()} }} /> } <div className="bg-gray-400 flex-grow flex flex-col"> <div className="mx-auto flex-grow flex flex h-full w-full text-gray-300 text-15"> <SignatureFinder mySigFields={mySigFields} fieldsByPage={fieldsByPage} numOfSigned={numOfSigned} instance={instance} docPage={docPage} updatedSignatures={updatedSignatures}/> <div className="flex-grow" ref={viewer}> </div> </div> </div> {newDocument && <TitleBar newChanges={newChanges} newDocument={newDocument} doc={doc} saving={saving} allFieldsFilled={allFieldsFilled} fieldChanges={fieldChanges} allFieldsRequiredBeforeSave={allFieldsRequiredBeforeSave} bar={{ button1: bar.button1, button1Action: bar.button1Action, button2: bar.button2, button2Action: ()=>{signContract()} }} /> } </> ); } export default withRouter(Sign);