diff --git a/web/cashtab/src/assets/download.svg b/web/cashtab/src/assets/download.svg new file mode 100644 index 000000000..0219a4914 --- /dev/null +++ b/web/cashtab/src/assets/download.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/cashtab/src/assets/plus.svg b/web/cashtab/src/assets/plus.svg new file mode 100644 index 000000000..c5361132c --- /dev/null +++ b/web/cashtab/src/assets/plus.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/cashtab/src/components/Airdrop/__tests__/__snapshots__/Airdrop.test.js.snap b/web/cashtab/src/components/Airdrop/__tests__/__snapshots__/Airdrop.test.js.snap index 0b81a96ed..df1e9a859 100644 --- a/web/cashtab/src/components/Airdrop/__tests__/__snapshots__/Airdrop.test.js.snap +++ b/web/cashtab/src/components/Airdrop/__tests__/__snapshots__/Airdrop.test.js.snap @@ -1,450 +1,450 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP @generated exports[`Wallet with BCH balances 1`] = ` Array [

MigrationTestAlpha

You currently have 0 XEC
Deposit some funds to use this feature
,
,
XEC Airdrop Calculator
, ] `; exports[`Wallet with BCH balances and tokens 1`] = ` Array [

MigrationTestAlpha

You currently have 0 XEC
Deposit some funds to use this feature
,
,
XEC Airdrop Calculator
, ] `; exports[`Wallet with BCH balances and tokens and state field 1`] = ` Array [

MigrationTestAlpha

0.06 XEC
,
,
XEC Airdrop Calculator
, ] `; exports[`Wallet without BCH balance 1`] = ` Array [

MigrationTestAlpha

You currently have 0 XEC
Deposit some funds to use this feature
,
,
XEC Airdrop Calculator
, ] `; exports[`Without wallet defined 1`] = ` Array [
You currently have 0 XEC
Deposit some funds to use this feature
,
,
XEC Airdrop Calculator
, ] `; diff --git a/web/cashtab/src/components/Common/CustomIcons.js b/web/cashtab/src/components/Common/CustomIcons.js index c7a2fe508..3a0d6a476 100644 --- a/web/cashtab/src/components/Common/CustomIcons.js +++ b/web/cashtab/src/components/Common/CustomIcons.js @@ -1,128 +1,143 @@ import * as React from 'react'; import styled from 'styled-components'; import { CopyOutlined, DollarOutlined, LoadingOutlined, WalletOutlined, QrcodeOutlined, SettingOutlined, LockOutlined, ContactsOutlined, } from '@ant-design/icons'; import { Image } from 'antd'; import { currency } from 'components/Common/Ticker'; import { ReactComponent as Send } from 'assets/send.svg'; import { ReactComponent as Receive } from 'assets/receive.svg'; import { ReactComponent as Genesis } from 'assets/flask.svg'; import { ReactComponent as Unparsed } from 'assets/alert-circle.svg'; import { ReactComponent as Home } from 'assets/home.svg'; import { ReactComponent as Settings } from 'assets/cog.svg'; import { ReactComponent as CopySolid } from 'assets/copy.svg'; import { ReactComponent as LinkSolid } from 'assets/external-link-square-alt.svg'; import { ReactComponent as Airdrop } from 'assets/airdrop-icon.svg'; import { ReactComponent as Pdf } from 'assets/file-pdf.svg'; - +import { ReactComponent as Plus } from 'assets/plus.svg'; +import { ReactComponent as Download } from 'assets/download.svg'; export const CashLoadingIcon = ; export const CashReceivedNotificationIcon = () => ( ); export const TokenReceivedNotificationIcon = () => ( ); export const MessageSignedNotificationIcon = () => ( ); export const ThemedCopyOutlined = styled(CopyOutlined)` color: ${props => props.theme.icons.outlined} !important; `; export const ThemedDollarOutlined = styled(DollarOutlined)` color: ${props => props.theme.icons.outlined} !important; `; export const ThemedWalletOutlined = styled(WalletOutlined)` color: ${props => props.theme.icons.outlined} !important; `; export const ThemedQrcodeOutlined = styled(QrcodeOutlined)` color: ${props => props.theme.walletBackground} !important; `; export const ThemedSettingOutlined = styled(SettingOutlined)` color: ${props => props.theme.icons.outlined} !important; `; export const ThemedLockOutlined = styled(LockOutlined)` color: ${props => props.theme.icons.outlined} !important; `; export const ThemedContactsOutlined = styled(ContactsOutlined)` color: ${props => props.theme.icons.outlined} !important; `; export const ThemedContactSendOutlined = styled(Send)` color: ${props => props.theme.icons.outlined} !important; transform: rotate(-35deg); padding: 0.15rem 0rem 0.18rem 0rem; height: 1.3em; width: 1.3em; `; export const ThemedCopySolid = styled(CopySolid)` fill: ${props => props.theme.contrast}; padding: 0rem 0rem 0.27rem 0rem; height: 1.3em; width: 1.3em; `; export const ThemedLinkSolid = styled(LinkSolid)` fill: ${props => props.theme.contrast}; padding: 0.15rem 0rem 0.18rem 0rem; height: 1.3em; width: 1.3em; `; export const ThemedPdfSolid = styled(Pdf)` fill: ${props => props.theme.contrast}; padding: 0.15rem 0rem 0.18rem 0rem; height: 1.3em; width: 1.3em; `; +export const ThemedPlusOutlined = styled(Plus)` + fill: ${props => props.theme.contrast}; + padding: 0.15rem 0rem 0.18rem 0rem; + height: 1.3em; + width: 1.3em; +`; + +export const ThemedDownloadOutlined = styled(Download)` + fill: ${props => props.theme.contrast}; + padding: 0.15rem 0rem 0.18rem 0rem; + height: 1.3em; + width: 1.3em; +`; + export const LoadingBlock = styled.div` width: 100%; display: flex; align-items: center; justify-content: center; padding: 24px; flex-direction: column; svg { width: 50px; height: 50px; fill: ${props => props.theme.eCashBlue}; } `; export const CashLoader = () => ( ); export const ReceiveIcon = () => ; export const GenesisIcon = () => ; export const UnparsedIcon = () => ; export const HomeIcon = () => ; export const SettingsIcon = () => ; export const AirdropIcon = () => ; export const SendIcon = styled(Send)` transform: rotate(-35deg); `; export const CustomSpinner = ; diff --git a/web/cashtab/src/components/Configure/Configure.js b/web/cashtab/src/components/Configure/Configure.js index 4eab4fea0..4c21559d5 100644 --- a/web/cashtab/src/components/Configure/Configure.js +++ b/web/cashtab/src/components/Configure/Configure.js @@ -1,1815 +1,1846 @@ 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, 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, FormLabel } 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, ThemedContactsOutlined, ThemedContactSendOutlined, + ThemedPlusOutlined, + ThemedDownloadOutlined, } 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'; import { isValidXecAddress } from 'utils/validation'; import { convertToEcashPrefix } from 'utils/cashMethods'; import { currency } from 'components/Common/Ticker.js'; 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; } `; 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}; 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: 20px; 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 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}; } `; 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}; 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 ContactListBtnCtn = styled.div` + display: flex; + align-items: center; + justify-content: center; + gap: 4px; +`; + +const ExpandedBtnText = styled.span` + @media (max-width: 335px) { + display: none; + } +`; + const ContactListBtn = styled.button` + display: flex; + justify-content: center; align-items: center; cursor: pointer; background: transparent; border: 1px solid #fff; box-shadow: none; color: #fff; border-radius: 3px; opacity: 0.6; + gap: 3px; transition: all 200ms ease-in-out; @media (max-width: 500px) { width: 100%; justify-content: center; } :hover { opacity: 1; background: ${props => props.theme.eCashBlue}; border-color: ${props => props.theme.eCashBlue}; } + svg { + fill: ${props => props.theme.contrast} !important; + } `; 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 [savedWalletContactModal, setSavedWalletContactModal] = 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(''); const [showDeleteContactModal, setShowDeleteContactModal] = useState(false); const [contactAddressToDelete, setContactAddressToDelete] = useState(null); const [contactDeleteValid, setContactDeleteValid] = useState(null); const [ confirmationOfContactToBeDeleted, setConfirmationOfContactToBeDeleted, ] = useState(''); const [showManualAddContactModal, setShowManualAddContactModal] = useState(false); const [manualContactName, setManualContactName] = useState(''); const [manualContactAddress, setManualContactAddress] = useState(''); const [manualContactNameIsValid, setManualContactNameIsValid] = useState(null); const [manualContactAddressIsValid, setManualContactAddressIsValid] = useState(null); 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', 'Success', ); } 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 ) { errorNotification( null, location.state.contactToAdd + ' already exists in the Contact List', 'handleManualAddContactModalOk() error', ); duplicateContact = true; break; } } // in the edge case of a fresh new wallet on a fresh new browser, remove the initialization entry to avoid an undefined contact in array if ( tempContactListArray && tempContactListArray[0].address === undefined ) { tempContactListArray.shift(); } // if address does not exist on the contact list, add it if (!duplicateContact) { tempContactListArray.push(newContactObj); generalNotification( location.state.contactToAdd + ' added to Contact List', 'Success', ); } } // 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); } // in the edge case of a fresh new wallet on a fresh new browser, remove the initialization entry to avoid an undefined contact in array if ( loadContactListStatus && loadContactListStatus[0].address === undefined ) { loadContactListStatus.shift(); } 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 > currency.localStorageMaxCharacters ) { 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 <= currency.localStorageMaxCharacters ) { 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 < currency.localStorageMaxCharacters ) { 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); }; const getContactNameByAddress = contactAddress => { if (!contactAddress) { return; } // filter contact from local contact list array const filteredContactList = contactListArray.filter( element => element.address === contactAddress, ); if (!filteredContactList) { return; } return filteredContactList[0].name; }; const deleteContactByAddress = async contactAddress => { if (!contactAddress) { return; } // filter contact from local contact list array const updatedContactList = contactListArray.filter( element => element.address !== contactAddress, ); // update local list setContactListArray(updatedContactList); // commit updated list to local storage let updateContactListStatus; try { updateContactListStatus = await updateContactListInLocalForage( updatedContactList, ); } catch (err) { console.log('Error in updateContactListInLocalForage()'); console.log(err); } if (updateContactListStatus) { generalNotification( contactAddressToDelete + ' removed from Contact List', 'Success', ); } else { errorNotification( null, 'Error removing ' + contactAddressToDelete + ' from Contact List', 'Updating localforage with contact list', ); } }; const handleDeleteContact = contactAddress => { if (!contactAddress) { console.log( 'handleDeleteContact() error: Invalid contact address for deletion', ); return; } setContactAddressToDelete(contactAddress); setShowDeleteContactModal(true); }; const handleDeleteContactModalCancel = () => { setShowDeleteContactModal(false); }; const handleDeleteContactModalOk = () => { if ( !contactDeleteValid || contactDeleteValid === null || !contactAddressToDelete ) { return; } setShowDeleteContactModal(false); deleteContactByAddress(contactAddressToDelete); }; const handleContactToDeleteInput = e => { const { value } = e.target; const contactName = getContactNameByAddress(contactAddressToDelete); if (value && value === 'delete ' + contactName) { setContactDeleteValid(true); } else { setContactDeleteValid(false); } setConfirmationOfContactToBeDeleted(value); }; const exportContactList = contactListArray => { if (!contactListArray) { errorNotification('Unable to export contact list'); return; } // convert object array into csv data let csvContent = 'data:text/csv;charset=utf-8,' + contactListArray.map( element => '\n' + element.name + '|' + element.address, ); // encode csv var encodedUri = encodeURI(csvContent); // hidden DOM node to set the default file name var csvLink = document.createElement('a'); csvLink.setAttribute('href', encodedUri); csvLink.setAttribute( 'download', 'Cashtab_Contacts_' + wallet.name + '.csv', ); document.body.appendChild(csvLink); csvLink.click(); }; const handleAddSavedWalletAsContactOk = async () => { let duplicateContact = false; let tempContactListArray = contactListArray; let newContactObj = { name: manualContactName, address: manualContactAddress, }; 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 } else { // contact list exists in local storage // check if address already exists in contact list let tempContactListArrayLength = tempContactListArray.length; for (let i = 0; i < tempContactListArrayLength; i++) { if (tempContactListArray[i].address === manualContactAddress) { errorNotification( null, manualContactAddress + ' already exists in the Contact List', 'handleAddSavedWalletAsContactOk() error', ); duplicateContact = true; break; } } // if address does not exist on the contact list, add it if (!duplicateContact) { tempContactListArray.push(newContactObj); generalNotification( manualContactAddress + ' added to Contact List', 'Success', ); } } // update localforage try { await updateContactListInLocalForage(tempContactListArray); } catch (err) { console.log('Error in handleAddSavedWalletAsContactOk()'); console.log(err); } // update local state array setContactListArray(tempContactListArray); setSavedWalletContactModal(false); setManualContactName(''); setManualContactAddress(''); }; const handleAddSavedWalletAsContactCancel = () => { setSavedWalletContactModal(false); setManualContactName(''); setManualContactAddress(''); }; const addSavedWalletToContact = walletInfo => { if (!walletInfo) { return; } // initialise saved wallet name and address to state for confirmation modal setManualContactName(walletInfo.name); setManualContactAddress( convertToEcashPrefix(walletInfo.Path1899.cashAddress), ); setSavedWalletContactModal(true); }; const handleManualAddContactModalOk = async () => { // if either inputs are invalid then go no further if (!manualContactNameIsValid || !manualContactAddressIsValid) { return; } let duplicateContact = false; let tempContactListArray = contactListArray; let newContactObj = { name: manualContactName, address: manualContactAddress, }; 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 } else { // contact list exists in local storage // check if address already exists in contact list let tempContactListArrayLength = tempContactListArray.length; for (let i = 0; i < tempContactListArrayLength; i++) { if (tempContactListArray[i].address === manualContactAddress) { errorNotification( null, manualContactAddress + ' already exists in the Contact List', 'handleManualAddContactModalOk() error', ); duplicateContact = true; break; } } // if address does not exist on the contact list, add it if (!duplicateContact) { tempContactListArray.push(newContactObj); generalNotification( manualContactAddress + ' added to Contact List', 'Success', ); } } // update local state array setContactListArray(tempContactListArray); // update localforage try { await updateContactListInLocalForage(tempContactListArray); } catch (err) { console.log('Error in handleManualAddContactModalOk()'); console.log(err); } setShowManualAddContactModal(false); setManualContactName(''); setManualContactAddress(''); }; const handleManualAddContactModalCancel = () => { setShowManualAddContactModal(false); setManualContactName(''); setManualContactAddress(''); }; const handleManualContactNameInput = e => { const { value } = e.target; if (value && value.length && value.length < 24) { setManualContactNameIsValid(true); } else { setManualContactNameIsValid(false); } setManualContactName(value); }; const handleManualContactAddressInput = e => { const { value } = e.target; setManualContactAddressIsValid(isValidXecAddress(value)); setManualContactAddress(value); }; return ( {savedWalletContactModal && ( handleAddSavedWalletAsContactOk()} onCancel={() => handleAddSavedWalletAsContactCancel()} >
Name: {manualContactName}
Address: {manualContactAddress}
)} {showManualAddContactModal && ( handleManualAddContactModalOk()} onCancel={() => handleManualAddContactModalCancel()} >
Name: handleManualContactNameInput(e) } /> eCash Address: handleManualContactAddressInput(e) } />
)} {showDeleteContactModal && ( <> handleDeleteContactModalOk()} onCancel={() => handleDeleteContactModalCancel()} >

are you sure you want to delete{' '} {getContactNameByAddress( contactAddressToDelete, )}{' '} from contact list?

} placeholder={`Type "delete ${getContactNameByAddress( contactAddressToDelete, )}" to confirm`} name="contactToBeDeletedInput" value={ confirmationOfContactToBeDeleted } onChange={e => handleContactToDeleteInput(e) } />
)} {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}

