@foreverrbum/ethsign
Version:
This package will allow you to electronically sign documents within your application
372 lines (341 loc) • 15.4 kB
JavaScript
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);