Browse Source

Merge branch 'develop' into secure-update

develop
meriadec 6 years ago
parent
commit
789d179a21
No known key found for this signature in database GPG Key ID: 1D2FC2305E2CB399
  1. 38
      .eslintrc
  2. 1
      .flowconfig
  3. 2
      .github/ISSUE_TEMPLATE/feature_request.md
  4. 3
      .storybook/webpack.config.js
  5. 3
      README.md
  6. 21
      package.json
  7. 2
      scripts/postinstall.sh
  8. 11
      src/actions/general.js
  9. 91
      src/api/Ethereum.js
  10. 31
      src/api/Fees.js
  11. 6
      src/api/Ledger.js
  12. 47
      src/api/Ripple.js
  13. 2
      src/api/network.js
  14. 3
      src/bridge/BridgeSyncContext.js
  15. 8
      src/bridge/EthereumJSBridge.js
  16. 2
      src/bridge/LibcoreBridge.js
  17. 19
      src/bridge/RippleJSBridge.js
  18. BIN
      src/commands/.DS_Store
  19. 12
      src/commands/debugAppInfosForCurrency.js
  20. 17
      src/commands/firmwareMain.js
  21. 18
      src/commands/firmwarePrepare.js
  22. 17
      src/commands/firmwareRepair.js
  23. 25
      src/commands/getAddress.js
  24. 21
      src/commands/getCurrentFirmware.js
  25. 11
      src/commands/getDeviceInfo.js
  26. 16
      src/commands/getIsGenuine.js
  27. 13
      src/commands/getLatestFirmwareForDevice.js
  28. 19
      src/commands/getMemInfo.js
  29. 28
      src/commands/index.js
  30. 20
      src/commands/installApp.js
  31. 21
      src/commands/installFinalFirmware.js
  32. 19
      src/commands/installMcu.js
  33. 27
      src/commands/installOsuFirmware.js
  34. 19
      src/commands/isDashboardOpen.js
  35. 5
      src/commands/libcoreGetFees.js
  36. 49
      src/commands/libcoreSignAndBroadcast.js
  37. 15
      src/commands/listAppVersions.js
  38. 15
      src/commands/listApps.js
  39. 17
      src/commands/listCategories.js
  40. 15
      src/commands/shouldFlashMcu.js
  41. 10
      src/commands/signTransaction.js
  42. 15
      src/commands/testApdu.js
  43. 18
      src/commands/uninstallApp.js
  44. 1
      src/components/App.js
  45. 9
      src/components/CurrenciesStatusBanner.js
  46. 4
      src/components/CurrencyDownStatusAlert.js
  47. 2
      src/components/DashboardPage/AccountsOrder.js
  48. 2
      src/components/EnsureDeviceApp.js
  49. 14
      src/components/ExchangePage/index.js
  50. 67
      src/components/ExchangePage/logos/btcdirect.js
  51. 116
      src/components/ExchangePage/logos/coinberry.js
  52. 5
      src/components/FeesField/BitcoinKind.js
  53. 4
      src/components/FeesField/EthereumKind.js
  54. 7
      src/components/FeesField/RippleKind.js
  55. 31
      src/components/GenuineCheck.js
  56. 171
      src/components/HSMStatusBanner.js
  57. 10
      src/components/Idler.js
  58. 2
      src/components/IsUnlocked.js
  59. 2
      src/components/ManagerPage/AppSearchBar.js
  60. 116
      src/components/ManagerPage/AppsList.js
  61. 2
      src/components/ManagerPage/Dashboard.js
  62. 85
      src/components/ManagerPage/FirmwareUpdate.js
  63. 13
      src/components/ManagerPage/UpdateFirmwareButton.js
  64. 2
      src/components/ManagerPage/index.js
  65. 64
      src/components/ProgressBar/index.js
  66. 16
      src/components/ProgressBar/stories.js
  67. 98
      src/components/ProgressCircle/index.js
  68. 16
      src/components/ProgressCircle/stories.js
  69. 12
      src/components/SelectCurrency/index.js
  70. 2
      src/components/SettingsPage/DisablePasswordModal.js
  71. 2
      src/components/SettingsPage/PasswordForm.js
  72. 2
      src/components/SettingsPage/PasswordModal.js
  73. 102
      src/components/SettingsPage/RepairDeviceButton.js
  74. 94
      src/components/SettingsPage/SocketExport.js
  75. 14
      src/components/SettingsPage/index.js
  76. 12
      src/components/SettingsPage/sections/Export.js
  77. 7
      src/components/SettingsPage/sections/Help.js
  78. 4
      src/components/WithFeesAPI.js
  79. 4
      src/components/base/Modal/ConfirmModal.js
  80. 234
      src/components/base/Modal/RepairModal.js
  81. 1
      src/components/base/Modal/index.js
  82. 18
      src/components/base/Select/index.js
  83. 2
      src/components/layout/Default.js
  84. 2
      src/components/modals/AccountSettingRenderBody.js
  85. 19
      src/components/modals/Receive/steps/04-step-receive-funds.js
  86. 4
      src/components/modals/Send/fields/RecipientField.js
  87. 2
      src/components/modals/Send/index.js
  88. 34
      src/components/modals/UpdateFirmware/Disclaimer.js
  89. 10
      src/components/modals/UpdateFirmware/Installing.js
  90. 35
      src/components/modals/UpdateFirmware/index.js
  91. 115
      src/components/modals/UpdateFirmware/steps/01-step-install-full-firmware.js
  92. 117
      src/components/modals/UpdateFirmware/steps/02-step-flash-mcu.js
  93. 9
      src/config/constants.js
  94. 1
      src/config/cryptocurrencies.js
  95. 58
      src/config/errors.js
  96. 4
      src/config/urls.js
  97. 52
      src/helpers/apps/installApp.js
  98. 30
      src/helpers/apps/listAppVersions.js
  99. 10
      src/helpers/apps/listApps.js
  100. 10
      src/helpers/apps/listCategories.js

38
.eslintrc

@ -38,6 +38,7 @@
"jsx-a11y/label-has-for": 0,
"prefer-destructuring": 0,
"new-cap": 0,
"no-continue": 0,
"no-await-in-loop": 0,
"no-restricted-globals": 0,
"no-unused-expressions": 0,
@ -48,7 +49,10 @@
"no-shadow": 0,
"no-underscore-dangle": 0,
"no-console": 2,
"no-unused-vars": ["error", { "argsIgnorePattern": "^_", "vars": "all", "args": "after-used", "ignoreRestSiblings": true }],
"no-unused-vars": [
"error",
{ "argsIgnorePattern": "^_", "vars": "all", "args": "after-used", "ignoreRestSiblings": true }
],
"no-use-before-define": 0,
"no-restricted-syntax": 0,
"no-prototype-builtins": 0,
@ -57,31 +61,31 @@
"react/jsx-curly-brace-presence": 0,
"react/jsx-filename-extension": 0,
"react/jsx-no-target-blank": 0,
"react/jsx-no-literals": [1, {"noStrings": false}],
"react/jsx-no-literals": [1, { "noStrings": false }],
"react/prefer-stateless-function": 0,
"react/require-default-props": 0,
"react/no-multi-comp": 0,
"react/sort-comp": [1, {
order: [
'static-methods',
'lifecycle',
'everything-else',
'render'
]
}]
"react/sort-comp": [
1,
{
"order": ["static-methods", "lifecycle", "everything-else", "render"]
}
]
},
"settings": {
"import/resolver": {
"babel-module": {},
"babel-module": {}
},
"flowtype": {
"onlyFilesWithFlowAnnotation": true,
"onlyFilesWithFlowAnnotation": true
}
},
"overrides": [{
"files": [ "*stories.js" ],
"rules": {
"react/jsx-no-literals": 0
"overrides": [
{
"files": ["*stories.js"],
"rules": {
"react/jsx-no-literals": 0
}
}
}]
]
}

1
.flowconfig

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

2
.github/ISSUE_TEMPLATE/feature_request.md

@ -1,6 +1,6 @@
---
name: ✨ Feature Request
about: Any feature you find missing in Ledger Live? Discuss to suggest feature requests. For crypto asset support, please read Issue 1650.
about: Any feature you find missing in Ledger Live? Discuss to suggest feature requests. For crypto asset support, please read Issue #1560.
---
- [ ] I have checked this feature was not yet requested.

3
.storybook/webpack.config.js

@ -10,4 +10,7 @@ module.exports = {
},
],
},
node: {
fs: 'empty',
},
}

3
README.md

