diff --git a/src/common/HandleLogin/index.jsx b/src/common/HandleLogin/index.jsx index f9b6271..359e71e 100644 --- a/src/common/HandleLogin/index.jsx +++ b/src/common/HandleLogin/index.jsx @@ -12,7 +12,7 @@ const mapDispatchToProps = (dispatch) => { export const HandleLoginComponent = ({ history, handlePendingSignIn }) => { if (isSignInPending()) { handlePendingSignIn().then(() => { - history.push('/') + history.push('/transactions') }) } return null diff --git a/src/common/Header/index.jsx b/src/common/Header/index.jsx index c1c738b..8f00b6f 100644 --- a/src/common/Header/index.jsx +++ b/src/common/Header/index.jsx @@ -7,15 +7,18 @@ import Logo from '../Logo/index' import LoginButton from '../LoginButton' import TopNav from '../TopNav' -const styles = { +const styles = theme => ({ + root: { + zIndex: theme.zIndex.drawer + 1 + }, toolbar: { display: 'flex', 'justify-content': 'space-between' } -} +}) const Header = ({ classes }) => ( - + diff --git a/src/common/LeftDrawer/index.jsx b/src/common/LeftDrawer/index.jsx new file mode 100644 index 0000000..fb32565 --- /dev/null +++ b/src/common/LeftDrawer/index.jsx @@ -0,0 +1,80 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { withStyles } from '@material-ui/core/styles' +import classNames from 'classnames' +import Drawer from '@material-ui/core/Drawer' +import List from '@material-ui/core/List' +import ListSubheader from '@material-ui/core/ListSubheader' +import ListItem from '@material-ui/core/ListItem' +import ListItemIcon from '@material-ui/core/ListItemIcon' +import ListItemText from '@material-ui/core/ListItemText' +import Typography from '@material-ui/core/Typography' +import DashboardIcon from '@material-ui/icons/Dashboard' +import Button from '@material-ui/core/Button' +import AddIcon from '@material-ui/icons/Add' +import Divider from '@material-ui/core/Divider' +import grey from '@material-ui/core/colors/grey' + +const styles = theme => ({ + toolbar: theme.mixins.toolbar, + drawerPaper: { + maxWidth: 200 + }, + menuTitle: { + margin: '20px' + }, + button: { + fontSize: 10, + padding: 5, + minHeight: 22, + marginLeft: 25 + }, + leftIcon: { + marginRight: theme.spacing.unit + }, + iconSmall: { + fontSize: 12 + }, + noAccounts: { + background: grey[100], + margin: '0 20px 20px 25px', + padding: theme.spacing.unit + } +}) + +const LeftDrawer = ({ classes }) => ( + +
+ + + + + + + + Accounts} + > + + You don't have any accounts yet + + + + +) + +LeftDrawer.propTypes = { + classes: PropTypes.object.isRequired +} + +export default withStyles(styles)(LeftDrawer) diff --git a/src/common/LoadingOverlay/index.jsx b/src/common/LoadingOverlay/index.jsx index 080829f..c8b676f 100644 --- a/src/common/LoadingOverlay/index.jsx +++ b/src/common/LoadingOverlay/index.jsx @@ -10,7 +10,7 @@ const styles = { left: 0, right: 0, bottom: 0, - 'z-index': 1000, + zIndex: 10000, padding: '1.2rem', display: 'flex', position: 'fixed', diff --git a/src/common/LoginButton/__tests__/index.test.jsx b/src/common/LoginButton/__tests__/index.test.jsx index 6e28aca..b52964a 100644 --- a/src/common/LoginButton/__tests__/index.test.jsx +++ b/src/common/LoginButton/__tests__/index.test.jsx @@ -17,7 +17,7 @@ describe('HandleLogin', () => { it('matches snapshot with logged out user', () => { const component = renderer.create(( @@ -28,7 +28,7 @@ describe('HandleLogin', () => { it('matches snapshot with logged in user profile', () => { const component = renderer.create(( { it('handles login', () => { const wrapper = mount(( diff --git a/src/common/LoginButton/index.jsx b/src/common/LoginButton/index.jsx index b5265c7..25bdf98 100644 --- a/src/common/LoginButton/index.jsx +++ b/src/common/LoginButton/index.jsx @@ -1,16 +1,19 @@ +/* eslint-disable no-console */ import React from 'react' import PropTypes from 'prop-types' +import { compose } from 'recompose' import { connect } from 'react-redux' +import { withStyles } from '@material-ui/core/styles' import Button from '@material-ui/core/Button' import Avatar from '@material-ui/core/Avatar' import Popper from '@material-ui/core/Popper' import MenuItem from '@material-ui/core/MenuItem' import MenuList from '@material-ui/core/MenuList' +import AccountBoxIcon from '@material-ui/icons/AccountBox' import Paper from '@material-ui/core/Paper' import Fade from '@material-ui/core/Fade' import ClickAwayListener from '@material-ui/core/ClickAwayListener' import Tooltip from '@material-ui/core/Tooltip' -import { withStyles } from '@material-ui/core/styles' import { userLogin, userLogout } from '../../store/user/actions' const styles = { @@ -57,7 +60,7 @@ export class LoginButtonComponent extends React.Component { handleLogout } = this.props - if (user.isAuthenticated) { + if (user.isAuthenticatedWith === 'blockstack') { return (
@@ -90,6 +93,38 @@ export class LoginButtonComponent extends React.Component { ) } + if (user.isAuthenticatedWith === 'guest') { + return ( + +
+ + + + + + + + {({ TransitionProps }) => ( + + + + Profile + Logout + + + + )} + +
+
+ ) + } return ( + + + + + + + Locally in your browser + + + Your data will be stored temporarily and it be removed once you close your browser + + + + + +
+ + Entaxy is free and you get to keep your data. + + + That's right, we + don't store your data in a big database so we + don't need to convince you to trust us. + +
+ + - - - -) + ) + } +} -Landing.propTypes = { - classes: PropTypes.object.isRequired +LandingComponent.propTypes = { + history: PropTypes.object.isRequired, + classes: PropTypes.object.isRequired, + handleLogin: PropTypes.func.isRequired } -export default withStyles(styles)(Landing) +export default compose( + connect(null, mapDispatchToProps), + withStyles(styles) +)(LandingComponent) diff --git a/src/core/Transactions/index.jsx b/src/core/Transactions/index.jsx index 550e544..fe5a00b 100644 --- a/src/core/Transactions/index.jsx +++ b/src/core/Transactions/index.jsx @@ -14,6 +14,7 @@ import AddIcon from '@material-ui/icons/Add' import { Column, Table, AutoSizer } from 'react-virtualized' import 'react-virtualized/styles.css' import Header from '../../common/Header' +import LeftDrawer from '../../common/LeftDrawer' import TransactionDialog from './TransactionDialog' import TableToolbar from '../../common/TableToolbar' import confirm from '../../util/confirm' @@ -22,15 +23,15 @@ import { sortedTransactions } from '../../store/transactions/selectors' const styles = theme => ({ root: { - height: '100vh' + height: '100vh', + paddingTop: 70 }, paper: { display: 'flex', flexDirection: 'column', position: 'relative', height: '100%', - margin: '10px', - padding: '0 10px' + margin: '0px 0px 0px 200px' }, tableWrapper: { flex: '1 1 auto' @@ -150,12 +151,13 @@ export class TransactionsComponent extends React.Component { return (
+ - + ( -
- - - - - - - - - - - -
-) -export default Routes +const mapStateToProps = ({ user }) => { + return { user } +} + +export class RoutesComponent extends React.Component { + loginRequired = Screen => (props) => { + console.log(this.props.user, Screen) + if (this.props.user.isAuthenticatedWith === null) { + return + } + return + } + + render() { + return ( +
+ + + + + + + + + + + +
+ ) + } +} + +RoutesComponent.propTypes = { + user: PropTypes.object.isRequired +} + +export default connect(mapStateToProps)(RoutesComponent) diff --git a/src/store/accounts/__test__/actions.test.js b/src/store/accounts/__test__/actions.test.js new file mode 100644 index 0000000..0f4e592 --- /dev/null +++ b/src/store/accounts/__test__/actions.test.js @@ -0,0 +1,86 @@ +import configureMockStore from 'redux-mock-store' +import thunk from 'redux-thunk' +import * as actions from '../actions' +import types from '../types' +import { initialState as accountsInitialState } from '../reducer' +import { initialState as settingsInitialState } from '../../settings/reducer' + +jest.mock('uuid/v4', () => jest.fn(() => 1)) + +beforeEach(() => { + jest.resetModules() + jest.clearAllMocks() +}) + +const account = { + name: 'Checking', + institution: 'TD', + initialBalance: 1000 +} + +describe('accounts actions', () => { + describe('loadAccounts', () => { + it('should load accounts', () => { + expect(actions.loadAccounts([account])).toEqual({ + type: types.LOAD_ACCOUNTS, + payload: [account] + }) + }) + }) + + describe('createAccount', () => { + it('should create a account', () => { + const mockStore = configureMockStore([thunk]) + const store = mockStore({ + accounts: accountsInitialState + }) + + return store.dispatch(actions.createAccount(account)) + .then(() => { + expect(store.getActions()).toEqual([ + { + type: 'CREATE_ACCOUNT', + payload: { ...account, id: 1 } + } + ]) + }) + }) + }) + + describe('UpdateAccount', () => { + it('should update a account', () => { + const mockStore = configureMockStore([thunk]) + const store = mockStore({ + accounts: [{ ...account, id: 1 }] + }) + return store.dispatch(actions.updateAccount({ ...account, id: 1, institution: 'TD' })) + .then(() => { + expect(store.getActions()).toEqual([ + { + type: 'UPDATE_ACCOUNT', + payload: { ...account, id: 1, institution: 'TD' } + } + ]) + }) + }) + }) + + describe('DeleteAccount', () => { + it('should delete a account', () => { + const mockStore = configureMockStore([thunk]) + const store = mockStore({ + accounts: [{ ...account, id: 1 }], + settings: settingsInitialState + }) + return store.dispatch(actions.deleteAccount(1)) + .then(() => { + expect(store.getActions()).toEqual([ + { + type: 'DELETE_ACCOUNT', + payload: 1 + } + ]) + }) + }) + }) +}) diff --git a/src/store/accounts/__test__/reducer.test.js b/src/store/accounts/__test__/reducer.test.js new file mode 100644 index 0000000..89e916a --- /dev/null +++ b/src/store/accounts/__test__/reducer.test.js @@ -0,0 +1,62 @@ +import accountReducer, { initialState } from '../reducer' +import types from '../types' + +jest.mock('uuid/v4', () => jest.fn(() => 1)) + +beforeEach(() => { + jest.resetModules() + jest.clearAllMocks() +}) + +const account = { + name: 'Checking', + institution: 'TD', + initialBalance: 1000 +} + +describe('account reducer', () => { + it('should return initial state', () => { + expect(accountReducer(undefined, {})).toEqual(initialState) + }) + + it('should handle LOAD_ACCOUNTS', () => { + const type = types.LOAD_ACCOUNTS + const payload = [account] + expect(accountReducer(undefined, { type, payload })).toEqual(payload) + }) + + it('should handle LOAD_ACCOUNTS with no existing data', () => { + const type = types.LOAD_ACCOUNTS + expect(accountReducer(undefined, { type, payload: null })).toEqual(initialState) + }) + + it('should handle CREATE_ACCOUNT', () => { + const type = types.CREATE_ACCOUNT + const payload = account + + expect(accountReducer(undefined, { type, payload })).toEqual([account]) + }) + + it('should handle UPDATE_ACCOUNT', () => { + const type = types.UPDATE_ACCOUNT + const state = [account] + const payload = { + name: 'Savings', + institution: 'BMO', + initialBalance: 2000 + } + + expect(accountReducer(state, { type, payload })).toEqual([{ + name: 'Savings', + institution: 'BMO', + initialBalance: 2000 + }]) + }) + + it('should handle DELETE_ACCOUNT', () => { + const type = types.DELETE_ACCOUNT + const state = [{ ...account, id: 1 }] + + expect(accountReducer(state, { type, payload: state[0].id })).toEqual([]) + }) +}) diff --git a/src/store/accounts/actions.js b/src/store/accounts/actions.js new file mode 100644 index 0000000..f5d2e6c --- /dev/null +++ b/src/store/accounts/actions.js @@ -0,0 +1,33 @@ +import uuid from 'uuid/v4' +import types from './types' +import { saveState } from '../user/actions' + +export const loadAccounts = (accounts) => { + return { type: types.LOAD_ACCOUNTS, payload: accounts } +} + +export const afterAccountsChanged = async () => { + // await dispatch(updatePortfolioFilters()) + await saveState() +} + +export const createAccount = (account) => { + return (dispatch) => { + dispatch({ type: types.CREATE_ACCOUNT, payload: { ...account, id: uuid() } }) + return afterAccountsChanged(dispatch) + } +} + +export const updateAccount = (account) => { + return async (dispatch) => { + dispatch({ type: types.UPDATE_ACCOUNT, payload: account }) + return afterAccountsChanged(dispatch) + } +} + +export const deleteAccount = (accountId) => { + return (dispatch) => { + dispatch({ type: types.DELETE_ACCOUNT, payload: accountId }) + return afterAccountsChanged(dispatch) + } +} diff --git a/src/store/accounts/reducer.js b/src/store/accounts/reducer.js new file mode 100644 index 0000000..f8dc506 --- /dev/null +++ b/src/store/accounts/reducer.js @@ -0,0 +1,22 @@ +import _ from 'lodash' +import types from './types' + +export const initialState = [] + +export default (state = initialState, action) => { + let index = null + + switch (action.type) { + case types.LOAD_ACCOUNTS: + return action.payload || initialState + case types.CREATE_ACCOUNT: + return [...state, action.payload] + case types.UPDATE_ACCOUNT: + index = _.findIndex(state, account => account.id === action.payload.id) + return [...state.slice(0, index), action.payload, ...state.slice(index + 1)] + case types.DELETE_ACCOUNT: + return state.filter(account => action.payload !== account.id) + default: + return state + } +} diff --git a/src/store/accounts/types.js b/src/store/accounts/types.js new file mode 100644 index 0000000..1812d2b --- /dev/null +++ b/src/store/accounts/types.js @@ -0,0 +1,6 @@ +export default { + LOAD_ACCOUNTS: 'LOAD_ACCOUNTS', + CREATE_ACCOUNT: 'CREATE_ACCOUNT', + UPDATE_ACCOUNT: 'UPDATE_ACCOUNT', + DELETE_ACCOUNT: 'DELETE_ACCOUNT' +} diff --git a/src/store/transactions/__tests__/reducers.test.js b/src/store/transactions/__tests__/reducers.test.js index ee7938e..89cf78b 100644 --- a/src/store/transactions/__tests__/reducers.test.js +++ b/src/store/transactions/__tests__/reducers.test.js @@ -1,6 +1,16 @@ import transactionReducer, { initialState } from '../reducer' import types from '../types' +const transaction = { + institution: 'Questrade', + account: 'RRSP', + type: 'buy', + ticker: 'VCE.TO', + shares: '1', + bookValue: '1', + createdAt: new Date() +} + describe('transaction reducer', () => { it('should return initial state', () => { expect(transactionReducer(undefined, {})).toEqual(initialState) @@ -8,15 +18,7 @@ describe('transaction reducer', () => { it('should handle LOAD_TRANSACTIONS', () => { const type = types.LOAD_TRANSACTIONS - const payload = [{ - institution: 'Questrade', - account: 'RRSP', - type: 'buy', - ticker: 'VCE.TO', - shares: '1', - bookValue: '1', - createdAt: new Date() - }] + const payload = [transaction] expect(transactionReducer(undefined, { type, payload })).toEqual(payload) }) @@ -28,15 +30,7 @@ describe('transaction reducer', () => { it('should handle CREATE_TRANSACTION with no existing transactions', () => { const type = types.CREATE_TRANSACTION - const payload = { - institution: 'Questrade', - account: 'RRSP', - type: 'buy', - ticker: 'VCE.TO', - shares: '1', - bookValue: '1', - createdAt: new Date() - } + const payload = transaction expect(transactionReducer(undefined, { type, payload })).toEqual({ ...initialState, list: [payload] }) }) @@ -54,15 +48,7 @@ describe('transaction reducer', () => { createdAt: new Date() }] } - const payload = { - institution: 'Questrade', - account: 'RRSP', - type: 'buy', - ticker: 'VCE.TO', - shares: '1', - bookValue: '1', - createdAt: new Date() - } + const payload = transaction expect(transactionReducer(state, { type, payload })).toEqual({ ...state, list: [...state.list, payload] }) }) @@ -70,27 +56,9 @@ describe('transaction reducer', () => { const type = types.UPDATE_TRANSACTION const state = { ...initialState, - list: [{ - id: 1, - institution: 'TD', - account: 'RRSP', - type: 'buy', - ticker: 'VCE.TO', - shares: '2', - bookValue: '2', - createdAt: new Date() - }] - } - const payload = { - id: 1, - institution: 'Questrade', - account: 'RRSP', - type: 'buy', - ticker: 'VCE.TO', - shares: '1', - bookValue: '1', - createdAt: new Date() + list: [{ ...transaction, id: 1, shares: '2', bookValue: '2'}] } + const payload = { ...transaction, id: 1 } expect(transactionReducer(state, { type, payload })).toEqual({ ...state, list: [payload] }) }) @@ -98,16 +66,7 @@ describe('transaction reducer', () => { const type = types.DELETE_TRANSACTIONS const state = { ...initialState, - list: [{ - id: 1, - institution: 'TD', - account: 'RRSP', - type: 'buy', - ticker: 'VCE.TO', - shares: '2', - bookValue: '2', - createdAt: new Date() - }] + list: [transaction] } const payload = [state.list[0].id] expect(transactionReducer(state, { type, payload })).toEqual(initialState) diff --git a/src/store/transactions/actions.js b/src/store/transactions/actions.js index 244febe..c8974b2 100644 --- a/src/store/transactions/actions.js +++ b/src/store/transactions/actions.js @@ -4,10 +4,6 @@ import types from './types' import { saveState } from '../user/actions' import { updatePortfolioFilters } from '../settings/actions' import { updateMarketValues } from '../marketValues/actions' -// import RbcCsvParser from './CsvParsers/RbcCsvParser' -// import BmoCsvParser from './CsvParsers/BmoCsvParser' -// import TdCsvParser from './CsvParsers/TdCsvParser' -// import TangerineCsvParser from './CsvParsers/TangerineCsvParser' export const afterTransactionsChanged = async (dispatch) => { await dispatch(updateMarketValues()) diff --git a/src/store/user/__tests__/actions.test.js b/src/store/user/__tests__/actions.test.js index 463f3c1..2448e8e 100644 --- a/src/store/user/__tests__/actions.test.js +++ b/src/store/user/__tests__/actions.test.js @@ -34,7 +34,7 @@ describe('user actions', () => { expect(dispatch).toHaveBeenCalledWith({ type: types.LOAD_USER_DATA_SUCCESS, payload: { - isAuthenticated: true, + isAuthenticatedWith: 'blockstack', name: 'mocked name', pictureUrl: 'mocked url', username: 'mocked username' @@ -113,7 +113,7 @@ describe('user actions', () => { { type: 'LOAD_USER_DATA_SUCCESS', payload: { - isAuthenticated: true, + isAuthenticatedWith: 'blockstack', username: 'mocked username', name: 'mocked name', pictureUrl: 'mocked url' diff --git a/src/store/user/__tests__/reducer.test.js b/src/store/user/__tests__/reducer.test.js index 34342b7..1b52527 100644 --- a/src/store/user/__tests__/reducer.test.js +++ b/src/store/user/__tests__/reducer.test.js @@ -9,7 +9,7 @@ describe('user reducer', () => { it('should handle LOAD_USER_DATA_SUCCESS with authenticated user', () => { const type = types.LOAD_USER_DATA_SUCCESS const payload = { - isAuthenticated: true, + isAuthenticatedWith: 'blockstack', username: 'test-username', name: 'Test Name', pictureUrl: 'Test URL' @@ -31,13 +31,13 @@ describe('user reducer', () => { it('should handle USER_LOGIN_SUCCESS', () => { const type = types.USER_LOGIN_SUCCESS - const payload = { isAuthenticated: true } + const payload = { isAuthenticatedWith: 'blockstack' } expect(userReducer(undefined, { type, payload })).toEqual({ ...initialState, ...payload }) }) it('should handle USER_LOGOUT', () => { const type = types.USER_LOGOUT - const payload = { isAuthenticated: true } + const payload = { isAuthenticatedWith: 'blockstack' } expect(userReducer(undefined, { type, payload })).toEqual({ ...initialState }) }) diff --git a/src/store/user/actions.js b/src/store/user/actions.js index 6c9c94c..3802b4f 100644 --- a/src/store/user/actions.js +++ b/src/store/user/actions.js @@ -34,7 +34,7 @@ export const loadUserData = () => { const person = new blockstack.Person(profile) dispatch(loadUserDataSuccess({ - isAuthenticated: true, + isAuthenticatedWith: 'blockstack', username, name: person.name(), pictureUrl: person.avatarUrl() @@ -53,10 +53,12 @@ export const loadUserData = () => { } } -export const userLogin = () => { - // Open the blockstack browser for sign in - blockstack.redirectToSignIn(`${window.location.origin}/handle-login`) - return { type: types.USER_LOGIN } +export const userLogin = (loginType) => { + if (loginType === 'blockstack') { + // Open the blockstack browser for sign in + blockstack.redirectToSignIn(`${window.location.origin}/handle-login`) + } + return { type: types.USER_LOGIN, payload: loginType } } export const userLoginError = (error) => { diff --git a/src/store/user/reducer.js b/src/store/user/reducer.js index 1742e67..cfd1b86 100644 --- a/src/store/user/reducer.js +++ b/src/store/user/reducer.js @@ -2,7 +2,7 @@ import types from './types' export const initialState = { isLoading: false, - isAuthenticated: false, + isAuthenticatedWith: null, isLoginPending: false, username: null, name: null, @@ -17,9 +17,18 @@ export default (state = initialState, action) => { case types.LOAD_USER_DATA_SUCCESS: return { ...state, ...action.payload } case types.USER_LOGIN: - return { ...state, isLoginPending: true } + return { + guest: { + ...state, + isAuthenticatedWith: 'guest', + username: null, + name: 'Guest user', + pictureUrl: null + }, + blockstack: { ...state, isLoginPending: true } + }[action.payload] case types.USER_LOGIN_SUCCESS: - return { ...state, isLoginPending: false, isAuthenticated: true } + return { ...state, isLoginPending: false, isAuthenticatedWith: 'blockstack' } case types.USER_LOGOUT: return initialState case types.USER_LOGIN_ERROR: