diff --git a/__tests__/actions/receive.test.js b/__tests__/actions/receive.test.js new file mode 100644 index 0000000..9ec0391 --- /dev/null +++ b/__tests__/actions/receive.test.js @@ -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, + }), + ); + }); +}); diff --git a/__tests__/actions/send.test.js b/__tests__/actions/send.test.js new file mode 100644 index 0000000..4f82909 --- /dev/null +++ b/__tests__/actions/send.test.js @@ -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 + }), + ); + }); +}); diff --git a/__tests__/actions/transactions.test.js b/__tests__/actions/transactions.test.js new file mode 100644 index 0000000..836ad4a --- /dev/null +++ b/__tests__/actions/transactions.test.js @@ -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, + }), + ); + }); +}); diff --git a/__tests__/reducers/wallet-summary.test.js b/__tests__/reducers/wallet-summary.test.js index 7095f5e..b7a9156 100644 --- a/__tests__/reducers/wallet-summary.test.js +++ b/__tests__/reducers/wallet-summary.test.js @@ -8,12 +8,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 +31,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 +57,9 @@ describe('WalletSummary Reducer', () => { ...action.payload, error: null, isLoading: false, - dollarValue: 0, + addresses: [], + transactions: [], + zecPrice: 0, }; expect(walletSummaryReducer(undefined, action)).toEqual(expectedState); @@ -72,9 +78,11 @@ describe('WalletSummary Reducer', () => { transparent: 0, error: action.payload.error, isLoading: false, - dollarValue: 0, + addresses: [], + transactions: [], + zecPrice: 0 }; expect(walletSummaryReducer(undefined, action)).toEqual(expectedState); }); -}); +}); \ No newline at end of file diff --git a/__tests__/utils/filter-object-null-keys.test.js b/__tests__/utils/filter-object-null-keys.test.js new file mode 100644 index 0000000..b0bb3fd --- /dev/null +++ b/__tests__/utils/filter-object-null-keys.test.js @@ -0,0 +1,22 @@ +// @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); + }) +}) \ No newline at end of file diff --git a/__tests__/utils/format-number.test.js b/__tests__/utils/format-number.test.js new file mode 100644 index 0000000..af44a55 --- /dev/null +++ b/__tests__/utils/format-number.test.js @@ -0,0 +1,37 @@ +// @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); + }); +}) \ No newline at end of file diff --git a/__tests__/utils/timestamp.test.js b/__tests__/utils/timestamp.test.js new file mode 100644 index 0000000..5d912db --- /dev/null +++ b/__tests__/utils/timestamp.test.js @@ -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)); + + }); +}) \ No newline at end of file diff --git a/__tests__/utils/truncate-address.test.js b/__tests__/utils/truncate-address.test.js new file mode 100644 index 0000000..460ede5 --- /dev/null +++ b/__tests__/utils/truncate-address.test.js @@ -0,0 +1,14 @@ +// @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); + }); +}) \ No newline at end of file diff --git a/app/assets/images/plus_icon.svg b/app/assets/images/plus_icon.svg new file mode 100644 index 0000000..32ba39b --- /dev/null +++ b/app/assets/images/plus_icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/components/wallet-address.js b/app/components/wallet-address.js index f2ba388..5957f78 100644 --- a/app/components/wallet-address.js +++ b/app/components/wallet-address.js @@ -3,7 +3,6 @@ import React, { PureComponent } from 'react'; import styled from 'styled-components'; import { Transition, animated } from 'react-spring'; -import { ColumnComponent } from './column'; import { Button } from './button'; import { QRCode } from './qrcode'; @@ -18,6 +17,7 @@ const AddressWrapper = styled.div` border-radius: 6px; padding: 7px 13px; width: 100%; + margin-bottom: 5px; `; const Input = styled.input` @@ -56,7 +56,6 @@ const QRCodeWrapper = styled.div` background-color: #000; border-radius: 6px; padding: 20px; - margin-top: 10px; width: 100%; `; @@ -108,7 +107,7 @@ export class WalletAddress extends PureComponent { const buttonLabel = `${isVisible ? 'Hide' : 'Show'} details and QR Code`; return ( - +
{ } - +
); } } diff --git a/app/containers/dashboard.js b/app/containers/dashboard.js index 28b88cd..1c0f085 100644 --- a/app/containers/dashboard.js +++ b/app/containers/dashboard.js @@ -14,7 +14,7 @@ import { loadWalletSummarySuccess, loadWalletSummaryError, } from '../redux/modules/wallet'; -import { sortBy } from '../utils/sort-by'; +import sortBy from '../utils/sort-by'; import type { AppState } from '../types/app-state'; import type { Dispatch } from '../types/redux'; diff --git a/app/containers/receive.js b/app/containers/receive.js index 783d75a..0318abc 100644 --- a/app/containers/receive.js +++ b/app/containers/receive.js @@ -7,6 +7,9 @@ import { ReceiveView } from '../views/receive'; import { loadAddressesSuccess, loadAddressesError, + getNewAddressSuccess, + getNewAddressError, + type addressType, } from '../redux/modules/receive'; import rpc from '../../services/api'; @@ -22,9 +25,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 +35,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 diff --git a/app/containers/transactions.js b/app/containers/transactions.js index fd8dffc..1584fec 100644 --- a/app/containers/transactions.js +++ b/app/containers/transactions.js @@ -15,7 +15,7 @@ import { import rpc from '../../services/api'; import store from '../../config/electron-store'; -import { sortBy } from '../utils/sort-by'; +import sortBy from '../utils/sort-by'; import type { AppState } from '../types/app-state'; import type { Dispatch } from '../types/redux'; diff --git a/app/redux/modules/receive.js b/app/redux/modules/receive.js index 18a5714..1d651d8 100644 --- a/app/redux/modules/receive.js +++ b/app/redux/modules/receive.js @@ -4,6 +4,8 @@ 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 +19,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 +58,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; } diff --git a/app/utils/format-number.js b/app/utils/format-number.js index 3fc4ad7..ca140f4 100644 --- a/app/utils/format-number.js +++ b/app/utils/format-number.js @@ -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()}`; diff --git a/app/utils/sort-by.js b/app/utils/sort-by.js index f8eefb6..25a102e 100644 --- a/app/utils/sort-by.js +++ b/app/utils/sort-by.js @@ -1,4 +1,4 @@ // @flow /* eslint-disable max-len */ // $FlowFixMe -export const sortBy = (field: string) => (arr: T[]): T[] => arr.sort((a, b) => (a[field] < b[field] ? 1 : -1)); +export default (field: string) => (arr: T[]): T[] => arr.sort((a, b) => (a[field] < b[field] ? 1 : -1)); diff --git a/app/utils/truncate-address.js b/app/utils/truncate-address.js index 49493cd..978a324 100644 --- a/app/utils/truncate-address.js +++ b/app/utils/truncate-address.js @@ -1,3 +1,6 @@ // @flow -export const truncateAddress = (address: string = '') => `${address.substr(0, 20)}...${address.substr(address.length - 10, address.length)}`; +export const truncateAddress = (address: string = '') => `${address.substr(0, 20)}...${address.substr( + address.length - 10, + address.length, +)}`; diff --git a/app/views/receive.js b/app/views/receive.js index 866c91b..c38e191 100644 --- a/app/views/receive.js +++ b/app/views/receive.js @@ -1,5 +1,5 @@ // @flow -import React, { Fragment, PureComponent } from 'react'; +import React, { PureComponent } from 'react'; import styled from 'styled-components'; import { Transition, animated } from 'react-spring'; @@ -9,6 +9,9 @@ import { TextComponent } from '../components/text'; import { WalletAddress } from '../components/wallet-address'; import MenuIcon from '../assets/images/menu_icon.svg'; +import PlusIcon from '../assets/images/plus_icon.svg'; + +import type { addressType } from '../redux/modules/receive'; const Row = styled(RowComponent)` margin-bottom: 10px; @@ -22,7 +25,7 @@ const Label = styled(InputLabelComponent)` margin-bottom: 5px; `; -const ShowMoreButton = styled.button` +const ActionButton = styled.button` background: none; border: none; cursor: pointer; @@ -39,12 +42,13 @@ const ShowMoreButton = styled.button` } `; -const ShowMoreIcon = styled.img` +const ActionIcon = styled.img` width: 25px; height: 25px; border: 1px solid ${props => props.theme.colors.text}; border-radius: 100%; margin-right: 11.5px; + padding: 5px; `; const RevealsMain = styled.div` @@ -54,17 +58,12 @@ const RevealsMain = styled.div` display: flex; align-items: flex-start; justify-content: flex-start; - - & > div { - top: 0; - right: 0; - left: 0; - } `; type Props = { addresses: Array, loadAddresses: () => void, + getNewAddress: ({ type: addressType }) => void, }; type State = { @@ -86,67 +85,65 @@ export class ReceiveView extends PureComponent { showAdditionalOptions: !prevState.showAdditionalOptions, })); - renderShieldedAddresses = (address: string) => { - const { showAdditionalOptions } = this.state; - const buttonText = `${showAdditionalOptions ? 'Hide' : 'Show'} Other Address Types`; + generateNewAddress = (type: addressType) => { + const { getNewAddress } = this.props; - return ( - - - ); - }; - - renderTransparentAddresses = (address: string) => { - const { showAdditionalOptions } = this.state; - - return ( - - - {show => show - && (props => ( - - - )) - } - - - ); + getNewAddress({ type }); }; render() { const { addresses } = this.props; + const { showAdditionalOptions } = this.state; + const buttonText = `${showAdditionalOptions ? 'Hide' : 'Show'} Other Address Types`; + + const shieldedAddresses = addresses.filter(addr => addr.startsWith('z')); + const transparentAddresses = addresses.filter(addr => addr.startsWith('t')); return (
- {(addresses || []).map((address, index) => { - if (index === 0) return this.renderShieldedAddresses(address); - return this.renderTransparentAddresses(address); - })} +
); } diff --git a/app/views/send.js b/app/views/send.js index 16f244c..abff385 100644 --- a/app/views/send.js +++ b/app/views/send.js @@ -425,7 +425,7 @@ export class SendView extends PureComponent { const isEmpty = amount === ''; - const fixedAmount = isEmpty ? '0.00' : amount; + const fixedAmount = isEmpty ? 0.0 : amount; const zecBalance = formatNumber({ value: balance, append: 'ZEC ' }); const zecBalanceInUsd = formatNumber({ diff --git a/flow-custom-typedefs/bignumber.js b/flow-custom-typedefs/bignumber.js index 65bbb11..612c835 100644 --- a/flow-custom-typedefs/bignumber.js +++ b/flow-custom-typedefs/bignumber.js @@ -999,10 +999,10 @@ x.toFormat(3, BigNumber.ROUND_UP, fmt) // '12.34.56.789,124' roundingMode: BigNumber$RoundingMode, format?: BigNumber$Format, ): string; - toFormat(decimalPlaces: number, roundingMode?: BigNumber$RoundingMode): string; - toFormat(decimalPlaces?: number): string; - toFormat(decimalPlaces: number, format: BigNumber$Format): string; - toFormat(format: BigNumber$Format): string; + toFormat(decimalPlaces: number, roundingMode?: BigNumber$RoundingMode): number; + toFormat(decimalPlaces?: number): number; + toFormat(decimalPlaces: number, format: BigNumber$Format): number; + toFormat(format: BigNumber$Format): number; /** * Returns an array of two BigNumbers representing the value of this BigNumber as a simple diff --git a/public/flow-coverage-badge.svg b/public/flow-coverage-badge.svg index 665ba93..d61c9ff 100644 --- a/public/flow-coverage-badge.svg +++ b/public/flow-coverage-badge.svg @@ -1 +1 @@ -flow-coverageflow-coverage82%82% \ No newline at end of file +flow-coverageflow-coverage81%81% \ No newline at end of file