@ -1,5 +1,7 @@
# Ledger Live (desktop) [![CircleCI](https://circleci.com/gh/LedgerHQ/ledger-live-desktop.svg?style=svg)](https://circleci.com/gh/LedgerHQ/ledger-live-desktop) [![Crowdin](https://d322cqt584bo4o.cloudfront.net/ledger-wallet/localized.svg)](https://crowdin.com/project/ledger-wallet)
- Related: [ledger-live-mobile](https://github.com/ledgerhq/ledger-live-mobile)
> Ledger Live is a new generation wallet desktop application providing a unique interface to maintain multiple cryptocurrencies for your Ledger Nano S / Blue. Manage your device, create accounts, receive and send cryptoassets, [...and many more](https://www.ledger.fr/2018/07/09/ledger-launches-ledger-live-the-all-in-one-companion-app-to-your-ledger-device/).
<a href="https://github.com/LedgerHQ/ledger-live-desktop/releases">
@ -85,6 +87,7 @@ SKIP_GENUINE=1
SKIP_ONBOARDING=1
SHOW_LEGACY_NEW_ACCOUNT=1
HIGHLIGHT_I18N=1
EXPERIMENTAL_WS_EXPORT=0
## constants
GET_CALLS_TIMEOUT=30000

21
package.json

@ -3,7 +3,7 @@
"productName": "Ledger Live",
"description": "Ledger Live - Desktop",
"repository": "https://github.com/LedgerHQ/ledger-live-desktop",
"version": "1.2.7",
"version": "1.3.4",
"author": "Ledger",
"license": "MIT",
"scripts": {
@ -35,13 +35,13 @@
}
},
"dependencies": {
"@ledgerhq/hw-app-btc": "^v4.30.0",
"@ledgerhq/hw-app-eth": "^4.24.0",
"@ledgerhq/hw-app-xrp": "^4.25.0",
"@ledgerhq/hw-transport": "^4.24.0",
"@ledgerhq/hw-transport-node-hid": "4.24.0",
"@ledgerhq/ledger-core": "2.0.0-rc.12",
"@ledgerhq/live-common": "4.6.0",
"@ledgerhq/hw-app-btc": "^4.34.0",
"@ledgerhq/hw-app-eth": "^4.32.0",
"@ledgerhq/hw-app-xrp": "^4.32.0",
"@ledgerhq/hw-transport": "^4.32.0",
"@ledgerhq/hw-transport-node-hid": "^4.32.0",
"@ledgerhq/ledger-core": "2.0.0-rc.16",
"@ledgerhq/live-common": "4.15.0-beta.0",
"animated": "^0.2.2",
"async": "^2.6.1",
"axios": "^0.18.0",
@ -64,6 +64,7 @@
"i18next": "^11.2.2",
"i18next-node-fs-backend": "^1.0.0",
"invariant": "^2.2.4",
"ip": "^1.1.5",
"jsqr": "^1.1.1",
"lodash": "^4.17.5",
"lru-cache": "^4.1.3",
@ -112,7 +113,7 @@
"winston": "^3.0.0",
"winston-transport": "^4.2.0",
"write-file-atomic": "^2.3.0",
"ws": "^5.1.1",
"ws": "^6.1.3",
"zxcvbn": "^4.4.2"
},
"devDependencies": {
@ -186,7 +187,7 @@
"yaml-loader": "^0.5.0"
},
"engines": {
"node": ">=8.9.0 <=8.14.0",
"node": ">=8.9.0 <=8.15.0",
"yarn": "^1.10.1"
},
"private": true

2
scripts/postinstall.sh

@ -55,7 +55,7 @@ function installFlowTyped {
"Failed installing flow-typed definitions"
runJob \
"rm flow-typed/npm/{react-i18next_v7.x.x.js,styled-components_v3.x.x.js,redux_*,winston*}" \
"rm flow-typed/npm/{react-i18next_v7.x.x.js,styled-components_v3.x.x.js,redux_*,winston*,react-hot-loader_*}" \
"Removing broken flow-typed definitions" \
"Removed broken flow-typed definitions" \
"Failed removing broken flow-typed definitions"

11
src/actions/general.js

@ -33,11 +33,8 @@ const selectAccountsBalanceAndOrder = createStructuredSelector({
export const refreshAccountsOrdering = () => (dispatch: *, getState: *) => {
const all = selectAccountsBalanceAndOrder(getState())
const allRatesAvailable = all.accountsBtcBalance.every(b => !!b)
if (allRatesAvailable) {
dispatch({
type: 'DB:REORDER_ACCOUNTS',
payload: sortAccounts(all),
})
}
dispatch({
type: 'DB:REORDER_ACCOUNTS',
payload: sortAccounts(all),
})
}

91
src/api/Ethereum.js

@ -1,91 +0,0 @@
// @flow
import type { CryptoCurrency } from '@ledgerhq/live-common/lib/types'
import { BigNumber } from 'bignumber.js'
import { LedgerAPINotAvailable } from 'config/errors'
import network from './network'
import { blockchainBaseURL } from './Ledger'
export type Block = { height: number } // TODO more fields actually
export type Tx = {
hash: string,
received_at: string,
nonce: string,
value: number,
gas: number,
gas_price: number,
cumulative_gas_used: number,
gas_used: number,
from: string,
to: string,
input: string,
index: number,
block?: {
hash: string,
height: number,
time: string,
},
confirmations: number,
}
export type API = {
getTransactions: (
address: string,
blockHash: ?string,
) => Promise<{
truncated: boolean,
txs: Tx[],
}>,
getCurrentBlock: () => Promise<Block>,
getAccountNonce: (address: string) => Promise<number>,
broadcastTransaction: (signedTransaction: string) => Promise<string>,
getAccountBalance: (address: string) => Promise<BigNumber>,
}
export const apiForCurrency = (currency: CryptoCurrency): API => {
const baseURL = blockchainBaseURL(currency)
if (!baseURL) {
throw new LedgerAPINotAvailable(`LedgerAPINotAvailable ${currency.id}`, {
currencyName: currency.name,
})
}
return {
async getTransactions(address, blockHash) {
const { data } = await network({
method: 'GET',
url: `${baseURL}/addresses/${address}/transactions`,
params: { blockHash, noToken: 1 },
})
return data
},
async getCurrentBlock() {
const { data } = await network({
method: 'GET',
url: `${baseURL}/blocks/current`,
})
return data
},
async getAccountNonce(address) {
const { data } = await network({
method: 'GET',
url: `${baseURL}/addresses/${address}/nonce`,
})
return data[0].nonce
},
async broadcastTransaction(tx) {
const { data } = await network({
method: 'POST',
url: `${baseURL}/transactions/send`,
data: { tx },
})
return data.result
},
async getAccountBalance(address) {
const { data } = await network({
method: 'GET',
url: `${baseURL}/addresses/${address}/balance`,
})
// FIXME precision lost here. nothing we can do easily
return BigNumber(data[0].balance)
},
}
}

31
src/api/Fees.js

@ -1,31 +0,0 @@
// @flow
import invariant from 'invariant'
import LRU from 'lru-cache'
import type { Currency } from '@ledgerhq/live-common/lib/types'
import { FeeEstimationFailed } from 'config/errors'
import { blockchainBaseURL } from './Ledger'
import network from './network'
export type Fees = {
[_: string]: number,
}
const cache = LRU({
maxAge: 5 * 60 * 1000,
})
export const getEstimatedFees = async (currency: Currency): Promise<Fees> => {
const key = currency.id
let promise = cache.get(key)
if (promise) return promise.then(r => r.data)
const baseURL = blockchainBaseURL(currency)
invariant(baseURL, `Fees for ${currency.id} are not supported`)
promise = network({ method: 'GET', url: `${baseURL}/fees` })
cache.set(key, promise)
const { data, status } = await promise
if (status < 200 || status >= 300) cache.del(key)
if (data) {
return data
}
throw new FeeEstimationFailed(`FeeEstimationFailed ${status}`, { httpStatus: status })
}

6
src/api/Ledger.js

@ -1,6 +0,0 @@
// @flow
import type { Currency } from '@ledgerhq/live-common/lib/types'
import { LEDGER_REST_API_BASE } from 'config/constants'
export const blockchainBaseURL = ({ ledgerExplorerId }: Currency): ?string =>
ledgerExplorerId ? `${LEDGER_REST_API_BASE}/blockchain/v2/${ledgerExplorerId}` : null

47
src/api/Ripple.js

@ -1,47 +0,0 @@
// @flow
import logger from 'logger'
import { BigNumber } from 'bignumber.js'
import { RippleAPI } from 'ripple-lib'
import {
parseCurrencyUnit,
getCryptoCurrencyById,
formatCurrencyUnit,
} from '@ledgerhq/live-common/lib/currencies'
const rippleUnit = getCryptoCurrencyById('ripple').units[0]
export const defaultEndpoint = 'wss://s2.ripple.com'
export const apiForEndpointConfig = (endpointConfig: ?string = null) => {
const server = endpointConfig || defaultEndpoint
const api = new RippleAPI({ server })
api.on('error', (errorCode, errorMessage) => {
logger.warn(`Ripple API error: ${errorCode}: ${errorMessage}`)
})
return api
}
export const parseAPIValue = (value: string) => parseCurrencyUnit(rippleUnit, value)
export const parseAPICurrencyObject = ({
currency,
value,
}: {
currency: string,
value: string,
}) => {
if (currency !== 'XRP') {
logger.warn(`RippleJS: attempt to parse unknown currency ${currency}`)
return BigNumber(0)
}
return parseAPIValue(value)
}
export const formatAPICurrencyXRP = (amount: BigNumber) => {
const value = formatCurrencyUnit(rippleUnit, amount, {
showAllDigits: true,
disableRounding: true,
useGrouping: false,
})
return { currency: 'XRP', value }
}

2
src/api/network.js

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

3
src/bridge/BridgeSyncContext.js

@ -77,7 +77,8 @@ class Provider extends Component<BridgeSyncProviderOwnProps, Sync> {
return
}
if (currencyDownStatusLocal(this.props.currenciesStatus, account.currency)) {
const downStatus = currencyDownStatusLocal(this.props.currenciesStatus, account.currency)
if (downStatus && !downStatus.keepSync) {
next()
return
}

8
src/bridge/EthereumJSBridge.js

@ -13,6 +13,7 @@ import {
getDerivationScheme,
runDerivationScheme,
isIterableDerivationMode,
derivationModeSupportsIndex,
getMandatoryEmptyAccountSkip,
} from '@ledgerhq/live-common/lib/derivation'
import {
@ -21,11 +22,11 @@ import {
} from '@ledgerhq/live-common/lib/account'
import type { Account, Operation } from '@ledgerhq/live-common/lib/types'
import eip55 from 'eip55'
import { apiForCurrency } from 'api/Ethereum'
import type { Tx } from 'api/Ethereum'
import { apiForCurrency } from '@ledgerhq/live-common/lib/api/Ethereum'
import type { Tx } from '@ledgerhq/live-common/lib/api/Ethereum'
import getAddressCommand from 'commands/getAddress'
import signTransactionCommand from 'commands/signTransaction'
import { NotEnoughBalance, FeeNotLoaded, ETHAddressNonEIP } from 'config/errors'
import { NotEnoughBalance, FeeNotLoaded, ETHAddressNonEIP } from '@ledgerhq/errors'
import type { EditProps, WalletBridge } from './types'
type Transaction = {
@ -314,6 +315,7 @@ const EthereumBridge: WalletBridge<Transaction> = {
const derivationScheme = getDerivationScheme({ derivationMode, currency })
const stopAt = isIterableDerivationMode(derivationMode) ? 255 : 1
for (let index = 0; index < stopAt; index++) {
if (!derivationModeSupportsIndex(derivationMode, index)) continue
const freshAddressPath = runDerivationScheme(derivationScheme, currency, {
account: index,
})

2
src/bridge/LibcoreBridge.js

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

19
src/bridge/RippleJSBridge.js

@ -3,6 +3,7 @@ import invariant from 'invariant'
import { BigNumber } from 'bignumber.js'
import { Observable } from 'rxjs'
import React from 'react'
import { RippleAPI } from 'ripple-lib'
import bs58check from 'ripple-bs58check'
import { computeBinaryTransactionHash } from 'ripple-hashes'
import throttle from 'lodash/throttle'
@ -12,6 +13,7 @@ import {
getDerivationScheme,
runDerivationScheme,
isIterableDerivationMode,
derivationModeSupportsIndex,
} from '@ledgerhq/live-common/lib/derivation'
import {
getAccountPlaceholderName,
@ -25,14 +27,14 @@ import {
parseAPIValue,
parseAPICurrencyObject,
formatAPICurrencyXRP,
} from 'api/Ripple'
} from '@ledgerhq/live-common/lib/api/Ripple'
import FeesRippleKind from 'components/FeesField/RippleKind'
import AdvancedOptionsRippleKind from 'components/AdvancedOptions/RippleKind'
import {
NotEnoughBalance,
FeeNotLoaded,
NotEnoughBalanceBecauseDestinationNotCreated,
} from 'config/errors'
} from '@ledgerhq/errors'
import type { WalletBridge, EditProps } from './types'
type Transaction = {
@ -62,7 +64,7 @@ const EditAdvancedOptions = ({ onChange, value }: EditProps<Transaction>) => (
)
async function signAndBroadcast({ a, t, deviceId, isCancelled, onSigned, onOperationBroadcasted }) {
const api = apiForEndpointConfig(a.endpointConfig)
const api = apiForEndpointConfig(RippleAPI, a.endpointConfig)
const { fee } = t
if (!fee) throw new FeeNotLoaded()
try {
@ -251,7 +253,7 @@ const getServerInfo = (map => endpointConfig => {
if (!endpointConfig) endpointConfig = ''
if (map[endpointConfig]) return map[endpointConfig]()
const f = throttle(async () => {
const api = apiForEndpointConfig(endpointConfig)
const api = apiForEndpointConfig(RippleAPI, endpointConfig)
try {
await api.connect()
const res = await api.getServerInfo()
@ -269,7 +271,7 @@ const getServerInfo = (map => endpointConfig => {
const recipientIsNew = async (endpointConfig, recipient) => {
if (!isRecipientValid(recipient)) return false
const api = apiForEndpointConfig(endpointConfig)
const api = apiForEndpointConfig(RippleAPI, endpointConfig)
try {
await api.connect()
try {
@ -301,7 +303,7 @@ const RippleJSBridge: WalletBridge<Transaction> = {
}
async function main() {
const api = apiForEndpointConfig()
const api = apiForEndpointConfig(RippleAPI)
try {
await api.connect()
const serverInfo = await getServerInfo()
@ -314,6 +316,7 @@ const RippleJSBridge: WalletBridge<Transaction> = {
const derivationScheme = getDerivationScheme({ derivationMode, currency })
const stopAt = isIterableDerivationMode(derivationMode) ? 255 : 1
for (let index = 0; index < stopAt; index++) {
if (!derivationModeSupportsIndex(derivationMode, index)) continue
const freshAddressPath = runDerivationScheme(derivationScheme, currency, {
account: index,
})
@ -421,7 +424,7 @@ const RippleJSBridge: WalletBridge<Transaction> = {
}
async function main() {
const api = apiForEndpointConfig(endpointConfig)
const api = apiForEndpointConfig(RippleAPI, endpointConfig)
try {
await api.connect()
if (finished) return
@ -615,7 +618,7 @@ const RippleJSBridge: WalletBridge<Transaction> = {
getDefaultEndpointConfig: () => defaultEndpoint,
validateEndpointConfig: async endpointConfig => {
const api = apiForEndpointConfig(endpointConfig)
const api = apiForEndpointConfig(RippleAPI, endpointConfig)
await api.connect()
},
}

BIN
src/commands/.DS_Store

Binary file not shown.

12
src/commands/debugAppInfosForCurrency.js

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

17
src/commands/firmwareRepair.js

@ -0,0 +1,17 @@
// @flow
import repair from '@ledgerhq/live-common/lib/hw/firmwareUpdate-repair'
import { createCommand, Command } from 'helpers/ipc'
type Input = {
version: ?string,
}
type Result = { progress: number }
const cmd: Command<Input, Result> = createCommand(
'firmwareRepair',
({ version }) => repair('', version), // 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 { createCommand, Command } from 'helpers/ipc'
import { fromPromise } from 'rxjs/observable/fromPromise'
import { withDevice } from 'helpers/deviceAccess'
import getAddressForCurrency from 'helpers/getAddressForCurrency'
import { DeviceAppVerifyNotSupported, UserRefusedAddress } from 'config/errors'
import { from } from 'rxjs'
import { withDevice } from '@ledgerhq/live-common/lib/hw/deviceAccess'
import getAddress from '@ledgerhq/live-common/lib/hw/getAddress'
type Input = {
currencyId: string,
devicePath: string,
path: string,
verify?: boolean,
segwit?: boolean,
}
type Result = {
@ -25,20 +22,8 @@ type Result = {
const cmd: Command<Input, Result> = createCommand(
'getAddress',
({ currencyId, devicePath, path, ...options }) =>
fromPromise(
withDevice(devicePath)(transport =>
getAddressForCurrency(transport, getCryptoCurrencyById(currencyId), path, options),
).catch(e => {
if (e && e.name === 'TransportStatusError') {
if (e.statusCode === 0x6b00 && options.verify) {
throw new DeviceAppVerifyNotSupported()
}
if (e.statusCode === 0x6985) {
throw new UserRefusedAddress()
}
}
throw e
}),
withDevice(devicePath)(transport =>
from(getAddress(transport, getCryptoCurrencyById(currencyId), path, options.verify)),
),
)

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

16
src/commands/getIsGenuine.js

@ -1,11 +1,12 @@
// @flow
import { createCommand, Command } from 'helpers/ipc'
import { fromPromise } from 'rxjs/observable/fromPromise'
import type { DeviceInfo } from 'helpers/types'
import getIsGenuine from 'helpers/devices/getIsGenuine'
import { withDevice } from 'helpers/deviceAccess'
import { of } from 'rxjs'
import { delay } from 'rxjs/operators'
import genuineCheck from '@ledgerhq/live-common/lib/hw/genuineCheck'
import { withDevice } from '@ledgerhq/live-common/lib/hw/deviceAccess'
import type { DeviceInfo } from '@ledgerhq/live-common/lib/types/manager'
import { SKIP_GENUINE } from 'config/constants'
type Input = {
devicePath: string,
@ -14,7 +15,10 @@ type Input = {
type Result = string
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

13
src/commands/getLatestFirmwareForDevice.js

@ -1,15 +1,14 @@
// @flow
import { createCommand, Command } from 'helpers/ipc'
import { fromPromise } from 'rxjs/observable/fromPromise'
import type { DeviceInfo, OsuFirmware } from 'helpers/types'
import { from } from 'rxjs'
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', data =>
fromPromise(getLatestFirmwareForDevice(data)),
const cmd: Command<DeviceInfo, Result> = createCommand('getLatestFirmwareForDevice', deviceInfo =>
from(manager.getLatestFirmwareForDevice(deviceInfo)),
)
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

28
src/commands/index.js

@ -5,17 +5,14 @@ import type { Command } from 'helpers/ipc'
import debugAppInfosForCurrency from 'commands/debugAppInfosForCurrency'
import autoUpdate from 'commands/autoUpdate'
import firmwarePrepare from 'commands/firmwarePrepare'
import firmwareMain from 'commands/firmwareMain'
import firmwareRepair from 'commands/firmwareRepair'
import getAddress from 'commands/getAddress'
import getDeviceInfo from 'commands/getDeviceInfo'
import getCurrentFirmware from 'commands/getCurrentFirmware'
import getIsGenuine from 'commands/getIsGenuine'
import getLatestFirmwareForDevice from 'commands/getLatestFirmwareForDevice'
import getMemInfo from 'commands/getMemInfo'
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 libcoreGetFees from 'commands/libcoreGetFees'
import libcoreGetVersion from 'commands/libcoreGetVersion'
@ -24,13 +21,9 @@ import libcoreScanFromXPUB from 'commands/libcoreScanFromXPUB'
import libcoreSignAndBroadcast from 'commands/libcoreSignAndBroadcast'
import libcoreSyncAccount from 'commands/libcoreSyncAccount'
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 ping from 'commands/ping'
import quitAndInstallElectronUpdate from 'commands/quitAndInstallElectronUpdate'
import shouldFlashMcu from 'commands/shouldFlashMcu'
import signTransaction from 'commands/signTransaction'
import testApdu from 'commands/testApdu'
import testCrash from 'commands/testCrash'
@ -38,19 +31,16 @@ import testInterval from 'commands/testInterval'
import uninstallApp from 'commands/uninstallApp'
const all: Array<Command<any, any>> = [
debugAppInfosForCurrency,
autoUpdate,
debugAppInfosForCurrency,
firmwarePrepare,
firmwareMain,
firmwareRepair,
getAddress,
getDeviceInfo,
getCurrentFirmware,
getIsGenuine,
getLatestFirmwareForDevice,
getMemInfo,
installApp,
installFinalFirmware,
installMcu,
installOsuFirmware,
isDashboardOpen,
killInternalProcess,
libcoreGetFees,
libcoreGetVersion,
@ -59,13 +49,9 @@ const all: Array<Command<any, any>> = [
libcoreSignAndBroadcast,
libcoreSyncAccount,
libcoreValidAddress,
listApps,
listAppVersions,
listCategories,
listenDevices,
ping,
quitAndInstallElectronUpdate,
shouldFlashMcu,
signTransaction,
testApdu,
testCrash,

20
src/commands/installApp.js

@ -1,25 +1,19 @@
// @flow
import { createCommand, Command } from 'helpers/ipc'
import { fromPromise } from 'rxjs/observable/fromPromise'
import { withDevice } from 'helpers/deviceAccess'
import installApp from 'helpers/apps/installApp'
import type { ApplicationVersion } from 'helpers/types'
import installApp from '@ledgerhq/live-common/lib/hw/installApp'
import { withDevice } from '@ledgerhq/live-common/lib/hw/deviceAccess'
import type { ApplicationVersion } from '@ledgerhq/live-common/lib/types/manager'
type Input = {
app: ApplicationVersion,
devicePath: string,
targetId: string | number,
app: ApplicationVersion,
}
type Result = void
type Result = { progress: number }
const cmd: Command<Input, Result> = createCommand(
'installApp',
({ devicePath, targetId, ...app }) =>
fromPromise(withDevice(devicePath)(transport => installApp(transport, targetId, app))),
const cmd: Command<Input, Result> = createCommand('installApp', ({ devicePath, targetId, app }) =>
withDevice(devicePath)(transport => installApp(transport, targetId, app)),
)
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,
getOrCreateWallet,
} from 'helpers/libcore'
import { InvalidAddress } from 'config/errors'
import { InvalidAddress } from '@ledgerhq/errors'
type BitcoinLikeTransaction = {
// TODO we rename this Transaction concept into transactionInput
@ -75,7 +75,8 @@ const cmd: Command<Input, Result> = createCommand(
njsWalletCurrency,
BigNumber(transaction.feePerByte),
)
const transactionBuilder = bitcoinLikeAccount.buildTransaction()
const isPartial = true
const transactionBuilder = bitcoinLikeAccount.buildTransaction(isPartial)
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
throw new InvalidAddress()

49
src/commands/libcoreSignAndBroadcast.js

@ -4,7 +4,7 @@ import logger from 'logger'
import { BigNumber } from 'bignumber.js'
import { StatusCodes } from '@ledgerhq/hw-transport'
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 { getCryptoCurrencyById } from '@ledgerhq/live-common/lib/currencies'
import type { OperationRaw, DerivationMode, CryptoCurrency } from '@ledgerhq/live-common/lib/types'
@ -14,11 +14,11 @@ import {
bigNumberToLibcoreAmount,
getOrCreateWallet,
} from 'helpers/libcore'
import { UpdateYourApp } from 'config/errors'
import { UpdateYourApp } from '@ledgerhq/errors'
import withLibcore from 'helpers/withLibcore'
import { createCommand, Command } from 'helpers/ipc'
import { withDevice } from 'helpers/deviceAccess'
import { withDevice } from '@ledgerhq/live-common/lib/hw/deviceAccess'
type BitcoinLikeTransaction = {
amount: string,
@ -110,7 +110,7 @@ async function signTransaction({
const additionals = []
let expiryHeight
if (currency.id === 'bitcoin_cash' || currency.id === 'bitcoin_gold') additionals.push('bip143')
if (currency.id === 'zcash') {
if (currency.id === 'zcash' || currency.id === 'komodo') {
expiryHeight = Buffer.from([0x00, 0x00, 0x00, 0x00])
if (blockHeight >= 419200) {
additionals.push('sapling')
@ -121,7 +121,7 @@ async function signTransaction({
}
const rawInputs = transaction.getInputs()
const hasExtraData = currency.id === 'zcash'
const hasExtraData = currency.id === 'zcash' || currency.id === 'komodo'
const inputs = await Promise.all(
rawInputs.map(async input => {
@ -227,7 +227,8 @@ export async function doSignAndBroadcast({
const njsWalletCurrency = njsWallet.getCurrency()
const amount = bigNumberToLibcoreAmount(core, njsWalletCurrency, BigNumber(transaction.amount))
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
@ -245,22 +246,26 @@ export async function doSignAndBroadcast({
const hasTimestamp = !!njsWalletCurrency.bitcoinLikeNetworkParameters.UsesTimestampedTransaction
// TODO: const timestampDelay = njsWalletCurrency.bitcoinLikeNetworkParameters.TimestampDelay
const signedTransaction = await withDevice(deviceId)(async transport =>
signTransaction({
hwApp: new Btc(transport),
currency,
blockHeight,
transaction: builded,
sigHashType: parseInt(sigHashType, 16),
hasTimestamp,
derivationMode,
}),
).catch(e => {
if (e && e.statusCode === StatusCodes.INCORRECT_P1_P2) {
throw new UpdateYourApp(`UpdateYourApp ${currency.id}`, currency)
}
throw e
})
const signedTransaction = await withDevice(deviceId)(transport =>
from(
signTransaction({
hwApp: new Btc(transport),
currency,
blockHeight,
transaction: builded,
sigHashType: parseInt(sigHashType, 16),
hasTimestamp,
derivationMode,
}),
),
)
.toPromise()
.catch(e => {
if (e && e.statusCode === StatusCodes.INCORRECT_P1_P2) {
throw new UpdateYourApp(`UpdateYourApp ${currency.id}`, currency)
}
throw e
})
if (!signedTransaction || isCancelled() || !njsAccount) return
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
import { createCommand, Command } from 'helpers/ipc'
import { fromPromise } from 'rxjs/observable/fromPromise'
import { withDevice } from 'helpers/deviceAccess'
import { from } from 'rxjs'
import { withDevice } from '@ledgerhq/live-common/lib/hw/deviceAccess'
import signTransactionForCurrency from 'helpers/signTransactionForCurrency'
type Input = {
@ -17,10 +17,8 @@ type Result = string
const cmd: Command<Input, Result> = createCommand(
'signTransaction',
({ currencyId, devicePath, path, transaction }) =>
fromPromise(
withDevice(devicePath)(transport =>
signTransactionForCurrency(currencyId)(transport, currencyId, path, transaction),
),
withDevice(devicePath)(transport =>
from(signTransactionForCurrency(currencyId)(transport, currencyId, path, transaction)),
),
)

15
src/commands/testApdu.js

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

18
src/commands/uninstallApp.js

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

1
src/components/App.js

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

9
src/components/CurrenciesStatusBanner.js

@ -116,7 +116,11 @@ class BannerItem extends PureComponent<{
render() {
const { item, t } = this.props
return (
<Box relative key={item.id} style={styles.banner}>
<Box
relative
key={item.id}
style={{ ...styles.banner, ...(item.warning ? styles.warning : null) }}
>
<CloseIcon onClick={this.dismiss} />
<Box horizontal flow={2}>
<IconTriangleWarning height={16} width={16} color="white" />
@ -159,6 +163,9 @@ const styles = {
left: 32,
bottom: 32,
},
warning: {
background: colors.orange,
},
banner: {
background: colors.alertRed,
overflow: 'hidden',

4
src/components/CurrencyDownStatusAlert.js

@ -28,7 +28,7 @@ const CurrencyDownBox = styled(Box).attrs({
py: 2,
mb: 4,
})`
background-color: ${p => p.theme.colors.alertRed};
background-color: ${p => (p.warning ? p.theme.colors.orange : p.theme.colors.alertRed)};
`
const Link = styled.span`
@ -48,7 +48,7 @@ class CurrencyDownStatusAlert extends PureComponent<Props> {
const { status, t } = this.props
if (!status) return null
return (
<CurrencyDownBox>
<CurrencyDownBox warning={!!status.warning}>
<Box mr={2}>
<IconTriangleWarning height={16} width={16} />
</Box>

2
src/components/DashboardPage/AccountsOrder.js

@ -118,7 +118,7 @@ class AccountsOrder extends Component<Props> {
<BoldToggle isBold={isActive}>{item.label}</BoldToggle>
</Box>
<OrderIcon isActive={isActive}>
{order === 'desc' ? <IconArrowUp size={14} /> : <IconArrowDown size={14} />}
{order === 'asc' ? <IconArrowUp size={14} /> : <IconArrowDown size={14} />}
</OrderIcon>
</DropDownItem>
)

2
src/components/EnsureDeviceApp.js

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

14
src/components/ExchangePage/index.js

@ -17,6 +17,8 @@ import ChangellyLogo from './logos/changelly'
import CoinmamaLogo from './logos/bigmama'
import SimplexLogo from './logos/simplex'
import PaybisLogo from './logos/paybis'
import Coinberry from './logos/coinberry'
import BtcDirect from './logos/btcdirect'
type Props = {
t: T,
@ -89,6 +91,18 @@ const cards = shuffle([
url: urls.thorSwap,
logo: <img src={i('logos/exchanges/thor-swap.png')} alt="ThorSwap" width={150} />,
},
{
key: 'coinberry',
id: 'coinberry',
url: urls.coinberry,
logo: <Coinberry width={150} />,
},
{
key: 'btcDirect',
id: 'btcDirect',
url: urls.btcDirect,
logo: <BtcDirect width={150} />,
},
])
class ExchangePage extends PureComponent<Props> {

67
src/components/ExchangePage/logos/btcdirect.js

@ -0,0 +1,67 @@
// @flow
import React from 'react'
const styles = {
fill: '#0086fb',
}
const inner = (
<>
<path
style={styles}
d="M528.34,146.17,561.36,24H465.6l-19.81,76c-17.61-6.61-36.32-11-55-14.31L407.27,24H311.51L296.1,83.44C213.56,94.44,137.61,137.37,87,204.51-18.68,342.09,7.73,540.21,145.32,645.87L112.3,768H207l19.81-75.94c17.61,6.6,36.33,11,55,14.31L265.29,768h94.65l16.51-59.43C459,697.6,535,654.68,585.58,587.54,692.34,448.85,665.92,251.84,528.34,146.17ZM336.83,469.77a74.85,74.85,0,1,1,74.84-74.85C411.67,436.75,378.65,469.77,336.83,469.77Z"
/>
<path
style={styles}
d="M590,713c0-9.91-6.61-15.41-17.61-15.41H553.66v42.93h11V728.42h8.81l6.6,12.11h12.11l-9.91-13.21C587.78,725.12,591.08,719.61,590,713Zm-9.91,0c0,4.4-2.2,7.7-6.6,7.7h-8.81v-14.3h7.71c3.3-1.1,6.6,2.2,7.7,5.5Z"
/>
<path
style={styles}
d="M571.27,675.59a46.23,46.23,0,1,0,46.22,46.23C616.39,696.5,596.58,675.59,571.27,675.59Zm0,81.45a35.23,35.23,0,1,1,35.22-35.22A34.77,34.77,0,0,1,571.27,757Z"
/>
<path
style={styles}
d="M1072.06,392.72c35.22-7.7,58.34-34.12,58.34-75.94,0-50.63-37.42-83.65-95.76-83.65H888.26V558.92H1039c59.44,0,96.86-35.22,96.86-90.25C1135.9,425.74,1109.49,397.12,1072.06,392.72Zm-95.75-96.86h35.22A32,32,0,0,1,1041.25,330v4.41c-1.1,17.61-16.51,30.81-35.22,29.71H976.31ZM1044.55,461c-1.1,17.61-17.61,31.92-36.32,29.72H977.41V420.24h36.32a33.24,33.24,0,0,1,30.82,35.22Z"
/>
<polygon
style={styles}
points="1152.41 315.68 1230.56 316.77 1230.56 558.92 1320.81 558.92 1320.81 316.77 1397.86 316.77 1397.86 233.13 1152.41 233.13 1152.41 315.68"
/>
<path
style={styles}
d="M1575.47,310c36.7,0,59.37,19.61,65.85,49h92.85c-6.48-85-66.94-134-158.7-134-97.16,0-164.1,70.8-164.1,171,0,99.12,65.86,171,164.1,171,91.76,0,151.14-49,158.7-134h-92.85c-5.4,29.41-29.15,49-65.85,49-45.34,0-72.33-32.67-72.33-86.05S1530.13,310,1575.47,310Z"
/>
<path
style={styles}
d="M1871.75,233.13h-90.26V558.92h90.26c100.16,0,154.09-79.25,154.09-162.9S1971.91,233.13,1871.75,233.13Zm-2.2,296.07h-59.44V261.74h59.44c83.65,0,126.57,66,126.57,134.28C1996.12,463.16,1953.2,529.2,1869.55,529.2Z"
/>
<path
style={styles}
d="M2087.85,248.54c-14.31,1.1-25.32,13.2-24.22,27.51v3.3c1.1,14.31,13.21,25.32,27.52,24.22s25.31-13.21,24.21-27.52v-3.3C2114.26,258.44,2102.16,247.43,2087.85,248.54Z"
/>
<rect style={styles} x="2075.19" y="346.49" width="28.62" height="212.43" />
<path
style={styles}
d="M2199.74,402.63V346.49h-27.51V557.82h27.51V466.46c0-46.22,17.61-95.75,62.74-95.75,6.6,0,12.11,0,18.71,1.1V344.29a96.38,96.38,0,0,0-17.61-2.2C2235,342.09,2208.55,360.8,2199.74,402.63Z"
/>
<path
style={styles}
d="M2403.36,341c-57.23,0-95.75,42.93-95.75,111.17s37.42,112.26,96.85,112.26c44,0,79.25-27.51,86.95-66H2463.9c-8.81,24.22-33,40.73-59.44,38.53-40.72,0-68.24-29.72-68.24-77h158.5c0-6.6,1.1-13.21,0-19.81C2494.72,382.81,2458.39,341,2403.36,341Zm-67.14,94.66c2.2-41.83,27.52-70.45,67.14-70.45s63.84,28.62,64.94,70.45Z"
/>
<path
style={styles}
d="M2627.89,369.61c35.23,0,57.24,16.51,62.74,49.53h27.52c-5.51-47.33-39.63-77.05-90.26-77.05-60.53,0-97.95,44-97.95,110.07s37.42,111.16,97.95,112.26c51.74,0,85.86-29.71,90.26-75.94h-27.52c-5.5,31.92-28.61,48.43-62.74,48.43-42.92,0-69.34-33-69.34-83.65S2585,369.61,2627.89,369.61Z"
/>
<path
style={styles}
d="M2843.62,534.71c-18.71,0-34.12-6.61-34.12-50.64V372.91h53.93V346.49H2809.5V276.05H2782v70.44h-37.43v26.42H2782V489.58c0,66,31.91,71.54,56.13,71.54,11,1.1,22-1.1,31.92-3.3V530.3C2861.23,532.5,2852.43,533.6,2843.62,534.71Z"
/>
</>
)
export default ({ width }: { width: number }) => (
<svg width={width} viewBox="0 0 2892.16 792.05">
{inner}
</svg>
)

116
src/components/ExchangePage/logos/coinberry.js

@ -0,0 +1,116 @@
// @flow
import React from 'react'
const styles = {
blue: {
fill: '#334F93',
},
pink: {
fill: '#EC2D6E',
},
}
const inner = (
<>
<path
style={styles.blue}
d="M146.3,101.8c5.2,0,9.4-4,9.4-9.4c0-5.2-4.1-9.5-9.4-9.5c-5.5,0-9.5,4.4-9.5,9.5
C136.8,97.7,140.7,101.8,146.3,101.8"
/>
<path
style={styles.blue}
d="M102.3,164.7c8.4,0,9.4-10.3,9.4-25.7c0-18.7-2-25.8-9.5-25.8c-8.9,0-9.6,12.1-9.6,25.8
C92.6,152.1,92.6,164.7,102.3,164.7 M102.2,109.4c15.9,0,27.3,12.7,27.3,29.6c0,16.1-10.8,29.5-27.3,29.5
c-16.9,0-27.4-13.7-27.4-29.5C74.8,123.4,85.4,109.4,102.2,109.4"
/>
<path
style={styles.blue}
d="M246.2,155c0,3.4,1.6,9.6,8.3,9.6c7.3,0,11.5-6.1,11.5-25.2c0-7.1-0.8-23.3-11.3-23.3c-5.6,0-7.4,4.5-8.5,7.1
V155z M222.6,84.9h23.6v31.4c2-2.2,6.5-6.8,14.7-6.8c14,0,22.8,12.3,22.8,28.1c0,15.4-8.3,30.9-28.3,30.9c-2.8,0-6.8-0.9-11-3.5
c-2.8-1.8-6.3-1.8-9,0.1l-4.7,3.3h-1.4V94.8c0-5.7-2.3-6.1-6.7-7V84.9z"
/>
<path
style={styles.blue}
d="M77.8,137.3H76c-0.2,6.7-2.4,12.8-5.9,16.9c-0.6,0.8-2.6,2.6-3.5,3.4c-2,1.7-4.8,2.8-8.8,2.8
c-9.5,0-16.9-9.8-16.9-26.8c0-5.5,0.9-20.3,10.3-20.3c1.8,0,4.9,1.1,4.9,4.7c0,6.6,0,12.5,6.8,12.5c1,0,8.3,0,8.3-7.7
c0-8.7-10.2-13.2-18.8-13.2c-11.4,0-28.6,7.4-28.6,31.3c0,15,10.4,27.8,25.9,27.8c7.6,0,14.3-3,19.5-8.3
C70.2,159.2,77.8,152.3,77.8,137.3"
/>
<path
style={styles.blue}
d="M370.1,166.7h-32.3v-2.9c3.9-0.8,6.6-1.2,6.6-7.3V121c0-6-2.9-6.7-6.6-7.1v-3h22.9l0.1,4.4c0,1.6,2,2.4,3.2,1.1
c3-3.4,7.3-7,12.8-7c8.3,0,10.1,6.6,10.1,9.5c0,3.1-1.7,8.1-7.7,8.1c-7,0-6.9-7.1-10.8-7.1c-1.8,0-7.1,2.7-7.1,11.5v22.9
c0,7.4,1.5,9.3,8.8,9.6v2.9H370.1z"
/>
<path
style={styles.blue}
d="M218.1,157v-30.4c0-13.3-9.6-17.1-16.2-17.1c-6.6,0-11.9,4.2-14.8,7.2c-1,1-2.6,0.3-2.6-1.1v-4.6h-23.1v2.9
c4.8,0.8,6.5,1.7,6.5,7.1v35.6v0.3v1.8c0,2.8-2.3,5.1-5.1,5.1H160c-2.8,0-5.1-2.3-5.1-5.1v-24.9v-22.9h-23.2v2.9
c4.6,0.9,6.5,1.7,6.5,7.1v35.6c0,5.8-2,6.1-6.5,7.3v2.9h27.4h3.6h28v-2.9c-5.6-0.6-5.9-4.1-5.9-6.9v-32.4c0-1.1,5.2-7,10.1-7
c6.6,0,6.7,6.7,6.7,10.1v29.4c0,4.5-1.7,6.6-5.7,6.9v2.9h28.5V164C218.1,163.6,218.1,159.9,218.1,157"
/>
<path
style={styles.blue}
d="M475.2,110.9v2.9c3,0.1,6.7,0.1,6.7,4c0,1.8-0.6,3.4-1.3,5.5l-8.3,23.2l-8.7-22.4c-1.1-2.7-2.5-5.8-2.5-7
c0-2.9,3.2-3,6.5-3.2v-3H446c-3,0-5,3.1-3.8,5.8l7.2,15.9l14.5,35.7l-2.2,6.5c-0.9,2.7-3.9,9.9-8,9.9c-1.5,0-2-1.5-2-2
c0-0.5,0.3-1.1,0.3-2.2c0-2.2-1.3-6.1-6.8-6.1c-7.3,0-8,6.1-8,7.9c0,3.9,3.2,9.5,11.2,9.5c11.4,0,14-6.9,20-23l18.1-48.1
c2.3-6,4.4-6.3,7-6.7v-3H475.2z"
/>
<path
style={styles.blue}
d="M420.9,166.7h-32.3v-2.9c3.9-0.8,6.6-1.2,6.6-7.3V121c0-6-2.9-6.7-6.6-7.1v-3h22.9l0.1,4.4c0,1.6,2,2.4,3.2,1.1
c3-3.4,7.3-7,12.8-7c8.3,0,10.1,6.6,10.1,9.5c0,3.1-1.7,8.1-7.7,8.1c-7,0-6.9-7.1-10.8-7.1c-1.8,0-7.1,2.7-7.1,11.5v22.9
c0,7.4,1.5,9.3,8.8,9.6v2.9H420.9z"
/>
<path
style={styles.blue}
d="M487.5,159.5h0.9c1,0,1.4-0.8,1.4-1.8c0-1.2-0.6-1.5-1.4-1.5h-0.9V159.5z M485.2,155.8h3.5
c1.2,0,2.4,0.5,2.4,1.8c0,1.1-0.6,1.8-1.6,2.2l1.2,1.6c0.5,0.8,1.2,1.4,1.5,1.7v0.2h-1.3c-0.6,0-1.2-1.4-2.5-3.2h-1v2
c0,0.9,0,0.9,1,1v0.3h-3.2v-0.3c1-0.1,1-0.1,1-1v-4.8c0-0.9,0-0.9-1-1V155.8z M488.6,153.4c-3.3,0-5.8,2.9-5.8,6.1s2.6,6.1,5.8,6.1
c3.2,0,5.8-2.9,5.8-6.1C494.3,156.3,491.8,153.4,488.6,153.4 M488.6,166.5c-3.9,0-7-3.1-7-7c0-3.9,3-7,7-7c3.9,0,6.9,3.1,6.9,7
C495.5,163.4,492.4,166.5,488.6,166.5"
/>
<path
style={styles.blue}
d="M321.8,132.6c-0.1-3.8-0.1-10.1-1.1-13.7c-0.8-2.7-2.3-5.7-6.6-5.7c-4.5,0-8.6,2.7-8.6,19.4H321.8z
M337.3,153.3c-7.4,11.3-14.3,15.2-24,15.2c-12.3,0-24.6-8.9-24.6-29.7c0-18,11.1-29.4,25-29.4c21.1,0,22.7,21.1,23,27.7h-30.6
c0.3,10.3,3,22.7,14.5,22.7c7.1,0,11.4-5.4,13.7-8.1L337.3,153.3z"
/>
<path
style={styles.blue}
d="M579,43c-7.2,0-9.7,6.1-18.8,3.3c-0.8-0.3-1.5,0.7-0.8,1.3c5.5,4.6,14.1,11.2,24,10.6
C591.3,57.6,590.7,43,579,43"
/>
<path
style={styles.pink}
d="M588.3,63.8c-0.5,2.5-1.3,5.5-2,7.4c-0.2,0.4-0.7,0.6-1.1,0.4c-18.2-8.8-38.9,11.7-60.7,5.1
c-17.7-5.3-18.7-40,3.9-40.5c18.3-0.4,32.8,30.4,59,26.6C587.9,62.7,588.4,63,588.3,63.8"
/>
<path
style={styles.pink}
d="M533,94.4c9.5,1.2,22.8,8.9,30.8,9.3c0.7,0,0.9,0.9,0.4,1.4c-4.7,3.9-9.5,6.3-13.8,6.3c-6.8,0-15-6.1-22.3-15.2
C526.9,94.5,531,94.2,533,94.4"
/>
<path
style={styles.pink}
d="M525.2,90.4c5.4-3.4,16.5,0.4,27.6,4.9c10.6,4.3,16.1,4.5,20.3,0.3c4.8-4.8,14.5-18.8,7.5-20.6
c-16.1-4.1-38.2,15.1-60.6,5.3c-0.7-0.3-1.3,0.3-1,1c0.4,1.1,2.8,6.1,4.9,8.9C524.3,90.6,524.6,90.8,525.2,90.4"
/>
<path
style={styles.pink}
d="M541.8,15.1c6,0,6.8,8.1,13.1,15.4l0,0c0.5,0.7-0.1,1.3-0.5,1.3C534.4,31,533.4,15.1,541.8,15.1"
/>
<path
style={styles.pink}
d="M560.4,28.9c-1.1-0.7-2.3-1.8-3.2-3.2c-0.2-0.2-0.2-0.5,0-0.8c1.8-2.9,3.4-6.7,5.8-8.3
c3.7-2.8,10.6,0.6,7.8,6.2c-1.1,2.2-3.6,4.5-6.2,6.1C562.8,30,561.2,29.5,560.4,28.9"
/>
</>
)
export default ({ width }: { width: number }) => (
<svg width={width} viewBox="0 0 612 205.1">
{inner}
</svg>
)

5
src/components/FeesField/BitcoinKind.js

@ -8,10 +8,10 @@ import { translate } from 'react-i18next'
import type { T } from 'types/common'
import { FeeNotLoaded } from 'config/errors'
import { FeeNotLoaded } from '@ledgerhq/errors'
import InputCurrency from 'components/base/InputCurrency'
import Select from 'components/base/Select'
import type { Fees } from 'api/Fees'
import type { Fees } from '@ledgerhq/live-common/lib/api/Fees'
import WithFeesAPI from '../WithFeesAPI'
import GenericContainer from './GenericContainer'
import Box from '../base/Box'
@ -120,6 +120,7 @@ class FeesField extends Component<OwnProps, State> {
if (selectedItem.feePerByte.isZero() && input.current) {
patch.isFocused = true
input.current.select()
onChange(selectedItem.feePerByte)
}
}
this.setState(patch)

4
src/components/FeesField/EthereumKind.js

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

7
src/components/FeesField/RippleKind.js

@ -1,10 +1,11 @@
// @flow
import React, { Component } from 'react'
import { RippleAPI } from 'ripple-lib'
import type { BigNumber } from 'bignumber.js'
import type { Account } from '@ledgerhq/live-common/lib/types'
import { apiForEndpointConfig, parseAPIValue } from 'api/Ripple'
import { FeeNotLoaded } from 'config/errors'
import { apiForEndpointConfig, parseAPIValue } from '@ledgerhq/live-common/lib/api/Ripple'
import { FeeNotLoaded } from '@ledgerhq/errors'
import InputCurrency from 'components/base/InputCurrency'
import GenericContainer from './GenericContainer'
@ -30,7 +31,7 @@ class FeesField extends Component<Props, State> {
}
syncId = 0
async sync() {
const api = apiForEndpointConfig(this.props.account.endpointConfig)
const api = apiForEndpointConfig(RippleAPI, this.props.account.endpointConfig)
const syncId = ++this.syncId
try {
await api.connect()

31
src/components/GenuineCheck.js

@ -9,12 +9,18 @@ import { delay, createCancelablePolling } from 'helpers/promise'
import logger from 'logger'
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 { getCurrentDevice } from 'reducers/devices'
import { CantOpenDevice, DeviceNotGenuineError, DeviceGenuineSocketEarlyClose } from 'config/errors'
import {
CantOpenDevice,
DeviceNotGenuineError,
DeviceGenuineSocketEarlyClose,
UnexpectedBootloader,
} from '@ledgerhq/errors'
import getDeviceInfo from 'commands/getDeviceInfo'
import getIsGenuine from 'commands/getIsGenuine'
@ -77,11 +83,26 @@ class GenuineCheck extends PureComponent<Props> {
device: Device,
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')
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)) {
logger.log("genuine was already checked. don't check again")
await delay(GENUINE_CACHE_DELAY)
@ -135,10 +156,10 @@ class GenuineCheck extends PureComponent<Props> {
{
id: 'deviceInfo',
title: (
<Trans i18nKey="deviceConnect.step2" parent="div">
<Trans i18nKey="deviceConnect.dashboard" parent="div">
{'Navigate to the '}
<Bold>{'dashboard'}</Bold>
{' app on your device'}
{' on your device'}
</Trans>
),
icon: homeIcon,

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)

10
src/components/Idler.js

@ -40,7 +40,11 @@ class Idler extends PureComponent<Props> {
lastAction: number = -1
debounceOnChange = debounce(_ => this.idleTimeHandler(), 1000)
idleTimeHandler = () => {
this.lastAction = Date.now()
}
debounceOnChange = debounce(this.idleTimeHandler, 1000, { maxWait: 1000, leading: true })
checkForAutoLock = _ => {
const timeout = this.props.autoLockTimeout
@ -51,10 +55,6 @@ class Idler extends PureComponent<Props> {
}
}
idleTimeHandler = _ => {
this.lastAction = Date.now()
}
render() {
return null
}

2
src/components/IsUnlocked.js

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

2
src/components/ManagerPage/AppSearchBar.js

@ -3,7 +3,7 @@
import React, { PureComponent, Fragment } from 'react'
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 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 { connect } from 'react-redux'
import { compose } from 'redux'
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 { developerModeSelector } from 'reducers/settings'
import listApps from 'commands/listApps'
import listAppVersions from 'commands/listAppVersions'
import installApp from 'commands/installApp'
import uninstallApp from 'commands/uninstallApp'
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 Tooltip from 'components/base/Tooltip'
import Text from 'components/base/Text'
import Progress from 'components/base/Progress'
import ProgressBar from 'components/ProgressBar'
import Spinner from 'components/base/Spinner'
import Button from 'components/base/Button'
import TranslatedError from 'components/TranslatedError'
@ -65,6 +65,7 @@ type State = {
appsLoaded: boolean,
app: string,
mode: Mode,
progress: number,
}
const oldAppsInstallDisabled = ['ZenCash', 'Ripple']
@ -86,6 +87,7 @@ class AppsList extends PureComponent<Props, State> {
appsLoaded: false,
app: '',
mode: 'home',
progress: 0,
}
componentDidMount() {
@ -98,53 +100,15 @@ class AppsList extends PureComponent<Props, State> {
_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() {
try {
const { deviceInfo } = this.props
const [
applicationsList,
compatibleAppVersionsList,
sortedCryptoCurrencies,
] = await Promise.all([
listApps.send().toPromise(),
listAppVersions.send(deviceInfo).toPromise(),
getFullListSortedCryptoCurrencies(),
])
const { deviceInfo, isDevMode } = this.props
const filteredAppVersionsList = this.prepareAppList({
applicationsList,
compatibleAppVersionsList,
sortedCryptoCurrencies,
})
try {
const filteredAppVersionsList = await manager.getAppsList(
deviceInfo,
isDevMode,
getFullListSortedCryptoCurrencies,
)
if (!this._unmounted) {
this.setState({
@ -158,41 +122,37 @@ class AppsList extends PureComponent<Props, State> {
}
}
handleInstallApp = (app: ApplicationVersion) => async () => {
this.setState({ status: 'busy', app: app.name, mode: 'installing' })
try {
const {
device: { path: devicePath },
deviceInfo,
} = this.props
const data = { app, devicePath, targetId: deviceInfo.targetId }
await installApp.send(data).toPromise()
this.setState({ status: 'success' })
} catch (err) {
this.setState({ status: 'error', error: err, mode: 'home' })
}
sub: *
runAppScript = (app: ApplicationVersion, mode: *, cmd: *) => {
this.setState({ status: 'busy', app: app.name, mode, progress: 0 })
const {
device: { path: devicePath },
deviceInfo: { targetId },
} = this.props
this.sub = cmd.send({ app, devicePath, targetId }).subscribe({
next: patch => {
this.setState(patch)
},
complete: () => {
this.setState({ status: 'success' })
},
error: error => {
this.setState({ status: 'error', error, app: '', mode: 'home' })
},
})
}
handleUninstallApp = (app: ApplicationVersion) => async () => {
this.setState({ status: 'busy', app: app.name, mode: 'uninstalling' })
try {
const {
device: { path: devicePath },
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' })
}
}
handleInstallApp = (app: ApplicationVersion) => () =>
this.runAppScript(app, 'installing', installApp)
handleUninstallApp = (app: ApplicationVersion) => () =>
this.runAppScript(app, 'uninstalling', uninstallApp)
handleCloseModal = () => this.setState({ status: 'idle', mode: 'home' })
renderModal = () => {
const { t } = this.props
const { app, status, error, mode } = this.state
const { app, status, error, mode, progress } = this.state
return (
<Modal
isOpened={status !== 'idle' && status !== 'loading'}
@ -217,7 +177,7 @@ class AppsList extends PureComponent<Props, State> {
{t(`manager.apps.${mode}`, { app })}
</Text>
<Box mt={6}>
<Progress style={{ width: '100%' }} infinite />
<ProgressBar width={150} progress={progress} />
</Box>
</ModalContent>
</Fragment>

2
src/components/ManagerPage/Dashboard.js

@ -4,7 +4,7 @@ import { translate } from 'react-i18next'
import styled from 'styled-components'
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 Text from 'components/base/Text'

85
src/components/ManagerPage/FirmwareUpdate.js

@ -3,20 +3,13 @@
import React, { PureComponent, Fragment } from 'react'
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 { DeviceInfo, OsuFirmware } from 'helpers/types'
import type { DeviceInfo, FirmwareUpdateContext } from '@ledgerhq/live-common/lib/types/manager'
import type { StepId } from 'components/modals/UpdateFirmware'
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 UpdateModal from 'components/modals/UpdateFirmware'
@ -42,32 +35,34 @@ type Props = {
}
type State = {
latestFirmware: ?OsuFirmware & ?{ shouldFlashMcu: boolean },
firmware: ?FirmwareUpdateContext,
modal: ModalStatus,
stepId: ?StepId,
shouldFlash: boolean,
ready: boolean,
}
const intializeState = ({ deviceInfo }): State => ({
latestFirmware: null,
firmware: null,
modal: 'closed',
stepId: deviceInfo.isBootloader ? 'updateMCU' : 'idCheck',
shouldFlash: false,
ready: false,
})
class FirmwareUpdate extends PureComponent<Props, State> {
state = intializeState(this.props)
componentDidMount() {
async componentDidMount() {
const { deviceInfo } = this.props
if (!deviceInfo.isOSU && !deviceInfo.isBootloader) {
this.fetchLatestFirmware()
} else if (deviceInfo.isOSU) {
this.shouldFlashMcu()
} else if (deviceInfo.isBootloader) {
this.handleInstallModal('updateMCU', true)
const firmware = await getLatestFirmwareForDevice.send(deviceInfo).toPromise()
if (firmware && !this._unmounting) {
/* eslint-disable */
this.setState({
firmware,
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
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' })
handleDisclaimerModal = () => this.setState({ modal: 'disclaimer' })
handleInstallModal = (stepId: StepId = 'idCheck', shouldFlash?: boolean) =>
this.setState({ modal: 'install', stepId, shouldFlash, ready: true })
handleDisclaimerNext = () => this.setState({ modal: 'install' })
render() {
const { deviceInfo, t, device } = this.props
const { latestFirmware, modal, stepId, shouldFlash, ready } = this.state
const { firmware, modal, stepId, ready } = this.state
return (
<Card p={4}>
<Box horizontal align="center" flow={2}>
@ -151,12 +106,12 @@ class FirmwareUpdate extends PureComponent<Props, State> {
})}
</Text>
</Box>
<UpdateFirmwareButton firmware={latestFirmware} onClick={this.handleDisclaimerModal} />
<UpdateFirmwareButton firmware={firmware} onClick={this.handleDisclaimerModal} />
</Box>
{ready ? (
<Fragment>
<DisclaimerModal
firmware={latestFirmware}
firmware={firmware}
status={modal}
goToNextStep={this.handleDisclaimerNext}
onClose={this.handleCloseModal}
@ -165,11 +120,7 @@ class FirmwareUpdate extends PureComponent<Props, State> {
status={modal}
stepId={stepId}
onClose={this.handleCloseModal}
firmware={latestFirmware}
shouldFlashMcu={shouldFlash}
installOsuFirmware={this.installOsuFirmware}
installFinalFirmware={this.installFinalFirmware}
flashMCU={this.flashMCU}
firmware={firmware}
/>
</Fragment>
) : null}

13
src/components/ManagerPage/UpdateFirmwareButton.js

@ -3,19 +3,14 @@ import React, { Fragment } from 'react'
import { translate } from 'react-i18next'
import type { T } from 'types/common'
import type { OsuFirmware, FinalFirmware } from '@ledgerhq/live-common/lib/types/manager'
import Button from 'components/base/Button'
import Text from 'components/base/Text'
import { getCleanVersion } from 'components/ManagerPage/FirmwareUpdate'
type FirmwareInfos = {
name: string,
notes: string,
}
type Props = {
t: T,
firmware: ?FirmwareInfos,
firmware: ?{ osu: OsuFirmware, finalFirmware: FinalFirmware },
onClick: () => void,
}
@ -23,14 +18,14 @@ const UpdateFirmwareButton = ({ t, firmware, onClick }: Props) =>
firmware ? (
<Fragment>
<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>
<Button
primary
onClick={onClick}
event={'Manager Firmware Update Click'}
eventProperties={{
firmwareName: firmware.name,
firmwareName: firmware.osu.name,
}}
>
{t('manager.firmware.update')}

2
src/components/ManagerPage/index.js

@ -5,7 +5,7 @@ import invariant from 'invariant'
import { openURL } from 'helpers/linking'
import { urls } from 'config/urls'
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 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 })}
/>
))

12
src/components/SelectCurrency/index.js

@ -18,6 +18,7 @@ type OwnProps = {
currencies?: CryptoCurrency[],
value?: CryptoCurrency,
placeholder: string,
autoFocus?: boolean,
t: T,
}
@ -29,12 +30,21 @@ const mapStateToProps = (state, props: OwnProps) => ({
currencies: props.currencies || availableCurrencies(state),
})
const SelectCurrency = ({ onChange, value, t, placeholder, currencies, ...props }: Props) => {
const SelectCurrency = ({
onChange,
value,
t,
placeholder,
currencies,
autoFocus,
...props
}: Props) => {
const options = currencies
? currencies.map(c => ({ ...c, value: c.id, label: c.name, currency: c }))
: []
return (
<Select
autoFocus={autoFocus}
value={value}
renderOption={renderOption}
renderValue={renderOption}

2
src/components/SettingsPage/DisablePasswordModal.js

@ -1,7 +1,7 @@
// @flow
import React, { PureComponent } from 'react'
import { PasswordIncorrectError } from 'config/errors'
import { PasswordIncorrectError } from '@ledgerhq/errors'
import db from 'helpers/db'
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 Label from 'components/base/Label'
import { PasswordsDontMatchError } from 'config/errors'
import { PasswordsDontMatchError } from '@ledgerhq/errors'
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 db from 'helpers/db'
import { PasswordIncorrectError } from 'config/errors'
import { PasswordIncorrectError } from '@ledgerhq/errors'
import Box from 'components/base/Box'
import Button from 'components/base/Button'
import { Modal, ModalContent, ModalBody, ModalTitle, ModalFooter } from 'components/base/Modal'

102
src/components/SettingsPage/RepairDeviceButton.js

@ -0,0 +1,102 @@
// @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 })
}
repair = (version = null) => {
if (this.state.isLoading) return
const { push } = this.props
this.setState({ isLoading: true })
this.sub = firmwareRepair.send({ version }).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}
repair={this.repair}
isLoading={isLoading}
title={t('settings.repairDevice.title')}
desc={t('settings.repairDevice.desc')}
progress={progress}
error={error}
/>
</Fragment>
)
}
}
const mapDispatchToProps = {
push,
}
export default compose(
translate(),
withRouter,
connect(
null,
mapDispatchToProps,
),
)(RepairDeviceButton)

94
src/components/SettingsPage/SocketExport.js

@ -0,0 +1,94 @@
// @flow
import React, { PureComponent } from 'react'
import WebSocket from 'ws'
import IP from 'ip'
import { createStructuredSelector } from 'reselect'
import { activeAccountsSelector } from 'reducers/accounts'
import { exportSettingsSelector } from 'reducers/settings'
import { encode } from '@ledgerhq/live-common/lib/cross'
import connect from 'react-redux/es/connect/connect'
import Button from '../base/Button'
import QRCode from '../base/QRCode'
type Props = {
accounts: *,
settings: *,
}
type State = {
active: boolean,
}
const mapStateToProps = createStructuredSelector({
accounts: activeAccountsSelector,
settings: exportSettingsSelector,
})
class SocketExport extends PureComponent<Props, State> {
state = {
active: false,
}
componentWillMount() {
this.resetServer()
}
componentDidUpdate() {
if (!this.state.active) return
if (!this.server) {
this.resetServer()
}
}
componentWillUnmount() {
if (this.server) this.server.close()
}
resetServer = () => {
this.server = new WebSocket.Server({ port: 1234 })
const { accounts, settings } = this.props
const data = encode({
accounts,
settings,
exporterName: 'desktop',
exporterVersion: __APP_VERSION__,
})
// Secret handshake to avoid intruders
this.secret = Math.random()
.toString(36)
.slice(2)
if (this.server) {
this.server.on('connection', ws => {
ws.on('message', message => {
if (message === this.secret) {
ws.send(data)
ws.close()
this.setState({ active: false })
this.server = undefined
}
})
})
}
}
secret: string
server: *
canvas = React.createRef()
render() {
return this.state.active ? (
<QRCode size={50} data={`${this.secret}~${IP.address()}`} />
) : (
<Button primary small onClick={() => this.setState({ active: true })}>
{'Generate Code'}
</Button>
)
}
}
export default connect(mapStateToProps)(SocketExport)

14
src/components/SettingsPage/index.js

@ -7,7 +7,6 @@ import { Trans, translate } from 'react-i18next'
import type { T } from 'types/common'
import { Switch, Route } from 'react-router'
import type { RouterHistory, Match, Location } from 'react-router'
import { EXPERIMENTAL_TOOLS_SETTINGS } from 'config/constants'
import { accountsSelector } from 'reducers/accounts'
import Pills from 'components/base/Pills'
import Box from 'components/base/Box'
@ -48,6 +47,11 @@ class SettingsPage extends PureComponent<Props, State> {
label: <Trans i18nKey="settings.tabs.currencies" />,
value: SectionCurrencies,
},
{
key: 'export',
label: <Trans i18nKey="settings.tabs.export" />,
value: SectionExport,
},
{
key: 'about',
label: <Trans i18nKey="settings.tabs.about" />,
@ -60,14 +64,6 @@ class SettingsPage extends PureComponent<Props, State> {
},
]
if (EXPERIMENTAL_TOOLS_SETTINGS) {
this._items.splice(2, 0, {
key: 'tool',
label: 'Experimental Tools',
value: SectionExport,
})
}
this.state = {
tab: this.getCurrentTab({ url: props.match.url, pathname: props.location.pathname }),
}

12
src/components/SettingsPage/sections/Export.js

@ -8,6 +8,7 @@ import type { T } from 'types/common'
import TrackPage from 'analytics/TrackPage'
import styled from 'styled-components'
import { SettingsSection as Section, SettingsSectionHeader as Header } from '../SettingsSection'
import { EXPERIMENTAL_WS_EXPORT } from '../../../config/constants'
import IconShare from '../../../icons/Share'
import Button from '../../base/Button'
import Modal, { ModalBody, ModalContent, ModalFooter, ModalTitle } from '../../base/Modal'
@ -15,6 +16,7 @@ import Box from '../../base/Box'
import QRCodeExporter from '../../QRCodeExporter'
import { BulletRow } from '../../Onboarding/helperComponents'
import Text from '../../base/Text'
import SocketExport from '../SocketExport'
const BulletRowIcon = styled(Box).attrs({
ff: 'Rubik|Regular',
@ -131,7 +133,7 @@ class SectionExport extends PureComponent<Props, State> {
<Header
icon={<IconShare size={16} />}
title={t('settings.tabs.export')}
title={t('settings.export.title')}
desc={t('settings.export.desc')}
renderRight={
<Button small onClick={this.onModalOpen} primary>
@ -139,6 +141,14 @@ class SectionExport extends PureComponent<Props, State> {
</Button>
}
/>
{EXPERIMENTAL_WS_EXPORT && (
<Header
icon={<IconShare size={16} />}
title="Experimental websocket local export ⚡"
desc="Generate a pairing code and use it on Ledger Live Mobile"
renderRight={<SocketExport />}
/>
)}
<Modal isOpened={isModalOpened} onClose={this.onModalClose} render={this.renderModal} />
</Section>
)

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

@ -11,6 +11,7 @@ import ExportLogsBtn from 'components/ExportLogsBtn'
import OpenUserDataDirectoryBtn from 'components/OpenUserDataDirectoryBtn'
import CleanButton from '../CleanButton'
import ResetButton from '../ResetButton'
import RepairDeviceButton from '../RepairDeviceButton'
import AboutRowItem from '../AboutRowItem'
import LaunchOnboardingBtn from '../LaunchOnboardingBtn'
@ -72,6 +73,12 @@ class SectionHelp extends PureComponent<Props> {
>
<ResetButton />
</Row>
<Row
title={t('settings.repairDevice.title')}
desc={t('settings.repairDevice.descSettings')}
>
<RepairDeviceButton />
</Row>
</Body>
</Section>
)

4
src/components/WithFeesAPI.js

@ -1,8 +1,8 @@
// @flow
import { Component } from 'react'
import type { Currency } from '@ledgerhq/live-common/lib/types'
import { getEstimatedFees } from 'api/Fees'
import type { Fees } from 'api/Fees'
import { getEstimatedFees } from '@ledgerhq/live-common/lib/api/Fees'
import type { Fees } from '@ledgerhq/live-common/lib/api/Fees'
// FIXME we need to abstract this out like we did for CounterValues
export default class WithFeesAPI extends Component<

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

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

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

@ -0,0 +1,234 @@
// @flow
import React, { PureComponent } from 'react'
import { translate } from 'react-i18next'
import styled from 'styled-components'
import { forceRepairChoices } from '@ledgerhq/live-common/lib/hw/firmwareUpdate-repair'
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 Select from 'components/base/Select'
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,
repair: (?string) => *,
t: T,
isLoading?: boolean,
analyticsName: string,
cancellable?: boolean,
progress: number,
error?: Error,
}
class RepairModal extends PureComponent<Props, *> {
state = {
selectedOption: forceRepairChoices[0],
}
onChange = selectedOption => {
this.setState({ selectedOption: selectedOption || forceRepairChoices[0] })
}
renderOption = option => (option && this.props.t(`settings.repairDevice.${option.label}`)) || null
renderValue = option =>
(option && this.props.t(`settings.repairDevice.${option.data.label}`)) || null
render() {
const {
cancellable,
isOpened,
title,
desc,
confirmText,
isDanger,
onReject,
repair,
isLoading,
renderIcon,
t,
analyticsName,
progress,
error,
...props
} = this.props
const { selectedOption } = this.state
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 && !error ? (
<Box py={2} px={5}>
<Select
isSearchable={false}
isClearable={false}
value={selectedOption}
onChange={this.onChange}
autoFocus
options={forceRepairChoices}
renderOption={this.renderOption}
renderValue={this.renderValue}
/>
</Box>
) : null}
{!isLoading ? (
<ModalFooter horizontal align="center" justify="flex-end" flow={2}>
{error ? <Button onClick={onReject}>{t(`common.close`)}</Button> : null}
{error ? null : (
<>
<Button
onClick={() => repair(selectedOption.value)}
primary={!isDanger}
danger={isDanger}
isLoading={isLoading}
disabled={isLoading}
>
{t('settings.repairDevice.button')}
</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 ConfirmModal } from './ConfirmModal'
export { default as RepairModal } from './RepairModal'
export { default as ModalTitle } from './ModalTitle'
const springConfig = {

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

@ -28,6 +28,7 @@ type Props = {
small: boolean,
width: number,
minWidth: number,
autoFocus: boolean,
}
export type Option = {
@ -37,6 +38,19 @@ export type Option = {
}
class Select extends PureComponent<Props> {
componentDidMount() {
if (this.ref && this.props.autoFocus) {
// $FlowFixMe
this.timeout = requestAnimationFrame(() => this.ref.focus())
}
}
componentWillUnmount() {
if (this.timeout) {
cancelAnimationFrame(this.timeout)
}
}
handleChange = (value, { action }) => {
const { onChange } = this.props
if (action === 'select-option') {
@ -47,6 +61,9 @@ class Select extends PureComponent<Props> {
}
}
ref: *
timeout: *
render() {
const {
value,
@ -68,6 +85,7 @@ class Select extends PureComponent<Props> {
return (
<ReactSelect
ref={c => (this.ref = c)}
value={value}
maxMenuHeight={300}
classNamePrefix="select"

2
src/components/layout/Default.js

@ -37,6 +37,7 @@ import SyncBackground from 'components/SyncBackground'
import DebugUpdater from 'components/Updater/DebugUpdater'
import SyncContinuouslyPendingOperations from '../SyncContinouslyPendingOperations'
import HSMStatusBanner from '../HSMStatusBanner'
const Main = styled(GrowScroll).attrs({
px: 6,
@ -110,6 +111,7 @@ class Default extends Component<Props> {
<SideBar />
<Box shrink grow bg="lightGrey" color="grey" overflow="hidden" relative>
<HSMStatusBanner />
<TopBar />
<Main innerRef={n => (this._scrollContainer = n)} tabIndex={-1}>
<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 { AccountNameRequiredError, EnpointConfigError } from 'config/errors'
import { AccountNameRequiredError, EnpointConfigError } from '@ledgerhq/errors'
import TrackPage from 'analytics/TrackPage'
import Spoiler from 'components/base/Spoiler'

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 getAddress from 'commands/getAddress'
import { isSegwitDerivationMode } from '@ledgerhq/live-common/lib/derivation'
import Box from 'components/base/Box'
import CurrentAddressForAccount from 'components/CurrentAddressForAccount'
import { DisconnectedDevice, WrongDeviceForAccount } from 'config/errors'
import { DisconnectedDevice, WrongDeviceForAccount } from '@ledgerhq/errors'
import type { StepProps } from '..'
@ -25,14 +24,14 @@ export default class StepReceiveFunds extends PureComponent<StepProps> {
if (!device || !account) {
throw new DisconnectedDevice()
}
const params = {
currencyId: account.currency.id,
devicePath: device.path,
path: account.freshAddressPath,
segwit: isSegwitDerivationMode(account.derivationMode),
verify: true,
}
const { address } = await getAddress.send(params).toPromise()
const { address } = await getAddress
.send({
currencyId: account.currency.id,
devicePath: device.path,
path: account.freshAddressPath,
verify: true,
})
.toPromise()
if (address !== account.freshAddress) {
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 RecipientAddress from 'components/RecipientAddress'
import { track } from 'analytics/segment'
import { createCustomErrorClass } from 'helpers/errors'
import { CantScanQRCode } from 'config/errors'
import { createCustomErrorClass } from '@ledgerhq/errors/lib/helpers'
import { CantScanQRCode } from '@ledgerhq/errors'
type Props<Transaction> = {
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 { accountsSelector } from 'reducers/accounts'
import { closeModal, openModal } from 'reducers/modals'
import { DisconnectedDevice, UserRefusedOnDevice } from 'config/errors'
import { DisconnectedDevice, UserRefusedOnDevice } from '@ledgerhq/errors'
import Modal from 'components/base/Modal'
import Stepper from 'components/base/Stepper'

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

@ -3,7 +3,7 @@
import React, { PureComponent, Fragment } from 'react'
import { translate, Trans } from 'react-i18next'
import type { OsuFirmware, FinalFirmware } from '@ledgerhq/live-common/lib/types/manager'
import type { T } from 'types/common'
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'
type FirmwareInfos = {
name: string,
notes: string,
}
type Props = {
t: T,
status: ModalStatus,
firmware: FirmwareInfos,
firmware: {
osu: ?OsuFirmware,
final: ?FinalFirmware,
},
goToNextStep: () => void,
onClose: () => void,
}
@ -50,7 +48,9 @@ class DisclaimerModal extends PureComponent<Props, State> {
<Trans i18nKey="manager.firmware.disclaimerTitle">
You are about to install
<Text ff="Open Sans|SemiBold" color="dark">
{`firmware version ${firmware ? getCleanVersion(firmware.name) : ''}`}
{`firmware version ${
firmware && firmware.osu ? getCleanVersion(firmware.osu.name) : ''
}`}
</Text>
</Trans>
</Text>
@ -59,14 +59,16 @@ class DisclaimerModal extends PureComponent<Props, State> {
{t('manager.firmware.disclaimerAppReinstall')}
</Text>
</ModalContent>
<ModalContent relative pb={0} style={{ height: 250, width: '100%' }}>
<GrowScroll pb={5}>
<Notes>
<Markdown>{firmware.notes}</Markdown>
</Notes>
</GrowScroll>
<GradientBox />
</ModalContent>
{firmware && firmware.osu ? (
<ModalContent relative pb={0} style={{ height: 250, width: '100%' }}>
<GrowScroll pb={5}>
<Notes>
<Markdown>{firmware.osu.notes}</Markdown>
</Notes>
</GrowScroll>
<GradientBox />
</ModalContent>
) : null}
<ModalFooter horizontal justifyContent="flex-end" style={{ width: '100%' }}>
<Button primary onClick={goToNextStep}>
{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 Text from 'components/base/Text'
import Spinner from 'components/base/Spinner'
import ProgressCircle from 'components/ProgressCircle'
import type { T } from 'types/common'
type Props = {
t: T,
progress: number,
installing: string,
}
function Installing({ t }: Props) {
function Installing({ t, progress, installing }: Props) {
return (
<Fragment>
<Box mx={7} align="center">
<Spinner color="fog" size={44} />
<ProgressCircle size={64} progress={progress} />
</Box>
<Box mx={7} mt={4} mb={2}>
<Text ff="Museo Sans|Regular" align="center" color="dark" fontSize={6}>
{t('manager.modal.installing')}
{t(`manager.modal.steps.${installing}`)}
</Text>
</Box>
<Box mx={7} mt={4} mb={7}>

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

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

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

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

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

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

9
src/config/constants.js

@ -58,12 +58,6 @@ export const LEDGER_REST_API_BASE = stringFromEnv(
'LEDGER_REST_API_BASE',
'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
export const FORCE_PROVIDER = intFromEnv('FORCE_PROVIDER', 0)
@ -84,12 +78,13 @@ export const LEDGER_DEBUG_ALL_LANGS = boolFromEnv('LEDGER_DEBUG_ALL_LANGS')
export const SKIP_GENUINE = boolFromEnv('SKIP_GENUINE')
export const SKIP_ONBOARDING = boolFromEnv('SKIP_ONBOARDING')
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 DISABLE_ACTIVITY_INDICATORS = boolFromEnv('DISABLE_ACTIVITY_INDICATORS')
export const EXPERIMENTAL_WS_EXPORT = boolFromEnv('EXPERIMENTAL_WS_EXPORT')
export const EXPERIMENTAL_CENTER_MODAL = boolFromEnv('EXPERIMENTAL_CENTER_MODAL')
export const EXPERIMENTAL_FIRMWARE_UPDATE = boolFromEnv('EXPERIMENTAL_FIRMWARE_UPDATE')
export const EXPERIMENTAL_HTTP_ON_RENDERER = boolFromEnv('EXPERIMENTAL_HTTP_ON_RENDERER')
export const EXPERIMENTAL_TOOLS_SETTINGS = boolFromEnv('EXPERIMENTAL_TOOLS_SETTINGS')
export const EXPERIMENTAL_MARKET_INDICATOR_SETTINGS = boolFromEnv(
'EXPERIMENTAL_MARKET_INDICATOR_SETTINGS',
)

1
src/config/cryptocurrencies.js

@ -24,6 +24,7 @@ const supported: CryptoCurrencyIds[] = [
'vertcoin',
'peercoin',
'viacoin',
'stakenet',
'stealthcoin',
'poswallet',
'clubcoin',

58
src/config/errors.js

@ -1,58 +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')
// auto-update errors
export const UpdateIncorrectHash = createCustomErrorClass('UpdateIncorrectHash')
export const UpdateIncorrectSig = createCustomErrorClass('UpdateIncorrectSig')
export const UpdateFetchFileFail = createCustomErrorClass('UpdateFetchFileFail')

4
src/config/urls.js

@ -33,9 +33,11 @@ export const urls = {
shapeshift: 'https://shapeshift.io/#/coins?affiliate=ledger',
genesis: 'https://genesistrading.com/ledger-live/',
kyberSwap: 'http://kyber.network/swap?ref=0xE2D8481eeF31CDA994833974FFfEccd576f8D71E',
changeNow: 'https://changenow.io/?utm_source=ledger_wallet',
changeNow: 'https://changenow.io?link_id=80ab1d8ad846e7',
thorSwap:
'https://www.thorswap.com/?utm_source=Wallet&utm_medium=ledger&utm_campaign=EmbedLink&utm_content=Link1',
coinberry: 'https://www.coinberry.com/?utm_source=ledger',
btcDirect: 'https://btcdirect.eu/en-gb?partnerId=261',
// Errors
errors: {

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 : []
}

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

Loading…
Cancel
Save