diff --git a/cashtab/package-lock.json b/cashtab/package-lock.json
--- a/cashtab/package-lock.json
+++ b/cashtab/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "cashtab",
- "version": "2.26.3",
+ "version": "2.26.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "cashtab",
- "version": "2.26.3",
+ "version": "2.26.4",
"dependencies": {
"@bitgo/utxo-lib": "^9.33.0",
"@zxing/browser": "^0.1.4",
diff --git a/cashtab/package.json b/cashtab/package.json
--- a/cashtab/package.json
+++ b/cashtab/package.json
@@ -1,6 +1,6 @@
{
"name": "cashtab",
- "version": "2.26.3",
+ "version": "2.26.4",
"private": true,
"scripts": {
"start": "node scripts/start.js",
diff --git a/cashtab/src/components/Airdrop/Airdrop.js b/cashtab/src/components/Airdrop/Airdrop.js
--- a/cashtab/src/components/Airdrop/Airdrop.js
+++ b/cashtab/src/components/Airdrop/Airdrop.js
@@ -8,7 +8,7 @@
import { BN } from 'slp-mdm';
import styled from 'styled-components';
import { WalletContext } from 'wallet/context';
-import PrimaryButton, { SecondaryLink } from 'components/Common/PrimaryButton';
+import PrimaryButton, { SecondaryLink } from 'components/Common/Buttons';
import CopyToClipboard from 'components/Common/CopyToClipboard';
import { getMintAddress } from 'chronik';
import {
diff --git a/cashtab/src/components/Alias/Alias.js b/cashtab/src/components/Alias/Alias.js
--- a/cashtab/src/components/Alias/Alias.js
+++ b/cashtab/src/components/Alias/Alias.js
@@ -7,7 +7,7 @@
import { WalletContext } from 'wallet/context';
import PropTypes from 'prop-types';
import { AlertMsg } from 'components/Common/Atoms';
-import PrimaryButton from 'components/Common/PrimaryButton';
+import PrimaryButton from 'components/Common/Buttons';
import { getWalletState } from 'utils/cashMethods';
import { toXec } from 'wallet';
import { meetsAliasSpec } from 'validation';
diff --git a/cashtab/src/components/Common/PrimaryButton.js b/cashtab/src/components/Common/Buttons.js
rename from cashtab/src/components/Common/PrimaryButton.js
rename to cashtab/src/components/Common/Buttons.js
--- a/cashtab/src/components/Common/PrimaryButton.js
+++ b/cashtab/src/components/Common/Buttons.js
@@ -2,8 +2,12 @@
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
+import React from 'react';
import styled, { css } from 'styled-components';
import { Link } from 'react-router-dom';
+import { CopyPasteIcon } from 'components/Common/CustomIcons';
+import { toast } from 'react-toastify';
+import PropTypes from 'prop-types';
const BaseButtonOrLinkCss = css`
font-size: 24px;
@@ -95,5 +99,80 @@
}
`;
+const SvgButtonOrLinkCss = css`
+ border: none;
+ background: none;
+ cursor: pointer;
+ svg {
+ height: 24px;
+ width: 24px;
+ fill: ${props => props.theme.eCashBlue};
+ }
+`;
+const SvgButton = styled.button`
+ ${SvgButtonOrLinkCss}
+`;
+
+const IconButton = ({ name, icon, onClick }) => (
+
+ {icon}
+
+);
+IconButton.propTypes = {
+ name: PropTypes.string,
+ icon: PropTypes.node,
+ onClick: PropTypes.func,
+};
+
+const SvgLink = styled(Link)`
+ ${SvgButtonOrLinkCss}
+`;
+
+const IconLink = ({ name, icon, to, state }) => (
+
+ {icon}
+
+);
+IconLink.propTypes = {
+ name: PropTypes.string,
+ icon: PropTypes.node,
+ to: PropTypes.string,
+ state: PropTypes.object,
+};
+
+const CopyIconButton = ({
+ name,
+ data,
+ showToast = false,
+ customMsg = false,
+}) => {
+ return (
+ {
+ if (navigator.clipboard) {
+ navigator.clipboard.writeText(data);
+ }
+ if (showToast) {
+ const toastMsg = customMsg
+ ? customMsg
+ : `"${data}" copied to clipboard`;
+ toast.success(toastMsg);
+ }
+ }}
+ >
+
+
+ );
+};
+
+CopyIconButton.propTypes = {
+ name: PropTypes.string,
+ data: PropTypes.string,
+ showToast: PropTypes.bool,
+ customMsg: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]),
+ children: PropTypes.node,
+};
+
export default PrimaryButton;
-export { SecondaryButton, SecondaryLink };
+export { SecondaryButton, SecondaryLink, IconButton, IconLink, CopyIconButton };
diff --git a/cashtab/src/components/Contacts/__tests__/index.test.js b/cashtab/src/components/Contacts/__tests__/index.test.js
--- a/cashtab/src/components/Contacts/__tests__/index.test.js
+++ b/cashtab/src/components/Contacts/__tests__/index.test.js
@@ -117,7 +117,23 @@
// Contacts component is rendered
expect(screen.getByTitle('Contacts')).toBeInTheDocument();
- // Click the first row Delete button
+ // We can copy a contact's address to the clipboard
+ await user.click(
+ screen.getByRole('button', {
+ name: /Copy alpha/i,
+ }),
+ );
+
+ // Confirm copy success notification is triggered
+ await waitFor(() => {
+ expect(
+ screen.getByText(
+ `"ecash:qp89xgjhcqdnzzemts0aj378nfe2mhu9yvxj9nhgg6" copied to clipboard`,
+ ),
+ ).toBeInTheDocument();
+ });
+
+ // Delete the first saved wallet, "alpha"
await user.click(
screen.getByRole('button', {
name: /Delete alpha/i,
@@ -281,7 +297,11 @@
);
// Click the first row Send button
- await user.click(screen.getAllByTitle('tx-sent')[0]);
+ await user.click(
+ screen.getByRole('link', {
+ name: /Send to alpha/i,
+ }),
+ );
// Now we are on the SendXec page and the address field is filled out
expect(screen.getByPlaceholderText('Address')).toHaveValue(
diff --git a/cashtab/src/components/Contacts/index.js b/cashtab/src/components/Contacts/index.js
--- a/cashtab/src/components/Contacts/index.js
+++ b/cashtab/src/components/Contacts/index.js
@@ -3,11 +3,8 @@
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
import React, { useState } from 'react';
-import { Link } from 'react-router-dom';
import { WalletContext } from 'wallet/context';
-import CopyToClipboard from 'components/Common/CopyToClipboard';
import {
- CopyPasteIcon,
TrashcanIcon,
EditIcon,
SendIcon,
@@ -17,7 +14,10 @@
import { toast } from 'react-toastify';
import PrimaryButton, {
SecondaryButton,
-} from 'components/Common/PrimaryButton';
+ IconButton,
+ IconLink,
+ CopyIconButton,
+} from 'components/Common/Buttons';
import { getContactAddressError, getContactNameError } from 'validation';
import {
ContactList,
@@ -26,7 +26,6 @@
ButtonRow,
ContactListName,
ButtonPanel,
- SvgButton,
} from 'components/Contacts/styles';
const Contacts = () => {
@@ -294,36 +293,33 @@
{contact.name}
-
-
-
-
+ }
onClick={() =>
setContactToBeRenamed(contact)
}
- >
-
-
-
+ }
to="/send"
state={{
contactSend: contact.address,
}}
- >
-
-
-
+ }
onClick={() =>
setContactToBeDeleted(contact)
}
- >
-
-
+ />
))
diff --git a/cashtab/src/components/Contacts/styles.js b/cashtab/src/components/Contacts/styles.js
--- a/cashtab/src/components/Contacts/styles.js
+++ b/cashtab/src/components/Contacts/styles.js
@@ -13,11 +13,6 @@
align-items: center;
gap: 12px;
color: ${props => props.theme.contrast};
- svg {
- height: 24px;
- width: 24px;
- fill: ${props => props.theme.eCashBlue};
- }
box-sizing: border-box;
*,
*:before,
@@ -64,9 +59,3 @@
gap: 9px;
align-items: baseline;
`;
-
-export const SvgButton = styled.button`
- border: none;
- background: none;
- cursor: pointer;
-`;
diff --git a/cashtab/src/components/Etokens/CreateTokenForm.js b/cashtab/src/components/Etokens/CreateTokenForm.js
--- a/cashtab/src/components/Etokens/CreateTokenForm.js
+++ b/cashtab/src/components/Etokens/CreateTokenForm.js
@@ -14,7 +14,7 @@
isValidTokenDocumentUrl,
isProbablyNotAScam,
} from 'validation';
-import PrimaryButton from 'components/Common/PrimaryButton';
+import PrimaryButton from 'components/Common/Buttons';
import {
Input,
SendTokenInput,
diff --git a/cashtab/src/components/Etokens/Etokens.js b/cashtab/src/components/Etokens/Etokens.js
--- a/cashtab/src/components/Etokens/Etokens.js
+++ b/cashtab/src/components/Etokens/Etokens.js
@@ -10,7 +10,7 @@
import { getWalletState } from 'utils/cashMethods';
import appConfig from 'config/app';
import { getUserLocale } from 'helpers';
-import { PrimaryLink } from 'components/Common/PrimaryButton';
+import { PrimaryLink } from 'components/Common/Buttons';
const EtokensCtn = styled.div`
color: ${props => props.theme.contrast};
diff --git a/cashtab/src/components/OnBoarding/OnBoarding.js b/cashtab/src/components/OnBoarding/OnBoarding.js
--- a/cashtab/src/components/OnBoarding/OnBoarding.js
+++ b/cashtab/src/components/OnBoarding/OnBoarding.js
@@ -6,9 +6,7 @@
import styled from 'styled-components';
import { WalletContext } from 'wallet/context';
import { Input, InputFlex } from 'components/Common/Inputs';
-import PrimaryButton, {
- SecondaryButton,
-} from 'components/Common/PrimaryButton';
+import PrimaryButton, { SecondaryButton } from 'components/Common/Buttons';
import { Event } from 'components/Common/GoogleAnalytics';
import { validateMnemonic } from 'validation';
import appConfig from 'config/app';
diff --git a/cashtab/src/components/Send/SendToken.js b/cashtab/src/components/Send/SendToken.js
--- a/cashtab/src/components/Send/SendToken.js
+++ b/cashtab/src/components/Send/SendToken.js
@@ -5,9 +5,7 @@
import React, { useState, useEffect } from 'react';
import { Link, useParams } from 'react-router-dom';
import { WalletContext } from 'wallet/context';
-import PrimaryButton, {
- SecondaryButton,
-} from 'components/Common/PrimaryButton';
+import PrimaryButton, { SecondaryButton } from 'components/Common/Buttons';
import { TxLink, SwitchLabel } from 'components/Common/Atoms';
import BalanceHeaderToken from 'components/Common/BalanceHeaderToken';
import { useNavigate } from 'react-router-dom';
diff --git a/cashtab/src/components/Send/SendXec.js b/cashtab/src/components/Send/SendXec.js
--- a/cashtab/src/components/Send/SendXec.js
+++ b/cashtab/src/components/Send/SendXec.js
@@ -7,7 +7,7 @@
import { WalletContext } from 'wallet/context';
import { CashReceivedNotificationIcon } from 'components/Common/CustomIcons';
import Modal from 'components/Common/Modal';
-import PrimaryButton from 'components/Common/PrimaryButton';
+import PrimaryButton from 'components/Common/Buttons';
import { toSatoshis, toXec } from 'wallet';
import { getMaxSendAmountSatoshis } from 'ecash-coinselect';
import { sumOneToManyXec } from 'utils/cashMethods';
diff --git a/cashtab/src/components/SignVerifyMsg/SignVerifyMsg.js b/cashtab/src/components/SignVerifyMsg/SignVerifyMsg.js
--- a/cashtab/src/components/SignVerifyMsg/SignVerifyMsg.js
+++ b/cashtab/src/components/SignVerifyMsg/SignVerifyMsg.js
@@ -8,9 +8,7 @@
import Switch from 'components/Common/Switch';
import { WalletContext } from 'wallet/context';
import CopyToClipboard from 'components/Common/CopyToClipboard';
-import PrimaryButton, {
- SecondaryButton,
-} from 'components/Common/PrimaryButton';
+import PrimaryButton, { SecondaryButton } from 'components/Common/Buttons';
import xecMessage from 'bitcoinjs-message';
import * as utxolib from '@bitgo/utxo-lib';
import cashaddr from 'ecashaddrjs';
diff --git a/cashtab/src/components/Swap/Swap.js b/cashtab/src/components/Swap/Swap.js
--- a/cashtab/src/components/Swap/Swap.js
+++ b/cashtab/src/components/Swap/Swap.js
@@ -4,7 +4,7 @@
import React from 'react';
import styled from 'styled-components';
-import PrimaryButton from 'components/Common/PrimaryButton';
+import PrimaryButton from 'components/Common/Buttons';
import { isValidSideshiftObj } from 'validation';
import { AlertMsg } from 'components/Common/Atoms';
diff --git a/cashtab/src/components/Wallets/__tests__/index.test.js b/cashtab/src/components/Wallets/__tests__/index.test.js
--- a/cashtab/src/components/Wallets/__tests__/index.test.js
+++ b/cashtab/src/components/Wallets/__tests__/index.test.js
@@ -126,8 +126,11 @@
expect((await screen.findAllByText('alpha'))[1]).toBeInTheDocument();
// Click button to add this saved wallet to contacts
- // Note we want index 1 of these buttons, as index 0 is the active wallet
- await user.click(screen.getAllByTitle('add-contact')[1]);
+ await user.click(
+ screen.getByRole('button', {
+ name: /Add alpha to contacts/i,
+ }),
+ );
// Confirm new wallet added to contacts
await waitFor(async () =>
@@ -142,6 +145,48 @@
),
).toBeInTheDocument();
});
+ it('We can copy the address of a savedWallet to the clipboard', async () => {
+ // localforage defaults
+ const mockedChronik = await initializeCashtabStateForTests(
+ walletWithXecAndTokens,
+ localforage,
+ );
+
+ // Custom contact list
+ await localforage.setItem('contactList', populatedContactList);
+
+ const savedWallet = validSavedWallets[0];
+
+ await localforage.setItem('wallets', [
+ walletWithXecAndTokens,
+ savedWallet,
+ ]);
+
+ render();
+
+ // Wait for the app to load
+ await waitFor(() =>
+ expect(
+ screen.queryByTitle('Cashtab Loading'),
+ ).not.toBeInTheDocument(),
+ );
+
+ // Click button to add this saved wallet to contacts
+ await user.click(
+ screen.getByRole('button', {
+ name: /Copy address of alpha/i,
+ }),
+ );
+
+ // Confirm copy success notification is triggered
+ await waitFor(() => {
+ expect(
+ screen.getByText(
+ `"ecash:qzs4zzxs0gvfrc6e2wqhkmvj4dmmh332cvfpd7yjep" copied to clipboard`,
+ ),
+ ).toBeInTheDocument();
+ });
+ });
it('Confirm mocked bip39.generateMnemonic() returns the expected seed', () => {
expect(bip39.generateMnemonic()).toBe(
'grant grass sock faculty behave guitar pepper tiger sustain task occur soon',
@@ -188,7 +233,11 @@
expect((await screen.findAllByText('echo'))[1]).toBeInTheDocument();
// Let's rename alpha. Its button will be the second edit button, as the first is for the active wallet.
- await user.click(screen.getAllByTitle('edit')[1]);
+ await user.click(
+ screen.getByRole('button', {
+ name: /Rename alpha/i,
+ }),
+ );
// We see a modal.
expect(await screen.findByText(`Rename "alpha"?`)).toBeInTheDocument();
@@ -229,7 +278,11 @@
).toBeInTheDocument();
// Now let's rename the active wallet
- await user.click(screen.getAllByTitle('edit')[0]);
+ await user.click(
+ screen.getByRole('button', {
+ name: /Rename Transaction Fixtures/i,
+ }),
+ );
await user.type(
await screen.findByPlaceholderText('Enter new wallet name'),
@@ -256,7 +309,11 @@
// We can delete a wallet
// Delete the first wallet in the savedWallets list
// It's the first appearance of the trashcan button bc we do not support deleting the active wallet
- await user.click(screen.getAllByTitle('trashcan')[0]);
+ await user.click(
+ screen.getByRole('button', {
+ name: /Delete ALPHA PRIME/i,
+ }),
+ );
// We see a confirmation modal
expect(
@@ -411,10 +468,10 @@
// We can change the active wallet
- // Activate the first wallet in the list
+ // Activate bravo
// Since ALPHA PRIME has been deleted, "bravo" is the first wallet in the list
await user.click(
- screen.getAllByRole('button', { name: 'Activate' })[0],
+ screen.getByRole('button', { name: /Activate bravo/ }),
);
// Now "bravo" is the active wallet
diff --git a/cashtab/src/components/Wallets/index.js b/cashtab/src/components/Wallets/index.js
--- a/cashtab/src/components/Wallets/index.js
+++ b/cashtab/src/components/Wallets/index.js
@@ -4,9 +4,7 @@
import React, { useState } from 'react';
import { WalletContext } from 'wallet/context';
-import CopyToClipboard from 'components/Common/CopyToClipboard';
import {
- CopyPasteIcon,
TrashcanIcon,
EditIcon,
AddContactIcon,
@@ -16,7 +14,9 @@
import { toast } from 'react-toastify';
import PrimaryButton, {
SecondaryButton,
-} from 'components/Common/PrimaryButton';
+ IconButton,
+ CopyIconButton,
+} from 'components/Common/Buttons';
import {
WalletsList,
WalletsPanel,
@@ -29,7 +29,6 @@
SvgButtonPanel,
WalletBalance,
ActivateButton,
- SvgButton,
} from 'components/Wallets/styles';
import { getWalletNameError, validateMnemonic } from 'validation';
import {
@@ -368,26 +367,25 @@
(active)
-
-
-
-
+ }
onClick={() =>
setWalletToBeRenamed(wallet)
}
- >
-
-
-
+ }
onClick={() =>
addWalletToContacts(wallet)
}
- >
-
-
+ />
) : (
@@ -410,38 +408,38 @@
-
-
-
-
+ }
onClick={() =>
setWalletToBeRenamed(wallet)
}
- >
-
-
-
+ }
onClick={() =>
addWalletToContacts(wallet)
}
- >
-
-
-
+ }
onClick={() =>
setWalletToBeDeleted(wallet)
}
- >
-
-
+ />
activateWallet(wallet, wallets)
}
diff --git a/cashtab/src/components/Wallets/styles.js b/cashtab/src/components/Wallets/styles.js
--- a/cashtab/src/components/Wallets/styles.js
+++ b/cashtab/src/components/Wallets/styles.js
@@ -13,11 +13,6 @@
align-items: center;
gap: 12px;
color: ${props => props.theme.contrast};
- svg {
- height: 24px;
- width: 24px;
- fill: ${props => props.theme.eCashBlue};
- }
box-sizing: border-box;
*,
*:before,
@@ -63,7 +58,6 @@
export const SvgButtonPanel = styled.div`
display: flex;
- gap: 9px;
align-items: baseline;
`;
export const ButtonPanel = styled.div`
@@ -72,11 +66,6 @@
align-items: center;
justify-content: center;
`;
-export const SvgButton = styled.div`
- border: none;
- background: none;
- cursor: pointer;
-`;
export const WalletBalance = styled.div`
display: flex;