Merge branch 'develop' of github.com:andrerfneves/zcash-reference-wallet into feature/build-pipeline

This commit is contained in:
George Lima 2019-02-15 14:01:48 -03:00
commit 4c2839bacc
137 changed files with 2566 additions and 535 deletions

View File

@ -1,6 +1,7 @@
[ignore]
.*/node_modules/polished/.*
./__tests__/**.js
./flow-typed/npm/styled-components_v4.x.x.js
[include]

View File

@ -0,0 +1,49 @@
// @flow
import configureStore from 'redux-mock-store';
import {
LOAD_ADDRESSES_SUCCESS,
LOAD_ADDRESSES_ERROR,
loadAddressesSuccess,
loadAddressesError,
} from '../../app/redux/modules/receive';
const store = configureStore()();
describe('Receive Actions', () => {
beforeEach(() => store.clearActions());
test('should create an action to load addresses with success', () => {
const payload = {
addresses: [
'tm0a9si0ds09gj02jj',
'smas098gk02jf0kskk',
],
};
store.dispatch(loadAddressesSuccess(payload));
expect(store.getActions()[0]).toEqual(
expect.objectContaining({
type: LOAD_ADDRESSES_SUCCESS,
payload,
}),
);
});
test('should create an action to load addresses with error', () => {
const payload = {
error: 'Something went wrong!',
};
store.dispatch(loadAddressesError(payload));
expect(store.getActions()[0]).toEqual(
expect.objectContaining({
type: LOAD_ADDRESSES_ERROR,
payload,
}),
);
});
});

View File

@ -0,0 +1,116 @@
// @flow
import configureStore from 'redux-mock-store';
import {
SEND_TRANSACTION,
SEND_TRANSACTION_SUCCESS,
SEND_TRANSACTION_ERROR,
RESET_SEND_TRANSACTION,
VALIDATE_ADDRESS_SUCCESS,
VALIDATE_ADDRESS_ERROR,
LOAD_ZEC_PRICE,
sendTransaction,
sendTransactionSuccess,
sendTransactionError,
resetSendTransaction,
validateAddressSuccess,
validateAddressError,
loadZECPrice,
} from '../../app/redux/modules/send';
const store = configureStore()();
describe('Send Actions', () => {
beforeEach(() => store.clearActions());
test('should create an action to send a transaction', () => {
store.dispatch(sendTransaction());
expect(store.getActions()[0]).toEqual(
expect.objectContaining({
type: SEND_TRANSACTION,
}),
);
});
test('should create an action to send transaction with success', () => {
const payload = {
operationId: '0b9ii4590ab-1d012klfo',
};
store.dispatch(sendTransactionSuccess(payload));
expect(store.getActions()[0]).toEqual(
expect.objectContaining({
type: SEND_TRANSACTION_SUCCESS,
payload,
}),
);
});
test('should create an action to send transaction with error', () => {
const payload = {
error: 'Something went wrong!',
};
store.dispatch(sendTransactionError(payload));
expect(store.getActions()[0]).toEqual(
expect.objectContaining({
type: SEND_TRANSACTION_ERROR,
payload,
}),
);
});
test('should reset a transaction', () => {
store.dispatch(resetSendTransaction());
expect(store.getActions()[0]).toEqual(
expect.objectContaining({
type: RESET_SEND_TRANSACTION,
}),
);
});
test('should validate a address with success', () => {
const payload = {
isValid: true,
};
store.dispatch(validateAddressSuccess(payload));
expect(store.getActions()[0]).toEqual(
expect.objectContaining({
type: VALIDATE_ADDRESS_SUCCESS,
payload,
}),
);
});
test('should validate a address with error', () => {
store.dispatch(validateAddressError());
expect(store.getActions()[0]).toEqual(
expect.objectContaining({
type: VALIDATE_ADDRESS_ERROR,
}),
);
});
test('should load ZEC price', () => {
const payload = {
value: 1.35,
};
store.dispatch(loadZECPrice(payload));
expect(store.getActions()[0]).toEqual(
expect.objectContaining({
type: LOAD_ZEC_PRICE,
payload,
}),
);
});
});

View File

@ -0,0 +1,59 @@
// @flow
import configureStore from 'redux-mock-store';
import {
LOAD_TRANSACTIONS,
LOAD_TRANSACTIONS_SUCCESS,
LOAD_TRANSACTIONS_ERROR,
loadTransactions,
loadTransactionsSuccess,
loadTransactionsError,
} from '../../app/redux/modules/transactions';
const store = configureStore()();
describe('Transactions Actions', () => {
beforeEach(() => store.clearActions());
test('should create an action to load transactions', () => {
store.dispatch(loadTransactions());
expect(store.getActions()[0]).toEqual(
expect.objectContaining({
type: LOAD_TRANSACTIONS,
}),
);
});
test('should create an action to load transactions with success', () => {
const payload = {
list: [],
zecPrice: 0,
};
store.dispatch(loadTransactionsSuccess(payload));
expect(store.getActions()[0]).toEqual(
expect.objectContaining({
type: LOAD_TRANSACTIONS_SUCCESS,
payload,
}),
);
});
test('should create an action to load transactions with error', () => {
const payload = {
error: 'Something went wrong!',
};
store.dispatch(loadTransactionsError(payload));
expect(store.getActions()[0]).toEqual(
expect.objectContaining({
type: LOAD_TRANSACTIONS_ERROR,
payload,
}),
);
});
});

View File

@ -0,0 +1,41 @@
// @flow
import React from 'react';
import { render, cleanup } from 'react-testing-library';
import { ThemeProvider } from 'styled-components';
import 'jest-dom/extend-expect';
import { Button } from '../../app/components/button';
import appTheme from '../../app/theme';
afterEach(cleanup);
describe('<Button />', () => {
test('should render primary button correctly', () => {
const { queryByTestId } = render(
<ThemeProvider theme={appTheme}>
<Button
label='Click me!'
onClick={() => alert('Clicked')} // eslint-disable-line
variant='primary'
/>
</ThemeProvider>,
);
expect(queryByTestId('PrimaryButton')).toBeInTheDocument();
});
test('should render secondary button correctly', () => {
const { queryByTestId } = render(
<ThemeProvider theme={appTheme}>
<Button
label='Click me!'
onClick={() => alert('Clicked')} // eslint-disable-line
variant='secondary'
/>
</ThemeProvider>,
);
expect(queryByTestId('SecondaryButton')).toBeInTheDocument();
});
});

View File

@ -0,0 +1,33 @@
// @flow
import React from 'react';
import { render, cleanup } from 'react-testing-library';
import { ThemeProvider } from 'styled-components';
import 'jest-dom/extend-expect';
import { Clipboard } from '../../app/components/clipboard';
import appTheme from '../../app/theme';
afterEach(cleanup);
describe('<Clipboard />', () => {
test('should render clipboard component correctly', () => {
const { queryByTestId } = render(
<ThemeProvider theme={appTheme}>
<Clipboard text='Click me!' />
</ThemeProvider>,
);
expect(queryByTestId('Clipboard')).toBeInTheDocument();
});
test('should render clipboard button correctly', () => {
const { queryByTestId } = render(
<ThemeProvider theme={appTheme}>
<Clipboard text='Click me!' />
</ThemeProvider>,
);
expect(queryByTestId('PrimaryButton')).toBeInTheDocument();
});
});

View File

@ -0,0 +1,28 @@
// @flow
import React from 'react';
import { render, cleanup } from 'react-testing-library';
import { ThemeProvider } from 'styled-components';
import 'jest-dom/extend-expect';
import { ColumnComponent } from '../../app/components/column';
import appTheme from '../../app/theme';
afterEach(cleanup);
describe('<ColumnComponent />', () => {
test('should render correctly', () => {
// $FlowFixMe
const { container } = render(
<ThemeProvider theme={appTheme}>
<ColumnComponent>
<h3>ZEC</h3>
<h3>React</h3>
<h3>Wallet</h3>
</ColumnComponent>
</ThemeProvider>,
);
expect(container).toBeVisible();
});
});

View File

@ -0,0 +1,54 @@
// @flow
import React from 'react';
import { render, cleanup } from 'react-testing-library';
import 'jest-dom/extend-expect';
import { ConfirmDialogComponent } from '../../app/components/confirm-dialog';
afterEach(cleanup);
describe('<ConfirmDialogComponent />', () => {
test('should render confirm dialog correctly', () => {
const { container } = render(
<ConfirmDialogComponent
title='Confirm example'
onConfirm={() => alert('Confirm')} // eslint-disable-line
renderTrigger={toggle => (
<button
onClick={toggle}
type='button'
>
Open!
</button>
)}
>
{(/* toggle */) => <div>Confirm content</div>}
</ConfirmDialogComponent>,
);
expect(container).toBeVisible();
});
test('should render confirm dialog trigger', () => {
const { queryByTestId } = render(
<ConfirmDialogComponent
title='Confirm example'
onConfirm={() => alert('Confirm')} // eslint-disable-line
renderTrigger={toggle => (
<button
data-testid='ConfirmDialogTrigger'
onClick={toggle}
type='button'
>
Open!
</button>
)}
>
{(/* toggle */) => <div>Confirm content</div>}
</ConfirmDialogComponent>,
);
expect(queryByTestId('ConfirmDialogTrigger')).toBeInTheDocument();
});
});

View File

@ -0,0 +1,27 @@
// @flow
import React from 'react';
import { render, cleanup } from 'react-testing-library';
import { ThemeProvider } from 'styled-components';
import 'jest-dom/extend-expect';
import { Divider } from '../../app/components/divider';
import appTheme from '../../app/theme';
afterEach(cleanup);
describe('<Divider />', () => {
test('should render correctly', () => {
// $FlowFixMe
const { container } = render(
<ThemeProvider theme={appTheme}>
<Divider opacity={0.3} />
</ThemeProvider>,
);
const divider = container.querySelector('div');
expect(divider).toBeVisible();
expect(divider).toHaveStyle('opacity: 0.3');
});
});

View File

@ -0,0 +1,66 @@
// @flow
import React from 'react';
import { render, cleanup } from 'react-testing-library';
import { ThemeProvider } from 'styled-components';
import 'jest-dom/extend-expect';
import { DropdownComponent } from '../../app/components/dropdown';
import { Button } from '../../app/components/button';
import appTheme from '../../app/theme';
afterEach(cleanup);
describe('<DropdownComponent />', () => {
test('should render dropdown correctly', () => {
const { queryByTestId } = render(
<ThemeProvider theme={appTheme}>
<div
style={{ height: '500px' }}
data-testid='DropdownWrapper'
>
<DropdownComponent
label='Addresses'
renderTrigger={toggleVisibility => (
<Button
label='Show Dropdown'
onClick={toggleVisibility}
/>
)}
options={[
{ label: 'asbh1yeasbdh23848asdasd', onClick: console.log },
{ label: 'urtyruhjr374hbfdjdhuh', onClick: console.log },
]}
/>
</div>
</ThemeProvider>,
);
expect(queryByTestId('DropdownWrapper')).toBeInTheDocument();
});
test('should render dropdown button trigger correctly', () => {
const { queryByTestId } = render(
<ThemeProvider theme={appTheme}>
<div style={{ height: '500px' }}>
<DropdownComponent
label='Addresses'
renderTrigger={toggleVisibility => (
<Button
label='Show Dropdown'
data-testid='DropdownAddressesWrapper'
onClick={toggleVisibility}
/>
)}
options={[
{ label: 'asbh1yeasbdh23848asdasd', onClick: console.log },
{ label: 'urtyruhjr374hbfdjdhuh', onClick: console.log },
]}
/>
</div>
</ThemeProvider>,
);
expect(queryByTestId('PrimaryButton')).toBeInTheDocument();
});
});

View File

@ -0,0 +1,33 @@
// @flow
import React from 'react';
import { render, cleanup, queryByText } from 'react-testing-library';
import { ThemeProvider } from 'styled-components';
import 'jest-dom/extend-expect';
import { EmptyTransactionsComponent } from '../../app/components/empty-transactions';
import appTheme from '../../app/theme';
afterEach(cleanup);
describe('<EmptyTransactions />', () => {
test('should render correctly', () => {
const { queryByTestId } = render(
<ThemeProvider theme={appTheme}>
<EmptyTransactionsComponent />
</ThemeProvider>,
);
expect(queryByTestId('NoTransactions')).toBeInTheDocument();
});
test('should show label correctly', () => {
const { container } = render(
<ThemeProvider theme={appTheme}>
<EmptyTransactionsComponent />
</ThemeProvider>,
);
expect(queryByText(container, /transactions/i)).toBeInTheDocument();
});
});

View File

@ -0,0 +1,35 @@
// @flow
import React from 'react';
import { render, cleanup, queryByText } from 'react-testing-library';
import { ThemeProvider } from 'styled-components';
import 'jest-dom/extend-expect';
import { InputLabelComponent } from '../../app/components/input-label';
import appTheme from '../../app/theme';
afterEach(cleanup);
describe('<InputLabelComponent />', () => {
test('should render correctly', () => {
const { container } = render(
<ThemeProvider theme={appTheme}>
<InputLabelComponent value='From' />
</ThemeProvider>,
);
const label = container.querySelector('p');
expect(label).toBeVisible();
});
test('should render input label string', () => {
const { container } = render(
<ThemeProvider theme={appTheme}>
<InputLabelComponent value='From' />
</ThemeProvider>,
);
expect(queryByText(container, /From/i)).toBeInTheDocument();
});
});

View File

@ -0,0 +1,42 @@
// @flow
import React from 'react';
import { render, cleanup } from 'react-testing-library';
import { ThemeProvider } from 'styled-components';
import 'jest-dom/extend-expect';
import { InputComponent } from '../../app/components/input';
import appTheme from '../../app/theme';
afterEach(cleanup);
describe('<InputComponent />', () => {
test('should render text input correctly', () => {
const { queryByTestId } = render(
<ThemeProvider theme={appTheme}>
<InputComponent
inputType='input'
value='Hello World!'
onChange={console.log} // eslint-disable-line
/>
</ThemeProvider>,
);
expect(queryByTestId('Input')).toBeInTheDocument();
});
test('should render textarea correctly', () => {
const { queryByTestId } = render(
<ThemeProvider theme={appTheme}>
<InputComponent
inputType='textarea'
value='I am Zcash Electron Wallet'
onChange={console.log} // eslint-disable-line
rows={10}
/>
</ThemeProvider>,
);
expect(queryByTestId('Textarea')).toBeInTheDocument();
});
});

View File

@ -0,0 +1,23 @@
// @flow
import React from 'react';
import { render, cleanup } from 'react-testing-library';
import { ThemeProvider } from 'styled-components';
import 'jest-dom/extend-expect';
import { LoadingScreen } from '../../app/components/loading-screen';
import appTheme from '../../app/theme';
afterEach(cleanup);
describe('<LoadingScreen />', () => {
test('should render status pill correctly', () => {
const { queryByTestId } = render(
<ThemeProvider theme={appTheme}>
<LoadingScreen progress={83.0} />
</ThemeProvider>,
);
expect(queryByTestId('LoadingScreen')).toBeInTheDocument();
});
});

View File

@ -0,0 +1,41 @@
// @flow
import React from 'react';
import { render, cleanup } from 'react-testing-library';
import 'jest-dom/extend-expect';
import { ModalComponent } from '../../app/components/modal';
afterEach(cleanup);
describe('<ModalComponent />', () => {
test('should render modal trigger correctly', () => {
const { queryByTestId } = render(
<ModalComponent
renderTrigger={toggleVisibility => (
<button
type='button'
data-testid='ModalTrigger'
onClick={toggleVisibility}
>
Open Modal
</button>
)}
>
{toggleVisibility => (
<div style={{ padding: '50px', backgroundColor: 'white' }}>
Modal Content
<button
type='button'
onClick={toggleVisibility}
>
Close Modal
</button>
</div>
)}
</ModalComponent>,
);
expect(queryByTestId('ModalTrigger')).toBeInTheDocument();
});
});

View File

@ -0,0 +1,19 @@
// @flow
import React from 'react';
import { render, cleanup } from 'react-testing-library';
import 'jest-dom/extend-expect';
import { QRCode } from '../../app/components/qrcode';
afterEach(cleanup);
describe('<QRCode />', () => {
test('should render qrcode component correctly', () => {
const { queryByTestId } = render(
<QRCode value='https://z.cash.foundation' />,
);
expect(queryByTestId('QRCode')).toBeInTheDocument();
});
});

View File

@ -0,0 +1,27 @@
// @flow
import React from 'react';
import { render, cleanup } from 'react-testing-library';
import { ThemeProvider } from 'styled-components';
import 'jest-dom/extend-expect';
import { RowComponent } from '../../app/components/row';
import appTheme from '../../app/theme';
afterEach(cleanup);
describe('<RowComponent />', () => {
test('should render correctly', () => {
const { container } = render(
<ThemeProvider theme={appTheme}>
<RowComponent>
<h3>ZEC</h3>
<h3>React</h3>
<h3>Wallet</h3>
</RowComponent>
</ThemeProvider>,
);
expect(container).toBeVisible();
});
});

View File

@ -0,0 +1,67 @@
// @flow
import React from 'react';
import { render, cleanup, queryByText } from 'react-testing-library';
import { ThemeProvider } from 'styled-components';
import 'jest-dom/extend-expect';
import { SelectComponent } from '../../app/components/select';
import appTheme from '../../app/theme';
afterEach(cleanup);
describe('<SelectComponent />', () => {
test('should generate snapshot correctly', () => {
const { container } = render(
<ThemeProvider theme={appTheme}>
<SelectComponent
onChange={console.log} // eslint-disable-line
value='asbh1yeasbdh23848asdasd'
placeholder='Select a address'
options={[
{ label: 'asbh1yeasbdh23848asdasd', value: '1' },
{ label: 'urtyruhjr374hbfdjdhuh', value: '1' },
]}
/>
</ThemeProvider>,
);
expect(container).toBeVisible();
});
test('should render correctly', () => {
const { queryByTestId } = render(
<ThemeProvider theme={appTheme}>
<SelectComponent
onChange={console.log}
value='asbh1yeasbdh23848asdasd'
placeholder='Select a address'
options={[
{ label: 'asbh1yeasbdh23848asdasd', value: '1' },
{ label: 'urtyruhjr374hbfdjdhuh', value: '1' },
]}
/>
</ThemeProvider>,
);
expect(queryByTestId('Select')).toBeInTheDocument();
});
test('should render select trigger string', () => {
const { container } = render(
<ThemeProvider theme={appTheme}>
<SelectComponent
onChange={console.log}
value='asbh1yeasbdh23848asdasd'
placeholder='Select a address'
options={[
{ label: 'asbh1yeasbdh23848asdasd', value: '1' },
{ label: 'urtyruhjr374hbfdjdhuh', value: '1' },
]}
/>
</ThemeProvider>,
);
expect(queryByText(container, /Select/i)).toBeInTheDocument();
});
});

View File

@ -0,0 +1,54 @@
// @flow
import React from 'react';
import { render, cleanup, queryByText } from 'react-testing-library';
import { ThemeProvider } from 'styled-components';
import 'jest-dom/extend-expect';
import { StatusPill } from '../../app/components/status-pill';
import appTheme from '../../app/theme';
afterEach(cleanup);
describe('<StatusPill />', () => {
test('should render status pill correctly', () => {
const { queryByTestId } = render(
<ThemeProvider theme={appTheme}>
<StatusPill progress={83.0} type='syncing' />
</ThemeProvider>,
);
expect(queryByTestId('StatusPill')).toBeInTheDocument();
});
test('should show percentage on status pill syncing', () => {
const { container } = render(
<ThemeProvider theme={appTheme}>
<StatusPill progress={56.0} type='syncing' />
</ThemeProvider>,
);
expect(queryByText(container, /%/i)).toBeInTheDocument();
});
test('should hide percentage on status pill', () => {
const { container } = render(
<ThemeProvider theme={appTheme}>
<StatusPill progress={100.0} type='ready' />
</ThemeProvider>,
);
expect(queryByText(container, /%/i)).not.toBeInTheDocument();
});
test('should show error string and hide percentage on status pill', () => {
const { container } = render(
<ThemeProvider theme={appTheme}>
<StatusPill progress={0.0} type='error' />
</ThemeProvider>,
);
expect(queryByText(container, /%/i)).not.toBeInTheDocument();
expect(queryByText(container, /error/i)).toBeInTheDocument();
});
});

View File

@ -0,0 +1,33 @@
// @flow
import React from 'react';
import { render, cleanup, queryByText } from 'react-testing-library';
import { ThemeProvider } from 'styled-components';
import 'jest-dom/extend-expect';
import { TextComponent } from '../../app/components/text';
import appTheme from '../../app/theme';
afterEach(cleanup);
describe('<TextComponent />', () => {
test('should render correctly', () => {
const { container } = render(
<ThemeProvider theme={appTheme}>
<TextComponent value='ZEC React Wallet' />
</ThemeProvider>,
);
expect(container).toBeVisible();
});
test('should render input label string', () => {
const { container } = render(
<ThemeProvider theme={appTheme}>
<TextComponent value='Test' />
</ThemeProvider>,
);
expect(queryByText(container, /Test/i)).toBeInTheDocument();
});
});

View File

@ -0,0 +1,30 @@
// @flow
import React from 'react';
import { render, cleanup } from 'react-testing-library';
import { ThemeProvider } from 'styled-components';
import 'jest-dom/extend-expect';
import { TransactionItemComponent } from '../../app/components/transaction-item';
import appTheme from '../../app/theme';
afterEach(cleanup);
describe('<TransactionItem />', () => {
test('should render a transaction item correctly', () => {
const { container } = render(
<ThemeProvider theme={appTheme}>
<TransactionItemComponent
type='send'
address='123456789123456789123456789123456789'
transactionId='a0s9dujo23j0'
amount={0.8652}
date={new Date().toISOString()}
zecPrice={2.94}
/>
</ThemeProvider>,
);
expect(container).toMatchSnapshot();
});
});

View File

@ -0,0 +1,46 @@
// @flow
import React from 'react';
import { render, cleanup } from 'react-testing-library';
import { ThemeProvider } from 'styled-components';
import 'jest-dom/extend-expect';
import { TransactionDailyComponent } from '../../app/components/transaction-daily';
import appTheme from '../../app/theme';
afterEach(cleanup);
describe('<TransactionDailyComponent />', () => {
describe('render()', () => {
test('should render user daily transactions', () => {
const { container } = render(
<ThemeProvider theme={appTheme}>
<TransactionDailyComponent
transactionsDate={new Date().toISOString()}
zecPrice={1.345}
transactions={[
{
type: 'receive',
transactionId: 's0a8das098fgh2348a',
address: '123456789123456789123456789123456789',
amount: 1.7891,
zecPrice: 1.345,
date: new Date().toISOString(),
},
{
type: 'send',
transactionId: '0asd908fgj90f01',
address: '123456789123456789123456789123456789',
amount: 0.8458,
zecPrice: 1.344,
date: new Date().toISOString(),
},
]}
/>
</ThemeProvider>,
);
expect(container).toMatchSnapshot();
});
});
});

View File

@ -0,0 +1,27 @@
// @flow
import React from 'react';
import { render, cleanup } from 'react-testing-library';
import { ThemeProvider } from 'styled-components';
import 'jest-dom/extend-expect';
import { WalletAddress } from '../../app/components/wallet-address';
import appTheme from '../../app/theme';
afterEach(cleanup);
describe('<WalletAddress />', () => {
test('should render wallet address component correctly', () => {
const { queryByTestId } = render(
<ThemeProvider theme={appTheme}>
<div style={{ width: '700px' }}>
<WalletAddress
address='t14oHp2v54vfmdgQ3v3SNuQga8JKHTNi2a1'
/>
</div>
</ThemeProvider>,
);
expect(queryByTestId('Address')).toBeInTheDocument();
});
});

View File

@ -1,4 +1,5 @@
// @flow
import { getApp } from '../setup/utils';
const app = getApp();
@ -8,6 +9,7 @@ beforeAll(async () => {
await app.client.waitUntilWindowLoaded();
await app.client.waitUntilTextExists('#sidebar', 'Console');
});
afterAll(() => app.stop());
describe('Console', () => {
@ -16,8 +18,10 @@ describe('Console', () => {
expect(app.client.getText('#header p:first-child')).resolves.toEqual('Console');
expect(app.client.element('#console-wrapper img').getAttribute('src')).resolves.toEqual(
expect.stringContaining('/assets/console_zcash.png'),
);
expect(app.client.element('#console-wrapper img')
.getAttribute('src'))
.resolves.toEqual(
expect.stringContaining('/assets/console_zcash.png'),
);
});
});

View File

@ -1,4 +1,5 @@
// @flow
import { getApp } from '../setup/utils';
const app = getApp();
@ -9,29 +10,28 @@ beforeEach(async () => {
await app.client.waitUntilTextExists('#sidebar', 'Send');
await app.client.element('#sidebar a:nth-child(2)').click();
});
afterEach(() => app.stop());
describe('Send', () => {
test('should load "Send Page"', async () => {
expect(app.client.element('#send-wrapper').isVisible()).resolves.toEqual(true);
expect(app.client.element('#send-wrapper')
.isVisible()).resolves.toEqual(true);
});
test('should show Additional Options click', async () => {
expect(app.client.element('#send-wrapper #send-fee-wrapper').isVisible()).resolves.toEqual(
false,
);
expect(app.client.element('#send-wrapper #send-fee-wrapper')
.isVisible()).resolves.toEqual(false);
await app.client.element('#send-show-additional-options-button').click();
expect(app.client.element('#send-wrapper #send-fee-wrapper').isVisible()).resolves.toEqual(
true,
);
expect(app.client.element('#send-wrapper #send-fee-wrapper')
.isVisible()).resolves.toEqual(true);
});
test('should disable send button if required fields are empty', async () => {
expect(app.client.element('#send-submit-button').getAttribute('disabled')).resolves.toEqual(
true,
);
expect(app.client.element('#send-submit-button')
.getAttribute('disabled')).resolves.toEqual(true);
});
test('should enable send button if required fields are filled', async () => {
@ -68,9 +68,8 @@ describe('Send', () => {
await app.client.element('#send-submit-button').click();
expect(app.client.element('#send-confirm-transaction-modal').isVisible()).resolves.toEqual(
true,
);
expect(app.client.element('#send-confirm-transaction-modal')
.isVisible()).resolves.toEqual(true);
});
test('should display a load indicator while the transaction is processed', async () => {
@ -88,17 +87,19 @@ describe('Send', () => {
await app.client.element('#send-submit-button').click();
await app.client.element('#confirm-modal-button').click();
expect(app.client.getAttribute('#send-confirm-transaction-modal img', 'src')).resolves.toEqual(
expect.stringContaining('/assets/sync_icon.png'),
);
expect(app.client.getText('#send-confirm-transaction-modal p')).resolves.toEqual(
'Processing transaction...',
);
expect(app.client.element('#confirm-modal-button').isVisible()).resolves.toEqual(false);
expect(app.client.getText('#send-confirm-transaction-modal p'))
.resolves.toEqual('Processing transaction...');
expect(app.client.element('#confirm-modal-button')
.isVisible()).resolves.toEqual(false);
});
test('should show an error in invalid transaction', async () => {
expect(app.client.element('#send-error-text').isVisible()).resolves.toEqual(false);
expect(app.client.element('#send-error-text')
.isVisible()).resolves.toEqual(false);
await app.client.element('#sidebar a:nth-child(1)').click();
await app.client.element('#sidebar a:nth-child(2)').click();
@ -123,7 +124,8 @@ describe('Send', () => {
});
test('should show a success screen after transaction and show a transaction item', async () => {
expect(app.client.element('#send-success-wrapper').isVisible()).resolves.toEqual(false);
expect(app.client.element('#send-success-wrapper')
.isVisible()).resolves.toEqual(false);
await app.client.element('#sidebar a:nth-child(1)').click();
await app.client.element('#sidebar a:nth-child(2)').click();
@ -147,9 +149,8 @@ describe('Send', () => {
await app.client.waitUntilTextExists('#transaction-item-operation-id-1', 'Send');
expect(await app.client.element('#transaction-item-operation-id-1 img').isVisible()).toEqual(
true,
);
expect(await app.client.element('#transaction-item-operation-id-1 img')
.isVisible()).toEqual(true);
expect(
await app.client.element('#transaction-item-operation-id-1 img').getAttribute('src'),
).toEndWith('/assets/transaction_sent_icon.svg');

View File

@ -9,6 +9,7 @@ beforeAll(async () => {
await app.client.waitUntilWindowLoaded();
await app.client.waitUntilTextExists('#sidebar', 'Dashboard');
});
afterAll(() => app.stop());
describe('Sidebar', () => {
@ -17,9 +18,8 @@ describe('Sidebar', () => {
expect(await app.client.getUrl()).toEndWith('/');
expect(await app.client.element('#sidebar a:nth-child(1)').getHTML()).toEqual(
expect.stringContaining('Dashboard'),
);
expect(await app.client.element('#sidebar a:nth-child(1)')
.getHTML()).toEqual(expect.stringContaining('Dashboard'));
expect(await app.client.element('#sidebar a:nth-child(1) img').getAttribute('src')).toEqual(
expect.stringContaining('/assets/dashboard_icon_active.svg'),
@ -31,11 +31,11 @@ describe('Sidebar', () => {
expect(await app.client.getUrl()).toEndWith('/send');
expect(await app.client.element('#sidebar a:nth-child(2)').getHTML()).toEqual(
expect.stringContaining('Send'),
);
expect(await app.client.element('#sidebar a:nth-child(2)')
.getHTML()).toEqual(expect.stringContaining('Send'));
expect(await app.client.element('#sidebar a:nth-child(2) img').getAttribute('src')).toEqual(
expect(await app.client.element('#sidebar a:nth-child(2) img')
.getAttribute('src')).toEqual(
expect.stringContaining('/assets/send_icon_active.svg'),
);
});
@ -45,11 +45,11 @@ describe('Sidebar', () => {
expect(await app.client.getUrl()).toEndWith('/receive');
expect(await app.client.element('#sidebar a:nth-child(3)').getHTML()).toEqual(
expect.stringContaining('Receive'),
);
expect(await app.client.element('#sidebar a:nth-child(3)')
.getHTML()).toEqual(expect.stringContaining('Receive'));
expect(await app.client.element('#sidebar a:nth-child(3) img').getAttribute('src')).toEqual(
expect(await app.client.element('#sidebar a:nth-child(3) img')
.getAttribute('src')).toEqual(
expect.stringContaining('/assets/receive_icon_active.svg'),
);
});
@ -59,11 +59,11 @@ describe('Sidebar', () => {
expect(await app.client.getUrl()).toEndWith('/transactions');
expect(await app.client.element('#sidebar a:nth-child(4)').getHTML()).toEqual(
expect.stringContaining('Transactions'),
);
expect(await app.client.element('#sidebar a:nth-child(4)')
.getHTML()).toEqual(expect.stringContaining('Transactions'));
expect(await app.client.element('#sidebar a:nth-child(4) img').getAttribute('src')).toEqual(
expect(await app.client.element('#sidebar a:nth-child(4) img')
.getAttribute('src')).toEqual(
expect.stringContaining('/assets/transactions_icon_active.svg'),
);
});
@ -73,11 +73,11 @@ describe('Sidebar', () => {
expect(await app.client.getUrl()).toEndWith('/settings');
expect(await app.client.element('#sidebar a:nth-child(5)').getHTML()).toEqual(
expect.stringContaining('Settings'),
);
expect(await app.client.element('#sidebar a:nth-child(5)')
.getHTML()).toEqual(expect.stringContaining('Settings'));
expect(await app.client.element('#sidebar a:nth-child(5) img').getAttribute('src')).toEqual(
expect(await app.client.element('#sidebar a:nth-child(5) img')
.getAttribute('src')).toEqual(
expect.stringContaining('/assets/settings_icon_active.svg'),
);
});
@ -87,11 +87,11 @@ describe('Sidebar', () => {
expect(await app.client.getUrl()).toEndWith('/console');
expect(await app.client.element('#sidebar a:nth-child(6)').getHTML()).toEqual(
expect.stringContaining('Console'),
);
expect(await app.client.element('#sidebar a:nth-child(6)')
.getHTML()).toEqual(expect.stringContaining('Console'));
expect(await app.client.element('#sidebar a:nth-child(6) img').getAttribute('src')).toEqual(
expect(await app.client.element('#sidebar a:nth-child(6) img')
.getAttribute('src')).toEqual(
expect.stringContaining('/assets/console_icon_active.svg'),
);
});

View File

@ -8,6 +8,7 @@ beforeAll(async () => {
await app.start();
await app.client.waitUntilWindowLoaded();
});
afterAll(() => app.stop());
describe('Startup', () => {

View File

@ -1,4 +1,5 @@
// @flow
import { getApp } from '../setup/utils';
const app = getApp();

View File

@ -1,4 +1,5 @@
// @flow
import walletSummaryReducer, {
LOAD_WALLET_SUMMARY,
LOAD_WALLET_SUMMARY_SUCCESS,
@ -8,12 +9,14 @@ import walletSummaryReducer, {
describe('WalletSummary Reducer', () => {
test('should return the valid initial state', () => {
const initialState = {
addresses: [],
transactions: [],
total: 0,
shielded: 0,
transparent: 0,
error: null,
isLoading: false,
dollarValue: 0,
zecPrice: 0,
};
const action = {
type: 'UNKNOWN_ACTION',
@ -29,12 +32,14 @@ describe('WalletSummary Reducer', () => {
payload: {},
};
const expectedState = {
addresses: [],
transactions: [],
total: 0,
shielded: 0,
transparent: 0,
error: null,
isLoading: true,
dollarValue: 0,
zecPrice: 0,
};
expect(walletSummaryReducer(undefined, action)).toEqual(expectedState);
@ -53,7 +58,9 @@ describe('WalletSummary Reducer', () => {
...action.payload,
error: null,
isLoading: false,
dollarValue: 0,
addresses: [],
transactions: [],
zecPrice: 0,
};
expect(walletSummaryReducer(undefined, action)).toEqual(expectedState);
@ -72,7 +79,9 @@ describe('WalletSummary Reducer', () => {
transparent: 0,
error: action.payload.error,
isLoading: false,
dollarValue: 0,
addresses: [],
transactions: [],
zecPrice: 0,
};
expect(walletSummaryReducer(undefined, action)).toEqual(expectedState);

View File

@ -0,0 +1,9 @@
// @flow
const path = require('path');
module.exports = {
process(filename: any) {
return `module.exports = ${JSON.stringify(path.basename(filename))};`;
},
};

View File

@ -1,4 +1,5 @@
// @flow
// eslint-disable-next-line import/no-unresolved
require('jest-extended');

View File

@ -1,4 +1,5 @@
// @flow
// eslint-disable-next-line import/no-extraneous-dependencies
import 'babel-polyfill';
@ -11,7 +12,7 @@ const sleep = (time: number) => new Promise(resolve => setTimeout(resolve, time)
createTestServer({
httpPort: '18232',
}).then(async (server) => {
console.log('[MOCK RPC API]', server.url);
console.log('[MOCK RPC API]', server.url); // eslint-disable-line
server.get('/', (req, res) => {
res.send('Zcash RPC');
@ -94,6 +95,10 @@ createTestServer({
return res.send({
result: [{ id: 'operation-id-1', status: 'success', result: { txid: 'txid-1' } }],
});
case 'z_getbalance':
return res.send({
result: 5,
});
default:
return null;
}

View File

@ -1,4 +1,5 @@
// @flow
/* eslint-disable import/no-extraneous-dependencies */
import electron from 'electron';
import { Application } from 'spectron';

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

@ -0,0 +1,23 @@
// @flow
import 'jest-dom/extend-expect';
import { filterObjectNullKeys } from '../../app/utils/filter-object-null-keys';
describe('filterObjectNullKeys', () => {
test('should filter null keys from object', () => {
const initialState = {
name: 'John Doe',
address: null,
amount: 0,
transactions: undefined,
};
const expectedState = {
name: 'John Doe',
amount: 0,
};
expect(filterObjectNullKeys(initialState)).toEqual(expectedState);
});
});

View File

@ -0,0 +1,38 @@
// @flow
import { BigNumber } from 'bignumber.js';
import 'jest-dom/extend-expect';
import { formatNumber } from '../../app/utils/format-number';
describe('formatNumber', () => {
test('should append ZEC in balance amount', () => {
const myBalance = formatNumber({ value: 2.5, append: 'ZEC ' });
const expectedState = 'ZEC 2.5';
expect(myBalance).toEqual(expectedState);
});
test('should multiply ZEC balance and show it in USD', () => {
const myBalanceInUsd = formatNumber({
value: new BigNumber(2.5).times(1.35).toNumber(),
append: 'USD $',
});
const expectedState = 'USD $3.375';
expect(myBalanceInUsd).toEqual(expectedState);
});
test('should multiply decimal ZEC balance and show it in USD', () => {
const myBalanceInUsd = formatNumber({
value: new BigNumber(0.1).times(0.2).toNumber(),
append: 'USD $',
});
const expectedState = 'USD $0.02';
expect(myBalanceInUsd).toEqual(expectedState);
});
});

View File

@ -0,0 +1,13 @@
// @flow
import 'jest-dom/extend-expect';
import { sortByDescend } from '../../app/utils/sort-by-descend';
describe('truncateAddress', () => {
test('should truncate ZEC address', () => {
expect(
sortByDescend('id')([{ id: 5 }, { id: 2 }, { id: 1 }, { id: 0 }, { id: 1 }, { id: 1 }]),
).toEqual([{ id: 5 }, { id: 2 }, { id: 1 }, { id: 1 }, { id: 1 }, { id: 0 }]);
});
});

View File

@ -0,0 +1,13 @@
// @flow
import 'jest-dom/extend-expect';
import { sortBy } from '../../app/utils/sort-by';
describe('truncateAddress', () => {
test('should truncate ZEC address', () => {
expect(
sortBy('id')([{ id: 5 }, { id: 2 }, { id: 1 }, { id: 0 }, { id: 1 }, { id: 1 }]),
).toEqual([{ id: 0 }, { id: 1 }, { id: 1 }, { id: 1 }, { id: 2 }, { id: 5 }]);
});
});

View File

@ -0,0 +1,13 @@
// @flow
import 'jest-dom/extend-expect';
import { getTimestamp } from '../../app/utils/timestamp';
describe('generate timestamp', () => {
test('should generate a random string', () => {
const now = getTimestamp();
expect(now).toEqual(expect.any(Number));
});
});

View File

@ -0,0 +1,15 @@
// @flow
import 'jest-dom/extend-expect';
import { truncateAddress } from '../../app/utils/truncate-address';
describe('truncateAddress', () => {
test('should truncate ZEC address', () => {
const myAddress = truncateAddress('t14oHp2v54vfmdgQ3v3SNuQga8JKHTNi2a1');
const expectedState = 't14oHp2v54vfmdgQ3v3S...8JKHTNi2a1';
expect(myAddress).toEqual(expectedState);
});
});

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

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="#FFF" d="M24 9h-9v-9h-6v9h-9v6h9v9h6v-9h9z"/></svg>

After

Width:  |  Height:  |  Size: 146 B

View File

@ -1,13 +1,8 @@
// @flow
import React from 'react';
import React, { type ElementProps } from 'react';
import styled from 'styled-components';
import { Link } from 'react-router-dom';
/* eslint-disable import/no-extraneous-dependencies */
/* eslint-disable max-len */
// $FlowFixMe
import { darken } from 'polished';
import type { ElementProps } from 'react';
const DefaultButton = styled.button`
align-items: center;
@ -15,13 +10,13 @@ const DefaultButton = styled.button`
justify-content: center;
padding: 10px 30px;
font-family: ${props => props.theme.fontFamily};
font-weight: ${props => props.theme.fontWeight.bold};
font-weight: ${props => String(props.theme.fontWeight.bold)};
font-size: ${props => `${props.theme.fontSize.regular}em`};
cursor: pointer;
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.transitionEase};
`;
const Primary = styled(DefaultButton)`
@ -30,7 +25,7 @@ const Primary = styled(DefaultButton)`
border: none;
&:hover {
background-color: ${props => darken(0.1, props.theme.colors.primary(props))};
opacity: 0.9;
}
&:disabled {
@ -102,12 +97,12 @@ export const Button = ({
const buttonLabel = isLoading ? 'Loading...' : label;
const component = variant === 'primary' ? (
<Primary {...props}>
<Primary {...props} data-testid='PrimaryButton'>
{icon ? <Icon src={icon} /> : null}
{buttonLabel}
</Primary>
) : (
<Secondary {...props}>
<Secondary {...props} data-testid='SecondaryButton'>
{icon ? <Icon src={icon} /> : null}
{buttonLabel}
</Secondary>

View File

@ -1,4 +1,5 @@
// @flow
import React, { PureComponent } from 'react';
import { Button } from './button';
@ -39,12 +40,14 @@ export class Clipboard extends PureComponent<Props, State> {
const { copied } = this.state;
return (
<Button
label={copied ? 'Copied!' : 'Copy!'}
className={className}
onClick={this.handleClick}
disabled={copied}
/>
<div data-testid='Clipboard'>
<Button
label={copied ? 'Copied!' : 'Copy!'}
className={className}
onClick={this.handleClick}
disabled={copied}
/>
</div>
);
}
}

View File

@ -3,12 +3,19 @@
import React, { type Node, type ElementProps } from 'react';
import styled from 'styled-components';
type FlexProps =
| {
alignItems: string,
justifyContent: string,
width: string,
}
| Object;
const Flex = styled.div`
display: flex;
flex-direction: column;
align-items: ${props => props.alignItems};
justify-content: ${props => props.justifyContent};
${props => props.width && `width: ${props.width};`}
align-items: ${(props: FlexProps) => props.alignItems};
justify-content: ${(props: FlexProps) => props.justifyContent};
${(props: FlexProps) => props.width && `width: ${props.width};`}
`;
type Props = {

View File

@ -1,5 +1,6 @@
// @flow
import React, { type Element } from 'react';
import React, { Fragment, type Element } from 'react';
import styled from 'styled-components';
import { TextComponent } from './text';
@ -11,7 +12,7 @@ import CloseIcon from '../assets/images/close_icon.svg';
const Wrapper = styled.div`
display: flex;
width: ${props => `${props.width}px`};
width: ${(props: PropsWithTheme<{ width: number }>) => `${String(props.width)}px`};
background-color: ${props => props.theme.colors.background};
flex-direction: column;
align-items: center;
@ -90,7 +91,7 @@ export const ConfirmDialogComponent = ({
<Divider opacity={0.3} />
{children(handleClose(toggle))}
{showButtons && (
<>
<Fragment>
<Btn
id='confirm-modal-button'
label='Confirm'
@ -103,7 +104,7 @@ export const ConfirmDialogComponent = ({
variant='secondary'
disabled={isLoading}
/>
</>
</Fragment>
)}
</Wrapper>
)}

View File

@ -1,11 +1,19 @@
// @flow
import styled from 'styled-components';
type Props = PropsWithTheme<{
color: ?string,
opacity: number,
marginBottom: string,
marginTop: string,
}>;
export const Divider = styled.div`
width: 100%;
height: 1px;
background-color: ${props => props.color || props.theme.colors.text};
opacity: ${props => props.opacity || 1};
margin-bottom: ${props => props.marginBottom || 0};
margin-top: ${props => props.marginTop || 0};
background-color: ${(props: Props) => props.color || props.theme.colors.text};
opacity: ${(props: Props) => String(props.opacity || 1)};
margin-bottom: ${(props: Props) => props.marginBottom || '0'};
margin-top: ${(props: Props) => props.marginTop || '0'};
`;

View File

@ -1,4 +1,5 @@
// @flow
import React, { type Node, Component } from 'react';
import styled from 'styled-components';
/* eslint-disable import/no-extraneous-dependencies */
@ -107,7 +108,7 @@ export class DropdownComponent extends Component<Props, State> {
</MenuItem>
)}
{options.map(({ label: optionLabel, onClick }) => (
<OptionItem onClick={onClick} key={optionLabel}>
<OptionItem onClick={onClick} key={optionLabel} data-testid='DropdownOption'>
<Option value={truncate ? truncateAddress(optionLabel) : optionLabel} />
</OptionItem>
))}
@ -123,7 +124,12 @@ export class DropdownComponent extends Component<Props, State> {
tipSize={7}
body={body}
>
{renderTrigger(() => this.setState(state => ({ isOpen: !state.isOpen })), isOpen)}
{renderTrigger(
() => this.setState(state => ({
isOpen: !state.isOpen,
})),
isOpen,
)}
</PopoverWithStyle>
);
}

View File

@ -1,4 +1,5 @@
// @flow
import React from 'react';
import styled from 'styled-components';
@ -13,7 +14,7 @@ const Wrapper = styled.div`
`;
export const EmptyTransactionsComponent = () => (
<Wrapper>
<Wrapper data-testid='NoTransactions'>
<TextComponent value='No transactions!' />
</Wrapper>
);

View File

@ -0,0 +1,95 @@
// @flow
import React, { PureComponent } from 'react';
import { createPortal } from 'react-dom';
import styled from 'styled-components';
import { TextComponent } from './text';
import { Button } from './button';
import ErrorIcon from '../assets/images/error_icon.png';
const ModalWrapper = styled.div`
width: 100vw;
height: 100vh;
position: fixed;
display: flex;
align-items: center;
justify-content: center;
top: 0;
left: 0;
background-color: rgba(0, 0, 0, 0.5);
`;
const ChildrenWrapper = styled.div`
width: 350px;
background-color: ${props => props.theme.colors.background};
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border-radius: 6px;
box-shadow: 0px 0px 30px 0px black;
position: relative;
z-index: 90;
min-height: 400px;
`;
const Message = styled(TextComponent)`
margin: 15px 0;
`;
const ErrorImage = styled.img`
width: 35px;
height: 35px;
margin-bottom: 15px;
`;
type Props = {
isVisible: boolean,
message: string,
onRequestClose: () => void,
};
const modalRoot = document.getElementById('modal-root');
export class ErrorModalComponent extends PureComponent<Props> {
element = document.createElement('div');
componentDidMount() {
const { isVisible } = this.props;
if (isVisible) {
if (modalRoot) modalRoot.appendChild(this.element);
}
}
componentDidUpdate = (prevProps: Props) => {
const { isVisible } = this.props;
if (!prevProps.isVisible && isVisible) {
if (modalRoot) modalRoot.appendChild(this.element);
}
if (prevProps.isVisible && !isVisible) {
if (modalRoot) modalRoot.removeChild(this.element);
}
};
render() {
const { isVisible, message, onRequestClose } = this.props;
return !isVisible
? null
: createPortal(
<ModalWrapper id='error-modal-portal-wrapper'>
<ChildrenWrapper>
<ErrorImage src={ErrorIcon} alt='Error Icon' />
<Message value={message} />
<Button label='Ok!' onClick={onRequestClose} />
</ChildrenWrapper>
</ModalWrapper>,
this.element,
);
}
}

View File

@ -1,6 +1,6 @@
// @flow
import React from 'react';
import React from 'react';
import styled from 'styled-components';
import { ZcashLogo } from './zcash-logo';
@ -9,6 +9,8 @@ import { Divider } from './divider';
import { RowComponent } from './row';
import { StatusPill } from './status-pill';
import { withSyncStatus } from '../../services/sync-status';
const Wrapper = styled.div`
height: ${props => props.theme.headerHeight};
width: 100vw;
@ -52,13 +54,15 @@ const Title = styled(TextComponent)`
margin-bottom: 10px;
text-transform: capitalize;
letter-spacing: 0.25px;
font-weight: ${props => props.theme.fontWeight.bold};
font-weight: ${props => String(props.theme.fontWeight.bold)};
`;
type Props = {
title: string,
};
const Status = withSyncStatus(StatusPill);
export const HeaderComponent = ({ title }: Props) => (
<Wrapper id='header'>
<LogoWrapper>
@ -67,7 +71,7 @@ export const HeaderComponent = ({ title }: Props) => (
<TitleWrapper>
<TitleRow alignItems='center' justifyContent='space-around'>
<Title value={title} />
<StatusPill />
<Status type='syncing' progress={0} />
</TitleRow>
<Divider opacity={0.1} />
</TitleWrapper>

View File

@ -1,9 +1,14 @@
// @flow
import styled from 'styled-components';
import { TextComponent } from './text';
type Props = PropsWithTheme<{
marginTop: string,
}>;
export const InputLabelComponent = styled(TextComponent)`
margin: ${props => props.marginTop || '20px'} 0 8.5px 0;
font-weight: ${props => props.theme.fontWeight.bold};
margin: ${(props: Props) => props.marginTop || '20px'} 0 8.5px 0;
font-weight: ${(props: Props) => String(props.theme.fontWeight.bold)};
`;

View File

@ -1,19 +1,40 @@
// @flow
/* eslint-disable max-len */
import React, { type Element } from 'react';
import styled from 'styled-components';
import theme from '../theme';
const getDefaultStyles = t => styled[t]`
border-radius: ${props => props.theme.boxBorderRadius};
type Props = {
inputType?: 'input' | 'textarea',
value: string,
onChange?: string => void,
onFocus?: (SyntheticFocusEvent<HTMLInputElement>) => void,
rows?: number,
disabled?: boolean,
type?: string,
step?: number,
name?: string,
renderRight?: () => Element<*> | null,
bgColor?: string,
};
type DefaultStylesProps = PropsWithTheme<{
bgColor: ?string,
withRightElement: boolean,
}>;
// $FlowFixMe
const getDefaultStyles: ($PropertyType<Props, 'inputType'>) => Element<*> = t => styled[t]`
border-radius: ${(props: DefaultStylesProps) => props.theme.boxBorderRadius};
border: none;
background-color: ${props => props.bgColor || props.theme.colors.inputBackground};
color: ${props => props.theme.colors.text};
background-color: ${(props: DefaultStylesProps) => props.bgColor || props.theme.colors.inputBackground};
color: ${(props: DefaultStylesProps) => props.theme.colors.text};
padding: 15px;
padding-right: ${props => (props.withRightElement ? '85px' : '15px')};
padding-right: ${(props: DefaultStylesProps) => (props.withRightElement ? '85px' : '15px')};
width: 100%;
outline: none;
font-family: ${props => props.theme.fontFamily};
font-family: ${(props: DefaultStylesProps) => props.theme.fontFamily};
::placeholder {
opacity: 0.5;
@ -33,20 +54,6 @@ const RightElement = styled.div`
const Input = getDefaultStyles('input');
const Textarea = getDefaultStyles('textarea');
type Props = {
inputType?: 'input' | 'textarea',
value: string,
onChange?: string => void,
onFocus?: (SyntheticFocusEvent<HTMLInputElement>) => void,
rows?: number,
disabled?: boolean,
type?: string,
step?: number,
name?: string,
renderRight?: () => Element<*> | null,
bgColor?: string,
};
export const InputComponent = ({
inputType,
bgColor,
@ -55,18 +62,23 @@ export const InputComponent = ({
...props
}: Props) => {
const rightElement = renderRight();
const inputTypes = {
input: () => (
<Input
onChange={evt => onChange(evt.target.value)}
withRightElement={Boolean(rightElement)}
bgColor={bgColor}
data-testid='Input'
{...props}
/>
),
textarea: () => (
<Textarea onChange={evt => onChange(evt.target.value)} bgColor={bgColor} {...props} />
<Textarea
onChange={evt => onChange(evt.target.value)}
bgColor={bgColor}
data-testid='Textarea'
{...props}
/>
),
};

View File

@ -1,7 +1,10 @@
// @flow
import React from 'react';
import React, { type Element } from 'react';
import styled from 'styled-components';
import { ErrorModalComponent } from './error-modal';
const Layout = styled.div`
display: flex;
flex-direction: column;
@ -14,12 +17,25 @@ const Layout = styled.div`
`;
type Props = {
chidren: any, // eslint-disable-line
children: Element<*>,
closeErrorModal: () => void,
isErrorModalVisible: boolean,
error: string,
};
export const LayoutComponent = (props: Props) => {
// $FlowFixMe
const { children } = props; // eslint-disable-line
const {
children, error, isErrorModalVisible, closeErrorModal,
} = props;
return <Layout id='layout'>{children}</Layout>;
return (
<Layout id='layout'>
{children}
<ErrorModalComponent
message={error}
isVisible={isErrorModalVisible}
onRequestClose={closeErrorModal}
/>
</Layout>
);
};

View File

@ -1,7 +1,10 @@
// @flow
import React, { PureComponent } from 'react';
import styled from 'styled-components';
import { Transition, animated } from 'react-spring';
// eslint-disable-next-line import/no-extraneous-dependencies
import { ipcRenderer } from 'electron';
import CircleProgressComponent from 'react-circle';
import { TextComponent } from './text';
@ -42,25 +45,34 @@ type Props = {
type State = {
start: boolean,
message: string,
};
const TIME_DELAY_ANIM = 100;
export class LoadingScreen extends PureComponent<Props, State> {
state = { start: false };
state = { start: false, message: 'ZEC Wallet Starting' };
componentDidMount() {
setTimeout(() => {
this.setState(() => ({ start: true }));
}, TIME_DELAY_ANIM);
ipcRenderer.on('zcashd-params-download', (event: Object, message: string) => {
this.setState(() => ({ message }));
});
}
componentWillUnmount() {
ipcRenderer.removeAllListeners('zcashd-log');
}
render() {
const { start } = this.state;
const { start, message } = this.state;
const { progress } = this.props;
return (
<Wrapper>
<Wrapper data-testid='LoadingScreen'>
<Transition
native
items={start}
@ -73,19 +85,29 @@ export class LoadingScreen extends PureComponent<Props, State> {
opacity: 0,
}}
>
{() => props => (
<animated.div style={props} id='loading-screen'>
{() => (props: Object) => (
<animated.div
id='loading-screen'
style={{
...props,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
}}
>
<CircleWrapper>
<Logo src={zcashLogo} alt='Zcash logo' />
<Logo src={zcashLogo} alt='Zcash Logo' />
<CircleProgressComponent
progress={progress}
s // TODO: check if this has any effect
responsive
showPercentage={false}
progressColor={theme.colors.activeItem}
bgColor={theme.colors.inactiveItem}
/>
</CircleWrapper>
<TextComponent value='ZEC Wallet Starting' />
<TextComponent value={message} />
</animated.div>
)}
</Transition>

View File

@ -1,4 +1,5 @@
// @flow
import React, { PureComponent, Fragment, type Element } from 'react';
import { createPortal } from 'react-dom';
import styled from 'styled-components';
@ -69,23 +70,19 @@ export class ModalComponent extends PureComponent<Props, State> {
}
};
open = () => {
this.setState(
() => ({ isVisible: true }),
() => {
if (modalRoot) modalRoot.appendChild(this.element);
},
);
};
open = () => this.setState(
() => ({ isVisible: true }),
() => {
if (modalRoot) modalRoot.appendChild(this.element);
},
);
close = () => {
this.setState(
() => ({ isVisible: false }),
() => {
if (modalRoot) modalRoot.removeChild(this.element);
},
);
};
close = () => this.setState(
() => ({ isVisible: false }),
() => {
if (modalRoot) modalRoot.removeChild(this.element);
},
);
render() {
const { renderTrigger, children, closeOnBackdropClick } = this.props;
@ -95,22 +92,21 @@ export class ModalComponent extends PureComponent<Props, State> {
return (
<Fragment>
{renderTrigger(toggleVisibility)}
{isVisible
? createPortal(
<ModalWrapper
id='modal-portal-wrapper'
onClick={(event) => {
if (
closeOnBackdropClick
&& event.target.id === 'modal-portal-wrapper'
) this.close();
}}
>
<ChildrenWrapper>{children(toggleVisibility)}</ChildrenWrapper>
</ModalWrapper>,
this.element,
)
: null}
{!isVisible ? null : createPortal(
<ModalWrapper
id='modal-portal-wrapper'
data-testid='Modal'
onClick={(event) => {
if (
closeOnBackdropClick
&& event.target.id === 'modal-portal-wrapper'
) this.close();
}}
>
<ChildrenWrapper>{children(toggleVisibility)}</ChildrenWrapper>
</ModalWrapper>,
this.element,
)}
</Fragment>
);
}

View File

@ -12,12 +12,15 @@ import { ModalComponent } from './modal.js'
<PropsTable of={ModalComponent} />
## Basic usage
## Basic Usage
<Playground>
<ModalComponent
renderTrigger={toggleVisibility => (
<button type="button" onClick={toggleVisibility}>
<button
type="button"
onClick={toggleVisibility}
>
Open Modal
</button>
)}
@ -25,7 +28,10 @@ import { ModalComponent } from './modal.js'
{toggleVisibility => (
<div style={{ padding: '50px', backgroundColor: 'white' }}>
Modal Content
<button type="button" onClick={toggleVisibility}>
<button
type="button"
onClick={toggleVisibility}
>
Close Modal
</button>
</div>
@ -40,7 +46,10 @@ import { ModalComponent } from './modal.js'
closeOnEsc={false}
closeOnBackdropClick={false}
renderTrigger={toggleVisibility => (
<button type="button" onClick={toggleVisibility}>
<button
type="button"
onClick={toggleVisibility}
>
Open Modal
</button>
)}
@ -48,7 +57,10 @@ import { ModalComponent } from './modal.js'
{toggleVisibility => (
<div style={{ padding: '50px', backgroundColor: 'white' }}>
Modal Content
<button type="button" onClick={toggleVisibility}>
<button
type="button"
onClick={toggleVisibility}
>
Close Modal
</button>
</div>

View File

@ -9,7 +9,11 @@ type Props = {
};
export const QRCode = ({ value, size }: Props) => (
<QR value={value} size={size} />
<QR
data-testid='QRCode'
value={value}
size={size}
/>
);
QRCode.defaultProps = {

View File

@ -12,13 +12,13 @@ import { QRCode } from './qrcode.js'
<PropsTable of={QRCode} />
## Basic usage
## Basic Usage
<Playground>
<QRCode value='https://z.cash.foundation' />
</Playground>
## Custom size
## Custom Size
<Playground>
<QRCode

View File

@ -4,11 +4,15 @@ import React from 'react';
import styled from 'styled-components';
import type { Node, ElementProps } from 'react';
type FlexProps = PropsWithTheme<{
alignItems: string,
justifyContent: string,
}>;
const Flex = styled.div`
display: flex;
flex-direction: row;
align-items: ${props => props.alignItems};
justify-content: ${props => props.justifyContent};
align-items: ${(props: FlexProps) => String(props.alignItems)};
justify-content: ${(props: FlexProps) => String(props.justifyContent)};
`;
type Props = {
@ -20,7 +24,7 @@ type Props = {
};
export const RowComponent = ({ children, ...props }: Props) => (
<Flex {...props}>{React.Children.map(children, ch => ch)}</Flex>
<Flex {...props}>{React.Children.map(children, (ch: Node) => ch)}</Flex>
);
RowComponent.defaultProps = {

View File

@ -1,4 +1,5 @@
// @flow
import React, { PureComponent } from 'react';
import styled from 'styled-components';
@ -9,30 +10,39 @@ import ChevronDown from '../assets/images/chevron-down.svg';
import theme from '../theme';
/* eslint-disable max-len */
type SelectWrapperProps = PropsWithTheme<{
bgColor: ?string,
isOpen: boolean,
placement: string,
}>;
const SelectWrapper = styled.div`
align-items: center;
display: flex;
flex-direction: row;
border-radius: ${props => props.theme.boxBorderRadius};
border-radius: ${(props: SelectWrapperProps) => props.theme.boxBorderRadius};
border: none;
background-color: ${props => props.bgColor || props.theme.colors.inputBackground};
color: ${props => props.theme.colors.text};
background-color: ${(props: SelectWrapperProps) => props.bgColor || props.theme.colors.inputBackground};
color: ${(props: SelectWrapperProps) => props.theme.colors.text};
width: 100%;
outline: none;
font-family: ${props => props.theme.fontFamily};
font-family: ${(props: SelectWrapperProps) => props.theme.fontFamily};
cursor: pointer;
position: relative;
${props => props.isOpen
&& `border-${props.placement}-left-radius: 0; border-${props.placement}-right-radius: 0;`}
${(props: SelectWrapperProps) => (props.isOpen
? `border-${String(props.placement)}-left-radius: 0; border-${String(
props.placement,
)}-right-radius: 0;`
: '')}
`;
/* eslint-enable max-len */
const ValueWrapper = styled.div`
width: 95%;
padding: 13px;
opacity: ${props => (props.hasValue ? '1' : '0.2')};
text-transform: capitalize;
opacity: ${(props: PropsWithTheme<{ hasValue: boolean }>) => (props.hasValue ? '1' : '0.2')};
text-transform: ${(props: PropsWithTheme<{ capitalize: boolean }>) => (props.capitalize ? 'capitalize' : 'none')};
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@ -46,15 +56,16 @@ const SelectMenuButtonWrapper = styled.div`
padding: 13px;
border-left: ${props => `1px solid ${props.theme.colors.background}`};
`;
/* eslint-disable max-len */
const SelectMenuButton = styled.button`
padding: 3px 7px;
outline: none;
background-color: transparent;
border: 1px solid ${props => (props.isOpen ? props.theme.colors.primary : '#29292D')};
border: 1px solid
${(props: PropsWithTheme<{ isOpen: boolean }>) => (props.isOpen ? props.theme.colors.primary : '#29292D')};
border-radius: 100%;
`;
/* eslint-enable max-len */
const Icon = styled.img`
width: 10px;
@ -66,7 +77,7 @@ const OptionsWrapper = styled.div`
flex-direction: column;
position: absolute;
width: 100%;
${props => `${props.placement}: ${`-${props.optionsAmount * 40}px`}`};
${(props: PropsWithTheme<{ placement: string, optionsAmount: number }>) => `${String(props.placement)}: ${`-${String((props.optionsAmount || 0) * 40)}px`}`};
overflow-y: auto;
`;
@ -77,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;
@ -99,7 +110,9 @@ type Props = {
onChange: string => void,
placement?: 'top' | 'bottom',
bgColor?: string,
capitalize?: boolean,
};
type State = {
isOpen: boolean,
};
@ -113,6 +126,7 @@ export class SelectComponent extends PureComponent<Props, State> {
placeholder: '',
placement: 'bottom',
bgColor: theme.colors.inputBackground,
capitalize: true,
};
onSelect = (value: string) => {
@ -123,7 +137,10 @@ export class SelectComponent extends PureComponent<Props, State> {
handleClickOutside = (event: Object) => {
const { isOpen } = this.state;
if (isOpen && event.target.id !== 'select-options-wrapper') this.setState(() => ({ isOpen: false }));
if (isOpen && event.target.id !== 'select-options-wrapper') {
this.setState(() => ({ isOpen: false }));
}
};
getSelectedLabel = (value: string) => {
@ -145,20 +162,21 @@ export class SelectComponent extends PureComponent<Props, State> {
};
render() {
const {
value, options, placeholder, placement, bgColor,
} = this.props;
const { isOpen } = this.state;
const {
value, options, placeholder, placement, bgColor, capitalize,
} = this.props;
return (
<SelectWrapper
data-testid='Select'
id='select-component'
isOpen={isOpen}
placement={placement}
onClick={() => this.setState(() => ({ isOpen: !isOpen }))}
bgColor={bgColor}
>
<ValueWrapper hasValue={Boolean(value)}>
<ValueWrapper hasValue={Boolean(value)} capitalize={capitalize}>
{this.getSelectedLabel(value) || placeholder}
</ValueWrapper>
<SelectMenuButtonWrapper>
@ -178,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

@ -1,4 +1,5 @@
// @flow
/* eslint-disable max-len */
import React from 'react';
import styled from 'styled-components';
@ -10,18 +11,20 @@ const Wrapper = styled.div`
flex-direction: column;
width: ${props => props.theme.sidebarWidth};
height: ${props => `calc(100vh - ${props.theme.headerHeight})`};
font-family: ${props => props.theme.fontFamily}
font-family: ${props => props.theme.fontFamily};
background-color: ${props => props.theme.colors.sidebarBg};
padding-top: 15px;
position: relative;
`;
/* eslint-disable max-len */
type StyledLinkProps = PropsWithTheme<{ isActive: boolean }>;
const StyledLink = styled.a`
color: ${props => (props.isActive ? props.theme.colors.sidebarItemActive : props.theme.colors.sidebarItem)};
font-size: ${props => `${props.theme.fontSize.regular}em`};
color: ${(props: StyledLinkProps) => (props.isActive ? props.theme.colors.sidebarItemActive : props.theme.colors.sidebarItem)};
font-size: ${(props: StyledLinkProps) => `${props.theme.fontSize.regular}em`};
text-decoration: none;
font-weight: ${props => props.theme.fontWeight.bold};
background-color: ${props => (props.isActive ? `${props.theme.colors.sidebarHoveredItem}` : 'transparent')};
font-weight: ${(props: StyledLinkProps) => String(props.theme.fontWeight.bold)};
background-color: ${(props: StyledLinkProps) => (props.isActive ? `${props.theme.colors.sidebarHoveredItem}` : 'transparent')};
letter-spacing: 0.25px;
padding: 25px 20px;
height: 35px;
@ -29,13 +32,13 @@ const StyledLink = styled.a`
display: flex;
align-items: center;
outline: none;
border-right: ${props => (props.isActive ? `3px solid ${props.theme.colors.sidebarItemActive}` : 'none')};
border-right: ${(props: StyledLinkProps) => (props.isActive ? `3px solid ${props.theme.colors.sidebarItemActive(props)}` : 'none')};
cursor: pointer;
transition: all 0.03s ${props => props.theme.colors.transitionEase};
transition: all 0.03s ${(props: StyledLinkProps) => props.theme.transitionEase};
&:hover {
color: ${props => (props.isActive ? props.theme.colors.sidebarItemActive : '#ddd')}
background-color: ${props => props.theme.colors.sidebarHoveredItem};
color: ${(props: StyledLinkProps) => (props.isActive ? props.theme.colors.sidebarItemActive : '#ddd')}
background-color: ${(props: StyledLinkProps) => props.theme.colors.sidebarHoveredItem};
}
`;
@ -45,7 +48,7 @@ const Icon = styled.img`
margin-right: 13px;
${StyledLink}:hover & {
filter: ${props => (props.isActive ? 'none' : 'brightness(300%)')};
filter: ${(props: StyledLinkProps) => (props.isActive ? 'none' : 'brightness(300%)')};
}
`;
@ -72,7 +75,7 @@ export const SidebarComponent = ({ options, location, history }: Props) => (
key={item.route}
onClick={() => (isActive ? {} : history.push(item.route))}
>
<Icon isActive={isActive} src={item.icon(isActive)} alt={`${item.route}`} />
<Icon isActive={isActive} src={item.icon(isActive)} Alt={`${item.route}`} />
{item.label}
</StyledLink>
);

View File

@ -1,4 +1,5 @@
// @flow
import React, { Component } from 'react';
import styled, { keyframes } from 'styled-components';
import eres from 'eres';
@ -15,7 +16,6 @@ const rotate = keyframes`
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
@ -34,12 +34,14 @@ const Icon = styled.img`
height: 12px;
margin-right: 8px;
animation: 2s linear infinite;
animation-name: ${props => (props.animated ? rotate : 'none')};
animation-name: ${/** $FlowFixMe */
(props: PropsWithTheme<{ animated: boolean }>) => (props.animated ? rotate : 'none')};
`;
const StatusPillLabel = styled(TextComponent)`
color: ${props => props.theme.colors.statusPillLabel};
font-weight: ${props => props.theme.fontWeight.bold};
font-weight: ${props => String(props.theme.fontWeight.bold)};
text-transform: uppercase;
font-size: 10px;
padding-top: 1px;
@ -81,6 +83,8 @@ export class StatusPill extends Component<Props, State> {
getBlockchainStatus = async () => {
const [blockchainErr, blockchaininfo] = await eres(rpc.getblockchaininfo());
if (blockchainErr || !blockchaininfo) return;
const newProgress = blockchaininfo.verificationprogress * 100;
this.setState({
@ -104,11 +108,12 @@ export class StatusPill extends Component<Props, State> {
type, icon, progress, isSyncing,
} = this.state;
const showPercent = isSyncing ? `(${progress.toFixed(2)}%)` : '';
const typeText = type === 'ready' ? 'Synced' : type;
return (
<Wrapper id='status-pill'>
<Icon src={icon} animated={isSyncing} />
<StatusPillLabel value={`${type} ${showPercent}`} />
<StatusPillLabel value={`${typeText} ${showPercent}`} />
</Wrapper>
);
}

View File

@ -13,10 +13,42 @@ import { DoczWrapper } from '../theme.js'
<PropsTable of={StatusPill} />
## Basic Usage
## Syching
<Playground>
<DoczWrapper>
{() => <StatusPill percent={83.} />}
{() =>
<StatusPill
progress={99.3}
type='syncing'
/>
}
</DoczWrapper>
</Playground>
## Ready
<Playground>
<DoczWrapper>
{() =>
<StatusPill
progress={100.}
type='ready'
/>
}
</DoczWrapper>
</Playground>
## With Error
<Playground>
<DoczWrapper>
{() =>
<StatusPill
progress={0.}
type='error'
/>
}
</DoczWrapper>
</Playground>

View File

@ -1,4 +1,5 @@
// @flow
/* eslint-disable max-len */
import React from 'react';
import styled from 'styled-components';
@ -6,16 +7,6 @@ import type { ElementProps } from 'react';
import theme from '../theme';
const Text = styled.p`
font-family: ${props => props.theme.fontFamily};
font-size: ${props => props.size};
color: ${props => props.color || props.theme.colors.text};
margin: 0;
padding: 0;
font-weight: ${props => (props.isBold ? props.theme.fontWeight.bold : props.theme.fontWeight.default)};
text-align: ${props => props.align};
`;
export type Props = {
...ElementProps<'p'>,
value: string,
@ -26,6 +17,16 @@ export type Props = {
align?: string,
};
const Text = styled.p`
font-family: ${(props: PropsWithTheme<Props>) => props.theme.fontFamily};
font-size: ${(props: PropsWithTheme<Props>) => String(props.size)};
color: ${(props: PropsWithTheme<Props>) => props.color || props.theme.colors.text};
margin: 0;
padding: 0;
font-weight: ${(props: PropsWithTheme<Props>) => String(props.isBold ? props.theme.fontWeight.bold : props.theme.fontWeight.default)};
text-align: ${(props: PropsWithTheme<Props>) => props.align || 'left'};
`;
export const TextComponent = ({
value, isBold, color, className, size, align, id,
}: Props) => (

View File

@ -1,4 +1,5 @@
// @flow
import React, { Fragment } from 'react';
import styled from 'styled-components';
@ -24,7 +25,7 @@ const Day = styled(TextComponent)`
text-transform: uppercase;
color: ${props => props.theme.colors.transactionsDate};
font-size: ${props => `${props.theme.fontSize.regular * 0.9}em`};
font-weight: ${props => props.theme.fontWeight.bold};
font-weight: ${props => String(props.theme.fontWeight.bold)};
margin-bottom: 5px;
`;
@ -34,31 +35,25 @@ type Props = {
zecPrice: number,
};
export const TransactionDailyComponent = ({
transactionsDate,
transactions,
zecPrice,
}: Props) => (
<Wrapper>
export const TransactionDailyComponent = ({ transactionsDate, transactions, zecPrice }: Props) => (
<Wrapper data-testid='TransactionsDaily'>
<Day value={transactionsDate} />
<TransactionsWrapper>
{transactions.map(
({
date, type, address, amount, transactionId,
}, idx) => (
<Fragment key={`${address}-${type}-${amount}-${date}`}>
<TransactionItemComponent
transactionId={transactionId}
type={type}
date={date}
address={address || ''}
amount={amount}
zecPrice={zecPrice}
/>
{idx < transactions.length - 1 && <Divider />}
</Fragment>
),
)}
{transactions.map(({
date, type, address, amount, transactionId,
}, idx) => (
<Fragment key={`${address}-${type}-${amount}-${date}`}>
<TransactionItemComponent
transactionId={transactionId}
type={type}
date={date}
address={address || ''}
amount={amount}
zecPrice={zecPrice}
/>
{idx < transactions.length - 1 && <Divider />}
</Fragment>
))}
</TransactionsWrapper>
</Wrapper>
);

View File

@ -1,4 +1,5 @@
// @flow
import React from 'react';
import styled from 'styled-components';
import dateFns from 'date-fns';
@ -90,7 +91,7 @@ const Divider = styled.div`
`;
const Label = styled(TextComponent)`
font-weight: ${props => props.theme.fontWeight.bold};
font-weight: ${props => String(props.theme.fontWeight.bold)};
color: ${props => props.theme.colors.transactionsDetailsLabel};
margin-bottom: 5px;
letter-spacing: 0.25px;

View File

@ -1,4 +1,5 @@
// @flow
import React from 'react';
import styled from 'styled-components';
import dateFns from 'date-fns';
@ -34,7 +35,7 @@ const Icon = styled.img`
/* eslint-disable max-len */
const TransactionTypeLabel = styled(TextComponent)`
color: ${props => (props.isReceived ? props.theme.colors.transactionReceived : props.theme.colors.transactionSent)};
color: ${(props: PropsWithTheme<{ isReceived: boolean }>) => (props.isReceived ? props.theme.colors.transactionReceived : props.theme.colors.transactionSent)};
text-transform: capitalize;
`;
/* eslint-enable max-len */
@ -42,7 +43,7 @@ const TransactionTypeLabel = styled(TextComponent)`
const TransactionAddress = styled(TextComponent)`
color: #a7a7a7;
${Wrapper}:hover & {
${String(Wrapper)}:hover & {
color: #fff;
}
`;

View File

@ -1,7 +1,7 @@
// @flow
import React, { PureComponent } from 'react';
import React, { Component } from 'react';
import styled from 'styled-components';
import { Transition, animated } from 'react-spring';
import { ColumnComponent } from './column';
import { Button } from './button';
@ -17,6 +17,7 @@ const AddressWrapper = styled.div`
background-color: #000;
border-radius: 6px;
padding: 7px 13px;
margin-bottom: 10px;
width: 100%;
`;
@ -35,68 +36,29 @@ const Input = styled.input`
}
`;
/* eslint-disable max-len */
const Btn = styled(Button)`
border-width: 1px;
font-weight: ${props => props.theme.fontWeight.regular};
border-color: ${props => (props.isVisible ? props.theme.colors.primary : props.theme.colors.buttonBorderColor)};
padding: 8px 10px;
min-width: 260px;
img {
height: 12px;
width: 20px;
}
`;
/* eslint-enable max-len */
const QRCodeWrapper = styled.div`
align-items: center;
display: flex;
background-color: #000;
border-radius: 6px;
padding: 20px;
margin-top: 10px;
margin-bottom: 10px;
width: 100%;
`;
const RevealsMain = styled.div`
width: 100%;
height: 100%;
position: relative;
display: flex;
align-items: flex-start;
justify-content: flex-start;
height: ${props => (props.isVisible ? '178px' : 0)}
transition: all 0.25s ease-in-out;
& > div {
top: 0;
right: 0;
left: 0;
}
`;
type Props = {
address: string,
isVisible?: boolean,
};
type State = {
isVisible: boolean,
};
export class WalletAddress extends PureComponent<Props, State> {
static defaultProps = {
export class WalletAddress extends Component<Props, State> {
state = {
isVisible: false,
};
constructor(props: Props) {
super(props);
this.state = { isVisible: Boolean(props.isVisible) };
}
show = () => this.setState(() => ({ isVisible: true }));
hide = () => this.setState(() => ({ isVisible: false }));
@ -105,7 +67,6 @@ export class WalletAddress extends PureComponent<Props, State> {
const { address } = this.props;
const { isVisible } = this.state;
const toggleVisibility = isVisible ? this.hide : this.show;
const buttonLabel = `${isVisible ? 'Hide' : 'Show'} details and QR Code`;
return (
<ColumnComponent width='100%'>
@ -115,43 +76,18 @@ export class WalletAddress extends PureComponent<Props, State> {
onChange={() => {}}
onFocus={event => event.currentTarget.select()}
/>
<Btn
<Button
icon={eyeIcon}
label={buttonLabel}
label={`${isVisible ? 'Hide' : 'Show'} full address and QR Code`}
onClick={toggleVisibility}
variant='secondary'
isVisible={isVisible}
/>
</AddressWrapper>
<RevealsMain isVisible={isVisible}>
<Transition
native
items={isVisible}
enter={[
{
height: 'auto',
opacity: 1,
},
]}
leave={{ height: 0, opacity: 0 }}
from={{
position: 'absolute',
overflow: 'hidden',
opacity: 0,
height: 0,
}}
>
{show => show
&& (props => (
<animated.div style={props}>
<QRCodeWrapper>
<QRCode value={address} />
</QRCodeWrapper>
</animated.div>
))
}
</Transition>
</RevealsMain>
{!isVisible ? null : (
<QRCodeWrapper>
<QRCode value={address} />
</QRCodeWrapper>
)}
</ColumnComponent>
);
}

View File

@ -1,4 +1,5 @@
// @flow
import React from 'react';
import styled from 'styled-components';
@ -38,7 +39,7 @@ const Label = styled(TextComponent)`
const USDValue = styled(TextComponent)`
opacity: 0.5;
font-weight: ${props => props.theme.fontWeight.light};
font-weight: ${props => String(props.theme.fontWeight.light)};
`;
const ShieldedValue = styled(Label)`

View File

@ -13,7 +13,7 @@ import { DoczWrapper } from '../theme.js'
<PropsTable of={WalletSummaryComponent} />
## Basic usage
## Basic Usage
<Playground>
<DoczWrapper>

View File

@ -1,4 +1,5 @@
// @flow
import React, { type ComponentType, Component } from 'react';
import { LoadingScreen } from './loading-screen';

View File

@ -2,7 +2,10 @@
import React from 'react';
export const ZcashLogo = () => (
<svg xmlns='http://www.w3.org/2000/svg' viewBox='-75 -10 175 175'>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='-75 -10 175 175'
>
<defs>
<style>{'.a{ fill:#040508; }'}</style>
</defs>

View File

@ -1,4 +1,5 @@
// @flow
import isDev from 'electron-is-dev';
export const ZCASH_EXPLORER_BASE_URL = isDev

View File

@ -1,4 +1,5 @@
// @flow
import DashboardIcon from '../assets/images/dashboard_icon.svg';
import DashboardIconActive from '../assets/images/dashboard_icon_active.svg';
import ConsoleIcon from '../assets/images/console_icon.svg';
@ -25,8 +26,7 @@ export const MENU_OPTIONS = [
{
label: 'Dashboard',
route: DASHBOARD_ROUTE,
// eslint-disable-next-line
icon: (isActive: boolean) => isActive ? DashboardIconActive : DashboardIcon,
icon: (isActive: boolean) => (isActive ? DashboardIconActive : DashboardIcon),
},
{
label: 'Send',
@ -41,8 +41,7 @@ export const MENU_OPTIONS = [
{
label: 'Transactions',
route: TRANSACTIONS_ROUTE,
// eslint-disable-next-line
icon: (isActive: boolean) => isActive ? TransactionsIconActive : TransactionsIcon,
icon: (isActive: boolean) => (isActive ? TransactionsIconActive : TransactionsIcon),
},
{
label: 'Settings',

24
app/containers/app.js Normal file
View File

@ -0,0 +1,24 @@
// @flow
import { connect } from 'react-redux';
import { closeErrorModal } from '../redux/modules/app';
import { LayoutComponent } from '../components/layout';
import type { Dispatch } from '../types/redux';
import type { AppState } from '../types/app-state';
const mapStateToProps = ({ app }: AppState) => ({
isErrorModalVisible: app.isErrorModalVisible,
error: app.error,
});
const mapDispatchToProps = (dispatch: Dispatch) => ({
closeErrorModal: () => dispatch(closeErrorModal()),
});
// $FlowFixMe
export const AppContainer = connect(
mapStateToProps,
mapDispatchToProps,
)(LayoutComponent);

View File

@ -8,13 +8,14 @@ import dateFns from 'date-fns';
import { BigNumber } from 'bignumber.js';
import { DashboardView } from '../views/dashboard';
import rpc from '../../services/api';
import { listShieldedTransactions } from '../../services/shielded-transactions';
import store from '../../config/electron-store';
import {
loadWalletSummary,
loadWalletSummarySuccess,
loadWalletSummaryError,
} from '../redux/modules/wallet';
import { sortBy } from '../utils/sort-by';
import { sortByDescend } from '../utils/sort-by-descend';
import type { AppState } from '../types/app-state';
import type { Dispatch } from '../types/redux';
@ -47,7 +48,7 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({
);
}
const formattedTransactions = flow([
const formattedTransactions: Array<Object> = flow([
arr => arr.map(transaction => ({
transactionId: transaction.txid,
type: transaction.category,
@ -58,10 +59,11 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({
arr => groupBy(arr, obj => dateFns.format(obj.date, 'MMM DD, YYYY')),
obj => Object.keys(obj).map(day => ({
day,
list: sortBy('date')(obj[day]),
jsDay: new Date(day),
list: sortByDescend('date')(obj[day]),
})),
sortBy('day'),
])(transactions);
sortByDescend('jsDay'),
])([...transactions, ...listShieldedTransactions()]);
if (!zAddresses.length) {
const [, newZAddress] = await eres(rpc.z_getnewaddress());

View File

@ -1,4 +1,5 @@
// @flow
import eres from 'eres';
import { connect } from 'react-redux';
@ -7,6 +8,9 @@ import { ReceiveView } from '../views/receive';
import {
loadAddressesSuccess,
loadAddressesError,
getNewAddressSuccess,
getNewAddressError,
type addressType,
} from '../redux/modules/receive';
import rpc from '../../services/api';
@ -22,9 +26,7 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({
loadAddresses: async () => {
const [zAddressesErr, zAddresses] = await eres(rpc.z_listaddresses());
const [tAddressesErr, transparentAddresses] = await eres(
rpc.getaddressesbyaccount(''),
);
const [tAddressesErr, transparentAddresses] = await eres(rpc.getaddressesbyaccount(''));
if (zAddressesErr || tAddressesErr) return dispatch(loadAddressesError({ error: 'Something went wrong!' }));
@ -34,6 +36,15 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({
}),
);
},
getNewAddress: async ({ type }: { type: addressType }) => {
const [error, address] = await eres(
type === 'shielded' ? rpc.z_getnewaddress() : rpc.getnewaddress(''),
);
if (error || !address) return dispatch(getNewAddressError({ error: 'Unable to generate a new address' }));
dispatch(getNewAddressSuccess({ address }));
},
});
// $FlowFixMe

View File

@ -1,6 +1,7 @@
// @flow
import { connect } from 'react-redux';
import eres from 'eres';
import { connect } from 'react-redux';
import { BigNumber } from 'bignumber.js';
import store from '../../config/electron-store';
@ -15,9 +16,12 @@ import {
resetSendTransaction,
validateAddressSuccess,
validateAddressError,
loadAddressBalanceSuccess,
loadAddressBalanceError,
} from '../redux/modules/send';
import { filterObjectNullKeys } from '../utils/filter-object-null-keys';
import { saveShieldedTransaction } from '../../services/shielded-transactions';
import type { AppState } from '../types/app-state';
import type { Dispatch } from '../types/redux';
@ -32,8 +36,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,
@ -89,6 +93,15 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({
if (operationStatus && operationStatus.status === 'success') {
clearInterval(interval);
if (from.startsWith('z')) {
saveShieldedTransaction({
category: 'send',
time: Date.now() / 1000,
address: '(Shielded)',
amount: new BigNumber(amount).toNumber(),
memo,
});
}
dispatch(sendTransactionSuccess({ operationId: operationStatus.result.txid }));
}
@ -140,6 +153,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

@ -1,4 +1,5 @@
// @flow
import { connect } from 'react-redux';
import { SettingsView } from '../views/settings';

View File

@ -1,6 +1,7 @@
// @flow
import { connect } from 'react-redux';
import eres from 'eres';
import { connect } from 'react-redux';
import flow from 'lodash.flow';
import groupBy from 'lodash.groupby';
import dateFns from 'date-fns';
@ -13,9 +14,10 @@ import {
loadTransactionsError,
} from '../redux/modules/transactions';
import rpc from '../../services/api';
import { listShieldedTransactions } from '../../services/shielded-transactions';
import store from '../../config/electron-store';
import { sortBy } from '../utils/sort-by';
import { sortByDescend } from '../utils/sort-by-descend';
import type { AppState } from '../types/app-state';
import type { Dispatch } from '../types/redux';
@ -31,7 +33,7 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({
getTransactions: async () => {
dispatch(loadTransactions());
const [transactionsErr, transactions = []] = await eres(rpc.listtransactions());
const [transactionsErr, transactions = []] = await eres(rpc.listtransactions('', 200));
if (transactionsErr) {
return dispatch(loadTransactionsError({ error: transactionsErr.message }));
@ -48,10 +50,11 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({
arr => groupBy(arr, obj => dateFns.format(obj.date, 'MMM DD, YYYY')),
obj => Object.keys(obj).map(day => ({
day,
list: sortBy('date')(obj[day]),
jsDay: new Date(day),
list: sortByDescend('date')(obj[day]),
})),
sortBy('day'),
])(transactions);
sortByDescend('jsDay'),
])([...transactions, ...listShieldedTransactions()]);
dispatch(
loadTransactionsSuccess({

View File

@ -7,6 +7,7 @@ import thunk from 'redux-thunk';
import type { RouterHistory } from 'react-router-dom';
import { createRootReducer } from './modules/reducer';
import { errorHandler } from './errorHandler';
export const history: RouterHistory = createHashHistory();
@ -14,7 +15,8 @@ const shouldEnableDevTools = (process.env.NODE_ENV !== 'production' || process.e
&& window.devToolsExtension;
export const configureStore = (initialState: Object) => {
const middleware = applyMiddleware(thunk, routerMiddleware(history));
// $FlowFixMe
const middleware = applyMiddleware(thunk, routerMiddleware(history), errorHandler);
const enhancer = compose(
middleware,

16
app/redux/errorHandler.js Normal file
View File

@ -0,0 +1,16 @@
// @flow
import { LOAD_ADDRESSES_ERROR } from './modules/receive';
import { LOAD_TRANSACTIONS_ERROR } from './modules/transactions';
import { LOAD_WALLET_SUMMARY_ERROR } from './modules/wallet';
import { showErrorModal } from './modules/app';
import type { Middleware } from '../types/redux';
const ERRORS = [LOAD_ADDRESSES_ERROR, LOAD_TRANSACTIONS_ERROR, LOAD_WALLET_SUMMARY_ERROR];
export const errorHandler: Middleware = ({ dispatch }) => next => (action) => {
// eslint-disable-next-line max-len
if (ERRORS.includes(action.type)) return dispatch(showErrorModal({ error: action.payload.error || 'Something went wrong!' }));
return next(action);
};

41
app/redux/modules/app.js Normal file
View File

@ -0,0 +1,41 @@
// @flow
import type { Action } from '../../types/redux';
// Actions
export const SHOW_ERROR_MODAL = 'SHOW_ERROR_MODAL';
export const HIDE_ERROR_MODAL = 'HIDE_ERROR_MODAL';
export const showErrorModal = ({ error }: { error: string }) => ({
type: SHOW_ERROR_MODAL,
payload: {
error,
},
});
export const closeErrorModal = () => ({
type: HIDE_ERROR_MODAL,
payload: {},
});
export type State = {
isErrorModalVisible: boolean,
error: string | null,
};
const initialState: State = {
isErrorModalVisible: false,
error: null,
};
// eslint-disable-next-line
export default (state: State = initialState, action: Action) => {
switch (action.type) {
case SHOW_ERROR_MODAL:
return { isErrorModalVisible: true, error: action.payload.error };
case HIDE_ERROR_MODAL:
return { isErrorModalVisible: false, error: null };
default:
return state;
}
};

View File

@ -1,9 +1,12 @@
// @flow
import type { Action } from '../../types/redux';
// Actions
export const LOAD_ADDRESSES_SUCCESS = 'LOAD_ADDRESSES_SUCCESS';
export const LOAD_ADDRESSES_ERROR = 'LOAD_ADDRESSES_ERROR';
export const GET_NEW_ADDRESS_SUCCESS = 'GET_NEW_ADDRESS_SUCCESS';
export const GET_NEW_ADDRESS_ERROR = 'GET_NEW_ADDRESS_ERROR';
export const loadAddressesSuccess = ({ addresses }: { addresses: string[] }) => ({
type: LOAD_ADDRESSES_SUCCESS,
@ -17,6 +20,22 @@ export const loadAddressesError = ({ error }: { error: string }) => ({
payload: { error },
});
export const getNewAddressSuccess = ({ address }: { address: string }) => ({
type: GET_NEW_ADDRESS_SUCCESS,
payload: {
address,
},
});
export const getNewAddressError = ({ error }: { error: string }) => ({
type: GET_NEW_ADDRESS_ERROR,
payload: {
error,
},
});
export type addressType = 'transparent' | 'shielded';
export type State = {
addresses: string[],
error: string | null,
@ -40,6 +59,16 @@ export default (state: State = initialState, action: Action) => {
error: action.payload.error,
addresses: [],
};
case GET_NEW_ADDRESS_SUCCESS:
return {
error: null,
addresses: [...state.addresses, action.payload.address],
};
case GET_NEW_ADDRESS_ERROR:
return {
...state,
error: action.payload.error,
};
default:
return state;
}

View File

@ -4,6 +4,7 @@ import { combineReducers } from 'redux';
import { connectRouter } from 'connected-react-router';
import type { RouterHistory } from 'react-router-dom';
import app from './app';
import wallet from './wallet';
import transactions from './transactions';
import send from './send';
@ -11,6 +12,7 @@ import receive from './receive';
// $FlowFixMe
export const createRootReducer = (history: RouterHistory) => combineReducers({
app,
walletSummary: wallet,
transactions,
sendStatus: send,

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,7 +83,9 @@ const initialState: State = {
operationId: null,
isToAddressValid: false,
zecPrice: 0,
addressBalance: 0,
};
// eslint-disable-next-line
export default (state: State = initialState, action: Action): State => {
switch (action.type) {
@ -103,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

@ -1,4 +1,5 @@
// @flow
import type { Action } from '../../types/redux';
import type { Transaction } from '../../components/transaction-item';
@ -46,6 +47,7 @@ const initialState = {
error: null,
isLoading: false,
};
// eslint-disable-next-line
export default (state: State = initialState, action: Action) => {
switch (action.type) {

View File

@ -1,4 +1,5 @@
// @flow
import type { Action } from '../../types/redux';
import type { TransactionsList } from './transactions';
@ -65,6 +66,7 @@ const initialState = {
addresses: [],
transactions: [],
};
// eslint-disable-next-line
export default (state: State = initialState, action: Action) => {
switch (action.type) {

View File

@ -1,4 +1,5 @@
// @flow
import { compose } from 'redux';
import { withRouter } from 'react-router-dom';
import { RouterComponent } from './router';

View File

@ -15,7 +15,7 @@ import { ReceiveContainer } from '../containers/receive';
import { SettingsContainer } from '../containers/settings';
import { NotFoundView } from '../views/not-found';
import { ConsoleView } from '../views/console';
import { LayoutComponent } from '../components/layout';
import { AppContainer as LayoutComponent } from '../containers/app';
import { HeaderComponent } from '../components/header';
import {
@ -55,17 +55,39 @@ export const RouterComponent = ({
<FullWrapper>
<HeaderComponent title={getTitle(location.pathname)} />
<ContentWrapper>
<SidebarContainer location={location} history={history} />
<SidebarContainer
location={location}
history={history}
/>
{/* $FlowFixMe */}
<LayoutComponent>
<ScrollTopComponent>
<Switch>
<Route exact path={DASHBOARD_ROUTE} component={DashboardContainer} />
<Route path={SEND_ROUTE} component={SendContainer} />
<Route path={RECEIVE_ROUTE} component={ReceiveContainer} />
<Route path={SETTINGS_ROUTE} component={SettingsContainer} />
<Route path={CONSOLE_ROUTE} component={ConsoleView} />
<Route path={TRANSACTIONS_ROUTE} component={TransactionsContainer} />
<Route
exact
path={DASHBOARD_ROUTE}
component={DashboardContainer}
/>
<Route
path={SEND_ROUTE}
component={SendContainer}
/>
<Route
path={RECEIVE_ROUTE}
component={ReceiveContainer}
/>
<Route
path={SETTINGS_ROUTE}
component={SettingsContainer}
/>
<Route
path={CONSOLE_ROUTE}
component={ConsoleView}
/>
<Route
path={TRANSACTIONS_ROUTE}
component={TransactionsContainer}
/>
<Route component={NotFoundView} />
</Switch>
</ScrollTopComponent>

View File

@ -1,5 +1,6 @@
// @flow
import React, { Fragment } from 'react';
import React, { Fragment, type Node } from 'react';
import theme from 'styled-theming';
import { ThemeProvider, createGlobalStyle } from 'styled-components';
// $FlowFixMe
@ -11,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';
@ -30,7 +30,7 @@ const selectButtonShadow = 'rgba(238,201,76,0.65)';
const statusPillLabel = '#727272';
const transactionsDetailsLabel = transactionsDate;
const appTheme = {
const appTheme: AppTheme = {
mode: DARK,
fontFamily: 'Roboto',
fontWeight: {
@ -53,29 +53,98 @@ const appTheme = {
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',
@ -94,9 +163,7 @@ export const GlobalStyle = createGlobalStyle`
}
`;
/* eslint-disable react/prop-types */
// $FlowFixMe
export const DoczWrapper = ({ children }) => (
export const DoczWrapper = ({ children }: { children: () => Node<*> }) => (
<ThemeProvider theme={appTheme}>
<Fragment>
<GlobalStyle />

View File

@ -4,10 +4,12 @@ import type { State as WalletSummaryState } from '../redux/modules/wallet';
import type { State as TransactionsState } from '../redux/modules/transactions';
import type { State as SendState } from '../redux/modules/send';
import type { State as ReceiveState } from '../redux/modules/receive';
import type { State as App } from '../redux/modules/app';
export type AppState = {
walletSummary: WalletSummaryState,
transactions: TransactionsState,
sendStatus: SendState,
receive: ReceiveState,
app: App,
};

View File

@ -1,7 +1,10 @@
// @flow
type State = {||};
import type { AppState } from './app-state';
export type Action = { type: $Subtype<string>, payload: Object };
export type GetState = () => State;
export type GetState = () => AppState;
export type Dispatch = (action: Action) => void;
export type Middleware = ({ dispatch: Dispatch, getState: GetState }) => (
(Action) => void,
) => Action => void;

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

@ -1,4 +1,5 @@
// @flow
export const filterObjectNullKeys = (obj: Object) => Object.keys(obj).reduce((acc, cur) => {
if (obj[cur] === null || obj[cur] === undefined || obj[cur] === '') {
return acc;

View File

@ -1,2 +1,3 @@
// @flow
export const formatNumber = ({ value, append = '' }: { value: number | string, append?: string }) => `${append}${(value || 0).toLocaleString()}`;
export const formatNumber = ({ value, append = '' }: { value: number, append?: string }) => `${append}${(value || 0).toLocaleString()}`;

View File

@ -1,4 +1,5 @@
// @flow
// eslint-disable-next-line
import electron from 'electron';

View File

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

Some files were not shown because too many files have changed in this diff Show More