Browse Source

Merge pull request #777 from gre/settings-refactoring

Settings refactoring
master
Meriadec Pillet 7 years ago
committed by GitHub
parent
commit
48a3bc1b63
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 13
      src/actions/settings.js
  2. 4
      src/components/RenderError.js
  3. 25
      src/components/SettingsPage/AboutRowItem.js
  4. 72
      src/components/SettingsPage/CleanButton.js
  5. 53
      src/components/SettingsPage/CounterValueExchangeSelect.js
  6. 58
      src/components/SettingsPage/CounterValueSelect.js
  7. 39
      src/components/SettingsPage/DevModeButton.js
  8. 133
      src/components/SettingsPage/DisablePasswordButton.js
  9. 61
      src/components/SettingsPage/LanguageSelect.js
  10. 48
      src/components/SettingsPage/MarketIndicatorRadio.js
  11. 67
      src/components/SettingsPage/RegionSelect.js
  12. 45
      src/components/SettingsPage/ReleaseNotesButton.js
  13. 82
      src/components/SettingsPage/ResetButton.js
  14. 39
      src/components/SettingsPage/SentryLogsButton.js
  15. 38
      src/components/SettingsPage/ShareAnalyticsButton.js
  16. 60
      src/components/SettingsPage/index.js
  17. 115
      src/components/SettingsPage/sections/About.js
  18. 96
      src/components/SettingsPage/sections/Currencies.js
  19. 114
      src/components/SettingsPage/sections/CurrencyRows.js
  20. 259
      src/components/SettingsPage/sections/Display.js
  21. 284
      src/components/SettingsPage/sections/Profile.js
  22. 5
      src/components/SettingsPage/sections/Tools.js
  23. 4
      src/config/constants.js
  24. 4
      src/middlewares/sentry.js
  25. 12
      src/reducers/settings.js
  26. 4
      src/renderer/init.js

13
src/actions/settings.js

