Browse Source

Merge pull request #817 from LedgerHQ/develop

Prepare for beta.6
master
Gaëtan Renaudeau 7 years ago
committed by GitHub
parent
commit
2db75def80
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      babel.config.js
  2. 5
      package.json
  3. 9
      scripts/compile.sh
  4. 2
      scripts/postinstall.sh
  5. 13
      src/actions/settings.js
  6. 7
      src/analytics/segment.js
  7. 7
      src/bridge/BridgeSyncContext.js
  8. 1
      src/bridge/LibcoreBridge.js
  9. 17
      src/commands/getAddress.js
  10. 2
      src/commands/index.js
  11. 15
      src/commands/ping.js
  12. 4
      src/components/AccountPage/AccountBalanceSummaryHeader.js
  13. 2
      src/components/AccountPage/EmptyStateAccount.js
  14. 3
      src/components/AccountPage/index.js
  15. 4
      src/components/AdvancedOptions/BitcoinKind.js
  16. 2
      src/components/AdvancedOptions/EthereumKind.js
  17. 18
      src/components/App.js
  18. 9
      src/components/DashboardPage/EmptyState.js
  19. 4
      src/components/DeviceInteraction/DeviceInteractionStep.js
  20. 28
      src/components/DeviceInteraction/components.js
  21. 21
      src/components/DeviceInteraction/stories.js
  22. 45
      src/components/EnsureDevice.js
  23. 4
      src/components/EnsureDeviceApp.js
  24. 45
      src/components/ExportLogsBtn.js
  25. 23
      src/components/FeesField/BitcoinKind.js
  26. 6
      src/components/FeesField/RippleKind.js
  27. 16
      src/components/GenuineCheck.js
  28. 4
      src/components/ManagerPage/AppsList.js
  29. 2
      src/components/ManagerPage/FirmwareUpdate.js
  30. 11
      src/components/Onboarding/OnboardingBreadcrumb.js
  31. 4
      src/components/Onboarding/OnboardingFooter.js
  32. 16
      src/components/Onboarding/index.js
  33. 6
      src/components/Onboarding/steps/Analytics.js
  34. 5
      src/components/Onboarding/steps/Finish.js
  35. 96
      src/components/Onboarding/steps/GenuineCheck/GenuineCheckErrorPage.js
  36. 5
      src/components/Onboarding/steps/GenuineCheck/GenuineCheckUnavailable.js
  37. 25
      src/components/Onboarding/steps/GenuineCheck/index.js
  38. 2
      src/components/Onboarding/steps/NoDevice.js
  39. 4
      src/components/Onboarding/steps/SetPassword.js
  40. 2
      src/components/Onboarding/steps/Start.js
  41. 28
      src/components/OnboardingOrElse.js
  42. 104
      src/components/OperationsList/index.js
  43. 10
      src/components/RenderError.js
  44. 25
      src/components/SettingsPage/AboutRowItem.js
  45. 72
      src/components/SettingsPage/CleanButton.js
  46. 53
      src/components/SettingsPage/CounterValueExchangeSelect.js
  47. 58
      src/components/SettingsPage/CounterValueSelect.js
  48. 39
      src/components/SettingsPage/DevModeButton.js
  49. 133
      src/components/SettingsPage/DisablePasswordButton.js
  50. 4
      src/components/SettingsPage/DisablePasswordModal.js
  51. 61
      src/components/SettingsPage/LanguageSelect.js
  52. 48
      src/components/SettingsPage/MarketIndicatorRadio.js
  53. 4
      src/components/SettingsPage/PasswordModal.js
  54. 67
      src/components/SettingsPage/RegionSelect.js
  55. 46
      src/components/SettingsPage/ReleaseNotesButton.js
  56. 82
      src/components/SettingsPage/ResetButton.js
  57. 39
      src/components/SettingsPage/SentryLogsButton.js
  58. 38
      src/components/SettingsPage/ShareAnalyticsButton.js
  59. 60
      src/components/SettingsPage/index.js
  60. 119
      src/components/SettingsPage/sections/About.js
  61. 96
      src/components/SettingsPage/sections/Currencies.js
  62. 114
      src/components/SettingsPage/sections/CurrencyRows.js
  63. 259
      src/components/SettingsPage/sections/Display.js
  64. 284
      src/components/SettingsPage/sections/Profile.js
  65. 7
      src/components/SettingsPage/sections/Tools.js
  66. 65
      src/components/SyncContinouslyPendingOperations.js
  67. 20
      src/components/WarnBox/index.js
  68. 4
      src/components/base/AccountsList/AccountRow.js
  69. 10
      src/components/base/Button/index.js
  70. 47
      src/components/base/CheckBox/index.js
  71. 4
      src/components/base/CheckBox/stories.js
  72. 40
      src/components/base/Defer.js
  73. 4
      src/components/base/GrowScroll/index.js
  74. 9
      src/components/base/Input/index.js
  75. 11
      src/components/base/InputCurrency/index.js
  76. 3
      src/components/base/Modal/index.js
  77. 15
      src/components/base/Progress/index.js
  78. 25
      src/components/base/Progress/stories.js
  79. 1
      src/components/base/Select/index.js
  80. 53
      src/components/base/Switch/index.js
  81. 12
      src/components/base/Switch/stories.js
  82. 4
      src/components/layout/Default.js
  83. 4
      src/components/modals/AccountSettingRenderBody.js
  84. 4
      src/components/modals/AddAccounts/index.js
  85. 57
      src/components/modals/AddAccounts/steps/04-step-finish.js
  86. 121
      src/components/modals/Debug.js
  87. 48
      src/components/modals/Disclaimer.js
  88. 2
      src/components/modals/OperationDetails.js
  89. 16
      src/components/modals/Receive/index.js
  90. 13
      src/components/modals/Receive/steps/03-step-confirm-address.js
  91. 2
      src/components/modals/ReleaseNotes.js
  92. 7
      src/components/modals/Send/fields/AmountField.js
  93. 7
      src/components/modals/Send/fields/RecipientField.js
  94. 12
      src/components/modals/Send/index.js
  95. 15
      src/components/modals/Send/steps/01-step-amount.js
  96. 2
      src/components/modals/UpdateFirmware/Disclaimer.js
  97. 1
      src/components/modals/index.js
  98. 8
      src/config/constants.js
  99. 20
      src/helpers/devices/getDeviceInfo.js
  100. 1
      src/helpers/hardReset.js

2
babel.config.js

@ -24,7 +24,7 @@ module.exports = () => ({
[
require('babel-plugin-styled-components'),
{
displayName: NODE_ENV === 'development',
displayName: true,
ssr: __TEST__,
},
],

5
package.json

@ -32,7 +32,6 @@
}
},
"resolutions": {
"uglify-es": "3.3.7",
"webpack-sources": "1.0.1"
},
"dependencies": {
@ -107,6 +106,9 @@
"tippy.js": "^2.5.2",
"uncontrollable": "^6.0.0",
"uuid": "^3.2.1",
"winston": "^3.0.0",
"winston-daily-rotate-file": "^3.2.3",
"winston-transport": "^4.2.0",
"ws": "^5.1.1",
"zxcvbn": "^4.4.2"
},
@ -157,7 +159,6 @@
"prettier": "^1.13.5",
"react-hot-loader": "^4.3.2",
"react-test-renderer": "^16.4.1",
"uglifyjs-webpack-plugin": "^1.2.6",
"webpack": "^4.6.0",
"webpack-bundle-analyzer": "^2.11.1",
"webpack-cli": "^2.0.14",

9
scripts/compile.sh

@ -2,11 +2,10 @@
set -e
GIT_REVISION=`git rev-parse HEAD`
SENTRY_URL=https://db8f5b9b021048d4a401f045371701cb@sentry.io/274561
NODE_ENV=production
export GIT_REVISION=`git rev-parse HEAD`
export SENTRY_URL=https://db8f5b9b021048d4a401f045371701cb@sentry.io/274561
rm -rf ./node_modules/.cache dist
JOBS=max yarn
yarn run webpack-cli --mode production --config webpack/internals.config.js
yarn run electron-webpack
NODE_ENV=production yarn run webpack-cli --mode production --config webpack/internals.config.js
NODE_ENV=production yarn run electron-webpack

2
scripts/postinstall.sh

