diff --git a/web/cashtab/src/components/Configure/Configure.js b/web/cashtab/src/components/Configure/Configure.js index 4ca88fd51..f5e00472d 100644 --- a/web/cashtab/src/components/Configure/Configure.js +++ b/web/cashtab/src/components/Configure/Configure.js @@ -1,1276 +1,1293 @@ import React, { useState, useEffect } from 'react'; import styled from 'styled-components'; import { useLocation, Link } from 'react-router-dom'; import { generalNotification, errorNotification, } from 'components/Common/Notifications'; -import { Collapse, Form, Input, Modal, Alert, Switch, Tag } from 'antd'; +import { + Collapse, + Form, + Input, + Modal, + Alert, + Switch, + Tag, + Tooltip, +} from 'antd'; import { Row, Col } from 'antd'; import { PlusSquareOutlined, WalletFilled, ImportOutlined, LockOutlined, CheckOutlined, CloseOutlined, LockFilled, ExclamationCircleFilled, } from '@ant-design/icons'; import { WalletContext, AuthenticationContext } from 'utils/context'; import { SidePaddingCtn } from 'components/Common/Atoms'; import { StyledCollapse } from 'components/Common/StyledCollapse'; import { AntdFormWrapper, CurrencySelectDropdown, } from 'components/Common/EnhancedInputs'; import PrimaryButton, { SecondaryButton, SmartButton, } from 'components/Common/PrimaryButton'; import { ThemedCopyOutlined, ThemedWalletOutlined, ThemedDollarOutlined, ThemedSettingOutlined, ThemedContactSendOutlined, } from 'components/Common/CustomIcons'; import { ReactComponent as Trashcan } from 'assets/trashcan.svg'; import { ReactComponent as Edit } from 'assets/edit.svg'; import { Event } from 'utils/GoogleAnalytics'; import ApiError from 'components/Common/ApiError'; import { formatSavedBalance } from 'utils/formatting'; const { Panel } = Collapse; const SettingsLink = styled.a` text-decoration: underline; color: ${props => props.theme.eCashBlue}; :visited { text-decoration: underline; color: ${props => props.theme.eCashBlue}; } :hover { color: ${props => props.theme.eCashPurple}; } `; const SWRow = styled.div` border-radius: 3px; padding: 10px 0; display: flex; align-items: center; justify-content: center; margin-bottom: 6px; @media (max-width: 500px) { flex-direction: column; margin-bottom: 12px; } `; const SWName = styled.div` width: 50%; display: flex; align-items: center; justify-content: space-between; word-wrap: break-word; hyphens: auto; @media (max-width: 500px) { width: 100%; justify-content: center; margin-bottom: 15px; } h3 { font-size: 16px; color: ${props => props.theme.darkBlue}; margin: 0; text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } h3.overflow { width: 100px; overflow: hidden; text-overflow: ellipsis; } - h3.overflow:hover { - background-color: ${props => props.theme.settings.background}; - overflow: visible; - inline-size: 100px; - white-space: normal; - } `; const SWBalance = styled.div` width: 50%; display: flex; align-items: center; justify-content: space-between; word-wrap: break-word; hyphens: auto; @media (max-width: 500px) { width: 100%; justify-content: center; margin-bottom: 15px; } div { font-size: 13px; color: ${props => props.theme.darkBlue}; margin: 0; text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } div.overflow { width: 150px; overflow: hidden; text-overflow: ellipsis; } div.overflow:hover { background-color: ${props => props.theme.settings.background}; - overflow: visible; inline-size: 150px; white-space: normal; } `; const SWButtonCtn = styled.div` width: 50%; display: flex; align-items: center; justify-content: flex-end; @media (max-width: 500px) { width: 100%; justify-content: center; } button { cursor: pointer; background: transparent; border: 1px solid #fff; box-shadow: none; color: #fff; border-radius: 3px; opacity: 0.6; transition: all 200ms ease-in-out; :hover { opacity: 1; background: ${props => props.theme.eCashBlue}; border-color: ${props => props.theme.eCashBlue}; } @media (max-width: 768px) { font-size: 14px; } } svg { stroke: ${props => props.theme.eCashBlue}; fill: ${props => props.theme.eCashBlue}; width: 25px; height: 25px; margin-right: 20px; cursor: pointer; :first-child:hover { stroke: ${props => props.theme.eCashBlue}; fill: ${props => props.theme.eCashBlue}; } :hover { stroke: ${props => props.theme.settings.delete}; fill: ${props => props.theme.settings.delete}; } } `; const ContactListRow = styled.div` border-radius: 3px; padding: 10px 0; display: flex; align-items: center; justify-content: center; margin-bottom: 6px; @media (max-width: 500px) { flex-direction: column; margin-bottom: 12px; } `; const ContactListAddress = styled.div` width: 40%; display: flex; align-items: center; justify-content: space-between; word-wrap: break-word; hyphens: auto; @media (max-width: 500px) { width: 100%; justify-content: center; margin-bottom: 15px; } div { font-size: 13px; color: ${props => props.theme.darkBlue}; margin: 12px; text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } div.overflow { width: 150px; overflow: hidden; text-overflow: ellipsis; } div.overflow:hover { background-color: ${props => props.theme.settings.background}; - overflow: visible; - inline-size: 150px; - white-space: normal; } `; const ContactListName = styled.div` width: 30%; display: flex; align-items: center; justify-content: space-between; word-wrap: break-word; hyphens: auto; @media (max-width: 500px) { width: 100%; justify-content: center; margin-bottom: 15px; } div { font-size: 13px; color: ${props => props.theme.darkBlue}; margin: 0px; text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } div.overflow { width: 150px; overflow: hidden; text-overflow: ellipsis; } div.overflow:hover { background-color: ${props => props.theme.settings.background}; - overflow: visible; inline-size: 150px; white-space: normal; } `; const ContactListCtn = styled.div` display: flex; align-items: center; justify-content: flex-end; @media (max-width: 500px) { width: 100%; justify-content: center; } button { cursor: pointer; background: transparent; border: 1px solid #fff; box-shadow: none; color: #fff; border-radius: 3px; opacity: 0.6; transition: all 200ms ease-in-out; :hover { opacity: 1; background: ${props => props.theme.eCashBlue}; border-color: ${props => props.theme.eCashBlue}; } @media (max-width: 768px) { font-size: 14px; } } svg { stroke: ${props => props.theme.eCashBlue}; fill: ${props => props.theme.eCashBlue}; width: 25px; height: 25px; margin-right: 10px; cursor: pointer; :first-child:hover { stroke: ${props => props.theme.eCashBlue}; fill: ${props => props.theme.eCashBlue}; } :hover { stroke: ${props => props.theme.settings.delete}; fill: ${props => props.theme.settings.delete}; } } `; const AWRow = styled.div` padding: 10px 0; display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; h3 { font-size: 16px; display: inline-block; color: ${props => props.theme.darkBlue}; margin: 0; text-align: left; font-weight: bold; @media (max-width: 500px) { font-size: 14px; } } h4 { font-size: 16px; display: inline-block; color: ${props => props.theme.eCashBlue} !important; margin: 0; text-align: right; } @media (max-width: 500px) { flex-direction: column; margin-bottom: 12px; } `; const StyledConfigure = styled.div` h2 { color: ${props => props.theme.contrast}; font-size: 25px; } svg { fill: ${props => props.theme.eCashBlue}; } p { color: ${props => props.theme.darkBlue}; } .ant-alert { color: ${props => props.theme.lightGrey} font-size: 14px; } .ant-collapse-header{ .anticon{ flex: 1; } .seedPhrase{ flex: 2; } } `; const StyledSpacer = styled.div` height: 1px; width: 100%; background-color: ${props => props.theme.lightWhite}; margin: 60px 0 50px; `; const GeneralSettingsItem = styled.div` display: flex; align-items: center; justify-content: space-between; .ant-switch svg { fill: #717171; } .title { color: ${props => props.theme.contrast}; } .anticon { color: ${props => props.theme.contrast}; } .ant-switch { background-color: #bdbdbd; } .ant-switch-checked { background-color: ${props => props.theme.eCashBlue}; svg { fill: ${props => props.theme.contrast}; } } .SendConfirm { color: ${props => props.theme.lightWhite}; } `; const Configure = () => { const ContextValue = React.useContext(WalletContext); const authentication = React.useContext(AuthenticationContext); const { wallet, apiError } = ContextValue; const location = useLocation(); const { addNewSavedWallet, activateWallet, renameWallet, deleteWallet, validateMnemonic, getSavedWallets, cashtabSettings, changeCashtabSettings, getContactListFromLocalForage, updateContactListInLocalForage, } = ContextValue; const [savedWallets, setSavedWallets] = useState([]); const [formData, setFormData] = useState({ dirty: true, mnemonic: '', }); const [showRenameWalletModal, setShowRenameWalletModal] = useState(false); const [showDeleteWalletModal, setShowDeleteWalletModal] = useState(false); const [walletToBeRenamed, setWalletToBeRenamed] = useState(null); const [walletToBeDeleted, setWalletToBeDeleted] = useState(null); const [newWalletName, setNewWalletName] = useState(''); const [ confirmationOfWalletToBeDeleted, setConfirmationOfWalletToBeDeleted, ] = useState(''); const [newWalletNameIsValid, setNewWalletNameIsValid] = useState(null); const [walletDeleteValid, setWalletDeleteValid] = useState(null); const [seedInput, openSeedInput] = useState(false); const [showTranslationWarning, setShowTranslationWarning] = useState(false); const showPopulatedDeleteWalletModal = walletInfo => { setWalletToBeDeleted(walletInfo); setShowDeleteWalletModal(true); }; const showPopulatedRenameWalletModal = walletInfo => { setWalletToBeRenamed(walletInfo); setShowRenameWalletModal(true); }; const cancelRenameWallet = () => { // Delete form value setNewWalletName(''); setShowRenameWalletModal(false); }; const cancelDeleteWallet = () => { setWalletToBeDeleted(null); setConfirmationOfWalletToBeDeleted(''); setShowDeleteWalletModal(false); }; const updateSavedWallets = async activeWallet => { if (activeWallet) { let savedWallets; try { savedWallets = await getSavedWallets(activeWallet); setSavedWallets(savedWallets); } catch (err) { console.log(`Error in getSavedWallets()`); console.log(err); } } }; const [isValidMnemonic, setIsValidMnemonic] = useState(null); const [contactListArray, setContactListArray] = useState([{}]); const [showRenameContactModal, setShowRenameContactModal] = useState(false); const [contactToBeRenamed, setContactToBeRenamed] = useState(null); //object const [newContactNameIsValid, setNewContactNameIsValid] = useState(null); const [ confirmationOfContactToBeRenamed, setConfirmationOfContactToBeRenamed, ] = useState(''); useEffect(() => { // Update savedWallets every time the active wallet changes updateSavedWallets(wallet); }, [wallet]); useEffect(async () => { const detectedBrowserLang = navigator.language; if (!detectedBrowserLang.includes('en-')) { setShowTranslationWarning(true); } // if this was routed from Home screen's Add to Contact link if (location && location.state && location.state.contactToAdd) { let tempContactListArray; try { tempContactListArray = await getContactListFromLocalForage(); } catch (err) { console.log('Error in getContactListFromLocalForage()'); console.log(err); } // set default name for contact and sender as address let newContactObj = { name: location.state.contactToAdd.substring(6, 11), address: location.state.contactToAdd, }; if (!tempContactListArray || tempContactListArray.length === 0) { // no existing contact list in local storage tempContactListArray = [{}]; // instantiates to mitigate null pointer issues tempContactListArray.push(newContactObj); tempContactListArray.shift(); // remove the initial entry from instantiation generalNotification( location.state.contactToAdd + ' added to Contact List', ); } else { // contact list exists in local storage // check if address already exists in contact list let duplicateContact = false; let tempContactListArrayLength = tempContactListArray.length; for (let i = 0; i < tempContactListArrayLength; i++) { if ( tempContactListArray[i].address === location.state.contactToAdd ) { generalNotification( location.state.contactToAdd + ' already exists in the Contact List', ); duplicateContact = true; break; } } // if address does not exist on the contact list, add it if (!duplicateContact) { tempContactListArray.push(newContactObj); generalNotification( location.state.contactToAdd + ' added to Contact List', ); } } // update local storage let updateContactListStatus; try { updateContactListStatus = await updateContactListInLocalForage( tempContactListArray, ); } catch (err) { console.log('Error in updateContactListInLocalForage()'); console.log(err); } if (!updateContactListStatus) { errorNotification( null, 'Error updating contact list in localforage', 'Updating localforage with contact list', ); } // commit to state for local operations setContactListArray(tempContactListArray); } else { // if this was just standard routing between cashtab components // i.e. Not via the Add to contacts action let loadContactListStatus; try { loadContactListStatus = await getContactListFromLocalForage(); } catch (err) { console.log('Error in getContactListFromLocalForage()'); console.log(err); } setContactListArray(loadContactListStatus); } }, []); // Need this function to ensure that savedWallets are updated on new wallet creation const updateSavedWalletsOnCreate = async importMnemonic => { // Event("Category", "Action", "Label") // Track number of times a different wallet is activated Event('Configure.js', 'Create Wallet', 'New'); const walletAdded = await addNewSavedWallet(importMnemonic); if (!walletAdded) { Modal.error({ title: 'This wallet already exists!', content: 'Wallet not added', }); } else { Modal.success({ content: 'Wallet added to your saved wallets', }); } await updateSavedWallets(wallet); }; // Same here // TODO you need to lock UI here until this is complete // Otherwise user may try to load an already-loading wallet, wreak havoc with indexedDB const updateSavedWalletsOnLoad = async walletToActivate => { // Event("Category", "Action", "Label") // Track number of times a different wallet is activated Event('Configure.js', 'Activate', ''); await activateWallet(walletToActivate); }; async function submit() { setFormData({ ...formData, dirty: false, }); // Exit if no user input if (!formData.mnemonic) { return; } // Exit if mnemonic is invalid if (!isValidMnemonic) { return; } // Event("Category", "Action", "Label") // Track number of times a different wallet is activated Event('Configure.js', 'Create Wallet', 'Imported'); updateSavedWalletsOnCreate(formData.mnemonic); } const handleChange = e => { const { value, name } = e.target; // Validate mnemonic on change // Import button should be disabled unless mnemonic is valid setIsValidMnemonic(validateMnemonic(value)); setFormData(p => ({ ...p, [name]: value })); }; const changeWalletName = async () => { if (newWalletName === '' || newWalletName.length > 24) { setNewWalletNameIsValid(false); return; } // Hide modal setShowRenameWalletModal(false); // Change wallet name console.log( `Changing wallet ${walletToBeRenamed.name} name to ${newWalletName}`, ); const renameSuccess = await renameWallet( walletToBeRenamed.name, newWalletName, ); if (renameSuccess) { Modal.success({ content: `Wallet "${walletToBeRenamed.name}" renamed to "${newWalletName}"`, }); } else { Modal.error({ content: `Rename failed. All wallets must have a unique name.`, }); } await updateSavedWallets(wallet); // Clear wallet name for form setNewWalletName(''); }; const deleteSelectedWallet = async () => { if (!walletDeleteValid && walletDeleteValid !== null) { return; } if ( confirmationOfWalletToBeDeleted !== `delete ${walletToBeDeleted.name}` ) { setWalletDeleteValid(false); return; } // Hide modal setShowDeleteWalletModal(false); // Change wallet name console.log(`Deleting wallet "${walletToBeDeleted.name}"`); const walletDeletedSuccess = await deleteWallet(walletToBeDeleted); if (walletDeletedSuccess) { Modal.success({ content: `Wallet "${walletToBeDeleted.name}" successfully deleted`, }); } else { Modal.error({ content: `Error deleting ${walletToBeDeleted.name}.`, }); } await updateSavedWallets(wallet); // Clear wallet delete confirmation from form setConfirmationOfWalletToBeDeleted(''); }; const handleWalletNameInput = e => { const { value } = e.target; // validation if (value && value.length && value.length < 24) { setNewWalletNameIsValid(true); } else { setNewWalletNameIsValid(false); } setNewWalletName(value); }; const handleWalletToDeleteInput = e => { const { value } = e.target; if (value && value === `delete ${walletToBeDeleted.name}`) { setWalletDeleteValid(true); } else { setWalletDeleteValid(false); } setConfirmationOfWalletToBeDeleted(value); }; const handleAppLockToggle = checked => { if (checked) { // if there is an existing credential, that means user has registered // simply turn on the Authentication Required flag if (authentication.credentialId) { authentication.turnOnAuthentication(); } else { // there is no existing credential, that means user has not registered // user need to register authentication.signUp(); } } else { authentication.turnOffAuthentication(); } }; const handleContactNameInput = e => { const { value } = e.target; if (value && value.length && value.length < 24) { setNewContactNameIsValid(true); } else { setNewContactNameIsValid(false); } setConfirmationOfContactToBeRenamed(value); }; const handleRenameContact = contactObj => { if (!contactObj) { console.log( 'handleRenameContact() error: Invalid contact object for update', ); return; } setContactToBeRenamed(contactObj); setShowRenameContactModal(true); }; const handleRenameContactCancel = () => { setShowRenameContactModal(false); }; const handleRenameContactModalOk = () => { if ( !newContactNameIsValid || newContactNameIsValid === null || !contactToBeRenamed ) { return; } renameContactByName(contactToBeRenamed); setShowRenameContactModal(false); }; const renameContactByName = async contactObj => { // obtain reference to the contact object in the array let contactObjToUpdate = contactListArray.find( element => element.address === contactObj.address, ); // if a match was found if (contactObjToUpdate) { // update the contact name contactObjToUpdate.name = confirmationOfContactToBeRenamed; // update local object array and local storage setContactListArray(contactListArray); let updateContactListStatus; try { updateContactListStatus = await updateContactListInLocalForage( contactListArray, ); } catch (err) { console.log('Error in updateContactListInLocalForage()'); console.log(err); } if (!updateContactListStatus) { errorNotification( null, 'Unable to update localforage with updated contact list', 'Updating localforage with contact list', ); } } else { errorNotification( null, 'Unable to find contact in array', 'Updating localforage with contact list', ); } }; const handleSendModalToggle = checkedState => { changeCashtabSettings('sendModal', checkedState); }; return ( {showRenameContactModal && ( handleRenameContactModalOk()} onCancel={() => handleRenameContactCancel()} >
} placeholder="Enter new contact name" name="newContactName" value={confirmationOfContactToBeRenamed} onChange={e => handleContactNameInput(e) } />
)} {walletToBeRenamed !== null && ( cancelRenameWallet()} >
} placeholder="Enter new wallet name" name="newName" value={newWalletName} onChange={e => handleWalletNameInput(e)} />
)} {walletToBeDeleted !== null && ( cancelDeleteWallet()} >
} placeholder={`Type "delete ${walletToBeDeleted.name}" to confirm`} name="walletToBeDeletedInput" value={confirmationOfWalletToBeDeleted} onChange={e => handleWalletToDeleteInput(e) } />
)}

