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;