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
,
,
,
]
`;
exports[`Wallet with BCH balances and tokens 1`] = `
Array [
MigrationTestAlpha
You currently have 0
XEC
Deposit some funds to use this feature
,
,
,
]
`;
exports[`Wallet with BCH balances and tokens and state field 1`] = `
Array [
MigrationTestAlpha
0.06
XEC
,
,
,
]
`;
exports[`Wallet without BCH balance 1`] = `
Array [
MigrationTestAlpha
You currently have 0
XEC
Deposit some funds to use this feature
,
,
,
]
`;
exports[`Without wallet defined 1`] = `
Array [
You currently have 0
XEC
Deposit some funds to use this feature
,
,
,
]
`;
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()}
>
)}
{showManualAddContactModal && (
handleManualAddContactModalOk()}
onCancel={() => handleManualAddContactModalCancel()}
>
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,
)
}
/>
updateSavedWalletsOnLoad(
sw,
)
}
>
Activate
))}
>
)}
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
New Wallet
Import Wallet
Fiat Currency
General Settings
[
Documentation
]
`;
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
New Wallet
Import Wallet
Fiat Currency
General Settings
[
Documentation
]
`;
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 [
,
Transactions
eTokens
🎉
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 [
,
Transactions
eTokens
🎉
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
,
Transactions
eTokens
🎉
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 [
,
Transactions
eTokens
🎉
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`] = `
`;
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`] = `
`;
exports[`Wallet with BCH balances and tokens 1`] = `
`;
exports[`Wallet with BCH balances and tokens and state field 1`] = `
`;
exports[`Wallet without BCH balance 1`] = `
`;
exports[`Without wallet defined 1`] = `
`;
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
,
,
]
`;
exports[`Wallet with BCH balances and tokens 1`] = `
Array [
MigrationTestAlpha
You currently have 0
XEC
Deposit some funds to use this feature
,
,
]
`;
exports[`Wallet with BCH balances and tokens and state field 1`] = `
Array [
MigrationTestAlpha
0.06
XEC
,
,
]
`;
exports[`Wallet without BCH balance 1`] = `
Array [
MigrationTestAlpha
You currently have 0
XEC
Deposit some funds to use this feature
,
,
]
`;
exports[`Without wallet defined 1`] = `
Array [
You currently have 0
XEC
Deposit some funds to use this feature
,
,
]
`;
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`] = `
`;
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
Create eToken
`;
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 [
,
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 [
,
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
Create eToken
,
]
`;
exports[`Wallet without BCH balance 1`] = `
Array [
,
Create a Token
You need at least
5.5
XEC
(
$
NaN
USD
) to create a token
,
]
`;
exports[`Without wallet defined 1`] = `
Array [
,
Create a Token
You need at least
5.5
XEC
(
$
NaN
USD
) to create a token
,
]
`;