Browse Source

Merge pull request #1718 from LedgerHQ/develop

Prepare for 1.2.8
master
Gaëtan Renaudeau 6 years ago
committed by GitHub
parent
commit
58f1498f74
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      .flowconfig
  2. 19
      package.json
  3. 2
      src/api/Ethereum.js
  4. 2
      src/api/Fees.js
  5. 6
      src/api/network.js
  6. 2
      src/bridge/EthereumJSBridge.js
  7. 2
      src/bridge/LibcoreBridge.js
  8. 11
      src/bridge/RippleJSBridge.js
  9. BIN
      src/commands/.DS_Store
  10. 12
      src/commands/debugAppInfosForCurrency.js
  11. 17
      src/commands/firmwareMain.js
  12. 18
      src/commands/firmwarePrepare.js
  13. 14
      src/commands/firmwareRepair.js
  14. 25
      src/commands/getAddress.js
  15. 21
      src/commands/getCurrentFirmware.js
  16. 11
      src/commands/getDeviceInfo.js
  17. 16
      src/commands/getIsGenuine.js
  18. 13
      src/commands/getLatestFirmwareForDevice.js
  19. 19
      src/commands/getMemInfo.js
  20. 26
      src/commands/index.js
  21. 20
      src/commands/installApp.js
  22. 21
      src/commands/installFinalFirmware.js
  23. 19
      src/commands/installMcu.js
  24. 27
      src/commands/installOsuFirmware.js
  25. 19
      src/commands/isDashboardOpen.js
  26. 5
      src/commands/libcoreGetFees.js
  27. 45
      src/commands/libcoreSignAndBroadcast.js
  28. 15
      src/commands/listAppVersions.js
  29. 15
      src/commands/listApps.js
  30. 17
      src/commands/listCategories.js
  31. 15
      src/commands/shouldFlashMcu.js
  32. 10
      src/commands/signTransaction.js
  33. 15
      src/commands/testApdu.js
  34. 18
      src/commands/uninstallApp.js
  35. 1
      src/components/App.js
  36. 6
      src/components/EnsureDeviceApp.js
  37. 13
      src/components/ExportLogsBtn.js
  38. 4
      src/components/FeesField/BitcoinKind.js
  39. 2
      src/components/FeesField/EthereumKind.js
  40. 2
      src/components/FeesField/RippleKind.js
  41. 27
      src/components/GenuineCheck.js
  42. 171
      src/components/HSMStatusBanner.js
  43. 2
      src/components/IsUnlocked.js
  44. 2
      src/components/ManagerPage/AppSearchBar.js
  45. 116
      src/components/ManagerPage/AppsList.js
  46. 2
      src/components/ManagerPage/Dashboard.js
  47. 85
      src/components/ManagerPage/FirmwareUpdate.js
  48. 13
      src/components/ManagerPage/UpdateFirmwareButton.js
  49. 2
      src/components/ManagerPage/index.js
  50. 64
      src/components/ProgressBar/index.js
  51. 16
      src/components/ProgressBar/stories.js
  52. 98
      src/components/ProgressCircle/index.js
  53. 16
      src/components/ProgressCircle/stories.js
  54. 2
      src/components/SettingsPage/DisablePasswordModal.js
  55. 2
      src/components/SettingsPage/PasswordForm.js
  56. 2
      src/components/SettingsPage/PasswordModal.js
  57. 103
      src/components/SettingsPage/RepairDeviceButton.js
  58. 10
      src/components/SettingsPage/sections/Help.js
  59. 4
      src/components/base/Modal/ConfirmModal.js
  60. 203
      src/components/base/Modal/RepairModal.js
  61. 1
      src/components/base/Modal/index.js
  62. 2
      src/components/layout/Default.js
  63. 2
      src/components/modals/AccountSettingRenderBody.js
  64. 12
      src/components/modals/OperationDetails.js
  65. 19
      src/components/modals/Receive/steps/04-step-receive-funds.js
  66. 4
      src/components/modals/Send/fields/RecipientField.js
  67. 2
      src/components/modals/Send/index.js
  68. 34
      src/components/modals/UpdateFirmware/Disclaimer.js
  69. 10
      src/components/modals/UpdateFirmware/Installing.js
  70. 35
      src/components/modals/UpdateFirmware/index.js
  71. 115
      src/components/modals/UpdateFirmware/steps/01-step-install-full-firmware.js
  72. 117
      src/components/modals/UpdateFirmware/steps/02-step-flash-mcu.js
  73. 7
      src/config/constants.js
  74. 53
      src/config/errors.js
  75. 52
      src/helpers/apps/installApp.js
  76. 30
      src/helpers/apps/listAppVersions.js
  77. 10
      src/helpers/apps/listApps.js
  78. 10
      src/helpers/apps/listCategories.js
  79. 41
      src/helpers/apps/uninstallApp.js
  80. 5
      src/helpers/db/db.spec.js
  81. 6
      src/helpers/db/index.js
  82. 9
      src/helpers/debugAppInfosForCurrency/btc.js
  83. 10
      src/helpers/debugAppInfosForCurrency/ethereum.js
  84. 29
      src/helpers/debugAppInfosForCurrency/index.js
  85. 10
      src/helpers/debugAppInfosForCurrency/ripple.js
  86. 64
      src/helpers/deviceAccess.js
  87. 21
      src/helpers/devices/getBitcoinLikeInfo.js
  88. 24
      src/helpers/devices/getCurrentFirmware.js
  89. 42
      src/helpers/devices/getDeviceInfo.js
  90. 17
      src/helpers/devices/getDeviceVersion.js
  91. 32
      src/helpers/devices/getIsGenuine.js
  92. 76
      src/helpers/devices/getLatestFirmwareForDevice.js
  93. 10
      src/helpers/devices/getMemInfo.js
  94. 23
      src/helpers/devices/getOsuFirmware.js
  95. 16
      src/helpers/devices/isDashboardOpen.js
  96. 49
      src/helpers/devices/shouldFlashMcu.js
  97. 96
      src/helpers/errors.js
  98. 8
      src/helpers/firmware/getFinalFirmwareById.js
  99. 51
      src/helpers/firmware/getFirmwareInfo.js
  100. 13
      src/helpers/firmware/getMcus.js

1
.flowconfig

@ -5,6 +5,7 @@
[untyped] [untyped]
.*/node_modules/react-select .*/node_modules/react-select
<PROJECT_ROOT>/node_modules/qrloop/lib/Buffer.js*
[include] [include]

19
package.json

@ -35,13 +35,13 @@
} }
}, },
"dependencies": { "dependencies": {
"@ledgerhq/hw-app-btc": "^v4.30.0", "@ledgerhq/hw-app-btc": "^4.32.0",
"@ledgerhq/hw-app-eth": "^4.24.0", "@ledgerhq/hw-app-eth": "^4.32.0",
"@ledgerhq/hw-app-xrp": "^4.25.0", "@ledgerhq/hw-app-xrp": "^4.32.0",
"@ledgerhq/hw-transport": "^4.24.0", "@ledgerhq/hw-transport": "^4.32.0",
"@ledgerhq/hw-transport-node-hid": "4.24.0", "@ledgerhq/hw-transport-node-hid": "^4.32.0",
"@ledgerhq/ledger-core": "2.0.0-rc.12", "@ledgerhq/ledger-core": "2.0.0-rc.14",
"@ledgerhq/live-common": "4.6.0", "@ledgerhq/live-common": "4.8.0-beta.23",
"animated": "^0.2.2", "animated": "^0.2.2",
"async": "^2.6.1", "async": "^2.6.1",
"axios": "^0.18.0", "axios": "^0.18.0",
@ -109,7 +109,6 @@
"uncontrollable": "^6.0.0", "uncontrollable": "^6.0.0",
"uuid": "^3.2.1", "uuid": "^3.2.1",
"winston": "^3.0.0", "winston": "^3.0.0",
"winston-daily-rotate-file": "^3.2.3",
"winston-transport": "^4.2.0", "winston-transport": "^4.2.0",
"write-file-atomic": "^2.3.0", "write-file-atomic": "^2.3.0",
"ws": "^5.1.1", "ws": "^5.1.1",
@ -186,8 +185,8 @@
"yaml-loader": "^0.5.0" "yaml-loader": "^0.5.0"
}, },
"engines": { "engines": {
"node": ">=8.9.0 <=8.12.0", "node": ">=8.9.0 <=8.14.0",
"yarn": "^1.10.1" "yarn": "^1.10.1"
}, },
"private": true "private": true
} }

2
src/api/Ethereum.js

@ -1,7 +1,7 @@
// @flow // @flow
import type { CryptoCurrency } from '@ledgerhq/live-common/lib/types' import type { CryptoCurrency } from '@ledgerhq/live-common/lib/types'
import { BigNumber } from 'bignumber.js' import { BigNumber } from 'bignumber.js'
import { LedgerAPINotAvailable } from 'config/errors' import { LedgerAPINotAvailable } from '@ledgerhq/live-common/lib/errors'
import network from './network' import network from './network'
import { blockchainBaseURL } from './Ledger' import { blockchainBaseURL } from './Ledger'

2
src/api/Fees.js

@ -2,7 +2,7 @@
import invariant from 'invariant' import invariant from 'invariant'
import LRU from 'lru-cache' import LRU from 'lru-cache'
import type { Currency } from '@ledgerhq/live-common/lib/types' import type { Currency } from '@ledgerhq/live-common/lib/types'
import { FeeEstimationFailed } from 'config/errors' import { FeeEstimationFailed } from '@ledgerhq/live-common/lib/errors'
import { blockchainBaseURL } from './Ledger' import { blockchainBaseURL } from './Ledger'
import network from './network' import network from './network'

6
src/api/network.js

@ -3,7 +3,11 @@ import axios from 'axios'
import { GET_CALLS_RETRY, GET_CALLS_TIMEOUT } from 'config/constants' import { GET_CALLS_RETRY, GET_CALLS_TIMEOUT } from 'config/constants'
import { retry } from 'helpers/promise' import { retry } from 'helpers/promise'
import logger from 'logger' import logger from 'logger'
import { LedgerAPIErrorWithMessage, LedgerAPIError, NetworkDown } from 'config/errors' import {
LedgerAPIErrorWithMessage,
LedgerAPIError,
NetworkDown,
} from '@ledgerhq/live-common/lib/errors'
import anonymizer from 'helpers/anonymizer' import anonymizer from 'helpers/anonymizer'
const userFriendlyError = <A>(p: Promise<A>, { url, method, startTime, ...rest }): Promise<A> => const userFriendlyError = <A>(p: Promise<A>, { url, method, startTime, ...rest }): Promise<A> =>

2
src/bridge/EthereumJSBridge.js

@ -25,7 +25,7 @@ import { apiForCurrency } from 'api/Ethereum'
import type { Tx } from 'api/Ethereum' import type { Tx } from 'api/Ethereum'
import getAddressCommand from 'commands/getAddress' import getAddressCommand from 'commands/getAddress'
import signTransactionCommand from 'commands/signTransaction' import signTransactionCommand from 'commands/signTransaction'
import { NotEnoughBalance, FeeNotLoaded, ETHAddressNonEIP } from 'config/errors' import { NotEnoughBalance, FeeNotLoaded, ETHAddressNonEIP } from '@ledgerhq/live-common/lib/errors'
import type { EditProps, WalletBridge } from './types' import type { EditProps, WalletBridge } from './types'
type Transaction = { type Transaction = {

2
src/bridge/LibcoreBridge.js

@ -11,7 +11,7 @@ import libcoreSyncAccount from 'commands/libcoreSyncAccount'
import libcoreSignAndBroadcast from 'commands/libcoreSignAndBroadcast' import libcoreSignAndBroadcast from 'commands/libcoreSignAndBroadcast'
import libcoreGetFees, { extractGetFeesInputFromAccount } from 'commands/libcoreGetFees' import libcoreGetFees, { extractGetFeesInputFromAccount } from 'commands/libcoreGetFees'
import libcoreValidAddress from 'commands/libcoreValidAddress' import libcoreValidAddress from 'commands/libcoreValidAddress'
import { NotEnoughBalance, FeeNotLoaded } from 'config/errors' import { NotEnoughBalance, FeeNotLoaded } from '@ledgerhq/live-common/lib/errors'
import type { WalletBridge, EditProps } from './types' import type { WalletBridge, EditProps } from './types'
const NOT_ENOUGH_FUNDS = 52 const NOT_ENOUGH_FUNDS = 52

11
src/bridge/RippleJSBridge.js

@ -32,7 +32,7 @@ import {
NotEnoughBalance, NotEnoughBalance,
FeeNotLoaded, FeeNotLoaded,
NotEnoughBalanceBecauseDestinationNotCreated, NotEnoughBalanceBecauseDestinationNotCreated,
} from 'config/errors' } from '@ledgerhq/live-common/lib/errors'
import type { WalletBridge, EditProps } from './types' import type { WalletBridge, EditProps } from './types'
type Transaction = { type Transaction = {
@ -123,6 +123,10 @@ async function signAndBroadcast({ a, t, deviceId, isCancelled, onSigned, onOpera
a.pendingOperations.length, a.pendingOperations.length,
extra: {}, extra: {},
} }
if (t.tag) {
op.extra.tag = t.tag
}
onOperationBroadcasted(op) onOperationBroadcasted(op)
} }
} finally { } finally {
@ -164,6 +168,7 @@ type Tx = {
currency: string, currency: string,
value: string, value: string,
}, },
tag?: string,
}, },
paths: string, paths: string,
}, },
@ -235,6 +240,10 @@ const txToOperation = (account: Account) => ({
transactionSequenceNumber: sequence, transactionSequenceNumber: sequence,
extra: {}, extra: {},
} }
if (destination.tag) {
op.extra.tag = destination.tag
}
return op return op
} }

BIN
src/commands/.DS_Store

Binary file not shown.

12
src/commands/debugAppInfosForCurrency.js

