Merge pull request #37 from andrerfneves/feature/settings-export

Feature/settings export
This commit is contained in:
George Lima 2019-01-21 12:50:40 -02:00 committed by GitHub
commit 7ab19e0029
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 512 additions and 25 deletions

View File

@ -17,7 +17,8 @@ const DefaultButton = styled.button`
outline: none;
min-width: 100px;
border-radius: 100px;
transition: background-color 0.1s ${props => props.theme.colors.transitionEase};
transition: background-color 0.1s
${props => props.theme.colors.transitionEase};
width: 100%;
`;
@ -66,7 +67,7 @@ const Icon = styled.img`
type Props = {
label: string,
onClick?: () => void,
to?: string,
to?: ?string,
variant?: 'primary' | 'secondary',
disabled?: boolean,
icon?: string,

View File

@ -0,0 +1,50 @@
// @flow
import React, { PureComponent } from 'react';
import { Button } from './button';
type Props = {
text: string,
className?: string,
};
type State = { copied: boolean };
export class Clipboard extends PureComponent<Props, State> {
static defaultProps = {
className: '',
};
state = {
copied: false,
};
handleClick = () => {
const { text } = this.props;
const el = document.createElement('textarea');
el.value = text;
if (document.body) document.body.appendChild(el);
el.select();
document.execCommand('copy');
if (document.body) document.body.removeChild(el);
this.setState({ copied: true });
};
render() {
const { className } = this.props;
const { copied } = this.state;
return (
<Button
label={copied ? 'Copied!' : 'Copy!'}
className={className}
onClick={this.handleClick}
disabled={copied}
/>
);
}
}

View File

@ -0,0 +1,103 @@
// @flow
import React, { type Element } from 'react';
import styled from 'styled-components';
import { TextComponent } from './text';
import { Button } from './button';
import { Divider } from './divider';
import { ModalComponent } from './modal';
import CloseIcon from '../assets/images/close_icon.svg';
const Wrapper = styled.div`
display: flex;
width: ${props => `${props.width}px`};
background-color: ${props => props.theme.colors.background};
flex-direction: column;
align-items: center;
border-radius: 6px;
box-shadow: 0px 0px 30px 0px black;
position: relative;
`;
const CloseIconWrapper = styled.div`
display: flex;
width: 100%;
align-items: flex-end;
justify-content: flex-end;
position: absolute;
`;
const TitleWrapper = styled.div`
margin-top: 20px;
margin-bottom: 20px;
`;
const CloseIconImg = styled.img`
width: 16px;
height: 16px;
margin-top: 12px;
margin-right: 12px;
cursor: pointer;
`;
const Btn = styled(Button)`
width: 95%;
margin-bottom: 10px;
`;
type Props = {
renderTrigger: (() => void) => Element<*>,
title: string,
onConfirm: () => void,
showButtons?: boolean,
width?: number,
isLoading?: boolean,
children: Element<*>,
};
export const ConfirmDialogComponent = ({
children,
title,
onConfirm,
renderTrigger,
showButtons,
isLoading,
width,
}: Props) => (
<ModalComponent
renderTrigger={renderTrigger}
closeOnBackdropClick={false}
closeOnEsc={false}
>
{toggle => (
<Wrapper width={width}>
<CloseIconWrapper>
<CloseIconImg src={CloseIcon} onClick={toggle} />
</CloseIconWrapper>
<TitleWrapper>
<TextComponent value={title} align='center' />
</TitleWrapper>
<Divider />
{React.Children.map(children, _ => _)}
{showButtons && (
<>
<Btn label='Confirm' onClick={onConfirm} isLoading={isLoading} />
<Btn
label='Cancel'
onClick={toggle}
variant='secondary'
disabled={isLoading}
/>
</>
)}
</Wrapper>
)}
</ModalComponent>
);
ConfirmDialogComponent.defaultProps = {
showButtons: true,
width: 460,
isLoading: false,
};

View File

@ -0,0 +1,31 @@
---
name: Confirm Dialog
---
import { Playground, PropsTable } from 'docz'
import { Button } from './button.js'
import { ConfirmDialogComponent } from './confirm-dialog.js'
import { DoczWrapper } from '../theme.js'
# Confirm Dialog
## Properties
<PropsTable of={ConfirmDialogComponent} />
## Basic Usage
<Playground>
<DoczWrapper>
{() => (
<ConfirmDialogComponent
title="Confirm example"
onConfirm={() => alert('Confirm')}
renderTrigger={toggle => <button onClick={toggle}> Open! </button>}
>
<div>Confirm content</div>
</ConfirmDialogComponent>
)}
</DoczWrapper>
</Playground>

View File

@ -4,18 +4,14 @@ import React from 'react';
import styled from 'styled-components';
const getDefaultStyles = t => styled[t]`
border-radius: ${// $FlowFixMe
props => props.theme.boxBorderRadius};
border-radius: ${props => props.theme.boxBorderRadius};
border: none;
background-color: ${// $FlowFixMe
props => props.theme.colors.inputBackground};
color: ${// $FlowFixMe
props => props.theme.colors.text};
background-color: ${props => props.theme.colors.inputBackground};
color: ${props => props.theme.colors.text};
padding: 15px;
width: 100%;
outline: none;
font-family: ${// $FlowFixMe
props => props.theme.fontFamily};
font-family: ${props => props.theme.fontFamily};
::placeholder {
opacity: 0.5;
@ -28,7 +24,7 @@ const Textarea = getDefaultStyles('textarea');
type Props = {
inputType?: 'input' | 'textarea',
value: string,
onChange: string => void,
onChange?: string => void,
onFocus?: (SyntheticFocusEvent<HTMLInputElement>) => void,
rows?: number,
disabled?: boolean,
@ -36,7 +32,11 @@ type Props = {
step?: number,
};
export const InputComponent = ({ inputType, onChange, ...props }: Props) => {
export const InputComponent = ({
inputType,
onChange = () => {},
...props
}: Props) => {
const inputTypes = {
input: () => (
<Input onChange={evt => onChange(evt.target.value)} {...props} />

View File

@ -43,7 +43,7 @@ const StyledLink = styled(Link)`
color: ${/* eslint-disable-next-line max-len */
props => (props.isActive
? props.theme.colors.sidebarItemActive
: props.theme.colors.sidebarHoveredItemLabel)};
: '#ddd')}
background-color: ${props => props.theme.colors.sidebarHoveredItem};
}
`;
@ -54,7 +54,7 @@ const Icon = styled.img`
margin-right: 13px;
${StyledLink}:hover & {
filter: ${props => (props.isActive ? 'none' : 'brightness(200%)')};
filter: ${props => (props.isActive ? 'none' : 'brightness(300%)')};
}
`;
@ -83,7 +83,7 @@ export const SidebarComponent = ({ options, location }: Props) => (
<Icon
isActive={isActive}
src={item.icon(isActive)}
alt={`Sidebar Icon ${item.route}`}
alt={`${item.route}`}
/>
{item.label}
</StyledLink>

View File

@ -26,7 +26,7 @@ const Wrapper = styled.div`
display: flex;
background-color: #000;
border-radius: 27px;
padding: 7px 13px;
padding: 8px 16px;
`;
const Icon = styled.img`
@ -41,6 +41,8 @@ const StatusPillLabel = styled(TextComponent)`
color: ${props => props.theme.colors.statusPillLabel};
font-weight: ${props => props.theme.fontWeight.bold};
text-transform: uppercase;
font-size: 10px;
padding-top: 1px;
`;
type Props = {};

View File

@ -0,0 +1,12 @@
// @flow
import { connect } from 'react-redux';
import { SettingsView } from '../views/settings';
import type { AppState } from '../types/app-state';
const mapStateToProps = ({ walletSummary }: AppState) => ({
addresses: walletSummary.addresses,
});
export const SettingsContainer = connect(mapStateToProps)(SettingsView);

View File

@ -10,7 +10,7 @@ import { DashboardContainer } from '../containers/dashboard';
import { TransactionsContainer } from '../containers/transactions';
import { SendContainer } from '../containers/send';
import { ReceiveContainer } from '../containers/receive';
import { SettingsView } from '../views/settings';
import { SettingsContainer } from '../containers/settings';
import { NotFoundView } from '../views/not-found';
import { ConsoleView } from '../views/console';
import { LayoutComponent } from '../components/layout';
@ -59,7 +59,7 @@ export const RouterComponent = ({ location }: { location: Location }) => (
/>
<Route path={SEND_ROUTE} component={SendContainer} />
<Route path={RECEIVE_ROUTE} component={ReceiveContainer} />
<Route path={SETTINGS_ROUTE} component={SettingsView} />
<Route path={SETTINGS_ROUTE} component={SettingsContainer} />
<Route path={CONSOLE_ROUTE} component={ConsoleView} />
<Route
path={TRANSACTIONS_ROUTE}

View File

@ -10,14 +10,15 @@ import { DARK } from './constants/themes';
const darkOne = '#F4B728';
const lightOne = '#ffffff';
const brandOne = '#000';
const brandTwo = '#3B3B3F';
// const brandTwo = '#3B3B3F';
const brandThree = '#5d5d65';
const activeItem = '#F4B728';
const text = '#FFF';
const cardBackgroundColor = '#000';
const sidebarLogoGradientBegin = '#F4B728';
const sidebarLogoGradientEnd = '#FFE240';
const sidebarHoveredItem = '#1C1C1C';
const sidebarHoveredItemLabel = '#76767e';
const sidebarHoveredItemLabel = '#8e8e96';
const background = '#212124';
const transactionSent = '#FF6C6C';
const transactionReceived = '#6AEAC0';
@ -51,14 +52,14 @@ const appTheme = {
dark: lightOne,
}),
sidebarBg: brandOne,
sidebarItem: brandTwo,
sidebarItem: brandThree,
sidebarItemActive: activeItem,
sidebarHoveredItem,
sidebarHoveredItemLabel,
cardBackgroundColor,
text,
activeItem,
inactiveItem: brandTwo,
inactiveItem: brandThree,
sidebarLogoGradientBegin,
sidebarLogoGradientEnd,
background,

View File

@ -1,5 +1,292 @@
// @flow
/* eslint-disable import/no-extraneous-dependencies */
import fs from 'fs';
import { promisify } from 'util';
import React, { PureComponent } from 'react';
import styled from 'styled-components';
import electron from 'electron';
import isDev from 'electron-is-dev';
import dateFns from 'date-fns';
import eres from 'eres';
import React from 'react';
import { Button } from '../components/button';
import { ConfirmDialogComponent } from '../components/confirm-dialog';
import { TextComponent } from '../components/text';
import { InputComponent } from '../components/input';
import { InputLabelComponent } from '../components/input-label';
import { RowComponent } from '../components/row';
import { Clipboard } from '../components/clipboard';
export const SettingsView = () => <div className='settings'>settings</div>;
import rpc from '../../services/api';
const HOME_DIR = electron.remote.app.getPath('home');
const Wrapper = styled.div`
margin-top: ${props => props.theme.layoutContentPaddingTop};
`;
const ModalContent = styled.div`
padding: 20px;
width: 100%;
max-height: 600px;
overflow-y: auto;
p {
word-break: break-all;
}
`;
const Btn = styled(Button)`
margin-bottom: 10px;
`;
const ClipboardButton = styled(Clipboard)`
width: 50px;
border-radius: ${props => props.theme.boxBorderRadius};
height: 45px;
margin-left: 5px;
`;
type Key = {
zAddress: string,
key: string,
};
type Props = {
addresses: string[],
};
type State = {
viewKeys: Key[],
privateKeys: Key[],
importedPrivateKeys: string,
successExportViewKeys: boolean,
successExportPrivateKeys: boolean,
successImportPrivateKeys: boolean,
isLoading: boolean,
error: string | null,
};
export class SettingsView extends PureComponent<Props, State> {
state = {
viewKeys: [],
privateKeys: [],
importedPrivateKeys: '',
isLoading: false,
successExportViewKeys: false,
successExportPrivateKeys: false,
successImportPrivateKeys: false,
error: null,
};
exportViewKeys = () => {
const { addresses } = this.props;
const zAddresses = addresses.filter(addr => addr.startsWith('z'));
this.setState({ isLoading: true });
Promise.all(
zAddresses.map(async (zAddr) => {
const viewKey = await rpc.z_exportviewingkey(zAddr);
return { zAddress: zAddr, key: viewKey };
}),
).then((viewKeys) => {
this.setState({
viewKeys,
successExportViewKeys: true,
isLoading: false,
});
});
};
exportPrivateKeys = () => {
const { addresses } = this.props;
const zAddresses = addresses.filter(addr => addr.startsWith('z'));
this.setState({ isLoading: true });
Promise.all(
zAddresses.map(async (zAddr) => {
const privateKey = await rpc.z_exportkey(zAddr);
return { zAddress: zAddr, key: privateKey };
}),
).then((privateKeys) => {
this.setState({
privateKeys,
successExportPrivateKeys: true,
isLoading: false,
});
});
};
importPrivateKeys = () => {
const { importedPrivateKeys } = this.state;
if (!importedPrivateKeys) return;
const keys = importedPrivateKeys
.split('\n')
.map(key => key.trim())
.filter(key => !!key);
this.setState({ isLoading: true, error: null });
Promise.all(keys.map(key => rpc.z_importkey(key)))
.then(() => {
this.setState({
successImportPrivateKeys: true,
isLoading: false,
});
})
.catch((error) => {
this.setState({ isLoading: false, error: error.message });
});
};
backupWalletDat = async () => {
const backupFileName = `zcash-wallet-backup-${dateFns.format(
new Date(),
'YYYY-MM-DD-mm-ss',
)}.dat`;
electron.remote.dialog.showSaveDialog(
{ defaultPath: backupFileName },
async (pathToSave) => {
if (!pathToSave) return;
const zcashDir = isDev ? `${HOME_DIR}/.zcash/testnet3` : HOME_DIR;
const walletDatPath = `${zcashDir}/wallet.dat`;
const [cannotAccess] = await eres(promisify(fs.access)(walletDatPath));
/* eslint-disable no-alert */
if (cannotAccess) {
alert(
"Couldn't backup the wallet.dat file. You need to back it up manually.",
);
}
const [error] = await eres(
promisify(fs.copyFile)(walletDatPath, pathToSave),
);
if (error) {
alert(
"Couldn't backup the wallet.dat file. You need to back it up manually.",
);
}
},
);
};
render = () => {
const {
viewKeys,
privateKeys,
importedPrivateKeys,
successExportViewKeys,
successExportPrivateKeys,
successImportPrivateKeys,
isLoading,
error,
} = this.state;
return (
<Wrapper>
<ConfirmDialogComponent
title='Export view keys'
renderTrigger={toggleVisibility => (
<Btn label='Export view keys' onClick={toggleVisibility} />
)}
onConfirm={this.exportViewKeys}
showButtons={!successExportViewKeys}
width={750}
>
<ModalContent>
{successExportViewKeys ? (
viewKeys.map(({ zAddress, key }) => (
<>
<InputLabelComponent value={zAddress} />
<RowComponent alignItems='center'>
<InputComponent
value={key}
onFocus={(event) => {
event.currentTarget.select();
}}
/>
<ClipboardButton text={key} />
</RowComponent>
</>
))
) : (
<TextComponent value='Ut id vulputate arcu. Curabitur mattis aliquam magna sollicitudin vulputate. Morbi tempus bibendum porttitor. Quisque dictum ac ipsum a luctus. Donec et lacus ac erat consectetur molestie a id erat.' />
)}
</ModalContent>
</ConfirmDialogComponent>
<ConfirmDialogComponent
title='Export private keys'
renderTrigger={toggleVisibility => (
<Btn label='Export private keys' onClick={toggleVisibility} />
)}
onConfirm={this.exportPrivateKeys}
showButtons={!successExportPrivateKeys}
width={750}
>
<ModalContent>
{successExportPrivateKeys ? (
privateKeys.map(({ zAddress, key }) => (
<>
<InputLabelComponent value={zAddress} />
<RowComponent alignItems='center'>
<InputComponent
value={key}
onFocus={(event) => {
event.currentTarget.select();
}}
/>
<ClipboardButton text={key} />
</RowComponent>
</>
))
) : (
<TextComponent value='Ut id vulputate arcu. Curabitur mattis aliquam magna sollicitudin vulputate. Morbi tempus bibendum porttitor. Quisque dictum ac ipsum a luctus. Donec et lacus ac erat consectetur molestie a id erat.' />
)}
</ModalContent>
</ConfirmDialogComponent>
<ConfirmDialogComponent
title='Import private keys'
renderTrigger={toggleVisibility => (
<Btn label='Import private keys' onClick={toggleVisibility} />
)}
onConfirm={this.importPrivateKeys}
showButtons={!successImportPrivateKeys}
width={900}
isLoading={isLoading}
>
<ModalContent>
<InputLabelComponent value='Please paste your private keys here, one per line. The keys will be imported into your zcashd node' />
<InputComponent
value={importedPrivateKeys}
onChange={value => this.setState({ importedPrivateKeys: value })}
inputType='textarea'
rows={10}
/>
{successImportPrivateKeys && (
<TextComponent
value='Private keys imported in your node'
align='center'
/>
)}
{error && <TextComponent value={error} align='center' />}
</ModalContent>
</ConfirmDialogComponent>
<Btn label='Backup wallet.dat' onClick={this.backupWalletDat} />
</Wrapper>
);
};
}

View File

@ -52,7 +52,7 @@ const createWindow = () => {
});
mainWindow = new BrowserWindow({
width: 800,
width: 1000,
height: 600,
transparent: false,
frame: true,