diff --git a/web/cashtab/src/components/Common/EnhancedInputs.js b/web/cashtab/src/components/Common/EnhancedInputs.js index aea72b569..e2c913123 100644 --- a/web/cashtab/src/components/Common/EnhancedInputs.js +++ b/web/cashtab/src/components/Common/EnhancedInputs.js @@ -1,178 +1,179 @@ import * as React from 'react'; import { Form, Input, Select } from 'antd'; import { DollarOutlined, WalletOutlined } from '@ant-design/icons'; import styled from 'styled-components'; import { ScanQRCode } from './ScanQRCode'; import useBCH from '@hooks/useBCH'; import { currency } from '@components/Common/Ticker.js'; export const InputAddonText = styled.span` width: 100%; height: 100%; display: block; ${props => props.disabled ? ` cursor: not-allowed; ` : `cursor: pointer;`} `; export const InputNumberAddonText = styled.span` background-color: #f4f4f4 !important; border: 1px solid rgb(234, 237, 243); color: #3e3f42 !important; height: 50px; line-height: 47px; * { color: #3e3f42 !important; } ${props => props.disabled ? ` cursor: not-allowed; ` : `cursor: pointer;`} `; export const SendBchInput = ({ onMax, inputProps, selectProps, ...otherProps }) => { const { Option } = Select; const currencies = [ { value: currency.ticker, label: currency.ticker, }, { value: 'USD', label: 'USD' }, ]; const currencyOptions = currencies.map(currency => { return ( ); }); const CurrencySelect = ( ); return ( ) : ( ) } {...inputProps} /> {CurrencySelect} max ); }; export const FormItemWithMaxAddon = ({ onMax, inputProps, ...otherProps }) => { return ( } addonAfter={ max } {...inputProps} /> ); }; // loadWithCameraOpen prop: if true, load page with camera scanning open export const FormItemWithQRCodeAddon = ({ onScan, loadWithCameraOpen, inputProps, ...otherProps }) => { return ( } + autoComplete="off" addonAfter={ } {...inputProps} /> ); }; export const AddressValidators = () => { const { BCH } = useBCH(); return { safelyDetectAddressFormat: value => { try { return BCH.Address.detectAddressFormat(value); } catch (error) { return null; } }, isSLPAddress: value => AddressValidators.safelyDetectAddressFormat(value) === 'slpaddr', isBCHAddress: value => AddressValidators.safelyDetectAddressFormat(value) === 'cashaddr', isLegacyAddress: value => AddressValidators.safelyDetectAddressFormat(value) === 'legacy', }(); }; diff --git a/web/cashtab/src/components/Configure/Configure.js b/web/cashtab/src/components/Configure/Configure.js index fd42f009a..5bd0044c7 100644 --- a/web/cashtab/src/components/Configure/Configure.js +++ b/web/cashtab/src/components/Configure/Configure.js @@ -1,559 +1,560 @@ /* eslint-disable react-hooks/exhaustive-deps */ import React, { useState, useEffect } from 'react'; import styled from 'styled-components'; import { Collapse, Form, Input, Modal, Spin } from 'antd'; import { PlusSquareOutlined, WalletFilled, ImportOutlined, CopyOutlined, LockOutlined, } from '@ant-design/icons'; import { WalletContext } from '@utils/context'; import { StyledCollapse } from '@components/Common/StyledCollapse'; import PrimaryButton, { SecondaryButton, SmartButton, } from '@components/Common/PrimaryButton'; import { CashLoader, CashLoadingIcon } 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'; const { Panel } = Collapse; 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: #444; margin: 0; text-align: left; } `; 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; @media (max-width: 768px) { font-size: 14px; } } svg { stroke: #444; fill: #444; width: 25px; height: 25px; margin-right: 20px; cursor: pointer; :first-child:hover { stroke: #ff8d00; fill: #ff8d00; } :hover { stroke: red; fill: red; } } `; 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: #444; margin: 0; text-align: left; font-weight: bold; } h4 { font-size: 16px; display: inline-block; color: #ff8d00 !important; margin: 0; text-align: right; } @media (max-width: 500px) { flex-direction: column; margin-bottom: 12px; } `; const StyledConfigure = styled.div` h2 { color: #444; font-size: 25px; } p { color: #444; } `; const StyledSpacer = styled.div` height: 1px; width: 100%; background-color: #e2e2e2; margin: 60px 0 50px; `; const Configure = () => { const ContextValue = React.useContext(WalletContext); const { wallet, loading, apiError } = ContextValue; const { addNewSavedWallet, activateWallet, renameWallet, deleteWallet, validateMnemonic, getSavedWallets, } = 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 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(false); useEffect(() => { // Update savedWallets every time the active wallet changes updateSavedWallets(wallet); }, [wallet]); // 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); await updateSavedWallets(wallet); }; 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) { 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); }; 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)} />
)}

Seed Phrase

Your seed phrase can be used to restore your wallet in case the original instance of it is destroyed. We highly recommend always making a copy of your seed phrase and keeping it somewhere safe.

{wallet && wallet.mnemonic && (

{wallet && wallet.mnemonic ? wallet.mnemonic : ''}

)} {savedWallets && savedWallets.length > 0 && ( <>

{wallet.name}

Currently active

{savedWallets.map(sw => (

{sw.name}

showPopulatedRenameWalletModal( sw, ) } /> showPopulatedDeleteWalletModal( sw, ) } />
))}
)} {apiError ? ( <>

An error occured on our end. Reconnecting...

) : ( <> updateSavedWalletsOnCreate()} > New Wallet openSeedInput(!seedInput)} > Import Wallet {seedInput && ( <>

Copy and paste your mnemonic seed phrase below to import an existing wallet

} placeholder="mnemonic (seed phrase)" name="mnemonic" + autoComplete="off" onChange={e => handleChange(e)} required /> submit()} > Import
)} )}
); }; export default Configure; diff --git a/web/cashtab/src/components/OnBoarding/OnBoarding.js b/web/cashtab/src/components/OnBoarding/OnBoarding.js index e60ad44a8..27f285d91 100644 --- a/web/cashtab/src/components/OnBoarding/OnBoarding.js +++ b/web/cashtab/src/components/OnBoarding/OnBoarding.js @@ -1,136 +1,137 @@ import React, { useState } from 'react'; import styled from 'styled-components'; import { WalletContext } from '@utils/context'; import { Input, Form, Modal } from 'antd'; import { ExclamationCircleOutlined, PlusSquareOutlined, ImportOutlined, LockOutlined, } from '@ant-design/icons'; import StyledOnboarding from '@components/Common/StyledOnBoarding'; import PrimaryButton, { SecondaryButton, SmartButton, } from '@components/Common/PrimaryButton'; import { currency } from '@components/Common/Ticker.js'; import { Event } from '@utils/GoogleAnalytics'; export const WelcomeText = styled.p` color: #444; width: 100%; font-size: 16px; margin-bottom: 60px; text-align: left; `; export const OnBoarding = ({ history }) => { const ContextValue = React.useContext(WalletContext); const { createWallet, validateMnemonic } = ContextValue; const [formData, setFormData] = useState({ dirty: true, mnemonic: '', }); const [seedInput, openSeedInput] = useState(false); const [isValidMnemonic, setIsValidMnemonic] = useState(false); const { confirm } = Modal; async function submit() { setFormData({ ...formData, dirty: false, }); if (!formData.mnemonic) { return; } // Event("Category", "Action", "Label") // Track number of created wallets from onboarding Event('Onboarding.js', 'Create Wallet', 'Imported'); createWallet(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 })); }; function showBackupConfirmModal() { confirm({ title: "Don't forget to back up your wallet", icon: , content: `Once your wallet is created you can back it up by writing down your 12-word seed. You can find your seed on the Settings page. If you are browsing in Incognito mode or if you clear your browser history, you will lose any funds that are not backed up!`, okText: 'Okay, make me a wallet!', onOk() { // Event("Category", "Action", "Label") // Track number of created wallets from onboarding Event('Onboarding.js', 'Create Wallet', 'New'); createWallet(); }, }); } return ( <> Welcome to CashTab! CashTab is an open source, non-custodial web wallet for {currency.name}.

Web wallets offer user convenience, but storing large amounts of money on a web wallet is not recommended.

Create a new wallet below to get started, or import an existing wallet using a seed phrase.
showBackupConfirmModal()}> New Wallet openSeedInput(!seedInput)}> Import Wallet {seedInput && (
} placeholder="mnemonic (seed phrase)" name="mnemonic" + autoComplete="off" onChange={e => handleChange(e)} required /> submit()} > Import
)} ); };