diff --git a/app/components/button.js b/app/components/button.js index 21b9120..4795d62 100644 --- a/app/components/button.js +++ b/app/components/button.js @@ -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, diff --git a/app/components/clipboard.js b/app/components/clipboard.js new file mode 100644 index 0000000..bda14ca --- /dev/null +++ b/app/components/clipboard.js @@ -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 { + 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 ( + } + > +
Confirm content
+ + )} + + diff --git a/app/components/input.js b/app/components/input.js index a9917f2..7b4fad0 100644 --- a/app/components/input.js +++ b/app/components/input.js @@ -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) => 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: () => ( onChange(evt.target.value)} {...props} /> diff --git a/app/components/sidebar.js b/app/components/sidebar.js index f36798b..a0fc896 100644 --- a/app/components/sidebar.js +++ b/app/components/sidebar.js @@ -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) => ( {item.label} diff --git a/app/components/status-pill.js b/app/components/status-pill.js index d4c8e53..235f50a 100644 --- a/app/components/status-pill.js +++ b/app/components/status-pill.js @@ -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 = {}; diff --git a/app/containers/settings.js b/app/containers/settings.js new file mode 100644 index 0000000..ffbf9e6 --- /dev/null +++ b/app/containers/settings.js @@ -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); diff --git a/app/router/router.js b/app/router/router.js index 2d9369e..ea67cd5 100644 --- a/app/router/router.js +++ b/app/router/router.js @@ -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 }) => ( /> - +
settings
; +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 { + 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 ( + + ( + + )} + onConfirm={this.exportViewKeys} + showButtons={!successExportViewKeys} + width={750} + > + + {successExportViewKeys ? ( + viewKeys.map(({ zAddress, key }) => ( + <> + + + { + event.currentTarget.select(); + }} + /> + + + + )) + ) : ( + + )} + + + + ( + + )} + onConfirm={this.exportPrivateKeys} + showButtons={!successExportPrivateKeys} + width={750} + > + + {successExportPrivateKeys ? ( + privateKeys.map(({ zAddress, key }) => ( + <> + + + { + event.currentTarget.select(); + }} + /> + + + + )) + ) : ( + + )} + + + + ( + + )} + onConfirm={this.importPrivateKeys} + showButtons={!successImportPrivateKeys} + width={900} + isLoading={isLoading} + > + + + this.setState({ importedPrivateKeys: value })} + inputType='textarea' + rows={10} + /> + {successImportPrivateKeys && ( + + )} + {error && } + + + + + + ); + }; +} diff --git a/config/electron.js b/config/electron.js index 7edd495..2096627 100644 --- a/config/electron.js +++ b/config/electron.js @@ -52,7 +52,7 @@ const createWindow = () => { }); mainWindow = new BrowserWindow({ - width: 800, + width: 1000, height: 600, transparent: false, frame: true,