Page MenuHomePhabricator

No OneTemporary

diff --git a/web/cashtab/src/components/Configure/Configure.js b/web/cashtab/src/components/Configure/Configure.js
index 2874c91bc..fd42f009a 100644
--- a/web/cashtab/src/components/Configure/Configure.js
+++ b/web/cashtab/src/components/Configure/Configure.js
@@ -1,557 +1,559 @@
/* 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;
`;
-export default () => {
+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 (
<Spin spinning={loading} indicator={CashLoadingIcon}>
<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={<WalletFilled />}
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={<WalletFilled />}
placeholder={`Type "delete ${walletToBeDeleted.name}" to confirm`}
name="walletToBeDeletedInput"
value={confirmationOfWalletToBeDeleted}
onChange={e => handleWalletToDeleteInput(e)}
/>
</Form.Item>
</Form>
</Modal>
)}
<h2>
<CopyOutlined /> 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">
<p>
{wallet && wallet.mnemonic
? wallet.mnemonic
: ''}
</p>
</Panel>
</StyledCollapse>
)}
{savedWallets && savedWallets.length > 0 && (
<>
<StyledSpacer />
<StyledCollapse>
<Panel header="Saved wallets" key="2">
<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()}
>
<PlusSquareOutlined /> New Wallet
</PrimaryButton>
<SecondaryButton
onClick={() => openSeedInput(!seedInput)}
>
<ImportOutlined /> 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={<LockOutlined />}
placeholder="mnemonic (seed phrase)"
name="mnemonic"
onChange={e => handleChange(e)}
required
/>
</Form.Item>
<SmartButton
disabled={!isValidMnemonic}
onClick={() => submit()}
>
Import
</SmartButton>
</Form>
</>
)}
</>
)}
</StyledConfigure>
</Spin>
);
};
+
+export default Configure;
diff --git a/web/cashtab/src/components/NotFound.js b/web/cashtab/src/components/NotFound.js
index af2f67bf8..2757f3a7d 100644
--- a/web/cashtab/src/components/NotFound.js
+++ b/web/cashtab/src/components/NotFound.js
@@ -1,10 +1,12 @@
import React from 'react';
import { Row, Col } from 'antd';
-export default () => (
+const NotFound = () => (
<Row justify="center" type="flex">
<Col span={8}>
<h1>Page not found</h1>
</Col>
</Row>
);
+
+export default NotFound;
diff --git a/web/cashtab/src/utils/GoogleAnalytics.js b/web/cashtab/src/utils/GoogleAnalytics.js
index ecb24cc68..c50d8386f 100644
--- a/web/cashtab/src/utils/GoogleAnalytics.js
+++ b/web/cashtab/src/utils/GoogleAnalytics.js
@@ -1,75 +1,77 @@
// utils/GoogleAnalytics.js
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import * as ReactGA from 'react-ga';
import { Route } from 'react-router-dom';
class GoogleAnalytics extends Component {
componentDidMount() {
this.logPageChange(
this.props.location.pathname,
this.props.location.search,
);
}
componentDidUpdate({ location: prevLocation }) {
const {
location: { pathname, search },
} = this.props;
const isDifferentPathname = pathname !== prevLocation.pathname;
const isDifferentSearch = search !== prevLocation.search;
if (isDifferentPathname || isDifferentSearch) {
this.logPageChange(pathname, search);
}
}
logPageChange(pathname, search = '') {
const page = pathname + search;
const { location } = window;
ReactGA.set({
page,
location: `${location.origin}${page}`,
...this.props.options,
});
ReactGA.pageview(page);
}
render() {
return null;
}
}
GoogleAnalytics.propTypes = {
location: PropTypes.shape({
pathname: PropTypes.string,
search: PropTypes.string,
}).isRequired,
options: PropTypes.object,
};
const RouteTracker = () => <Route component={GoogleAnalytics} />;
const init = (options = {}) => {
const isGAEnabled = process.env.NODE_ENV === 'production';
if (isGAEnabled) {
ReactGA.initialize('UA-183678810-1');
}
return isGAEnabled;
};
export const Event = (category, action, label) => {
ReactGA.event({
category: category,
action: action,
label: label,
});
};
-export default {
+const GoogleAnalyticsDefault = {
GoogleAnalytics,
RouteTracker,
init,
};
+
+export default GoogleAnalyticsDefault;

File Metadata

Mime Type
text/x-diff
Expires
Sun, Mar 2, 12:34 (1 d, 13 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
5187857
Default Alt Text
(22 KB)

Event Timeline