Merge branch 'develop' of github.com:andrerfneves/zec-react-wallet into develop

This commit is contained in:
André Neves 2019-02-10 10:29:23 -05:00
commit 9eba0b5503
16 changed files with 342 additions and 99 deletions

View File

@ -0,0 +1,18 @@
// @flow
import 'jest-dom/extend-expect';
import { ascii2hex } from '../../app/utils/ascii-to-hexadecimal';
describe('filterObjectNullKeys', () => {
test('should filter null keys from object', () => {
expect(ascii2hex('zcash')).toEqual('7a63617368');
expect(ascii2hex('some text with spaces')).toEqual(
'736f6d652074657874207769746820737061636573',
);
expect(ascii2hex('0')).toEqual('30');
expect(ascii2hex('16')).toEqual('3136');
expect(ascii2hex()).toEqual('');
expect(ascii2hex('')).toEqual('');
});
});

View File

@ -1,6 +1,6 @@
// @flow
import React, { Fragment } from 'react';
import React, { Component, Fragment } from 'react';
import { Provider } from 'react-redux';
import { ConnectedRouter } from 'connected-react-router';
import { ThemeProvider } from 'styled-components';
@ -8,18 +8,43 @@ import { ThemeProvider } from 'styled-components';
import { configureStore, history } from './redux/create';
import { Router } from './router/container';
import theme, { GlobalStyle } from './theme';
import electronStore from '../config/electron-store';
import { DARK } from './constants/themes';
const store = configureStore({});
export const App = () => (
<ThemeProvider theme={theme}>
<Fragment>
<GlobalStyle />
<Provider store={store}>
<ConnectedRouter history={history}>
<Router />
</ConnectedRouter>
</Provider>
</Fragment>
</ThemeProvider>
);
type Props = {};
type State = {
themeMode: string,
};
export class App extends Component<Props, State> {
state = {
themeMode: electronStore.get('THEME_MODE') || DARK,
};
componentDidMount() {
if (!electronStore.has('THEME_MODE')) {
electronStore.set('THEME_MODE', DARK);
}
electronStore.onDidChange('THEME_MODE', newValue => this.setState({ themeMode: newValue }));
}
render() {
const { themeMode } = this.state;
return (
<ThemeProvider theme={{ ...theme, mode: themeMode }}>
<Fragment>
<GlobalStyle />
<Provider store={store}>
<ConnectedRouter history={history}>
<Router />
</ConnectedRouter>
</Provider>
</Fragment>
</ThemeProvider>
);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 747 B

View File

@ -4,17 +4,15 @@ import React from 'react';
import styled from 'styled-components';
import type { Node, ElementProps } from 'react';
type FlexProps =
| {
alignItems: string,
justifyContent: string,
}
| Object;
type FlexProps = PropsWithTheme<{
alignItems: string,
justifyContent: string,
}>;
const Flex = styled.div`
display: flex;
flex-direction: row;
align-items: ${(props: FlexProps) => props.alignItems};
justify-content: ${(props: FlexProps) => props.justifyContent};
align-items: ${(props: FlexProps) => String(props.alignItems)};
justify-content: ${(props: FlexProps) => String(props.justifyContent)};
`;
type Props = {

View File

@ -42,7 +42,7 @@ const ValueWrapper = styled.div`
width: 95%;
padding: 13px;
opacity: ${(props: PropsWithTheme<{ hasValue: boolean }>) => (props.hasValue ? '1' : '0.2')};
text-transform: capitalize;
text-transform: ${(props: PropsWithTheme<{ capitalize: boolean }>) => (props.capitalize ? 'capitalize' : 'none')};
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@ -88,7 +88,7 @@ const Option = styled.button`
background-color: #5d5d5d;
cursor: pointer;
z-index: 99;
text-transform: capitalize;
text-transform: ${(props: PropsWithTheme<{ capitalize: boolean }>) => (props.capitalize ? 'capitalize' : 'none')};
padding: 5px 10px;
border-bottom: 1px solid #4e4b4b;
@ -110,6 +110,7 @@ type Props = {
onChange: string => void,
placement?: 'top' | 'bottom',
bgColor?: string,
capitalize?: boolean,
};
type State = {
@ -125,6 +126,7 @@ export class SelectComponent extends PureComponent<Props, State> {
placeholder: '',
placement: 'bottom',
bgColor: theme.colors.inputBackground,
capitalize: true,
};
onSelect = (value: string) => {
@ -162,7 +164,7 @@ export class SelectComponent extends PureComponent<Props, State> {
render() {
const { isOpen } = this.state;
const {
value, options, placeholder, placement, bgColor,
value, options, placeholder, placement, bgColor, capitalize,
} = this.props;
return (
@ -174,7 +176,7 @@ export class SelectComponent extends PureComponent<Props, State> {
onClick={() => this.setState(() => ({ isOpen: !isOpen }))}
bgColor={bgColor}
>
<ValueWrapper hasValue={Boolean(value)}>
<ValueWrapper hasValue={Boolean(value)} capitalize={capitalize}>
{this.getSelectedLabel(value) || placeholder}
</ValueWrapper>
<SelectMenuButtonWrapper>
@ -194,6 +196,7 @@ export class SelectComponent extends PureComponent<Props, State> {
key={label + optionValue}
onClick={() => this.onSelect(optionValue)}
bgColor={bgColor}
capitalize={capitalize}
>
<TextComponent value={label} />
</Option>

View File

@ -14,7 +14,7 @@ import {
loadWalletSummarySuccess,
loadWalletSummaryError,
} from '../redux/modules/wallet';
import sortBy from '../utils/sort-by';
import { sortBy } from '../utils/sort-by';
import type { AppState } from '../types/app-state';
import type { Dispatch } from '../types/redux';

View File

@ -16,6 +16,8 @@ import {
resetSendTransaction,
validateAddressSuccess,
validateAddressError,
loadAddressBalanceSuccess,
loadAddressBalanceError,
} from '../redux/modules/send';
import { filterObjectNullKeys } from '../utils/filter-object-null-keys';
@ -23,10 +25,7 @@ import { filterObjectNullKeys } from '../utils/filter-object-null-keys';
import type { AppState } from '../types/app-state';
import type { Dispatch } from '../types/redux';
import {
loadAddressesSuccess,
loadAddressesError,
} from '../redux/modules/receive';
import { loadAddressesSuccess, loadAddressesError } from '../redux/modules/receive';
export type SendTransactionInput = {
from: string,
@ -36,8 +35,8 @@ export type SendTransactionInput = {
memo: string,
};
const mapStateToProps = ({ walletSummary, sendStatus, receive }: AppState) => ({
balance: walletSummary.total,
const mapStateToProps = ({ sendStatus, receive }: AppState) => ({
balance: sendStatus.addressBalance,
zecPrice: sendStatus.zecPrice,
addresses: receive.addresses,
error: sendStatus.error,
@ -144,6 +143,13 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({
value: Number(store.get('ZEC_DOLLAR_PRICE')),
}),
),
getAddressBalance: async ({ address }: { address: string }) => {
const [err, balance] = await eres(rpc.z_getbalance(address));
if (err) return dispatch(loadAddressBalanceError({ error: "Can't load your balance address" }));
return dispatch(loadAddressBalanceSuccess({ balance }));
},
});
// $FlowFixMe

View File

@ -16,7 +16,7 @@ import {
import rpc from '../../services/api';
import store from '../../config/electron-store';
import sortBy from '../utils/sort-by';
import { sortBy } from '../utils/sort-by';
import type { AppState } from '../types/app-state';
import type { Dispatch } from '../types/redux';

View File

@ -8,6 +8,8 @@ export const RESET_SEND_TRANSACTION = 'RESET_SEND_TRANSACTION';
export const VALIDATE_ADDRESS_SUCCESS = 'VALIDATE_ADDRESS_SUCCESS';
export const VALIDATE_ADDRESS_ERROR = 'VALIDATE_ADDRESS_SUCCESS';
export const LOAD_ZEC_PRICE = 'LOAD_ZEC_PRICE';
export const LOAD_ADDRESS_BALANCE_SUCCESS = 'LOAD_ADDRESS_BALANCE_SUCCESS';
export const LOAD_ADDRESS_BALANCE_ERROR = 'LOAD_ADDRESS_BALANCE_ERROR';
export const sendTransaction = () => ({
type: SEND_TRANSACTION,
@ -52,12 +54,27 @@ export const loadZECPrice = ({ value }: { value: number }) => ({
},
});
export const loadAddressBalanceSuccess = ({ balance }: { balance: number }) => ({
type: LOAD_ADDRESS_BALANCE_SUCCESS,
payload: {
balance,
},
});
export const loadAddressBalanceError = ({ error }: { error: string }) => ({
type: LOAD_ADDRESS_BALANCE_SUCCESS,
payload: {
error,
},
});
export type State = {
isSending: boolean,
isToAddressValid: boolean,
error: string | null,
operationId: string | null,
zecPrice: number,
addressBalance: number,
};
const initialState: State = {
@ -66,6 +83,7 @@ const initialState: State = {
operationId: null,
isToAddressValid: false,
zecPrice: 0,
addressBalance: 0,
};
// eslint-disable-next-line
@ -104,6 +122,10 @@ export default (state: State = initialState, action: Action): State => {
};
case LOAD_ZEC_PRICE:
return { ...state, zecPrice: action.payload.value };
case LOAD_ADDRESS_BALANCE_SUCCESS:
return { ...state, addressBalance: action.payload.balance };
case LOAD_ADDRESS_BALANCE_ERROR:
return { ...state, error: action.payload.error };
case RESET_SEND_TRANSACTION:
return initialState;
default:

View File

@ -12,7 +12,6 @@ const darkOne = '#F4B728';
const blackTwo = '#171717';
const lightOne = '#ffffff';
const brandOne = '#000';
// const brandTwo = '#3B3B3F';
const brandThree = '#5d5d65';
const buttonBorderColor = '#3e3c42';
const activeItem = '#F4B728';
@ -46,39 +45,106 @@ const appTheme: AppTheme = {
small: 0.667,
},
colors: {
// $FlowFixMe
primary: theme('mode', {
light: lightOne,
dark: darkOne,
}),
// $FlowFixMe
secondary: theme('mode', {
light: darkOne,
dark: lightOne,
}),
sidebarBg: brandOne,
sidebarItem: brandThree,
sidebarItemActive: activeItem,
sidebarHoveredItem,
sidebarHoveredItemLabel,
cardBackgroundColor,
text,
activeItem,
inactiveItem: brandThree,
sidebarLogoGradientBegin,
sidebarLogoGradientEnd,
background,
transactionSent,
transactionReceived,
transactionsDate,
transactionsItemHovered,
inputBackground: brandOne,
selectButtonShadow,
transactionsDetailsLabel,
statusPillLabel,
modalItemLabel: transactionsDate,
blackTwo,
buttonBorderColor,
sidebarBg: theme('mode', {
light: brandOne,
dark: brandOne,
}),
sidebarItem: theme('mode', {
light: brandThree,
dark: brandThree,
}),
sidebarItemActive: theme('mode', {
light: activeItem,
dark: activeItem,
}),
sidebarHoveredItem: theme('mode', {
light: sidebarHoveredItem,
dark: sidebarHoveredItem,
}),
sidebarHoveredItemLabel: theme('mode', {
light: sidebarHoveredItemLabel,
dark: sidebarHoveredItemLabel,
}),
cardBackgroundColor: theme('mode', {
light: cardBackgroundColor,
dark: cardBackgroundColor,
}),
text: theme('mode', {
light: '#000',
dark: text,
}),
activeItem: theme('mode', {
light: activeItem,
dark: activeItem,
}),
inactiveItem: theme('mode', {
light: brandThree,
dark: brandThree,
}),
sidebarLogoGradientBegin: theme('mode', {
light: sidebarLogoGradientBegin,
dark: sidebarLogoGradientBegin,
}),
sidebarLogoGradientEnd: theme('mode', {
light: sidebarLogoGradientEnd,
dark: sidebarLogoGradientEnd,
}),
background: theme('mode', {
light: '#FFF',
dark: background,
}),
transactionSent: theme('mode', {
light: transactionSent,
dark: transactionSent,
}),
transactionReceived: theme('mode', {
light: transactionReceived,
dark: transactionReceived,
}),
transactionsDate: theme('mode', {
light: transactionsDate,
dark: transactionsDate,
}),
transactionsItemHovered: theme('mode', {
light: transactionsItemHovered,
dark: transactionsItemHovered,
}),
inputBackground: theme('mode', {
light: brandOne,
dark: brandOne,
}),
selectButtonShadow: theme('mode', {
light: selectButtonShadow,
dark: selectButtonShadow,
}),
transactionsDetailsLabel: theme('mode', {
light: transactionsDetailsLabel,
dark: transactionsDetailsLabel,
}),
statusPillLabel: theme('mode', {
light: statusPillLabel,
dark: statusPillLabel,
}),
modalItemLabel: theme('mode', {
light: transactionsDate,
dark: transactionsDate,
}),
blackTwo: theme('mode', {
light: blackTwo,
dark: blackTwo,
}),
buttonBorderColor: theme('mode', {
light: buttonBorderColor,
dark: buttonBorderColor,
}),
},
sidebarWidth: '180px',
headerHeight: '60px',

View File

@ -0,0 +1,9 @@
// @flow
export function ascii2hex(ascii: ?string): string {
if (!ascii) return '';
return ascii
.split('')
.map(letter => letter.charCodeAt(0).toString(16))
.join('');
}

View File

@ -2,4 +2,4 @@
/* eslint-disable max-len */
// $FlowFixMe
export default <T>(field: string) => (arr: T[]): T[] => arr.sort((a, b) => (a[field] < b[field] ? 1 : -1));
export const sortBy = <T>(field: string) => (arr: T[]): T[] => arr.sort((a, b) => (a[field] < b[field] ? 1 : -1));

View File

@ -18,6 +18,7 @@ import { Button } from '../components/button';
import { ConfirmDialogComponent } from '../components/confirm-dialog';
import { formatNumber } from '../utils/format-number';
import { ascii2hex } from '../utils/ascii-to-hexadecimal';
import type { SendTransactionInput } from '../containers/send';
import type { State as SendState } from '../redux/modules/send';
@ -27,6 +28,7 @@ import MenuIcon from '../assets/images/menu_icon.svg';
import ValidIcon from '../assets/images/green_check.png';
import InvalidIcon from '../assets/images/error_icon.png';
import LoadingIcon from '../assets/images/sync_icon.png';
import ArrowUpIcon from '../assets/images/arrow_up.png';
import theme from '../theme';
@ -209,6 +211,35 @@ const RevealsMain = styled.div`
}
`;
// $FlowFixMe
const Checkbox = styled.input.attrs({
type: 'checkbox',
})`
margin-right: 10px;
`;
const MaxAvailableAmount = styled.button`
margin-top: -15px;
margin-right: -15px;
width: 45px;
height: 48px;
border: none;
background: none;
color: white;
cursor: pointer;
border-left: ${props => `1px solid ${props.theme.colors.background}`};
opacity: 0.8;
&:hover {
opacity: 1;
}
`;
const MaxAvailableAmountImg = styled.img`
width: 20px;
height: 20px;
`;
type Props = {
...SendState,
balance: number,
@ -220,6 +251,7 @@ type Props = {
validateAddress: ({ address: string }) => void,
loadAddresses: () => void,
loadZECPrice: () => void,
getAddressBalance: ({ address: string }) => void,
};
type State = {
@ -230,6 +262,7 @@ type State = {
feeType: string | number,
fee: number | null,
memo: string,
isHexMemo: boolean,
};
const initialState = {
@ -240,6 +273,7 @@ const initialState = {
feeType: FEES.LOW,
fee: FEES.LOW,
memo: '',
isHexMemo: false,
};
export class SendView extends PureComponent<Props, State> {
@ -253,18 +287,35 @@ export class SendView extends PureComponent<Props, State> {
loadZECPrice();
}
handleChange = (field: string) => (value: string) => {
const { validateAddress } = this.props;
handleChange = (field: string) => (value: string | number) => {
const { validateAddress, getAddressBalance, balance } = this.props;
const { fee, amount } = this.state;
if (field === 'to') {
// eslint-disable-next-line max-len
this.setState(() => ({ [field]: value }), () => validateAddress({ address: value }));
this.setState(() => ({ [field]: value }), () => validateAddress({ address: String(value) }));
} else if (field === 'amount') {
const amountWithFee = new BigNumber(value).plus(fee || 0);
const validAmount = amountWithFee.isGreaterThan(balance)
? new BigNumber(balance).minus(fee || 0).toNumber()
: value;
this.setState(() => ({ [field]: validAmount }));
} else {
this.setState(() => ({ [field]: value }));
if (field === 'from') getAddressBalance({ address: String(value) });
this.setState(
() => ({ [field]: value }),
() => {
if (field === 'fee') this.handleChange('amount')(amount);
},
);
}
};
handleChangeFeeType = (value: string) => {
const { amount } = this.state;
if (value === FEES.CUSTOM) {
this.setState(() => ({
feeType: FEES.CUSTOM,
@ -273,16 +324,19 @@ export class SendView extends PureComponent<Props, State> {
} else {
const fee = new BigNumber(value);
this.setState(() => ({
feeType: fee.toString(),
fee: fee.toNumber(),
}));
this.setState(
() => ({
feeType: fee.toString(),
fee: fee.toNumber(),
}),
() => this.handleChange('amount')(amount),
);
}
};
handleSubmit = () => {
const {
from, amount, to, memo, fee,
from, amount, to, memo, fee, isHexMemo,
} = this.state;
const { sendTransaction } = this.props;
@ -293,7 +347,7 @@ export class SendView extends PureComponent<Props, State> {
to,
amount,
fee,
memo,
memo: isHexMemo ? memo : ascii2hex(memo),
});
};
@ -445,7 +499,7 @@ export class SendView extends PureComponent<Props, State> {
append: 'USD $',
});
const valueSent = formatNumber({
value: new BigNumber(fixedAmount).toFormat(2),
value: new BigNumber(fixedAmount).toFormat(4),
append: 'ZEC ',
});
const valueSentInUsd = formatNumber({
@ -456,16 +510,25 @@ export class SendView extends PureComponent<Props, State> {
return (
<RowComponent id='send-wrapper' justifyContent='space-between'>
<FormWrapper>
<InputLabelComponent value='From' />
<InputLabelComponent value='From an address' />
<SelectComponent
onChange={this.handleChange('from')}
value={from}
placeholder='Select a address'
options={addresses.map(addr => ({ value: addr, label: addr }))}
capitalize={false}
/>
<InputLabelComponent value='Amount' />
<AmountWrapper isEmpty={isEmpty}>
<AmountInput
renderRight={() => (
<MaxAvailableAmount
onClick={() => this.handleChange('amount')(balance)}
disabled={!from}
>
<MaxAvailableAmountImg src={ArrowUpIcon} />
</MaxAvailableAmount>
)}
isEmpty={isEmpty}
type='number'
onChange={this.handleChange('amount')}
@ -473,6 +536,7 @@ export class SendView extends PureComponent<Props, State> {
placeholder='ZEC 0.0'
min={0.01}
name='amount'
disabled={!from}
/>
</AmountWrapper>
<InputLabelComponent value='To' />
@ -491,6 +555,10 @@ export class SendView extends PureComponent<Props, State> {
placeholder='Enter a text here'
name='memo'
/>
<RowComponent justifyContent='flex-end'>
<Checkbox onChange={event => this.setState({ isHexMemo: event.target.checked })} />
<TextComponent value='Hexadecimal memo' />
</RowComponent>
<ShowFeeButton
id='send-show-additional-options-button'
onClick={() => this.setState(state => ({

View File

@ -17,8 +17,11 @@ import { InputComponent } from '../components/input';
import { InputLabelComponent } from '../components/input-label';
import { RowComponent } from '../components/row';
import { Clipboard } from '../components/clipboard';
import { SelectComponent } from '../components/select';
import rpc from '../../services/api';
import { DARK, LIGHT } from '../constants/themes';
import electronStore from '../../config/electron-store';
const HOME_DIR = electron.remote.app.getPath('home');
@ -67,6 +70,10 @@ const SettingsContent = styled(TextComponent)`
margin-top: 10px;
`;
const ThemeSelectWrapper = styled.div`
margin-bottom: 20px;
`;
type Key = {
zAddress: string,
key: string,
@ -210,6 +217,23 @@ export class SettingsView extends PureComponent<Props, State> {
return (
<Wrapper>
<ThemeSelectWrapper>
<SettingsTitle value='Theme' />
<SelectComponent
onChange={newMode => electronStore.set('THEME_MODE', newMode)}
options={[
{
label: 'Dark',
value: DARK,
},
{
label: 'Light',
value: LIGHT,
},
]}
value={electronStore.get('THEME_MODE')}
/>
</ThemeSelectWrapper>
<ConfirmDialogComponent
title='Export view keys'
renderTrigger={toggleVisibility => (

View File

@ -1,4 +1,6 @@
declare module 'electron-store' {
declare function callback(string, string): void;
declare class ElectronStore {
constructor({
defaults?: Object,
@ -14,6 +16,7 @@ declare module 'electron-store' {
has(key: string): boolean;
delete(key: string): void;
clear(): void;
onDidChange(key: string, cb: typeof callback): void;
size: number;
store: Object;
path: string;

View File

@ -1,29 +1,31 @@
import { ThemeSet } from 'styled-theming';
type Colors = {
primary: string,
secondary: string,
sidebarBg: string,
sidebarItem: string,
sidebarItemActive: string,
sidebarHoveredItem: string,
sidebarHoveredItemLabel: string,
cardBackgroundColor: string,
text: string,
activeItem: string,
inactiveItem: string,
sidebarLogoGradientBegin: string,
sidebarLogoGradientEnd: string,
background: string,
transactionSent: string,
transactionReceived: string,
transactionsDate: string,
transactionsItemHovered: string,
inputBackground: string,
selectButtonShadow: string,
transactionsDetailsLabel: string,
statusPillLabel: string,
modalItemLabel: string,
blackTwo: string,
buttonBorderColor: string,
primary: ThemeSet,
secondary: ThemeSet,
sidebarBg: ThemeSet,
sidebarItem: ThemeSet,
sidebarItemActive: ThemeSet,
sidebarHoveredItem: ThemeSet,
sidebarHoveredItemLabel: ThemeSet,
cardBackgroundColor: ThemeSet,
text: ThemeSet,
activeItem: ThemeSet,
inactiveItem: ThemeSet,
sidebarLogoGradientBegin: ThemeSet,
sidebarLogoGradientEnd: ThemeSet,
background: ThemeSet,
transactionSent: ThemeSet,
transactionReceived: ThemeSet,
transactionsDate: ThemeSet,
transactionsItemHovered: ThemeSet,
inputBackground: ThemeSet,
selectButtonShadow: ThemeSet,
transactionsDetailsLabel: ThemeSet,
statusPillLabel: ThemeSet,
modalItemLabel: ThemeSet,
blackTwo: ThemeSet,
buttonBorderColor: ThemeSet,
};
type FontSize = {
@ -54,7 +56,6 @@ type AppTheme = {
transitionEase: string,
};
// =(
declare type PropsWithTheme<T = {}> = {
...T,
theme: AppTheme,