@foreverrbum/ethsign
Version:
This package will allow you to electronically sign documents within your application
426 lines (375 loc) • 13 kB
JavaScript
/**
* @author pzhu
*/
import React, { useEffect, useState } from 'react';
import { storeNotif } from './dashboard';
import { useIntl } from 'react-intl';
import * as EmailValidator from 'email-validator';
import { useLocation } from 'react-router';
const endpoint = 'https://auxiliary.ethsign.xyz';
export const EMAIL_NOTIFICATION = 'email-notification';
/**
*
* @param {function} provider
* @returns {string, function} email handleEmail
* @description a hook for operation email address
*/
export const useEmail = (provider) => {
const [email, handleEmail] = useState('');
const { pathname } = useLocation();
const [sameUser, handleSameUser] = useSameUser(provider);
const { formatMessage } =useIntl();
useEffect(() => {
if (!sameUser) {
useClearEmailLocalStorage();
handleEmail('');
}
if (sameUser) {
checkEmail(provider, formatMessage)
.then((hasEmail) => {
if (hasEmail && pathname === '/manage-profile') {
getEmail(provider, formatMessage)
.then(({email, rejectSign}) => {
handleEmail(email);
});
}
});
}
}, [sameUser, pathname]);
return [email, handleEmail];
};
export const useHasEmail = (provider) => {
const [hasEmail, handleHasEmail] = useState(false);
const { formatMessage } = useIntl();
useEffect(() => {
checkEmail(provider, formatMessage)
.then((hasEmail) => {
handleHasEmail(hasEmail);
})
}, [provider]);
return { hasEmail };
};
export const useSignNow = () => {
const [signNow, handleSignNow] = useState(() => {
const { email, hasEmail } = JSON.parse(localStorage.getItem(EMAIL_NOTIFICATION) || '{}');
if ((!hasEmail && !email) || (hasEmail && !email)) {
return false;
}
return true;
});
return [signNow, handleSignNow];
};
export const useCheckEmail = (provider, handleOpen) => {
const { pathname } = useLocation();
const [sameUser,] = useSameUser(provider);
const { formatMessage } = useIntl();
useEffect(()=> {
if (!sameUser && pathname !== '/manage-profile') {
useClearEmailLocalStorage();
}
checkEmail(provider, formatMessage)
.then((hasEmail) => {
const { skip } = getEmailLocalData();
if (pathname === "/contracts") {
if (checkCanPopup(skip, hasEmail)) {
handleOpen(true);
} else {
handleOpen(false);
}
}
})
},[pathname, sameUser]);
}
/**
*
* @returns {object, object} ethAddress, ethSignature
* @description a hook for operation user information
*/
export const useUserInfo = (provider) =>{
const [ethAddress, handleEthAddress] = useState();
const [sameUser, handleSameUser] = useSameUser(provider);
useEffect(() => {
const localData = JSON.parse(localStorage.getItem(EMAIL_NOTIFICATION) || '{}');
if (localData?.ethAddress) {
handleEthAddress(localData?.ethAddress);
} else {
(async ()=> {
const signer = provider.getSigner();
const ethAddress = await signer.getAddress();
handleEthAddress(ethAddress);
})(provider);
}
},[sameUser, provider]);
return { ethAddress };
};
export const useClearEmailLocalStorage = () => {
localStorage.setItem(EMAIL_NOTIFICATION, JSON.stringify({skip: false}));
};
/**
*
* @param {function} provider
* @param {function} formatMessage i18n
* @returns {Promise} email info
* @description for get email info
*/
export const getEmail = async (provider, formatMessage) =>{
// get data from local storage
const {email, timeStamp, ethAddress, rejectSign} = getEmailLocalData();
// check timeStamp is expired
// if not isExpired and the same user , return the local data
// else return the backend data
const sameUser = await isSameUser(provider, ethAddress);
if (!isExpired(timeStamp, 24) && sameUser && !rejectSign) {
return Promise.resolve({ email, rejectSign});
} else {
const {ethAddress, ethSignature, rejectSign} = await getSignerInfo(provider, formatMessage);
if (rejectSign) {
setEmailLocalData({skip: false, email: ''})
return Promise.resolve({email: '', rejectSign});
} else {
return await fetch(`${endpoint}/getEmail`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
ethAddress,
ethSignature
})
})
.then(res => res.json())
.then(({email}) => {
setEmailLocalData({timeStamp: new Date().getTime(), email})
return { email, rejectSign };
})
.catch(error => {
storeNotif(formatMessage({id: 'BAD_REQUEST'}), formatMessage({id: 'REQUEST_EMAIL_ADDRESS_FAILED'}), 'danger');
});
}
}
}
/**
*
* @param {function} provider
* @param {string} email
* @param {function} formatMessage i18n
* @returns {Promise} save result
* @description a function for save email
*/
export const saveEmail = async (provider, email, formatMessage) => {
// if email is empty string "", it is to show customer want to delete email
if (email) {
//validate email address
const validateResult = EmailValidator.validate(email);
if (!validateResult) {
storeNotif(formatMessage({id: 'INVALID_EMAIL'}), formatMessage({id: 'INVALID_EMAIL_MESSAGE'}), 'danger');
return Promise.resolve({success: false});
}
} else if (email === '') {
email = null;
}
const {ethAddress, ethSignature} = await getSignerInfo(provider, formatMessage);
return await fetch(`${endpoint}/setEmail`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
ethAddress,
ethSignature,
email
})
})
.then(res => res.json())
.then(data => {
// update hasEmial value when save email success
let hasEmail = true;
if (email === null) {
hasEmail = false;
}
// update email address
setEmailLocalData({hasEmail, email})
storeNotif(formatMessage({id: 'SAVE_SUCCESS'}), formatMessage({id: 'EMAIL_SAVE_SUCCESS'}), 'success');
return data;
})
.catch(error => {
storeNotif(formatMessage({id: 'BAD_REQUEST'}), formatMessage({id: 'REQUEST_EMAIL_ADDRESS_FAILED'}), 'danger');
return Promise.resolve({success: false});
});
}
export const checkEmail = async (provider, formatMessage) => {
const { hasEmail, ethAddress, timeStamp } = JSON.parse(localStorage.getItem(EMAIL_NOTIFICATION) || '{}');
const sameUser = await isSameUser(provider, ethAddress);
if (!isExpired(timeStamp, 24) && sameUser) {
return Promise.resolve(hasEmail)
} else {
const ethAddress = await getEthAddress(provider);
if(!ethAddress) {
// Return true here to ensure the "Welcome to EthSign" popup does not show.
// We want to display an error, not a welcome message.
return true;
}
return await fetch(`${endpoint}/checkEmail`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
ethAddresses: [ethAddress]
})
})
.then(res => {
return res.json()
})
.then(res => {
let hasEmail = false;
const findItem = res.find(r => {
return Object.keys(r)[0] === ethAddress;
});
if (findItem) {
hasEmail = Object.values(findItem)[0];
// update hasEmail on localstorage
setEmailLocalData({hasEmail});
}
return hasEmail;
})
.catch(e => {
storeNotif(formatMessage({id: 'BAD_REQUEST'}), formatMessage({id: 'CHECK_EMAIL_ADDRESS_FAILED'}, {error: e.toString()}), 'danger')
});
}
}
export const getEthAddress = async (provider) => {
const signer = provider.getSigner();
try {
return await signer.getAddress();
} catch(err) {
// getAddress returned an error (user logged out)
return null;
}
}
/**
*
* @param {function} provider
* @param {function} formatMessage i18n
* @returns {object, object} ethAddress, ethSignature
*/
const getSignerInfo = async (provider, formatMessage) => {
let rejectSign = false;
// get info from local first
let localData = getEmailLocalData();
let message = localData?.message;
const timeStamp = localData?.timeStamp;
let ethAddress = localData?.ethAddress;
let ethSignature = localData?.ethSignature;
// check timeStamp is expired
// if isExpired (24 h) and rpc user is not equal the local user, request data from backend
// else response directly local data
const sameUser = await isSameUser(provider, ethAddress);
if (isExpired(timeStamp, 24) || !ethSignature) {
// request data from backend
const resData = await getSignMessage(formatMessage);
message = resData?.message;
const signer = provider.getSigner();
ethAddress = (await signer.getAddress());
try {
ethSignature = await signer.signMessage(message);
} catch (e) {
rejectSign = true;
ethSignature = undefined;
message = undefined;
}
setEmailLocalData({timeStamp: new Date().getTime(), message, ethAddress, ethSignature, rejectSign})
}
return {ethAddress, ethSignature, rejectSign}
}
/**
*
* @param {function} provider
* @param {String} ethAddressFromLocal
* @returns {boolean}
* @description check the ethAddressFromLocal is equal with ethAddressFromRpc
*/
const isSameUser = async (provider, ethAddressFromLocal) => {
try {
// if the ethAddressFromLocal equal undefined, it is to show this is a new user, default the new user is a sigle user, should return true
const signer = provider.getSigner();
const ethAddressFromRpc = await signer.getAddress();
if (ethAddressFromLocal === undefined) {
setEmailLocalData({ethAddress:ethAddressFromRpc})
return true
};
return ethAddressFromLocal === ethAddressFromRpc;
} catch (err) {
// getAddress threw an error
return false;
}
}
/**
*
* @param {function} formatMessage i18n
* @returns {Promise} validate message
*/
const getSignMessage = async (formatMessage) => {
return await fetch(`${endpoint}/getMessage`)
.then(res => res.json())
.then(res => {
return res
})
.catch(error => {
storeNotif(formatMessage({id: 'REQUEST_MESSAGE_FAILDED'}), formatMessage({id: 'REQUEST_EMAIL_ADDRESS_FAILED'}), 'danger')
})
}
/**
*
* @param {millisecond} time
* @param {hours} expiredTime
* @returns {boolean}
*/
const isExpired = (time = 0, expiredTime) => {
const currentTime = new Date().getTime();
return (currentTime - time) /(1000*60*60) > expiredTime;
}
/**
*
* @param {*} provider
* @returns [boolean, function]
* @description check the user have change
*/
export const useSameUser = (provider) => {
const [sameUser, handleSameUser] = useState(true);
useEffect(()=> {
(async () =>{
const localData = JSON.parse(localStorage.getItem(EMAIL_NOTIFICATION) || '{}');
handleSameUser(await isSameUser(provider, localData?.ethAddress))
})();
});
return [sameUser, handleSameUser];
}
/**
*
* @param {boolean} skip
* @param {boolean} hasEmail the use have email set before
* @returns {boolean} decide popup the email notification
*/
export const checkCanPopup = (skip, hasEmail) => {
if (!skip && !hasEmail) {
return true;
}
return false;
}
/**
*
* @returns the local storage data about EMAIL_NOTIFICATION
*/
export const getEmailLocalData = () => {
return JSON.parse(localStorage.getItem(EMAIL_NOTIFICATION) || '{}')
}
/**
*
* @param {Object} value
* @description update the local storage data about EMAIL_NOTIFICATION
*/
export const setEmailLocalData = (value) => {
const localData = getEmailLocalData();
localStorage.setItem(EMAIL_NOTIFICATION, JSON.stringify({...localData, ...value}));
}