Backup your wallet

{showTranslationWarning && ( )} {wallet && wallet.mnemonic && ( Click to reveal seed phrase } >

{wallet && wallet.mnemonic ? wallet.mnemonic : ''}

)}

Manage Wallets

{apiError ? ( ) : ( <> updateSavedWalletsOnCreate()} > New Wallet openSeedInput(!seedInput)} > Import Wallet {seedInput && ( <>

Copy and paste your mnemonic seed phrase below to import an existing wallet

} type="email" placeholder="mnemonic (seed phrase)" name="mnemonic" autoComplete="off" onChange={e => handleChange(e)} required /> submit()} > Import
)} )} {savedWallets && savedWallets.length > 0 && ( <> -

- {wallet.name} -

+ +

+ {wallet.name} +

+

Currently active

{savedWallets.map(sw => ( - -

- {sw.name} -

-
+ + +

+ {sw.name} +

+
+
[ {sw && sw.state ? formatSavedBalance( sw.state.balances .totalBalance, ) : 'N/A'}{' '} XEC]
showPopulatedRenameWalletModal( sw, ) } /> showPopulatedDeleteWalletModal( sw, ) } />
))}
)}
{contactListArray && contactListArray.length > 0 ? ( contactListArray.map( (element, index) => ( - -
- {element.name} -
-
- -
{ - navigator.clipboard.writeText( - element.address, - ); - generalNotification( - element.address + - ' copied to clipboard', - 'Copied', - ); - }} - > - { - element.address - } -
-
+ + +
+ { + element.name + } +
+
+
+ + +
{ + navigator.clipboard.writeText( + element.address, + ); + generalNotification( + element.address + + ' copied to clipboard', + 'Copied', + ); + }} + > + { + element.address + } +
+
+
handleRenameContact( element, ) } />
), ) ) : (

{ 'Your contact list is empty.' }

{ 'Contacts can be added by clicking on a received transaction and looking for the "Add to contacts" icon.' }

)}

Fiat Currency

changeCashtabSettings('fiatCurrency', fiatCode) } />

General Settings

Lock App
{authentication ? ( } unCheckedChildren={} checked={ authentication.isAuthenticationRequired && authentication.credentialId ? true : false } // checked={false} onChange={handleAppLockToggle} /> ) : ( }> Not Supported )}
Send Confirmations
} unCheckedChildren={} checked={ cashtabSettings ? cashtabSettings.sendModal : false } onChange={handleSendModalToggle} />
[ Documentation ]
); }; export default Configure;