Currently active

{savedWallets.map(sw => (

{sw.name}

[ {sw && sw.state ? formatSavedBalance( sw.state.balances .totalBalance, ) : 'N/A'}{' '} XEC]
showPopulatedRenameWalletModal( sw, ) } /> addSavedWalletToContact( 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', 'Success', ); }} > { element.address }
handleRenameContact( element, ) } /> handleDeleteContact( element.address, ) } />
), ) ) : (

{ 'Your contact list is empty.' }

{ 'Contacts can be added by clicking on a received transaction and looking for the "Add to contacts" icon or via the "New Contact" button below.' }

)} {/* Export button will only show when there are contacts */} - {contactListArray && - contactListArray.length > 0 && ( - - exportContactList( - contactListArray, - ) - } - > - Export contacts - - )} -
-
- - setShowManualAddContactModal( - true, - ) - } - > - New Contact - + + {contactListArray && + contactListArray.length > 0 && ( + + exportContactList( + contactListArray, + ) + } + > + + + Download + + CSV + + )} +
+
+ + setShowManualAddContactModal( + true, + ) + } + > + + + Add + + Contact + +

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; diff --git a/web/cashtab/src/components/Configure/__tests__/__snapshots__/Configure.test.js.snap b/web/cashtab/src/components/Configure/__tests__/__snapshots__/Configure.test.js.snap index 84dc301f8..431ee2dca 100644 --- a/web/cashtab/src/components/Configure/__tests__/__snapshots__/Configure.test.js.snap +++ b/web/cashtab/src/components/Configure/__tests__/__snapshots__/Configure.test.js.snap @@ -1,975 +1,975 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP @generated exports[`Wallet with BCH balances and tokens 1`] = `

Backup your wallet

Your seed phrase is the only way to restore your wallet. Write it down. Keep it safe.

Manage Wallets

Contact List

Fiat Currency

US Dollar ($)

General Settings

Lock App
Not Supported
Send Confirmations
`; exports[`Without wallet defined 1`] = `

Backup your wallet

Your seed phrase is the only way to restore your wallet. Write it down. Keep it safe.

Manage Wallets

Contact List

Fiat Currency

US Dollar ($)

General Settings

Lock App
Not Supported
Send Confirmations
`; diff --git a/web/cashtab/src/components/Home/__tests__/__snapshots__/Home.test.js.snap b/web/cashtab/src/components/Home/__tests__/__snapshots__/Home.test.js.snap index fe7935bc4..bf6a1bd99 100644 --- a/web/cashtab/src/components/Home/__tests__/__snapshots__/Home.test.js.snap +++ b/web/cashtab/src/components/Home/__tests__/__snapshots__/Home.test.js.snap @@ -1,449 +1,449 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP @generated exports[`Wallet with BCH balances 1`] = ` Array [

MigrationTestAlpha

0 XEC
,
🎉 Congratulations on your new wallet! 🎉
Start using the wallet immediately to receive XEC payments, or load it up with XEC to send to others
Create eToken

Tokens sent to your eToken address will appear here

, ] `; exports[`Wallet with BCH balances and tokens 1`] = ` Array [

MigrationTestAlpha

0 XEC
,
🎉 Congratulations on your new wallet! 🎉
Start using the wallet immediately to receive XEC payments, or load it up with XEC to send to others
Create eToken

Tokens sent to your eToken address will appear here

, ] `; exports[`Wallet with BCH balances and tokens and state field 1`] = ` Array [

MigrationTestAlpha

0.06 XEC
,
🎉 Congratulations on your new wallet! 🎉
Start using the wallet immediately to receive XEC payments, or load it up with XEC to send to others
, ] `; exports[`Wallet without BCH balance 1`] = ` Array [

MigrationTestAlpha

0 XEC
,
🎉 Congratulations on your new wallet! 🎉
Start using the wallet immediately to receive XEC payments, or load it up with XEC to send to others
Create eToken

Tokens sent to your eToken address will appear here

, ] `; exports[`Without wallet defined 1`] = `

Welcome to Cashtab!

Cashtab is an open source, non-custodial web wallet for eCash .

Want to learn more? Check out the Cashtab documentation.

`; diff --git a/web/cashtab/src/components/Receive/__tests__/__snapshots__/Receive.test.js.snap b/web/cashtab/src/components/Receive/__tests__/__snapshots__/Receive.test.js.snap index 9908d4e34..1a5b2a422 100644 --- a/web/cashtab/src/components/Receive/__tests__/__snapshots__/Receive.test.js.snap +++ b/web/cashtab/src/components/Receive/__tests__/__snapshots__/Receive.test.js.snap @@ -1,534 +1,534 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP @generated exports[`Wallet with BCH balances 1`] = `

Receive XEC

Copied
ecash:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7ccxkrr6zd
ecash: qzagy47m vh6qxkvcn3acjnz73rkhkc6y7c cxkrr6zd
XEC
eToken
`; exports[`Wallet with BCH balances and tokens 1`] = `

Receive XEC

Copied
ecash:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7ccxkrr6zd
ecash: qzagy47m vh6qxkvcn3acjnz73rkhkc6y7c cxkrr6zd
XEC
eToken
`; exports[`Wallet with BCH balances and tokens and state field 1`] = `

Receive XEC

Copied
ecash:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7ccxkrr6zd
ecash: qzagy47m vh6qxkvcn3acjnz73rkhkc6y7c cxkrr6zd
XEC
eToken
`; exports[`Wallet without BCH balance 1`] = `

Receive XEC

Copied
ecash:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7ccxkrr6zd
ecash: qzagy47m vh6qxkvcn3acjnz73rkhkc6y7c cxkrr6zd
XEC
eToken
`; exports[`Without wallet defined 1`] = `

Welcome to Cashtab!

Cashtab is an open source, non-custodial web wallet for eCash .

Want to learn more? Check out the Cashtab documentation.

`; diff --git a/web/cashtab/src/components/Send/__tests__/__snapshots__/Send.test.js.snap b/web/cashtab/src/components/Send/__tests__/__snapshots__/Send.test.js.snap index 8d5880af7..92c0aff9a 100644 --- a/web/cashtab/src/components/Send/__tests__/__snapshots__/Send.test.js.snap +++ b/web/cashtab/src/components/Send/__tests__/__snapshots__/Send.test.js.snap @@ -1,2731 +1,2731 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP @generated exports[`Wallet with BCH balances 1`] = ` Array [

MigrationTestAlpha

You currently have 0 XEC
Deposit some funds to use this feature
,
XEC
max

0 XEC

= $ NaN USD
Advanced
Sign Message
Verify Message
, ] `; exports[`Wallet with BCH balances and tokens 1`] = ` Array [

MigrationTestAlpha

You currently have 0 XEC
Deposit some funds to use this feature
,
XEC
max

0 XEC

= $ NaN USD
Advanced
Sign Message
Verify Message
, ] `; exports[`Wallet with BCH balances and tokens and state field 1`] = ` Array [

MigrationTestAlpha

0.06 XEC
,
XEC
max

0 XEC

= $ NaN USD
Advanced
Sign Message
Verify Message
, ] `; exports[`Wallet without BCH balance 1`] = ` Array [

MigrationTestAlpha

You currently have 0 XEC
Deposit some funds to use this feature
,
XEC
max

0 XEC

= $ NaN USD
Advanced
Sign Message
Verify Message
, ] `; exports[`Without wallet defined 1`] = ` Array [
You currently have 0 XEC
Deposit some funds to use this feature
,
XEC
max

0 XEC

= $ NaN USD
Advanced
Sign Message
Verify Message
, ] `; diff --git a/web/cashtab/src/components/Send/__tests__/__snapshots__/SendToken.test.js.snap b/web/cashtab/src/components/Send/__tests__/__snapshots__/SendToken.test.js.snap index ba795b9fa..43e0f9861 100644 --- a/web/cashtab/src/components/Send/__tests__/__snapshots__/SendToken.test.js.snap +++ b/web/cashtab/src/components/Send/__tests__/__snapshots__/SendToken.test.js.snap @@ -1,250 +1,250 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP @generated exports[`Wallet with BCH balances and tokens 1`] = `null`; exports[`Wallet with BCH balances and tokens and state field 1`] = `
6.001 TBS
`; exports[`Without wallet defined 1`] = `null`; diff --git a/web/cashtab/src/components/Tokens/__tests__/__snapshots__/CreateTokenForm.test.js.snap b/web/cashtab/src/components/Tokens/__tests__/__snapshots__/CreateTokenForm.test.js.snap index 5d865ade0..825f6cba9 100644 --- a/web/cashtab/src/components/Tokens/__tests__/__snapshots__/CreateTokenForm.test.js.snap +++ b/web/cashtab/src/components/Tokens/__tests__/__snapshots__/CreateTokenForm.test.js.snap @@ -1,380 +1,380 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP @generated exports[`Wallet with BCH balances and tokens and state field 1`] = `

Create a Token

Click, or drag file to this area to upload

Only jpg or png accepted

`; diff --git a/web/cashtab/src/components/Tokens/__tests__/__snapshots__/Tokens.test.js.snap b/web/cashtab/src/components/Tokens/__tests__/__snapshots__/Tokens.test.js.snap index 8c4037f34..ffb3fc949 100644 --- a/web/cashtab/src/components/Tokens/__tests__/__snapshots__/Tokens.test.js.snap +++ b/web/cashtab/src/components/Tokens/__tests__/__snapshots__/Tokens.test.js.snap @@ -1,585 +1,585 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP @generated exports[`Wallet with BCH balances 1`] = ` Array [

MigrationTestAlpha

0 XEC
,

Create a Token

You need at least 5.5 XEC ( $ NaN USD ) to create a token

, ] `; exports[`Wallet with BCH balances and tokens 1`] = ` Array [

MigrationTestAlpha

0 XEC
,

Create a Token

You need at least 5.5 XEC ( $ NaN USD ) to create a token

, ] `; exports[`Wallet with BCH balances and tokens and state field 1`] = ` Array [

MigrationTestAlpha

0.06 XEC
,

Create a Token

Click, or drag file to this area to upload

Only jpg or png accepted

, ] `; exports[`Wallet without BCH balance 1`] = ` Array [

MigrationTestAlpha

0 XEC
,

Create a Token

You need at least 5.5 XEC ( $ NaN USD ) to create a token

, ] `; exports[`Without wallet defined 1`] = ` Array [
0 XEC
,

Create a Token

You need at least 5.5 XEC ( $ NaN USD ) to create a token

, ] `;