Browse Source

Merge pull request #32 from loeck/master

Add IsUnlocked, and simple application reducer
master
Meriadec Pillet 7 years ago
committed by GitHub
parent
commit
8ee3a0c5dd
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 7
      .eslintrc
  2. 1
      .flowconfig
  3. 2
      package.json
  4. 4
      src/actions/accounts.js
  5. 2
      src/actions/devices.js
  6. 17
      src/actions/settings.js
  7. 29
      src/components/AccountPage.js
  8. 100
      src/components/IsUnlocked/index.js
  9. 115
      src/components/SettingsPage.js
  10. 6
      src/components/SideBar/index.js
  11. 42
      src/components/Wrapper.js
  12. 25
      src/helpers/db.js
  13. 11
      src/middlewares/db.js
  14. 25
      src/reducers/accounts.js
  15. 32
      src/reducers/application.js
  16. 8
      src/reducers/index.js
  17. 16
      src/reducers/settings.js
  18. 10
      src/renderer/index.js
  19. 1
      src/styles/helpers.js
  20. 5
      src/types/common.js
  21. 8
      yarn.lock

7
.eslintrc

@ -10,20 +10,21 @@
},
"rules": {
"camelcase": 0,
"import/no-extraneous-dependencies": 0,
"import/prefer-default-export": 0,
"jsx-a11y/anchor-is-valid": 0,
"new-cap": 0,
"no-nested-ternary": 0,
"no-param-reassign": 0,
"no-plusplus": 0,
"no-return-assign": 0,
"no-shadow": 0,
"no-underscore-dangle": 0,
"no-void": 0,
"no-plusplus": 0,
"import/no-extraneous-dependencies": 0,
"react/forbid-prop-types": 0,
"react/jsx-curly-brace-presence": 0,
"react/jsx-filename-extension": 0,
"react/prefer-stateless-function": 0,
"react/forbid-prop-types": 0,
},
"settings": {
"import/resolver": {

1
.flowconfig

@ -1,4 +1,5 @@
[ignore]
<PROJECT_ROOT>/node_modules/bcryptjs/src/bower.json
[include]

2
package.json

@ -36,6 +36,7 @@
"@ledgerhq/hw-app-eth": "^1.1.2-beta.068e2a14",
"@ledgerhq/hw-transport": "^1.1.2-beta.068e2a14",
"@ledgerhq/hw-transport-node-hid": "^1.1.2-beta.068e2a14",
"bcryptjs": "^2.4.3",
"color": "^2.0.1",
"downshift": "^1.25.0",
"electron-store": "^1.3.0",
@ -57,6 +58,7 @@
"redux": "^3.7.2",
"redux-actions": "^2.2.1",
"redux-thunk": "^2.2.0",
"shortid": "^2.2.8",
"source-map-support": "^0.5.0",
"styled-components": "^2.2.4",
"styled-system": "^1.1.1"

4
src/actions/accounts.js

@ -1,7 +1,5 @@
// @flow
/* eslint-disable import/prefer-default-export */
import db from 'helpers/db'
import type { Account } from 'types/common'
@ -15,5 +13,5 @@ export const addAccount: AddAccount = payload => ({
type FetchAccounts = () => { type: string }
export const fetchAccounts: FetchAccounts = () => ({
type: 'FETCH_ACCOUNTS',
payload: db.get('accounts', []),
payload: db('accounts'),
})

2
src/actions/devices.js

@ -1,7 +1,5 @@
// @flow
/* eslint-disable import/prefer-default-export */
import type { Device, Devices } from 'types/common'
export type SetCurrentDevice = (Device | null) => { type: string, payload: Device | null }

17
src/actions/settings.js

@ -0,0 +1,17 @@
// @flow
import db from 'helpers/db'
import type { Settings } from 'types/common'
export type SaveSettings = Settings => { type: string, payload: Settings }
export const saveSettings: SaveSettings = payload => ({
type: 'DB:SAVE_SETTINGS',
payload,
})
type FetchSettings = () => { type: string }
export const fetchSettings: FetchSettings = () => ({
type: 'FETCH_SETTINGS',
payload: db('settings'),
})

29
src/components/AccountPage.js

@ -1,13 +1,36 @@
// @flow
import React, { PureComponent } from 'react'
import { connect } from 'react-redux'
import type { MapStateToProps } from 'react-redux'
import type { Account } from 'types/common'
import { getAccountById } from 'reducers/accounts'
import Box from 'components/base/Box'
class AccountPage extends PureComponent<{}> {
type Props = {
account: Account,
}
const mapStateToProps: MapStateToProps<*, *, *> = (state, props) => ({
account: getAccountById(state, props.match.params.id),
})
class AccountPage extends PureComponent<Props> {
render() {
return <Box>{'account page'}</Box>
const { account } = this.props
return (
<Box>
<Box>{'account page'}</Box>
<Box>
{account.name} - {account.address}
</Box>
</Box>
)
}
}
export default AccountPage
export default connect(mapStateToProps)(AccountPage)

100
src/components/IsUnlocked/index.js

@ -0,0 +1,100 @@
// @flow
import React, { PureComponent } from 'react'
import { connect } from 'react-redux'
import bcrypt from 'bcryptjs'
import type { MapStateToProps } from 'react-redux'
import type { Settings } from 'types/common'
import get from 'lodash/get'
import { setEncryptionKey } from 'helpers/db'
import { fetchAccounts } from 'actions/accounts'
import { isLocked, unlock } from 'reducers/application'
import Box from 'components/base/Box'
import Input from 'components/base/Input'
type InputValue = {
password: string,
}
type Props = {
fetchAccounts: Function,
isLocked: boolean,
render: Function,
settings: Settings,
unlock: Function,
}
type State = {
inputValue: InputValue,
}
const mapStateToProps: MapStateToProps<*, *, *> = state => ({
settings: state.settings,
isLocked: isLocked(state),
})
const mapDispatchToProps = {
fetchAccounts,
unlock,
}
class IsUnlocked extends PureComponent<Props, State> {
state = {
inputValue: {
password: '',
},
}
handleChangeInput = (key: $Keys<InputValue>) => (value: $Values<InputValue>) =>
this.setState(prev => ({
inputValue: {
...prev.inputValue,
[key]: value,
},
}))
handleSubmit = (e: SyntheticEvent<HTMLFormElement>) => {
e.preventDefault()
const { settings, unlock, fetchAccounts } = this.props
const { inputValue } = this.state
if (bcrypt.compareSync(inputValue.password, get(settings, 'password.value'))) {
setEncryptionKey('accounts', inputValue.password)
fetchAccounts()
unlock()
}
}
render() {
const { inputValue } = this.state
const { isLocked, render } = this.props
if (isLocked) {
return (
<Box sticky align="center" justify="center">
<form onSubmit={this.handleSubmit}>
<Box>
<Input
placeholder="Password"
type="password"
onChange={this.handleChangeInput('password')}
value={inputValue.password}
/>
</Box>
</form>
</Box>
)
}
return render()
}
}
export default connect(mapStateToProps, mapDispatchToProps, null, {
pure: false,
})(IsUnlocked)

115
src/components/SettingsPage.js

@ -1,15 +1,122 @@
// @flow
import React, { PureComponent } from 'react'
import styled from 'styled-components'
import { connect } from 'react-redux'
import bcrypt from 'bcryptjs'
import get from 'lodash/get'
import set from 'lodash/set'
import { setEncryptionKey } from 'helpers/db'
import type { MapStateToProps } from 'react-redux'
import type { Settings } from 'types/common'
import { saveSettings } from 'actions/settings'
import Box from 'components/base/Box'
import Input from 'components/base/Input'
import Button from 'components/base/Button'
const Label = styled.label`
display: block;
text-transform: uppercase;
`
type InputValue = Settings
type Props = {}
type Props = {
settings: Settings,
saveSettings: Function,
}
type State = {
inputValue: InputValue,
}
const mapStateToProps: MapStateToProps<*, *, *> = state => ({
settings: state.settings,
})
const mapDispatchToProps = {
saveSettings,
}
class SettingsPage extends PureComponent<Props, State> {
state = {
inputValue: {
...this.props.settings,
password: {
...this.props.settings.password,
value: undefined,
},
},
}
handleChangeInput = (key: $Keys<InputValue>) => (value: $Values<InputValue>) =>
this.setState(prev => ({
inputValue: {
...set(prev.inputValue, key, value),
},
}))
handleSubmit = (e: SyntheticEvent<HTMLFormElement>) => {
e.preventDefault()
const { saveSettings } = this.props
const { inputValue } = this.state
const settings = {
...inputValue,
password: {
...inputValue.password,
value: '',
},
}
const password = get(inputValue, 'password', {})
if (password.state === true && password.value.trim() !== '') {
settings.password.value = bcrypt.hashSync(password.value, 8)
setEncryptionKey('accounts', password.value)
}
saveSettings(settings)
}
class SettingsPage extends PureComponent<Props> {
render() {
return <Box>{'settings'}</Box>
const { inputValue } = this.state
return (
<form onSubmit={this.handleSubmit}>
<Box p={3}>
<Box>{'settings'}</Box>
<Box horizontal>
<input
type="checkbox"
checked={get(inputValue, 'password.state')}
onChange={e => this.handleChangeInput('password.state')(e.target.checked)}
/>{' '}
with password
</Box>
{get(inputValue, 'password.state') === true && (
<Box>
<Label>Password</Label>
<Input
value={get(inputValue, 'password.value', 'My secure password')}
onChange={this.handleChangeInput('password.value')}
type="password"
/>
</Box>
)}
<Box>
<Button type="submit">Save</Button>
</Box>
</Box>
</form>
)
}
}
export default SettingsPage
export default connect(mapStateToProps, mapDispatchToProps)(SettingsPage)

6
src/components/SideBar/index.js

@ -76,11 +76,11 @@ class SideBar extends PureComponent<Props> {
<Box flow={2}>
<CapsSubtitle>{'Accounts'}</CapsSubtitle>
<div>
{accounts.map((account, i) => (
{Object.entries(accounts).map(([id, account]: [string, any]) => (
<Item
linkTo="/account/brian"
linkTo={`/account/${id}`}
desc={`${account.type.toUpperCase()} 3.78605936`}
key={i} // eslint-disable-line react/no-array-index-key
key={id}
>
{account.name}
</Item>

42
src/components/Wrapper.js

@ -5,8 +5,8 @@ import { ipcRenderer } from 'electron'
import { Route } from 'react-router'
import { translate } from 'react-i18next'
import Box from 'components/base/Box'
import * as modals from 'components/modals'
import Box from 'components/base/Box'
import AccountPage from 'components/AccountPage'
import DashboardPage from 'components/DashboardPage'
@ -14,6 +14,7 @@ import SettingsPage from 'components/SettingsPage'
import UpdateNotifier from 'components/UpdateNotifier'
import AppRegionDrag from 'components/AppRegionDrag'
import IsUnlocked from 'components/IsUnlocked'
import SideBar from 'components/SideBar'
import TopBar from 'components/TopBar'
@ -26,22 +27,29 @@ class Wrapper extends PureComponent<{}> {
return (
<Fragment>
<AppRegionDrag />
<UpdateNotifier />
{Object.entries(modals).map(([name, ModalComponent]: [string, any]) => (
<ModalComponent key={name} />
))}
<Box grow horizontal>
<SideBar />
<Box shrink grow bg="cream">
<TopBar />
<Route path="/" exact component={DashboardPage} />
<Route path="/settings" component={SettingsPage} />
<Route path="/account/:account" component={AccountPage} />
</Box>
</Box>
<IsUnlocked
render={() => (
<Fragment>
<UpdateNotifier />
{Object.entries(modals).map(([name, ModalComponent]: [string, any]) => (
<ModalComponent key={name} />
))}
<Box grow horizontal>
<SideBar />
<Box shrink grow bg="cream">
<TopBar />
<Route path="/" exact component={DashboardPage} />
<Route path="/settings" component={SettingsPage} />
<Route path="/account/:id" component={AccountPage} />
</Box>
</Box>
</Fragment>
)}
/>
</Fragment>
)
}

25
src/helpers/db.js

@ -1,7 +1,24 @@
import Store from 'electron-store'
const store = new Store({
// encryptionKey: 'toto',
})
const encryptionKey = {}
export default store
const store = type =>
new Store({
name: type,
defaults: {},
encryptionKey: encryptionKey[type],
})
export function setEncryptionKey(type, value) {
encryptionKey[type] = value
}
export default (type, values) => {
const db = store(type)
if (values) {
db.store = values
}
return db.store
}

11
src/middlewares/db.js

@ -1,6 +1,9 @@
/* eslint-disable consistent-return */
import db from 'helpers/db'
// eslint-disable-next-line consistent-return
import { getAccounts } from 'reducers/accounts'
export default store => next => action => {
if (!action.type.startsWith('DB:')) {
return next(action)
@ -11,7 +14,9 @@ export default store => next => action => {
dispatch({ type, payload: action.payload })
const { accounts } = getState()
const state = getState()
const { settings } = state
db.set('accounts', accounts.accounts)
db('settings', settings)
db('accounts', getAccounts(state))
}

25
src/reducers/accounts.js

@ -1,6 +1,7 @@
// @flow
import { handleActions } from 'redux-actions'
import shortid from 'shortid'
import type { Account, Accounts } from 'types/common'
@ -9,14 +10,24 @@ export type AccountsState = {
}
const state: AccountsState = {
accounts: [],
accounts: {},
}
const handlers: Object = {
ADD_ACCOUNT: (state: AccountsState, { payload: account }: { payload: Account }) => ({
...state,
accounts: [...state.accounts, account],
}),
ADD_ACCOUNT: (state: AccountsState, { payload: account }: { payload: Account }) => {
const id = shortid.generate()
return {
...state,
accounts: {
...state.accounts,
[id]: {
id,
...account,
},
},
}
},
FETCH_ACCOUNTS: (state: AccountsState, { payload: accounts }: { payload: Accounts }) => ({
...state,
accounts,
@ -27,4 +38,8 @@ export function getAccounts(state: { accounts: AccountsState }) {
return state.accounts.accounts
}
export function getAccountById(state: { accounts: AccountsState }, id: string) {
return getAccounts(state)[id]
}
export default handleActions(handlers, state)

32
src/reducers/application.js

@ -0,0 +1,32 @@
// @flow
import { handleActions, createAction } from 'redux-actions'
import get from 'lodash/get'
export type ApplicationState = {}
const state: ApplicationState = {}
const handlers = {
APPLICATION_SET_DATA: (state, { payload }: { payload: ApplicationState }) => ({
...state,
...payload,
}),
}
// Actions
export const unlock = createAction('APPLICATION_SET_DATA', () => ({ isLocked: false }))
export const lock = createAction('APPLICATION_SET_DATA', () => ({ isLocked: true }))
// Selectors
export const isLocked = (state: Object) =>
state.application.isLocked === undefined
? get(state.settings, 'password.state', false)
: state.application.isLocked
// Exporting reducer
export default handleActions(handlers, state)

8
src/reducers/index.js

@ -5,28 +5,36 @@ import { routerReducer as router } from 'react-router-redux'
import type { LocationShape } from 'react-router'
import application from './application'
import accounts from './accounts'
import devices from './devices'
import modals from './modals'
import settings from './settings'
import update from './update'
import type { ApplicationState } from './application'
import type { AccountsState } from './accounts'
import type { DevicesState } from './devices'
import type { ModalsState } from './modals'
import type { SettingsState } from './settings'
import type { UpdateState } from './update'
export type State = {
application: ApplicationState,
accounts: AccountsState,
devices: DevicesState,
modals: ModalsState,
router: LocationShape,
settings: SettingsState,
update: UpdateState,
}
export default combineReducers({
application,
accounts,
devices,
modals,
router,
settings,
update,
})

16
src/reducers/settings.js

@ -0,0 +1,16 @@
// @flow
import { handleActions } from 'redux-actions'
import type { Settings } from 'types/common'
export type SettingsState = Object
const state: SettingsState = {}
const handlers: Object = {
SAVE_SETTINGS: (state: SettingsState, { payload: settings }: { payload: Settings }) => settings,
FETCH_SETTINGS: (state: SettingsState, { payload: settings }: { payload: Settings }) => settings,
}
export default handleActions(handlers, state)

10
src/renderer/index.js

@ -9,6 +9,8 @@ import createStore from 'renderer/createStore'
import events from 'renderer/events'
import { fetchAccounts } from 'actions/accounts'
import { fetchSettings } from 'actions/settings'
import { isLocked } from 'reducers/application'
import App from 'components/App'
@ -20,7 +22,13 @@ const rootNode = document.getElementById('app')
events(store)
store.dispatch(fetchAccounts())
store.dispatch(fetchSettings())
const state = store.getState() || {}
if (!isLocked(state)) {
store.dispatch(fetchAccounts())
}
function r(Comp) {
if (rootNode) {

1
src/styles/helpers.js

@ -2,7 +2,6 @@
import Color from 'color'
// eslint-disable-next-line import/prefer-default-export
export const rgba = (c: string, a: number) =>
Color(c)
.alpha(a)

5
src/types/common.js

@ -9,11 +9,14 @@ export type Device = {
export type Devices = Array<Device>
export type Account = {
id: string,
name: string,
type: string,
address: string,
}
export type Accounts = Array<Account>
export type Accounts = Object
export type Settings = Object
export type T = (string, ?Object) => string

8
yarn.lock

@ -1675,6 +1675,10 @@ bcrypt-pbkdf@^1.0.0:
dependencies:
tweetnacl "^0.14.3"
bcryptjs@^2.4.3:
version "2.4.3"
resolved "https://registry.yarnpkg.com/bcryptjs/-/bcryptjs-2.4.3.tgz#9ab5627b93e60621ff7cdac5da9733027df1d0cb"
big.js@^3.1.3:
version "3.2.0"
resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e"
@ -7934,6 +7938,10 @@ shelljs@^0.7.8:
interpret "^1.0.0"
rechoir "^0.6.2"
shortid@^2.2.8:
version "2.2.8"
resolved "https://registry.yarnpkg.com/shortid/-/shortid-2.2.8.tgz#033b117d6a2e975804f6f0969dbe7d3d0b355131"
signal-exit@^3.0.0, signal-exit@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"

Loading…
Cancel
Save