Browse Source

Merge branch 'master' into pixel-push

master
Thibaut Boustany 7 years ago
parent
commit
73cdcd2405
No known key found for this signature in database GPG Key ID: 32475B11A2B13EEC
  1. 2
      package.json
  2. 2
      scripts/release.sh
  3. 18
      src/bridge/LibcoreBridge.js
  4. 7
      src/commands/getIsGenuine.js
  5. 6
      src/components/DashboardPage/EmptyState.js
  6. 4
      src/components/DeviceConfirm/index.js
  7. 53
      src/components/IsUnlocked.js
  8. 5
      src/components/ManagerPage/EnsureDashboard.js
  9. 17
      src/components/ManagerPage/EnsureGenuine.js
  10. 2
      src/components/ManagerPage/Workflow.js
  11. 2
      src/components/Onboarding/steps/Analytics.js
  12. 7
      src/components/Onboarding/steps/GenuineCheck.js
  13. 20
      src/components/SettingsPage/sections/Profile.js
  14. 2
      src/components/TopBar/ActivityIndicator.js
  15. 82
      src/components/base/InputCurrency/index.js
  16. 9
      src/components/base/Modal/ConfirmModal.js
  17. 1
      src/components/modals/AccountSettingRenderBody.js
  18. 4
      src/components/modals/AddAccounts/steps/03-step-import.js
  19. 2
      src/components/modals/Receive/index.js
  20. 5
      src/components/modals/Send/03-step-verification.js
  21. 16
      src/components/modals/Send/index.js
  22. 2
      src/config/constants.js
  23. 2
      src/helpers/apps/installApp.js
  24. 2
      src/helpers/apps/uninstallApp.js
  25. 34
      src/helpers/common.js
  26. 6
      src/helpers/constants.js
  27. 3
      src/helpers/createCustomErrorClass.js
  28. 3
      src/helpers/deviceAccess.js
  29. 13
      src/helpers/devices/getIsGenuine.js
  30. 2
      src/helpers/firmware/installFinalFirmware.js
  31. 2
      src/helpers/firmware/installOsuFirmware.js
  32. 6
      src/helpers/libcore.js
  33. 16
      src/icons/TriangleWarning.js
  34. 10
      src/main/bridge.js
  35. 2
      src/reducers/settings.js
  36. 8
      static/i18n/en/app.yml
  37. 1
      static/i18n/en/errors.yml
  38. 3
      static/i18n/en/onboarding.yml
  39. 8
      static/i18n/fr/app.yml
  40. 1
      static/i18n/fr/errors.yml
  41. 3
      static/i18n/fr/onboarding.yml

2
package.json

