Changeset View
Changeset View
Standalone View
Standalone View
web/cashtab/src/components/Configure/Configure.js
- This file was added.
/* eslint-disable react-hooks/exhaustive-deps */ | |||||
import React, { useState, useEffect } from 'react'; | |||||
import styled from 'styled-components'; | |||||
import { Icon, Collapse, Form, Input, Modal } from 'antd'; | |||||
import { CashSpin, CashSpinIcon } from '../Common/CustomSpinner'; | |||||
import { WalletContext } from '../../utils/context'; | |||||
import { StyledCollapse } from '../Common/StyledCollapse'; | |||||
import PrimaryButton, { SecondaryButton, SmartButton } from '../Common/PrimaryButton'; | |||||
import { CashLoader } from '../Common/CustomIcons'; | |||||
import { ReactComponent as Trashcan } from '../../assets/trashcan.svg'; | |||||
import { ReactComponent as Edit } from '../../assets/edit.svg'; | |||||
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; | |||||
`; | |||||
export default () => { | |||||
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 => { | |||||
const savedWallets = await getSavedWallets(activeWallet); | |||||
setSavedWallets(savedWallets); | |||||
}; | |||||
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 => { | |||||
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 => { | |||||
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; | |||||
} | |||||
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 ( | |||||
<CashSpin spinning={loading} indicator={CashSpinIcon}> | |||||
<StyledConfigure> | |||||
{walletToBeRenamed !== null && ( | |||||
<Modal | |||||
title={`Rename Wallet ${walletToBeRenamed.name}`} | |||||
visible={showRenameWalletModal} | |||||
onOk={changeWalletName} | |||||
onCancel={() => cancelRenameWallet()} | |||||
> | |||||
<Form style={{ width: 'auto' }}> | |||||
<Form.Item | |||||
validateStatus={ | |||||
newWalletNameIsValid !== null && newWalletNameIsValid ? '' : 'error' | |||||
} | |||||
help={ | |||||
newWalletNameIsValid !== null && newWalletNameIsValid | |||||
? '' | |||||
: 'Wallet name must be a string between 1 and 24 characters long' | |||||
} | |||||
> | |||||
<Input | |||||
prefix={<Icon type="wallet" />} | |||||
placeholder="Enter new wallet name" | |||||
name="newName" | |||||
value={newWalletName} | |||||
onChange={e => handleWalletNameInput(e)} | |||||
/> | |||||
</Form.Item> | |||||
</Form> | |||||
</Modal> | |||||
)} | |||||
{walletToBeDeleted !== null && ( | |||||
<Modal | |||||
title={`Are you suer you want to delete wallet "${walletToBeDeleted.name}"?`} | |||||
visible={showDeleteWalletModal} | |||||
onOk={deleteSelectedWallet} | |||||
onCancel={() => cancelDeleteWallet()} | |||||
> | |||||
<Form style={{ width: 'auto' }}> | |||||
<Form.Item | |||||
validateStatus={walletDeleteValid !== null && walletDeleteValid ? '' : 'error'} | |||||
help={ | |||||
walletDeleteValid !== null && walletDeleteValid | |||||
? '' | |||||
: 'Your confirmation phrase must match exactly' | |||||
} | |||||
> | |||||
<Input | |||||
prefix={<Icon type="wallet" />} | |||||
placeholder={`Type "delete ${walletToBeDeleted.name}" to confirm`} | |||||
name="walletToBeDeletedInput" | |||||
value={confirmationOfWalletToBeDeleted} | |||||
onChange={e => handleWalletToDeleteInput(e)} | |||||
/> | |||||
</Form.Item> | |||||
</Form> | |||||
</Modal> | |||||
)} | |||||
<h2> | |||||
<Icon type="copy" theme="outlined" /> Seed Phrase | |||||
</h2> | |||||
<p> | |||||
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. | |||||
</p> | |||||
{wallet && wallet.mnemonic && ( | |||||
<StyledCollapse> | |||||
<Panel header="Click to reveal seed phrase" key="1" disabled={!(wallet || {}).mnemonic}> | |||||
<p>{wallet && wallet.mnemonic ? wallet.mnemonic : ''}</p> | |||||
</Panel> | |||||
</StyledCollapse> | |||||
)} | |||||
{savedWallets && savedWallets.length > 0 && ( | |||||
<> | |||||
<StyledSpacer /> | |||||
<StyledCollapse> | |||||
<Panel | |||||
header="Saved wallets" | |||||
key="2" | |||||
disabled={savedWallets && savedWallets.length < 1} | |||||
> | |||||
<AWRow> | |||||
<h3>{wallet.name}</h3> | |||||
<h4>Currently active</h4> | |||||
</AWRow> | |||||
<div> | |||||
{savedWallets.map(sw => ( | |||||
<SWRow key={sw.name}> | |||||
<SWName> | |||||
<h3>{sw.name}</h3> | |||||
</SWName> | |||||
<SWButtonCtn> | |||||
<Edit onClick={() => showPopulatedRenameWalletModal(sw)} /> | |||||
<Trashcan onClick={() => showPopulatedDeleteWalletModal(sw)} /> | |||||
<button onClick={() => updateSavedWalletsOnLoad(sw)}>Activate</button> | |||||
</SWButtonCtn> | |||||
</SWRow> | |||||
))} | |||||
</div> | |||||
</Panel> | |||||
</StyledCollapse> | |||||
</> | |||||
)} | |||||
<StyledSpacer /> | |||||
{apiError ? ( | |||||
<> | |||||
<CashLoader /> | |||||
<p style={{ color: 'red' }}> | |||||
<b>An error occured on our end. Reconnecting...</b> | |||||
</p> | |||||
</> | |||||
) : ( | |||||
<> | |||||
<PrimaryButton onClick={() => updateSavedWalletsOnCreate()}> | |||||
<Icon type="plus-square" /> New Wallet | |||||
</PrimaryButton> | |||||
<SecondaryButton onClick={() => openSeedInput(!seedInput)}> | |||||
<Icon type="import" /> Import Wallet | |||||
</SecondaryButton> | |||||
{seedInput && ( | |||||
<> | |||||
<p>Copy and paste your mnemonic seed phrase below to import an existing wallet</p> | |||||
<Form style={{ width: 'auto' }}> | |||||
<Form.Item | |||||
validateStatus={!formData.dirty && !formData.mnemonic ? 'error' : ''} | |||||
help={ | |||||
!formData.dirty && !formData.mnemonic ? 'Mnemonic seed phrase required' : '' | |||||
} | |||||
> | |||||
<Input | |||||
prefix={<Icon type="lock" />} | |||||
placeholder="mnemonic (seed phrase)" | |||||
name="mnemonic" | |||||
onChange={e => handleChange(e)} | |||||
required | |||||
/> | |||||
</Form.Item> | |||||
<SmartButton disabled={!isValidMnemonic} onClick={() => submit()}> | |||||
Import | |||||
</SmartButton> | |||||
</Form> | |||||
</> | |||||
)} | |||||
</> | |||||
)} | |||||
</StyledConfigure> | |||||
</CashSpin> | |||||
); | |||||
}; |