diff --git a/web/cashtab/extension/public/manifest.json b/web/cashtab/extension/public/manifest.json index cdbcdfe14..a8012d2e8 100644 --- a/web/cashtab/extension/public/manifest.json +++ b/web/cashtab/extension/public/manifest.json @@ -1,30 +1,30 @@ { "manifest_version": 2, "name": "Cashtab", "description": "A browser-integrated eCash wallet from Bitcoin ABC", - "version": "1.0.5", + "version": "1.0.6", "content_scripts": [ { "matches": ["file://*/*", "http://*/*", "https://*/*"], "js": ["contentscript.js"], "run_at": "document_idle", "all_frames": true } ], "background": { "scripts": ["background.js"], "persistent": false }, "browser_action": { "default_popup": "index.html", "default_title": "Cashtab" }, "icons": { "16": "ecash16.png", "48": "ecash48.png", "128": "ecash128.png", "192": "ecash192.png", "512": "ecash512.png" } } diff --git a/web/cashtab/src/components/Configure/Configure.js b/web/cashtab/src/components/Configure/Configure.js index bebf2baa7..8dfd5d52d 100644 --- a/web/cashtab/src/components/Configure/Configure.js +++ b/web/cashtab/src/components/Configure/Configure.js @@ -1,792 +1,813 @@ /* eslint-disable react-hooks/exhaustive-deps */ import React, { useState, useEffect } from 'react'; import styled from 'styled-components'; import { Collapse, Form, Input, Modal, Alert, Switch, Tag } from 'antd'; import { PlusSquareOutlined, WalletFilled, ImportOutlined, LockOutlined, CheckOutlined, CloseOutlined, LockFilled, ExclamationCircleFilled, } from '@ant-design/icons'; import { WalletContext, AuthenticationContext } from '@utils/context'; import { SidePaddingCtn } from '@components/Common/Atoms'; import { StyledCollapse } from '@components/Common/StyledCollapse'; import { AntdFormWrapper, CurrencySelectDropdown, } from '@components/Common/EnhancedInputs'; import PrimaryButton, { SecondaryButton, SmartButton, } from '@components/Common/PrimaryButton'; import { ThemedCopyOutlined, ThemedWalletOutlined, ThemedDollarOutlined, ThemedSettingOutlined, } 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 { useHistory } from 'react-router-dom'; 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 PrivacyLink = styled.button` + text-decoration: underline; + color: ${props => props.theme.eCashBlue}; + :visited { + text-decoration: underline; + color: ${props => props.theme.eCashBlue}; + } + :hover { + color: ${props => props.theme.eCashPurple}; + } + background: none !important; + border: none; + padding: 0 !important; + cursor: pointer; +`; + const SWRow = styled.div` border-radius: 3px; padding: 10px 0; display: flex; align-items: center; justify-content: center; margin-bottom: 6px; @media (max-width: 500px) { flex-direction: column; margin-bottom: 12px; } `; const SWName = styled.div` width: 50%; display: flex; align-items: center; justify-content: space-between; word-wrap: break-word; hyphens: auto; @media (max-width: 500px) { width: 100%; justify-content: center; margin-bottom: 15px; } h3 { font-size: 16px; color: ${props => props.theme.darkBlue}; margin: 0; text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } h3.overflow { width: 100px; overflow: hidden; text-overflow: ellipsis; } h3.overflow:hover { background-color: ${props => props.theme.settings.background}; overflow: visible; inline-size: 100px; white-space: normal; } `; const SWBalance = styled.div` width: 50%; display: flex; align-items: center; justify-content: space-between; word-wrap: break-word; hyphens: auto; @media (max-width: 500px) { width: 100%; justify-content: center; margin-bottom: 15px; } div { font-size: 13px; color: ${props => props.theme.darkBlue}; margin: 0; text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } div.overflow { width: 150px; overflow: hidden; text-overflow: ellipsis; } div.overflow:hover { background-color: ${props => props.theme.settings.background}; overflow: visible; inline-size: 150px; white-space: normal; } `; const SWButtonCtn = styled.div` width: 50%; display: flex; align-items: center; justify-content: flex-end; @media (max-width: 500px) { width: 100%; justify-content: center; } button { cursor: pointer; background: transparent; border: 1px solid #fff; box-shadow: none; color: #fff; border-radius: 3px; opacity: 0.6; transition: all 200ms ease-in-out; :hover { opacity: 1; background: ${props => props.theme.eCashBlue}; border-color: ${props => props.theme.eCashBlue}; } @media (max-width: 768px) { font-size: 14px; } } svg { stroke: ${props => props.theme.eCashBlue}; fill: ${props => props.theme.eCashBlue}; width: 25px; height: 25px; margin-right: 20px; cursor: pointer; :first-child:hover { stroke: ${props => props.theme.eCashBlue}; fill: ${props => props.theme.eCashBlue}; } :hover { stroke: ${props => props.theme.settings.delete}; fill: ${props => props.theme.settings.delete}; } } `; const 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}; } `; 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}; } } `; const Configure = () => { const ContextValue = React.useContext(WalletContext); const authentication = React.useContext(AuthenticationContext); const { wallet, apiError } = ContextValue; + const history = useHistory(); const { addNewSavedWallet, activateWallet, renameWallet, deleteWallet, validateMnemonic, getSavedWallets, cashtabSettings, changeCashtabSettings, } = ContextValue; const [savedWallets, setSavedWallets] = useState([]); const [formData, setFormData] = useState({ dirty: true, mnemonic: '', }); const [showRenameWalletModal, setShowRenameWalletModal] = useState(false); const [showDeleteWalletModal, setShowDeleteWalletModal] = useState(false); const [walletToBeRenamed, setWalletToBeRenamed] = useState(null); const [walletToBeDeleted, setWalletToBeDeleted] = useState(null); const [newWalletName, setNewWalletName] = useState(''); const [ confirmationOfWalletToBeDeleted, setConfirmationOfWalletToBeDeleted, ] = useState(''); const [newWalletNameIsValid, setNewWalletNameIsValid] = useState(null); const [walletDeleteValid, setWalletDeleteValid] = useState(null); const [seedInput, openSeedInput] = useState(false); const [showTranslationWarning, setShowTranslationWarning] = useState(false); const showPopulatedDeleteWalletModal = walletInfo => { setWalletToBeDeleted(walletInfo); setShowDeleteWalletModal(true); }; const showPopulatedRenameWalletModal = walletInfo => { setWalletToBeRenamed(walletInfo); setShowRenameWalletModal(true); }; const cancelRenameWallet = () => { // Delete form value setNewWalletName(''); setShowRenameWalletModal(false); }; const cancelDeleteWallet = () => { setWalletToBeDeleted(null); setConfirmationOfWalletToBeDeleted(''); setShowDeleteWalletModal(false); }; const updateSavedWallets = async activeWallet => { if (activeWallet) { let savedWallets; try { savedWallets = await getSavedWallets(activeWallet); setSavedWallets(savedWallets); } catch (err) { console.log(`Error in getSavedWallets()`); console.log(err); } } }; const [isValidMnemonic, setIsValidMnemonic] = useState(null); useEffect(() => { // Update savedWallets every time the active wallet changes updateSavedWallets(wallet); }, [wallet]); useEffect(() => { const detectedBrowserLang = navigator.language; if (!detectedBrowserLang.includes('en-')) { setShowTranslationWarning(true); } }, []); // Need this function to ensure that savedWallets are updated on new wallet creation const updateSavedWalletsOnCreate = async importMnemonic => { // Event("Category", "Action", "Label") // Track number of times a different wallet is activated Event('Configure.js', 'Create Wallet', 'New'); const walletAdded = await addNewSavedWallet(importMnemonic); if (!walletAdded) { Modal.error({ title: 'This wallet already exists!', content: 'Wallet not added', }); } else { Modal.success({ content: 'Wallet added to your saved wallets', }); } await updateSavedWallets(wallet); }; // Same here // TODO you need to lock UI here until this is complete // Otherwise user may try to load an already-loading wallet, wreak havoc with indexedDB const updateSavedWalletsOnLoad = async walletToActivate => { // Event("Category", "Action", "Label") // Track number of times a different wallet is activated Event('Configure.js', 'Activate', ''); await activateWallet(walletToActivate); }; async function submit() { setFormData({ ...formData, dirty: false, }); // Exit if no user input if (!formData.mnemonic) { return; } // Exit if mnemonic is invalid if (!isValidMnemonic) { return; } // Event("Category", "Action", "Label") // Track number of times a different wallet is activated Event('Configure.js', 'Create Wallet', 'Imported'); updateSavedWalletsOnCreate(formData.mnemonic); } const handleChange = e => { const { value, name } = e.target; // Validate mnemonic on change // Import button should be disabled unless mnemonic is valid setIsValidMnemonic(validateMnemonic(value)); setFormData(p => ({ ...p, [name]: value })); }; const changeWalletName = async () => { if (newWalletName === '' || newWalletName.length > 24) { setNewWalletNameIsValid(false); return; } // Hide modal setShowRenameWalletModal(false); // Change wallet name console.log( `Changing wallet ${walletToBeRenamed.name} name to ${newWalletName}`, ); const renameSuccess = await renameWallet( walletToBeRenamed.name, newWalletName, ); if (renameSuccess) { Modal.success({ content: `Wallet "${walletToBeRenamed.name}" renamed to "${newWalletName}"`, }); } else { Modal.error({ content: `Rename failed. All wallets must have a unique name.`, }); } await updateSavedWallets(wallet); // Clear wallet name for form setNewWalletName(''); }; const deleteSelectedWallet = async () => { if (!walletDeleteValid && walletDeleteValid !== null) { return; } if ( confirmationOfWalletToBeDeleted !== `delete ${walletToBeDeleted.name}` ) { setWalletDeleteValid(false); return; } // Hide modal setShowDeleteWalletModal(false); // Change wallet name console.log(`Deleting wallet "${walletToBeDeleted.name}"`); const walletDeletedSuccess = await deleteWallet(walletToBeDeleted); if (walletDeletedSuccess) { Modal.success({ content: `Wallet "${walletToBeDeleted.name}" successfully deleted`, }); } else { Modal.error({ content: `Error deleting ${walletToBeDeleted.name}.`, }); } await updateSavedWallets(wallet); // Clear wallet delete confirmation from form setConfirmationOfWalletToBeDeleted(''); }; const handleWalletNameInput = e => { const { value } = e.target; // validation if (value && value.length && value.length < 24) { setNewWalletNameIsValid(true); } else { setNewWalletNameIsValid(false); } setNewWalletName(value); }; const handleWalletToDeleteInput = e => { const { value } = e.target; if (value && value === `delete ${walletToBeDeleted.name}`) { setWalletDeleteValid(true); } else { setWalletDeleteValid(false); } setConfirmationOfWalletToBeDeleted(value); }; const handleAppLockToggle = (checked, e) => { 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(); } }; return ( {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 && (

{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, ) } /> showPopulatedDeleteWalletModal( sw, ) } />
))}
)}

Fiat Currency

changeCashtabSettings('fiatCurrency', fiatCode) } />

General Settings

Lock App
{authentication ? ( } unCheckedChildren={} checked={ authentication.isAuthenticationRequired && authentication.credentialId ? true : false } // checked={false} onChange={handleAppLockToggle} /> ) : ( }> Not Supported )}
[ Documentation | - + history.push('/privacy')} + > Privacy Policy - + ]
); }; 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 7dd868b0b..f27cbd546 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,805 +1,805 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP @generated exports[`Configure with a wallet 1`] = `

Backup your wallet

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

Manage Wallets

Fiat Currency

US Dollar ($)

General Settings

Lock App
`; exports[`Configure without a wallet 1`] = `

Backup your wallet

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

Manage Wallets

Fiat Currency

US Dollar ($)

General Settings

Lock App
`;