@ -1,10 +1,10 @@
// @flow // @flow
import { getCryptoCurrencyById } from '@ledgerhq/live-common/lib/currencies' import { getCryptoCurrencyById } from '@ledgerhq/live-common/lib/currencies'
import debugAppInfosForCurrency from '@ledgerhq/live-common/lib/hw/debugAppInfosForCurrency'
import { createCommand, Command } from 'helpers/ipc' import { createCommand, Command } from 'helpers/ipc'
import { fromPromise } from 'rxjs/observable/fromPromise' import { from } from 'rxjs'
import { withDevice } from 'helpers/deviceAccess' import { withDevice } from '@ledgerhq/live-common/lib/hw/deviceAccess'
import debugAppInfosForCurrency from 'helpers/debugAppInfosForCurrency'
type Input = { type Input = {
currencyId: string, currencyId: string,
@ -18,10 +18,8 @@ type Result = {
const cmd: Command<Input, Result> = createCommand( const cmd: Command<Input, Result> = createCommand(
'debugAppInfosForCurrency', 'debugAppInfosForCurrency',
({ currencyId, devicePath }) => ({ currencyId, devicePath }) =>
fromPromise( withDevice(devicePath)(transport =>
withDevice(devicePath)(transport => from(debugAppInfosForCurrency(transport, getCryptoCurrencyById(currencyId))),
debugAppInfosForCurrency(transport, getCryptoCurrencyById(currencyId)),
),
), ),
) )

17
src/commands/firmwareMain.js

@ -0,0 +1,17 @@
// @flow
import main from '@ledgerhq/live-common/lib/hw/firmwareUpdate-main'
import type { FirmwareUpdateContext } from '@ledgerhq/live-common/lib/types/manager'
import { createCommand, Command } from 'helpers/ipc'
type Input = FirmwareUpdateContext
type Result = { progress: number, installing: ?string }
const cmd: Command<Input, Result> = createCommand(
'firmwareMain',
firmware => main('', firmware),
// devicePath='' HACK to not depend on a devicePath because it's dynamic
)
export default cmd

18
src/commands/firmwarePrepare.js

@ -0,0 +1,18 @@
// @flow
import prepare from '@ledgerhq/live-common/lib/hw/firmwareUpdate-prepare'
import type { FirmwareUpdateContext } from '@ledgerhq/live-common/lib/types/manager'
import { createCommand, Command } from 'helpers/ipc'
type Input = {
devicePath: string,
firmware: FirmwareUpdateContext,
}
type Result = { progress: number }
const cmd: Command<Input, Result> = createCommand('firmwarePrepare', ({ devicePath, firmware }) =>
prepare(devicePath, firmware),
)
export default cmd

14
src/commands/firmwareRepair.js

@ -0,0 +1,14 @@
// @flow
import repair from '@ledgerhq/live-common/lib/hw/firmwareUpdate-repair'
import { createCommand, Command } from 'helpers/ipc'
type Input = void
type Result = { progress: number }
const cmd: Command<Input, Result> = createCommand(
'firmwareRepair',
() => repair(''), // devicePath='' HACK to not depend on a devicePath because it's dynamic
)
export default cmd

25
src/commands/getAddress.js

@ -2,18 +2,15 @@
import { getCryptoCurrencyById } from '@ledgerhq/live-common/lib/currencies' import { getCryptoCurrencyById } from '@ledgerhq/live-common/lib/currencies'
import { createCommand, Command } from 'helpers/ipc' import { createCommand, Command } from 'helpers/ipc'
import { fromPromise } from 'rxjs/observable/fromPromise' import { from } from 'rxjs'
import { withDevice } from 'helpers/deviceAccess' import { withDevice } from '@ledgerhq/live-common/lib/hw/deviceAccess'
import getAddressForCurrency from 'helpers/getAddressForCurrency' import getAddress from '@ledgerhq/live-common/lib/hw/getAddress'
import { DeviceAppVerifyNotSupported, UserRefusedAddress } from 'config/errors'
type Input = { type Input = {
currencyId: string, currencyId: string,
devicePath: string, devicePath: string,
path: string, path: string,
verify?: boolean, verify?: boolean,
segwit?: boolean,
} }
type Result = { type Result = {
@ -25,20 +22,8 @@ type Result = {
const cmd: Command<Input, Result> = createCommand( const cmd: Command<Input, Result> = createCommand(
'getAddress', 'getAddress',
({ currencyId, devicePath, path, ...options }) => ({ currencyId, devicePath, path, ...options }) =>
fromPromise( withDevice(devicePath)(transport =>
withDevice(devicePath)(transport => from(getAddress(transport, getCryptoCurrencyById(currencyId), path, options.verify)),
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
}),
), ),
) )

21
src/commands/getCurrentFirmware.js

@ -1,21 +0,0 @@
// @flow
import { createCommand, Command } from 'helpers/ipc'
import { fromPromise } from 'rxjs/observable/fromPromise'
import getCurrentFirmware from 'helpers/devices/getCurrentFirmware'
import type { FinalFirmware } from 'helpers/types'
type Input = {
deviceId: string | number,
fullVersion: string,
provider: number,
}
type Result = FinalFirmware
const cmd: Command<Input, Result> = createCommand('getCurrentFirmware', data =>
fromPromise(getCurrentFirmware(data)),
)
export default cmd

11
src/commands/getDeviceInfo.js

@ -1,11 +1,10 @@
// @flow // @flow
import { createCommand, Command } from 'helpers/ipc' import { createCommand, Command } from 'helpers/ipc'
import { fromPromise } from 'rxjs/observable/fromPromise' import { from } from 'rxjs'
import { withDevice } from 'helpers/deviceAccess' import { withDevice } from '@ledgerhq/live-common/lib/hw/deviceAccess'
import getDeviceInfo from '@ledgerhq/live-common/lib/hw/getDeviceInfo'
import getDeviceInfo from 'helpers/devices/getDeviceInfo' import type { DeviceInfo } from '@ledgerhq/live-common/lib/types/manager'
import type { DeviceInfo } from 'helpers/types'
type Input = { type Input = {
devicePath: string, devicePath: string,
@ -14,7 +13,7 @@ type Input = {
type Result = DeviceInfo type Result = DeviceInfo
const cmd: Command<Input, Result> = createCommand('getDeviceInfo', ({ devicePath }) => const cmd: Command<Input, Result> = createCommand('getDeviceInfo', ({ devicePath }) =>
fromPromise(withDevice(devicePath)(transport => getDeviceInfo(transport))), withDevice(devicePath)(transport => from(getDeviceInfo(transport))),
) )
export default cmd export default cmd

16
src/commands/getIsGenuine.js

@ -1,11 +1,12 @@
// @flow // @flow
import { createCommand, Command } from 'helpers/ipc' import { createCommand, Command } from 'helpers/ipc'
import { fromPromise } from 'rxjs/observable/fromPromise' import { of } from 'rxjs'
import type { DeviceInfo } from 'helpers/types' import { delay } from 'rxjs/operators'
import genuineCheck from '@ledgerhq/live-common/lib/hw/genuineCheck'
import getIsGenuine from 'helpers/devices/getIsGenuine' import { withDevice } from '@ledgerhq/live-common/lib/hw/deviceAccess'
import { withDevice } from 'helpers/deviceAccess' import type { DeviceInfo } from '@ledgerhq/live-common/lib/types/manager'
import { SKIP_GENUINE } from 'config/constants'
type Input = { type Input = {
devicePath: string, devicePath: string,
@ -14,7 +15,10 @@ type Input = {
type Result = string type Result = string
const cmd: Command<Input, Result> = createCommand('getIsGenuine', ({ devicePath, deviceInfo }) => const cmd: Command<Input, Result> = createCommand('getIsGenuine', ({ devicePath, deviceInfo }) =>
fromPromise(withDevice(devicePath)(transport => getIsGenuine(transport, deviceInfo))), withDevice(devicePath)(
transport =>
SKIP_GENUINE ? of('0000').pipe(delay(1000)) : genuineCheck(transport, deviceInfo),
),
) )
export default cmd export default cmd

13
src/commands/getLatestFirmwareForDevice.js

@ -1,15 +1,14 @@
// @flow // @flow
import { createCommand, Command } from 'helpers/ipc' import { createCommand, Command } from 'helpers/ipc'
import { fromPromise } from 'rxjs/observable/fromPromise' import { from } from 'rxjs'
import type { DeviceInfo, OsuFirmware } from 'helpers/types' import type { DeviceInfo, FirmwareUpdateContext } from '@ledgerhq/live-common/lib/types/manager'
import manager from '@ledgerhq/live-common/lib/manager'
import getLatestFirmwareForDevice from '../helpers/devices/getLatestFirmwareForDevice' type Result = ?FirmwareUpdateContext
type Result = ?(OsuFirmware & { shouldFlashMcu: boolean }) const cmd: Command<DeviceInfo, Result> = createCommand('getLatestFirmwareForDevice', deviceInfo =>
from(manager.getLatestFirmwareForDevice(deviceInfo)),
const cmd: Command<DeviceInfo, Result> = createCommand('getLatestFirmwareForDevice', data =>
fromPromise(getLatestFirmwareForDevice(data)),
) )
export default cmd export default cmd

19
src/commands/getMemInfo.js

@ -1,19 +0,0 @@
// @flow
import { createCommand, Command } from 'helpers/ipc'
import { fromPromise } from 'rxjs/observable/fromPromise'
import { withDevice } from 'helpers/deviceAccess'
import getMemInfo from 'helpers/devices/getMemInfo'
type Input = {
devicePath: string,
}
type Result = *
const cmd: Command<Input, Result> = createCommand('getMemInfo', ({ devicePath }) =>
fromPromise(withDevice(devicePath)(transport => getMemInfo(transport))),
)
export default cmd

26
src/commands/index.js

@ -4,17 +4,14 @@ import invariant from 'invariant'
import type { Command } from 'helpers/ipc' import type { Command } from 'helpers/ipc'
import debugAppInfosForCurrency from 'commands/debugAppInfosForCurrency' import debugAppInfosForCurrency from 'commands/debugAppInfosForCurrency'
import firmwarePrepare from 'commands/firmwarePrepare'
import firmwareMain from 'commands/firmwareMain'
import firmwareRepair from 'commands/firmwareRepair'
import getAddress from 'commands/getAddress' import getAddress from 'commands/getAddress'
import getDeviceInfo from 'commands/getDeviceInfo' import getDeviceInfo from 'commands/getDeviceInfo'
import getCurrentFirmware from 'commands/getCurrentFirmware'
import getIsGenuine from 'commands/getIsGenuine' import getIsGenuine from 'commands/getIsGenuine'
import getLatestFirmwareForDevice from 'commands/getLatestFirmwareForDevice' import getLatestFirmwareForDevice from 'commands/getLatestFirmwareForDevice'
import getMemInfo from 'commands/getMemInfo'
import installApp from 'commands/installApp' import installApp from 'commands/installApp'
import installFinalFirmware from 'commands/installFinalFirmware'
import installMcu from 'commands/installMcu'
import installOsuFirmware from 'commands/installOsuFirmware'
import isDashboardOpen from 'commands/isDashboardOpen'
import killInternalProcess from 'commands/killInternalProcess' import killInternalProcess from 'commands/killInternalProcess'
import libcoreGetFees from 'commands/libcoreGetFees' import libcoreGetFees from 'commands/libcoreGetFees'
import libcoreGetVersion from 'commands/libcoreGetVersion' import libcoreGetVersion from 'commands/libcoreGetVersion'
@ -23,12 +20,8 @@ import libcoreScanFromXPUB from 'commands/libcoreScanFromXPUB'
import libcoreSignAndBroadcast from 'commands/libcoreSignAndBroadcast' import libcoreSignAndBroadcast from 'commands/libcoreSignAndBroadcast'
import libcoreSyncAccount from 'commands/libcoreSyncAccount' import libcoreSyncAccount from 'commands/libcoreSyncAccount'
import libcoreValidAddress from 'commands/libcoreValidAddress' import libcoreValidAddress from 'commands/libcoreValidAddress'
import listApps from 'commands/listApps'
import listAppVersions from 'commands/listAppVersions'
import listCategories from 'commands/listCategories'
import listenDevices from 'commands/listenDevices' import listenDevices from 'commands/listenDevices'
import ping from 'commands/ping' import ping from 'commands/ping'
import shouldFlashMcu from 'commands/shouldFlashMcu'
import signTransaction from 'commands/signTransaction' import signTransaction from 'commands/signTransaction'
import testApdu from 'commands/testApdu' import testApdu from 'commands/testApdu'
import testCrash from 'commands/testCrash' import testCrash from 'commands/testCrash'
@ -37,17 +30,14 @@ import uninstallApp from 'commands/uninstallApp'
const all: Array<Command<any, any>> = [ const all: Array<Command<any, any>> = [
debugAppInfosForCurrency, debugAppInfosForCurrency,
firmwarePrepare,
firmwareMain,
firmwareRepair,
getAddress, getAddress,
getDeviceInfo, getDeviceInfo,
getCurrentFirmware,
getIsGenuine, getIsGenuine,
getLatestFirmwareForDevice, getLatestFirmwareForDevice,
getMemInfo,
installApp, installApp,
installFinalFirmware,
installMcu,
installOsuFirmware,
isDashboardOpen,
killInternalProcess, killInternalProcess,
libcoreGetFees, libcoreGetFees,
libcoreGetVersion, libcoreGetVersion,
@ -56,12 +46,8 @@ const all: Array<Command<any, any>> = [
libcoreSignAndBroadcast, libcoreSignAndBroadcast,
libcoreSyncAccount, libcoreSyncAccount,
libcoreValidAddress, libcoreValidAddress,
listApps,
listAppVersions,
listCategories,
listenDevices, listenDevices,
ping, ping,
shouldFlashMcu,
signTransaction, signTransaction,
testApdu, testApdu,
testCrash, testCrash,

20
src/commands/installApp.js

@ -1,25 +1,19 @@
// @flow // @flow
import { createCommand, Command } from 'helpers/ipc' import { createCommand, Command } from 'helpers/ipc'
import { fromPromise } from 'rxjs/observable/fromPromise' import installApp from '@ledgerhq/live-common/lib/hw/installApp'
import { withDevice } from '@ledgerhq/live-common/lib/hw/deviceAccess'
import { withDevice } from 'helpers/deviceAccess' import type { ApplicationVersion } from '@ledgerhq/live-common/lib/types/manager'
import installApp from 'helpers/apps/installApp'
import type { ApplicationVersion } from 'helpers/types'
type Input = { type Input = {
app: ApplicationVersion,
devicePath: string, devicePath: string,
targetId: string | number, targetId: string | number,
app: ApplicationVersion,
} }
type Result = void type Result = { progress: number }
const cmd: Command<Input, Result> = createCommand( const cmd: Command<Input, Result> = createCommand('installApp', ({ devicePath, targetId, app }) =>
'installApp', withDevice(devicePath)(transport => installApp(transport, targetId, app)),
({ devicePath, targetId, ...app }) =>
fromPromise(withDevice(devicePath)(transport => installApp(transport, targetId, app))),
) )
export default cmd export default cmd

21
src/commands/installFinalFirmware.js

@ -1,21 +0,0 @@
// @flow
import { createCommand, Command } from 'helpers/ipc'
import { fromPromise } from 'rxjs/observable/fromPromise'
import { withDevice } from 'helpers/deviceAccess'
import installFinalFirmware from 'helpers/firmware/installFinalFirmware'
type Input = {
devicePath: string,
}
type Result = {
success: boolean,
}
const cmd: Command<Input, Result> = createCommand('installFinalFirmware', ({ devicePath }) =>
fromPromise(withDevice(devicePath)(transport => installFinalFirmware(transport))),
)
export default cmd

19
src/commands/installMcu.js

@ -1,19 +0,0 @@
// @flow
import { createCommand, Command } from 'helpers/ipc'
import { fromPromise } from 'rxjs/observable/fromPromise'
import { withDevice } from 'helpers/deviceAccess'
import installMcu from 'helpers/firmware/installMcu'
type Input = {
devicePath: string,
}
type Result = void
const cmd: Command<Input, Result> = createCommand('installMcu', ({ devicePath }) =>
fromPromise(withDevice(devicePath)(transport => installMcu(transport))),
)
export default cmd

27
src/commands/installOsuFirmware.js

@ -1,27 +0,0 @@
// @flow
import { createCommand, Command } from 'helpers/ipc'
import { fromPromise } from 'rxjs/observable/fromPromise'
import { withDevice } from 'helpers/deviceAccess'
import installOsuFirmware from 'helpers/firmware/installOsuFirmware'
import type { Firmware } from 'components/modals/UpdateFirmware'
type Input = {
devicePath: string,
targetId: string | number,
firmware: Firmware,
}
type Result = { success: boolean }
const cmd: Command<Input, Result> = createCommand(
'installOsuFirmware',
({ devicePath, firmware, targetId }) =>
fromPromise(
withDevice(devicePath)(transport => installOsuFirmware(transport, targetId, firmware)),
),
)
export default cmd

19
src/commands/isDashboardOpen.js

@ -1,19 +0,0 @@
// @flow
import { createCommand, Command } from 'helpers/ipc'
import { fromPromise } from 'rxjs/observable/fromPromise'
import { withDevice } from 'helpers/deviceAccess'
import isDashboardOpen from '../helpers/devices/isDashboardOpen'
type Input = {
devicePath: string,
}
type Result = boolean
const cmd: Command<Input, Result> = createCommand('isDashboardOpen', ({ devicePath }) =>
fromPromise(withDevice(devicePath)(transport => isDashboardOpen(transport))),
)
export default cmd

5
src/commands/libcoreGetFees.js

@ -13,7 +13,7 @@ import {
bigNumberToLibcoreAmount, bigNumberToLibcoreAmount,
getOrCreateWallet, getOrCreateWallet,
} from 'helpers/libcore' } from 'helpers/libcore'
import { InvalidAddress } from 'config/errors' import { InvalidAddress } from '@ledgerhq/live-common/lib/errors'
type BitcoinLikeTransaction = { type BitcoinLikeTransaction = {
// TODO we rename this Transaction concept into transactionInput // TODO we rename this Transaction concept into transactionInput
@ -75,7 +75,8 @@ const cmd: Command<Input, Result> = createCommand(
njsWalletCurrency, njsWalletCurrency,
BigNumber(transaction.feePerByte), BigNumber(transaction.feePerByte),
) )
const transactionBuilder = bitcoinLikeAccount.buildTransaction() const isPartial = true
const transactionBuilder = bitcoinLikeAccount.buildTransaction(isPartial)
if (!isValidAddress(core, njsWalletCurrency, transaction.recipient)) { if (!isValidAddress(core, njsWalletCurrency, transaction.recipient)) {
// FIXME this is a bug in libcore. later it will probably check this and we can remove this check // FIXME this is a bug in libcore. later it will probably check this and we can remove this check
throw new InvalidAddress() throw new InvalidAddress()

45
src/commands/libcoreSignAndBroadcast.js

@ -4,7 +4,7 @@ import logger from 'logger'
import { BigNumber } from 'bignumber.js' import { BigNumber } from 'bignumber.js'
import { StatusCodes } from '@ledgerhq/hw-transport' import { StatusCodes } from '@ledgerhq/hw-transport'
import Btc from '@ledgerhq/hw-app-btc' import Btc from '@ledgerhq/hw-app-btc'
import { Observable } from 'rxjs' import { Observable, from } from 'rxjs'
import { isSegwitDerivationMode } from '@ledgerhq/live-common/lib/derivation' import { isSegwitDerivationMode } from '@ledgerhq/live-common/lib/derivation'
import { getCryptoCurrencyById } from '@ledgerhq/live-common/lib/currencies' import { getCryptoCurrencyById } from '@ledgerhq/live-common/lib/currencies'
import type { OperationRaw, DerivationMode, CryptoCurrency } from '@ledgerhq/live-common/lib/types' import type { OperationRaw, DerivationMode, CryptoCurrency } from '@ledgerhq/live-common/lib/types'
@ -14,11 +14,11 @@ import {
bigNumberToLibcoreAmount, bigNumberToLibcoreAmount,
getOrCreateWallet, getOrCreateWallet,
} from 'helpers/libcore' } from 'helpers/libcore'
import { UpdateYourApp } from 'config/errors' import { UpdateYourApp } from '@ledgerhq/live-common/lib/errors'
import withLibcore from 'helpers/withLibcore' import withLibcore from 'helpers/withLibcore'
import { createCommand, Command } from 'helpers/ipc' import { createCommand, Command } from 'helpers/ipc'
import { withDevice } from 'helpers/deviceAccess' import { withDevice } from '@ledgerhq/live-common/lib/hw/deviceAccess'
type BitcoinLikeTransaction = { type BitcoinLikeTransaction = {
amount: string, amount: string,
@ -227,7 +227,8 @@ export async function doSignAndBroadcast({
const njsWalletCurrency = njsWallet.getCurrency() const njsWalletCurrency = njsWallet.getCurrency()
const amount = bigNumberToLibcoreAmount(core, njsWalletCurrency, BigNumber(transaction.amount)) const amount = bigNumberToLibcoreAmount(core, njsWalletCurrency, BigNumber(transaction.amount))
const fees = bigNumberToLibcoreAmount(core, njsWalletCurrency, BigNumber(transaction.feePerByte)) const fees = bigNumberToLibcoreAmount(core, njsWalletCurrency, BigNumber(transaction.feePerByte))
const transactionBuilder = bitcoinLikeAccount.buildTransaction() const isPartial = false
const transactionBuilder = bitcoinLikeAccount.buildTransaction(isPartial)
// TODO: check if is valid address. if not, it will fail silently on invalid // TODO: check if is valid address. if not, it will fail silently on invalid
@ -245,22 +246,26 @@ export async function doSignAndBroadcast({
const hasTimestamp = !!njsWalletCurrency.bitcoinLikeNetworkParameters.UsesTimestampedTransaction const hasTimestamp = !!njsWalletCurrency.bitcoinLikeNetworkParameters.UsesTimestampedTransaction
// TODO: const timestampDelay = njsWalletCurrency.bitcoinLikeNetworkParameters.TimestampDelay // TODO: const timestampDelay = njsWalletCurrency.bitcoinLikeNetworkParameters.TimestampDelay
const signedTransaction = await withDevice(deviceId)(async transport => const signedTransaction = await withDevice(deviceId)(transport =>
signTransaction({ from(
hwApp: new Btc(transport), signTransaction({
currency, hwApp: new Btc(transport),
blockHeight, currency,
transaction: builded, blockHeight,
sigHashType: parseInt(sigHashType, 16), transaction: builded,
hasTimestamp, sigHashType: parseInt(sigHashType, 16),
derivationMode, hasTimestamp,
}), derivationMode,
).catch(e => { }),
if (e && e.statusCode === StatusCodes.INCORRECT_P1_P2) { ),
throw new UpdateYourApp(`UpdateYourApp ${currency.id}`, currency) )
} .toPromise()
throw e .catch(e => {
}) if (e && e.statusCode === StatusCodes.INCORRECT_P1_P2) {
throw new UpdateYourApp(`UpdateYourApp ${currency.id}`, currency)
}
throw e
})
if (!signedTransaction || isCancelled() || !njsAccount) return if (!signedTransaction || isCancelled() || !njsAccount) return
onSigned() onSigned()

15
src/commands/listAppVersions.js

@ -1,15 +0,0 @@
// @flow
import { createCommand, Command } from 'helpers/ipc'
import { fromPromise } from 'rxjs/observable/fromPromise'
import type { DeviceInfo, ApplicationVersion } from 'helpers/types'
import listAppVersions from 'helpers/apps/listAppVersions'
type Result = Array<ApplicationVersion>
const cmd: Command<DeviceInfo, Result> = createCommand('listAppVersions', deviceInfo =>
fromPromise(listAppVersions(deviceInfo)),
)
export default cmd

15
src/commands/listApps.js

@ -1,15 +0,0 @@
// @flow
import { createCommand, Command } from 'helpers/ipc'
import { fromPromise } from 'rxjs/observable/fromPromise'
import listApps from 'helpers/apps/listApps'
import type { Application } from 'helpers/types'
type Input = void
type Result = Array<Application>
const cmd: Command<Input, Result> = createCommand('listApps', () => fromPromise(listApps()))
export default cmd

17
src/commands/listCategories.js

@ -1,17 +0,0 @@
// @flow
import { createCommand, Command } from 'helpers/ipc'
import { fromPromise } from 'rxjs/observable/fromPromise'
import listCategories from 'helpers/apps/listCategories'
import type { Category } from 'helpers/types'
type Input = void
type Result = Array<Category>
const cmd: Command<Input, Result> = createCommand('listCategories', () =>
fromPromise(listCategories()),
)
export default cmd

15
src/commands/shouldFlashMcu.js

@ -1,15 +0,0 @@
// @flow
import { createCommand, Command } from 'helpers/ipc'
import { fromPromise } from 'rxjs/observable/fromPromise'
import shouldFlashMcu from 'helpers/devices/shouldFlashMcu'
import type { DeviceInfo } from 'helpers/types'
type Result = boolean
const cmd: Command<DeviceInfo, Result> = createCommand('shouldFlashMcu', data =>
fromPromise(shouldFlashMcu(data)),
)
export default cmd

10
src/commands/signTransaction.js

@ -1,8 +1,8 @@
// @flow // @flow
import { createCommand, Command } from 'helpers/ipc' import { createCommand, Command } from 'helpers/ipc'
import { fromPromise } from 'rxjs/observable/fromPromise' import { from } from 'rxjs'
import { withDevice } from 'helpers/deviceAccess' import { withDevice } from '@ledgerhq/live-common/lib/hw/deviceAccess'
import signTransactionForCurrency from 'helpers/signTransactionForCurrency' import signTransactionForCurrency from 'helpers/signTransactionForCurrency'
type Input = { type Input = {
@ -17,10 +17,8 @@ type Result = string
const cmd: Command<Input, Result> = createCommand( const cmd: Command<Input, Result> = createCommand(
'signTransaction', 'signTransaction',
({ currencyId, devicePath, path, transaction }) => ({ currencyId, devicePath, path, transaction }) =>
fromPromise( withDevice(devicePath)(transport =>
withDevice(devicePath)(transport => from(signTransactionForCurrency(currencyId)(transport, currencyId, path, transaction)),
signTransactionForCurrency(currencyId)(transport, currencyId, path, transaction),
),
), ),
) )

15
src/commands/testApdu.js

@ -3,8 +3,8 @@
// This is a test example for dev testing purpose. // This is a test example for dev testing purpose.
import { createCommand, Command } from 'helpers/ipc' import { createCommand, Command } from 'helpers/ipc'
import { fromPromise } from 'rxjs/observable/fromPromise' import { from } from 'rxjs'
import { withDevice } from 'helpers/deviceAccess' import { withDevice } from '@ledgerhq/live-common/lib/hw/deviceAccess'
type Input = { type Input = {
devicePath: string, devicePath: string,
@ -15,11 +15,12 @@ type Result = {
} }
const cmd: Command<Input, Result> = createCommand('testApdu', ({ apduHex, devicePath }) => const cmd: Command<Input, Result> = createCommand('testApdu', ({ apduHex, devicePath }) =>
fromPromise( withDevice(devicePath)(transport =>
withDevice(devicePath)(async transport => { from(
const res = await transport.exchange(Buffer.from(apduHex, 'hex')) transport
return { responseHex: res.toString('hex') } .exchange(Buffer.from(apduHex, 'hex'))
}), .then(res => ({ responseHex: res.toString('hex') })),
),
), ),
) )

18
src/commands/uninstallApp.js

@ -1,12 +1,8 @@
// @flow // @flow
import { createCommand, Command } from 'helpers/ipc' import { createCommand, Command } from 'helpers/ipc'
import { fromPromise } from 'rxjs/observable/fromPromise' import { withDevice } from '@ledgerhq/live-common/lib/hw/deviceAccess'
import { withDevice } from 'helpers/deviceAccess' import uninstallApp from '@ledgerhq/live-common/lib/hw/uninstallApp'
import type { ApplicationVersion } from '@ledgerhq/live-common/lib/types/manager'
import uninstallApp from 'helpers/apps/uninstallApp'
import type { ApplicationVersion } from 'helpers/types'
type Input = { type Input = {
app: ApplicationVersion, app: ApplicationVersion,
@ -14,12 +10,10 @@ type Input = {
targetId: string | number, targetId: string | number,
} }
type Result = void type Result = *
const cmd: Command<Input, Result> = createCommand( const cmd: Command<Input, Result> = createCommand('uninstallApp', ({ devicePath, targetId, app }) =>
'uninstallApp', withDevice(devicePath)(transport => uninstallApp(transport, targetId, app)),
({ devicePath, targetId, ...app }) =>
fromPromise(withDevice(devicePath)(transport => uninstallApp(transport, targetId, app))),
) )
export default cmd export default cmd

1
src/components/App.js

@ -45,4 +45,5 @@ const App = ({
</Provider> </Provider>
) )
// $FlowFixMe
export default hot(module)(App) export default hot(module)(App)

6
src/components/EnsureDeviceApp.js

@ -23,7 +23,11 @@ import IconUsb from 'icons/Usb'
import type { Device } from 'types/common' import type { Device } from 'types/common'
import { WrongDeviceForAccount, CantOpenDevice, UpdateYourApp } from 'config/errors' import {
WrongDeviceForAccount,
CantOpenDevice,
UpdateYourApp,
} from '@ledgerhq/live-common/lib/errors'
import { getCurrentDevice } from 'reducers/devices' import { getCurrentDevice } from 'reducers/devices'
const usbIcon = <IconUsb size={16} /> const usbIcon = <IconUsb size={16} />

13
src/components/ExportLogsBtn.js

@ -2,12 +2,20 @@
import logger from 'logger' import logger from 'logger'
import moment from 'moment' import moment from 'moment'
import fs from 'fs' import fs from 'fs'
import { webFrame, remote } from 'electron' import { ipcRenderer, webFrame, remote } from 'electron'
import React, { Component } from 'react' import React, { Component } from 'react'
import { translate } from 'react-i18next' import { translate } from 'react-i18next'
import KeyHandler from 'react-key-handler' import KeyHandler from 'react-key-handler'
import Button from './base/Button' import Button from './base/Button'
const queryLogs = () =>
new Promise(success => {
ipcRenderer.once('logs', (event: any, { logs }) => {
success(logs)
})
ipcRenderer.send('queryLogs')
})
function writeToFile(file, data) { function writeToFile(file, data) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
fs.writeFile(file, data, error => { fs.writeFile(file, data, error => {
@ -33,7 +41,6 @@ class ExportLogsBtn extends Component<{
environment: __DEV__ ? 'development' : 'production', environment: __DEV__ ? 'development' : 'production',
userAgent: window.navigator.userAgent, userAgent: window.navigator.userAgent,
}) })
const date = new Date() // we don't want all the logs that happen after the Export was pressed ^^
const path = remote.dialog.showSaveDialog({ const path = remote.dialog.showSaveDialog({
title: 'Export logs', title: 'Export logs',
defaultPath: `ledgerlive-export-${moment().format( defaultPath: `ledgerlive-export-${moment().format(
@ -47,7 +54,7 @@ class ExportLogsBtn extends Component<{
], ],
}) })
if (path) { if (path) {
const logs = await logger.queryAllLogs(date) const logs = await queryLogs()
const json = JSON.stringify(logs) const json = JSON.stringify(logs)
await writeToFile(path, json) await writeToFile(path, json)
} }

4
src/components/FeesField/BitcoinKind.js

@ -8,7 +8,7 @@ import { translate } from 'react-i18next'
import type { T } from 'types/common' import type { T } from 'types/common'
import { FeeNotLoaded } from 'config/errors' import { FeeNotLoaded } from '@ledgerhq/live-common/lib/errors'
import InputCurrency from 'components/base/InputCurrency' import InputCurrency from 'components/base/InputCurrency'
import Select from 'components/base/Select' import Select from 'components/base/Select'
import type { Fees } from 'api/Fees' import type { Fees } from 'api/Fees'
@ -89,7 +89,7 @@ class FeesField extends Component<OwnProps, State> {
} }
items.push(!feePerByte && !error ? notLoadedItem : customItem) items.push(!feePerByte && !error ? notLoadedItem : customItem)
const selectedItem = const selectedItem =
!feePerByte && prevState.selectedItem.feePerByte.eq(feePerByte) feePerByte && prevState.selectedItem.feePerByte.eq(feePerByte)
? prevState.selectedItem ? prevState.selectedItem
: items.find(f => f.feePerByte.eq(feePerByte)) || items[items.length - 1] : items.find(f => f.feePerByte.eq(feePerByte)) || items[items.length - 1]
return { items, selectedItem } return { items, selectedItem }

2
src/components/FeesField/EthereumKind.js

@ -4,7 +4,7 @@ import React, { Component } from 'react'
import { BigNumber } from 'bignumber.js' import { BigNumber } from 'bignumber.js'
import type { Account } from '@ledgerhq/live-common/lib/types' import type { Account } from '@ledgerhq/live-common/lib/types'
import { FeeNotLoaded } from 'config/errors' import { FeeNotLoaded } from '@ledgerhq/live-common/lib/errors'
import InputCurrency from 'components/base/InputCurrency' import InputCurrency from 'components/base/InputCurrency'
import type { Fees } from 'api/Fees' import type { Fees } from 'api/Fees'
import WithFeesAPI from '../WithFeesAPI' import WithFeesAPI from '../WithFeesAPI'

2
src/components/FeesField/RippleKind.js

@ -4,7 +4,7 @@ import React, { Component } from 'react'
import type { BigNumber } from 'bignumber.js' import type { BigNumber } from 'bignumber.js'
import type { Account } from '@ledgerhq/live-common/lib/types' import type { Account } from '@ledgerhq/live-common/lib/types'
import { apiForEndpointConfig, parseAPIValue } from 'api/Ripple' import { apiForEndpointConfig, parseAPIValue } from 'api/Ripple'
import { FeeNotLoaded } from 'config/errors' import { FeeNotLoaded } from '@ledgerhq/live-common/lib/errors'
import InputCurrency from 'components/base/InputCurrency' import InputCurrency from 'components/base/InputCurrency'
import GenericContainer from './GenericContainer' import GenericContainer from './GenericContainer'

27
src/components/GenuineCheck.js

@ -9,12 +9,18 @@ import { delay, createCancelablePolling } from 'helpers/promise'
import logger from 'logger' import logger from 'logger'
import type { T, Device } from 'types/common' import type { T, Device } from 'types/common'
import type { DeviceInfo } from 'helpers/types' import manager from '@ledgerhq/live-common/lib/manager'
import type { DeviceInfo } from '@ledgerhq/live-common/lib/types/manager'
import { GENUINE_TIMEOUT, DEVICE_INFOS_TIMEOUT, GENUINE_CACHE_DELAY } from 'config/constants' import { GENUINE_TIMEOUT, DEVICE_INFOS_TIMEOUT, GENUINE_CACHE_DELAY } from 'config/constants'
import { getCurrentDevice } from 'reducers/devices' import { getCurrentDevice } from 'reducers/devices'
import { CantOpenDevice, DeviceNotGenuineError, DeviceGenuineSocketEarlyClose } from 'config/errors' import {
CantOpenDevice,
DeviceNotGenuineError,
DeviceGenuineSocketEarlyClose,
UnexpectedBootloader,
} from '@ledgerhq/live-common/lib/errors'
import getDeviceInfo from 'commands/getDeviceInfo' import getDeviceInfo from 'commands/getDeviceInfo'
import getIsGenuine from 'commands/getIsGenuine' import getIsGenuine from 'commands/getIsGenuine'
@ -77,11 +83,26 @@ class GenuineCheck extends PureComponent<Props> {
device: Device, device: Device,
deviceInfo: DeviceInfo, deviceInfo: DeviceInfo,
}) => { }) => {
if (deviceInfo.isOSU || deviceInfo.isBootloader) { if (deviceInfo.isBootloader) {
logger.log('device is in bootloader mode')
throw new UnexpectedBootloader()
}
if (deviceInfo.isOSU) {
logger.log('device is in update mode. skipping genuine') logger.log('device is in update mode. skipping genuine')
return true return true
} }
// Preload things in parallel
Promise.all([
// Step dashboard, we preload the applist before entering manager while we're still doing the genuine check
manager.getAppsList(deviceInfo),
// we also preload as much info as possible in case of a MCU
manager.getLatestFirmwareForDevice(deviceInfo),
]).catch(e => {
logger.warn(e)
})
if (genuineDevices.has(device)) { if (genuineDevices.has(device)) {
logger.log("genuine was already checked. don't check again") logger.log("genuine was already checked. don't check again")
await delay(GENUINE_CACHE_DELAY) await delay(GENUINE_CACHE_DELAY)

171
src/components/HSMStatusBanner.js

@ -0,0 +1,171 @@
// @flow
import React, { PureComponent } from 'react'
import { warnings } from '@ledgerhq/live-common/lib/api/socket'
import { translate } from 'react-i18next'
import styled from 'styled-components'
import { colors } from 'styles/theme'
import uniqueId from 'lodash/uniqueId'
import { openURL } from 'helpers/linking'
import IconCross from 'icons/Cross'
import IconExclamationCircle from 'icons/ExclamationCircle'
import IconChevronRight from 'icons/ChevronRight'
import Box from 'components/base/Box'
import { SHOW_MOCK_HSMWARNINGS } from '../config/constants'
import { urls } from '../config/urls'
const CloseIconContainer = styled.div`
position: absolute;
top: 0;
right: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 10px;
border-bottom-left-radius: 4px;
`
const CloseIcon = (props: *) => (
<CloseIconContainer {...props}>
<IconCross size={16} color="white" />
</CloseIconContainer>
)
type Props = {
t: *,
}
type State = {
pendingMessages: HSMStatus[],
}
type HSMStatus = {
id: string,
message: string,
}
class HSMStatusBanner extends PureComponent<Props, State> {
state = {
pendingMessages: SHOW_MOCK_HSMWARNINGS
? [
{
id: 'mock1',
message: 'Lorem Ipsum dolor sit amet #1',
},
]
: [],
}
componentDidMount() {
this.warningSub = warnings.subscribe({
next: message => {
this.setState(prevState => ({
pendingMessages: [...prevState.pendingMessages, { id: uniqueId(), message }],
}))
},
})
}
componentWillUnmount() {
if (this.warningSub) {
this.warningSub.unsubscribe()
}
}
warningSub = null
dismiss = dismissedItem =>
this.setState(prevState => ({
pendingMessages: prevState.pendingMessages.filter(item => item.id !== dismissedItem.id),
}))
render() {
const { t } = this.props
const { pendingMessages } = this.state
if (!pendingMessages.length) return null
const item = pendingMessages[0]
return (
<Box flow={2} style={styles.container}>
<BannerItem key={item.id} t={t} item={item} onItemDismiss={this.dismiss} />
</Box>
)
}
}
class BannerItem extends PureComponent<{
item: HSMStatus,
onItemDismiss: HSMStatus => void,
t: *,
}> {
onLinkClick = () => openURL(urls.contactSupport)
dismiss = () => this.props.onItemDismiss(this.props.item)
render() {
const { item, t } = this.props
return (
<Box relative key={item.id} style={styles.banner}>
<CloseIcon onClick={this.dismiss} />
<Box horizontal flow={2}>
<IconExclamationCircle size={16} color="white" />
<Box shrink ff="Open Sans|SemiBold" style={styles.message}>
{item.message}
</Box>
</Box>
<BannerItemLink t={t} onClick={this.onLinkClick} />
</Box>
)
}
}
const UnderlinedLink = styled.span`
border-bottom: 1px solid transparent;
&:hover {
border-bottom-color: white;
}
`
const BannerItemLink = ({ t, onClick }: { t: *, onClick: void => * }) => (
<Box
mt={2}
ml={4}
flow={1}
horizontal
align="center"
cursor="pointer"
onClick={onClick}
color="white"
>
<IconChevronRight size={16} color="white" />
<UnderlinedLink>{t('common.learnMore')}</UnderlinedLink>
</Box>
)
const styles = {
container: {
position: 'fixed',
left: 32,
bottom: 32,
zIndex: 100,
},
banner: {
background: colors.orange,
overflow: 'hidden',
borderRadius: 4,
fontSize: 13,
paddingTop: 17,
padding: 15,
color: 'white',
fontWeight: 'bold',
paddingRight: 30,
width: 350,
},
message: {
marginTop: -3,
},
}
export default translate()(HSMStatusBanner)

2
src/components/IsUnlocked.js

@ -17,7 +17,7 @@ import { hardReset } from 'helpers/reset'
import { fetchAccounts } from 'actions/accounts' import { fetchAccounts } from 'actions/accounts'
import { isLocked, unlock } from 'reducers/application' import { isLocked, unlock } from 'reducers/application'
import { PasswordIncorrectError } from 'config/errors' import { PasswordIncorrectError } from '@ledgerhq/live-common/lib/errors'
import Box from 'components/base/Box' import Box from 'components/base/Box'
import InputPassword from 'components/base/InputPassword' import InputPassword from 'components/base/InputPassword'

2
src/components/ManagerPage/AppSearchBar.js

@ -3,7 +3,7 @@
import React, { PureComponent, Fragment } from 'react' import React, { PureComponent, Fragment } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import type { ApplicationVersion } from 'helpers/types' import type { ApplicationVersion } from '@ledgerhq/live-common/lib/types/manager'
import Box from 'components/base/Box' import Box from 'components/base/Box'
import Space from 'components/base/Space' import Space from 'components/base/Space'

116
src/components/ManagerPage/AppsList.js

@ -6,12 +6,12 @@ import styled from 'styled-components'
import { translate } from 'react-i18next' import { translate } from 'react-i18next'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { compose } from 'redux' import { compose } from 'redux'
import type { Device, T } from 'types/common' import type { Device, T } from 'types/common'
import type { ApplicationVersion, DeviceInfo } from 'helpers/types' import type { ApplicationVersion, DeviceInfo } from '@ledgerhq/live-common/lib/types/manager'
import manager from '@ledgerhq/live-common/lib/manager'
import { getFullListSortedCryptoCurrencies } from 'helpers/countervalues' import { getFullListSortedCryptoCurrencies } from 'helpers/countervalues'
import { developerModeSelector } from 'reducers/settings' import { developerModeSelector } from 'reducers/settings'
import listApps from 'commands/listApps'
import listAppVersions from 'commands/listAppVersions'
import installApp from 'commands/installApp' import installApp from 'commands/installApp'
import uninstallApp from 'commands/uninstallApp' import uninstallApp from 'commands/uninstallApp'
import Box from 'components/base/Box' import Box from 'components/base/Box'
@ -19,7 +19,7 @@ import Space from 'components/base/Space'
import Modal, { ModalBody, ModalFooter, ModalTitle, ModalContent } from 'components/base/Modal' import Modal, { ModalBody, ModalFooter, ModalTitle, ModalContent } from 'components/base/Modal'
import Tooltip from 'components/base/Tooltip' import Tooltip from 'components/base/Tooltip'
import Text from 'components/base/Text' import Text from 'components/base/Text'
import Progress from 'components/base/Progress' import ProgressBar from 'components/ProgressBar'
import Spinner from 'components/base/Spinner' import Spinner from 'components/base/Spinner'
import Button from 'components/base/Button' import Button from 'components/base/Button'
import TranslatedError from 'components/TranslatedError' import TranslatedError from 'components/TranslatedError'
@ -65,6 +65,7 @@ type State = {
appsLoaded: boolean, appsLoaded: boolean,
app: string, app: string,
mode: Mode, mode: Mode,
progress: number,
} }
const oldAppsInstallDisabled = ['ZenCash', 'Ripple'] const oldAppsInstallDisabled = ['ZenCash', 'Ripple']
@ -86,6 +87,7 @@ class AppsList extends PureComponent<Props, State> {
appsLoaded: false, appsLoaded: false,
app: '', app: '',
mode: 'home', mode: 'home',
progress: 0,
} }
componentDidMount() { componentDidMount() {
@ -98,53 +100,15 @@ class AppsList extends PureComponent<Props, State> {
_unmounted = false _unmounted = false
prepareAppList = ({ applicationsList, compatibleAppVersionsList, sortedCryptoCurrencies }) => {
const filtered = this.props.isDevMode
? compatibleAppVersionsList.slice(0)
: compatibleAppVersionsList.filter(version => {
const app = applicationsList.find(e => e.id === version.app)
if (app) {
return app.category !== 2
}
return false
})
const sortedCryptoApps = []
// sort by crypto first
sortedCryptoCurrencies.forEach(crypto => {
const app = filtered.find(
item => item.name.toLowerCase() === crypto.managerAppName.toLowerCase(),
)
if (app) {
filtered.splice(filtered.indexOf(app), 1)
sortedCryptoApps.push(app)
}
})
return sortedCryptoApps.concat(filtered)
}
async fetchAppList() { async fetchAppList() {
try { const { deviceInfo, isDevMode } = this.props
const { deviceInfo } = this.props
const [
applicationsList,
compatibleAppVersionsList,
sortedCryptoCurrencies,
] = await Promise.all([
listApps.send().toPromise(),
listAppVersions.send(deviceInfo).toPromise(),
getFullListSortedCryptoCurrencies(),
])
const filteredAppVersionsList = this.prepareAppList({ try {
applicationsList, const filteredAppVersionsList = await manager.getAppsList(
compatibleAppVersionsList, deviceInfo,
sortedCryptoCurrencies, isDevMode,
}) getFullListSortedCryptoCurrencies,
)
if (!this._unmounted) { if (!this._unmounted) {
this.setState({ this.setState({
@ -158,41 +122,37 @@ class AppsList extends PureComponent<Props, State> {
} }
} }
handleInstallApp = (app: ApplicationVersion) => async () => { sub: *
this.setState({ status: 'busy', app: app.name, mode: 'installing' }) runAppScript = (app: ApplicationVersion, mode: *, cmd: *) => {
try { this.setState({ status: 'busy', app: app.name, mode, progress: 0 })
const { const {
device: { path: devicePath }, device: { path: devicePath },
deviceInfo, deviceInfo: { targetId },
} = this.props } = this.props
const data = { app, devicePath, targetId: deviceInfo.targetId } this.sub = cmd.send({ app, devicePath, targetId }).subscribe({
await installApp.send(data).toPromise() next: patch => {
this.setState({ status: 'success' }) this.setState(patch)
} catch (err) { },
this.setState({ status: 'error', error: err, mode: 'home' }) complete: () => {
} this.setState({ status: 'success' })
},
error: error => {
this.setState({ status: 'error', error, app: '', mode: 'home' })
},
})
} }
handleUninstallApp = (app: ApplicationVersion) => async () => { handleInstallApp = (app: ApplicationVersion) => () =>
this.setState({ status: 'busy', app: app.name, mode: 'uninstalling' }) this.runAppScript(app, 'installing', installApp)
try {
const { handleUninstallApp = (app: ApplicationVersion) => () =>
device: { path: devicePath }, this.runAppScript(app, 'uninstalling', uninstallApp)
deviceInfo,
} = this.props
const data = { app, devicePath, targetId: deviceInfo.targetId }
await uninstallApp.send(data).toPromise()
this.setState({ status: 'success' })
} catch (err) {
this.setState({ status: 'error', error: err, app: '', mode: 'home' })
}
}
handleCloseModal = () => this.setState({ status: 'idle', mode: 'home' }) handleCloseModal = () => this.setState({ status: 'idle', mode: 'home' })
renderModal = () => { renderModal = () => {
const { t } = this.props const { t } = this.props
const { app, status, error, mode } = this.state const { app, status, error, mode, progress } = this.state
return ( return (
<Modal <Modal
isOpened={status !== 'idle' && status !== 'loading'} isOpened={status !== 'idle' && status !== 'loading'}
@ -217,7 +177,7 @@ class AppsList extends PureComponent<Props, State> {
{t(`manager.apps.${mode}`, { app })} {t(`manager.apps.${mode}`, { app })}
</Text> </Text>
<Box mt={6}> <Box mt={6}>
<Progress style={{ width: '100%' }} infinite /> <ProgressBar width={150} progress={progress} />
</Box> </Box>
</ModalContent> </ModalContent>
</Fragment> </Fragment>

2
src/components/ManagerPage/Dashboard.js

@ -4,7 +4,7 @@ import { translate } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
import type { T, Device } from 'types/common' import type { T, Device } from 'types/common'
import type { DeviceInfo } from 'helpers/types' import type { DeviceInfo } from '@ledgerhq/live-common/lib/types/manager'
import Box from 'components/base/Box' import Box from 'components/base/Box'
import Text from 'components/base/Text' import Text from 'components/base/Text'

85
src/components/ManagerPage/FirmwareUpdate.js

@ -3,20 +3,13 @@
import React, { PureComponent, Fragment } from 'react' import React, { PureComponent, Fragment } from 'react'
import { translate } from 'react-i18next' import { translate } from 'react-i18next'
import isEqual from 'lodash/isEqual'
import isEmpty from 'lodash/isEmpty'
import invariant from 'invariant'
import type { Device, T } from 'types/common' import type { Device, T } from 'types/common'
import type { DeviceInfo, OsuFirmware } from 'helpers/types' import type { DeviceInfo, FirmwareUpdateContext } from '@ledgerhq/live-common/lib/types/manager'
import type { StepId } from 'components/modals/UpdateFirmware' import type { StepId } from 'components/modals/UpdateFirmware'
import getLatestFirmwareForDevice from 'commands/getLatestFirmwareForDevice' import getLatestFirmwareForDevice from 'commands/getLatestFirmwareForDevice'
import shouldFlashMcu from 'commands/shouldFlashMcu'
import installOsuFirmware from 'commands/installOsuFirmware'
import installFinalFirmware from 'commands/installFinalFirmware'
import installMcu from 'commands/installMcu'
import DisclaimerModal from 'components/modals/UpdateFirmware/Disclaimer' import DisclaimerModal from 'components/modals/UpdateFirmware/Disclaimer'
import UpdateModal from 'components/modals/UpdateFirmware' import UpdateModal from 'components/modals/UpdateFirmware'
@ -42,32 +35,34 @@ type Props = {
} }
type State = { type State = {
latestFirmware: ?OsuFirmware & ?{ shouldFlashMcu: boolean }, firmware: ?FirmwareUpdateContext,
modal: ModalStatus, modal: ModalStatus,
stepId: ?StepId, stepId: ?StepId,
shouldFlash: boolean,
ready: boolean, ready: boolean,
} }
const intializeState = ({ deviceInfo }): State => ({ const intializeState = ({ deviceInfo }): State => ({
latestFirmware: null, firmware: null,
modal: 'closed', modal: 'closed',
stepId: deviceInfo.isBootloader ? 'updateMCU' : 'idCheck', stepId: deviceInfo.isBootloader ? 'updateMCU' : 'idCheck',
shouldFlash: false,
ready: false, ready: false,
}) })
class FirmwareUpdate extends PureComponent<Props, State> { class FirmwareUpdate extends PureComponent<Props, State> {
state = intializeState(this.props) state = intializeState(this.props)
componentDidMount() { async componentDidMount() {
const { deviceInfo } = this.props const { deviceInfo } = this.props
if (!deviceInfo.isOSU && !deviceInfo.isBootloader) { const firmware = await getLatestFirmwareForDevice.send(deviceInfo).toPromise()
this.fetchLatestFirmware() if (firmware && !this._unmounting) {
} else if (deviceInfo.isOSU) { /* eslint-disable */
this.shouldFlashMcu() this.setState({
} else if (deviceInfo.isBootloader) { firmware,
this.handleInstallModal('updateMCU', true) ready: true,
modal: deviceInfo.isOSU ? 'install' : 'closed',
stepId: deviceInfo.isOSU ? 'updateMCU' : 'idCheck',
})
/* eslint-enable */
} }
} }
@ -77,55 +72,15 @@ class FirmwareUpdate extends PureComponent<Props, State> {
_unmounting = false _unmounting = false
fetchLatestFirmware = async () => {
const { deviceInfo } = this.props
const latestFirmware = await getLatestFirmwareForDevice.send(deviceInfo).toPromise()
if (
!isEmpty(latestFirmware) &&
!isEqual(this.state.latestFirmware, latestFirmware) &&
!this._unmounting
) {
this.setState({ latestFirmware, ready: true })
}
}
shouldFlashMcu = async () => {
const { deviceInfo } = this.props
const shouldFlash = await shouldFlashMcu.send(deviceInfo).toPromise()
if (!this._unmounting) {
this.setState({ shouldFlash, modal: 'install', stepId: 'idCheck', ready: true })
}
}
installOsuFirmware = async (device: Device) => {
const { latestFirmware } = this.state
const { deviceInfo } = this.props
invariant(latestFirmware, 'did not find a new firmware or firmware is not set')
this.setState({ modal: 'install' })
const result = await installOsuFirmware
.send({ devicePath: device.path, firmware: latestFirmware, targetId: deviceInfo.targetId })
.toPromise()
return result
}
installFinalFirmware = (device: Device) =>
installFinalFirmware.send({ devicePath: device.path }).toPromise()
flashMCU = async (device: Device) => installMcu.send({ devicePath: device.path }).toPromise()
handleCloseModal = () => this.setState({ modal: 'closed' }) handleCloseModal = () => this.setState({ modal: 'closed' })
handleDisclaimerModal = () => this.setState({ modal: 'disclaimer' }) handleDisclaimerModal = () => this.setState({ modal: 'disclaimer' })
handleInstallModal = (stepId: StepId = 'idCheck', shouldFlash?: boolean) =>
this.setState({ modal: 'install', stepId, shouldFlash, ready: true })
handleDisclaimerNext = () => this.setState({ modal: 'install' }) handleDisclaimerNext = () => this.setState({ modal: 'install' })
render() { render() {
const { deviceInfo, t, device } = this.props const { deviceInfo, t, device } = this.props
const { latestFirmware, modal, stepId, shouldFlash, ready } = this.state const { firmware, modal, stepId, ready } = this.state
return ( return (
<Card p={4}> <Card p={4}>
<Box horizontal align="center" flow={2}> <Box horizontal align="center" flow={2}>
@ -151,12 +106,12 @@ class FirmwareUpdate extends PureComponent<Props, State> {
})} })}
</Text> </Text>
</Box> </Box>
<UpdateFirmwareButton firmware={latestFirmware} onClick={this.handleDisclaimerModal} /> <UpdateFirmwareButton firmware={firmware} onClick={this.handleDisclaimerModal} />
</Box> </Box>
{ready ? ( {ready ? (
<Fragment> <Fragment>
<DisclaimerModal <DisclaimerModal
firmware={latestFirmware} firmware={firmware}
status={modal} status={modal}
goToNextStep={this.handleDisclaimerNext} goToNextStep={this.handleDisclaimerNext}
onClose={this.handleCloseModal} onClose={this.handleCloseModal}
@ -165,11 +120,7 @@ class FirmwareUpdate extends PureComponent<Props, State> {
status={modal} status={modal}
stepId={stepId} stepId={stepId}
onClose={this.handleCloseModal} onClose={this.handleCloseModal}
firmware={latestFirmware} firmware={firmware}
shouldFlashMcu={shouldFlash}
installOsuFirmware={this.installOsuFirmware}
installFinalFirmware={this.installFinalFirmware}
flashMCU={this.flashMCU}
/> />
</Fragment> </Fragment>
) : null} ) : null}

13
src/components/ManagerPage/UpdateFirmwareButton.js

@ -3,19 +3,14 @@ import React, { Fragment } from 'react'
import { translate } from 'react-i18next' import { translate } from 'react-i18next'
import type { T } from 'types/common' import type { T } from 'types/common'
import type { OsuFirmware, FinalFirmware } from '@ledgerhq/live-common/lib/types/manager'
import Button from 'components/base/Button' import Button from 'components/base/Button'
import Text from 'components/base/Text' import Text from 'components/base/Text'
import { getCleanVersion } from 'components/ManagerPage/FirmwareUpdate' import { getCleanVersion } from 'components/ManagerPage/FirmwareUpdate'
type FirmwareInfos = {
name: string,
notes: string,
}
type Props = { type Props = {
t: T, t: T,
firmware: ?FirmwareInfos, firmware: ?{ osu: OsuFirmware, finalFirmware: FinalFirmware },
onClick: () => void, onClick: () => void,
} }
@ -23,14 +18,14 @@ const UpdateFirmwareButton = ({ t, firmware, onClick }: Props) =>
firmware ? ( firmware ? (
<Fragment> <Fragment>
<Text ff="Open Sans|Regular" fontSize={4} style={{ marginLeft: 'auto', marginRight: 15 }}> <Text ff="Open Sans|Regular" fontSize={4} style={{ marginLeft: 'auto', marginRight: 15 }}>
{t('manager.firmware.latest', { version: getCleanVersion(firmware.name) })} {t('manager.firmware.latest', { version: getCleanVersion(firmware.osu.name) })}
</Text> </Text>
<Button <Button
primary primary
onClick={onClick} onClick={onClick}
event={'Manager Firmware Update Click'} event={'Manager Firmware Update Click'}
eventProperties={{ eventProperties={{
firmwareName: firmware.name, firmwareName: firmware.osu.name,
}} }}
> >
{t('manager.firmware.update')} {t('manager.firmware.update')}

2
src/components/ManagerPage/index.js

@ -5,7 +5,7 @@ import invariant from 'invariant'
import { openURL } from 'helpers/linking' import { openURL } from 'helpers/linking'
import { urls } from 'config/urls' import { urls } from 'config/urls'
import type { Device } from 'types/common' import type { Device } from 'types/common'
import type { DeviceInfo } from 'helpers/types' import type { DeviceInfo } from '@ledgerhq/live-common/lib/types/manager'
import { getFullListSortedCryptoCurrencies } from 'helpers/countervalues' import { getFullListSortedCryptoCurrencies } from 'helpers/countervalues'
import Dashboard from './Dashboard' import Dashboard from './Dashboard'

64
src/components/ProgressBar/index.js

@ -0,0 +1,64 @@
// @flow
import React, { PureComponent } from 'react'
import styled, { css, keyframes } from 'styled-components'
import { colors } from 'styles/theme'
const animIndeterminate = keyframes`
0% {
transform: scaleX(0) translate3d(0, 0, 0);
}
50% {
transform: scaleX(1) translate3d(100%, 0, 0);
}
100% {
transform: scaleX(0) translate3d(0, 0, 0);
}
`
const Outer = styled.div`
background-color: ${colors.fog};
border-radius: 3px;
overflow: hidden;
height: 5px;
width: ${p => p.width}px;
position: relative;
`
const Inner = styled.div`
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: ${colors.wallet};
transform-origin: center left;
${p =>
p.progress === 0
? css`
animation: ${animIndeterminate} 2s cubic-bezier(0.61, 0.01, 0.39, 1.03) infinite;
`
: css`
transform: scaleX(${p => p.progress});
`};
`
type Props = {
progress: number,
width: number,
}
class ProgressBar extends PureComponent<Props> {
render() {
const { progress, width } = this.props
return (
<Outer width={width}>
<Inner progress={progress} />
</Outer>
)
}
}
export default ProgressBar

16
src/components/ProgressBar/stories.js

@ -0,0 +1,16 @@
// @flow
import React from 'react'
import { storiesOf } from '@storybook/react'
import { number } from '@storybook/addon-knobs'
import ProgressBar from 'components/ProgressBar'
const stories = storiesOf('Components', module)
stories.add('ProgressBar', () => (
<ProgressBar
progress={number('progress', 0, { min: 0, max: 1, step: 0.05 })}
width={number('width', 200, { min: 50, max: 500, step: 10 })}
/>
))

98
src/components/ProgressCircle/index.js

@ -0,0 +1,98 @@
// @flow
import React, { PureComponent } from 'react'
import styled, { css, keyframes } from 'styled-components'
import { colors } from 'styles/theme'
import Text from 'components/base/Text'
const STROKE_WIDTH = 5
type Props = {
progress: number,
size: number,
}
const animIndeterminate = keyframes`
0% {
}
50% {
}
100% {
}
`
const InnerCircle = styled.circle`
transform-origin: 50% 50%;
${p =>
p.progress === 0
? css`
animation: ${animIndeterminate} 3s cubic-bezier(0.61, 0.01, 0.39, 1.03) infinite;
`
: css`
transition: stroke-dashoffset 0.35s;
transform: rotate(-90deg);
`};
`
const Container = styled.div`
position: relative;
width: ${p => p.size}px;
height: ${p => p.size}px;
`
const TextContainer = styled.div`
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
`
class ProgressCircle extends PureComponent<Props> {
render() {
const { size, progress } = this.props
const radius = size / 2
const normalizedRadius = radius - STROKE_WIDTH / 2
const circumference = normalizedRadius * 2 * Math.PI
const strokeDashoffset = circumference - progress * circumference
return (
<Container size={size}>
<TextContainer>
<Text ff="Rubik|Regular" color="graphite" fontSize={5}>
{`${Math.round(progress * 100)}%`}
</Text>
</TextContainer>
<svg height={size} width={size}>
<circle
stroke={colors.fog}
fill="transparent"
strokeWidth={STROKE_WIDTH}
style={{ strokeDashoffset }}
r={normalizedRadius}
cx={radius}
cy={radius}
/>
<InnerCircle
progress={progress}
stroke={colors.wallet}
fill="transparent"
strokeWidth={STROKE_WIDTH}
strokeDasharray={`${circumference} ${circumference}`}
style={{ strokeDashoffset }}
r={normalizedRadius}
cx={radius}
cy={radius}
/>
</svg>
</Container>
)
}
}
export default ProgressCircle

16
src/components/ProgressCircle/stories.js

@ -0,0 +1,16 @@
// @flow
import React from 'react'
import { storiesOf } from '@storybook/react'
import { number } from '@storybook/addon-knobs'
import ProgressCircle from 'components/ProgressCircle'
const stories = storiesOf('Components', module)
stories.add('ProgressCircle', () => (
<ProgressCircle
progress={number('progress', 0, { min: 0, max: 1, step: 0.01 })}
size={number('width', 150, { min: 50, max: 500, step: 10 })}
/>
))

2
src/components/SettingsPage/DisablePasswordModal.js

@ -1,7 +1,7 @@
// @flow // @flow
import React, { PureComponent } from 'react' import React, { PureComponent } from 'react'
import { PasswordIncorrectError } from 'config/errors' import { PasswordIncorrectError } from '@ledgerhq/live-common/lib/errors'
import db from 'helpers/db' import db from 'helpers/db'
import Box from 'components/base/Box' import Box from 'components/base/Box'

2
src/components/SettingsPage/PasswordForm.js

@ -6,7 +6,7 @@ import Box from 'components/base/Box'
import InputPassword from 'components/base/InputPassword' import InputPassword from 'components/base/InputPassword'
import Label from 'components/base/Label' import Label from 'components/base/Label'
import { PasswordsDontMatchError } from 'config/errors' import { PasswordsDontMatchError } from '@ledgerhq/live-common/lib/errors'
import type { T } from 'types/common' import type { T } from 'types/common'

2
src/components/SettingsPage/PasswordModal.js

@ -5,7 +5,7 @@ import React, { PureComponent } from 'react'
import type { T } from 'types/common' import type { T } from 'types/common'
import db from 'helpers/db' import db from 'helpers/db'
import { PasswordIncorrectError } from 'config/errors' import { PasswordIncorrectError } from '@ledgerhq/live-common/lib/errors'
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 { Modal, ModalContent, ModalBody, ModalTitle, ModalFooter } from 'components/base/Modal' import { Modal, ModalContent, ModalBody, ModalTitle, ModalFooter } from 'components/base/Modal'

103
src/components/SettingsPage/RepairDeviceButton.js

@ -0,0 +1,103 @@
// @flow
import React, { Fragment, PureComponent } from 'react'
import { compose } from 'redux'
import { connect } from 'react-redux'
import { withRouter } from 'react-router'
import { translate } from 'react-i18next'
import { push } from 'react-router-redux'
import type { T } from 'types/common'
import firmwareRepair from 'commands/firmwareRepair'
import Button from 'components/base/Button'
import { RepairModal } from 'components/base/Modal'
type Props = {
t: T,
push: string => void,
}
type State = {
opened: boolean,
isLoading: boolean,
error: ?Error,
progress: number,
}
class RepairDeviceButton extends PureComponent<Props, State> {
state = {
opened: false,
isLoading: false,
error: null,
progress: 0,
}
open = () => this.setState({ opened: true, error: null })
sub: *
close = () => {
if (this.sub) this.sub.unsubscribe()
this.setState({ opened: false, isLoading: false, error: null, progress: 0 })
}
action = () => {
if (this.state.isLoading) return
const { push } = this.props
this.setState({ isLoading: true })
this.sub = firmwareRepair.send().subscribe({
next: patch => {
this.setState(patch)
},
error: error => {
this.setState({ error, isLoading: false, progress: 0 })
},
complete: () => {
this.setState({ opened: false, isLoading: false, progress: 0 }, () => {
push('/manager')
})
},
})
}
render() {
const { t } = this.props
const { opened, isLoading, error, progress } = this.state
return (
<Fragment>
<Button small primary onClick={this.open} event="RepairDeviceButton">
{t('settings.repairDevice.button')}
</Button>
<RepairModal
cancellable
analyticsName="RepairDevice"
isOpened={opened}
onClose={this.close}
onReject={this.close}
onConfirm={this.action}
isLoading={isLoading}
title={t('settings.repairDevice.title')}
desc={t('settings.repairDevice.desc')}
confirmText={t('settings.repairDevice.button')}
progress={progress}
error={error}
/>
</Fragment>
)
}
}
const mapDispatchToProps = {
push,
}
export default compose(
translate(),
withRouter,
connect(
null,
mapDispatchToProps,
),
)(RepairDeviceButton)

10
src/components/SettingsPage/sections/Help.js

@ -5,13 +5,13 @@ import { translate } from 'react-i18next'
import type { T } from 'types/common' import type { T } from 'types/common'
import TrackPage from 'analytics/TrackPage' import TrackPage from 'analytics/TrackPage'
import IconHelp from 'icons/Help' import IconHelp from 'icons/Help'
import { resolveLogsDirectory } from 'helpers/log'
import { urls } from 'config/urls' import { urls } from 'config/urls'
import ExportLogsBtn from 'components/ExportLogsBtn' import ExportLogsBtn from 'components/ExportLogsBtn'
import OpenUserDataDirectoryBtn from 'components/OpenUserDataDirectoryBtn' import OpenUserDataDirectoryBtn from 'components/OpenUserDataDirectoryBtn'
import CleanButton from '../CleanButton' import CleanButton from '../CleanButton'
import ResetButton from '../ResetButton' import ResetButton from '../ResetButton'
import RepairDeviceButton from '../RepairDeviceButton'
import AboutRowItem from '../AboutRowItem' import AboutRowItem from '../AboutRowItem'
import LaunchOnboardingBtn from '../LaunchOnboardingBtn' import LaunchOnboardingBtn from '../LaunchOnboardingBtn'
@ -52,10 +52,7 @@ class SectionHelp extends PureComponent<Props> {
> >
<CleanButton /> <CleanButton />
</Row> </Row>
<Row <Row title={t('settings.exportLogs.title')} desc={t('settings.exportLogs.desc')}>
title={t('settings.exportLogs.title')}
desc={t('settings.exportLogs.desc', { logsDirectory: resolveLogsDirectory() })}
>
<ExportLogsBtn /> <ExportLogsBtn />
</Row> </Row>
<Row <Row
@ -76,6 +73,9 @@ class SectionHelp extends PureComponent<Props> {
> >
<ResetButton /> <ResetButton />
</Row> </Row>
<Row title={t('settings.repairDevice.title')} desc={t('settings.repairDevice.desc')}>
<RepairDeviceButton />
</Row>
</Body> </Body>
</Section> </Section>
) )

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

@ -25,11 +25,13 @@ type Props = {
t: T, t: T,
isLoading?: boolean, isLoading?: boolean,
analyticsName: string, analyticsName: string,
cancellable?: boolean,
} }
class ConfirmModal extends PureComponent<Props> { class ConfirmModal extends PureComponent<Props> {
render() { render() {
const { const {
cancellable,
isOpened, isOpened,
title, title,
subTitle, subTitle,
@ -54,7 +56,7 @@ class ConfirmModal extends PureComponent<Props> {
preventBackdropClick={isLoading} preventBackdropClick={isLoading}
{...props} {...props}
render={({ onClose }) => ( render={({ onClose }) => (
<ModalBody onClose={isLoading ? undefined : onClose}> <ModalBody onClose={!cancellable && isLoading ? undefined : onClose}>
<TrackPage category="Modal" name={analyticsName} /> <TrackPage category="Modal" name={analyticsName} />
<ModalTitle>{title}</ModalTitle> <ModalTitle>{title}</ModalTitle>
<ModalContent> <ModalContent>

203
src/components/base/Modal/RepairModal.js

@ -0,0 +1,203 @@
// @flow
import React, { PureComponent } from 'react'
import { translate } from 'react-i18next'
import styled from 'styled-components'
import type { T } from 'types/common'
import { i } from 'helpers/staticPath'
import TrackPage from 'analytics/TrackPage'
import Button from 'components/base/Button'
import Box from 'components/base/Box'
import Text from 'components/base/Text'
import ProgressCircle from 'components/ProgressCircle'
import TranslatedError from 'components/TranslatedError'
import ExclamationCircleThin from 'icons/ExclamationCircleThin'
import { Modal, ModalContent, ModalBody, ModalTitle, ModalFooter } from './index'
const Container = styled(Box).attrs({
alignItems: 'center',
fontSize: 4,
color: 'dark',
})``
const Bullet = styled.span`
font-weight: 600;
color: #142533;
`
const Separator = styled(Box).attrs({
color: 'fog',
})`
height: 1px;
width: 100%;
background-color: currentColor;
`
const DisclaimerStep = ({ desc }: { desc?: string }) => (
<ModalContent>
{desc ? (
<Box ff="Open Sans" color="smoke" fontSize={4} textAlign="center" mb={2}>
{desc}
</Box>
) : null}
</ModalContent>
)
const FlashStep = ({ progress, t }: { progress: number, t: * }) =>
progress === 0 ? (
<ModalContent>
<Box mx={7}>
<Text ff="Open Sans|Regular" align="center" color="smoke">
<Bullet>{'1.'}</Bullet>
{t('manager.modal.mcuFirst')}
</Text>
<img
src={i('logos/unplugDevice.png')}
style={{ width: '100%', maxWidth: 368, marginTop: 30 }}
alt={t('manager.modal.mcuFirst')}
/>
</Box>
<Separator my={6} />
<Box mx={7}>
<Text ff="Open Sans|Regular" align="center" color="smoke">
<Bullet>{'2.'}</Bullet>
{t('manager.modal.mcuSecond')}
</Text>
<img
src={i('logos/bootloaderMode.png')}
style={{ width: '100%', maxWidth: 368, marginTop: 30 }}
alt={t('manager.modal.mcuFirst')}
/>
</Box>
</ModalContent>
) : (
<ModalContent>
<Box mx={7} align="center">
<ProgressCircle size={64} progress={progress} />
</Box>
<Box mx={7} mt={3} mb={2} ff="Museo Sans|Regular" color="dark" textAlign="center">
{t(`manager.modal.steps.flash`)}
</Box>
<Box mx={7} mt={2} mb={2}>
<Text ff="Open Sans|Regular" align="center" color="graphite" fontSize={4}>
{t('manager.modal.mcuPin')}
</Text>
</Box>
</ModalContent>
)
const ErrorStep = ({ error }: { error: Error }) => (
<ModalContent>
<Container>
<Box color="alertRed">
<ExclamationCircleThin size={44} />
</Box>
<Box
color="dark"
mt={4}
fontSize={6}
ff="Museo Sans|Regular"
textAlign="center"
style={{ maxWidth: 350 }}
>
<TranslatedError error={error} field="title" />
</Box>
<Box
color="graphite"
mt={4}
fontSize={6}
ff="Open Sans"
textAlign="center"
style={{ maxWidth: 350 }}
>
<TranslatedError error={error} field="description" />
</Box>
</Container>
</ModalContent>
)
type Props = {
isOpened: boolean,
isDanger: boolean,
title: string,
subTitle?: string,
desc: string,
renderIcon?: Function,
confirmText?: string,
cancelText?: string,
onReject: Function,
onConfirm: Function,
t: T,
isLoading?: boolean,
analyticsName: string,
cancellable?: boolean,
progress: number,
error?: Error,
}
class RepairModal extends PureComponent<Props> {
render() {
const {
cancellable,
isOpened,
title,
desc,
confirmText,
isDanger,
onReject,
onConfirm,
isLoading,
renderIcon,
t,
analyticsName,
progress,
error,
...props
} = this.props
const realConfirmText = confirmText || t('common.confirm')
return (
<Modal
isOpened={isOpened}
preventBackdropClick={isLoading}
{...props}
render={({ onClose }) => (
<ModalBody onClose={!cancellable && isLoading ? undefined : onClose}>
<TrackPage category="Modal" name={analyticsName} />
<ModalTitle>{title}</ModalTitle>
{error ? (
<ErrorStep error={error} />
) : isLoading ? (
<FlashStep t={t} progress={progress} />
) : (
<DisclaimerStep desc={desc} />
)}
{!isLoading ? (
<ModalFooter horizontal align="center" justify="flex-end" flow={2}>
<Button onClick={onReject}>{t(`common.${error ? 'close' : 'cancel'}`)}</Button>
{error ? null : (
<Button
onClick={onConfirm}
primary={!isDanger}
danger={isDanger}
isLoading={isLoading}
disabled={isLoading}
>
{realConfirmText}
</Button>
)}
</ModalFooter>
) : null}
</ModalBody>
)}
/>
)
}
}
export default translate()(RepairModal)

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

@ -22,6 +22,7 @@ import GrowScroll from 'components/base/GrowScroll'
export { default as ModalBody } from './ModalBody' export { default as ModalBody } from './ModalBody'
export { default as ConfirmModal } from './ConfirmModal' export { default as ConfirmModal } from './ConfirmModal'
export { default as RepairModal } from './RepairModal'
export { default as ModalTitle } from './ModalTitle' export { default as ModalTitle } from './ModalTitle'
const springConfig = { const springConfig = {

2
src/components/layout/Default.js

@ -35,6 +35,7 @@ import SideBar from 'components/MainSideBar'
import TopBar from 'components/TopBar' import TopBar from 'components/TopBar'
import SyncBackground from 'components/SyncBackground' import SyncBackground from 'components/SyncBackground'
import SyncContinuouslyPendingOperations from '../SyncContinouslyPendingOperations' import SyncContinuouslyPendingOperations from '../SyncContinouslyPendingOperations'
import HSMStatusBanner from '../HSMStatusBanner'
const Main = styled(GrowScroll).attrs({ const Main = styled(GrowScroll).attrs({
px: 6, px: 6,
@ -106,6 +107,7 @@ class Default extends Component<Props> {
<SideBar /> <SideBar />
<Box shrink grow bg="lightGrey" color="grey" overflow="hidden" relative> <Box shrink grow bg="lightGrey" color="grey" overflow="hidden" relative>
<HSMStatusBanner />
<TopBar /> <TopBar />
<Main innerRef={n => (this._scrollContainer = n)} tabIndex={-1}> <Main innerRef={n => (this._scrollContainer = n)} tabIndex={-1}>
<Route path="/" exact component={DashboardPage} /> <Route path="/" exact component={DashboardPage} />

2
src/components/modals/AccountSettingRenderBody.js

@ -17,7 +17,7 @@ import { setDataModal } from 'reducers/modals'
import { getBridgeForCurrency } from 'bridge' import { getBridgeForCurrency } from 'bridge'
import { AccountNameRequiredError, EnpointConfigError } from 'config/errors' import { AccountNameRequiredError, EnpointConfigError } from '@ledgerhq/live-common/lib/errors'
import TrackPage from 'analytics/TrackPage' import TrackPage from 'analytics/TrackPage'
import Spoiler from 'components/base/Spoiler' import Spoiler from 'components/base/Spoiler'

12
src/components/modals/OperationDetails.js

@ -3,7 +3,7 @@
import React, { Fragment, Component } from 'react' import React, { Fragment, Component } from 'react'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { openURL } from 'helpers/linking' import { openURL } from 'helpers/linking'
import { translate } from 'react-i18next' import { Trans, translate } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
import moment from 'moment' import moment from 'moment'
import { getOperationAmountNumber } from '@ledgerhq/live-common/lib/operation' import { getOperationAmountNumber } from '@ledgerhq/live-common/lib/operation'
@ -114,7 +114,7 @@ type Props = {
const OperationDetails = connect(mapStateToProps)((props: Props) => { const OperationDetails = connect(mapStateToProps)((props: Props) => {
const { t, onClose, operation, account, currencySettings, marketIndicator } = props const { t, onClose, operation, account, currencySettings, marketIndicator } = props
if (!operation || !account || !currencySettings) return null if (!operation || !account || !currencySettings) return null
const { hash, date, senders, type, fee, recipients } = operation const { extra, hash, date, senders, type, fee, recipients } = operation
const { name, unit, currency } = account const { name, unit, currency } = account
const amount = getOperationAmountNumber(operation) const amount = getOperationAmountNumber(operation)
@ -226,6 +226,14 @@ const OperationDetails = connect(mapStateToProps)((props: Props) => {
<OpDetailsTitle>{t('operationDetails.to')}</OpDetailsTitle> <OpDetailsTitle>{t('operationDetails.to')}</OpDetailsTitle>
<DataList lines={recipients} t={t} /> <DataList lines={recipients} t={t} />
</Box> </Box>
{Object.entries(extra).map(([key, value]) => (
<Box key={key}>
<OpDetailsTitle>
<Trans i18nKey={`operationDetails.extra.${key}`} defaults={key} />
</OpDetailsTitle>
<OpDetailsData>{value}</OpDetailsData>
</Box>
))}
</Box> </Box>
</GrowScroll> </GrowScroll>
<GradientBox /> <GradientBox />

19
src/components/modals/Receive/steps/04-step-receive-funds.js

@ -5,10 +5,9 @@ import React, { PureComponent } from 'react'
import TrackPage from 'analytics/TrackPage' import TrackPage from 'analytics/TrackPage'
import getAddress from 'commands/getAddress' import getAddress from 'commands/getAddress'
import { isSegwitDerivationMode } from '@ledgerhq/live-common/lib/derivation'
import Box from 'components/base/Box' import Box from 'components/base/Box'
import CurrentAddressForAccount from 'components/CurrentAddressForAccount' import CurrentAddressForAccount from 'components/CurrentAddressForAccount'
import { DisconnectedDevice, WrongDeviceForAccount } from 'config/errors' import { DisconnectedDevice, WrongDeviceForAccount } from '@ledgerhq/live-common/lib/errors'
import type { StepProps } from '..' import type { StepProps } from '..'
@ -25,14 +24,14 @@ export default class StepReceiveFunds extends PureComponent<StepProps> {
if (!device || !account) { if (!device || !account) {
throw new DisconnectedDevice() throw new DisconnectedDevice()
} }
const params = { const { address } = await getAddress
currencyId: account.currency.id, .send({
devicePath: device.path, currencyId: account.currency.id,
path: account.freshAddressPath, devicePath: device.path,
segwit: isSegwitDerivationMode(account.derivationMode), path: account.freshAddressPath,
verify: true, verify: true,
} })
const { address } = await getAddress.send(params).toPromise() .toPromise()
if (address !== account.freshAddress) { if (address !== account.freshAddress) {
throw new WrongDeviceForAccount(`WrongDeviceForAccount ${account.name}`, { throw new WrongDeviceForAccount(`WrongDeviceForAccount ${account.name}`, {

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

@ -9,8 +9,8 @@ import Box from 'components/base/Box'
import LabelWithExternalIcon from 'components/base/LabelWithExternalIcon' import LabelWithExternalIcon from 'components/base/LabelWithExternalIcon'
import RecipientAddress from 'components/RecipientAddress' import RecipientAddress from 'components/RecipientAddress'
import { track } from 'analytics/segment' import { track } from 'analytics/segment'
import { createCustomErrorClass } from 'helpers/errors' import { createCustomErrorClass } from '@ledgerhq/live-common/lib/errors/helpers'
import { CantScanQRCode } from 'config/errors' import { CantScanQRCode } from '@ledgerhq/live-common/lib/errors'
type Props<Transaction> = { type Props<Transaction> = {
t: T, t: T,

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

@ -20,7 +20,7 @@ import type { StepProps as DefaultStepProps } from 'components/base/Stepper'
import { getCurrentDevice } from 'reducers/devices' import { getCurrentDevice } from 'reducers/devices'
import { accountsSelector } from 'reducers/accounts' import { accountsSelector } from 'reducers/accounts'
import { closeModal, openModal } from 'reducers/modals' import { closeModal, openModal } from 'reducers/modals'
import { DisconnectedDevice, UserRefusedOnDevice } from 'config/errors' import { DisconnectedDevice, UserRefusedOnDevice } from '@ledgerhq/live-common/lib/errors'
import Modal from 'components/base/Modal' import Modal from 'components/base/Modal'
import Stepper from 'components/base/Stepper' import Stepper from 'components/base/Stepper'

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

@ -3,7 +3,7 @@
import React, { PureComponent, Fragment } from 'react' import React, { PureComponent, Fragment } from 'react'
import { translate, Trans } from 'react-i18next' import { translate, Trans } from 'react-i18next'
import type { OsuFirmware, FinalFirmware } from '@ledgerhq/live-common/lib/types/manager'
import type { T } from 'types/common' import type { T } from 'types/common'
import Modal, { ModalBody, ModalFooter, ModalTitle, ModalContent } from 'components/base/Modal' import Modal, { ModalBody, ModalFooter, ModalTitle, ModalContent } from 'components/base/Modal'
@ -18,15 +18,13 @@ import type { ModalStatus } from 'components/ManagerPage/FirmwareUpdate'
import { getCleanVersion } from 'components/ManagerPage/FirmwareUpdate' import { getCleanVersion } from 'components/ManagerPage/FirmwareUpdate'
type FirmwareInfos = {
name: string,
notes: string,
}
type Props = { type Props = {
t: T, t: T,
status: ModalStatus, status: ModalStatus,
firmware: FirmwareInfos, firmware: {
osu: ?OsuFirmware,
final: ?FinalFirmware,
},
goToNextStep: () => void, goToNextStep: () => void,
onClose: () => void, onClose: () => void,
} }
@ -50,7 +48,9 @@ class DisclaimerModal extends PureComponent<Props, State> {
<Trans i18nKey="manager.firmware.disclaimerTitle"> <Trans i18nKey="manager.firmware.disclaimerTitle">
You are about to install You are about to install
<Text ff="Open Sans|SemiBold" color="dark"> <Text ff="Open Sans|SemiBold" color="dark">
{`firmware version ${firmware ? getCleanVersion(firmware.name) : ''}`} {`firmware version ${
firmware && firmware.osu ? getCleanVersion(firmware.osu.name) : ''
}`}
</Text> </Text>
</Trans> </Trans>
</Text> </Text>
@ -59,14 +59,16 @@ class DisclaimerModal extends PureComponent<Props, State> {
{t('manager.firmware.disclaimerAppReinstall')} {t('manager.firmware.disclaimerAppReinstall')}
</Text> </Text>
</ModalContent> </ModalContent>
<ModalContent relative pb={0} style={{ height: 250, width: '100%' }}> {firmware && firmware.osu ? (
<GrowScroll pb={5}> <ModalContent relative pb={0} style={{ height: 250, width: '100%' }}>
<Notes> <GrowScroll pb={5}>
<Markdown>{firmware.notes}</Markdown> <Notes>
</Notes> <Markdown>{firmware.osu.notes}</Markdown>
</GrowScroll> </Notes>
<GradientBox /> </GrowScroll>
</ModalContent> <GradientBox />
</ModalContent>
) : null}
<ModalFooter horizontal justifyContent="flex-end" style={{ width: '100%' }}> <ModalFooter horizontal justifyContent="flex-end" style={{ width: '100%' }}>
<Button primary onClick={goToNextStep}> <Button primary onClick={goToNextStep}>
{t('common.continue')} {t('common.continue')}

10
src/components/modals/UpdateFirmware/Installing.js

@ -4,23 +4,25 @@ import { translate } from 'react-i18next'
import Box from 'components/base/Box' import Box from 'components/base/Box'
import Text from 'components/base/Text' import Text from 'components/base/Text'
import Spinner from 'components/base/Spinner' import ProgressCircle from 'components/ProgressCircle'
import type { T } from 'types/common' import type { T } from 'types/common'
type Props = { type Props = {
t: T, t: T,
progress: number,
installing: string,
} }
function Installing({ t }: Props) { function Installing({ t, progress, installing }: Props) {
return ( return (
<Fragment> <Fragment>
<Box mx={7} align="center"> <Box mx={7} align="center">
<Spinner color="fog" size={44} /> <ProgressCircle size={64} progress={progress} />
</Box> </Box>
<Box mx={7} mt={4} mb={2}> <Box mx={7} mt={4} mb={2}>
<Text ff="Museo Sans|Regular" align="center" color="dark" fontSize={6}> <Text ff="Museo Sans|Regular" align="center" color="dark" fontSize={6}>
{t('manager.modal.installing')} {t(`manager.modal.steps.${installing}`)}
</Text> </Text>
</Box> </Box>
<Box mx={7} mt={4} mb={7}> <Box mx={7} mt={4} mb={7}>

35
src/components/modals/UpdateFirmware/index.js

@ -2,22 +2,22 @@
import React, { PureComponent } from 'react' import React, { PureComponent } from 'react'
import { translate } from 'react-i18next' import { translate } from 'react-i18next'
import type { T, Device } from 'types/common' import type { T } from 'types/common'
import Modal from 'components/base/Modal' import Modal from 'components/base/Modal'
import Stepper from 'components/base/Stepper' import Stepper from 'components/base/Stepper'
import SyncSkipUnderPriority from 'components/SyncSkipUnderPriority' import SyncSkipUnderPriority from 'components/SyncSkipUnderPriority'
import type { FirmwareUpdateContext } from '@ledgerhq/live-common/lib/types/manager'
import type { StepProps as DefaultStepProps, Step } from 'components/base/Stepper' import type { StepProps as DefaultStepProps, Step } from 'components/base/Stepper'
import type { ModalStatus } from 'components/ManagerPage/FirmwareUpdate' import type { ModalStatus } from 'components/ManagerPage/FirmwareUpdate'
import type { OsuFirmware } from 'helpers/types'
import { FreezeDeviceChangeEvents } from '../../ManagerPage/HookDeviceChange' import { FreezeDeviceChangeEvents } from '../../ManagerPage/HookDeviceChange'
import StepFullFirmwareInstall from './steps/01-step-install-full-firmware' import StepFullFirmwareInstall from './steps/01-step-install-full-firmware'
import StepFlashMcu from './steps/02-step-flash-mcu' import StepFlashMcu from './steps/02-step-flash-mcu'
import StepConfirmation, { StepConfirmFooter } from './steps/03-step-confirmation' import StepConfirmation, { StepConfirmFooter } from './steps/03-step-confirmation'
const createSteps = ({ t, shouldFlashMcu }: { t: T, shouldFlashMcu: boolean }): Array<*> => { const createSteps = ({ t }: { t: T }): Array<*> => {
const updateStep = { const updateStep = {
id: 'idCheck', id: 'idCheck',
label: t('manager.modal.identifier'), label: t('manager.modal.identifier'),
@ -45,26 +45,12 @@ const createSteps = ({ t, shouldFlashMcu }: { t: T, shouldFlashMcu: boolean }):
hideFooter: true, hideFooter: true,
} }
const steps = [updateStep] return [updateStep, mcuStep, finalStep]
if (shouldFlashMcu) {
steps.push(mcuStep)
}
steps.push(finalStep)
return steps
} }
export type Firmware = OsuFirmware & { shouldFlashMcu: boolean }
export type StepProps = DefaultStepProps & { export type StepProps = DefaultStepProps & {
firmware: Firmware, firmware: FirmwareUpdateContext,
onCloseModal: () => void, onCloseModal: () => void,
installOsuFirmware: (device: Device) => void,
installFinalFirmware: (device: Device) => void,
flashMCU: (device: Device) => void,
shouldFlashMcu: boolean,
error: ?Error, error: ?Error,
setError: Error => void, setError: Error => void,
} }
@ -75,11 +61,7 @@ type Props = {
t: T, t: T,
status: ModalStatus, status: ModalStatus,
onClose: () => void, onClose: () => void,
firmware: Firmware, firmware: FirmwareUpdateContext,
shouldFlashMcu: boolean,
installOsuFirmware: (device: Device) => void,
installFinalFirmware: (device: Device) => void,
flashMCU: (device: Device) => void,
stepId: StepId | string, stepId: StepId | string,
} }
@ -98,9 +80,6 @@ class UpdateModal extends PureComponent<Props, State> {
STEPS = createSteps({ STEPS = createSteps({
t: this.props.t, t: this.props.t,
shouldFlashMcu: this.props.firmware
? this.props.firmware.shouldFlashMcu
: this.props.shouldFlashMcu,
}) })
setError = (e: Error) => this.setState({ error: e }) setError = (e: Error) => this.setState({ error: e })
@ -114,10 +93,10 @@ class UpdateModal extends PureComponent<Props, State> {
const { stepId, error, nonce } = this.state const { stepId, error, nonce } = this.state
const additionalProps = { const additionalProps = {
firmware,
error, error,
onCloseModal: onClose, onCloseModal: onClose,
setError: this.setError, setError: this.setError,
firmware,
...props, ...props,
} }

115
src/components/modals/UpdateFirmware/steps/01-step-install-full-firmware.js

@ -3,24 +3,17 @@
import React, { PureComponent, Fragment } from 'react' import React, { PureComponent, Fragment } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { timeout } from 'rxjs/operators/timeout'
import { DEVICE_INFOS_TIMEOUT } from 'config/constants'
import getDeviceInfo from 'commands/getDeviceInfo'
import firmwarePrepare from 'commands/firmwarePrepare'
import { getCurrentDevice } from 'reducers/devices' import { getCurrentDevice } from 'reducers/devices'
import { createCancelablePolling, delay } from 'helpers/promise'
import TrackPage from 'analytics/TrackPage' import TrackPage from 'analytics/TrackPage'
import Box from 'components/base/Box' import Box from 'components/base/Box'
import Text from 'components/base/Text' import Text from 'components/base/Text'
import ProgressBar from 'components/ProgressBar'
import DeviceConfirm from 'components/DeviceConfirm' import DeviceConfirm from 'components/DeviceConfirm'
import type { Device } from 'types/common' import type { Device } from 'types/common'
import type { StepProps } from '../' import type { StepProps } from '../'
import Installing from '../Installing'
const Container = styled(Box).attrs({ const Container = styled(Box).attrs({
alignItems: 'center', alignItems: 'center',
fontSize: 4, fontSize: 4,
@ -55,79 +48,44 @@ type Props = StepProps & {
device: Device, device: Device,
} }
type State = { class StepFullFirmwareInstall extends PureComponent<Props, { progress: number }> {
installing: boolean,
}
class StepFullFirmwareInstall extends PureComponent<Props, State> {
state = { state = {
installing: false, progress: 0,
} }
componentDidMount() { componentDidMount() {
this.install() const { firmware, device, transitionTo, setError } = this.props
}
componentWillUnmount() { if (!firmware.osu) {
if (this._unsubConnect) this._unsubConnect() transitionTo('finish')
} return
ensureDevice = () => {
const { unsubscribe, promise } = createCancelablePolling(async () => {
const { device } = this.props
if (!device) {
throw new Error('No device')
}
const deviceInfo = await getDeviceInfo
.send({ devicePath: device.path })
.pipe(timeout(DEVICE_INFOS_TIMEOUT))
.toPromise()
return { device, deviceInfo }
})
this._unsubConnect = unsubscribe
return promise
}
install = async () => {
const {
installOsuFirmware,
installFinalFirmware,
firmware,
shouldFlashMcu,
transitionTo,
setError,
} = this.props
const { device, deviceInfo } = await this.ensureDevice()
if (deviceInfo.isBootloader) {
transitionTo('updateMCU')
} }
try { this.sub = firmwarePrepare
if (deviceInfo.isOSU) { .send({
this.setState({ installing: true }) devicePath: device.path,
await installFinalFirmware(device) firmware,
transitionTo('finish') })
} else { .subscribe({
await installOsuFirmware(device) next: patch => {
this.setState({ installing: true }) this.setState(patch)
if (this._unsubConnect) this._unsubConnect() },
if ((firmware && firmware.shouldFlashMcu) || shouldFlashMcu) { complete: () => {
delay(1000)
transitionTo('updateMCU') transitionTo('updateMCU')
} else { },
const { device: freshDevice } = await this.ensureDevice() error: error => {
await installFinalFirmware(freshDevice) setError(error)
transitionTo('finish') transitionTo('finish')
} },
} })
} catch (error) { }
setError(error)
transitionTo('finish') componentWillUnmount() {
} if (this.sub) this.sub.unsubscribe()
} }
sub: *
formatHashName = (hash: string): string => { formatHashName = (hash: string): string => {
if (!hash) { if (!hash) {
return '' return ''
@ -138,21 +96,19 @@ class StepFullFirmwareInstall extends PureComponent<Props, State> {
} }
renderBody = () => { renderBody = () => {
const { installing } = this.state
const { t, firmware } = this.props const { t, firmware } = this.props
return installing ? ( return (
<Installing />
) : (
<Fragment> <Fragment>
<Text ff="Open Sans|Regular" align="center" color="smoke"> <Text ff="Open Sans|Regular" align="center" color="smoke">
{t('manager.modal.confirmIdentifierText')} {t('manager.modal.confirmIdentifierText')}
</Text> </Text>
<Box mx={7} mt={5}> <Box mx={7} my={5}>
<Text ff="Open Sans|SemiBold" align="center" color="smoke"> <Text ff="Open Sans|SemiBold" align="center" color="smoke">
{t('manager.modal.identifier')} {t('manager.modal.identifier')}
</Text> </Text>
<Address>{firmware && this.formatHashName(firmware.hash)}</Address> <Address>{firmware.osu && this.formatHashName(firmware.osu.hash)}</Address>
</Box> </Box>
<ProgressBar progress={this.state.progress} width={200} />
<Box mt={5}> <Box mt={5}>
<DeviceConfirm /> <DeviceConfirm />
</Box> </Box>
@ -160,14 +116,11 @@ class StepFullFirmwareInstall extends PureComponent<Props, State> {
) )
} }
_unsubConnect: *
render() { render() {
const { installing } = this.state
const { t } = this.props const { t } = this.props
return ( return (
<Container> <Container>
<Title>{installing ? '' : t('manager.modal.confirmIdentifier')}</Title> <Title>{t('manager.modal.confirmIdentifier')}</Title>
<TrackPage category="Manager" name="InstallFirmware" /> <TrackPage category="Manager" name="InstallFirmware" />
{this.renderBody()} {this.renderBody()}
</Container> </Container>

117
src/components/modals/UpdateFirmware/steps/02-step-flash-mcu.js

@ -2,21 +2,14 @@
import React, { PureComponent, Fragment } from 'react' import React, { PureComponent, Fragment } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { connect } from 'react-redux'
import { timeout } from 'rxjs/operators/timeout'
import { DEVICE_INFOS_TIMEOUT } from 'config/constants'
import { i } from 'helpers/staticPath' import { i } from 'helpers/staticPath'
import { getCurrentDevice } from 'reducers/devices' import firmwareMain from 'commands/firmwareMain'
import { createCancelablePolling } from 'helpers/promise'
import getDeviceInfo from 'commands/getDeviceInfo'
import TrackPage from 'analytics/TrackPage' import TrackPage from 'analytics/TrackPage'
import Box from 'components/base/Box' import Box from 'components/base/Box'
import Text from 'components/base/Text' import Text from 'components/base/Text'
import type { Device } from 'types/common'
import type { StepProps } from '../' import type { StepProps } from '../'
import Installing from '../Installing' import Installing from '../Installing'
@ -46,107 +39,48 @@ const Separator = styled(Box).attrs({
background-color: currentColor; background-color: currentColor;
` `
const mapStateToProps = state => ({ type Props = StepProps
device: getCurrentDevice(state),
})
type Props = StepProps & { device?: Device }
type State = { type State = {
installing: boolean, installing: ?string,
progress: number,
} }
class StepFlashMcu extends PureComponent<Props, State> { class StepFlashMcu extends PureComponent<Props, State> {
state = { state = {
installing: false, installing: null,
progress: 0,
} }
componentDidMount() { componentDidMount() {
this.install() const { firmware, transitionTo, setError } = this.props
}
componentWillUnmount() {
if (this._unsubConnect) this._unsubConnect()
if (this._unsubDeviceInfo) this._unsubDeviceInfo()
}
getDeviceInfo = () => {
const { unsubscribe, promise } = createCancelablePolling(async () => {
const { device } = this.props
if (!device) {
throw new Error('No device')
}
const deviceInfo = await getDeviceInfo
.send({ devicePath: device.path })
.pipe(timeout(DEVICE_INFOS_TIMEOUT))
.toPromise()
return { device, deviceInfo }
})
this._unsubDeviceInfo = unsubscribe
return promise
}
waitForDeviceInBootloader = () => {
const { unsubscribe, promise } = createCancelablePolling(async () => {
const { device } = this.props
if (!device) {
throw new Error('No device')
}
const deviceInfo = await getDeviceInfo
.send({ devicePath: device.path })
.pipe(timeout(DEVICE_INFOS_TIMEOUT))
.toPromise()
if (!deviceInfo.isBootloader) {
throw new Error('Device is not in bootloader')
}
return { device, deviceInfo }
})
this._unsubConnect = unsubscribe
return promise
}
flash = async () => {
await this.waitForDeviceInBootloader()
const { flashMCU, device } = this.props
if (device) {
this.setState({ installing: true })
await flashMCU(device)
}
}
install = async () => {
const { transitionTo, installFinalFirmware, setError } = this.props
const { deviceInfo, device } = await this.getDeviceInfo()
try { this.sub = firmwareMain.send(firmware).subscribe({
if (deviceInfo.isBootloader) { next: patch => {
await this.flash() this.setState(patch)
this.install() },
} else if (deviceInfo.isOSU) { complete: () => {
await installFinalFirmware(device)
transitionTo('finish') transitionTo('finish')
} else { },
error: error => {
setError(error)
transitionTo('finish') transitionTo('finish')
} },
} catch (error) { })
setError(error)
transitionTo('finish')
}
} }
firstFlash = async () => { componentWillUnmount() {
await this.flash() if (this.sub) this.sub.unsubscribe()
this.install()
} }
sub: *
renderBody = () => { renderBody = () => {
const { installing } = this.state const { installing, progress } = this.state
const { t } = this.props const { t } = this.props
return installing ? ( return installing ? (
<Installing /> <Installing installing={installing} progress={progress} />
) : ( ) : (
<Fragment> <Fragment>
<Box mx={7}> <Box mx={7}>
@ -176,9 +110,6 @@ class StepFlashMcu extends PureComponent<Props, State> {
) )
} }
_unsubConnect: *
_unsubDeviceInfo: *
render() { render() {
const { t } = this.props const { t } = this.props
const { installing } = this.state const { installing } = this.state
@ -192,4 +123,4 @@ class StepFlashMcu extends PureComponent<Props, State> {
} }
} }
export default connect(mapStateToProps)(StepFlashMcu) export default StepFlashMcu

7
src/config/constants.js

@ -58,12 +58,6 @@ export const LEDGER_REST_API_BASE = stringFromEnv(
'LEDGER_REST_API_BASE', 'LEDGER_REST_API_BASE',
'https://explorers.api.live.ledger.com', 'https://explorers.api.live.ledger.com',
) )
export const MANAGER_API_BASE = stringFromEnv(
'MANAGER_API_BASE',
'https://manager.api.live.ledger.com/api',
)
export const BASE_SOCKET_URL = stringFromEnv('BASE_SOCKET_URL', 'wss://api.ledgerwallet.com/update')
// Provider // Provider
export const FORCE_PROVIDER = intFromEnv('FORCE_PROVIDER', 0) export const FORCE_PROVIDER = intFromEnv('FORCE_PROVIDER', 0)
@ -84,6 +78,7 @@ export const LEDGER_DEBUG_ALL_LANGS = boolFromEnv('LEDGER_DEBUG_ALL_LANGS')
export const SKIP_GENUINE = boolFromEnv('SKIP_GENUINE') export const SKIP_GENUINE = boolFromEnv('SKIP_GENUINE')
export const SKIP_ONBOARDING = boolFromEnv('SKIP_ONBOARDING') export const SKIP_ONBOARDING = boolFromEnv('SKIP_ONBOARDING')
export const SHOW_LEGACY_NEW_ACCOUNT = boolFromEnv('SHOW_LEGACY_NEW_ACCOUNT') export const SHOW_LEGACY_NEW_ACCOUNT = boolFromEnv('SHOW_LEGACY_NEW_ACCOUNT')
export const SHOW_MOCK_HSMWARNINGS = boolFromEnv('SHOW_MOCK_HSMWARNINGS')
export const HIGHLIGHT_I18N = boolFromEnv('HIGHLIGHT_I18N') export const HIGHLIGHT_I18N = boolFromEnv('HIGHLIGHT_I18N')
export const DISABLE_ACTIVITY_INDICATORS = boolFromEnv('DISABLE_ACTIVITY_INDICATORS') export const DISABLE_ACTIVITY_INDICATORS = boolFromEnv('DISABLE_ACTIVITY_INDICATORS')
export const EXPERIMENTAL_CENTER_MODAL = boolFromEnv('EXPERIMENTAL_CENTER_MODAL') export const EXPERIMENTAL_CENTER_MODAL = boolFromEnv('EXPERIMENTAL_CENTER_MODAL')

53
src/config/errors.js

@ -1,53 +0,0 @@
// @flow
// TODO we need to start porting all custom errors here.
import { createCustomErrorClass } from 'helpers/errors'
export const AccountNameRequiredError = createCustomErrorClass('AccountNameRequired')
export const BtcUnmatchedApp = createCustomErrorClass('BtcUnmatchedApp')
export const CantOpenDevice = createCustomErrorClass('CantOpenDevice')
export const DeviceAppVerifyNotSupported = createCustomErrorClass('DeviceAppVerifyNotSupported')
export const DeviceGenuineSocketEarlyClose = createCustomErrorClass('DeviceGenuineSocketEarlyClose')
export const DeviceNotGenuineError = createCustomErrorClass('DeviceNotGenuine')
export const DeviceSocketFail = createCustomErrorClass('DeviceSocketFail')
export const DeviceSocketNoBulkStatus = createCustomErrorClass('DeviceSocketNoBulkStatus')
export const DeviceSocketNoHandler = createCustomErrorClass('DeviceSocketNoHandler')
export const DisconnectedDevice = createCustomErrorClass('DisconnectedDevice')
export const EnpointConfigError = createCustomErrorClass('EnpointConfig')
export const FeeEstimationFailed = createCustomErrorClass('FeeEstimationFailed')
export const HardResetFail = createCustomErrorClass('HardResetFail')
export const InvalidAddress = createCustomErrorClass('InvalidAddress')
export const LatestMCUInstalledError = createCustomErrorClass('LatestMCUInstalledError')
export const LedgerAPIError = createCustomErrorClass('LedgerAPIError')
export const LedgerAPIErrorWithMessage = createCustomErrorClass('LedgerAPIErrorWithMessage')
export const LedgerAPINotAvailable = createCustomErrorClass('LedgerAPINotAvailable')
export const ManagerAppAlreadyInstalledError = createCustomErrorClass('ManagerAppAlreadyInstalled')
export const ManagerAppRelyOnBTCError = createCustomErrorClass('ManagerAppRelyOnBTC')
export const ManagerDeviceLockedError = createCustomErrorClass('ManagerDeviceLocked')
export const ManagerNotEnoughSpaceError = createCustomErrorClass('ManagerNotEnoughSpace')
export const ManagerUninstallBTCDep = createCustomErrorClass('ManagerUninstallBTCDep')
export const NetworkDown = createCustomErrorClass('NetworkDown')
export const NoAddressesFound = createCustomErrorClass('NoAddressesFound')
export const NotEnoughBalance = createCustomErrorClass('NotEnoughBalance')
export const NotEnoughBalanceBecauseDestinationNotCreated = createCustomErrorClass(
'NotEnoughBalanceBecauseDestinationNotCreated',
)
export const PasswordsDontMatchError = createCustomErrorClass('PasswordsDontMatch')
export const PasswordIncorrectError = createCustomErrorClass('PasswordIncorrect')
export const TimeoutTagged = createCustomErrorClass('TimeoutTagged')
export const UpdateYourApp = createCustomErrorClass('UpdateYourApp')
export const UserRefusedAddress = createCustomErrorClass('UserRefusedAddress')
export const UserRefusedFirmwareUpdate = createCustomErrorClass('UserRefusedFirmwareUpdate')
export const UserRefusedOnDevice = createCustomErrorClass('UserRefusedOnDevice') // TODO rename because it's just for transaction refusal
export const WebsocketConnectionError = createCustomErrorClass('WebsocketConnectionError')
export const WebsocketConnectionFailed = createCustomErrorClass('WebsocketConnectionFailed')
export const WrongDeviceForAccount = createCustomErrorClass('WrongDeviceForAccount')
export const ETHAddressNonEIP = createCustomErrorClass('ETHAddressNonEIP')
export const CantScanQRCode = createCustomErrorClass('CantScanQRCode')
export const FeeNotLoaded = createCustomErrorClass('FeeNotLoaded')
// db stuff, no need to translate
export const NoDBPathGiven = createCustomErrorClass('NoDBPathGiven')
export const DBWrongPassword = createCustomErrorClass('DBWrongPassword')
export const DBNotReset = createCustomErrorClass('DBNotReset')

52
src/helpers/apps/installApp.js

@ -1,52 +0,0 @@
// @flow
import type Transport from '@ledgerhq/hw-transport'
import { createDeviceSocket } from 'helpers/socket'
import type { ApplicationVersion } from 'helpers/types'
import { WS_INSTALL } from 'helpers/urls'
import {
ManagerNotEnoughSpaceError,
ManagerDeviceLockedError,
ManagerAppAlreadyInstalledError,
ManagerAppRelyOnBTCError,
} from 'config/errors'
function remapError(promise) {
return promise.catch((e: Error) => {
switch (true) {
case e.message.endsWith('6982'):
throw new ManagerDeviceLockedError()
case e.message.endsWith('6a84') || e.message.endsWith('6a85'):
throw new ManagerNotEnoughSpaceError()
case e.message.endsWith('6a80') || e.message.endsWith('6a81'):
throw new ManagerAppAlreadyInstalledError()
case e.message.endsWith('6a83'):
throw new ManagerAppRelyOnBTCError()
default:
throw e
}
})
}
/**
* Install an app on the device
*/
export default async function installApp(
transport: Transport<*>,
targetId: string | number,
{ app }: { app: ApplicationVersion },
): Promise<void> {
const params = {
targetId,
perso: app.perso,
deleteKey: app.delete_key,
firmware: app.firmware,
firmwareKey: app.firmware_key,
hash: app.hash,
}
const url = WS_INSTALL(params)
await remapError(createDeviceSocket(transport, url).toPromise())
}

30
src/helpers/apps/listAppVersions.js

@ -1,30 +0,0 @@
// @flow
import network from 'api/network'
import type { DeviceInfo, DeviceVersion, FinalFirmware, ApplicationVersion } from 'helpers/types'
import { APPLICATIONS_BY_DEVICE } from 'helpers/urls'
import getDeviceVersion from 'helpers/devices/getDeviceVersion'
import getCurrentFirmware from 'helpers/devices/getCurrentFirmware'
type NetworkResponse = { data: { application_versions: Array<ApplicationVersion> } }
export default async (deviceInfo: DeviceInfo): Promise<Array<ApplicationVersion>> => {
const deviceData: DeviceVersion = await getDeviceVersion(
deviceInfo.targetId,
deviceInfo.providerId,
)
const firmwareData: FinalFirmware = await getCurrentFirmware({
deviceId: deviceData.id,
fullVersion: deviceInfo.fullVersion,
provider: deviceInfo.providerId,
})
const params = {
provider: deviceInfo.providerId,
current_se_firmware_final_version: firmwareData.id,
device_version: deviceData.id,
}
const {
data: { application_versions },
}: NetworkResponse = await network({ method: 'POST', url: APPLICATIONS_BY_DEVICE, data: params })
return application_versions.length > 0 ? application_versions : []
}

10
src/helpers/apps/listApps.js

@ -1,10 +0,0 @@
// @flow
import network from 'api/network'
import { GET_APPLICATIONS } from 'helpers/urls'
import type { Application } from 'helpers/types'
export default async (): Promise<Array<Application>> => {
const { data } = await network({ method: 'GET', url: GET_APPLICATIONS })
return data.length > 0 ? data : []
}

10
src/helpers/apps/listCategories.js

@ -1,10 +0,0 @@
// @flow
import network from 'api/network'
import { GET_CATEGORIES } from 'helpers/urls'
import type { Category } from 'helpers/types'
export default async (): Promise<Array<Category>> => {
const { data }: { data: Array<Category> } = await network({ method: 'GET', url: GET_CATEGORIES })
return data.length > 0 ? data : []
}

41
src/helpers/apps/uninstallApp.js

@ -1,41 +0,0 @@
// @flow
import type Transport from '@ledgerhq/hw-transport'
import { createDeviceSocket } from 'helpers/socket'
import type { ApplicationVersion } from 'helpers/types'
import { ManagerDeviceLockedError, ManagerUninstallBTCDep } from 'config/errors'
import { WS_INSTALL } from 'helpers/urls'
function remapError(promise) {
return promise.catch((e: Error) => {
switch (true) {
case e.message.endsWith('6982'):
throw new ManagerDeviceLockedError()
case e.message.endsWith('6a83'):
throw new ManagerUninstallBTCDep()
default:
throw e
}
})
}
/**
* Install an app on the device
*/
export default async function uninstallApp(
transport: Transport<*>,
targetId: string | number,
{ app }: { app: ApplicationVersion },
): Promise<void> {
const params = {
targetId,
perso: app.perso,
deleteKey: app.delete_key,
firmware: app.delete,
firmwareKey: app.delete_key,
hash: app.hash,
}
const url = WS_INSTALL(params)
await remapError(createDeviceSocket(transport, url).toPromise())
}

5
src/helpers/db/db.spec.js

@ -1,15 +1,12 @@
import os from 'os' import os from 'os'
import path from 'path' import path from 'path'
import fs from 'fs'
import rimrafModule from 'rimraf' import rimrafModule from 'rimraf'
import db from 'helpers/db' import db from 'helpers/db'
import { promisify } from 'helpers/promise' import { promisify } from 'helpers/promise'
import { fsReadFile, fsWriteFile, fsMkdir } from 'helpers/fs'
const rimraf = promisify(rimrafModule) const rimraf = promisify(rimrafModule)
const fsReadFile = promisify(fs.readFile)
const fsWriteFile = promisify(fs.writeFile)
const fsMkdir = promisify(fs.mkdir)
const accountsTransform = { const accountsTransform = {
get: accounts => accounts.map(account => ({ ...account, balance: Number(account.balance) })), get: accounts => accounts.map(account => ({ ...account, balance: Number(account.balance) })),

6
src/helpers/db/index.js

@ -1,6 +1,5 @@
// @flow // @flow
import fs from 'fs'
import path from 'path' import path from 'path'
import crypto from 'crypto' import crypto from 'crypto'
import cloneDeep from 'lodash/cloneDeep' import cloneDeep from 'lodash/cloneDeep'
@ -9,17 +8,16 @@ import get from 'lodash/get'
import set from 'lodash/set' import set from 'lodash/set'
import logger from 'logger' import logger from 'logger'
import { fsReadFile, fsUnlink } from 'helpers/fs'
import { promisify, debounce } from 'helpers/promise' import { promisify, debounce } from 'helpers/promise'
import { NoDBPathGiven, DBWrongPassword } from 'config/errors' import { NoDBPathGiven, DBWrongPassword } from '@ledgerhq/live-common/lib/errors'
type Transform = { type Transform = {
get: any => any, get: any => any,
set: any => any, set: any => any,
} }
const fsReadFile = promisify(fs.readFile)
const fsUnlink = promisify(fs.unlink)
const writeFileAtomic = promisify(writeFileAtomicModule) const writeFileAtomic = promisify(writeFileAtomicModule)
const ALGORITHM = 'aes-256-cbc' const ALGORITHM = 'aes-256-cbc'

9
src/helpers/debugAppInfosForCurrency/btc.js

@ -1,9 +0,0 @@
// @flow
import type Transport from '@ledgerhq/hw-transport'
export default async (transport: Transport<*>) => {
const r = await transport.send(0xe0, 0xc4, 0, 0)
const version = `${r[2]}.${r[3]}.${r[4]}`
return { version }
}

10
src/helpers/debugAppInfosForCurrency/ethereum.js

@ -1,10 +0,0 @@
// @flow
import Eth from '@ledgerhq/hw-app-eth'
import type Transport from '@ledgerhq/hw-transport'
export default async (transport: Transport<*>) => {
const eth = new Eth(transport)
const { version } = await eth.getAppConfiguration()
return { version }
}

29
src/helpers/debugAppInfosForCurrency/index.js

@ -1,29 +0,0 @@
// @flow
import type { CryptoCurrency } from '@ledgerhq/live-common/lib/types'
import invariant from 'invariant'
import type Transport from '@ledgerhq/hw-transport'
import bitcoin from './btc'
import ethereum from './ethereum'
import ripple from './ripple'
type Resolver = (
transport: Transport<*>,
currency: CryptoCurrency,
) => Promise<{
version?: string,
}>
const perFamily: { [_: string]: * } = {
bitcoin,
ethereum,
ripple,
}
const proxy: Resolver = (transport, currency) => {
const getAddress = perFamily[currency.family]
invariant(getAddress, `getAddress not implemented for ${currency.id}`)
return getAddress(transport)
}
export default proxy

10
src/helpers/debugAppInfosForCurrency/ripple.js

@ -1,10 +0,0 @@
// @flow
import Xrp from '@ledgerhq/hw-app-xrp'
import type Transport from '@ledgerhq/hw-transport'
export default async (transport: Transport<*>) => {
const xrp = new Xrp(transport)
const { version } = await xrp.getAppConfiguration()
return { version }
}

64
src/helpers/deviceAccess.js

@ -1,64 +0,0 @@
// @flow
import logger from 'logger'
import throttle from 'lodash/throttle'
import type Transport from '@ledgerhq/hw-transport'
import TransportNodeHid from '@ledgerhq/hw-transport-node-hid'
import { DisconnectedDevice, CantOpenDevice } from 'config/errors'
import { retry } from './promise'
// all open to device must use openDevice so we can prevent race conditions
// and guarantee we do one device access at a time. It also will handle the .close()
// NOTE optim: in the future we can debounce the close & reuse the same transport instance.
type WithDevice = (devicePath: string) => <T>(job: (Transport<*>) => Promise<*>) => Promise<T>
const mapError = e => {
if (e && e.message && e.message.indexOf('cannot open device with path') >= 0) {
throw new CantOpenDevice(e.message)
}
if (e && e.message && e.message.indexOf('HID') >= 0) {
throw new DisconnectedDevice(e.message)
}
throw e
}
let queue = Promise.resolve()
let busy = false
TransportNodeHid.setListenDevicesPollingSkip(() => busy)
const refreshBusyUIState = throttle(() => {
if (process.env.CLI) return
process.send({
type: 'setDeviceBusy',
busy,
})
}, 100)
export const withDevice: WithDevice = devicePath => job => {
const p = queue.then(async () => {
busy = true
refreshBusyUIState()
try {
// $FlowFixMe not sure what's wrong
const t = await retry(() => TransportNodeHid.open(devicePath), { maxRetry: 2 }).catch(
mapError,
)
t.setDebugMode(logger.apdu)
try {
const res = await job(t).catch(mapError)
return res
} finally {
await t.close()
}
} finally {
busy = false
refreshBusyUIState()
}
})
queue = p.catch(() => null)
return p
}

21
src/helpers/devices/getBitcoinLikeInfo.js

@ -1,21 +0,0 @@
// @flow
import type Transport from '@ledgerhq/hw-transport'
const getBitcoinLikeInfo = (
transport: Transport<any>,
): Promise<{
P2PKH: number,
P2SH: number,
message: Buffer,
short: Buffer,
}> =>
transport.send(0xe0, 0x16, 0x00, 0x00).then(res => {
const P2PKH = res.readUInt16BE(0)
const P2SH = res.readUInt16BE(2)
const message = res.slice(5, res.readUInt8(4))
const short = res.slice(5 + message.length + 1, res.readUInt8(5 + message.length))
return { P2PKH, P2SH, message, short }
})
export default getBitcoinLikeInfo

24
src/helpers/devices/getCurrentFirmware.js

@ -1,24 +0,0 @@
// @flow
import network from 'api/network'
import { GET_CURRENT_FIRMWARE } from 'helpers/urls'
import type { FinalFirmware } from 'helpers/types'
type Input = {
fullVersion: string,
deviceId: string | number,
provider: number,
}
export default async (input: Input): Promise<FinalFirmware> => {
const { data }: { data: FinalFirmware } = await network({
method: 'POST',
url: GET_CURRENT_FIRMWARE,
data: {
device_version: input.deviceId,
version_name: input.fullVersion,
provider: input.provider,
},
})
return data
}

42
src/helpers/devices/getDeviceInfo.js

@ -1,42 +0,0 @@
// @flow
import type Transport from '@ledgerhq/hw-transport'
import getFirmwareInfo from 'helpers/firmware/getFirmwareInfo'
import { FORCE_PROVIDER } from 'config/constants'
import type { DeviceInfo } from 'helpers/types'
const PROVIDERS = {
'': 1,
das: 2,
club: 3,
shitcoins: 4,
ee: 5,
}
export default async (transport: Transport<*>): Promise<DeviceInfo> => {
const res = await getFirmwareInfo(transport)
const { seVersion } = res
const { targetId, mcuVersion, flags } = res
const parsedVersion =
seVersion.match(/([0-9]+.[0-9])+(.[0-9]+)?((?!-osu)-([a-z]+))?(-osu)?/) || []
const isOSU = typeof parsedVersion[5] !== 'undefined'
const providerName = parsedVersion[4] || ''
const providerId = FORCE_PROVIDER || PROVIDERS[providerName]
const isBootloader = targetId === 0x01000001
const majMin = parsedVersion[1]
const patch = parsedVersion[2] || '.0'
const fullVersion = `${majMin}${patch}${providerName ? `-${providerName}` : ''}`
return {
targetId,
seVersion: majMin + patch,
isOSU,
mcuVersion,
isBootloader,
providerName,
providerId,
flags,
fullVersion,
}
}

17
src/helpers/devices/getDeviceVersion.js

@ -1,17 +0,0 @@
// @flow
import { GET_DEVICE_VERSION } from 'helpers/urls'
import network from 'api/network'
import type { DeviceVersion } from 'helpers/types'
export default async (targetId: string | number, provider: number): Promise<DeviceVersion> => {
const { data }: { data: DeviceVersion } = await network({
method: 'POST',
url: GET_DEVICE_VERSION,
data: {
provider,
target_id: targetId,
},
})
return data
}

32
src/helpers/devices/getIsGenuine.js

@ -1,32 +0,0 @@
// @flow
import type Transport from '@ledgerhq/hw-transport'
import { SKIP_GENUINE } from 'config/constants'
import { WS_GENUINE } from 'helpers/urls'
import type { DeviceInfo, FinalFirmware, DeviceVersion } from 'helpers/types'
import { createDeviceSocket } from 'helpers/socket'
import getCurrentFirmware from './getCurrentFirmware'
import getDeviceVersion from './getDeviceVersion'
export default async (transport: Transport<*>, deviceInfo: DeviceInfo): Promise<string> => {
const deviceVersion: DeviceVersion = await getDeviceVersion(
deviceInfo.targetId,
deviceInfo.providerId,
)
const firmware: FinalFirmware = await getCurrentFirmware({
deviceId: deviceVersion.id,
fullVersion: deviceInfo.fullVersion,
provider: deviceInfo.providerId,
})
const params = {
targetId: deviceInfo.targetId,
perso: firmware.perso,
}
const url = WS_GENUINE(params)
return SKIP_GENUINE
? new Promise(resolve => setTimeout(() => resolve('0000'), 1000))
: createDeviceSocket(transport, url).toPromise()
}

76
src/helpers/devices/getLatestFirmwareForDevice.js

@ -1,76 +0,0 @@
// @flow
import network from 'api/network'
import { GET_LATEST_FIRMWARE } from 'helpers/urls'
import type {
DeviceInfo,
DeviceVersion,
FinalFirmware,
OsuFirmware,
McuVersion,
} from 'helpers/types'
import getFinalFirmwareById from 'helpers/firmware/getFinalFirmwareById'
import getMcus from 'helpers/firmware/getMcus'
import getCurrentFirmware from './getCurrentFirmware'
import getDeviceVersion from './getDeviceVersion'
type NetworkResponse = {
data: {
result: string,
se_firmware_osu_version: OsuFirmware,
},
}
type Result = ?(OsuFirmware & { shouldFlashMcu: boolean })
export default async (deviceInfo: DeviceInfo): Promise<Result> => {
// Get device infos from targetId
const deviceVersion: DeviceVersion = await getDeviceVersion(
deviceInfo.targetId,
deviceInfo.providerId,
)
// Get firmware infos with firmware name and device version
const seFirmwareVersion: FinalFirmware = await getCurrentFirmware({
fullVersion: deviceInfo.fullVersion,
deviceId: deviceVersion.id,
provider: deviceInfo.providerId,
})
// Fetch next possible firmware
const { data }: NetworkResponse = await network({
method: 'POST',
url: GET_LATEST_FIRMWARE,
data: {
current_se_firmware_final_version: seFirmwareVersion.id,
device_version: deviceVersion.id,
provider: deviceInfo.providerId,
},
})
if (data.result === 'null') {
return null
}
const { se_firmware_osu_version } = data
const { next_se_firmware_final_version } = se_firmware_osu_version
const seFirmwareFinalVersion: FinalFirmware = await getFinalFirmwareById(
next_se_firmware_final_version,
)
const mcus: Array<McuVersion> = await getMcus()
const currentMcuVersionId: Array<number> = mcus
.filter(mcu => mcu.name === deviceInfo.mcuVersion)
.map(mcu => mcu.id)
if (!seFirmwareFinalVersion.mcu_versions.includes(...currentMcuVersionId)) {
return {
...se_firmware_osu_version,
shouldFlashMcu: true,
}
}
return { ...se_firmware_osu_version, shouldFlashMcu: false }
}

10
src/helpers/devices/getMemInfo.js

@ -1,10 +0,0 @@
// @flow
import type Transport from '@ledgerhq/hw-transport'
import getFirmwareInfo from 'helpers/firmware/getFirmwareInfo'
export default async function getMemInfos(transport: Transport<*>): Promise<Object> {
const { targetId } = await getFirmwareInfo(transport) // eslint-disable-line
return new Promise(resolve => setTimeout(() => resolve({}), 1000))
}

23
src/helpers/devices/getOsuFirmware.js

@ -1,23 +0,0 @@
// @flow
import network from 'api/network'
import { GET_CURRENT_OSU } from 'helpers/urls'
type Input = {
version: string,
deviceId: string | number,
provider: number,
}
export default async (input: Input): Promise<*> => {
const { data } = await network({
method: 'POST',
url: GET_CURRENT_OSU,
data: {
device_version: input.deviceId,
version_name: `${input.version}-osu`,
provider: input.provider,
},
})
return data
}

16
src/helpers/devices/isDashboardOpen.js

@ -1,16 +0,0 @@
// @flow
import type Transport from '@ledgerhq/hw-transport'
import getFirmwareInfo from 'helpers/firmware/getFirmwareInfo'
type Result = boolean
export default async (transport: Transport<*>): Promise<Result> => {
const { targetId, seVersion } = await getFirmwareInfo(transport)
if (targetId && seVersion) {
return true
}
return false
}

49
src/helpers/devices/shouldFlashMcu.js

@ -1,49 +0,0 @@
// @flow
import network from 'api/network'
import { GET_LATEST_FIRMWARE } from 'helpers/urls'
import type { DeviceInfo } from 'helpers/types'
import getFinalFirmwareById from 'helpers/firmware/getFinalFirmwareById'
import getMcus from 'helpers/firmware/getMcus'
import getOsuFirmware from './getOsuFirmware'
import getDeviceVersion from './getDeviceVersion'
export default async (deviceInfo: DeviceInfo): Promise<boolean> => {
// Get device infos from targetId
const deviceVersion = await getDeviceVersion(deviceInfo.targetId, deviceInfo.providerId)
// Get firmware infos with firmware name and device version
const seFirmwareVersion = await getOsuFirmware({
version: deviceInfo.fullVersion,
deviceId: deviceVersion.id,
provider: deviceInfo.providerId,
})
// Fetch next possible firmware
const { data } = await network({
method: 'POST',
url: GET_LATEST_FIRMWARE,
data: {
current_se_firmware_final_version: seFirmwareVersion.id,
device_version: deviceVersion.id,
provider: deviceInfo.providerId,
},
})
if (data.result === 'null') {
return false
}
const { se_firmware_osu_version } = data
const { next_se_firmware_final_version } = se_firmware_osu_version
const seFirmwareFinalVersion = await getFinalFirmwareById(next_se_firmware_final_version)
const mcus = await getMcus()
const currentMcuVersionId = mcus
.filter(mcu => mcu.name === deviceInfo.mcuVersion)
.map(mcu => mcu.id)
return !seFirmwareFinalVersion.mcu_versions.includes(...currentMcuVersionId)
}

96
src/helpers/errors.js

@ -1,96 +0,0 @@
// @flow
/* eslint-disable no-continue */
// TODO we need to centralize the error in one place. so all are recorded
const errorClasses = {}
export const createCustomErrorClass = (name: string): Class<any> => {
const C = function CustomError(message?: string, fields?: Object) {
Object.assign(this, fields)
this.name = name
this.message = message || name
this.stack = new Error().stack
}
// $FlowFixMe
C.prototype = new Error()
errorClasses[name] = C
// $FlowFixMe we can't easily type a subset of Error for now...
return C
}
// inspired from https://github.com/programble/errio/blob/master/index.js
export const deserializeError = (object: mixed): Error => {
if (typeof object === 'object' && object) {
try {
// $FlowFixMe FIXME HACK
const msg = JSON.parse(object.message)
if (msg.message && msg.name) {
object = msg
}
} catch (e) {
// nothing
}
const constructor =
object.name === 'Error'
? Error
: typeof object.name === 'string'
? errorClasses[object.name] || createCustomErrorClass(object.name)
: Error
const error = Object.create(constructor.prototype)
for (const prop in object) {
if (object.hasOwnProperty(prop)) {
error[prop] = object[prop]
}
}
if (!error.stack && Error.captureStackTrace) {
Error.captureStackTrace(error, deserializeError)
}
return error
}
return new Error(String(object))
}
// inspired from https://github.com/sindresorhus/serialize-error/blob/master/index.js
export const serializeError = (value: mixed) => {
if (!value) return value
if (typeof value === 'object') {
return destroyCircular(value, [])
}
if (typeof value === 'function') {
return `[Function: ${value.name || 'anonymous'}]`
}
return value
}
// https://www.npmjs.com/package/destroy-circular
function destroyCircular(from: Object, seen) {
const to = {}
seen.push(from)
for (const key of Object.keys(from)) {
const value = from[key]
if (typeof value === 'function') {
continue
}
if (!value || typeof value !== 'object') {
to[key] = value
continue
}
if (seen.indexOf(from[key]) === -1) {
to[key] = destroyCircular(from[key], seen.slice(0))
continue
}
to[key] = '[Circular]'
}
if (typeof from.name === 'string') {
to.name = from.name
}
if (typeof from.message === 'string') {
to.message = from.message
}
if (typeof from.stack === 'string') {
to.stack = from.stack
}
return to
}

8
src/helpers/firmware/getFinalFirmwareById.js

@ -1,8 +0,0 @@
// @flow
import network from 'api/network'
import { GET_FINAL_FIRMWARE } from 'helpers/urls'
export default async (id: number) => {
const { data } = await network({ method: 'GET', url: `${GET_FINAL_FIRMWARE}/${id}` })
return data
}

51
src/helpers/firmware/getFirmwareInfo.js

@ -1,51 +0,0 @@
// @flow
import type Transport from '@ledgerhq/hw-transport'
import type { FirmwareInfo } from 'helpers/types'
const APDUS = {
GET_FIRMWARE: [0xe0, 0x01, 0x00, 0x00],
// we dont have common call that works inside app & dashboard
// TODO: this should disappear.
GET_FIRMWARE_FALLBACK: [0xe0, 0xc4, 0x00, 0x00],
}
/**
* Retrieve targetId and firmware version from device
*/
export default async function getFirmwareInfo(transport: Transport<*>): Promise<FirmwareInfo> {
const res = await transport.send(...APDUS.GET_FIRMWARE)
const byteArray = [...res]
const data = byteArray.slice(0, byteArray.length - 2)
const targetIdStr = Buffer.from(data.slice(0, 4))
const targetId = targetIdStr.readUIntBE(0, 4)
const seVersionLength = data[4]
const seVersion = Buffer.from(data.slice(5, 5 + seVersionLength)).toString()
const flagsLength = data[5 + seVersionLength]
const flags = Buffer.from(
data.slice(5 + seVersionLength + 1, 5 + seVersionLength + 1 + flagsLength),
).toString()
const mcuVersionLength = data[5 + seVersionLength + 1 + flagsLength]
let mcuVersion = Buffer.from(
data.slice(
7 + seVersionLength + flagsLength,
7 + seVersionLength + flagsLength + mcuVersionLength,
),
)
if (mcuVersion[mcuVersion.length - 1] === 0) {
mcuVersion = mcuVersion.slice(0, mcuVersion.length - 1)
}
mcuVersion = mcuVersion.toString()
if (!seVersionLength) {
return {
targetId,
seVersion: '0.0.0',
flags: '',
mcuVersion: '',
}
}
return { targetId, seVersion, flags, mcuVersion }
}

13
src/helpers/firmware/getMcus.js

@ -1,13 +0,0 @@
// @flow
import network from 'api/network'
import { GET_MCUS } from 'helpers/urls'
export default async (): Promise<*> => {
const { data } = await network({
method: 'GET',
url: GET_MCUS,
})
return data
}

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

Loading…
Cancel
Save