@ -4,12 +4,23 @@ import type { Dispatch } from 'redux'
import type { SettingsState as Settings } from 'reducers/settings'
import type { Currency } from '@ledgerhq/live-common/lib/types'
export type SaveSettings = Settings => { type: string, payload: $Shape<Settings> }
export type SaveSettings = ($Shape<Settings>) => { type: string, payload: $Shape<Settings> }
export const saveSettings: SaveSettings = payload => ({
type: 'DB:SAVE_SETTINGS',
payload,
})
export const setDeveloperMode = (developerMode: boolean) => saveSettings({ developerMode })
export const setSentryLogs = (sentryLogs: boolean) => saveSettings({ sentryLogs })
export const setShareAnalytics = (shareAnalytics: boolean) => saveSettings({ shareAnalytics })
export const setMarketIndicator = (marketIndicator: *) => saveSettings({ marketIndicator })
export const setCounterValue = (counterValue: string) => saveSettings({ counterValue })
export const setLanguage = (language: ?string) => saveSettings({ language })
export const setRegion = (region: ?string) => saveSettings({ region })
export const setCounterValueExchange = (counterValueExchange: ?string) =>
saveSettings({ counterValueExchange })
type FetchSettings = (*) => (Dispatch<*>) => void
export const fetchSettings: FetchSettings = (settings: *) => dispatch => {
dispatch({

4
src/components/RenderError.js

@ -17,7 +17,9 @@ import Space from 'components/base/Space'
import Button from 'components/base/Button'
import ConfirmModal from 'components/base/Modal/ConfirmModal'
import IconTriangleWarning from 'icons/TriangleWarning'
import { IconWrapperCircle } from './SettingsPage/sections/Profile'
// SERIOUSLY plz refactor to use <ResetButton>
import { IconWrapperCircle } from './SettingsPage/ResetButton'
type Props = {
error: Error,

25
src/components/SettingsPage/AboutRowItem.js

@ -0,0 +1,25 @@
// @flow
import React, { PureComponent } from 'react'
import { shell } from 'electron'
import IconExternalLink from 'icons/ExternalLink'
import { Tabbable } from 'components/base/Box'
import { SettingsSectionRow } from './SettingsSection'
export default class AboutRowItem extends PureComponent<{
url: string,
title: string,
desc: string,
}> {
onClick = () => shell.openExternal(this.props.url)
render() {
const { title, desc } = this.props
return (
<SettingsSectionRow title={title} desc={desc}>
<Tabbable p={2} borderRadius={1} onClick={this.onClick}>
<IconExternalLink style={{ cursor: 'pointer' }} size={16} />
</Tabbable>
</SettingsSectionRow>
)
}
}

72
src/components/SettingsPage/CleanButton.js

@ -0,0 +1,72 @@
// @flow
import React, { Fragment, PureComponent } from 'react'
import { connect } from 'react-redux'
import { translate } from 'react-i18next'
import type { T } from 'types/common'
import { remote } from 'electron'
import { cleanAccountsCache } from 'actions/accounts'
import db from 'helpers/db'
import { delay } from 'helpers/promise'
import Button from 'components/base/Button'
import { ConfirmModal } from 'components/base/Modal'
const mapDispatchToProps = {
cleanAccountsCache,
}
type Props = {
t: T,
cleanAccountsCache: () => *,
}
type State = {
opened: boolean,
}
class CleanButton extends PureComponent<Props, State> {
state = {
opened: false,
}
open = () => this.setState({ opened: true })
close = () => this.setState({ opened: false })
action = async () => {
this.props.cleanAccountsCache()
await delay(500)
db.cleanCache()
remote.getCurrentWindow().webContents.reload()
}
render() {
const { t } = this.props
const { opened } = this.state
return (
<Fragment>
<Button primary onClick={this.open} event="ClearCacheIntent">
{t('app:settings.profile.softReset')}
</Button>
<ConfirmModal
isDanger
isOpened={opened}
onClose={this.close}
onReject={this.close}
onConfirm={this.action}
title={t('app:settings.softResetModal.title')}
subTitle={t('app:settings.softResetModal.subTitle')}
desc={t('app:settings.softResetModal.desc')}
/>
</Fragment>
)
}
}
export default translate()(
connect(
null,
mapDispatchToProps,
)(CleanButton),
)

53
src/components/SettingsPage/CounterValueExchangeSelect.js

@ -0,0 +1,53 @@
// @flow
import React, { Fragment, PureComponent } from 'react'
import { connect } from 'react-redux'
import { createStructuredSelector } from 'reselect'
import {
counterValueCurrencySelector,
counterValueExchangeSelector,
intermediaryCurrency,
} from 'reducers/settings'
import { setCounterValueExchange } from 'actions/settings'
import type { Currency } from '@ledgerhq/live-common/lib/types'
import SelectExchange from 'components/SelectExchange'
type Props = {
counterValueCurrency: Currency,
counterValueExchange: ?string,
setCounterValueExchange: (?string) => void,
}
class CounterValueExchangeSelect extends PureComponent<Props> {
handleChangeExchange = (exchange: *) =>
this.props.setCounterValueExchange(exchange ? exchange.id : null)
render() {
const { counterValueCurrency, counterValueExchange } = this.props
return (
<Fragment>
{counterValueCurrency ? (
<SelectExchange
small
from={intermediaryCurrency}
to={counterValueCurrency}
exchangeId={counterValueExchange}
onChange={this.handleChangeExchange}
minWidth={200}
/>
) : null}
</Fragment>
)
}
}
export default connect(
createStructuredSelector({
counterValueCurrency: counterValueCurrencySelector,
counterValueExchange: counterValueExchangeSelector,
}),
{
setCounterValueExchange,
},
)(CounterValueExchangeSelect)

58
src/components/SettingsPage/CounterValueSelect.js

@ -0,0 +1,58 @@
// @flow
import React, { Fragment, PureComponent } from 'react'
import { connect } from 'react-redux'
import { createStructuredSelector } from 'reselect'
import { listFiatCurrencies } from '@ledgerhq/live-common/lib/helpers/currencies'
import type { Currency } from '@ledgerhq/live-common/lib/types'
import { setCounterValue } from 'actions/settings'
import { counterValueCurrencySelector } from 'reducers/settings'
import Select from 'components/base/Select'
const fiats = listFiatCurrencies()
.map(f => f.units[0])
// For now we take first unit, in the future we'll need to figure out something else
.map(fiat => ({
value: fiat.code,
label: `${fiat.name} - ${fiat.code}${fiat.symbol ? ` (${fiat.symbol})` : ''}`,
fiat,
}))
type Props = {
counterValueCurrency: Currency,
setCounterValue: string => void,
}
class CounterValueSelect extends PureComponent<Props> {
handleChangeCounterValue = (item: Object) => {
const { setCounterValue } = this.props
setCounterValue(item.fiat.code)
}
render() {
const { counterValueCurrency } = this.props
const cvOption = fiats.find(f => f.value === counterValueCurrency.ticker)
return (
<Fragment>
{/* TODO Track */}
<Select
small
minWidth={250}
onChange={this.handleChangeCounterValue}
itemToString={item => (item ? item.name : '')}
renderSelected={item => item && item.name}
options={fiats}
value={cvOption}
/>
</Fragment>
)
}
}
export default connect(
createStructuredSelector({
counterValueCurrency: counterValueCurrencySelector,
}),
{ setCounterValue },
)(CounterValueSelect)

39
src/components/SettingsPage/DevModeButton.js

@ -0,0 +1,39 @@
// @flow
import React, { Fragment, PureComponent } from 'react'
import { connect } from 'react-redux'
import { createStructuredSelector } from 'reselect'
import { setDeveloperMode } from 'actions/settings'
import { developerModeSelector } from 'reducers/settings'
import Track from 'analytics/Track'
import CheckBox from 'components/base/CheckBox'
const mapStateToProps = createStructuredSelector({
developerMode: developerModeSelector,
})
const mapDispatchToProps = {
setDeveloperMode,
}
type Props = {
developerMode: boolean,
setDeveloperMode: boolean => void,
}
class DevModeButton extends PureComponent<Props> {
render() {
const { developerMode, setDeveloperMode } = this.props
return (
<Fragment>
<Track onUpdate event={developerMode ? 'DevModeEnabled' : 'DevModeDisabled'} />
<CheckBox isChecked={developerMode} onChange={setDeveloperMode} />
</Fragment>
)
}
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(DevModeButton)

133
src/components/SettingsPage/DisablePasswordButton.js

@ -0,0 +1,133 @@
// @flow
import React, { Fragment, PureComponent } from 'react'
import { connect } from 'react-redux'
import { translate } from 'react-i18next'
import type { T } from 'types/common'
import { createStructuredSelector } from 'reselect'
import bcrypt from 'bcryptjs'
import { setEncryptionKey } from 'helpers/db'
import { cleanAccountsCache } from 'actions/accounts'
import { saveSettings } from 'actions/settings'
import { storeSelector } from 'reducers/settings'
import type { SettingsState } from 'reducers/settings'
import { unlock } from 'reducers/application' // FIXME should be in actions
import Track from 'analytics/Track'
import CheckBox from 'components/base/CheckBox'
import Box from 'components/base/Box'
import Button from 'components/base/Button'
import PasswordModal from './PasswordModal'
import DisablePasswordModal from './DisablePasswordModal'
const mapStateToProps = createStructuredSelector({
// FIXME in future we should use dedicated password selector and a savePassword action (you don't know the shape of settings)
settings: storeSelector,
})
const mapDispatchToProps = {
unlock,
cleanAccountsCache,
saveSettings,
}
type Props = {
t: T,
unlock: () => void,
settings: SettingsState,
saveSettings: Function,
}
type State = {
isPasswordModalOpened: boolean,
isDisablePasswordModalOpened: boolean,
}
class DisablePasswordButton extends PureComponent<Props, State> {
state = {
isPasswordModalOpened: false,
isDisablePasswordModalOpened: false,
}
setPassword = password => {
const { saveSettings, unlock } = this.props
window.requestIdleCallback(() => {
setEncryptionKey('accounts', password)
const hash = password ? bcrypt.hashSync(password, 8) : undefined
saveSettings({
password: {
isEnabled: hash !== undefined,
value: hash,
},
})
unlock()
})
}
handleOpenPasswordModal = () => this.setState({ isPasswordModalOpened: true })
handleClosePasswordModal = () => this.setState({ isPasswordModalOpened: false })
handleDisablePassowrd = () => this.setState({ isDisablePasswordModalOpened: true })
handleCloseDisablePasswordModal = () => this.setState({ isDisablePasswordModalOpened: false })
handleChangePasswordCheck = isChecked => {
if (isChecked) {
this.handleOpenPasswordModal()
} else {
this.handleDisablePassowrd()
}
}
handleChangePassword = (password: ?string) => {
if (password) {
this.setPassword(password)
this.handleClosePasswordModal()
} else {
this.setPassword(undefined)
this.handleCloseDisablePasswordModal()
}
}
render() {
const { t, settings } = this.props
const { isDisablePasswordModalOpened, isPasswordModalOpened } = this.state
const isPasswordEnabled = settings.password.isEnabled === true
return (
<Fragment>
<Track onUpdate event={isPasswordEnabled ? 'PasswordEnabled' : 'PasswordDisabled'} />
<Box horizontal flow={2} align="center">
{isPasswordEnabled && (
<Button onClick={this.handleOpenPasswordModal}>
{t('app:settings.profile.changePassword')}
</Button>
)}
<CheckBox isChecked={isPasswordEnabled} onChange={this.handleChangePasswordCheck} />
</Box>
<PasswordModal
t={t}
isOpened={isPasswordModalOpened}
onClose={this.handleClosePasswordModal}
onChangePassword={this.handleChangePassword}
isPasswordEnabled={isPasswordEnabled}
currentPasswordHash={settings.password.value}
/>
<DisablePasswordModal
t={t}
isOpened={isDisablePasswordModalOpened}
onClose={this.handleCloseDisablePasswordModal}
onChangePassword={this.handleChangePassword}
isPasswordEnabled={isPasswordEnabled}
currentPasswordHash={settings.password.value}
/>
</Fragment>
)
}
}
export default translate()(
connect(
mapStateToProps,
mapDispatchToProps,
)(DisablePasswordButton),
)

61
src/components/SettingsPage/LanguageSelect.js

@ -0,0 +1,61 @@
// @flow
import React, { Fragment, PureComponent } from 'react'
import moment from 'moment'
import { translate } from 'react-i18next'
import type { T } from 'types/common'
import { connect } from 'react-redux'
import { setLanguage } from 'actions/settings'
import { langAndRegionSelector } from 'reducers/settings'
import languageKeys from 'config/languages'
import Select from 'components/base/Select'
type Props = {
t: T,
useSystem: boolean,
language: string,
setLanguage: (?string) => void,
i18n: Object,
}
class LanguageSelect extends PureComponent<Props> {
handleChangeLanguage = ({ value: languageKey }: *) => {
const { i18n, setLanguage } = this.props
i18n.changeLanguage(languageKey)
moment.locale(languageKey)
setLanguage(languageKey)
}
languages = [{ value: null, label: this.props.t(`language:system`) }].concat(
languageKeys.map(key => ({ value: key, label: this.props.t(`language:${key}`) })),
)
render() {
const { language, useSystem } = this.props
const currentLanguage = useSystem
? this.languages[0]
: this.languages.find(l => l.value === language)
return (
<Fragment>
<Select
small
minWidth={250}
isSearchable={false}
onChange={this.handleChangeLanguage}
renderSelected={item => item && item.name}
value={currentLanguage}
options={this.languages}
/>
</Fragment>
)
}
}
export default translate()(
connect(
langAndRegionSelector,
{
setLanguage,
},
)(LanguageSelect),
)

48
src/components/SettingsPage/MarketIndicatorRadio.js

@ -0,0 +1,48 @@
// @flow
import React, { PureComponent } from 'react'
import { translate } from 'react-i18next'
import type { T } from 'types/common'
import { connect } from 'react-redux'
import { createStructuredSelector } from 'reselect'
import { setMarketIndicator } from 'actions/settings'
import { marketIndicatorSelector } from 'reducers/settings'
import RadioGroup from 'components/base/RadioGroup'
type Props = {
t: T,
setMarketIndicator: (*) => *,
marketIndicator: *,
}
class MarketIndicatorRadio extends PureComponent<Props> {
indicators = [
{
label: this.props.t('app:common.eastern'),
key: 'eastern',
},
{
label: this.props.t('app:common.western'),
key: 'western',
},
]
onChange = (item: Object) => {
const { setMarketIndicator } = this.props
setMarketIndicator(item.key)
}
render() {
const { marketIndicator } = this.props
return (
<RadioGroup items={this.indicators} activeKey={marketIndicator} onChange={this.onChange} />
)
}
}
export default translate()(
connect(
createStructuredSelector({ marketIndicator: marketIndicatorSelector }),
{ setMarketIndicator },
)(MarketIndicatorRadio),
)

67
src/components/SettingsPage/RegionSelect.js

@ -0,0 +1,67 @@
// @flow
import React, { Fragment, PureComponent } from 'react'
import { connect } from 'react-redux'
import { createSelector } from 'reselect'
import { setRegion } from 'actions/settings'
import { langAndRegionSelector, counterValueCurrencySelector } from 'reducers/settings'
import type { Currency } from '@ledgerhq/live-common/lib/types'
import type { T } from 'types/common'
import regionsByKey from 'helpers/regions.json'
import Select from 'components/base/Select'
const regions = Object.keys(regionsByKey).map(key => {
const [language, region] = key.split('-')
return { value: key, language, region, label: regionsByKey[key] }
})
type Props = {
t: T,
counterValueCurrency: Currency,
useSystem: boolean,
language: string,
region: ?string,
setRegion: (?string) => void,
}
class RegionSelect extends PureComponent<Props> {
handleChangeRegion = ({ region }: *) => {
const { setRegion } = this.props
setRegion(region)
}
render() {
const { language, region } = this.props
const regionsFiltered = regions.filter(item => language === item.language)
const currentRegion = regionsFiltered.find(item => item.region === region) || regionsFiltered[0]
return (
<Fragment>
<Select
small
minWidth={250}
onChange={this.handleChangeRegion}
renderSelected={item => item && item.name}
value={currentRegion}
options={regionsFiltered}
/>
</Fragment>
)
}
}
export default connect(
createSelector(
langAndRegionSelector,
counterValueCurrencySelector,
(langAndRegion, counterValueCurrency) => ({
...langAndRegion,
counterValueCurrency,
}),
),
{
setRegion,
},
)(RegionSelect)

45
src/components/SettingsPage/ReleaseNotesButton.js

@ -0,0 +1,45 @@
// @flow
import React, { PureComponent } from 'react'
import { translate } from 'react-i18next'
import type { T } from 'types/common'
import { shell } from 'electron'
import { connect } from 'react-redux'
import { openModal } from 'reducers/modals'
import { MODAL_RELEASES_NOTES } from 'config/constants'
import Button from 'components/base/Button'
type Props = {
t: T,
openModal: Function,
}
const mapDispatchToProps = {
openModal,
}
class ReleaseNotesButton extends PureComponent<Props> {
handleOpenLink = (url: string) => shell.openExternal(url)
render() {
const { t, openModal } = this.props
const version = __APP_VERSION__
return (
<Button
primary
onClick={() => {
openModal(MODAL_RELEASES_NOTES, version)
}}
>
{t('app:settings.about.releaseNotesBtn')}
</Button>
)
}
}
export default translate()(
connect(
null,
mapDispatchToProps,
)(ReleaseNotesButton),
)

82
src/components/SettingsPage/ResetButton.js

@ -0,0 +1,82 @@
// @flow
import React, { Fragment, PureComponent } from 'react'
import styled from 'styled-components'
import { remote } from 'electron'
import { translate } from 'react-i18next'
import type { T } from 'types/common'
import hardReset from 'helpers/hardReset'
import Box from 'components/base/Box'
import Button from 'components/base/Button'
import { ConfirmModal } from 'components/base/Modal'
import IconTriangleWarning from 'icons/TriangleWarning'
type Props = {
t: T,
}
type State = {
opened: boolean,
pending: boolean,
}
class ResetButton extends PureComponent<Props, State> {
state = {
opened: false,
pending: false,
}
open = () => this.setState({ opened: true })
close = () => this.setState({ opened: false })
action = async () => {
this.setState({ pending: true })
try {
await hardReset()
remote.getCurrentWindow().webContents.reloadIgnoringCache()
} catch (err) {
this.setState({ pending: false })
}
}
render() {
const { t } = this.props
const { opened, pending } = this.state
return (
<Fragment>
<Button danger onClick={this.open} event="HardResetIntent">
{t('app:settings.profile.hardReset')}
</Button>
<ConfirmModal
isDanger
isLoading={pending}
isOpened={opened}
onClose={this.close}
onReject={this.close}
onConfirm={this.action}
confirmText={t('app:common.reset')}
title={t('app:settings.hardResetModal.title')}
desc={t('app:settings.hardResetModal.desc')}
renderIcon={() => (
// FIXME why not pass in directly the DOM 🤷🏻
<IconWrapperCircle color="alertRed">
<IconTriangleWarning width={23} height={21} />
</IconWrapperCircle>
)}
/>
</Fragment>
)
}
}
export const IconWrapperCircle = styled(Box)`
width: 50px;
height: 50px;
border-radius: 50%;
background: #ea2e4919;
text-align: center;
justify-content: center;
`
export default translate()(ResetButton)

39
src/components/SettingsPage/SentryLogsButton.js

@ -0,0 +1,39 @@
// @flow
import React, { Fragment, PureComponent } from 'react'
import { connect } from 'react-redux'
import { createStructuredSelector } from 'reselect'
import { setSentryLogs } from 'actions/settings'
import { sentryLogsSelector } from 'reducers/settings'
import Track from 'analytics/Track'
import CheckBox from 'components/base/CheckBox'
const mapStateToProps = createStructuredSelector({
sentryLogs: sentryLogsSelector,
})
const mapDispatchToProps = {
setSentryLogs,
}
type Props = {
sentryLogs: boolean,
setSentryLogs: boolean => void,
}
class SentryLogsButton extends PureComponent<Props> {
render() {
const { sentryLogs, setSentryLogs } = this.props
return (
<Fragment>
<Track onUpdate event={sentryLogs ? 'SentryEnabled' : 'SentryDisabled'} />
<CheckBox isChecked={sentryLogs} onChange={setSentryLogs} />
</Fragment>
)
}
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(SentryLogsButton)

38
src/components/SettingsPage/ShareAnalyticsButton.js

@ -0,0 +1,38 @@
// @flow
import React, { Fragment, PureComponent } from 'react'
import { connect } from 'react-redux'
import { createStructuredSelector } from 'reselect'
import { setShareAnalytics } from 'actions/settings'
import { shareAnalyticsSelector } from 'reducers/settings'
import Track from 'analytics/Track'
import CheckBox from 'components/base/CheckBox'
const mapStateToProps = createStructuredSelector({
shareAnalytics: shareAnalyticsSelector,
})
const mapDispatchToProps = {
setShareAnalytics,
}
type Props = {
shareAnalytics: boolean,
setShareAnalytics: boolean => void,
}
class ShareAnalytics extends PureComponent<Props> {
render() {
const { shareAnalytics, setShareAnalytics } = this.props
return (
<Fragment>
<Track onUpdate event={shareAnalytics ? 'AnalyticsEnabled' : 'AnalyticsDisabled'} />
<CheckBox isChecked={shareAnalytics} onChange={setShareAnalytics} />
</Fragment>
)
}
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(ShareAnalytics)

60
src/components/SettingsPage/index.js

@ -4,42 +4,26 @@ import React, { PureComponent } from 'react'
import { compose } from 'redux'
import { connect } from 'react-redux'
import { translate } from 'react-i18next'
import type { T } from 'types/common'
import { Switch, Route } from 'react-router'
import type { SettingsState as Settings } from 'reducers/settings'
import type { RouterHistory, Match, Location } from 'react-router'
import type { T } from 'types/common'
import type { SaveSettings } from 'actions/settings'
import { saveSettings } from 'actions/settings'
import { EXPERIMENTAL_TOOLS_SETTINGS } from 'config/constants'
import { accountsSelector } from 'reducers/accounts'
import Pills from 'components/base/Pills'
import Box from 'components/base/Box'
import SectionDisplay from './sections/Display'
import SectionCurrencies from './sections/Currencies'
import SectionProfile from './sections/Profile'
import SectionAbout from './sections/About'
import SectionTools from './sections/Tools'
// FIXME this component should not be connected. each single tab should be.
// maybe even each single settings row should be connected!!
const mapStateToProps = state => ({
settings: state.settings,
accountsCount: accountsSelector(state).length,
})
const mapDispatchToProps = {
saveSettings,
}
type Props = {
history: RouterHistory,
i18n: Object,
location: Location,
match: Match,
saveSettings: SaveSettings,
settings: Settings,
accountsCount: number,
t: T,
}
@ -56,25 +40,28 @@ class SettingsPage extends PureComponent<Props, State> {
{
key: 'display',
label: props.t('app:settings.tabs.display'),
value: p => () => <SectionDisplay {...p} />,
value: SectionDisplay,
},
{
key: 'currencies',
label: props.t('app:settings.tabs.currencies'),
value: p => () => <SectionCurrencies {...p} />,
},
{
key: 'profile',
label: props.t('app:settings.tabs.profile'),
value: p => () => <SectionProfile {...p} />,
value: SectionCurrencies,
},
{
key: 'about',
label: props.t('app:settings.tabs.about'),
value: p => () => <SectionAbout {...p} />,
value: SectionAbout,
},
]
if (EXPERIMENTAL_TOOLS_SETTINGS) {
this._items.push({
key: 'tool',
label: 'Experimental Tools',
value: SectionTools,
})
}
this.state = {
tab: this.getCurrentTab({ url: props.match.url, pathname: props.location.pathname }),
}
@ -105,14 +92,8 @@ class SettingsPage extends PureComponent<Props, State> {
}
render() {
const { match, settings, t, i18n, saveSettings, accountsCount } = this.props
const { match, t, accountsCount } = this.props
const { tab } = this.state
const props = {
t,
settings,
saveSettings,
i18n,
}
const defaultItem = this._items[0]
const items = this._items.filter(item => item.key !== 'currencies' || accountsCount > 0)
@ -124,10 +105,8 @@ class SettingsPage extends PureComponent<Props, State> {
</Box>
<Pills mb={4} items={items} activeKey={tab.key} onChange={this.handleChangeTab} />
<Switch>
{items.map(i => (
<Route key={i.key} path={`${match.url}/${i.key}`} render={i.value && i.value(props)} />
))}
<Route render={defaultItem.value && defaultItem.value(props)} />
{items.map(i => <Route key={i.key} path={`${match.url}/${i.key}`} component={i.value} />)}
<Route component={defaultItem.value} />
</Switch>
</Box>
)
@ -135,9 +114,6 @@ class SettingsPage extends PureComponent<Props, State> {
}
export default compose(
connect(
mapStateToProps,
mapDispatchToProps,
),
connect(mapStateToProps),
translate(),
)(SettingsPage)

115
src/components/SettingsPage/sections/About.js

@ -1,20 +1,16 @@
// @flow
/* eslint-disable react/no-multi-comp */
import React, { PureComponent } from 'react'
import { shell } from 'electron'
import { connect } from 'react-redux'
import { translate } from 'react-i18next'
import type { T } from 'types/common'
import TrackPage from 'analytics/TrackPage'
import IconHelp from 'icons/Help'
import IconExternalLink from 'icons/ExternalLink'
import Button from 'components/base/Button'
import { Tabbable } from 'components/base/Box'
import { openModal } from 'reducers/modals'
import { MODAL_RELEASES_NOTES } from 'config/constants'
import TrackPage from 'analytics/TrackPage'
import ExportLogsBtn from 'components/ExportLogsBtn'
import CleanButton from '../CleanButton'
import ResetButton from '../ResetButton'
import ReleaseNotesButton from '../ReleaseNotesButton'
import AboutRowItem from '../AboutRowItem'
import {
SettingsSection as Section,
@ -25,90 +21,59 @@ import {
type Props = {
t: T,
openModal: Function,
}
const mapDispatchToProps = {
openModal,
}
const ITEMS = [
{
key: 'faq',
title: t => t('app:settings.about.faq'),
desc: t => t('app:settings.about.faqDesc'),
url: 'https://support.ledgerwallet.com/hc/en-us',
},
{
key: 'terms',
title: t => t('app:settings.about.terms'),
desc: t => t('app:settings.about.termsDesc'),
url: 'https://www.ledgerwallet.com/terms',
},
]
class SectionAbout extends PureComponent<Props> {
handleOpenLink = (url: string) => shell.openExternal(url)
render() {
const { t, openModal } = this.props
const { t } = this.props
const version = __APP_VERSION__
return (
<Section>
<TrackPage category="Settings" name="About" />
<Header
icon={<IconHelp size={16} />}
title={t('app:settings.tabs.about')}
desc={t('app:settings.about.desc')}
/>
<Body>
<Row title={t('app:settings.about.version')} desc={version}>
<Button
primary
onClick={() => {
openModal(MODAL_RELEASES_NOTES, version)
}}
>
{t('app:settings.about.releaseNotesBtn')}
</Button>
<ReleaseNotesButton />
</Row>
<Row
title={t('app:settings.profile.softResetTitle')}
desc={t('app:settings.profile.softResetDesc')}
>
<CleanButton />
</Row>
<Row
title={t('app:settings.profile.hardResetTitle')}
desc={t('app:settings.profile.hardResetDesc')}
>
<ResetButton />
</Row>
<Row title={t('app:settings.exportLogs.title')} desc={t('app:settings.exportLogs.desc')}>
<ExportLogsBtn />
</Row>
{ITEMS.map(item => (
<AboutRowItem
key={item.key}
title={item.title(t)}
desc={item.desc(t)}
url={item.url}
onClick={this.handleOpenLink}
/>
))}
<AboutRowItem
title={t('app:settings.about.faq')}
desc={t('app:settings.about.faqDesc')}
url="https://support.ledgerwallet.com/hc/en-us"
/>
<AboutRowItem
title={t('app:settings.about.terms')}
desc={t('app:settings.about.termsDesc')}
url="https://www.ledgerwallet.com/terms"
/>
</Body>
</Section>
)
}
}
class AboutRowItem extends PureComponent<{
onClick: string => void,
url: string,
title: string,
desc: string,
url: string,
}> {
render() {
const { onClick, title, desc, url } = this.props
const boundOnClick = () => onClick(url)
return (
<Row title={title} desc={desc}>
<Tabbable p={2} borderRadius={1} onClick={boundOnClick}>
<IconExternalLink style={{ cursor: 'pointer' }} size={16} />
</Tabbable>
</Row>
)
}
}
export default connect(
null,
mapDispatchToProps,
)(SectionAbout)
export default translate()(SectionAbout)

96
src/components/SettingsPage/sections/Currencies.js

@ -1,41 +1,25 @@
// @flow
// TODO refactoring:
// this component shouldn't accept the full settings object, actually it needs to be connected to nothing,
// it doesn't need saveSettings nor settings, instead, it just need to track selected Currency and delegate to 2 new components:
// - a new ConnectedSelectCurrency , that filters only the currency that comes from accounts (use the existing selector)
// - a new CurrencySettings component, that receives currency & will connect to the store to grab the relevant settings as well as everything it needs (counterValueCurrency), it also takes a saveCurrencySettings action (create if not existing)
import React, { PureComponent } from 'react'
import { translate } from 'react-i18next'
import { connect } from 'react-redux'
import { createStructuredSelector } from 'reselect'
import type { CryptoCurrency } from '@ledgerhq/live-common/lib/types'
import type { T } from 'types/common'
import { intermediaryCurrency, currencySettingsLocaleSelector } from 'reducers/settings'
import type { SettingsState } from 'reducers/settings'
import { currenciesSelector } from 'reducers/accounts'
import { currencySettingsDefaults } from 'helpers/SettingsDefaults'
import IconCurrencies from 'icons/Currencies'
import TrackPage from 'analytics/TrackPage'
import SelectCurrency from 'components/SelectCurrency'
import StepperNumber from 'components/base/StepperNumber'
import ExchangeSelect from 'components/SelectExchange'
import IconCurrencies from 'icons/Currencies'
import CurrencyRows from './CurrencyRows'
import {
SettingsSection as Section,
SettingsSectionHeader as Header,
SettingsSectionBody as Body,
SettingsSectionRow as Row,
} from '../SettingsSection'
type Props = {
currencies: CryptoCurrency[],
settings: SettingsState,
saveSettings: ($Shape<SettingsState>) => void,
t: T,
}
@ -54,45 +38,10 @@ class TabCurrencies extends PureComponent<Props, State> {
handleChangeCurrency = (currency: CryptoCurrency) => this.setState({ currency })
handleChangeConfirmationsToSpend = (nb: number) =>
this.updateCurrencySettings('confirmationsToSpend', nb)
handleChangeConfirmationsNb = (nb: number) => this.updateCurrencySettings('confirmationsNb', nb)
handleChangeExchange = exchange =>
this.updateCurrencySettings('exchange', exchange ? exchange.id : null)
updateCurrencySettings = (key: string, val: *) => {
// FIXME this really should be a dedicated action
const { settings, saveSettings } = this.props
const { currency } = this.state
const currencySettings = settings.currenciesSettings[currency.id]
let newCurrenciesSettings = []
if (!currencySettings) {
newCurrenciesSettings = {
...settings.currenciesSettings,
[currency.id]: {
[key]: val,
},
}
} else {
newCurrenciesSettings = {
...settings.currenciesSettings,
[currency.id]: {
...currencySettings,
[key]: val,
},
}
}
saveSettings({ currenciesSettings: newCurrenciesSettings })
}
render() {
const { currency } = this.state
if (!currency) return null // this case means there is no accounts
const { t, currencies, settings } = this.props
const { confirmationsNb, exchange } = currencySettingsLocaleSelector(settings, currency)
const defaults = currencySettingsDefaults(currency)
const { t, currencies } = this.props
return (
<Section key={currency.id}>
<TrackPage category="Settings" name="Currencies" />
@ -101,7 +50,6 @@ class TabCurrencies extends PureComponent<Props, State> {
title={t('app:settings.tabs.currencies')}
desc={t('app:settings.currencies.desc')}
renderRight={
// TODO this should only be the subset of currencies of the app
<SelectCurrency
small
minWidth={200}
@ -112,43 +60,11 @@ class TabCurrencies extends PureComponent<Props, State> {
}
/>
<Body>
{currency !== intermediaryCurrency ? (
<Row
title={t('app:settings.currencies.exchange', {
ticker: currency.ticker,
})}
desc={t('app:settings.currencies.exchangeDesc', {
currencyName: currency.name,
})}
>
<ExchangeSelect
small
from={currency}
to={intermediaryCurrency}
exchangeId={exchange}
onChange={this.handleChangeExchange}
minWidth={200}
/>
</Row>
) : null}
{defaults.confirmationsNb ? (
<Row
title={t('app:settings.currencies.confirmationsNb')}
desc={t('app:settings.currencies.confirmationsNbDesc')}
>
<StepperNumber
min={defaults.confirmationsNb.min}
max={defaults.confirmationsNb.max}
step={1}
onChange={this.handleChangeConfirmationsNb}
value={confirmationsNb}
/>
</Row>
) : null}
<CurrencyRows currency={currency} />
</Body>
</Section>
)
}
}
export default connect(mapStateToProps)(TabCurrencies)
export default translate()(connect(mapStateToProps)(TabCurrencies))

114
src/components/SettingsPage/sections/CurrencyRows.js

@ -0,0 +1,114 @@
// @flow
import React, { Fragment, PureComponent } from 'react'
import { translate } from 'react-i18next'
import { connect } from 'react-redux'
import { createStructuredSelector } from 'reselect'
import type { CryptoCurrency } from '@ledgerhq/live-common/lib/types'
import type { T } from 'types/common'
import { saveSettings } from 'actions/settings'
import { intermediaryCurrency, currencySettingsSelector, storeSelector } from 'reducers/settings'
import type { SettingsState, CurrencySettings } from 'reducers/settings'
import { currencySettingsDefaults } from 'helpers/SettingsDefaults'
import StepperNumber from 'components/base/StepperNumber'
import ExchangeSelect from 'components/SelectExchange'
import { SettingsSectionRow as Row } from '../SettingsSection'
type Props = {
t: T,
currency: CryptoCurrency,
currencySettings: CurrencySettings,
// FIXME: the stuff bellow to be to be gone!
settings: SettingsState,
saveSettings: ($Shape<SettingsState>) => void,
}
class CurrencyRows extends PureComponent<Props> {
handleChangeConfirmationsToSpend = (nb: number) =>
this.updateCurrencySettings('confirmationsToSpend', nb)
handleChangeConfirmationsNb = (nb: number) => this.updateCurrencySettings('confirmationsNb', nb)
handleChangeExchange = (exchange: *) =>
this.updateCurrencySettings('exchange', exchange ? exchange.id : null)
updateCurrencySettings = (key: string, val: *) => {
// FIXME this really should be a dedicated action
const { settings, saveSettings, currency } = this.props
const currencySettings = settings.currenciesSettings[currency.id]
let newCurrenciesSettings = []
if (!currencySettings) {
newCurrenciesSettings = {
...settings.currenciesSettings,
[currency.id]: {
[key]: val,
},
}
} else {
newCurrenciesSettings = {
...settings.currenciesSettings,
[currency.id]: {
...currencySettings,
[key]: val,
},
}
}
saveSettings({ currenciesSettings: newCurrenciesSettings })
}
render() {
const { currency, t, currencySettings } = this.props
const { confirmationsNb, exchange } = currencySettings
const defaults = currencySettingsDefaults(currency)
return (
<Fragment>
{currency !== intermediaryCurrency ? (
<Row
title={t('app:settings.currencies.exchange', {
ticker: currency.ticker,
})}
desc={t('app:settings.currencies.exchangeDesc', {
currencyName: currency.name,
})}
>
<ExchangeSelect
small
from={currency}
to={intermediaryCurrency}
exchangeId={exchange}
onChange={this.handleChangeExchange}
minWidth={200}
/>
</Row>
) : null}
{defaults.confirmationsNb ? (
<Row
title={t('app:settings.currencies.confirmationsNb')}
desc={t('app:settings.currencies.confirmationsNbDesc')}
>
<StepperNumber
min={defaults.confirmationsNb.min}
max={defaults.confirmationsNb.max}
step={1}
onChange={this.handleChangeConfirmationsNb}
value={confirmationsNb}
/>
</Row>
) : null}
</Fragment>
)
}
}
export default translate()(
connect(
createStructuredSelector({
currencySettings: currencySettingsSelector,
settings: storeSelector,
}),
{
saveSettings,
},
)(CurrencyRows),
)

259
src/components/SettingsPage/sections/Display.js

@ -1,26 +1,25 @@
// @flow
import React, { PureComponent } from 'react'
import moment from 'moment'
import { listFiatCurrencies } from '@ledgerhq/live-common/lib/helpers/currencies'
import {
intermediaryCurrency,
counterValueCurrencyLocalSelector,
counterValueExchangeLocalSelector,
} from 'reducers/settings'
import type { SettingsState as Settings } from 'reducers/settings'
import { translate } from 'react-i18next'
import { connect } from 'react-redux'
import { createSelector } from 'reselect'
import { langAndRegionSelector, counterValueCurrencySelector } from 'reducers/settings'
import type { Currency } from '@ledgerhq/live-common/lib/types'
import type { T } from 'types/common'
import { EXPERIMENTAL_MARKET_INDICATOR_SETTINGS } from 'config/constants'
import TrackPage from 'analytics/TrackPage'
import SelectExchange from 'components/SelectExchange'
import Select from 'components/base/Select'
import RadioGroup from 'components/base/RadioGroup'
import IconDisplay from 'icons/Display'
import languageKeys from 'config/languages'
import regionsByKey from 'helpers/regions.json'
import TrackPage from 'analytics/TrackPage'
import MarketIndicatorRadio from '../MarketIndicatorRadio'
import LanguageSelect from '../LanguageSelect'
import CounterValueSelect from '../CounterValueSelect'
import CounterValueExchangeSelect from '../CounterValueExchangeSelect'
import RegionSelect from '../RegionSelect'
import DisablePasswordButton from '../DisablePasswordButton'
import DevModeButton from '../DevModeButton'
import SentryLogsButton from '../SentryLogsButton'
import ShareAnalyticsButton from '../ShareAnalyticsButton'
import {
SettingsSection as Section,
@ -29,120 +28,15 @@ import {
SettingsSectionRow as Row,
} from '../SettingsSection'
const regions = Object.keys(regionsByKey).map(key => {
const [language, region] = key.split('-')
return { value: key, language, region, label: regionsByKey[key] }
})
const fiats = listFiatCurrencies()
.map(f => f.units[0])
// For now we take first unit, in the future we'll need to figure out something else
.map(fiat => ({
value: fiat.code,
label: `${fiat.name} - ${fiat.code}${fiat.symbol ? ` (${fiat.symbol})` : ''}`,
fiat,
}))
type Props = {
t: T,
settings: Settings,
saveSettings: Function,
i18n: Object,
}
type State = {
cachedMarketIndicator: string,
cachedLanguageKey: ?string,
cachedCounterValue: ?Object,
cachedRegion: ?string,
counterValueCurrency: Currency,
useSystem: boolean,
}
class TabProfile extends PureComponent<Props, State> {
state = {
cachedMarketIndicator: this.props.settings.marketIndicator,
cachedLanguageKey: this.props.settings.language,
cachedCounterValue: fiats.find(fiat => fiat.fiat.code === this.props.settings.counterValue),
cachedRegion: this.props.settings.region,
}
getMarketIndicators() {
const { t } = this.props
return [
{
label: t('app:common.eastern'),
key: 'eastern',
},
{
label: t('app:common.western'),
key: 'western',
},
]
}
handleChangeCounterValue = (item: Object) => {
const { saveSettings } = this.props
this.setState({ cachedCounterValue: item.fiat })
window.requestIdleCallback(() => {
saveSettings({ counterValue: item.fiat.code })
})
}
handleChangeLanguage = ({ value: languageKey }: *) => {
const { i18n, saveSettings } = this.props
this.setState({ cachedLanguageKey: languageKey })
window.requestIdleCallback(() => {
i18n.changeLanguage(languageKey)
moment.locale(languageKey)
saveSettings({ language: languageKey })
})
}
handleChangeRegion = ({ region }: *) => {
const { saveSettings } = this.props
this.setState({ cachedRegion: region })
window.requestIdleCallback(() => {
saveSettings({ region })
})
}
handleChangeMarketIndicator = (item: Object) => {
const { saveSettings } = this.props
const marketIndicator = item.key
this.setState({
cachedMarketIndicator: marketIndicator,
})
window.requestIdleCallback(() => {
saveSettings({ marketIndicator })
})
}
handleChangeExchange = (exchange: *) =>
this.props.saveSettings({ counterValueExchange: exchange ? exchange.id : null })
class TabGeneral extends PureComponent<Props> {
render() {
const { t, settings } = this.props
const {
cachedMarketIndicator,
cachedLanguageKey,
cachedCounterValue,
cachedRegion,
} = this.state
const counterValueCurrency = counterValueCurrencyLocalSelector(settings)
const counterValueExchange = counterValueExchangeLocalSelector(settings)
const languages = [{ value: null, label: t(`language:system`) }].concat(
languageKeys.map(key => ({ value: key, label: t(`language:${key}`) })),
)
const regionsFiltered = regions.filter(({ language }) => cachedLanguageKey === language)
const currentLanguage = languages.find(l => l.value === cachedLanguageKey)
const currentRegion =
regionsFiltered.find(({ region }) => cachedRegion === region) || regionsFiltered[0]
const cvOption = cachedCounterValue
? fiats.find(f => f.value === cachedCounterValue.value)
: null
const { t, useSystem, counterValueCurrency } = this.props
return (
<Section>
@ -157,72 +51,64 @@ class TabProfile extends PureComponent<Props, State> {
title={t('app:settings.display.counterValue')}
desc={t('app:settings.display.counterValueDesc')}
>
<Select
small
minWidth={250}
onChange={this.handleChangeCounterValue}
itemToString={item => (item ? item.name : '')}
renderSelected={item => item && item.name}
options={fiats}
value={cvOption}
/>
<CounterValueSelect />
</Row>
<Row
title={t('app:settings.display.exchange', {
ticker: counterValueCurrency.ticker,
fiat: counterValueCurrency.name,
})}
desc={t('app:settings.display.exchangeDesc', {
fiat: counterValueCurrency.name,
ticker: counterValueCurrency.ticker,
})}
>
<CounterValueExchangeSelect />
</Row>
{counterValueCurrency ? (
<Row
title={t('app:settings.display.exchange', {
ticker: counterValueCurrency.ticker,
fiat: counterValueCurrency.name,
})}
desc={t('app:settings.display.exchangeDesc', {
fiat: counterValueCurrency.name,
ticker: counterValueCurrency.ticker,
})}
>
<SelectExchange
small
from={intermediaryCurrency}
to={counterValueCurrency}
exchangeId={counterValueExchange}
onChange={this.handleChangeExchange}
minWidth={200}
/>
</Row>
) : null}
<Row
title={t('app:settings.display.language')}
desc={t('app:settings.display.languageDesc')}
>
<Select
small
minWidth={250}
isSearchable={false}
onChange={this.handleChangeLanguage}
renderSelected={item => item && item.name}
value={currentLanguage}
options={languages}
/>
<LanguageSelect />
</Row>
{regionsFiltered.length === 0 ? null : (
{useSystem ? null : (
<Row
title={t('app:settings.display.region')}
desc={t('app:settings.display.regionDesc')}
>
<Select
small
minWidth={250}
onChange={this.handleChangeRegion}
renderSelected={item => item && item.name}
value={currentRegion}
options={regionsFiltered}
/>
<RegionSelect />
</Row>
)}
<Row title={t('app:settings.display.stock')} desc={t('app:settings.display.stockDesc')}>
<RadioGroup
items={this.getMarketIndicators()}
activeKey={cachedMarketIndicator}
onChange={this.handleChangeMarketIndicator}
/>
{EXPERIMENTAL_MARKET_INDICATOR_SETTINGS ? (
<Row title={t('app:settings.display.stock')} desc={t('app:settings.display.stockDesc')}>
<MarketIndicatorRadio />
</Row>
) : null}
<Row
title={t('app:settings.profile.password')}
desc={t('app:settings.profile.passwordDesc')}
>
<DisablePasswordButton />
</Row>
<Row
title={t('app:settings.profile.reportErrors')}
desc={t('app:settings.profile.reportErrorsDesc')}
>
<SentryLogsButton />
</Row>
<Row
title={t('app:settings.profile.analytics')}
desc={t('app:settings.profile.analyticsDesc')}
>
<ShareAnalyticsButton />
</Row>
<Row
title={t('app:settings.profile.developerMode')}
desc={t('app:settings.profile.developerModeDesc')}
>
<DevModeButton />
</Row>
</Body>
</Section>
@ -230,4 +116,15 @@ class TabProfile extends PureComponent<Props, State> {
}
}
export default TabProfile
export default translate()(
connect(
createSelector(
langAndRegionSelector,
counterValueCurrencySelector,
({ useSystem }, counterValueCurrency) => ({
useSystem,
counterValueCurrency,
}),
),
)(TabGeneral),
)

284
src/components/SettingsPage/sections/Profile.js

@ -1,284 +0,0 @@
// @flow
import React, { PureComponent } from 'react'
import { connect } from 'react-redux'
import styled from 'styled-components'
import { remote } from 'electron'
import bcrypt from 'bcryptjs'
import { cleanAccountsCache } from 'actions/accounts'
import { unlock } from 'reducers/application' // FIXME should be in actions
import db, { setEncryptionKey } from 'helpers/db'
import { delay } from 'helpers/promise'
import hardReset from 'helpers/hardReset'
import type { SettingsState } from 'reducers/settings'
import type { T } from 'types/common'
import Track from 'analytics/Track'
import TrackPage from 'analytics/TrackPage'
import ExportLogsBtn from 'components/ExportLogsBtn'
import CheckBox from 'components/base/CheckBox'
import Box from 'components/base/Box'
import Button from 'components/base/Button'
import { ConfirmModal } from 'components/base/Modal'
import IconTriangleWarning from 'icons/TriangleWarning'
import IconUser from 'icons/User'
import PasswordModal from '../PasswordModal'
import DisablePasswordModal from '../DisablePasswordModal'
import {
SettingsSection as Section,
SettingsSectionHeader as Header,
SettingsSectionBody as Body,
SettingsSectionRow as Row,
} from '../SettingsSection'
const mapDispatchToProps = {
unlock,
cleanAccountsCache,
}
type Props = {
t: T,
settings: SettingsState,
unlock: Function,
saveSettings: Function,
cleanAccountsCache: () => *,
}
type State = {
isHardResetModalOpened: boolean,
isSoftResetModalOpened: boolean,
isPasswordModalOpened: boolean,
isDisablePasswordModalOpened: boolean,
isHardResetting: boolean,
}
class TabProfile extends PureComponent<Props, State> {
state = {
isHardResetModalOpened: false,
isSoftResetModalOpened: false,
isPasswordModalOpened: false,
isDisablePasswordModalOpened: false,
isHardResetting: false,
}
setPassword = password => {
const { saveSettings, unlock } = this.props
window.requestIdleCallback(() => {
setEncryptionKey('accounts', password)
const hash = password ? bcrypt.hashSync(password, 8) : undefined
saveSettings({
password: {
isEnabled: hash !== undefined,
value: hash,
},
})
unlock()
})
}
handleOpenSoftResetModal = () => this.setState({ isSoftResetModalOpened: true })
handleCloseSoftResetModal = () => this.setState({ isSoftResetModalOpened: false })
handleOpenHardResetModal = () => this.setState({ isHardResetModalOpened: true })
handleCloseHardResetModal = () => this.setState({ isHardResetModalOpened: false })
handleOpenPasswordModal = () => this.setState({ isPasswordModalOpened: true })
handleClosePasswordModal = () => this.setState({ isPasswordModalOpened: false })
handleDisablePassowrd = () => this.setState({ isDisablePasswordModalOpened: true })
handleCloseDisablePasswordModal = () => this.setState({ isDisablePasswordModalOpened: false })
handleSoftReset = async () => {
this.props.cleanAccountsCache()
await delay(500)
db.cleanCache()
remote.getCurrentWindow().webContents.reload()
}
handleHardReset = async () => {
this.setState({ isHardResetting: true })
try {
await hardReset()
remote.getCurrentWindow().webContents.reloadIgnoringCache()
} catch (err) {
this.setState({ isHardResetting: false })
}
}
handleChangePasswordCheck = isChecked => {
if (isChecked) {
this.handleOpenPasswordModal()
} else {
this.handleDisablePassowrd()
}
}
handleChangePassword = (password: ?string) => {
if (password) {
this.setPassword(password)
this.handleClosePasswordModal()
} else {
this.setPassword(undefined)
this.handleCloseDisablePasswordModal()
}
}
handleDeveloperMode = developerMode => {
this.props.saveSettings({
developerMode,
})
}
hardResetIconRender = () => (
<IconWrapperCircle color="alertRed">
<IconTriangleWarning width={23} height={21} />
</IconWrapperCircle>
)
render() {
const { t, settings, saveSettings } = this.props
const {
isSoftResetModalOpened,
isHardResetModalOpened,
isPasswordModalOpened,
isDisablePasswordModalOpened,
isHardResetting,
} = this.state
const isPasswordEnabled = settings.password.isEnabled === true
return (
<Section>
<TrackPage category="Settings" name="Profile" />
<Header
icon={<IconUser size={16} />}
title={t('app:settings.tabs.profile')}
desc={t('app:settings.display.desc')}
/>
<Body>
<Row
title={t('app:settings.profile.password')}
desc={t('app:settings.profile.passwordDesc')}
>
<Track onUpdate event={isPasswordEnabled ? 'PasswordEnabled' : 'PasswordDisabled'} />
<Box horizontal flow={2} align="center">
{isPasswordEnabled && (
<Button onClick={this.handleOpenPasswordModal}>
{t('app:settings.profile.changePassword')}
</Button>
)}
<CheckBox isChecked={isPasswordEnabled} onChange={this.handleChangePasswordCheck} />
</Box>
</Row>
<Row
title={t('app:settings.profile.reportErrors')}
desc={t('app:settings.profile.reportErrorsDesc')}
>
<Track onUpdate event={settings.sentryLogs ? 'SentryEnabled' : 'SentryDisabled'} />
<CheckBox
isChecked={settings.sentryLogs}
onChange={sentryLogs => saveSettings({ sentryLogs })}
/>
</Row>
<Row
title={t('app:settings.profile.analytics')}
desc={t('app:settings.profile.analyticsDesc')}
>
<Track
onUpdate
event={settings.shareAnalytics ? 'AnalyticsEnabled' : 'AnalyticsDisabled'}
/>
<CheckBox
isChecked={settings.shareAnalytics}
onChange={shareAnalytics => saveSettings({ shareAnalytics })}
/>
</Row>
<Row
title={t('app:settings.profile.developerMode')}
desc={t('app:settings.profile.developerModeDesc')}
>
<Track onUpdate event={settings.developerMode ? 'DevModeEnabled' : 'DevModeDisabled'} />
<CheckBox
isChecked={settings.developerMode}
onChange={developerMode => saveSettings({ developerMode })}
/>
</Row>
<Row
title={t('app:settings.profile.softResetTitle')}
desc={t('app:settings.profile.softResetDesc')}
>
<Button primary onClick={this.handleOpenSoftResetModal} event="ClearCacheIntent">
{t('app:settings.profile.softReset')}
</Button>
</Row>
<Row title={t('app:settings.exportLogs.title')} desc={t('app:settings.exportLogs.desc')}>
<ExportLogsBtn />
</Row>
<Row
title={t('app:settings.profile.hardResetTitle')}
desc={t('app:settings.profile.hardResetDesc')}
>
<Button danger onClick={this.handleOpenHardResetModal} event="HardResetIntent">
{t('app:settings.profile.hardReset')}
</Button>
</Row>
</Body>
<ConfirmModal
isDanger
isOpened={isSoftResetModalOpened}
onClose={this.handleCloseSoftResetModal}
onReject={this.handleCloseSoftResetModal}
onConfirm={this.handleSoftReset}
title={t('app:settings.softResetModal.title')}
subTitle={t('app:settings.softResetModal.subTitle')}
desc={t('app:settings.softResetModal.desc')}
/>
<ConfirmModal
isDanger
isLoading={isHardResetting}
isOpened={isHardResetModalOpened}
onClose={this.handleCloseHardResetModal}
onReject={this.handleCloseHardResetModal}
onConfirm={this.handleHardReset}
confirmText={t('app:common.reset')}
title={t('app:settings.hardResetModal.title')}
desc={t('app:settings.hardResetModal.desc')}
renderIcon={this.hardResetIconRender}
/>
<PasswordModal
t={t}
isOpened={isPasswordModalOpened}
onClose={this.handleClosePasswordModal}
onChangePassword={this.handleChangePassword}
isPasswordEnabled={isPasswordEnabled}
currentPasswordHash={settings.password.value}
/>
<DisablePasswordModal
t={t}
isOpened={isDisablePasswordModalOpened}
onClose={this.handleCloseDisablePasswordModal}
onChangePassword={this.handleChangePassword}
isPasswordEnabled={isPasswordEnabled}
currentPasswordHash={settings.password.value}
/>
</Section>
)
}
}
export default connect(
null,
mapDispatchToProps,
)(TabProfile)
// TODO: need a helper file for common styles across the app
export const IconWrapperCircle = styled(Box).attrs({})`
width: 50px;
height: 50px;
border-radius: 50%;
background: #ea2e4919;
text-align: -webkit-center;
justify-content: center;
`

5
src/components/SettingsPage/sections/Tools.js

@ -1,8 +1,7 @@
// @flow
/* eslint-disable react/jsx-no-literals */ // FIXME
import React, { PureComponent } from 'react'
import { translate } from 'react-i18next'
import Box, { Card } from 'components/base/Box'
import Modal, { ModalBody, ModalContent, ModalTitle } from 'components/base/Modal'
import Button from 'components/base/Button'
@ -53,4 +52,4 @@ class TabProfile extends PureComponent<*, *> {
}
}
export default TabProfile
export default translate()(TabProfile)

4
src/config/constants.js

@ -80,6 +80,10 @@ export const DISABLE_ACTIVITY_INDICATORS = boolFromEnv('DISABLE_ACTIVITY_INDICAT
export const EXPERIMENTAL_CENTER_MODAL = boolFromEnv('EXPERIMENTAL_CENTER_MODAL')
export const EXPERIMENTAL_FIRMWARE_UPDATE = boolFromEnv('EXPERIMENTAL_FIRMWARE_UPDATE')
export const EXPERIMENTAL_HTTP_ON_RENDERER = boolFromEnv('EXPERIMENTAL_HTTP_ON_RENDERER')
export const EXPERIMENTAL_TOOLS_SETTINGS = boolFromEnv('EXPERIMENTAL_TOOLS_SETTINGS')
export const EXPERIMENTAL_MARKET_INDICATOR_SETTINGS = boolFromEnv(
'EXPERIMENTAL_MARKET_INDICATOR_SETTINGS',
)
// Other constants

4
src/middlewares/sentry.js

@ -1,12 +1,12 @@
import { ipcRenderer } from 'electron'
import { sentryLogsBooleanSelector } from 'reducers/settings'
import { sentryLogsSelector } from 'reducers/settings'
let isSentryInstalled = false
export default store => next => action => {
next(action)
const state = store.getState()
const sentryLogs = sentryLogsBooleanSelector(state)
const sentryLogs = sentryLogsSelector(state)
if (sentryLogs !== isSentryInstalled) {
isSentryInstalled = sentryLogs
ipcRenderer.send('sentryLogsChanged', { value: sentryLogs })

12
src/reducers/settings.js

@ -27,6 +27,8 @@ export const timeRangeDaysByKey = {
export type TimeRange = $Keys<typeof timeRangeDaysByKey>
export type { CurrencySettings }
export type SettingsState = {
loaded: boolean, // is the settings loaded from db (it not we don't save them)
hasCompletedOnboarding: boolean,
@ -163,10 +165,12 @@ export const lastUsedVersionSelector = (state: State): string => state.settings.
export const availableCurrencies = createSelector(developerModeSelector, listCryptoCurrencies)
export const langAndRegionSelector = (state: State): { language: string, region: ?string } => {
export const langAndRegionSelector = (
state: State,
): { language: string, region: ?string, useSystem: boolean } => {
let { language, region } = state.settings
if (language && languages.includes(language)) {
return { language, region }
return { language, region, useSystem: false }
}
const locale = getSystemLocale()
language = locale.language
@ -175,7 +179,7 @@ export const langAndRegionSelector = (state: State): { language: string, region:
language = 'en'
region = 'US'
}
return { language, region }
return { language, region, useSystem: true }
}
export const languageSelector = createSelector(langAndRegionSelector, o => o.language)
@ -220,7 +224,7 @@ export const exchangeSettingsForAccountSelector: ESFAS = createSelector(
)
export const marketIndicatorSelector = (state: State) => state.settings.marketIndicator
export const sentryLogsBooleanSelector = (state: State) => state.settings.sentryLogs
export const sentryLogsSelector = (state: State) => state.settings.sentryLogs
export const shareAnalyticsSelector = (state: State) => state.settings.shareAnalytics
export const selectedTimeRangeSelector = (state: State) => state.settings.selectedTimeRange
export const hasCompletedOnboardingSelector = (state: State) =>

4
src/renderer/init.js

@ -17,7 +17,7 @@ import { enableGlobalTab, disableGlobalTab, isGlobalTabEnabled } from 'config/gl
import { fetchAccounts } from 'actions/accounts'
import { fetchSettings } from 'actions/settings'
import { isLocked } from 'reducers/application'
import { languageSelector, sentryLogsBooleanSelector } from 'reducers/settings'
import { languageSelector, sentryLogsSelector } from 'reducers/settings'
import libcoreGetVersion from 'commands/libcoreGetVersion'
import db from 'helpers/db'
@ -58,7 +58,7 @@ async function init() {
const language = languageSelector(state)
moment.locale(language)
sentry(() => sentryLogsBooleanSelector(store.getState()))
sentry(() => sentryLogsSelector(store.getState()))
// FIXME IMO init() really should only be for window. any other case is a hack!
const isMainWindow = remote.getCurrentWindow().name === 'MainWindow'

Loading…
Cancel
Save