@ -18,7 +18,7 @@ function INSTALL_FLOW_TYPED {
echo "> Installing flow-typed defs"
flow-typed install -s --overwrite
echo "> Removing broken flow definitions"
rm flow-typed/npm/{react-i18next_v7.x.x.js,styled-components_v3.x.x.js,redux_*}
rm flow-typed/npm/{react-i18next_v7.x.x.js,styled-components_v3.x.x.js,redux_*,winston*}
SET_HASH 'flow-typed' $LATEST_FLOW_TYPED_COMMIT_HASH
fi
}

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({

7
src/analytics/segment.js

@ -3,13 +3,17 @@
import uuid from 'uuid/v4'
import logger from 'logger'
import invariant from 'invariant'
import user from 'helpers/user'
import { langAndRegionSelector } from 'reducers/settings'
import { getSystemLocale } from 'helpers/systemLocale'
import { load } from './inject-in-window'
invariant(typeof window !== 'undefined', 'analytics/segment must be called on renderer thread')
let user = null
if (!process.env.STORYBOOK_ENV) {
user = require('helpers/user').default
}
const sessionId = uuid()
const getContext = store => {
@ -31,6 +35,7 @@ const getContext = store => {
let storeInstance // is the redux store. it's also used as a flag to know if analytics is on or off.
export const start = (store: *) => {
if (!user) return
const { id } = user()
logger.analyticsStart(id)
storeInstance = store

7
src/bridge/BridgeSyncContext.js

@ -45,6 +45,7 @@ export type BehaviorAction =
| { type: 'BACKGROUND_TICK' }
| { type: 'SET_SKIP_UNDER_PRIORITY', priority: number }
| { type: 'SYNC_ONE_ACCOUNT', accountId: string, priority: number }
| { type: 'SYNC_SOME_ACCOUNTS', accountIds: string[], priority: number }
| { type: 'SYNC_ALL_ACCOUNTS', priority: number }
export type Sync = (action: BehaviorAction) => void
@ -64,7 +65,6 @@ const actions = {
class Provider extends Component<BridgeSyncProviderOwnProps, Sync> {
constructor() {
super()
const synchronize = (accountId: string, next: () => void) => {
const state = syncStateLocalSelector(this.props.bridgeSync, { accountId })
if (state.pending) {
@ -104,6 +104,7 @@ class Provider extends Component<BridgeSyncProviderOwnProps, Sync> {
const schedule = (ids: string[], priority: number) => {
if (priority < skipUnderPriority) return
// by convention we remove concurrent tasks with same priority
// FIXME this is somehow a hack. ideally we should just dedup the account ids in the pending queue...
syncQueue.remove(o => priority === o.priority)
syncQueue.push(ids, -priority)
}
@ -131,6 +132,10 @@ class Provider extends Component<BridgeSyncProviderOwnProps, Sync> {
SYNC_ONE_ACCOUNT: ({ accountId, priority }) => {
schedule([accountId], priority)
},
SYNC_SOME_ACCOUNTS: ({ accountIds, priority }) => {
schedule(accountIds, priority)
},
}
const sync = (action: BehaviorAction) => {

1
src/bridge/LibcoreBridge.js

@ -52,6 +52,7 @@ const isRecipientValid = (currency, recipient) => {
const key = `${currency.id}_${recipient}`
let promise = recipientValidLRU.get(key)
if (promise) return promise
if (!recipient) return Promise.resolve(false)
promise = libcoreValidAddress
.send({
address: recipient,

17
src/commands/getAddress.js

@ -6,6 +6,11 @@ import { fromPromise } from 'rxjs/observable/fromPromise'
import { withDevice } from 'helpers/deviceAccess'
import getAddressForCurrency from 'helpers/getAddressForCurrency'
import { createCustomErrorClass } from 'helpers/errors'
const DeviceAppVerifyNotSupported = createCustomErrorClass('DeviceAppVerifyNotSupported')
const UserRefusedAddress = createCustomErrorClass('UserRefusedAddress')
type Input = {
currencyId: string,
devicePath: string,
@ -26,7 +31,17 @@ const cmd: Command<Input, Result> = createCommand(
fromPromise(
withDevice(devicePath)(transport =>
getAddressForCurrency(transport, getCryptoCurrencyById(currencyId), path, options),
),
).catch(e => {
if (e && e.name === 'TransportStatusError') {
if (e.statusCode === 0x6b00 && options.verify) {
throw new DeviceAppVerifyNotSupported()
}
if (e.statusCode === 0x6985) {
throw new UserRefusedAddress()
}
}
throw e
}),
),
)

2
src/commands/index.js

@ -25,6 +25,7 @@ import listApps from 'commands/listApps'
import listAppVersions from 'commands/listAppVersions'
import listCategories from 'commands/listCategories'
import listenDevices from 'commands/listenDevices'
import ping from 'commands/ping'
import signTransaction from 'commands/signTransaction'
import testApdu from 'commands/testApdu'
import testCrash from 'commands/testCrash'
@ -54,6 +55,7 @@ const all: Array<Command<any, any>> = [
listAppVersions,
listCategories,
listenDevices,
ping,
signTransaction,
testApdu,
testCrash,

15
src/commands/ping.js

@ -0,0 +1,15 @@
// @flow
// This is a test example for dev testing purpose.
import { Observable } from 'rxjs'
import { createCommand, Command } from 'helpers/ipc'
const cmd: Command<void, string> = createCommand('ping', () =>
Observable.create(o => {
o.next('pong')
o.complete()
}),
)
export default cmd

4
src/components/AccountPage/AccountBalanceSummaryHeader.js

@ -28,7 +28,7 @@ type OwnProps = {
totalBalance: number,
sinceBalance: number,
refBalance: number,
accountId: string, // eslint-disable-line
accountId: string,
}
type Props = OwnProps & {
@ -57,6 +57,7 @@ class AccountBalanceSummaryHeader extends PureComponent<Props> {
render() {
const {
account,
accountId,
t,
counterValue,
selectedTimeRange,
@ -76,6 +77,7 @@ class AccountBalanceSummaryHeader extends PureComponent<Props> {
unit={account.unit}
>
<FormattedVal
key={accountId}
animateTicker
disableRounding
alwaysShowSign={false}

2
src/components/AccountPage/EmptyStateAccount.js

@ -50,7 +50,7 @@ class EmptyStateAccount extends PureComponent<Props, *> {
{'app is installed to receive funds.'}
</Trans>
</Description>
<Button mt={5} padded primary onClick={() => openModal(MODAL_RECEIVE, { account })}>
<Button mt={5} primary onClick={() => openModal(MODAL_RECEIVE, { account })}>
<Box horizontal flow={1} alignItems="center">
<IconReceive size={12} />
<Box>{t('app:account.emptyState.buttons.receiveFunds')}</Box>

3
src/components/AccountPage/index.js

@ -68,8 +68,7 @@ class AccountPage extends PureComponent<Props> {
}
return (
// `key` forces re-render account page when going an another account (skip animations)
<Box key={account.id}>
<Box>
<TrackPage
category="Account"
currency={account.currency.id}

4
src/components/AdvancedOptions/BitcoinKind.js

@ -3,7 +3,7 @@ import React from 'react'
import { translate } from 'react-i18next'
import Box from 'components/base/Box'
import CheckBox from 'components/base/CheckBox'
import Switch from 'components/base/Switch'
import Label from 'components/base/Label'
import LabelInfoTooltip from 'components/base/LabelInfoTooltip'
import Spoiler from 'components/base/Spoiler'
@ -24,7 +24,7 @@ export default translate()(({ isRBF, onChangeRBF, t }: Props) => (
</Label>
</Box>
<Box grow>
<CheckBox isChecked={isRBF} onChange={onChangeRBF} />
<Switch isChecked={isRBF} onChange={onChangeRBF} />
</Box>
</Box>

2
src/components/AdvancedOptions/EthereumKind.js

@ -25,7 +25,7 @@ export default translate()(({ gasLimit, onChangeGasLimit, t }: Props) => (
<Input
value={gasLimit}
onChange={str => {
const gasLimit = parseInt(str, 10)
const gasLimit = parseInt(str || 0, 10)
if (!isNaN(gasLimit) && isFinite(gasLimit)) onChangeGasLimit(gasLimit)
else onChangeGasLimit(0x5208)
}}

18
src/components/App.js

@ -12,8 +12,7 @@ import theme from 'styles/theme'
import i18n from 'renderer/i18n/electron'
import Onboarding from 'components/Onboarding'
import OnboardingOrElse from 'components/OnboardingOrElse'
import ThrowBlock from 'components/ThrowBlock'
import Default from 'components/layout/Default'
import Print from 'components/layout/Print'
@ -35,13 +34,14 @@ const App = ({
<I18nextProvider i18n={i18n} initialLanguage={language}>
<ThemeProvider theme={theme}>
<ThrowBlock>
<Onboarding />
<ConnectedRouter history={history}>
<Switch>
<Route path="/print" component={Print} />
<Route component={Default} />
</Switch>
</ConnectedRouter>
<OnboardingOrElse>
<ConnectedRouter history={history}>
<Switch>
<Route path="/print" component={Print} />
<Route component={Default} />
</Switch>
</ConnectedRouter>
</OnboardingOrElse>
</ThrowBlock>
</ThemeProvider>
</I18nextProvider>

9
src/components/DashboardPage/EmptyState.js

@ -50,15 +50,10 @@ class EmptyState extends PureComponent<Props, *> {
{t('app:emptyState.dashboard.desc')}
</Description>
<Box mt={5} horizontal style={{ width: 300 }} flow={3} justify="center">
<Button padded primary style={{ minWidth: 120 }} onClick={this.handleInstallApp}>
<Button primary style={{ minWidth: 120 }} onClick={this.handleInstallApp}>
{t('app:emptyState.dashboard.buttons.installApp')}
</Button>
<Button
padded
outline
style={{ minWidth: 120 }}
onClick={() => openModal(MODAL_ADD_ACCOUNTS)}
>
<Button outline style={{ minWidth: 120 }} onClick={() => openModal(MODAL_ADD_ACCOUNTS)}>
{t('app:emptyState.dashboard.buttons.addAccount')}
</Button>
</Box>

4
src/components/DeviceInteraction/DeviceInteractionStep.js

@ -177,10 +177,10 @@ class DeviceInteractionStep extends PureComponent<
isPrecedentActive={isPrecedentActive}
isError={isError}
>
<IconContainer>{step.icon}</IconContainer>
<IconContainer isTransparent={!isActive && !isSuccess}>{step.icon}</IconContainer>
<Box py={4} justify="center" grow shrink>
{title && (
<Box color={isActive && !isSuccess ? 'dark' : ''} ff="Open Sans|SemiBold">
<Box color={isActive || isSuccess ? 'dark' : ''} ff="Open Sans|SemiBold">
{title}
</Box>
)}

28
src/components/DeviceInteraction/components.js

@ -27,7 +27,7 @@ export const DeviceInteractionStepContainer = styled(Box).attrs({
min-height: 80px;
border: 1px solid ${p => p.theme.colors.fog};
border-color: ${p =>
p.isError ? p.theme.colors.alertRed : p.isActive && !p.isFinished ? p.theme.colors.wallet : ''};
p.isError ? p.theme.colors.alertRed : p.isActive || p.isSuccess ? p.theme.colors.wallet : ''};
border-top-color: ${p => (p.isFirst || p.isActive ? '' : 'transparent')};
border-bottom-color: ${p => (p.isPrecedentActive ? 'transparent' : '')};
border-bottom-left-radius: ${p => (p.isLast ? `${radii[1]}px` : 0)};
@ -42,8 +42,22 @@ export const DeviceInteractionStepContainer = styled(Box).attrs({
: 'none'};
`
export const IconContainer = ({ children }: { children: any }) => (
<Box align="center" justify="center" style={{ width: 70 }}>
export const IconContainer = ({
children,
isTransparent,
}: {
children: any,
isTransparent: boolean,
}) => (
<Box
align="center"
justify="center"
color="dark"
style={{
width: 70,
opacity: isTransparent ? 0.5 : 1,
}}
>
{children}
</Box>
)
@ -58,10 +72,10 @@ const SpinnerContainerWrapper = styled.div`
display: flex;
align-items: center;
justify-content: center;
transition: 350ms cubic-bezier(0.62, 0.28, 0.39, 0.94);
transition: 300ms cubic-bezier(0.62, 0.28, 0.39, 0.94);
transition-property: transform opacity;
opacity: ${p => (p.isVisible ? 1 : 0)};
transform: translate3d(0, ${p => (!p.isVisible ? -40 : 0)}px, 0);
transform: translate3d(0, ${p => (!p.isVisible ? -20 : 0)}px, 0);
`
export const SpinnerContainer = ({ isVisible }: { isVisible: boolean }) => (
@ -72,7 +86,7 @@ export const SpinnerContainer = ({ isVisible }: { isVisible: boolean }) => (
const SuccessContainerWrapper = styled(SpinnerContainerWrapper)`
color: ${p => p.theme.colors.wallet};
transform: translate3d(0, ${p => (!p.isVisible ? 40 : 0)}px, 0);
transform: translate3d(0, ${p => (!p.isVisible ? 20 : 0)}px, 0);
`
export const SuccessContainer = ({ isVisible }: { isVisible: boolean }) => (
@ -83,7 +97,7 @@ export const SuccessContainer = ({ isVisible }: { isVisible: boolean }) => (
const ErrorContainerWrapper = styled(SpinnerContainerWrapper)`
color: ${p => p.theme.colors.alertRed};
transform: translate3d(0, ${p => (!p.isVisible ? 40 : 0)}px, 0);
transform: translate3d(0, ${p => (!p.isVisible ? 20 : 0)}px, 0);
`
export const ErrorContainer = ({ isVisible }: { isVisible: boolean }) => (

21
src/components/DeviceInteraction/stories.js

@ -22,7 +22,7 @@ const MockIcon = styled.div`
border-radius: 50%;
`
const mockIcon = <MockIcon size={36} />
const mockIcon = <MockIcon size={26} />
class Wrapper extends React.Component<any> {
_ref = null
@ -37,11 +37,18 @@ class Wrapper extends React.Component<any> {
shouldRenderRetry
ref={n => (this._ref = n)}
steps={[
{
id: 'deviceOpen',
title: ({ deviceConnect: device }) =>
`Open the Bitcoin application on your ${device ? `${device.name} ` : ''}device`,
desc: 'To be able to retriev your Bitcoins',
icon: mockIcon,
run: () => new Promise(resolve => setTimeout(resolve, 1 * 1000)),
},
{
id: 'deviceConnect',
title: 'Connect your device',
icon: <IconUsb size={36} />,
desc: 'If you dont connect your device, we wont be able to read on it',
icon: <IconUsb size={26} />,
render: ({ onSuccess, onFail }) => (
<Box p={2} bg="lightGrey" mt={2} borderRadius={1}>
<Box horizontal flow={2}>
@ -58,14 +65,6 @@ class Wrapper extends React.Component<any> {
</Box>
),
},
{
id: 'deviceOpen',
title: ({ deviceConnect: device }) =>
`Open the Bitcoin application on your ${device ? `${device.name} ` : ''}device`,
desc: 'To be able to retriev your Bitcoins',
icon: mockIcon,
run: () => new Promise(resolve => setTimeout(resolve, 1 * 1000)),
},
{
id: 'check',
title: 'Checking if all is alright...',

45
src/components/EnsureDevice.js

@ -1,45 +0,0 @@
// @flow
/* eslint-disable react/no-multi-comp */
import { Component, PureComponent } from 'react'
import { connect } from 'react-redux'
import type { Node } from 'react'
import type { Device } from 'types/common'
import { getCurrentDevice } from 'reducers/devices'
type Props = {
device: Device,
children: (device: Device) => Node,
}
let prevents = 0
export class PreventDeviceChangeRecheck extends PureComponent<{}> {
componentDidMount() {
prevents++
}
componentWillUnmount() {
prevents--
}
render() {
return null
}
}
class EnsureDevice extends Component<Props> {
shouldComponentUpdate(nextProps) {
if (prevents > 0) return false
return nextProps.device !== this.props.device
}
render() {
const { device, children } = this.props
return children(device)
}
}
const mapStateToProps = state => ({
device: getCurrentDevice(state),
})
export default connect(mapStateToProps)(EnsureDevice)

4
src/components/EnsureDeviceApp.js

@ -27,7 +27,7 @@ import { getCurrentDevice } from 'reducers/devices'
export const WrongAppOpened = createCustomErrorClass('WrongAppOpened')
export const WrongDeviceForAccount = createCustomErrorClass('WrongDeviceForAccount')
const usbIcon = <IconUsb size={36} />
const usbIcon = <IconUsb size={30} />
const Bold = props => <Text ff="Open Sans|Bold" {...props} />
const mapStateToProps = state => ({
@ -107,7 +107,7 @@ class EnsureDeviceApp extends Component<{
{
id: 'address',
title: this.renderOpenAppTitle,
icon: Icon ? <Icon size={24} /> : null,
icon: Icon ? <Icon size={30} /> : null,
run: this.openAppInteractionHandler,
},
]}

45
src/components/ExportLogsBtn.js

@ -5,58 +5,37 @@ import fs from 'fs'
import { webFrame, remote } from 'electron'
import React, { Component } from 'react'
import { translate } from 'react-i18next'
import { connect } from 'react-redux'
import KeyHandler from 'react-key-handler'
import { createStructuredSelector, createSelector } from 'reselect'
import { accountsSelector, encodeAccountsModel } from 'reducers/accounts'
import { storeSelector as settingsSelector } from 'reducers/settings'
import { getCurrentLogFile } from 'helpers/resolveLogsDirectory'
import Button from './base/Button'
const mapStateToProps = createStructuredSelector({
accounts: createSelector(accountsSelector, encodeAccountsModel),
settings: settingsSelector,
})
class ExportLogsBtn extends Component<{
t: *,
settings: ?*,
accounts: ?*,
hookToShortcut?: boolean,
}> {
handleExportLogs = () => {
const { accounts, settings } = this.props
const logs = logger.exportLogs()
const srcLogFile = getCurrentLogFile()
const resourceUsage = webFrame.getResourceUsage()
const report = {
logger.log('exportLogsMeta', {
resourceUsage,
logs,
accounts,
settings,
date: new Date(),
release: __APP_VERSION__,
git_commit: __GIT_REVISION__,
environment: __DEV__ ? 'development' : 'production',
}
console.log(report) // eslint-disable-line no-console
const reportJSON = JSON.stringify(report)
})
const path = remote.dialog.showSaveDialog({
title: 'Export logs',
defaultPath: `ledgerlive-export-${moment().format(
'YYYY.MM.DD-HH.mm.ss',
)}-${__GIT_REVISION__ || 'unversionned'}.json`,
)}-${__GIT_REVISION__ || 'unversionned'}.log`,
filters: [
{
name: 'All Files',
extensions: ['json'],
extensions: ['log'],
},
],
})
if (path) {
fs.writeFile(path, reportJSON, err => {
if (err) {
logger.error(err)
}
})
fs.createReadStream(srcLogFile).pipe(fs.createWriteStream(path))
}
}
@ -71,17 +50,11 @@ class ExportLogsBtn extends Component<{
return hookToShortcut ? (
<KeyHandler keyValue="e" onKeyHandle={this.onKeyHandle} />
) : (
<Button primary event="ExportLogs" onClick={this.handleExportLogs}>
<Button small primary event="ExportLogs" onClick={this.handleExportLogs}>
{t('app:settings.exportLogs.btn')}
</Button>
)
}
}
const WithAppData = connect(mapStateToProps)(ExportLogsBtn)
const WithoutAppData = ExportLogsBtn
const ExportLogsBtnDispatcher = ({ withAppData, ...rest }: *) =>
withAppData ? <WithAppData {...rest} /> : <WithoutAppData {...rest} />
export default translate()(ExportLogsBtnDispatcher)
export default translate()(ExportLogsBtn)

23
src/components/FeesField/BitcoinKind.js

@ -50,10 +50,9 @@ const customItem = {
feePerByte: 0,
}
class FeesField extends Component<
Props & { fees?: Fees, error?: Error },
{ isFocused: boolean, items: FeeItem[], selectedItem: FeeItem },
> {
type State = { isFocused: boolean, items: FeeItem[], selectedItem: FeeItem }
class FeesField extends Component<Props & { fees?: Fees, error?: Error }, State> {
state = {
items: [customItem],
selectedItem: customItem,
@ -103,10 +102,21 @@ class FeesField extends Component<
onSelectChange = selectedItem => {
const { onChange } = this.props
this.setState({ selectedItem })
if (selectedItem.feePerByte) onChange(selectedItem.feePerByte)
const patch: $Shape<State> = { selectedItem }
if (selectedItem.feePerByte) {
onChange(selectedItem.feePerByte)
} else {
const { input } = this
if (!selectedItem.feePerByte && input.current) {
patch.isFocused = true
input.current.select()
}
}
this.setState(patch)
}
input = React.createRef()
render() {
const { account, feePerByte, error, onChange, t } = this.props
const { items, selectedItem } = this.state
@ -118,6 +128,7 @@ class FeesField extends Component<
<GenericContainer error={error} help={t('app:send.steps.amount.unitPerByte')}>
<Select width={156} options={items} value={selectedItem} onChange={this.onSelectChange} />
<InputCurrency
ref={this.input}
defaultUnit={satoshi}
units={units}
containerProps={{ grow: true }}

6
src/components/FeesField/RippleKind.js

@ -23,11 +23,17 @@ class FeesField extends Component<Props, State> {
componentDidMount() {
this.sync()
}
componentWillUnmount() {
this.syncId++
}
syncId = 0
async sync() {
const api = apiForEndpointConfig(this.props.account.endpointConfig)
const syncId = ++this.syncId
try {
await api.connect()
const info = await api.getServerInfo()
if (syncId !== this.syncId) return
const serverFee = parseAPIValue(info.validatedLedger.baseFeeXRP)
if (!this.props.fee) {
this.props.onChange(serverFee)

16
src/components/GenuineCheck.js

@ -24,21 +24,21 @@ import Text from 'components/base/Text'
import IconUsb from 'icons/Usb'
import IconHome from 'icons/Home'
import IconEye from 'icons/Eye'
import IconGenuineCheck from 'icons/GenuineCheck'
const DeviceNotGenuineError = createCustomErrorClass('DeviceNotGenuine')
type Props = {
t: T,
onSuccess: void => void,
onFail: Error => void,
onUnavailable: Error => void,
onFail?: Error => void,
onUnavailable?: Error => void,
device: ?Device,
}
const usbIcon = <IconUsb size={36} />
const usbIcon = <IconUsb size={26} />
const homeIcon = <IconHome size={24} />
const eyeIcon = <IconEye size={24} />
const genuineCheckIcon = <IconGenuineCheck size={24} />
const mapStateToProps = state => ({
device: getCurrentDevice(state),
@ -92,9 +92,9 @@ class GenuineCheck extends PureComponent<Props> {
handleFail = (err: Error) => {
const { onFail, onUnavailable } = this.props
if (err instanceof DeviceNotGenuineError) {
onFail(err)
onFail && onFail(err)
} else {
onUnavailable(err)
onUnavailable && onUnavailable(err)
}
}
@ -133,7 +133,7 @@ class GenuineCheck extends PureComponent<Props> {
{' on your device'}
</Trans>
),
icon: eyeIcon,
icon: genuineCheckIcon,
run: this.checkGenuineInteractionHandler,
},
]

4
src/components/ManagerPage/AppsList.js

@ -226,7 +226,7 @@ class AppsList extends PureComponent<Props, State> {
</Box>
</ModalContent>
<ModalFooter horizontal justifyContent="flex-end" style={{ width: '100%' }}>
<Button primary padded onClick={this.handleCloseModal}>
<Button primary onClick={this.handleCloseModal}>
{t('app:common.close')}
</Button>
</ModalFooter>
@ -254,7 +254,7 @@ class AppsList extends PureComponent<Props, State> {
</Box>
</ModalContent>
<ModalFooter horizontal justifyContent="flex-end" style={{ width: '100%' }}>
<Button primary padded onClick={this.handleCloseModal}>
<Button primary onClick={this.handleCloseModal}>
{t('app:common.close')}
</Button>
</ModalFooter>

2
src/components/ManagerPage/FirmwareUpdate.js

@ -27,7 +27,6 @@ import Text from 'components/base/Text'
import NanoS from 'icons/device/NanoS'
import CheckFull from 'icons/CheckFull'
import { PreventDeviceChangeRecheck } from 'components/EnsureDevice'
import UpdateFirmwareButton from './UpdateFirmwareButton'
export const getCleanVersion = (input: string): string =>
@ -143,7 +142,6 @@ class FirmwareUpdate extends PureComponent<Props, State> {
</Box>
<UpdateFirmwareButton firmware={latestFirmware} onClick={this.handleDisclaimerModal} />
</Box>
{modal !== 'closed' ? <PreventDeviceChangeRecheck /> : null}
{latestFirmware && (
<Fragment>
<DisclaimerModal

11
src/components/Onboarding/OnboardingBreadcrumb.js

@ -21,10 +21,17 @@ type Props = {
function OnboardingBreadcrumb(props: Props) {
const { onboarding, t } = props
const { stepName, genuine } = onboarding
const isInitializedFlow = onboarding.flowType === 'initializedDevice'
const filteredSteps = onboarding.steps
const regularFilteredSteps = onboarding.steps
.filter(step => !step.external)
.map(step => ({ ...step, label: t(step.label) })) // TODO: translate
.map(step => ({ ...step, label: t(step.label) }))
const alreadyInitializedSteps = onboarding.steps
.filter(step => !step.external && step.name !== 'writeSeed' && step.name !== 'selectPIN')
.map(step => ({ ...step, label: t(step.label) }))
const filteredSteps = isInitializedFlow ? alreadyInitializedSteps : regularFilteredSteps
const stepIndex = findIndex(filteredSteps, s => s.name === stepName)
const genuineStepIndex = findIndex(filteredSteps, s => s.name === 'genuineCheck')

4
src/components/Onboarding/OnboardingFooter.js

@ -23,10 +23,10 @@ const OnboardingFooter = ({
...props
}: Props) => (
<OnboardingFooterWrapper {...props}>
<Button padded outlineGrey onClick={() => prevStep()}>
<Button outlineGrey onClick={() => prevStep()}>
{t('app:common.back')}
</Button>
<Button padded disabled={isContinueDisabled} primary onClick={() => nextStep()} ml="auto">
<Button disabled={isContinueDisabled} primary onClick={() => nextStep()} ml="auto">
{t('app:common.continue')}
</Button>
</OnboardingFooterWrapper>

16
src/components/Onboarding/index.js

@ -10,7 +10,9 @@ import type { T } from 'types/common'
import type { OnboardingState } from 'reducers/onboarding'
import type { SettingsState } from 'reducers/settings'
import { MODAL_DISCLAIMER, MODAL_DISCLAIMER_DELAY } from 'config/constants'
import { saveSettings } from 'actions/settings'
import { openModal } from 'reducers/modals'
import {
nextStep,
prevStep,
@ -23,7 +25,9 @@ import { getCurrentDevice } from 'reducers/devices'
import { unlock } from 'reducers/application'
import ExportLogsBtn from 'components/ExportLogsBtn'
import Box from 'components/base/Box'
import TriggerAppReady from '../TriggerAppReady'
import Start from './steps/Start'
import InitStep from './steps/Init'
@ -63,6 +67,7 @@ const mapDispatchToProps = {
prevStep,
jumpStep,
unlock,
openModal,
}
type Props = {
@ -76,6 +81,7 @@ type Props = {
jumpStep: Function,
getCurrentDevice: Function,
unlock: Function,
openModal: string => void,
}
export type StepProps = {
@ -96,7 +102,12 @@ export type StepProps = {
class Onboarding extends PureComponent<Props> {
getDeviceInfo = () => this.props.getCurrentDevice
finish = () => this.props.saveSettings({ hasCompletedOnboarding: true })
finish = () => {
this.props.saveSettings({ hasCompletedOnboarding: true })
setTimeout(() => {
this.props.openModal(MODAL_DISCLAIMER)
}, MODAL_DISCLAIMER_DELAY)
}
savePassword = hash => {
this.props.saveSettings({
password: {
@ -147,6 +158,9 @@ class Onboarding extends PureComponent<Props> {
return (
<Container>
<TriggerAppReady />
<ExportLogsBtn hookToShortcut />
{step.options.showBreadcrumb && <OnboardingBreadcrumb />}
<StepContainer>
<StepComponent {...stepProps} />

6
src/components/Onboarding/steps/Analytics.js

@ -5,7 +5,7 @@ import styled from 'styled-components'
import { connect } from 'react-redux'
import { saveSettings } from 'actions/settings'
import Box from 'components/base/Box'
import CheckBox from 'components/base/CheckBox'
import Switch from 'components/base/Switch'
import TrackPage from 'analytics/TrackPage'
import Track from 'analytics/Track'
import { Title, Description, FixedTopContainer, StepContainerInner } from '../helperComponents'
@ -77,7 +77,7 @@ class Analytics extends PureComponent<StepProps, State> {
: 'Sentry Logs Disabled Onboarding'
}
/>
<CheckBox isChecked={sentryLogsToggle} onChange={this.handleSentryLogsToggle} />
<Switch isChecked={sentryLogsToggle} onChange={this.handleSentryLogsToggle} />
</Box>
</Container>
<Container>
@ -94,7 +94,7 @@ class Analytics extends PureComponent<StepProps, State> {
: 'Analytics Disabled Onboarding'
}
/>
<CheckBox isChecked={analyticsToggle} onChange={this.handleAnalyticsToggle} />
<Switch isChecked={analyticsToggle} onChange={this.handleAnalyticsToggle} />
</Box>
</Container>
</Box>

5
src/components/Onboarding/steps/Finish.js

@ -29,6 +29,7 @@ const ConfettiLayer = styled.div`
`
const socialMedia = [
// FIXME it should just be vdom in place
{
key: 'twitter',
url: 'https://twitter.com/LedgerHQ',
@ -37,7 +38,7 @@ const socialMedia = [
},
{
key: 'github',
url: 'https://github.com/LedgerHQ',
url: 'https://github.com/LedgerHQ/ledger-live-desktop',
icon: <IconSocialGithub size={24} />,
onClick: url => shell.openExternal(url),
},
@ -101,7 +102,7 @@ export default class Finish extends Component<StepProps, *> {
<Description>{t('onboarding:finish.desc')}</Description>
</Box>
<Box p={5}>
<Button primary padded onClick={() => finish()}>
<Button primary onClick={() => finish()}>
{t('onboarding:finish.openAppButton')}
</Button>
</Box>

96
src/components/Onboarding/steps/GenuineCheck/GenuineCheckErrorPage.js

@ -1,6 +1,6 @@
// @flow
import React, { Fragment } from 'react'
import React, { PureComponent, Fragment } from 'react'
import { i } from 'helpers/staticPath'
import type { T } from 'types/common'
@ -12,54 +12,80 @@ import TrackPage from 'analytics/TrackPage'
import { Title, Description, OnboardingFooterWrapper } from '../../helperComponents'
export function GenuineCheckErrorPage({
redoGenuineCheck,
contactSupport,
onboarding,
t,
}: {
type Props = {
t: T,
redoGenuineCheck: () => void,
contactSupport: () => void,
onboarding: OnboardingState,
t: T,
}) {
return (
<Box sticky pt={50}>
}
class GenuineCheckErrorPage extends PureComponent<Props, *> {
trackErrorPage = (page: string) => {
const { onboarding } = this.props
return (
<TrackPage
category="Onboarding"
name="Genuine Check Error Page"
name={`Genuine Check Error Page - ${page}`}
flowType={onboarding.flowType}
deviceType={onboarding.isLedgerNano ? 'Nano S' : 'Blue'}
/>
<Box grow alignItems="center" justifyContent="center">
{onboarding.isLedgerNano ? (
)
}
renderErrorPage = () => {
const { onboarding, t } = this.props
return (
<Fragment>
{onboarding.genuine.isGenuineFail ? (
<Fragment>
<Title>{t('onboarding:genuineCheck.errorPage.ledgerNano.title')}</Title>
<Description>{t('onboarding:genuineCheck.errorPage.ledgerNano.desc')}</Description>
<Box mt={5} mr={7}>
<img alt="" src={i('nano-error-onb.svg')} />
</Box>
{this.trackErrorPage('Not Genuine')}
<Title>{t('onboarding:genuineCheck.errorPage.title.isGenuineFail')}</Title>
<Description>{t('onboarding:genuineCheck.errorPage.desc.isGenuineFail')}</Description>
</Fragment>
) : !onboarding.genuine.pinStepPass ? (
<Fragment>
{this.trackErrorPage('PIN Step')}
<Title>{t('onboarding:genuineCheck.errorPage.title.pinFailed')}</Title>
<Description>{t('onboarding:genuineCheck.errorPage.desc.pinFailed')}</Description>
</Fragment>
) : (
<Fragment>
<Title>{t('onboarding:genuineCheck.errorPage.ledgerBlue.title')}</Title>
<Description pb={5}>
{t('onboarding:genuineCheck.errorPage.ledgerBlue.desc')}
{this.trackErrorPage('Recovery Phase Step')}
<Title>{t('onboarding:genuineCheck.errorPage.title.recoveryPhraseFailed')}</Title>
<Description>
{t('onboarding:genuineCheck.errorPage.desc.recoveryPhraseFailed')}
</Description>
<Box alignItems="center">
<img alt="" src={i('blue-error-onb.svg')} />
</Box>
</Fragment>
)}
<Box mt={5} mr={7}>
{onboarding.isLedgerNano ? (
<img alt="" src={i('nano-error-onb.svg')} />
) : (
<img alt="" src={i('blue-error-onb.svg')} />
)}
</Box>
</Fragment>
)
}
render() {
const { redoGenuineCheck, contactSupport, t } = this.props
return (
<Box sticky pt={50}>
<Box grow alignItems="center" justifyContent="center">
{this.renderErrorPage()}
</Box>
<OnboardingFooterWrapper>
<Button outlineGrey onClick={() => redoGenuineCheck()}>
{t('app:common.back')}
</Button>
<Button danger onClick={() => contactSupport()} ml="auto">
{t('onboarding:genuineCheck.buttons.contactSupport')}
</Button>
</OnboardingFooterWrapper>
</Box>
<OnboardingFooterWrapper>
<Button padded outlineGrey onClick={() => redoGenuineCheck()}>
{t('app:common.back')}
</Button>
<Button padded danger onClick={() => contactSupport()} ml="auto">
{t('onboarding:genuineCheck.buttons.contactSupport')}
</Button>
</OnboardingFooterWrapper>
</Box>
)
)
}
}
export default GenuineCheckErrorPage

5
src/components/Onboarding/steps/GenuineCheck/GenuineCheckUnavailable.js

@ -26,12 +26,11 @@ export function GenuineCheckUnavailableFooter({
}) {
return (
<OnboardingFooterWrapper>
<Button padded outlineGrey onClick={() => prevStep()}>
<Button outlineGrey onClick={() => prevStep()}>
{t('app:common.back')}
</Button>
<Box horizontal ml="auto">
<Button
padded
disabled={false}
event="Onboarding Skip Genuine Check"
onClick={() => nextStep()}
@ -39,7 +38,7 @@ export function GenuineCheckUnavailableFooter({
>
{t('app:common.skipThisStep')}
</Button>
<Button padded onClick={nextStep} disabled primary>
<Button onClick={nextStep} disabled primary>
{t('app:common.continue')}
</Button>
</Box>

25
src/components/Onboarding/steps/GenuineCheck/index.js

@ -26,7 +26,7 @@ import {
GenuineCheckCardWrapper,
} from '../../helperComponents'
import { GenuineCheckErrorPage } from './GenuineCheckErrorPage'
import GenuineCheckErrorPage from './GenuineCheckErrorPage'
import {
GenuineCheckUnavailableFooter,
GenuineCheckUnavailableMessage,
@ -81,14 +81,8 @@ class GenuineCheck extends PureComponent<StepProps, State> {
}
if (!item.pass) {
this.setState(INITIAL_STATE)
this.props.updateGenuineCheck({
displayErrorScreen: true,
pinStepPass: false,
recoveryStepPass: false,
isGenuineFail: false,
isDeviceGenuine: false,
genuineCheckUnavailable: null,
})
}
}
@ -137,7 +131,15 @@ class GenuineCheck extends PureComponent<StepProps, State> {
}
redoGenuineCheck = () => {
this.props.updateGenuineCheck({ displayErrorScreen: false })
this.setState(INITIAL_STATE)
this.props.updateGenuineCheck({
displayErrorScreen: false,
pinStepPass: false,
recoveryStepPass: false,
isGenuineFail: false,
isDeviceGenuine: false,
genuineCheckUnavailable: null,
})
}
contactSupport = () => {
@ -146,6 +148,11 @@ class GenuineCheck extends PureComponent<StepProps, State> {
shell.openExternal(contactSupportUrl)
}
handlePrevStep = () => {
const { prevStep, onboarding, jumpStep } = this.props
onboarding.flowType === 'initializedDevice' ? jumpStep('selectDevice') : prevStep()
}
renderGenuineFail = () => (
<GenuineCheckErrorPage
redoGenuineCheck={this.redoGenuineCheck}
@ -275,7 +282,7 @@ class GenuineCheck extends PureComponent<StepProps, State> {
<OnboardingFooter
t={t}
nextStep={nextStep}
prevStep={prevStep}
prevStep={this.handlePrevStep}
isContinueDisabled={!genuine.isDeviceGenuine}
/>
)}

2
src/components/Onboarding/steps/NoDevice.js

@ -65,7 +65,7 @@ class NoDevice extends PureComponent<StepProps, *> {
</Box>
</GrowScroll>
<OnboardingFooterWrapper>
<Button padded outlineGrey onClick={() => prevStep()} mr="auto">
<Button outlineGrey onClick={() => prevStep()} mr="auto">
{t('app:common.back')}
</Button>
</OnboardingFooterWrapper>

4
src/components/Onboarding/steps/SetPassword.js

@ -131,12 +131,11 @@ class SetPassword extends PureComponent<StepProps, State> {
</StepContainerInner>
<OnboardingFooterWrapper>
<Button padded outlineGrey onClick={() => prevStep()}>
<Button outlineGrey onClick={() => prevStep()}>
{t('app:common.back')}
</Button>
<Box horizontal ml="auto">
<Button
padded
event="Onboarding Skip Password"
onClick={() => nextStep()}
disabled={false}
@ -145,7 +144,6 @@ class SetPassword extends PureComponent<StepProps, State> {
{t('app:common.skipThisStep')}
</Button>
<Button
padded
onClick={this.handleSave}
disabled={!this.isValid() || !newPassword.length || !confirmPassword.length}
primary

2
src/components/Onboarding/steps/Start.js

@ -23,7 +23,7 @@ export default (props: StepProps) => {
<Box my={5}>
<Title>{t('onboarding:start.title')}</Title>
</Box>
<Button padded primary onClick={() => jumpStep('init')}>
<Button primary onClick={() => jumpStep('init')}>
{t('onboarding:start.startBtn')}
</Button>
</Box>

28
src/components/OnboardingOrElse.js

@ -0,0 +1,28 @@
// @flow
import React, { PureComponent } from 'react'
import { connect } from 'react-redux'
import { createStructuredSelector } from 'reselect'
import { hasCompletedOnboardingSelector } from 'reducers/settings'
import Onboarding from './Onboarding'
type Props = {
hasCompletedOnboarding: boolean,
children: *,
}
class OnboardingOrElse extends PureComponent<Props> {
render() {
const { hasCompletedOnboarding, children } = this.props
if (hasCompletedOnboarding) {
return children
}
return <Onboarding />
}
}
export default connect(
createStructuredSelector({
hasCompletedOnboarding: hasCompletedOnboardingSelector,
}),
)(OnboardingOrElse)

104
src/components/OperationsList/index.js

@ -24,7 +24,6 @@ import IconAngleDown from 'icons/AngleDown'
import Box, { Card } from 'components/base/Box'
import Text from 'components/base/Text'
import Defer from 'components/base/Defer'
import Track from 'analytics/Track'
import SectionTitle from './SectionTitle'
@ -100,61 +99,56 @@ export class OperationsList extends PureComponent<Props, State> {
const accountsMap = accounts ? keyBy(accounts, 'id') : { [account.id]: account }
return (
<Defer>
<Box flow={4}>
{title && (
<Text color="dark" ff="Museo Sans" fontSize={6}>
{title}
<Box flow={4}>
{title && (
<Text color="dark" ff="Museo Sans" fontSize={6}>
{title}
</Text>
)}
{groupedOperations.sections.map(group => (
<Box flow={2} key={group.day.toISOString()}>
<SectionTitle day={group.day} />
<Card p={0}>
{group.data.map(operation => {
const account = accountsMap[operation.accountId]
if (!account) {
return null
}
return (
<OperationC
operation={operation}
account={account}
key={`${account.id}_${operation.id}`}
onOperationClick={this.handleClickOperation}
t={t}
withAccount={withAccount}
/>
)
})}
</Card>
</Box>
))}
{groupedOperations.completed ? (
<Track
onMount
event="OperationsListEndReached"
totalSections={groupedOperations.sections.length}
totalOperations={groupedOperations.sections.reduce((sum, s) => sum + s.data.length, 0)}
/>
) : null}
{!groupedOperations.completed ? (
<ShowMore onClick={this.fetchMoreOperations}>
<span>{t('app:common.showMore')}</span>
<IconAngleDown size={12} />
</ShowMore>
) : (
<Box p={6} align="center">
<Text ff="Open Sans" fontSize={3}>
{t('app:operationList.noMoreOperations')}
</Text>
)}
{groupedOperations.sections.map(group => (
<Box flow={2} key={group.day.toISOString()}>
<SectionTitle day={group.day} />
<Card p={0}>
{group.data.map(operation => {
const account = accountsMap[operation.accountId]
if (!account) {
return null
}
return (
<OperationC
operation={operation}
account={account}
key={`${account.id}_${operation.id}`}
onOperationClick={this.handleClickOperation}
t={t}
withAccount={withAccount}
/>
)
})}
</Card>
</Box>
))}
{groupedOperations.completed ? (
<Track
onMount
event="OperationsListEndReached"
totalSections={groupedOperations.sections.length}
totalOperations={groupedOperations.sections.reduce(
(sum, s) => sum + s.data.length,
0,
)}
/>
) : null}
{!groupedOperations.completed ? (
<ShowMore onClick={this.fetchMoreOperations}>
<span>{t('app:common.showMore')}</span>
<IconAngleDown size={12} />
</ShowMore>
) : (
<Box p={6} align="center">
<Text ff="Open Sans" fontSize={3}>
{t('app:operationList.noMoreOperations')}
</Text>
</Box>
)}
</Box>
</Defer>
</Box>
)}
</Box>
)
}
}

10
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,
@ -98,14 +100,14 @@ ${error.stack}
</Box>
<Space of={30} />
<Box horizontal flow={2}>
<Button primary onClick={this.handleRestart}>
<Button small primary onClick={this.handleRestart}>
{t('app:crash.restart')}
</Button>
<ExportLogsBtn withoutAppData={withoutAppData} />
<Button primary onClick={this.handleCreateIssue}>
<Button small primary onClick={this.handleCreateIssue}>
{t('app:crash.createTicket')}
</Button>
<Button danger onClick={this.handleOpenHardResetModal}>
<Button small danger onClick={this.handleOpenHardResetModal}>
{t('app:crash.reset')}
</Button>
</Box>

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 small 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 Switch from 'components/base/Switch'
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'} />
<Switch 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 Switch from 'components/base/Switch'
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 small onClick={this.handleOpenPasswordModal}>
{t('app:settings.profile.changePassword')}
</Button>
)}
<Switch 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),
)

4
src/components/SettingsPage/DisablePasswordModal.js

@ -98,12 +98,12 @@ class DisablePasswordModal extends PureComponent<Props, State> {
</Box>
</ModalContent>
<ModalFooter horizontal align="center" justify="flex-end" flow={2}>
<Button type="button" padded onClick={onClose}>
<Button small type="Button small" onClick={onClose}>
{t('app:common.cancel')}
</Button>
<Button
small
primary
padded
onClick={this.disablePassword}
disabled={!currentPassword && !incorrectPassword}
>

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),
)

4
src/components/SettingsPage/PasswordModal.js

@ -111,11 +111,11 @@ class PasswordModal extends PureComponent<Props, State> {
/>
</ModalContent>
<ModalFooter horizontal align="center" justify="flex-end" flow={2}>
<Button type="button" padded onClick={onClose}>
<Button small type="Button small" onClick={onClose}>
{t('app:common.cancel')}
</Button>
<Button
padded
small
primary
onClick={this.handleSave}
disabled={!this.isValid() || !newPassword.length || !confirmPassword.length}

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)

46
src/components/SettingsPage/ReleaseNotesButton.js

@ -0,0 +1,46 @@
// @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
small
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 small 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;
align-items: 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 Switch from 'components/base/Switch'
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'} />
<Switch 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 Switch from 'components/base/Switch'
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'} />
<Switch 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)

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

@ -1,20 +1,17 @@
// @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 resolveLogsDirectory from 'helpers/resolveLogsDirectory'
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 +22,62 @@ 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', { logsDirectory: resolveLogsDirectory() })}
>
<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;
`

7
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'
@ -38,7 +37,7 @@ class TabProfile extends PureComponent<*, *> {
return (
<Card flow={3}>
<Box horizontal>
<Button onClick={this.onQRCodeMobileExport} primary>
<Button small onClick={this.onQRCodeMobileExport} primary>
QRCode Mobile Export
</Button>
@ -53,4 +52,4 @@ class TabProfile extends PureComponent<*, *> {
}
}
export default TabProfile
export default translate()(TabProfile)

65
src/components/SyncContinouslyPendingOperations.js

@ -0,0 +1,65 @@
// @flow
// Sync continuously the accounts that have pending operations
import React, { Component } from 'react'
import logger from 'logger'
import { createStructuredSelector, createSelector } from 'reselect'
import { connect } from 'react-redux'
import type { Account } from '@ledgerhq/live-common/lib/types'
import { BridgeSyncConsumer } from 'bridge/BridgeSyncContext'
import type { Sync } from 'bridge/BridgeSyncContext'
import { accountsSelector } from 'reducers/accounts'
const accountsWithPendingOperationsSelector = createSelector(accountsSelector, accounts =>
accounts.filter(a => a.pendingOperations.length > 0),
)
class SyncContPendingOpsConnected extends Component<{
sync: Sync,
accounts: Account[],
priority: number,
interval: number,
}> {
componentDidMount() {
this.timeout = setTimeout(this.check, this.props.interval)
}
componentWillUnmount() {
clearTimeout(this.timeout)
}
check = () => {
const { sync, accounts, priority, interval } = this.props
setTimeout(this.check, interval)
if (accounts.length > 0) {
logger.log(`SyncContinouslyPendingOperations: found ${accounts.length} accounts`, accounts)
sync({
type: 'SYNC_SOME_ACCOUNTS',
accountIds: accounts.map(a => a.id),
priority,
})
}
}
timeout: *
render() {
return null
}
}
const Effect = connect(
createStructuredSelector({
accounts: accountsWithPendingOperationsSelector,
}),
)(SyncContPendingOpsConnected)
const SyncContinuouslyPendingOperations = ({
priority,
interval,
}: {
priority: number,
interval: number,
}) => (
<BridgeSyncConsumer>
{sync => <Effect sync={sync} interval={interval} priority={priority} />}
</BridgeSyncConsumer>
)
export default SyncContinuouslyPendingOperations

20
src/components/WarnBox/index.js

@ -20,27 +20,29 @@ const Container = styled(Box).attrs({
align-items: center;
`
const svg = (
<svg width="36" height="43">
<g fill="none">
export const HandShield = ({ size }: { size: number }) => (
<svg width={size} height={size} viewBox="0 0 50.8 56.914">
<g transform="translate(-.6 -.48644)" fill="none">
<path
fill="#4B84FF"
d="m26.25 2c-0.167 30.976-0.25 49.258-0.25 54.843h0.5c13.25-3.1 23.5-15.976 23.5-29.806v-15.022z"
fill="#4b84ff"
fillOpacity=".08"
d="M18.177 2C18.06 24.126 18 37.184 18 41.174h.354C27.74 38.96 35 29.762 35 19.884V9.154L18.177 2z"
/>
<path
d="m26 2-24 9.86v14.792c0 13.618 10.105 26.296 23.747 29.348h0.506c13.39-3.052 23.747-15.73 23.747-29.348v-14.791z"
stroke="#142533"
strokeWidth="2"
d="M18 2L1 9.153v10.73c0 9.88 7.158 19.077 16.821 21.29h.358C27.663 38.96 35 29.764 35 19.884V9.154L18 2z"
strokeWidth="2.8"
/>
<path
fill="#4B84FF"
d="M23.733 15.036c-.568 0-1.03.448-1.03 1.001l-.019 4.488s.002.313-.308.313c-.316 0-.307-.313-.307-.313v-6.474c0-.553-.456-.982-1.024-.982-.57 0-.974.43-.974.982v6.474s-.035.316-.34.316c-.303 0-.327-.316-.327-.316v-7.553c0-.552-.428-.972-.996-.972-.569 0-1 .42-1 .972v7.553s-.016.303-.344.303c-.321 0-.323-.303-.323-.303v-5.611c0-.553-.445-.9-1.013-.9-.57 0-.985.347-.985.9v8.2s-.056.365-.594-.237c-1.378-1.543-2.097-1.85-2.097-1.85s-1.31-.622-1.889.503c-.42.816.025.86.712 1.861.607.888 2.529 3.221 2.529 3.221s2.28 3.126 5.355 3.126c0 0 6.024.25 6.024-5.544l-.021-8.157c0-.553-.46-1.001-1.03-1.001"
d="m33.58 20.108c-0.769 0-1.394 0.606-1.394 1.354l-0.025 6.072s3e-3 0.424-0.416 0.424c-0.428 0-0.417-0.424-0.417-0.424v-8.76c0-0.747-0.616-1.327-1.385-1.327-0.77 0-1.318 0.58-1.318 1.328v8.759s-0.047 0.428-0.46 0.428c-0.41 0-0.441-0.428-0.441-0.428v-10.219c0-0.747-0.58-1.315-1.349-1.315-0.77 0-1.353 0.568-1.353 1.315v10.219s-0.022 0.41-0.465 0.41c-0.435 0-0.437-0.41-0.437-0.41v-7.591c0-0.748-0.602-1.217-1.37-1.217-0.77 0-1.333 0.469-1.333 1.217v11.094s-0.076 0.493-0.803-0.321c-1.865-2.087-2.838-2.502-2.838-2.502s-1.77-0.843-2.555 0.68c-0.568 1.103 0.034 1.162 0.963 2.518 0.822 1.2 3.421 4.357 3.421 4.357s3.084 4.229 7.245 4.229c0 0 8.15 0.338 8.15-7.5l-0.028-11.036c0-0.748-0.623-1.354-1.392-1.354"
fill="#4b84ff"
/>
</g>
</svg>
)
const svg = <HandShield size={43} />
type Props = {
children: React.Node,
}

4
src/components/base/AccountsList/AccountRow.js

@ -7,7 +7,7 @@ import type { Account } from '@ledgerhq/live-common/lib/types'
import { darken } from 'styles/helpers'
import Box, { Tabbable } from 'components/base/Box'
import Radio from 'components/base/Radio'
import CheckBox from 'components/base/CheckBox'
import CryptoCurrencyIcon from 'components/CryptoCurrencyIcon'
import FormattedVal from 'components/base/FormattedVal'
import Input from 'components/base/Input'
@ -103,7 +103,7 @@ export default class AccountRow extends PureComponent<Props> {
/>
) : null}
{!isDisabled ? (
<Radio disabled isChecked={isChecked || !!isDisabled} />
<CheckBox disabled isChecked={isChecked || !!isDisabled} />
) : (
<div style={{ width: 20 }} />
)}

10
src/components/base/Button/index.js

@ -140,9 +140,9 @@ function getStyles(props, state) {
const Base = styled.button.attrs({
ff: 'Museo Sans|Regular',
fontSize: p => p.fontSize || 3,
px: p => (p.padded ? 4 : 2),
py: p => (p.padded ? 2 : 0),
fontSize: p => p.fontSize || (!p.small ? 4 : 3),
px: p => (!p.small ? 4 : 3),
py: p => (!p.small ? 2 : 0),
color: 'grey',
bg: 'transparent',
})`
@ -154,7 +154,7 @@ const Base = styled.button.attrs({
border: none;
border-radius: ${p => p.theme.radii[1]}px;
cursor: ${p => (p.disabled ? 'default' : 'pointer')};
height: ${p => (p.small ? 30 : p.padded ? 40 : 36)}px;
height: ${p => (p.small ? 34 : 40)}px;
pointer-events: ${p => (p.disabled ? 'none' : '')};
outline: none;
@ -179,7 +179,6 @@ type Props = {
disabled?: boolean,
onClick?: Function,
small?: boolean,
padded?: boolean,
isLoading?: boolean,
event?: string,
eventProperties?: Object,
@ -195,7 +194,6 @@ class Button extends PureComponent<
onClick: noop,
primary: false,
small: false,
padded: false,
danger: false,
}

47
src/components/base/CheckBox/index.js

@ -1,35 +1,32 @@
// @flow
import React from 'react'
import noop from 'lodash/noop'
import styled from 'styled-components'
import Check from 'icons/Check'
import { Tabbable } from 'components/base/Box'
const Base = styled(Tabbable).attrs({
bg: p => (p.isChecked ? 'wallet' : 'lightFog'),
horizontal: true,
relative: true,
align: 'center',
justifyContent: 'center',
})`
backround: red;
width: 50px;
height: 26px;
border-radius: 13px;
transition: 250ms linear background-color;
cursor: pointer;
outline: none;
border-radius: 4px;
background-color: ${p => (p.isChecked ? p.theme.colors.wallet : p.theme.colors.white)};
border: 1px solid;
border-color: ${p => (p.isChecked ? p.theme.colors.wallet : p.theme.colors.fog)};
color: ${p => p.theme.colors.white};
height: 18px;
width: 18px;
transition: all ease-in-out 0.1s;
&:focus {
outline: none;
box-shadow: 0 0 4px 1px ${p => p.theme.colors.wallet};
border-color: ${p => p.theme.colors.wallet};
}
&:hover {
border-color: ${p => p.theme.colors.wallet};
}
`
const Ball = styled.div`
width: 20px;
height: 20px;
border-radius: 50%;
background: white;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.2);
transition: 250ms ease-in-out transform;
transform: translate3d(${p => (p.isChecked ? '27px' : '3px')}, 0, 0);
`
type Props = {
@ -38,16 +35,12 @@ type Props = {
}
function CheckBox(props: Props) {
const { isChecked, onChange, ...p } = props
const { isChecked, onChange } = props
return (
<Base isChecked={isChecked} onClick={() => onChange && onChange(!isChecked)} {...p}>
<Ball isChecked={isChecked} />
<Base {...props} isChecked={isChecked} onClick={() => onChange && onChange(!isChecked)}>
<Check size={12} />
</Base>
)
}
CheckBox.defaultProps = {
onChange: noop,
}
export default CheckBox

4
src/components/base/CheckBox/stories.js

@ -1,12 +1,12 @@
import React from 'react'
import { storiesOf } from '@storybook/react'
import { boolean } from '@storybook/addon-knobs'
import { action } from '@storybook/addon-actions'
import { boolean } from '@storybook/addon-knobs'
import CheckBox from 'components/base/CheckBox'
const stories = storiesOf('Components/base', module)
stories.add('CheckBox', () => (
<CheckBox isChecked={boolean('isChecked', false)} onChange={action('onChange')} />
<CheckBox isChecked={boolean('checked', false)} onChange={action('onChange')} />
))

40
src/components/base/Defer.js

@ -1,40 +0,0 @@
// @flow
import { PureComponent } from 'react'
type Props = {
children: any,
}
type State = {
shouldRender: boolean,
}
class Defer extends PureComponent<Props, State> {
state = {
shouldRender: false,
}
componentDidMount() {
this._mounted = true
window.requestAnimationFrame(() =>
window.requestAnimationFrame(() => this._mounted && this.setState({ shouldRender: true })),
)
}
componentWillUnmount() {
this._mounted = false
}
_mounted = false
render() {
const { children } = this.props
const { shouldRender } = this.state
return shouldRender ? children : null
}
}
export default Defer

4
src/components/base/GrowScroll/index.js

@ -52,6 +52,8 @@ class GrowScroll extends PureComponent<Props> {
overflowY: 'scroll',
marginRight: `-${80 + scrollbarWidth}px`,
paddingRight: `80px`,
display: 'flex',
flexDirection: 'column',
...(maxHeight
? {
maxHeight,
@ -68,7 +70,7 @@ class GrowScroll extends PureComponent<Props> {
return (
<div style={rootStyles}>
<div style={scrollContainerStyles} ref={this.onScrollContainerRef}>
<Box {...props}>
<Box grow {...props}>
<GrowScrollContext.Provider value={this.valueProvider}>
{children}
</GrowScrollContext.Provider>

9
src/components/base/Input/index.js

@ -151,9 +151,12 @@ class Input extends PureComponent<Props, State> {
onBlur(e)
}
handleSelectEverything = () => {
this._input && this._input.setSelectionRange(0, this._input.value.length)
this._input && this._input.focus()
select = () => {
const { _input } = this
if (_input) {
_input.select()
_input.focus()
}
}
_input = null

11
src/components/base/InputCurrency/index.js

@ -190,6 +190,16 @@ class InputCurrency extends PureComponent<Props, State> {
)
}
select = () => {
// TODO we should fowardRef so InputCurrency ref is on Input
this.input && this.input.select()
}
input: ?Input
onRef = (input: ?Input) => {
this.input = input
}
render() {
const { renderRight, showAllDigits, unit, subMagnitude } = this.props
const { displayValue } = this.state
@ -198,6 +208,7 @@ class InputCurrency extends PureComponent<Props, State> {
<Input
{...this.props}
ff="Rubik"
ref={this.onRef}
value={displayValue}
onChange={this.handleChange}
onFocus={this.handleFocus}

3
src/components/base/Modal/index.js

@ -19,7 +19,6 @@ import { closeModal, isModalOpened, getModalData } from 'reducers/modals'
import Box from 'components/base/Box'
import GrowScroll from 'components/base/GrowScroll'
import Defer from 'components/base/Defer'
export { default as ModalBody } from './ModalBody'
export { default as ConfirmModal } from './ConfirmModal'
@ -126,7 +125,7 @@ class Pure extends Component<any> {
render() {
const { data, onClose, render } = this.props
return <Defer>{render({ data, onClose })}</Defer>
return render({ data, onClose })
}
}

15
src/components/base/Progress/index.js

@ -6,10 +6,17 @@ import Box from 'components/base/Box'
const inifiteAnimation = keyframes`
0% {
left: 0
left: -18%;
width: 20%;
}
50% {
left: 98%;
}
100% {
left: 102%
left: -18%;
width: 20%;
}
`
@ -45,7 +52,7 @@ const Progression = styled(Bar).attrs({
${p =>
p.infinite
? `
animation: 1000ms ${inifiteAnimation} ease-out infinite;
animation: ${p.timing}ms ${inifiteAnimation} infinite;
`
: `
animation: ${p.timing}ms ${fillInAnimation} ease-out;
@ -70,7 +77,7 @@ class Progress extends Component<Props, State> {
render() {
const { infinite, color, timing } = this.props
const styles = infinite ? { width: '20%' } : { width: '100%' }
const styles = infinite ? { width: '0%' } : { width: '100%' }
return (
<Bar>
<Progression infinite={infinite} color={color} style={styles} timing={timing} />

25
src/components/base/Progress/stories.js

@ -0,0 +1,25 @@
// @flow
import React from 'react'
import { storiesOf } from '@storybook/react'
import { text, number, boolean } from '@storybook/addon-knobs'
import Progress from 'components/base/Progress'
const stories = storiesOf('Components/base', module)
stories.add('Progress (infinite)', () => (
<Progress
infinite={boolean('infinite', true)}
timing={number('timing (ms)', 3000)}
color={text('color (css or from theme)', 'wallet')}
/>
))
stories.add('Progress', () => (
<Progress
infinite={boolean('infinite', false)}
timing={number('timing (ms)', 3000)}
color={text('color (css or from theme)', 'wallet')}
/>
))

1
src/components/base/Select/index.js

@ -75,6 +75,7 @@ class Select extends PureComponent<Props> {
isLoading={isLoading}
isClearable={isClearable}
isSearchable={isSearchable}
menuPlacement="auto"
blurInputOnSelect={false}
backspaceRemovesValue
menuShouldBlockScroll

53
src/components/base/Switch/index.js

@ -0,0 +1,53 @@
// @flow
import React from 'react'
import noop from 'lodash/noop'
import styled from 'styled-components'
import { Tabbable } from 'components/base/Box'
const Base = styled(Tabbable).attrs({
bg: p => (p.isChecked ? 'wallet' : 'lightFog'),
horizontal: true,
align: 'center',
})`
backround: red;
width: 50px;
height: 26px;
border-radius: 13px;
transition: 250ms linear background-color;
cursor: pointer;
&:focus {
outline: none;
}
`
const Ball = styled.div`
width: 20px;
height: 20px;
border-radius: 50%;
background: white;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.2);
transition: 250ms ease-in-out transform;
transform: translate3d(${p => (p.isChecked ? '27px' : '3px')}, 0, 0);
`
type Props = {
isChecked: boolean,
onChange?: Function,
}
function Switch(props: Props) {
const { isChecked, onChange, ...p } = props
return (
<Base isChecked={isChecked} onClick={() => onChange && onChange(!isChecked)} {...p}>
<Ball isChecked={isChecked} />
</Base>
)
}
Switch.defaultProps = {
onChange: noop,
}
export default Switch

12
src/components/base/Switch/stories.js

@ -0,0 +1,12 @@
import React from 'react'
import { storiesOf } from '@storybook/react'
import { boolean } from '@storybook/addon-knobs'
import { action } from '@storybook/addon-actions'
import Switch from 'components/base/Switch'
const stories = storiesOf('Components/base', module)
stories.add('Switch', () => (
<Switch isChecked={boolean('isChecked', false)} onChange={action('onChange')} />
))

4
src/components/layout/Default.js

@ -5,6 +5,7 @@ import { compose } from 'redux'
import styled from 'styled-components'
import { Route, withRouter } from 'react-router'
import { translate } from 'react-i18next'
import { SYNC_PENDING_INTERVAL } from 'config/constants'
import type { Location } from 'react-router'
@ -26,6 +27,7 @@ import AppRegionDrag from 'components/AppRegionDrag'
import IsUnlocked from 'components/IsUnlocked'
import SideBar from 'components/MainSideBar'
import TopBar from 'components/TopBar'
import SyncContinuouslyPendingOperations from '../SyncContinouslyPendingOperations'
const Main = styled(GrowScroll).attrs({
px: 6,
@ -83,6 +85,8 @@ class Default extends Component<Props> {
<ModalComponent key={name} />
))}
<SyncContinuouslyPendingOperations priority={20} interval={SYNC_PENDING_INTERVAL} />
<div id="sticky-back-to-top-root" />
<Box grow horizontal bg="white">

4
src/components/modals/AccountSettingRenderBody.js

@ -275,10 +275,10 @@ class HelperComp extends PureComponent<Props, State> {
</Spoiler>
</ModalContent>
<ModalFooter horizontal>
<Button padded danger type="button" onClick={this.handleOpenRemoveAccountModal}>
<Button danger type="button" onClick={this.handleOpenRemoveAccountModal}>
{t('app:common.delete')}
</Button>
<Button padded ml="auto" type="submit" primary>
<Button ml="auto" type="submit" primary>
{t('app:common.apply')}
</Button>
</ModalFooter>

4
src/components/modals/AddAccounts/index.js

@ -28,7 +28,7 @@ import { validateNameEdition } from 'helpers/accountName'
import StepChooseCurrency, { StepChooseCurrencyFooter } from './steps/01-step-choose-currency'
import StepConnectDevice, { StepConnectDeviceFooter } from './steps/02-step-connect-device'
import StepImport, { StepImportFooter } from './steps/03-step-import'
import StepFinish from './steps/04-step-finish'
import StepFinish, { StepFinishFooter } from './steps/04-step-finish'
const createSteps = ({ t }: { t: T }) => {
const onBack = ({ transitionTo, resetScanState }: StepProps) => {
@ -64,7 +64,7 @@ const createSteps = ({ t }: { t: T }) => {
id: 'finish',
label: t('app:addAccounts.breadcrumb.finish'),
component: StepFinish,
footer: null,
footer: StepFinishFooter,
onBack: null,
hideFooter: true,
},

57
src/components/modals/AddAccounts/steps/04-step-finish.js

@ -1,32 +1,57 @@
// @flow
import React from 'react'
import React, { Fragment } from 'react'
import styled from 'styled-components'
import TrackPage from 'analytics/TrackPage'
import Box from 'components/base/Box'
import Button from 'components/base/Button'
import IconCheckCircle from 'icons/CheckCircle'
import IconCheckFull from 'icons/CheckFull'
import { CurrencyCircleIcon } from '../../../base/CurrencyBadge'
import type { StepProps } from '../index'
function StepFinish({ onCloseModal, onGoStep1, t }: StepProps) {
const Title = styled(Box).attrs({
ff: 'Museo Sans',
fontSize: 5,
mt: 2,
color: 'dark',
})`
text-align: center;
`
const Text = styled(Box).attrs({
ff: 'Open Sans',
fontSize: 4,
mt: 2,
})`
text-align: center;
`
function StepFinish({ currency, t, checkedAccountsIds }: StepProps) {
return (
<Box align="center" py={6}>
<TrackPage category="AddAccounts" name="Step4" />
<Box color="positiveGreen">
<IconCheckCircle size={40} />
</Box>
<Box p={4}>{t('app:addAccounts.success')}</Box>
<Box horizontal>
<Button mr={2} outline onClick={onGoStep1}>
{t('app:addAccounts.cta.addMore')}
</Button>
<Button primary onClick={onCloseModal}>
{t('app:common.close')}
</Button>
</Box>
{currency ? (
<Box color="positiveGreen" style={{ position: 'relative' }}>
<CurrencyCircleIcon size={50} currency={currency} />
<IconCheckFull size={18} style={{ position: 'absolute', top: 0, right: 0 }} />
</Box>
) : null}
<Title>{t('app:addAccounts.success', { count: checkedAccountsIds.length })}</Title>
<Text>{t('app:addAccounts.successDescription', { count: checkedAccountsIds.length })}</Text>
</Box>
)
}
export default StepFinish
export const StepFinishFooter = ({ onCloseModal, onGoStep1, t }: StepProps) => (
<Fragment>
<Button mr={2} outline onClick={onGoStep1}>
{t('app:addAccounts.cta.addMore')}
</Button>
<Button primary onClick={onCloseModal}>
{t('app:common.close')}
</Button>
</Fragment>
)

121
src/components/modals/Debug.js

@ -1,18 +1,23 @@
// @flow
/* eslint-disable react/jsx-no-literals */
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { createStructuredSelector } from 'reselect'
import { getCryptoCurrencyById } from '@ledgerhq/live-common/lib/helpers/currencies'
import last from 'lodash/last'
import React, { Component } from 'react'
import Modal, { ModalBody, ModalTitle, ModalContent } from 'components/base/Modal'
import { getCurrentDevice } from 'reducers/devices'
import Button from 'components/base/Button'
import Box from 'components/base/Box'
import Input from 'components/base/Input'
import EnsureDevice from 'components/EnsureDevice'
import { getDerivations } from 'helpers/derivations'
import getAddress from 'commands/getAddress'
import testInterval from 'commands/testInterval'
import testCrash from 'commands/testCrash'
import testApdu from 'commands/testApdu'
import ping from 'commands/ping'
import libcoreGetVersion from 'commands/libcoreGetVersion'
import SyncSkipUnderPriority from '../SyncSkipUnderPriority'
class Debug extends Component<*, *> {
state = {
@ -37,18 +42,13 @@ class Debug extends Component<*, *> {
const currency = getCryptoCurrencyById('bitcoin')
const derivation = last(getDerivations(currency))
for (let x = 0; x < 20; x++) {
const obj = {
path: derivation({ currency, segwit: true, x }),
currencyId: currency.id,
devicePath: device.path,
}
// we start one in parallel just to stress device even more. this test race condition!
getAddress.send(obj)
getAddress.send(obj)
const { address, path } = await getAddress.send(obj).toPromise()
const { address, path } = await getAddress
.send({
path: derivation({ currency, segwit: true, x }),
currencyId: currency.id,
devicePath: device.path,
})
.toPromise()
this.log(`derivated ${path} = ${address}`)
}
} catch (e) {
@ -72,6 +72,38 @@ class Debug extends Component<*, *> {
.subscribe(o => this.log(o.responseHex), e => this.error(e))
}
benchmark = (device: *) => async () => {
const run = async (name, job) => {
const before = window.performance.now()
const res = await job()
const after = window.performance.now()
this.log(
`benchmark: ${Math.round((after - before) * 100) / 100}ms: ${name} => ${String(res)}`,
)
}
await run('ping process', () => ping.send().toPromise())
await run('libcore version', () =>
libcoreGetVersion
.send()
.toPromise()
.then(o => o.stringVersion),
)
const currency = getCryptoCurrencyById('bitcoin')
const derivation = last(getDerivations(currency))
const obj = {
path: derivation({ currency, segwit: true, x: 0 }),
currencyId: currency.id,
devicePath: device.path,
}
await run('getAddress', () =>
getAddress
.send(obj)
.toPromise()
.then(o => o.address),
)
}
log = (txt: string) => {
this.setState(({ logs }) => ({ logs: logs.concat({ txt, type: 'log' }) }))
}
@ -83,6 +115,7 @@ class Debug extends Component<*, *> {
}
render() {
const { device } = this.props
const { logs } = this.state
return (
<Modal
@ -90,18 +123,24 @@ class Debug extends Component<*, *> {
onHide={this.onHide}
render={({ onClose }: *) => (
<ModalBody onClose={onClose}>
<SyncSkipUnderPriority priority={99999999} />
<ModalTitle>developer internal tools</ModalTitle>
<ModalContent>
<Box style={{ height: 60, overflow: 'auto' }}>
<Box horizontal style={{ padding: 10 }}>
<EnsureDevice>
{device => (
<Button onClick={this.onClickStressDevice(device)} primary>
Stress getAddress (BTC)
</Button>
)}
</EnsureDevice>
</Box>
{device && (
<Box horizontal style={{ padding: 10 }}>
<Button onClick={this.benchmark(device)} primary>
Benchmark
</Button>
</Box>
)}
{device && (
<Box horizontal style={{ padding: 10 }}>
<Button onClick={this.onClickStressDevice(device)} primary>
Derivate BTC addresses
</Button>
</Box>
)}
<Box horizontal style={{ padding: 10 }}>
<Button onClick={this.onCrash} danger>
crash process
@ -113,22 +152,20 @@ class Debug extends Component<*, *> {
</Button>
<Button onClick={this.cancelAllPeriods}>Cancel</Button>
</Box>
<EnsureDevice>
{device => (
<Box horizontal style={{ padding: 10 }}>
<Box grow>
<Input
placeholder="APDU hex ( e.g. E016000000 )"
value={this.state.apdu}
onChange={apdu => this.setState({ apdu })}
/>
</Box>
<Button onClick={this.runApdu(device)} primary>
RUN
</Button>
</Box>
)}
</EnsureDevice>
device && (
<Box horizontal style={{ padding: 10 }}>
<Box grow>
<Input
placeholder="APDU hex ( e.g. E016000000 )"
value={this.state.apdu}
onChange={apdu => this.setState({ apdu })}
/>
</Box>
<Button onClick={this.runApdu(device)} primary>
RUN
</Button>
</Box>
)
</Box>
<Box
style={{
@ -167,4 +204,8 @@ class Debug extends Component<*, *> {
}
}
export default Debug
export default connect(
createStructuredSelector({
device: getCurrentDevice,
}),
)(Debug)

48
src/components/modals/Disclaimer.js

@ -0,0 +1,48 @@
// @flow
import React, { PureComponent } from 'react'
import { translate } from 'react-i18next'
import type { T } from 'types/common'
import { MODAL_DISCLAIMER } from 'config/constants'
import Modal, { ModalBody, ModalTitle, ModalContent, ModalFooter } from 'components/base/Modal'
import Button from 'components/base/Button'
import Box from 'components/base/Box'
import { HandShield } from 'components/WarnBox'
type Props = {
t: T,
}
class DisclaimerModal extends PureComponent<Props> {
render() {
const { t } = this.props
return (
<Modal
name={MODAL_DISCLAIMER}
render={({ onClose }) => (
<ModalBody onClose={onClose}>
<ModalTitle>{t('app:disclaimerModal.title')}</ModalTitle>
<ModalContent flow={4} ff="Open Sans|Regular" fontSize={4} color="smoke">
<Box align="center" mt={4} pb={4}>
<HandShield size={55} />
</Box>
<p>{t('app:disclaimerModal.desc_1')}</p>
<p>{t('app:disclaimerModal.desc_2')}</p>
</ModalContent>
<ModalFooter horizontal justifyContent="flex-end">
<Button onClick={onClose} primary>
{t('app:disclaimerModal.cta')}
</Button>
</ModalFooter>
</ModalBody>
)}
/>
)
}
}
export default translate()(DisclaimerModal)

2
src/components/modals/OperationDetails.js

@ -229,7 +229,7 @@ const OperationDetails = connect(mapStateToProps)((props: Props) => {
{url && (
<ModalFooter horizontal justify="flex-end" flow={2}>
<Button primary padded onClick={() => shell.openExternal(url)}>
<Button primary onClick={() => shell.openExternal(url)}>
{t('app:operationDetails.viewOperation')}
</Button>
</ModalFooter>

16
src/components/modals/Receive/index.js

@ -41,6 +41,7 @@ type State = {
isAddressVerified: ?boolean,
disabledSteps: number[],
errorSteps: number[],
verifyAddressError: ?Error,
}
export type StepProps = DefaultStepProps & {
@ -49,12 +50,13 @@ export type StepProps = DefaultStepProps & {
closeModal: void => void,
isAppOpened: boolean,
isAddressVerified: ?boolean,
verifyAddressError: ?Error,
onRetry: void => void,
onSkipConfirm: void => void,
onResetSkip: void => void,
onChangeAccount: (?Account) => void,
onChangeAppOpened: boolean => void,
onChangeAddressVerified: (?boolean) => void,
onChangeAddressVerified: (?boolean, ?Error) => void,
}
const createSteps = ({ t }: { t: T }) => [
@ -102,6 +104,7 @@ const INITIAL_STATE = {
isAddressVerified: null,
disabledSteps: [],
errorSteps: [],
verifyAddressError: null,
}
class ReceiveModal extends PureComponent<Props, State> {
@ -114,7 +117,7 @@ class ReceiveModal extends PureComponent<Props, State> {
if (!account) {
if (data && data.account) {
this.setState({ account: data.account })
this.setState({ account: data.account, stepId: 'device' })
} else {
this.setState({ account: accounts[0] })
}
@ -127,16 +130,17 @@ class ReceiveModal extends PureComponent<Props, State> {
handleStepChange = step => this.setState({ stepId: step.id })
handleChangeAccount = (account: ?Account) => this.setState({ account })
handleChangeAppOpened = (isAppOpened: boolean) => this.setState({ isAppOpened })
handleChangeAddressVerified = (isAddressVerified: boolean) => {
handleChangeAddressVerified = (isAddressVerified: boolean, err: ?Error) => {
if (isAddressVerified) {
this.setState({ isAddressVerified })
this.setState({ isAddressVerified, verifyAddressError: err })
} else if (isAddressVerified === null) {
this.setState({ isAddressVerified: null, errorSteps: [] })
this.setState({ isAddressVerified: null, errorSteps: [], verifyAddressError: err })
} else {
const confirmStepIndex = this.STEPS.findIndex(step => step.id === 'confirm')
if (confirmStepIndex > -1) {
this.setState({
isAddressVerified,
verifyAddressError: err,
errorSteps: [confirmStepIndex],
})
}
@ -161,6 +165,7 @@ class ReceiveModal extends PureComponent<Props, State> {
isAddressVerified,
disabledSteps,
errorSteps,
verifyAddressError,
} = this.state
const addtionnalProps = {
@ -168,6 +173,7 @@ class ReceiveModal extends PureComponent<Props, State> {
account,
isAppOpened,
isAddressVerified,
verifyAddressError,
closeModal: this.handleCloseModal,
onRetry: this.handleRetry,
onSkipConfirm: this.handleSkipConfirm,

13
src/components/modals/Receive/steps/03-step-confirm-address.js

@ -15,6 +15,7 @@ import CurrentAddressForAccount from 'components/CurrentAddressForAccount'
import { WrongDeviceForAccount } from 'components/EnsureDeviceApp'
import type { StepProps } from '../index'
import TranslatedError from '../../../TranslatedError'
export default class StepConfirmAddress extends PureComponent<StepProps> {
componentDidMount() {
@ -43,12 +44,12 @@ export default class StepConfirmAddress extends PureComponent<StepProps> {
onChangeAddressVerified(true)
transitionTo('receive')
} catch (err) {
onChangeAddressVerified(false)
onChangeAddressVerified(false, err)
}
}
render() {
const { t, device, account, isAddressVerified } = this.props
const { t, device, account, isAddressVerified, verifyAddressError } = this.props
invariant(account, 'No account given')
invariant(device, 'No device given')
return (
@ -56,8 +57,12 @@ export default class StepConfirmAddress extends PureComponent<StepProps> {
<TrackPage category="Receive" name="Step3" />
{isAddressVerified === false ? (
<Fragment>
<Title>{t('app:receive.steps.confirmAddress.error.title')}</Title>
<Text mb={5}>{t('app:receive.steps.confirmAddress.error.text')}</Text>
<Title>
<TranslatedError error={verifyAddressError} />
</Title>
<Text mb={5}>
<TranslatedError error={verifyAddressError} field="description" />
</Text>
<DeviceConfirm error />
</Fragment>
) : (

2
src/components/modals/ReleaseNotes.js

@ -138,7 +138,7 @@ export const Notes = styled(Box).attrs({
}
}
input[type='checkbox'] {
input[type='Switch'] {
margin-right: 0.5em;
}
`

7
src/components/modals/Send/fields/AmountField.js

@ -20,13 +20,14 @@ class AmountField extends Component<*, { canBeSpent: boolean }> {
}
}
componentWillUnmount() {
this.unmount = true
this.syncId++
}
unmount = false
syncId = 0
async resync() {
const { account, bridge, transaction } = this.props
const syncId = ++this.syncId
const canBeSpent = await bridge.canBeSpent(account, transaction)
if (this.unmount) return
if (this.syncId !== syncId) return
this.setState({ canBeSpent })
}

7
src/components/modals/Send/fields/RecipientField.js

@ -33,16 +33,17 @@ class RecipientField<Transaction> extends Component<Props<Transaction>, { isVali
}
}
componentWillUnmount() {
this.unmount = true
this.syncId++
}
unmount = false
syncId = 0
async resync() {
const { account, bridge, transaction } = this.props
const syncId = ++this.syncId
const isValid = await bridge.isRecipientValid(
account.currency,
bridge.getTransactionRecipient(account, transaction),
)
if (this.unmount) return
if (syncId !== this.syncId) return
this.setState({ isValid })
}

12
src/components/modals/Send/index.js

@ -44,6 +44,7 @@ type Props = {
type State<Transaction> = {
stepId: string,
openedFromAccount: boolean,
account: ?Account,
bridge: ?WalletBridge<Transaction>,
transaction: ?Transaction,
@ -54,6 +55,7 @@ type State<Transaction> = {
}
export type StepProps<Transaction> = DefaultStepProps & {
openedFromAccount: boolean,
device: ?Device,
account: ?Account,
bridge: ?WalletBridge<Transaction>,
@ -118,6 +120,7 @@ const mapDispatchToProps = {
const INITIAL_STATE = {
stepId: 'amount',
amount: 0,
openedFromAccount: false,
account: null,
bridge: null,
transaction: null,
@ -151,7 +154,12 @@ class SendModal extends PureComponent<Props, State<*>> {
const account = (data && data.account) || accounts[0]
const bridge = account ? getBridgeForCurrency(account.currency) : null
const transaction = bridge ? bridge.createTransaction(account) : null
this.setState({ account, bridge, transaction })
this.setState({
openedFromAccount: !!(data && data.account),
account,
bridge,
transaction,
})
}
}
@ -227,6 +235,7 @@ class SendModal extends PureComponent<Props, State<*>> {
const { t, device, openModal } = this.props
const {
stepId,
openedFromAccount,
account,
isAppOpened,
bridge,
@ -237,6 +246,7 @@ class SendModal extends PureComponent<Props, State<*>> {
const addtionnalProps = {
device,
openedFromAccount,
account,
bridge,
transaction,

15
src/components/modals/Send/steps/01-step-amount.js

@ -20,6 +20,7 @@ export default ({
t,
account,
bridge,
openedFromAccount,
transaction,
onChangeAccount,
onChangeTransaction,
@ -35,14 +36,14 @@ export default ({
<Box flow={4}>
<Box flow={1}>
<Label>{t('app:send.steps.amount.selectAccountDebit')}</Label>
<SelectAccount onChange={onChangeAccount} value={account} />
<SelectAccount autoFocus={!openedFromAccount} onChange={onChangeAccount} value={account} />
</Box>
{account &&
bridge &&
transaction && (
<RecipientField
autoFocus
autoFocus={openedFromAccount}
account={account}
bridge={bridge}
transaction={transaction}
@ -112,14 +113,16 @@ export class StepAmountFooter extends PureComponent<
}
componentWillUnmount() {
this._isUnmounted = true
this.syncId++
}
_isUnmounted = false
syncId = 0
async resync() {
const { account, bridge, transaction } = this.props
const syncId = ++this.syncId
if (!account || !transaction || !bridge) {
return
}
@ -128,9 +131,9 @@ export class StepAmountFooter extends PureComponent<
try {
const totalSpent = await bridge.getTotalSpent(account, transaction)
if (this._isUnmounted) return
if (syncId !== this.syncId) return
const canBeSpent = await bridge.canBeSpent(account, transaction)
if (this._isUnmounted) return
if (syncId !== this.syncId) return
this.setState({ totalSpent, canBeSpent, isSyncing: false })
} catch (err) {
this.setState({ isSyncing: false })

2
src/components/modals/UpdateFirmware/Disclaimer.js

@ -67,7 +67,7 @@ class DisclaimerModal extends PureComponent<Props, State> {
<GradientBox />
</ModalContent>
<ModalFooter horizontal justifyContent="flex-end" style={{ width: '100%' }}>
<Button primary padded onClick={goToNextStep}>
<Button primary onClick={goToNextStep}>
{t('app:manager.firmware.continue')}
</Button>
</ModalFooter>

1
src/components/modals/index.js

@ -5,3 +5,4 @@ export Receive from './Receive'
export Send from './Send'
export SettingsAccount from './SettingsAccount'
export ReleaseNotes from './ReleaseNotes'
export Disclaimer from './Disclaimer'

8
src/config/constants.js

@ -35,6 +35,7 @@ export const LISTEN_DEVICES_POLLING_INTERVAL = intFromEnv('LISTEN_DEVICES_POLLIN
export const OUTDATED_CONSIDERED_DELAY = intFromEnv('OUTDATED_CONSIDERED_DELAY', 5 * 60 * 1000)
export const SYNC_ALL_INTERVAL = 120 * 1000
export const SYNC_BOOT_DELAY = 2 * 1000
export const SYNC_PENDING_INTERVAL = 10 * 1000
export const SYNC_MAX_CONCURRENT = intFromEnv('LEDGER_SYNC_MAX_CONCURRENT', 1)
export const SYNC_TIMEOUT = intFromEnv('SYNC_TIMEOUT', 30 * 1000)
@ -80,6 +81,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
@ -91,3 +96,6 @@ export const MODAL_RECEIVE = 'MODAL_RECEIVE'
export const MODAL_SEND = 'MODAL_SEND'
export const MODAL_SETTINGS_ACCOUNT = 'MODAL_SETTINGS_ACCOUNT'
export const MODAL_RELEASES_NOTES = 'MODAL_RELEASES_NOTES'
export const MODAL_DISCLAIMER = 'MODAL_DISCLAIMER'
export const MODAL_DISCLAIMER_DELAY = 1 * 1000

20
src/helpers/devices/getDeviceInfo.js

@ -16,13 +16,6 @@ export type DeviceInfo = {
fullVersion: string,
}
// prettier-ignore
const DETECT_CLUBCOIN = [
[0xe0, 0x04, 0x00, 0x00, Buffer.from([0x31, 0x10, 0x00, 0x02])],
[0xe0, 0x50, 0x00, 0x00, Buffer.from([0xe4, 0x6c, 0x4c, 0x71, 0x8b, 0xc8, 0x7f, 0xb7])],
[0xe0, 0x51, 0x80, 0x00, Buffer.from([0x41, 0x04, 0xc9, 0x8c, 0xa0, 0x99, 0x53, 0x47, 0x2b, 0x36, 0x06, 0x1e, 0x0e, 0x40, 0xc9, 0x3d, 0x50, 0x52, 0x34, 0x09, 0x0e, 0xfd, 0x74, 0xf1, 0xd7, 0xa2, 0x93, 0xe8, 0x28, 0x15, 0x9a, 0x97, 0x71, 0x1b, 0x33, 0xd1, 0x8a, 0xfc, 0x17, 0xad, 0x15, 0x6e, 0xae, 0xd9, 0x9c, 0xf4, 0x3b, 0x20, 0xe1, 0x5d, 0x64, 0xaf, 0x39, 0xa5, 0x51, 0x3b, 0x4e, 0x3c, 0x5f, 0x43, 0x17, 0xe6, 0x42, 0x70, 0x2f, 0x05, 0x47, 0x30, 0x45, 0x02, 0x21, 0x00, 0xf1, 0xd2, 0xb8, 0x34, 0x99, 0x4a, 0x0c, 0x1f, 0x25, 0xea, 0x20, 0xcf, 0x33, 0xe3, 0x2b, 0xd0, 0x6b, 0xcf, 0x7c, 0x42, 0x4a, 0x02, 0xee, 0xe8, 0xf6, 0x96, 0x99, 0x20, 0xe1, 0xe8, 0xc2, 0xb3, 0x02, 0x20, 0x63, 0x2d, 0x19, 0xbd, 0x30, 0xab, 0x20, 0x76, 0x18, 0x78, 0x78, 0xae, 0xaa, 0x0f, 0x4d, 0x48, 0x04, 0x01, 0x32, 0x79, 0xd0, 0x16, 0xde, 0xca, 0x66, 0x93, 0xf3, 0x7b, 0x4e, 0x50, 0x7f, 0x43])],
]
const PROVIDERS = {
'': 1,
das: 2,
@ -32,19 +25,8 @@ const PROVIDERS = {
export default async (transport: Transport<*>): Promise<DeviceInfo> => {
const res = await getFirmwareInfo(transport)
let { seVersion } = res
const { seVersion } = res
const { targetId, mcuVersion, flags } = res
if (seVersion === '1.2') {
try {
for (let i = 0; i < DETECT_CLUBCOIN.length; i++) {
const instructions = DETECT_CLUBCOIN[i]
await transport.send(...instructions)
}
seVersion = '1.2.0-club'
} catch (e) {
seVersion = '1.2.0'
}
}
const parsedVersion =
seVersion.match(/([0-9]+.[0-9])+(.[0-9]+)?((?!-osu)-([a-z]+))?(-osu)?/) || []
const isOSU = typeof parsedVersion[5] !== 'undefined'

1
src/helpers/hardReset.js

@ -9,4 +9,5 @@ export default async function hardReset() {
disableDBMiddleware()
db.resetAll()
await delay(500)
window.location.href = ''
}

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

Loading…
Cancel
Save