@ -3,7 +3,7 @@
"productName": "Ledger Live", "productName": "Ledger Live",
"description": "Ledger Live - Desktop", "description": "Ledger Live - Desktop",
"repository": "https://github.com/LedgerHQ/ledger-live-desktop", "repository": "https://github.com/LedgerHQ/ledger-live-desktop",
"version": "0.1.0-alpha.10", "version": "0.1.0-alpha.11",
"author": "Ledger", "author": "Ledger",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {

2
scripts/release.sh

@ -21,5 +21,7 @@ fi
# TODO check if local git HEAD is EXACTLY our remote master HEAD # TODO check if local git HEAD is EXACTLY our remote master HEAD
export SENTRY_URL=https://db8f5b9b021048d4a401f045371701cb@sentry.io/274561 export SENTRY_URL=https://db8f5b9b021048d4a401f045371701cb@sentry.io/274561
rm -rf ./node_modules/.cache
yarn
yarn compile yarn compile
build build

18
src/bridge/LibcoreBridge.js

@ -48,7 +48,7 @@ const EditAdvancedOptions = ({ onChange, value }: EditProps<Transaction>) => (
const recipientValidLRU = LRU({ max: 100 }) const recipientValidLRU = LRU({ max: 100 })
const isRecipientValid = (currency, recipient): Promise<boolean> => { const isRecipientValid = (currency, recipient) => {
const key = `${currency.id}_${recipient}` const key = `${currency.id}_${recipient}`
let promise = recipientValidLRU.get(key) let promise = recipientValidLRU.get(key)
if (promise) return promise if (promise) return promise
@ -172,14 +172,18 @@ const LibcoreBridge: WalletBridge<Transaction> = {
isValidTransaction: (a, t) => (t.amount > 0 && t.recipient && true) || false, isValidTransaction: (a, t) => (t.amount > 0 && t.recipient && true) || false,
canBeSpent: (a, t) => canBeSpent: (a, t) =>
getFees(a, t) !t.amount
.then(fees => fees !== null) ? Promise.resolve(true)
.catch(() => false), : getFees(a, t)
.then(() => true)
.catch(() => false),
getTotalSpent: (a, t) => getTotalSpent: (a, t) =>
getFees(a, t) !t.amount
.then(totalFees => t.amount + (totalFees || 0)) ? Promise.resolve(0)
.catch(() => 0), : getFees(a, t)
.then(totalFees => t.amount + (totalFees || 0))
.catch(() => 0),
getMaxAmount: (a, t) => getMaxAmount: (a, t) =>
getFees(a, t) getFees(a, t)

7
src/commands/getIsGenuine.js

@ -4,10 +4,13 @@ import { createCommand, Command } from 'helpers/ipc'
import { fromPromise } from 'rxjs/observable/fromPromise' import { fromPromise } from 'rxjs/observable/fromPromise'
import getIsGenuine from 'helpers/devices/getIsGenuine' import getIsGenuine from 'helpers/devices/getIsGenuine'
import { withDevice } from 'helpers/deviceAccess'
type Input = * type Input = *
type Result = boolean type Result = string
const cmd: Command<Input, Result> = createCommand('getIsGenuine', () => fromPromise(getIsGenuine())) const cmd: Command<Input, Result> = createCommand('getIsGenuine', ({ devicePath, targetId }) =>
fromPromise(withDevice(devicePath)(transport => getIsGenuine(transport, { targetId }))),
)
export default cmd export default cmd

6
src/components/DashboardPage/EmptyState.js

@ -48,6 +48,9 @@ class EmptyState extends PureComponent<Props, *> {
<Title>{t('app:emptyState.dashboard.title')}</Title> <Title>{t('app:emptyState.dashboard.title')}</Title>
<Description>{t('app:emptyState.dashboard.desc')}</Description> <Description>{t('app:emptyState.dashboard.desc')}</Description>
<Box mt={3} horizontal justifyContent="space-around" style={{ width: 300 }}> <Box mt={3} horizontal justifyContent="space-around" style={{ width: 300 }}>
<Button padded primary style={{ width: 120 }} onClick={this.handleInstallApp}>
{t('app:emptyState.dashboard.buttons.installApp')}
</Button>
<Button <Button
padded padded
primary primary
@ -56,9 +59,6 @@ class EmptyState extends PureComponent<Props, *> {
> >
{t('app:emptyState.dashboard.buttons.addAccount')} {t('app:emptyState.dashboard.buttons.addAccount')}
</Button> </Button>
<Button padded primary style={{ width: 120 }} onClick={this.handleInstallApp}>
{t('app:emptyState.dashboard.buttons.installApp')}
</Button>
</Box> </Box>
</Box> </Box>
</Box> </Box>

4
src/components/DeviceConfirm/index.js

@ -41,7 +41,7 @@ const WrapperIcon = styled(Box)`
} }
` `
const Check = ({ error }: { error: * }) => ( const Check = ({ error }: { error?: boolean }) => (
<WrapperIcon error={error}> <WrapperIcon error={error}>
{error ? <IconCross size={10} /> : <IconCheck size={10} />} {error ? <IconCross size={10} /> : <IconCheck size={10} />}
</WrapperIcon> </WrapperIcon>
@ -74,7 +74,7 @@ const PushButton = styled(Box)`
` `
type Props = { type Props = {
error: *, error?: boolean,
} }
const SVG = ( const SVG = (

53
src/components/IsUnlocked.js

@ -4,22 +4,26 @@ import bcrypt from 'bcryptjs'
import React, { Component } from 'react' import React, { Component } from 'react'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { compose } from 'redux' import { compose } from 'redux'
import { remote } from 'electron'
import styled from 'styled-components' import styled from 'styled-components'
import { translate } from 'react-i18next' import { translate } from 'react-i18next'
import type { SettingsState as Settings } from 'reducers/settings' import type { SettingsState as Settings } from 'reducers/settings'
import type { T } from 'types/common' import type { T } from 'types/common'
import IconLockScreen from 'icons/LockScreen' import IconLockScreen from 'icons/LockScreen'
import IconTriangleWarning from 'icons/TriangleWarning'
import get from 'lodash/get' import get from 'lodash/get'
import { setEncryptionKey } from 'helpers/db' import { setEncryptionKey } from 'helpers/db'
import hardReset from 'helpers/hardReset'
import { fetchAccounts } from 'actions/accounts' import { fetchAccounts } from 'actions/accounts'
import { isLocked, unlock } from 'reducers/application' import { isLocked, unlock } from 'reducers/application'
import Box from 'components/base/Box' import Box from 'components/base/Box'
import InputPassword from 'components/base/InputPassword' import InputPassword from 'components/base/InputPassword'
import Button from './base/Button/index'
import ConfirmModal from './base/Modal/ConfirmModal'
type InputValue = { type InputValue = {
password: string, password: string,
@ -36,6 +40,8 @@ type Props = {
type State = { type State = {
inputValue: InputValue, inputValue: InputValue,
incorrectPassword: boolean, incorrectPassword: boolean,
isHardResetting: boolean,
isHardResetModalOpened: boolean,
} }
const mapStateToProps = state => ({ const mapStateToProps = state => ({
@ -53,6 +59,8 @@ const defaultState = {
password: '', password: '',
}, },
incorrectPassword: false, incorrectPassword: false,
isHardResetting: false,
isHardResetModalOpened: false,
} }
export const PageTitle = styled(Box).attrs({ export const PageTitle = styled(Box).attrs({
@ -96,6 +104,7 @@ class IsUnlocked extends Component<Props, State> {
...prev.inputValue, ...prev.inputValue,
[key]: value, [key]: value,
}, },
incorrectPassword: false,
})) }))
handleSubmit = async (e: SyntheticEvent<HTMLFormElement>) => { handleSubmit = async (e: SyntheticEvent<HTMLFormElement>) => {
@ -117,8 +126,25 @@ class IsUnlocked extends Component<Props, State> {
} }
} }
handleOpenHardResetModal = () => this.setState({ isHardResetModalOpened: true })
handleCloseHardResetModal = () => this.setState({ isHardResetModalOpened: false })
handleHardReset = async () => {
this.setState({ isHardResetting: true })
try {
await hardReset()
remote.getCurrentWindow().webContents.reloadIgnoringCache()
} catch (err) {
this.setState({ isHardResetting: false })
}
}
hardResetIconRender = () => (
<IconWrapperCircle color="alertRed">
<IconTriangleWarning width={23} height={21} />
</IconWrapperCircle>
)
render() { render() {
const { inputValue, incorrectPassword } = this.state const { inputValue, incorrectPassword, isHardResetting, isHardResetModalOpened } = this.state
const { isLocked, t } = this.props const { isLocked, t } = this.props
if (isLocked) { if (isLocked) {
@ -143,8 +169,22 @@ class IsUnlocked extends Component<Props, State> {
error={incorrectPassword && t('app:password.errorMessageIncorrectPassword')} error={incorrectPassword && t('app:password.errorMessageIncorrectPassword')}
/> />
</Box> </Box>
<Button type="button" mt={3} small onClick={this.handleOpenHardResetModal}>
{t('app:common.lockScreen.lostPassword')}
</Button>
</Box> </Box>
</form> </form>
<ConfirmModal
isDanger
isLoading={isHardResetting}
isOpened={isHardResetModalOpened}
onClose={this.handleCloseHardResetModal}
onReject={this.handleCloseHardResetModal}
onConfirm={this.handleHardReset}
title={t('app:settings.hardResetModal.title')}
desc={t('app:settings.hardResetModal.desc')}
renderIcon={this.hardResetIconRender}
/>
</Box> </Box>
) )
} }
@ -164,3 +204,12 @@ export default compose(
), ),
translate(), translate(),
)(IsUnlocked) )(IsUnlocked)
const IconWrapperCircle = styled(Box).attrs({})`
width: 50px;
height: 50px;
border-radius: 50%;
background: #ea2e4919;
text-align: -webkit-center;
justify-content: center;
`

5
src/components/ManagerPage/EnsureDashboard.js

@ -42,6 +42,7 @@ class EnsureDashboard extends PureComponent<Props, State> {
componentDidMount() { componentDidMount() {
this.checkForDashboard() this.checkForDashboard()
this._interval = setInterval(this.checkForDashboard, 1000)
} }
componentDidUpdate() { componentDidUpdate() {
@ -50,12 +51,14 @@ class EnsureDashboard extends PureComponent<Props, State> {
componentWillUnmount() { componentWillUnmount() {
this._unmounting = true this._unmounting = true
clearInterval(this._interval)
} }
_checking = false _checking = false
_unmounting = false _unmounting = false
_interval: *
async checkForDashboard() { checkForDashboard = async () => {
const { device } = this.props const { device } = this.props
if (device && !this._checking) { if (device && !this._checking) {
this._checking = true this._checking = true

17
src/components/ManagerPage/EnsureGenuine.js

@ -12,8 +12,14 @@ type Error = {
stack: string, stack: string,
} }
type DeviceInfos = {
targetId: number | string,
version: string,
}
type Props = { type Props = {
device: ?Device, device: ?Device,
infos: ?DeviceInfos,
children: (isGenuine: ?boolean, error: ?Error) => Node, children: (isGenuine: ?boolean, error: ?Error) => Node,
} }
@ -49,12 +55,15 @@ class EnsureGenuine extends PureComponent<Props, State> {
_unmounting = false _unmounting = false
async checkIsGenuine() { async checkIsGenuine() {
const { device } = this.props const { device, infos } = this.props
if (device && !this._checking) { if (device && infos && !this._checking) {
this._checking = true this._checking = true
try { try {
const isGenuine = await getIsGenuine.send().toPromise() const res = await getIsGenuine
if (!this.state.genuine || this.state.error) { .send({ devicePath: device.path, targetId: infos.targetId })
.toPromise()
const isGenuine = res === '0000'
if ((!this.state.genuine || this.state.error) && isGenuine) {
!this._unmounting && this.setState({ genuine: isGenuine, error: null }) !this._unmounting && this.setState({ genuine: isGenuine, error: null })
} }
} catch (err) { } catch (err) {

2
src/components/ManagerPage/Workflow.js

@ -52,7 +52,7 @@ class Workflow extends PureComponent<Props, State> {
{(device: Device) => ( {(device: Device) => (
<EnsureDashboard device={device}> <EnsureDashboard device={device}>
{(deviceInfo: ?DeviceInfo, dashboardError: ?Error) => ( {(deviceInfo: ?DeviceInfo, dashboardError: ?Error) => (
<EnsureGenuine device={device}> <EnsureGenuine device={device} infos={deviceInfo}>
{(isGenuine: ?boolean, genuineError: ?Error) => { {(isGenuine: ?boolean, genuineError: ?Error) => {
if (dashboardError || genuineError) { if (dashboardError || genuineError) {
return renderError return renderError

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

@ -19,7 +19,7 @@ type State = {
} }
const INITIAL_STATE = { const INITIAL_STATE = {
analyticsToggle: true, analyticsToggle: false,
sentryLogsToggle: true, sentryLogsToggle: true,
} }

7
src/components/Onboarding/steps/GenuineCheck.js

@ -129,7 +129,12 @@ class GenuineCheck extends PureComponent<StepProps, State> {
<FixedTopContainer> <FixedTopContainer>
<Box grow alignItems="center"> <Box grow alignItems="center">
<Title>{t('onboarding:genuineCheck.title')}</Title> <Title>{t('onboarding:genuineCheck.title')}</Title>
<Description>{t('onboarding:genuineCheck.desc')}</Description> {onboarding.isLedgerNano ? (
<Description>{t('onboarding:genuineCheck.descNano')}</Description>
) : (
<Description>{t('onboarding:genuineCheck.descBlue')}</Description>
)}
<Box mt={5}> <Box mt={5}>
<CardWrapper> <CardWrapper>
<Box justify="center"> <Box justify="center">

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

@ -2,6 +2,7 @@
import React, { PureComponent } from 'react' import React, { PureComponent } from 'react'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import styled from 'styled-components'
import { remote } from 'electron' import { remote } from 'electron'
import bcrypt from 'bcryptjs' import bcrypt from 'bcryptjs'
@ -19,6 +20,7 @@ import CheckBox from 'components/base/CheckBox'
import Box from 'components/base/Box' import Box from 'components/base/Box'
import Button from 'components/base/Button' import Button from 'components/base/Button'
import { ConfirmModal } from 'components/base/Modal' import { ConfirmModal } from 'components/base/Modal'
import IconTriangleWarning from 'icons/TriangleWarning'
import IconUser from 'icons/User' import IconUser from 'icons/User'
import PasswordModal from '../PasswordModal' import PasswordModal from '../PasswordModal'
import DisablePasswordModal from '../DisablePasswordModal' import DisablePasswordModal from '../DisablePasswordModal'
@ -125,6 +127,12 @@ class TabProfile extends PureComponent<Props, State> {
}) })
} }
hardResetIconRender = () => (
<IconWrapperCircle color="alertRed">
<IconTriangleWarning width={23} height={21} />
</IconWrapperCircle>
)
render() { render() {
const { t, settings, saveSettings } = this.props const { t, settings, saveSettings } = this.props
const { const {
@ -223,8 +231,8 @@ class TabProfile extends PureComponent<Props, State> {
onReject={this.handleCloseHardResetModal} onReject={this.handleCloseHardResetModal}
onConfirm={this.handleHardReset} onConfirm={this.handleHardReset}
title={t('app:settings.hardResetModal.title')} title={t('app:settings.hardResetModal.title')}
subTitle={t('app:settings.hardResetModal.subTitle')}
desc={t('app:settings.hardResetModal.desc')} desc={t('app:settings.hardResetModal.desc')}
renderIcon={this.hardResetIconRender}
/> />
<PasswordModal <PasswordModal
@ -253,3 +261,13 @@ export default connect(
null, null,
mapDispatchToProps, mapDispatchToProps,
)(TabProfile) )(TabProfile)
// TODO: need a helper file for common styles across the app
const IconWrapperCircle = styled(Box).attrs({})`
width: 50px;
height: 50px;
border-radius: 50%;
background: #ea2e4919;
text-align: -webkit-center;
justify-content: center;
`

2
src/components/TopBar/ActivityIndicator.js

@ -70,7 +70,7 @@ class ActivityIndicatorInner extends Component<Props, State> {
render() { render() {
const { isPending, isError, t } = this.props const { isPending, isError, t } = this.props
const { hasClicked, isFirstSync } = this.state const { hasClicked, isFirstSync } = this.state
const isDisabled = isFirstSync || hasClicked || isError const isDisabled = isError || (isPending && (isFirstSync || hasClicked))
const isRotating = isPending && (hasClicked || isFirstSync) const isRotating = isPending && (hasClicked || isFirstSync)
return ( return (

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

@ -6,7 +6,6 @@ import styled from 'styled-components'
import { formatCurrencyUnit } from '@ledgerhq/live-common/lib/helpers/currencies' import { formatCurrencyUnit } from '@ledgerhq/live-common/lib/helpers/currencies'
import noop from 'lodash/noop' import noop from 'lodash/noop'
import isNaN from 'lodash/isNaN'
import Box from 'components/base/Box' import Box from 'components/base/Box'
import Input from 'components/base/Input' import Input from 'components/base/Input'
@ -14,8 +13,41 @@ import Select from 'components/base/LegacySelect'
import type { Unit } from '@ledgerhq/live-common/lib/types' import type { Unit } from '@ledgerhq/live-common/lib/types'
function parseValue(value) { // TODO move this back to live common
return value.toString().replace(/,/g, '.') const numbers = '0123456789'
const sanitizeValueString = (
unit: Unit,
valueString: string,
): {
display: string,
value: string,
} => {
let display = ''
let value = ''
let decimals = -1
for (let i = 0; i < valueString.length; i++) {
const c = valueString[i]
if (numbers.indexOf(c) !== -1) {
if (decimals >= 0) {
decimals++
if (decimals > unit.magnitude) break
value += c
display += c
} else if (value !== '0') {
value += c
display += c
}
} else if (decimals === -1 && (c === ',' || c === '.')) {
if (i === 0) display = '0'
decimals = 0
display += '.'
}
}
for (let i = Math.max(0, decimals); i < unit.magnitude; ++i) {
value += '0'
}
if (!value) value = '0'
return { display, value }
} }
function format(unit: Unit, value: number, { isFocused, showAllDigits, subMagnitude }) { function format(unit: Unit, value: number, { isFocused, showAllDigits, subMagnitude }) {
@ -85,9 +117,10 @@ class InputCurrency extends PureComponent<Props, State> {
componentWillReceiveProps(nextProps: Props) { componentWillReceiveProps(nextProps: Props) {
const { value, showAllDigits, unit } = this.props const { value, showAllDigits, unit } = this.props
const needsToBeReformatted = const needsToBeReformatted =
value !== nextProps.value || !this.state.isFocused &&
showAllDigits !== nextProps.showAllDigits || (value !== nextProps.value ||
unit !== nextProps.unit showAllDigits !== nextProps.showAllDigits ||
unit !== nextProps.unit)
if (needsToBeReformatted) { if (needsToBeReformatted) {
const { isFocused } = this.state const { isFocused } = this.state
this.setState({ this.setState({
@ -104,28 +137,13 @@ class InputCurrency extends PureComponent<Props, State> {
} }
handleChange = (v: string) => { handleChange = (v: string) => {
v = parseValue(v) const { onChange, unit, value } = this.props
const r = sanitizeValueString(unit, v)
// allow to type directly `.` in input to have `0.` const satoshiValue = parseInt(r.value, 10)
if (v.startsWith('.')) { if (value !== satoshiValue) {
v = `0${v}` onChange(satoshiValue, unit)
}
// forbid multiple 0 at start
if (v === '' || v.startsWith('00')) {
const { onChange, unit } = this.props
onChange(0, unit)
this.setState({ displayValue: '' })
return
}
// Check if value is valid Number
if (isNaN(Number(v))) {
return
} }
this.setState({ displayValue: r.display })
this.emitOnChange(v)
this.setState({ displayValue: v || '' })
} }
handleBlur = () => { handleBlur = () => {
@ -149,16 +167,6 @@ class InputCurrency extends PureComponent<Props, State> {
}) })
} }
emitOnChange = (v: string) => {
const { onChange, unit } = this.props
const { displayValue } = this.state
if (displayValue.toString() !== v.toString()) {
const satoshiValue = Number(v) * 10 ** unit.magnitude
onChange(satoshiValue, unit)
}
}
renderItem = item => item.code renderItem = item => item.code
renderSelected = item => <Currency>{item.code}</Currency> renderSelected = item => <Currency>{item.code}</Currency>

9
src/components/base/Modal/ConfirmModal.js

@ -14,8 +14,9 @@ type Props = {
isOpened: boolean, isOpened: boolean,
isDanger: boolean, isDanger: boolean,
title: string, title: string,
subTitle: string, subTitle?: string,
desc: string, desc: string,
renderIcon?: Function,
confirmText?: string, confirmText?: string,
cancelText?: string, cancelText?: string,
onReject: Function, onReject: Function,
@ -37,6 +38,7 @@ class ConfirmModal extends PureComponent<Props> {
onReject, onReject,
onConfirm, onConfirm,
isLoading, isLoading,
renderIcon,
t, t,
...props ...props
} = this.props } = this.props
@ -57,6 +59,11 @@ class ConfirmModal extends PureComponent<Props> {
{subTitle} {subTitle}
</Box> </Box>
)} )}
{renderIcon && (
<Box justifyContent="center" alignItems="center" mt={4} mb={3}>
{renderIcon()}
</Box>
)}
<Box ff="Open Sans" color="smoke" fontSize={4} textAlign="center"> <Box ff="Open Sans" color="smoke" fontSize={4} textAlign="center">
{desc} {desc}
</Box> </Box>

1
src/components/modals/AccountSettingRenderBody.js

@ -151,6 +151,7 @@ class HelperComp extends PureComponent<Props, State> {
<Box> <Box>
<Input <Input
value={account.name} value={account.name}
maxLength={30}
onChange={this.handleChangeName} onChange={this.handleChangeName}
renderLeft={<InputLeft currency={account.currency} />} renderLeft={<InputLeft currency={account.currency} />}
onFocus={e => this.handleFocus(e, 'accountName')} onFocus={e => this.handleFocus(e, 'accountName')}

4
src/components/modals/AddAccounts/steps/03-step-import.js

@ -160,7 +160,9 @@ class StepImport extends PureComponent<StepProps> {
count: importableAccounts.length, count: importableAccounts.length,
}) })
const importableAccountsEmpty = t('app:addAccounts.noAccountToImport', { currencyName: currency ? ` ${currency.name}}` : ''}) const importableAccountsEmpty = t('app:addAccounts.noAccountToImport', {
currencyName: currency ? ` ${currency.name}}` : '',
})
return ( return (
<Fragment> <Fragment>

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

@ -198,7 +198,7 @@ class ReceiveModal extends PureComponent<Props, State> {
}) })
} else { } else {
this.setState({ this.setState({
account: accounts[0] account: accounts[0],
}) })
} }
} }

5
src/components/modals/Send/03-step-verification.js

@ -27,14 +27,13 @@ const Info = styled(Box).attrs({
` `
type Props = { type Props = {
hasError: boolean,
t: T, t: T,
} }
export default ({ t, hasError }: Props) => ( export default ({ t }: Props) => (
<Container> <Container>
<WarnBox>{multiline(t('app:send.steps.verification.warning'))}</WarnBox> <WarnBox>{multiline(t('app:send.steps.verification.warning'))}</WarnBox>
<Info>{t('app:send.steps.verification.body')}</Info> <Info>{t('app:send.steps.verification.body')}</Info>
<DeviceConfirm error={hasError} /> <DeviceConfirm />
</Container> </Container>
) )

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

@ -15,6 +15,7 @@ import { getBridgeForCurrency } from 'bridge'
import { accountsSelector } from 'reducers/accounts' import { accountsSelector } from 'reducers/accounts'
import { updateAccountWithUpdater } from 'actions/accounts' import { updateAccountWithUpdater } from 'actions/accounts'
import createCustomErrorClass from 'helpers/createCustomErrorClass'
import { MODAL_SEND } from 'config/constants' import { MODAL_SEND } from 'config/constants'
import Modal, { ModalBody, ModalContent, ModalTitle } from 'components/base/Modal' import Modal, { ModalBody, ModalContent, ModalTitle } from 'components/base/Modal'
@ -32,6 +33,8 @@ import StepAmount from './01-step-amount'
import StepVerification from './03-step-verification' import StepVerification from './03-step-verification'
import StepConfirmation from './04-step-confirmation' import StepConfirmation from './04-step-confirmation'
export const UserRefusedOnDevice = createCustomErrorClass('UserRefusedOnDevice')
type Props = { type Props = {
updateAccountWithUpdater: (string, (Account) => Account) => void, updateAccountWithUpdater: (string, (Account) => Account) => void,
accounts: Account[], accounts: Account[],
@ -226,14 +229,11 @@ class SendModal extends Component<Props, State<*>> {
}) })
} }
onOperationError = (error: Error) => { onOperationError = (error: *) => {
// $FlowFixMe this.setState({
if (error.statusCode === 0x6985) { error: error.statusCode === 0x6985 ? new UserRefusedOnDevice() : error,
// User denied on device stepIndex: 3,
this.setState({ error }) })
} else {
this.setState({ error, stepIndex: 3 })
}
} }
onChangeAccount = account => { onChangeAccount = account => {

2
src/config/constants.js

@ -9,7 +9,7 @@ const intFromEnv = (key: string, def: number) => {
export const GET_CALLS_TIMEOUT = intFromEnv('GET_CALLS_TIMEOUT', 30 * 1000) export const GET_CALLS_TIMEOUT = intFromEnv('GET_CALLS_TIMEOUT', 30 * 1000)
export const GET_CALLS_RETRY = intFromEnv('GET_CALLS_RETRY', 2) export const GET_CALLS_RETRY = intFromEnv('GET_CALLS_RETRY', 2)
export const SYNC_MAX_CONCURRENT = intFromEnv('LEDGER_SYNC_MAX_CONCURRENT', 2) export const SYNC_MAX_CONCURRENT = intFromEnv('LEDGER_SYNC_MAX_CONCURRENT', 6)
export const SYNC_BOOT_DELAY = 2 * 1000 export const SYNC_BOOT_DELAY = 2 * 1000
export const SYNC_ALL_INTERVAL = 60 * 1000 export const SYNC_ALL_INTERVAL = 60 * 1000

2
src/helpers/apps/installApp.js

@ -12,5 +12,5 @@ export default async function installApp(
transport: Transport<*>, transport: Transport<*>,
{ appParams }: { appParams: LedgerScriptParams }, { appParams }: { appParams: LedgerScriptParams },
): Promise<void> { ): Promise<void> {
return createSocketDialog(transport, '/update/install', appParams) return createSocketDialog(transport, '/install', appParams)
} }

2
src/helpers/apps/uninstallApp.js

@ -17,5 +17,5 @@ export default async function uninstallApp(
firmware: appParams.delete, firmware: appParams.delete,
firmwareKey: appParams.deleteKey, firmwareKey: appParams.deleteKey,
} }
return createSocketDialog(transport, '/update/install', params) return createSocketDialog(transport, '/install', params)
} }

34
src/helpers/common.js

@ -5,7 +5,7 @@ import Websocket from 'ws'
import qs from 'qs' import qs from 'qs'
import type Transport from '@ledgerhq/hw-transport' import type Transport from '@ledgerhq/hw-transport'
import { BASE_SOCKET_URL, APDUS } from './constants' import { BASE_SOCKET_URL, APDUS, MANAGER_API_URL } from './constants'
type WebsocketType = { type WebsocketType = {
send: (string, any) => void, send: (string, any) => void,
@ -24,34 +24,11 @@ export type LedgerScriptParams = {
firmwareKey?: string, firmwareKey?: string,
delete?: string, delete?: string,
deleteKey?: string, deleteKey?: string,
targetId?: string | number,
} }
type FirmwareUpdateType = 'osu' | 'final' type FirmwareUpdateType = 'osu' | 'final'
// /**
// * Install an app on the device
// */
// export async function installApp(
// transport: Transport<*>,
// { appParams }: { appParams: LedgerScriptParams },
// ): Promise<void> {
// return createSocketDialog(transport, '/update/install', appParams)
// }
/**
* Uninstall an app on the device
*/
export async function uninstallApp(
transport: Transport<*>,
{ appParams }: { appParams: LedgerScriptParams },
): Promise<void> {
return createSocketDialog(transport, '/update/install', {
...appParams,
firmware: appParams.delete,
firmwareKey: appParams.deleteKey,
})
}
export async function getMemInfos(transport: Transport<*>): Promise<Object> { export async function getMemInfos(transport: Transport<*>): Promise<Object> {
const { targetId } = await getFirmwareInfo(transport) const { targetId } = await getFirmwareInfo(transport)
// Dont ask me about this `perso_11`: I don't know. But we need it. // Dont ask me about this `perso_11`: I don't know. But we need it.
@ -119,11 +96,14 @@ export async function createSocketDialog(
transport: Transport<*>, transport: Transport<*>,
endpoint: string, endpoint: string,
params: LedgerScriptParams, params: LedgerScriptParams,
managerUrl: boolean = false,
) { ) {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
try { try {
let lastData let lastData
const url = `${BASE_SOCKET_URL}${endpoint}?${qs.stringify(params)}` const url = `${managerUrl ? MANAGER_API_URL : BASE_SOCKET_URL}${endpoint}?${qs.stringify(
params,
)}`
log('WS CONNECTING', url) log('WS CONNECTING', url)
const ws: WebsocketType = new Websocket(url) const ws: WebsocketType = new Websocket(url)
@ -142,6 +122,8 @@ export async function createSocketDialog(
success: msg => { success: msg => {
if (msg.data) { if (msg.data) {
lastData = msg.data lastData = msg.data
} else if (msg.result) {
lastData = msg.result
} }
}, },
error: msg => { error: msg => {

6
src/helpers/constants.js

@ -1,10 +1,8 @@
// Socket endpoint // Socket endpoint
export const BASE_SOCKET_URL = 'ws://api.ledgerwallet.com' export const BASE_SOCKET_URL = 'ws://api.ledgerwallet.com/update'
export const BASE_SOCKET_URL_TEMP = 'ws://manager.ledger.fr:3500' export const MANAGER_API_URL = 'wss://api.ledgerwallet.com/update'
export const API_BASE_URL = process.env.API_BASE_URL || 'https://beta.manager.live.ledger.fr/api' export const API_BASE_URL = process.env.API_BASE_URL || 'https://beta.manager.live.ledger.fr/api'
// If you want to test locally with https://github.com/LedgerHQ/ledger-update-python-api
// export const BASE_SOCKET_URL = 'ws://localhost:3001/update'
// List of APDUS // List of APDUS
export const APDUS = { export const APDUS = {

3
src/helpers/createCustomErrorClass.js

@ -1,6 +1,6 @@
// @flow // @flow
export default (name: string) => { export default (name: string): Class<any> => {
const C = function CustomError(message?: string, fields?: Object) { const C = function CustomError(message?: string, fields?: Object) {
this.name = name this.name = name
this.message = message || name this.message = message || name
@ -9,5 +9,6 @@ export default (name: string) => {
} }
// $FlowFixMe // $FlowFixMe
C.prototype = new Error() C.prototype = new Error()
// $FlowFixMe we can't easily type a subset of Error for now...
return C return C
} }

3
src/helpers/deviceAccess.js

@ -19,7 +19,8 @@ export const withDevice: WithDevice = devicePath => {
return job => return job =>
takeSemaphorePromise(sem, async () => { takeSemaphorePromise(sem, async () => {
const t = await retry(() => TransportNodeHid.open(devicePath), { maxRetry: 1 }) const t = await retry(() => TransportNodeHid.open(devicePath), { maxRetry: 1 })
if (process.env.DEBUG_DEVICE) t.setDebugMode(true)
if (process.env.DEBUG_DEVICE > 0) t.setDebugMode(true)
try { try {
const res = await job(t) const res = await job(t)
// $FlowFixMe // $FlowFixMe

13
src/helpers/devices/getIsGenuine.js

@ -1,6 +1,11 @@
// @flow // @flow
import type Transport from '@ledgerhq/hw-transport'
import { createSocketDialog } from 'helpers/common'
// import type Transport from '@ledgerhq/hw-transport' export default async (
transport: Transport<*>,
export default async (/* transport: Transport<*> */) => { targetId }: { targetId: string | number },
new Promise(resolve => setTimeout(() => resolve(true), 1000)) ): Promise<*> =>
process.env.SKIP_GENUINE > 0
? new Promise(resolve => setTimeout(() => resolve('0000'), 1000))
: createSocketDialog(transport, '/genuine', { targetId }, true)

2
src/helpers/firmware/installFinalFirmware.js

@ -12,7 +12,7 @@ const buildOsuParams = buildParamsFromFirmware('final')
export default async (transport: Transport<*>, firmware: Input): Result => { export default async (transport: Transport<*>, firmware: Input): Result => {
try { try {
const osuData = buildOsuParams(firmware) const osuData = buildOsuParams(firmware)
await createSocketDialog(transport, '/update/install', osuData) await createSocketDialog(transport, '/install', osuData)
return { success: true } return { success: true }
} catch (err) { } catch (err) {
const error = Error(err.message) const error = Error(err.message)

2
src/helpers/firmware/installOsuFirmware.js

@ -13,7 +13,7 @@ const buildOsuParams = buildParamsFromFirmware('osu')
export default async (transport: Transport<*>, firmware: Input): Result => { export default async (transport: Transport<*>, firmware: Input): Result => {
try { try {
const osuData = buildOsuParams(firmware) const osuData = buildOsuParams(firmware)
await createSocketDialog(transport, '/update/install', osuData) await createSocketDialog(transport, '/install', osuData)
return { success: true } return { success: true }
} catch (err) { } catch (err) {
const error = Error(err.message) const error = Error(err.message)

6
src/helpers/libcore.js

@ -312,7 +312,7 @@ function buildOperationRaw({
const bitcoinLikeTransaction = bitcoinLikeOperation.getTransaction() const bitcoinLikeTransaction = bitcoinLikeOperation.getTransaction()
const hash = bitcoinLikeTransaction.getHash() const hash = bitcoinLikeTransaction.getHash()
const operationType = op.getOperationType() const operationType = op.getOperationType()
const value = op.getAmount().toLong() let value = op.getAmount().toLong()
const fee = op.getFees().toLong() const fee = op.getFees().toLong()
const OperationTypeMap: { [_: $Keys<typeof core.OPERATION_TYPES>]: OperationType } = { const OperationTypeMap: { [_: $Keys<typeof core.OPERATION_TYPES>]: OperationType } = {
@ -323,6 +323,10 @@ function buildOperationRaw({
// if transaction is a send, amount becomes negative // if transaction is a send, amount becomes negative
const type = OperationTypeMap[operationType] const type = OperationTypeMap[operationType]
if (type === 'OUT') {
value += fee
}
return { return {
id, id,
hash, hash,

16
src/icons/TriangleWarning.js

@ -0,0 +1,16 @@
// @flow
import React from 'react'
const path = (
<path
fill="currentColor"
d="M6.217 2.188a2.085 2.085 0 0 1 3.566 0l5.653 9.437a2.083 2.083 0 0 1-1.79 3.125h-11.3A2.083 2.083 0 0 1 .57 11.615l5.647-9.427zm1.285.773l-5.64 9.414a.583.583 0 0 0 .491.875h11.285a.583.583 0 0 0 .505-.865L8.5 2.962a.583.583 0 0 0-.997-.001zM7.25 6a.75.75 0 0 1 1.5 0v2.667a.75.75 0 0 1-1.5 0V6zm1.5 5a.75.75 0 1 1-1.5 0v-.01a.75.75 0 1 1 1.5 0V11z"
/>
)
export default ({ height, width, ...p }: { height: number, width: number }) => (
<svg viewBox="0 0 19 17" height={height} width={width} {...p}>
{path}
</svg>
)

10
src/main/bridge.js

@ -27,12 +27,17 @@ sentry(() => sentryEnabled, userId)
const killInternalProcess = () => { const killInternalProcess = () => {
if (internalProcess) { if (internalProcess) {
logger.log('killing internal process...') logger.log('killing internal process...')
internalProcess.removeListener('exit', handleExit)
internalProcess.kill('SIGINT') internalProcess.kill('SIGINT')
internalProcess = null internalProcess = null
} }
} }
const forkBundlePath = path.resolve(__dirname, `${__DEV__ ? '../../' : './'}dist/internals`) const forkBundlePath = path.resolve(__dirname, `${__DEV__ ? '../../' : './'}dist/internals`)
const handleExit = code => {
logger.warn(`Internal process ended with code ${code}`)
internalProcess = null
}
const bootInternalProcess = () => { const bootInternalProcess = () => {
logger.log('booting internal process...') logger.log('booting internal process...')
@ -45,10 +50,7 @@ const bootInternalProcess = () => {
}, },
}) })
internalProcess.on('message', handleGlobalInternalMessage) internalProcess.on('message', handleGlobalInternalMessage)
internalProcess.on('exit', code => { internalProcess.on('exit', handleExit)
logger.warn(`Internal process ended with code ${code}`)
internalProcess = null
})
} }
process.on('exit', () => { process.on('exit', () => {

2
src/reducers/settings.js

@ -72,7 +72,7 @@ const INITIAL_STATE: SettingsState = {
region, region,
developerMode: !!process.env.__DEV__, developerMode: !!process.env.__DEV__,
loaded: false, loaded: false,
shareAnalytics: true, shareAnalytics: false,
sentryLogs: true, sentryLogs: true,
lastUsedVersion: __APP_VERSION__, lastUsedVersion: __APP_VERSION__,
} }

8
static/i18n/en/app.yml

@ -31,6 +31,7 @@ common:
subTitle: Your application is locked subTitle: Your application is locked
description: Please enter your password to continue description: Please enter your password to continue
inputPlaceholder: Type your password inputPlaceholder: Type your password
lostPassword: I lost my password
sync: sync:
syncing: Syncing... syncing: Syncing...
upToDate: Up to date upToDate: Up to date
@ -118,7 +119,7 @@ exchange:
coinmama: 'Coinmama is a financial service that makes it fast, safe and fun to buy digital currency, anywhere in the world.' coinmama: 'Coinmama is a financial service that makes it fast, safe and fun to buy digital currency, anywhere in the world.'
genuinecheck: genuinecheck:
modal: modal:
title: Genuine check, bro title: Genuine check
addAccounts: addAccounts:
title: Add accounts title: Add accounts
breadcrumb: breadcrumb:
@ -309,9 +310,8 @@ settings:
terms: Terms and Privacy policy terms: Terms and Privacy policy
termsDesc: Lorem ipsum dolor sit amet termsDesc: Lorem ipsum dolor sit amet
hardResetModal: hardResetModal:
title: Hard reset title: Reset Ledger Live
subTitle: Are you sure houston? desc: Resetting will erase all Ledger Live data stored on your computer, including your profile, accounts, transaction history and application settings. The keys to access your crypto assets in the blockchain remain secure on your Ledger device.
desc: Lorem ipsum dolor sit amet
softResetModal: softResetModal:
title: Clean application cache title: Clean application cache
subTitle: Are you sure houston? subTitle: Are you sure houston?

1
static/i18n/en/errors.yml

@ -11,3 +11,4 @@ LedgerAPINotAvailable: 'Ledger API is not available for currency {{currencyName}
LedgerAPIError: 'A problem occurred with Ledger API. Please try again later. (HTTP {{status}})' LedgerAPIError: 'A problem occurred with Ledger API. Please try again later. (HTTP {{status}})'
NetworkDown: 'Your internet connection seems down. Please try again later.' NetworkDown: 'Your internet connection seems down. Please try again later.'
NoAddressesFound: 'No accounts found' NoAddressesFound: 'No accounts found'
UserRefusedOnDevice: Transaction have been aborted

3
static/i18n/en/onboarding.yml

@ -74,7 +74,8 @@ writeSeed:
note4: Never use a device supplied with a recovery phrase and/or a PIN code. note4: Never use a device supplied with a recovery phrase and/or a PIN code.
genuineCheck: genuineCheck:
title: Final security check title: Final security check
desc: Your Ledger Nano S should now display Your device is now ready. Before getting started, please confirm that descNano: Your Ledger Nano S should now display Your device is now ready. Before getting started, please confirm that
descBlue: Your Ledger Blue should now display Your device is now ready. Before getting started, please confirm that
steps: steps:
step1: step1:
title: Did you choose your PIN code by yourself? title: Did you choose your PIN code by yourself?

8
static/i18n/fr/app.yml

@ -32,6 +32,7 @@ common:
subTitle: Your application is locked subTitle: Your application is locked
description: Please enter your password to continue description: Please enter your password to continue
inputPlaceholder: Type your password inputPlaceholder: Type your password
lostPassword: I lost my password
sync: sync:
syncing: Syncing... syncing: Syncing...
upToDate: Up to date upToDate: Up to date
@ -119,7 +120,7 @@ exchange:
coinmama: 'Coinmama is a financial service that makes it fast, safe and fun to buy digital currency, anywhere in the world.' coinmama: 'Coinmama is a financial service that makes it fast, safe and fun to buy digital currency, anywhere in the world.'
genuinecheck: genuinecheck:
modal: modal:
title: Genuine check, bro title: Genuine check
addAccounts: addAccounts:
title: Add accounts title: Add accounts
breadcrumb: breadcrumb:
@ -307,9 +308,8 @@ settings:
terms: Terms and Privacy policy terms: Terms and Privacy policy
termsDesc: Lorem ipsum dolor sit amet termsDesc: Lorem ipsum dolor sit amet
hardResetModal: hardResetModal:
title: Hard reset title: Reset Ledger Live
subTitle: Are you sure houston? desc: Resetting will erase all Ledger Live data stored on your computer, including your profile, accounts, transaction history and application settings. The keys to access your crypto assets in the blockchain remain secure on your Ledger device.
desc: Lorem ipsum dolor sit amet
softResetModal: softResetModal:
title: Clean application cache title: Clean application cache
subTitle: Are you sure houston? subTitle: Are you sure houston?

1
static/i18n/fr/errors.yml

@ -16,3 +16,4 @@ LedgerAPINotAvailable: 'Ledger API is not available for currency {{currencyName}
LedgerAPIError: 'A problem occurred with Ledger API. Please try again later. (HTTP {{status}})' LedgerAPIError: 'A problem occurred with Ledger API. Please try again later. (HTTP {{status}})'
NetworkDown: 'Your internet connection seems down. Please try again later.' NetworkDown: 'Your internet connection seems down. Please try again later.'
NoAddressesFound: 'No accounts found' NoAddressesFound: 'No accounts found'
UserRefusedOnDevice: Transaction have been aborted

3
static/i18n/fr/onboarding.yml

@ -75,7 +75,8 @@ writeSeed:
note4: Never use a device supplied with a recovery phrase and/or a PIN code. note4: Never use a device supplied with a recovery phrase and/or a PIN code.
genuineCheck: genuineCheck:
title: Final security check title: Final security check
desc: Your Ledger Nano S should now display Your device is now ready. Before getting started, please confirm that descNano: Your Ledger Nano S should now display Your device is now ready. Before getting started, please confirm that
descBlue: Your Ledger Blue should now display Your device is now ready. Before getting started, please confirm that
steps: steps:
step1: step1:
title: Did you choose your PIN code by yourself? title: Did you choose your PIN code by yourself?

Loading…
Cancel
Save