Browse Source

Merge pull request #643 from LedgerHQ/develop

Prepare alpha.13
master
Gaëtan Renaudeau 7 years ago
committed by GitHub
parent
commit
c8c2e5a964
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. BIN
      build/background.png
  2. BIN
      build/background@2x.png
  3. BIN
      build/icon.icns
  4. BIN
      build/icon.ico
  5. BIN
      build/icon.png
  6. BIN
      build/icons/512x512.png
  7. BIN
      build/installerSidebar.bmp
  8. BIN
      build/uninstallerSidebar.bmp
  9. 4
      src/actions/accounts.js
  10. 13
      src/api/Fees.js
  11. 13
      src/bridge/BridgeSyncContext.js
  12. 4
      src/bridge/EthereumJSBridge.js
  13. 9
      src/bridge/RippleJSBridge.js
  14. 19
      src/commands/getCurrentFirmware.js
  15. 19
      src/commands/getFirmwareInfo.js
  16. 14
      src/commands/getIsGenuine.js
  17. 4
      src/commands/index.js
  18. 9
      src/commands/installApp.js
  19. 25
      src/commands/installMcu.js
  20. 11
      src/commands/installOsuFirmware.js
  21. 5
      src/commands/listApps.js
  22. 9
      src/commands/uninstallApp.js
  23. 12
      src/components/AccountPage/index.js
  24. 23
      src/components/AppError.js
  25. 41
      src/components/BalanceSummary/BalanceInfos.js
  26. 28
      src/components/BalanceSummary/index.js
  27. 9
      src/components/DashboardPage/index.js
  28. 38
      src/components/DeviceBusyIndicator.js
  29. 37
      src/components/LibcoreBusyIndicator.js
  30. 17
      src/components/ManagerPage/AppSearchBar.js
  31. 41
      src/components/ManagerPage/AppsList.js
  32. 2
      src/components/ManagerPage/Dashboard.js
  33. 16
      src/components/ManagerPage/FirmwareUpdate.js
  34. 55
      src/components/ManagerPage/FlashMcu.js
  35. 5
      src/components/ManagerPage/UpdateFirmwareButton.js
  36. 7
      src/components/ManagerPage/index.js
  37. 8
      src/components/Onboarding/OnboardingBreadcrumb.js
  38. 44
      src/components/Onboarding/steps/GenuineCheck.js
  39. 4
      src/components/Onboarding/steps/WriteSeed/WriteSeedBlue.js
  40. 4
      src/components/Onboarding/steps/WriteSeed/WriteSeedNano.js
  41. 2
      src/components/OperationsList/index.js
  42. 9
      src/components/Placeholder.js
  43. 131
      src/components/RenderError.js
  44. 3
      src/components/SelectAccount/index.js
  45. 3
      src/components/SelectCurrency/index.js
  46. 19
      src/components/SelectExchange.js
  47. 2
      src/components/SettingsPage/sections/Display.js
  48. 16
      src/components/SyncAgo.js
  49. 85
      src/components/ThrowBlock.js
  50. 13
      src/components/TopBar/ActivityIndicator.js
  51. 9
      src/components/TopBar/index.js
  52. 8
      src/components/TranslatedError.js
  53. 16
      src/components/TriggerAppReady.js
  54. 15
      src/components/TriggerOnMount/index.js
  55. 5
      src/components/Workflow/EnsureGenuine.js
  56. 30
      src/components/Workflow/index.js
  57. 8
      src/components/base/Button/index.js
  58. 1
      src/components/base/Modal/ModalTitle.js
  59. 16
      src/components/base/Spoiler/index.js
  60. 10
      src/components/layout/Default.js
  61. 3
      src/components/modals/AccountSettingRenderBody.js
  62. 85
      src/components/modals/AddAccounts/steps/03-step-import.js
  63. 163
      src/components/modals/OperationDetails.js
  64. 2
      src/components/modals/ReleaseNotes.js
  65. 7
      src/config/constants.js
  66. 17
      src/helpers/apps/installApp.js
  67. 26
      src/helpers/apps/listApps.js
  68. 19
      src/helpers/apps/uninstallApp.js
  69. 49
      src/helpers/common.js
  70. 3
      src/helpers/countervalues.js
  71. 19
      src/helpers/deviceAccess.js
  72. 26
      src/helpers/devices/getCurrentFirmware.js
  73. 19
      src/helpers/devices/getDeviceVersion.js
  74. 34
      src/helpers/devices/getFirmwareInfo.js
  75. 24
      src/helpers/devices/getIsGenuine.js
  76. 37
      src/helpers/devices/getLatestFirmwareForDevice.js
  77. 7
      src/helpers/devices/getMemInfo.js
  78. 26
      src/helpers/devices/getNextMCU.js
  79. 10
      src/helpers/firmware/installFinalFirmware.js
  80. 25
      src/helpers/firmware/installMcu.js
  81. 23
      src/helpers/firmware/installOsuFirmware.js
  82. 27
      src/helpers/urls.js
  83. 18
      src/helpers/withLibcore.js
  84. 59
      src/index.ejs
  85. 15
      src/main/bridge.js
  86. 14
      src/reducers/onboarding.js
  87. 16
      src/renderer/events.js
  88. 3
      src/renderer/init.js
  89. 22
      static/i18n/en/app.yml
  90. 11
      static/i18n/en/errors.yml
  91. 142
      static/i18n/en/onboarding.yml
  92. 22
      static/i18n/fr/app.yml
  93. 15
      static/i18n/fr/errors.yml
  94. 4
      static/i18n/fr/language.yml
  95. 127
      static/i18n/fr/onboarding.yml
  96. 1
      static/images/crash-screen.svg
  97. 28
      static/images/ledgerlive-logo.svg
  98. BIN
      static/videos/loader.mp4

BIN
build/background.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
build/background@2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
build/icon.icns

Binary file not shown.

BIN
build/icon.ico

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

BIN
build/icon.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 135 KiB

BIN
build/icons/512x512.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

BIN
build/installerSidebar.bmp

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

BIN
build/uninstallerSidebar.bmp

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

4
src/actions/accounts.js

@ -6,7 +6,7 @@ import db from 'helpers/db'
export type AddAccount = Account => * export type AddAccount = Account => *
export const addAccount: AddAccount = payload => ({ export const addAccount: AddAccount = payload => ({
type: 'ADD_ACCOUNT', type: 'DB:ADD_ACCOUNT',
payload, payload,
}) })
@ -48,4 +48,4 @@ export const updateAccount: UpdateAccount = payload => ({
}, },
}) })
export const cleanAccountsCache = () => ({ type: 'CLEAN_ACCOUNTS_CACHE' }) export const cleanAccountsCache = () => ({ type: 'DB:CLEAN_ACCOUNTS_CACHE' })

13
src/api/Fees.js

@ -1,5 +1,6 @@
// @flow // @flow
import invariant from 'invariant' import invariant from 'invariant'
import LRU from 'lru-cache'
import type { Currency } from '@ledgerhq/live-common/lib/types' import type { Currency } from '@ledgerhq/live-common/lib/types'
import createCustomErrorClass from 'helpers/createCustomErrorClass' import createCustomErrorClass from 'helpers/createCustomErrorClass'
import { blockchainBaseURL } from './Ledger' import { blockchainBaseURL } from './Ledger'
@ -11,10 +12,20 @@ export type Fees = {
[_: string]: number, [_: string]: number,
} }
const cache = LRU({
maxAge: 5 * 60 * 1000,
})
export const getEstimatedFees = async (currency: Currency): Promise<Fees> => { 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) const baseURL = blockchainBaseURL(currency)
invariant(baseURL, `Fees for ${currency.id} are not supported`) invariant(baseURL, `Fees for ${currency.id} are not supported`)
const { data, status } = await network({ method: 'GET', url: `${baseURL}/fees` }) 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) { if (data) {
return data return data
} }

13
src/bridge/BridgeSyncContext.js

@ -6,6 +6,7 @@
import invariant from 'invariant' import invariant from 'invariant'
import logger from 'logger' import logger from 'logger'
import shuffle from 'lodash/shuffle' import shuffle from 'lodash/shuffle'
import { timeout } from 'rxjs/operators/timeout'
import React, { Component } from 'react' import React, { Component } from 'react'
import priorityQueue from 'async/priorityQueue' import priorityQueue from 'async/priorityQueue'
import { connect } from 'react-redux' import { connect } from 'react-redux'
@ -16,7 +17,12 @@ import { setAccountSyncState } from 'actions/bridgeSync'
import { bridgeSyncSelector, syncStateLocalSelector } from 'reducers/bridgeSync' import { bridgeSyncSelector, syncStateLocalSelector } from 'reducers/bridgeSync'
import type { BridgeSyncState } from 'reducers/bridgeSync' import type { BridgeSyncState } from 'reducers/bridgeSync'
import { accountsSelector } from 'reducers/accounts' import { accountsSelector } from 'reducers/accounts'
import { SYNC_BOOT_DELAY, SYNC_ALL_INTERVAL, SYNC_MAX_CONCURRENT } from 'config/constants' import {
SYNC_BOOT_DELAY,
SYNC_ALL_INTERVAL,
SYNC_MAX_CONCURRENT,
SYNC_TIMEOUT,
} from 'config/constants'
import { getBridgeForCurrency } from '.' import { getBridgeForCurrency } from '.'
type BridgeSyncProviderProps = { type BridgeSyncProviderProps = {
@ -73,7 +79,10 @@ class Provider extends Component<BridgeSyncProviderOwnProps, Sync> {
this.props.setAccountSyncState(accountId, { pending: true, error: null }) this.props.setAccountSyncState(accountId, { pending: true, error: null })
// TODO use Subscription to unsubscribe at relevant time // TODO use Subscription to unsubscribe at relevant time
bridge.synchronize(account).subscribe({ bridge
.synchronize(account)
.pipe(timeout(SYNC_TIMEOUT))
.subscribe({
next: accountUpdater => { next: accountUpdater => {
this.props.updateAccountWithUpdater(accountId, accountUpdater) this.props.updateAccountWithUpdater(accountId, accountUpdater)
}, },

4
src/bridge/EthereumJSBridge.js

@ -170,7 +170,7 @@ const EthereumBridge: WalletBridge<Transaction> = {
async function stepAddress( async function stepAddress(
index, index,
{ address, path: freshAddressPath }, { address, path: freshAddressPath, publicKey },
isStandard, isStandard,
): { account?: Account, complete?: boolean } { ): { account?: Account, complete?: boolean } {
const balance = await api.getAccountBalance(address) const balance = await api.getAccountBalance(address)
@ -181,7 +181,7 @@ const EthereumBridge: WalletBridge<Transaction> = {
if (finished) return { complete: true } if (finished) return { complete: true }
const freshAddress = address const freshAddress = address
const accountId = `ethereumjs:${currency.id}:${address}` const accountId = `ethereumjs:${currency.id}:${address}:${publicKey}`
if (txs.length === 0) { if (txs.length === 0) {
// this is an empty account // this is an empty account

9
src/bridge/RippleJSBridge.js

@ -256,14 +256,15 @@ const RippleJSBridge: WalletBridge<Transaction> = {
const derivations = getDerivations(currency) const derivations = getDerivations(currency)
for (const derivation of derivations) { for (const derivation of derivations) {
const legacy = derivation !== derivations[derivations.length - 1]
for (let index = 0; index < 255; index++) { for (let index = 0; index < 255; index++) {
const freshAddressPath = derivation({ currency, x: index, segwit: false }) const freshAddressPath = derivation({ currency, x: index, segwit: false })
const { address } = await await getAddress const { address, publicKey } = await await getAddress
.send({ currencyId: currency.id, devicePath: deviceId, path: freshAddressPath }) .send({ currencyId: currency.id, devicePath: deviceId, path: freshAddressPath })
.toPromise() .toPromise()
if (finished) return if (finished) return
const accountId = `ripplejs:${currency.id}:${address}` const accountId = `ripplejs:${currency.id}:${address}:${publicKey}`
let info let info
try { try {
@ -280,6 +281,7 @@ const RippleJSBridge: WalletBridge<Transaction> = {
if (!info) { if (!info) {
// account does not exist in Ripple server // account does not exist in Ripple server
// we are generating a new account locally // we are generating a new account locally
if (!legacy) {
next({ next({
id: accountId, id: accountId,
xpub: '', xpub: '',
@ -296,6 +298,7 @@ const RippleJSBridge: WalletBridge<Transaction> = {
archived: false, archived: false,
lastSyncDate: new Date(), lastSyncDate: new Date(),
}) })
}
break break
} }
@ -315,7 +318,7 @@ const RippleJSBridge: WalletBridge<Transaction> = {
const account: $Exact<Account> = { const account: $Exact<Account> = {
id: accountId, id: accountId,
xpub: '', xpub: '',
name: getAccountPlaceholderName(currency, index), name: getAccountPlaceholderName(currency, index, legacy),
freshAddress, freshAddress,
freshAddressPath, freshAddressPath,
balance, balance,

19
src/commands/getCurrentFirmware.js

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

19
src/commands/getFirmwareInfo.js

@ -1,19 +0,0 @@
// @flow
import { createCommand, Command } from 'helpers/ipc'
import { fromPromise } from 'rxjs/observable/fromPromise'
import getFirmwareInfo from 'helpers/devices/getFirmwareInfo'
type Input = {
targetId: string | number,
version: string,
}
type Result = *
const cmd: Command<Input, Result> = createCommand('getFirmwareInfo', data =>
fromPromise(getFirmwareInfo(data)),
)
export default cmd

14
src/commands/getIsGenuine.js

@ -6,11 +6,19 @@ import { fromPromise } from 'rxjs/observable/fromPromise'
import getIsGenuine from 'helpers/devices/getIsGenuine' import getIsGenuine from 'helpers/devices/getIsGenuine'
import { withDevice } from 'helpers/deviceAccess' import { withDevice } from 'helpers/deviceAccess'
type Input = * // FIXME ! type Input = {
devicePath: string,
targetId: string | number,
version: string,
}
type Result = string type Result = string
const cmd: Command<Input, Result> = createCommand('getIsGenuine', ({ devicePath, targetId }) => const cmd: Command<Input, Result> = createCommand(
fromPromise(withDevice(devicePath)(transport => getIsGenuine(transport, { targetId }))), 'getIsGenuine',
({ devicePath, targetId, version }) =>
fromPromise(
withDevice(devicePath)(transport => getIsGenuine(transport, { targetId, version })),
),
) )
export default cmd export default cmd

4
src/commands/index.js

@ -5,7 +5,7 @@ import type { Command } from 'helpers/ipc'
import getAddress from 'commands/getAddress' import getAddress from 'commands/getAddress'
import getDeviceInfo from 'commands/getDeviceInfo' import getDeviceInfo from 'commands/getDeviceInfo'
import getFirmwareInfo from 'commands/getFirmwareInfo' import getCurrentFirmware from 'commands/getCurrentFirmware'
import getIsGenuine from 'commands/getIsGenuine' import getIsGenuine from 'commands/getIsGenuine'
import getLatestFirmwareForDevice from 'commands/getLatestFirmwareForDevice' import getLatestFirmwareForDevice from 'commands/getLatestFirmwareForDevice'
import getMemInfo from 'commands/getMemInfo' import getMemInfo from 'commands/getMemInfo'
@ -32,7 +32,7 @@ import uninstallApp from 'commands/uninstallApp'
const all: Array<Command<any, any>> = [ const all: Array<Command<any, any>> = [
getAddress, getAddress,
getDeviceInfo, getDeviceInfo,
getFirmwareInfo, getCurrentFirmware,
getIsGenuine, getIsGenuine,
getLatestFirmwareForDevice, getLatestFirmwareForDevice,
getMemInfo, getMemInfo,

9
src/commands/installApp.js

@ -9,14 +9,17 @@ import installApp from 'helpers/apps/installApp'
import type { LedgerScriptParams } from 'helpers/common' import type { LedgerScriptParams } from 'helpers/common'
type Input = { type Input = {
appParams: LedgerScriptParams, app: LedgerScriptParams,
devicePath: string, devicePath: string,
targetId: string | number,
} }
type Result = * type Result = *
const cmd: Command<Input, Result> = createCommand('installApp', ({ devicePath, ...rest }) => const cmd: Command<Input, Result> = createCommand(
fromPromise(withDevice(devicePath)(transport => installApp(transport, rest))), 'installApp',
({ devicePath, targetId, ...app }) =>
fromPromise(withDevice(devicePath)(transport => installApp(transport, targetId, app))),
) )
export default cmd export default cmd

25
src/commands/installMcu.js

@ -3,24 +3,21 @@
import { createCommand, Command } from 'helpers/ipc' import { createCommand, Command } from 'helpers/ipc'
import { fromPromise } from 'rxjs/observable/fromPromise' import { fromPromise } from 'rxjs/observable/fromPromise'
// import { withDevice } from 'helpers/deviceAccess' import { withDevice } from 'helpers/deviceAccess'
import installMcu from 'helpers/firmware/installMcu' import installMcu from 'helpers/firmware/installMcu'
// type Input = { type Input = {
// devicePath: string, devicePath: string,
// firmware: Object, targetId: string | number,
// } version: string,
}
// type Result = {
// targetId: number | string,
// version: string,
// final: boolean,
// mcu: boolean,
// }
type Input = *
type Result = * type Result = *
const cmd: Command<Input, Result> = createCommand('installMcu', () => fromPromise(installMcu())) const cmd: Command<Input, Result> = createCommand(
'installMcu',
({ devicePath, targetId, version }) =>
fromPromise(withDevice(devicePath)(transport => installMcu(transport, { targetId, version }))),
)
export default cmd export default cmd

11
src/commands/installOsuFirmware.js

@ -6,17 +6,22 @@ import { fromPromise } from 'rxjs/observable/fromPromise'
import { withDevice } from 'helpers/deviceAccess' import { withDevice } from 'helpers/deviceAccess'
import installOsuFirmware from 'helpers/firmware/installOsuFirmware' import installOsuFirmware from 'helpers/firmware/installOsuFirmware'
import type { LedgerScriptParams } from 'helpers/common'
type Input = { type Input = {
devicePath: string, devicePath: string,
firmware: Object, targetId: string | number,
firmware: LedgerScriptParams,
} }
type Result = * type Result = *
const cmd: Command<Input, Result> = createCommand( const cmd: Command<Input, Result> = createCommand(
'installOsuFirmware', 'installOsuFirmware',
({ devicePath, firmware }) => ({ devicePath, firmware, targetId }) =>
fromPromise(withDevice(devicePath)(transport => installOsuFirmware(transport, firmware))), fromPromise(
withDevice(devicePath)(transport => installOsuFirmware(transport, targetId, firmware)),
),
) )
export default cmd export default cmd

5
src/commands/listApps.js

@ -7,12 +7,13 @@ import listApps from 'helpers/apps/listApps'
type Input = { type Input = {
targetId: string | number, targetId: string | number,
version: string,
} }
type Result = * type Result = *
const cmd: Command<Input, Result> = createCommand('listApps', ({ targetId }) => const cmd: Command<Input, Result> = createCommand('listApps', ({ targetId, version }) =>
fromPromise(listApps(targetId)), fromPromise(listApps(targetId, version)),
) )
export default cmd export default cmd

9
src/commands/uninstallApp.js

@ -9,14 +9,17 @@ import uninstallApp from 'helpers/apps/uninstallApp'
import type { LedgerScriptParams } from 'helpers/common' import type { LedgerScriptParams } from 'helpers/common'
type Input = { type Input = {
appParams: LedgerScriptParams, app: LedgerScriptParams,
devicePath: string, devicePath: string,
targetId: string | number,
} }
type Result = * type Result = *
const cmd: Command<Input, Result> = createCommand('uninstallApp', ({ devicePath, ...rest }) => const cmd: Command<Input, Result> = createCommand(
fromPromise(withDevice(devicePath)(transport => uninstallApp(transport, rest))), 'uninstallApp',
({ devicePath, targetId, ...rest }) =>
fromPromise(withDevice(devicePath)(transport => uninstallApp(transport, targetId, rest))),
) )
export default cmd export default cmd

12
src/components/AccountPage/index.js

@ -129,11 +129,9 @@ class AccountPage extends PureComponent<Props> {
)} )}
<Tooltip render={() => t('app:account.settings.title')}> <Tooltip render={() => t('app:account.settings.title')}>
<ButtonSettings onClick={() => openModal(MODAL_SETTINGS_ACCOUNT, { account })}> <ButtonSettings onClick={() => openModal(MODAL_SETTINGS_ACCOUNT, { account })}>
<Button small outlineGrey>
<Box justifyContent="center"> <Box justifyContent="center">
<IconAccountSettings size={16} /> <IconAccountSettings size={16} />
</Box> </Box>
</Button>
</ButtonSettings> </ButtonSettings>
</Tooltip> </Tooltip>
</Box> </Box>
@ -148,10 +146,14 @@ class AccountPage extends PureComponent<Props> {
counterValue={counterValue} counterValue={counterValue}
daysCount={daysCount} daysCount={daysCount}
selectedTimeRange={selectedTimeRange} selectedTimeRange={selectedTimeRange}
renderHeader={({ totalBalance, sinceBalance, refBalance }) => ( renderHeader={({ isAvailable, totalBalance, sinceBalance, refBalance }) => (
<Box flow={4} mb={2}> <Box flow={4} mb={2}>
<Box horizontal> <Box horizontal>
<BalanceTotal totalBalance={account.balance} unit={account.unit}> <BalanceTotal
isAvailable={isAvailable}
totalBalance={account.balance}
unit={account.unit}
>
<FormattedVal <FormattedVal
animateTicker animateTicker
alwaysShowSign={false} alwaysShowSign={false}
@ -171,6 +173,7 @@ class AccountPage extends PureComponent<Props> {
</Box> </Box>
<Box horizontal justifyContent="center" flow={7}> <Box horizontal justifyContent="center" flow={7}>
<BalanceSincePercent <BalanceSincePercent
isAvailable={isAvailable}
t={t} t={t}
alignItems="center" alignItems="center"
totalBalance={totalBalance} totalBalance={totalBalance}
@ -179,6 +182,7 @@ class AccountPage extends PureComponent<Props> {
since={selectedTimeRange} since={selectedTimeRange}
/> />
<BalanceSinceDiff <BalanceSinceDiff
isAvailable={isAvailable}
t={t} t={t}
counterValue={counterValue} counterValue={counterValue}
alignItems="center" alignItems="center"

23
src/components/AppError.js

@ -0,0 +1,23 @@
// @flow
import React from 'react'
import { ThemeProvider } from 'styled-components'
import { I18nextProvider } from 'react-i18next'
import theme from 'styles/theme'
import i18n from 'renderer/i18n/electron'
import TriggerAppReady from './TriggerAppReady'
import RenderError from './RenderError'
// Like App except it just render an error
const App = ({ language, error }: { error: Error, language: string }) => (
<I18nextProvider i18n={i18n} initialLanguage={language}>
<ThemeProvider theme={theme}>
<RenderError disableExport error={error}>
<TriggerAppReady />
</RenderError>
</ThemeProvider>
</I18nextProvider>
)
export default App

41
src/components/BalanceSummary/BalanceInfos.js

@ -9,6 +9,7 @@ import type { T } from 'types/common'
import Box from 'components/base/Box' import Box from 'components/base/Box'
import FormattedVal from 'components/base/FormattedVal' import FormattedVal from 'components/base/FormattedVal'
import DeltaChange from '../DeltaChange' import DeltaChange from '../DeltaChange'
import { PlaceholderLine } from '../Placeholder'
const Sub = styled(Box).attrs({ const Sub = styled(Box).attrs({
ff: 'Open Sans', ff: 'Open Sans',
@ -22,12 +23,14 @@ type BalanceSinceProps = {
totalBalance: number, totalBalance: number,
sinceBalance: number, sinceBalance: number,
refBalance: number, refBalance: number,
isAvailable: boolean,
t: T, t: T,
} }
type BalanceTotalProps = { type BalanceTotalProps = {
children?: any, children?: any,
unit: Unit, unit: Unit,
isAvailable: boolean,
totalBalance: number, totalBalance: number,
} }
@ -36,9 +39,12 @@ type Props = {
} & BalanceSinceProps } & BalanceSinceProps
export function BalanceSincePercent(props: BalanceSinceProps) { export function BalanceSincePercent(props: BalanceSinceProps) {
const { t, totalBalance, sinceBalance, refBalance, since, ...otherProps } = props const { t, totalBalance, sinceBalance, refBalance, since, isAvailable, ...otherProps } = props
return ( return (
<Box {...otherProps}> <Box {...otherProps}>
{!isAvailable ? (
<PlaceholderLine width={100} />
) : (
<DeltaChange <DeltaChange
from={refBalance} from={refBalance}
to={totalBalance} to={totalBalance}
@ -47,15 +53,23 @@ export function BalanceSincePercent(props: BalanceSinceProps) {
fontSize={7} fontSize={7}
withIcon withIcon
/> />
)}
{!isAvailable ? (
<PlaceholderLine dark width={60} />
) : (
<Sub>{t(`app:time.since.${since}`)}</Sub> <Sub>{t(`app:time.since.${since}`)}</Sub>
)}
</Box> </Box>
) )
} }
export function BalanceSinceDiff(props: Props) { export function BalanceSinceDiff(props: Props) {
const { t, totalBalance, sinceBalance, since, counterValue, ...otherProps } = props const { t, totalBalance, sinceBalance, since, counterValue, isAvailable, ...otherProps } = props
return ( return (
<Box {...otherProps}> <Box {...otherProps}>
{!isAvailable ? (
<PlaceholderLine width={100} />
) : (
<FormattedVal <FormattedVal
color="dark" color="dark"
animateTicker animateTicker
@ -65,15 +79,23 @@ export function BalanceSinceDiff(props: Props) {
val={totalBalance - sinceBalance} val={totalBalance - sinceBalance}
withIcon withIcon
/> />
)}
{!isAvailable ? (
<PlaceholderLine dark width={60} />
) : (
<Sub>{t(`app:time.since.${since}`)}</Sub> <Sub>{t(`app:time.since.${since}`)}</Sub>
)}
</Box> </Box>
) )
} }
export function BalanceTotal(props: BalanceTotalProps) { export function BalanceTotal(props: BalanceTotalProps) {
const { unit, totalBalance, children } = props const { unit, totalBalance, isAvailable, children } = props
return ( return (
<Box grow {...props}> <Box grow {...props}>
{!isAvailable ? (
<PlaceholderLine width={150} />
) : (
<FormattedVal <FormattedVal
animateTicker animateTicker
color="dark" color="dark"
@ -82,7 +104,8 @@ export function BalanceTotal(props: BalanceTotalProps) {
showCode showCode
val={totalBalance} val={totalBalance}
/> />
{children} )}
{!isAvailable ? <PlaceholderLine dark width={50} /> : children}
</Box> </Box>
) )
} }
@ -93,16 +116,21 @@ BalanceTotal.defaultProps = {
} }
function BalanceInfos(props: Props) { function BalanceInfos(props: Props) {
const { t, totalBalance, since, sinceBalance, refBalance, counterValue } = props const { t, totalBalance, since, sinceBalance, refBalance, isAvailable, counterValue } = props
return ( return (
<Box horizontal alignItems="center" flow={7}> <Box horizontal alignItems="center" flow={7}>
<BalanceTotal unit={counterValue.units[0]} totalBalance={totalBalance}> <BalanceTotal
unit={counterValue.units[0]}
isAvailable={isAvailable}
totalBalance={totalBalance}
>
<Sub>{t('app:dashboard.totalBalance')}</Sub> <Sub>{t('app:dashboard.totalBalance')}</Sub>
</BalanceTotal> </BalanceTotal>
<BalanceSincePercent <BalanceSincePercent
alignItems="flex-end" alignItems="flex-end"
totalBalance={totalBalance} totalBalance={totalBalance}
sinceBalance={sinceBalance} sinceBalance={sinceBalance}
isAvailable={isAvailable}
refBalance={refBalance} refBalance={refBalance}
since={since} since={since}
t={t} t={t}
@ -110,6 +138,7 @@ function BalanceInfos(props: Props) {
<BalanceSinceDiff <BalanceSinceDiff
counterValue={counterValue} counterValue={counterValue}
alignItems="flex-end" alignItems="flex-end"
isAvailable={isAvailable}
totalBalance={totalBalance} totalBalance={totalBalance}
sinceBalance={sinceBalance} sinceBalance={sinceBalance}
refBalance={refBalance} refBalance={refBalance}

28
src/components/BalanceSummary/index.js

@ -21,6 +21,7 @@ type Props = {
totalBalance: number, totalBalance: number,
sinceBalance: number, sinceBalance: number,
refBalance: number, refBalance: number,
isAvailable: boolean,
}) => *, }) => *,
} }
@ -37,12 +38,12 @@ const BalanceSummary = ({
return ( return (
<Card p={0} py={5}> <Card p={0} py={5}>
<CalculateBalance accounts={accounts} daysCount={daysCount}> <CalculateBalance accounts={accounts} daysCount={daysCount}>
{({ isAvailable, balanceHistory, balanceStart, balanceEnd }) => {({ isAvailable, balanceHistory, balanceStart, balanceEnd }) => (
!isAvailable ? null : (
<Fragment> <Fragment>
{renderHeader ? ( {renderHeader ? (
<Box px={6}> <Box px={6}>
{renderHeader({ {renderHeader({
isAvailable,
selectedTimeRange, selectedTimeRange,
// FIXME refactor these // FIXME refactor these
totalBalance: balanceEnd, totalBalance: balanceEnd,
@ -55,12 +56,26 @@ const BalanceSummary = ({
<Chart <Chart
id={chartId} id={chartId}
unit={account ? account.unit : null} unit={account ? account.unit : null}
color={chartColor} color={!isAvailable ? '#eee' : chartColor}
data={balanceHistory} data={
isAvailable
? balanceHistory
: balanceHistory.map(i => ({
...i,
value:
10000 *
(1 +
0.1 * Math.sin(i.date * Math.cos(i.date)) + // random-ish
0.5 * Math.cos(i.date / 2000000000 + Math.sin(i.date / 1000000000))), // general curve trend
}))
}
height={200} height={200}
currency={counterValue} currency={counterValue}
tickXScale={selectedTimeRange} tickXScale={selectedTimeRange}
renderTickY={val => formatShort(counterValue.units[0], val)} renderTickY={
isAvailable ? val => formatShort(counterValue.units[0], val) : () => ''
}
isInteractive={isAvailable}
renderTooltip={ renderTooltip={
isAvailable && !account isAvailable && !account
? d => ( ? d => (
@ -83,8 +98,7 @@ const BalanceSummary = ({
/> />
</Box> </Box>
</Fragment> </Fragment>
) )}
}
</CalculateBalance> </CalculateBalance>
</Card> </Card>
) )

9
src/components/DashboardPage/index.js

@ -120,10 +120,17 @@ class DashboardPage extends PureComponent<Props> {
accounts={accounts} accounts={accounts}
selectedTimeRange={selectedTimeRange} selectedTimeRange={selectedTimeRange}
daysCount={daysCount} daysCount={daysCount}
renderHeader={({ totalBalance, selectedTimeRange, sinceBalance, refBalance }) => ( renderHeader={({
isAvailable,
totalBalance,
selectedTimeRange,
sinceBalance,
refBalance,
}) => (
<BalanceInfos <BalanceInfos
t={t} t={t}
counterValue={counterValue} counterValue={counterValue}
isAvailable={isAvailable}
totalBalance={totalBalance} totalBalance={totalBalance}
since={selectedTimeRange} since={selectedTimeRange}
sinceBalance={sinceBalance} sinceBalance={sinceBalance}

38
src/components/DeviceBusyIndicator.js

@ -0,0 +1,38 @@
import React, { PureComponent } from 'react'
import styled from 'styled-components'
const Indicator = styled.div`
opacity: ${p => (p.busy ? 0.1 : 0)};
width: 6px;
height: 6px;
border-radius: 3px;
background-color: black;
position: fixed;
top: 4px;
right: 4px;
z-index: 999;
`
// NB this is done like this to be extremely performant. we don't want redux for this..
const perPaths = {}
const instances = []
export const onSetDeviceBusy = (path, busy) => {
perPaths[path] = busy
instances.forEach(i => i.forceUpdate())
}
class DeviceBusyIndicator extends PureComponent<{}> {
componentDidMount() {
instances.push(this)
}
componentWillUnmount() {
const i = instances.indexOf(this)
instances.splice(i, 1)
}
render() {
const busy = Object.values(perPaths).reduce((busy, b) => busy || b, false)
return <Indicator busy={busy} />
}
}
export default DeviceBusyIndicator

37
src/components/LibcoreBusyIndicator.js

@ -0,0 +1,37 @@
import React, { PureComponent } from 'react'
import styled from 'styled-components'
const Indicator = styled.div`
opacity: ${p => (p.busy ? 0.1 : 0)};
width: 6px;
height: 6px;
border-radius: 3px;
background-color: black;
position: fixed;
bottom: 4px;
right: 4px;
z-index: 999;
`
// NB this is done like this to be extremely performant. we don't want redux for this..
let busy = false
const instances = []
export const onSetLibcoreBusy = b => {
busy = b
instances.forEach(i => i.forceUpdate())
}
class LibcoreBusyIndicator extends PureComponent<{}> {
componentDidMount() {
instances.push(this)
}
componentWillUnmount() {
const i = instances.indexOf(this)
instances.splice(i, 1)
}
render() {
return <Indicator busy={busy} />
}
}
export default LibcoreBusyIndicator

17
src/components/ManagerPage/AppSearchBar.js

@ -4,6 +4,8 @@ import styled from 'styled-components'
import { color, fontSize, space } from 'styled-system' import { color, fontSize, space } from 'styled-system'
import fontFamily from 'styles/styled/fontFamily' import fontFamily from 'styles/styled/fontFamily'
import type { LedgerScriptParams } from 'helpers/common'
import { ff } from 'styles/helpers' import { ff } from 'styles/helpers'
import Box from 'components/base/Box' import Box from 'components/base/Box'
@ -12,20 +14,9 @@ import Search from 'components/base/Search'
import SearchIcon from 'icons/Search' import SearchIcon from 'icons/Search'
import CrossIcon from 'icons/Cross' import CrossIcon from 'icons/Cross'
type LedgerApp = {
name: string,
version: string,
icon: string,
app: Object,
bolos_version: {
min: number,
max: number,
},
}
type Props = { type Props = {
list: Array<LedgerApp>, list: Array<LedgerScriptParams>,
children: (list: Array<LedgerApp>) => React$Node, children: (list: Array<LedgerScriptParams>) => React$Node,
} }
type State = { type State = {

41
src/components/ManagerPage/AppsList.js

@ -6,6 +6,7 @@ import styled from 'styled-components'
import { translate } from 'react-i18next' import { translate } from 'react-i18next'
import type { Device, T } from 'types/common' import type { Device, T } from 'types/common'
import type { LedgerScriptParams } from 'helpers/common'
import listApps from 'commands/listApps' import listApps from 'commands/listApps'
import installApp from 'commands/installApp' import installApp from 'commands/installApp'
@ -43,27 +44,17 @@ const ICONS_FALLBACK = {
type Status = 'loading' | 'idle' | 'busy' | 'success' | 'error' type Status = 'loading' | 'idle' | 'busy' | 'success' | 'error'
type Mode = 'home' | 'installing' | 'uninstalling' type Mode = 'home' | 'installing' | 'uninstalling'
type LedgerApp = {
name: string,
version: string,
icon: string,
app: Object,
bolos_version: {
min: number,
max: number,
},
}
type Props = { type Props = {
device: Device, device: Device,
targetId: string | number, targetId: string | number,
t: T, t: T,
version: string,
} }
type State = { type State = {
status: Status, status: Status,
error: string | null, error: string | null,
appsList: LedgerApp[], appsList: LedgerScriptParams[] | Array<*>,
app: string, app: string,
mode: Mode, mode: Mode,
} }
@ -89,8 +80,8 @@ class AppsList extends PureComponent<Props, State> {
async fetchAppList() { async fetchAppList() {
try { try {
const { targetId } = this.props const { targetId, version } = this.props
const appsList = CACHED_APPS || (await listApps.send({ targetId }).toPromise()) const appsList = CACHED_APPS || (await listApps.send({ targetId, version }).toPromise())
CACHED_APPS = appsList CACHED_APPS = appsList
if (!this._unmounted) { if (!this._unmounted) {
this.setState({ appsList, status: 'idle' }) this.setState({ appsList, status: 'idle' })
@ -100,14 +91,14 @@ class AppsList extends PureComponent<Props, State> {
} }
} }
handleInstallApp = (args: { app: any, name: string }) => async () => { handleInstallApp = (app: LedgerScriptParams) => async () => {
const { app: appParams, name } = args this.setState({ status: 'busy', app: app.name, mode: 'installing' })
this.setState({ status: 'busy', app: name, mode: 'installing' })
try { try {
const { const {
device: { path: devicePath }, device: { path: devicePath },
targetId,
} = this.props } = this.props
const data = { appParams, devicePath } const data = { app, devicePath, targetId }
await installApp.send(data).toPromise() await installApp.send(data).toPromise()
this.setState({ status: 'success', app: '' }) this.setState({ status: 'success', app: '' })
} catch (err) { } catch (err) {
@ -115,14 +106,14 @@ class AppsList extends PureComponent<Props, State> {
} }
} }
handleUninstallApp = (args: { app: any, name: string }) => async () => { handleUninstallApp = (app: LedgerScriptParams) => async () => {
const { app: appParams, name } = args this.setState({ status: 'busy', app: app.name, mode: 'uninstalling' })
this.setState({ status: 'busy', app: name, mode: 'uninstalling' })
try { try {
const { const {
device: { path: devicePath }, device: { path: devicePath },
targetId,
} = this.props } = this.props
const data = { appParams, devicePath } const data = { app, devicePath, targetId }
await uninstallApp.send(data).toPromise() await uninstallApp.send(data).toPromise()
this.setState({ status: 'success', app: '' }) this.setState({ status: 'success', app: '' })
} catch (err) { } catch (err) {
@ -184,15 +175,15 @@ class AppsList extends PureComponent<Props, State> {
} }
renderList() { renderList() {
const { appsList } = this.state const { appsList, status } = this.state
return appsList.length > 0 ? ( return status === 'idle' ? (
<Box> <Box>
<AppSearchBar list={appsList}> <AppSearchBar list={appsList}>
{items => ( {items => (
<List> <List>
{items.map(c => ( {items.map(c => (
<ManagerApp <ManagerApp
key={`${c.name}_${c.version}_${c.bolos_version.min}`} key={`${c.name}_${c.version}`}
name={c.name} name={c.name}
version={`Version ${c.version}`} version={`Version ${c.version}`}
icon={ICONS_FALLBACK[c.icon] || c.icon} icon={ICONS_FALLBACK[c.icon] || c.icon}

2
src/components/ManagerPage/Dashboard.js

@ -43,7 +43,7 @@ const Dashboard = ({ device, deviceInfo, t }: Props) => (
/> />
</Box> </Box>
<Box mt={5}> <Box mt={5}>
<AppsList device={device} targetId={deviceInfo.targetId} /> <AppsList device={device} targetId={deviceInfo.targetId} version={deviceInfo.version} />
</Box> </Box>
</Box> </Box>
) )

16
src/components/ManagerPage/FirmwareUpdate.js

@ -10,6 +10,8 @@ import logger from 'logger'
import type { Device, T } from 'types/common' import type { Device, T } from 'types/common'
import type { LedgerScriptParams } from 'helpers/common'
import getLatestFirmwareForDevice from 'commands/getLatestFirmwareForDevice' import getLatestFirmwareForDevice from 'commands/getLatestFirmwareForDevice'
import installOsuFirmware from 'commands/installOsuFirmware' import installOsuFirmware from 'commands/installOsuFirmware'
@ -23,11 +25,6 @@ import UpdateFirmwareButton from './UpdateFirmwareButton'
let CACHED_LATEST_FIRMWARE = null let CACHED_LATEST_FIRMWARE = null
type FirmwareInfos = {
name: string,
notes: string,
}
type DeviceInfos = { type DeviceInfos = {
targetId: number | string, targetId: number | string,
version: string, version: string,
@ -40,7 +37,7 @@ type Props = {
} }
type State = { type State = {
latestFirmware: ?FirmwareInfos, latestFirmware: ?LedgerScriptParams,
} }
class FirmwareUpdate extends PureComponent<Props, State> { class FirmwareUpdate extends PureComponent<Props, State> {
@ -84,12 +81,13 @@ class FirmwareUpdate extends PureComponent<Props, State> {
installFirmware = async () => { installFirmware = async () => {
try { try {
const { latestFirmware } = this.state const { latestFirmware } = this.state
const { infos } = this.props
invariant(latestFirmware, 'did not find a new firmware or firmware is not set') invariant(latestFirmware, 'did not find a new firmware or firmware is not set')
const { const {
device: { path: devicePath }, device: { path: devicePath },
} = this.props } = this.props
const { success } = await installOsuFirmware const { success } = await installOsuFirmware
.send({ devicePath, firmware: latestFirmware }) .send({ devicePath, firmware: latestFirmware, targetId: infos.targetId })
.toPromise() .toPromise()
if (success) { if (success) {
this.fetchLatestFirmware() this.fetchLatestFirmware()
@ -119,7 +117,9 @@ class FirmwareUpdate extends PureComponent<Props, State> {
</Box> </Box>
</Box> </Box>
<Text ff="Open Sans|SemiBold" fontSize={2}> <Text ff="Open Sans|SemiBold" fontSize={2}>
{t('app:manager.firmware.installed', { version: infos.version })} {t('app:manager.firmware.installed', {
version: infos.version,
})}
</Text> </Text>
</Box> </Box>
<UpdateFirmwareButton firmware={latestFirmware} installFirmware={this.installFirmware} /> <UpdateFirmwareButton firmware={latestFirmware} installFirmware={this.installFirmware} />

55
src/components/ManagerPage/FlashMcu.js

@ -0,0 +1,55 @@
// @flow
import React, { PureComponent } from 'react'
import type { Device } from 'types/common'
import installMcu from 'commands/installMcu'
type DeviceInfo = {
targetId: number | string,
version: string,
final: boolean,
mcu: boolean,
}
type Props = {
device: Device,
deviceInfo: DeviceInfo,
}
type State = {
flashing: boolean,
}
class FlashMcu extends PureComponent<Props, State> {
state = {
flashing: false,
}
flashMCU = async () => {
const { device, deviceInfo } = this.props
const { flashing } = this.state
if (!flashing) {
this.setState({ flashing: true })
await installMcu
.send({
devicePath: device.path,
targetId: deviceInfo.targetId,
version: deviceInfo.version,
})
.toPromise()
this.setState({ flashing: false })
}
}
render() {
return (
<div>
<h1>Flashing MCU</h1>
<button onClick={this.flashMCU}>flash</button>
</div>
)
}
}
export default FlashMcu

5
src/components/ManagerPage/UpdateFirmwareButton.js

@ -18,11 +18,14 @@ type Props = {
installFirmware: () => void, installFirmware: () => void,
} }
const getCleanVersion = (input: string): string =>
input.endsWith('-osu') ? input.replace('-osu', '') : input
const UpdateFirmwareButton = ({ t, firmware, installFirmware }: Props) => const UpdateFirmwareButton = ({ t, firmware, installFirmware }: Props) =>
firmware ? ( firmware ? (
<Fragment> <Fragment>
<Text ff="Open Sans|Regular" fontSize={4} style={{ marginLeft: 'auto', marginRight: 15 }}> <Text ff="Open Sans|Regular" fontSize={4} style={{ marginLeft: 'auto', marginRight: 15 }}>
{t('app:manager.firmware.latest', { version: firmware.name })} {t('app:manager.firmware.latest', { version: getCleanVersion(firmware.name) })}
</Text> </Text>
<Button primary onClick={installFirmware}> <Button primary onClick={installFirmware}>
{t('app:manager.firmware.update')} {t('app:manager.firmware.update')}

7
src/components/ManagerPage/index.js

@ -9,6 +9,7 @@ import type { Device } from 'types/common'
import Workflow from 'components/Workflow' import Workflow from 'components/Workflow'
import WorkflowWithIcon from 'components/Workflow/WorkflowWithIcon' import WorkflowWithIcon from 'components/Workflow/WorkflowWithIcon'
import Dashboard from './Dashboard' import Dashboard from './Dashboard'
import FlashMcu from './FlashMcu'
type DeviceInfo = { type DeviceInfo = {
targetId: number | string, targetId: number | string,
@ -25,11 +26,11 @@ type Error = {
function ManagerPage(): Node { function ManagerPage(): Node {
return ( return (
<Workflow <Workflow
renderFinalUpdate={(deviceInfo: DeviceInfo) => ( renderFinalUpdate={(device: Device, deviceInfo: DeviceInfo) => (
<p>UPDATE FINAL FIRMARE (TEMPLATE + ACTION WIP) {deviceInfo.final}</p> <p>UPDATE FINAL FIRMARE (TEMPLATE + ACTION WIP) {deviceInfo.final}</p>
)} )}
renderMcuUpdate={(deviceInfo: DeviceInfo) => ( renderMcuUpdate={(device: Device, deviceInfo: DeviceInfo) => (
<p>FLASH MCU (TEMPLATE + ACTION WIP) {deviceInfo.mcu}</p> <FlashMcu device={device} deviceInfo={deviceInfo} />
)} )}
renderDashboard={(device: Device, deviceInfo: DeviceInfo) => ( renderDashboard={(device: Device, deviceInfo: DeviceInfo) => (
<Dashboard device={device} deviceInfo={deviceInfo} /> <Dashboard device={device} deviceInfo={deviceInfo} />

8
src/components/Onboarding/OnboardingBreadcrumb.js

@ -3,6 +3,7 @@
import React from 'react' import React from 'react'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import findIndex from 'lodash/findIndex' import findIndex from 'lodash/findIndex'
import { translate } from 'react-i18next'
import type { OnboardingState } from 'reducers/onboarding' import type { OnboardingState } from 'reducers/onboarding'
@ -14,15 +15,16 @@ const mapStateToProps = state => ({
type Props = { type Props = {
onboarding: OnboardingState, onboarding: OnboardingState,
t: *,
} }
function OnboardingBreadcrumb(props: Props) { function OnboardingBreadcrumb(props: Props) {
const { onboarding } = props const { onboarding, t } = props
const { stepName, genuine } = onboarding const { stepName, genuine } = onboarding
const filteredSteps = onboarding.steps const filteredSteps = onboarding.steps
.filter(step => !step.external) .filter(step => !step.external)
.map(step => ({ ...step, label: step.label })) // TODO: translate .map(step => ({ ...step, label: t(step.label) })) // TODO: translate
const stepIndex = findIndex(filteredSteps, s => s.name === stepName) const stepIndex = findIndex(filteredSteps, s => s.name === stepName)
const genuineStepIndex = findIndex(filteredSteps, s => s.name === 'genuineCheck') const genuineStepIndex = findIndex(filteredSteps, s => s.name === 'genuineCheck')
@ -36,4 +38,4 @@ function OnboardingBreadcrumb(props: Props) {
) )
} }
export default connect(mapStateToProps)(OnboardingBreadcrumb) export default translate()(connect(mapStateToProps)(OnboardingBreadcrumb))

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

@ -82,10 +82,12 @@ class GenuineCheck extends PureComponent<StepProps, State> {
if (!item.pass) { if (!item.pass) {
this.setState(INITIAL_STATE) this.setState(INITIAL_STATE)
this.props.updateGenuineCheck({ this.props.updateGenuineCheck({
isGenuineFail: true, displayErrorScreen: true,
recoveryStepPass: false,
pinStepPass: false, pinStepPass: false,
recoveryStepPass: false,
isGenuineFail: false,
isDeviceGenuine: false, isDeviceGenuine: false,
genuineCheckUnavailable: null,
}) })
} }
} }
@ -116,6 +118,7 @@ class GenuineCheck extends PureComponent<StepProps, State> {
isGenuineFail: true, isGenuineFail: true,
isDeviceGenuine: false, isDeviceGenuine: false,
genuineCheckUnavailable: null, genuineCheckUnavailable: null,
displayErrorScreen: true,
}) })
}) })
} }
@ -125,12 +128,13 @@ class GenuineCheck extends PureComponent<StepProps, State> {
this.props.updateGenuineCheck({ this.props.updateGenuineCheck({
isDeviceGenuine: false, isDeviceGenuine: false,
genuineCheckUnavailable: error, genuineCheckUnavailable: error,
displayErrorScreen: false,
}) })
}) })
} }
redoGenuineCheck = () => { redoGenuineCheck = () => {
this.props.updateGenuineCheck({ isGenuineFail: false }) this.props.updateGenuineCheck({ displayErrorScreen: false })
} }
contactSupport = () => { contactSupport = () => {
@ -153,7 +157,7 @@ class GenuineCheck extends PureComponent<StepProps, State> {
const { genuine } = onboarding const { genuine } = onboarding
const { cachedPinStepButton, cachedRecoveryStepButton, isGenuineCheckModalOpened } = this.state const { cachedPinStepButton, cachedRecoveryStepButton, isGenuineCheckModalOpened } = this.state
if (genuine.isGenuineFail) { if (genuine.displayErrorScreen) {
return this.renderGenuineFail() return this.renderGenuineFail()
} }
@ -225,17 +229,22 @@ class GenuineCheck extends PureComponent<StepProps, State> {
</GenuineSuccessText> </GenuineSuccessText>
</Box> </Box>
) : genuine.genuineCheckUnavailable ? ( ) : genuine.genuineCheckUnavailable ? (
<Box horizontal align="center" flow={1} color={colors.alertRed}> <Box align="center" flow={1} color={colors.alertRed}>
<IconCross size={16} />
<Box ff="Open Sans|Regular" fontSize={4} style={{ maxWidth: '200px' }}>
<TranslatedError error={genuine.genuineCheckUnavailable} />
<FakeLink <FakeLink
color="alertRed" ff="Open Sans|Regular"
fontSize={4}
underline underline
onClick={this.handleOpenGenuineCheckModal} onClick={this.handleOpenGenuineCheckModal}
> >
{t('app:common.retry')} {t('app:common.retry')}
</FakeLink> </FakeLink>
<Box horizontal justify="center">
<Box justifyContent="center">
<IconCross size={12} />
</Box>
<Box ff="Open Sans|Regular" fontSize={2} ml={1}>
<TranslatedError error={genuine.genuineCheckUnavailable} />
</Box>
</Box> </Box>
</Box> </Box>
) : ( ) : (
@ -252,6 +261,21 @@ class GenuineCheck extends PureComponent<StepProps, State> {
</CardWrapper> </CardWrapper>
</Box> </Box>
</StepContainerInner> </StepContainerInner>
{genuine.genuineCheckUnavailable ? (
<OnboardingFooterWrapper>
<Button padded outlineGrey onClick={() => prevStep()}>
{t('app:common.back')}
</Button>
<Box horizontal ml="auto">
<Button padded disabled={false} onClick={() => nextStep()} mx={2}>
{t('app:common.skipThisStep')}
</Button>
<Button padded onClick={nextStep} disabled primary>
{t('app:common.continue')}
</Button>
</Box>
</OnboardingFooterWrapper>
) : (
<OnboardingFooter <OnboardingFooter
horizontal horizontal
align="center" align="center"
@ -261,6 +285,8 @@ class GenuineCheck extends PureComponent<StepProps, State> {
prevStep={prevStep} prevStep={prevStep}
isContinueDisabled={!genuine.isDeviceGenuine} isContinueDisabled={!genuine.isDeviceGenuine}
/> />
)}
<GenuineCheckModal <GenuineCheckModal
isOpened={isGenuineCheckModalOpened} isOpened={isGenuineCheckModalOpened}
onClose={this.handleCloseGenuineCheckModal} onClose={this.handleCloseGenuineCheckModal}

4
src/components/Onboarding/steps/WriteSeed/WriteSeedBlue.js

@ -69,8 +69,8 @@ class WriteSeedBlue extends PureComponent<Props, *> {
return ( return (
<Fragment> <Fragment>
<Box mb={3}> <Box mb={3}>
<Title>{t('onboarding:writeSeed.blue.title')}</Title> <Title>{t('onboarding:writeSeed.title')}</Title>
<Description>{t('onboarding:writeSeed.blue.desc')}</Description> <Description>{t('onboarding:writeSeed.desc')}</Description>
</Box> </Box>
<Box align="center"> <Box align="center">
<Inner style={{ width: 760 }}> <Inner style={{ width: 760 }}>

4
src/components/Onboarding/steps/WriteSeed/WriteSeedNano.js

@ -69,8 +69,8 @@ class WriteSeedNano extends PureComponent<Props, *> {
return ( return (
<Fragment> <Fragment>
<Box mb={3}> <Box mb={3}>
<Title>{t('onboarding:writeSeed.nano.title')}</Title> <Title>{t('onboarding:writeSeed.title')}</Title>
<Description>{t('onboarding:writeSeed.nano.desc')}</Description> <Description>{t('onboarding:writeSeed.desc')}</Description>
</Box> </Box>
<Box align="center" mt={3}> <Box align="center" mt={3}>
<Inner style={{ width: 700 }}> <Inner style={{ width: 700 }}>

2
src/components/OperationsList/index.js

@ -119,7 +119,7 @@ export class OperationsList extends PureComponent<Props, State> {
<OperationC <OperationC
operation={operation} operation={operation}
account={account} account={account}
key={operation.id} key={`${account.id}_${operation.id}`}
onOperationClick={this.handleClickOperation} onOperationClick={this.handleClickOperation}
t={t} t={t}
withAccount={withAccount} withAccount={withAccount}

9
src/components/Placeholder.js

@ -0,0 +1,9 @@
import styled from 'styled-components'
export const PlaceholderLine = styled.div`
background-color: ${p => (p.dark ? '#C2C2C2' : '#D6D6D6')};
width: ${p => p.width}px;
height: 10px;
border-radius: 5px;
margin: 5px 0;
`

131
src/components/RenderError.js

@ -0,0 +1,131 @@
// @flow
import React, { PureComponent } from 'react'
import { shell, remote } from 'electron'
import qs from 'querystring'
import { translate } from 'react-i18next'
import { i } from 'helpers/staticPath'
import hardReset from 'helpers/hardReset'
import type { T } from 'types/common'
import Spoiler from 'components/base/Spoiler'
import ExportLogsBtn from 'components/ExportLogsBtn'
import Box from 'components/base/Box'
import Space from 'components/base/Space'
import Button from 'components/base/Button'
import TranslatedError from './TranslatedError'
type Props = {
error: Error,
t: T,
disableExport?: boolean,
children?: *,
}
class RenderError extends PureComponent<Props, { isHardResetting: boolean }> {
state = {
isHardResetting: false,
}
handleCreateIssue = () => {
const { error } = this.props
if (!error) {
return
}
const q = qs.stringify({
title: `Error: ${error.message}`,
body: `Error was thrown:
\`\`\`
${error.stack}
\`\`\`
`,
})
shell.openExternal(`https://github.com/LedgerHQ/ledger-live-desktop/issues/new?${q}`)
}
handleRestart = () => {
remote.getCurrentWindow().webContents.reloadIgnoringCache()
}
handleHardReset = async () => {
this.setState({ isHardResetting: true })
try {
await hardReset()
remote.getCurrentWindow().webContents.reloadIgnoringCache()
} catch (err) {
this.setState({ isHardResetting: false })
}
}
render() {
const { error, t, disableExport, children } = this.props
const { isHardResetting } = this.state
return (
<Box align="center" grow>
<Space of={100} />
<img alt="" src={i('crash-screen.svg')} width={380} />
<Space of={40} />
<Box ff="Museo Sans|Regular" fontSize={7} color="dark">
{t('app:crash.oops')}
</Box>
<Space of={15} />
<Box
style={{ width: 500 }}
textAlign="center"
ff="Open Sans|Regular"
color="smoke"
fontSize={4}
>
{t('app:crash.uselessText')}
</Box>
<Space of={30} />
<Box horizontal flow={2}>
<Button primary onClick={this.handleRestart}>
{t('app:crash.restart')}
</Button>
{!disableExport ? <ExportLogsBtn /> : null}
<Button primary onClick={this.handleCreateIssue}>
{t('app:crash.createTicket')}
</Button>
<Button danger onClick={this.handleHardReset} isLoading={isHardResetting}>
{t('app:crash.reset')}
</Button>
</Box>
<Space of={20} />
<Spoiler color="wallet" title={t('app:crash.showError')}>
<ErrContainer>
<TranslatedError error={error} />
</ErrContainer>
</Spoiler>
<Space of={10} />
<Spoiler color="wallet" title={t('app:crash.showDetails')}>
<ErrContainer>{error.stack}</ErrContainer>
</Spoiler>
<Space of={100} />
{children}
</Box>
)
}
}
const ErrContainer = ({ children }: { children: any }) => (
<pre
style={{
marginTop: 10,
maxWidth: '80%',
overflow: 'auto',
fontSize: 10,
fontFamily: 'monospace',
background: 'rgba(0, 0, 0, 0.05)',
cursor: 'text',
userSelect: 'text',
}}
>
{children}
</pre>
)
export default translate()(RenderError)

3
src/components/SelectAccount/index.js

@ -69,6 +69,9 @@ const RawSelectAccount = ({ accounts, onChange, value, t, ...props }: Props) =>
renderValue={renderOption} renderValue={renderOption}
renderOption={renderOption} renderOption={renderOption}
placeholder={t('app:common.selectAccount')} placeholder={t('app:common.selectAccount')}
noOptionsMessage={({ inputValue }) =>
t('app:common.selectAccountNoOption', { accountName: inputValue })
}
onChange={onChange} onChange={onChange}
/> />
) )

3
src/components/SelectCurrency/index.js

@ -40,6 +40,9 @@ const SelectCurrency = ({ onChange, value, t, placeholder, currencies, ...props
renderValue={renderOption} renderValue={renderOption}
options={options} options={options}
placeholder={placeholder || t('app:common.selectCurrency')} placeholder={placeholder || t('app:common.selectCurrency')}
noOptionsMessage={({ inputValue }: { inputValue: string }) =>
t('app:common.selectCurrencyNoOption', { currencyName: inputValue })
}
onChange={item => onChange(item ? item.currency : null)} onChange={item => onChange(item ? item.currency : null)}
{...props} {...props}
/> />

19
src/components/SelectExchange.js

@ -1,6 +1,7 @@
// @flow // @flow
import React, { Component } from 'react' import React, { Component } from 'react'
import { translate } from 'react-i18next' import { translate } from 'react-i18next'
import LRU from 'lru-cache'
import type { Currency } from '@ledgerhq/live-common/lib/types' import type { Currency } from '@ledgerhq/live-common/lib/types'
import type { Exchange } from '@ledgerhq/live-common/lib/countervalues/types' import type { Exchange } from '@ledgerhq/live-common/lib/countervalues/types'
import logger from 'logger' import logger from 'logger'
@ -10,6 +11,18 @@ import Text from 'components/base/Text'
import CounterValues from 'helpers/countervalues' import CounterValues from 'helpers/countervalues'
import type { T } from 'types/common' import type { T } from 'types/common'
const cache = LRU({ max: 100 })
const getExchanges = (from: Currency, to: Currency) => {
const key = `${from.ticker}_${to.ticker}`
let promise = cache.get(key)
if (promise) return promise
promise = CounterValues.fetchExchangesForPair(from, to)
promise.catch(() => cache.del(key)) // if it's a failure, we don't want to keep the cache
cache.set(key, promise)
return promise
}
class SelectExchange extends Component< class SelectExchange extends Component<
{ {
from: Currency, from: Currency,
@ -65,7 +78,7 @@ class SelectExchange extends Component<
const { _loadId } = this const { _loadId } = this
const { from, to } = this.props const { from, to } = this.props
try { try {
const exchanges = await CounterValues.fetchExchangesForPair(from, to) const exchanges = await getExchanges(from, to)
if (!this._unmounted && this._loadId === _loadId) { if (!this._unmounted && this._loadId === _loadId) {
this.setState({ exchanges }) this.setState({ exchanges })
} }
@ -93,6 +106,10 @@ class SelectExchange extends Component<
options={options} options={options}
onChange={onChange} onChange={onChange}
isLoading={options.length === 0} isLoading={options.length === 0}
placeholder={t('app:common.selectExchange')}
noOptionsMessage={({ inputValue }) =>
t('app:common.selectExchangeNoOption', { exchangeName: inputValue })
}
{...props} {...props}
/> />
) )

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

@ -169,7 +169,7 @@ class TabProfile extends PureComponent<Props, State> {
to={counterValueCurrency} to={counterValueCurrency}
exchangeId={counterValueExchange} exchangeId={counterValueExchange}
onChange={this.handleChangeExchange} onChange={this.handleChangeExchange}
minWidth={150} minWidth={200}
/> />
</Box> </Box>
</Row> </Row>

16
src/components/SyncAgo.js

@ -0,0 +1,16 @@
// @flow
import React, { PureComponent } from 'react'
import moment from 'moment'
import { translate } from 'react-i18next'
import type { T } from 'types/common'
import Box from './base/Box'
class SyncAgo extends PureComponent<{ t: T, date: Date }> {
render() {
const { t, date } = this.props
return <Box p={4}>{t('app:common.sync.ago', { time: moment(date).fromNow() })}</Box>
}
}
export default translate()(SyncAgo)

85
src/components/ThrowBlock.js

@ -1,49 +1,16 @@
// @flow // @flow
import logger from 'logger' import logger from 'logger'
import React, { PureComponent } from 'react' import React, { PureComponent } from 'react'
import styled from 'styled-components' import RenderError from 'components/RenderError'
import { shell, remote } from 'electron'
import qs from 'querystring'
import { translate } from 'react-i18next'
import { rgba } from 'styles/helpers'
import db from 'helpers/db'
import type { T } from 'types/common'
import ExportLogsBtn from 'components/ExportLogsBtn'
import Box from 'components/base/Box'
import Button from 'components/base/Button'
import TranslatedError from './TranslatedError'
type Props = { type Props = {
children: any, children: any,
t: T,
} }
type State = { type State = {
error: ?Error, error: ?Error,
} }
const Container = styled(Box).attrs({
grow: true,
align: 'center',
justify: 'center',
bg: 'lightGraphite',
color: 'alertRed',
ff: 'Museo Sans|Bold',
flow: 2,
})``
const Inner = styled(Box).attrs({
p: 2,
bg: p => rgba(p.theme.colors.alertRed, 0.05),
borderRadius: 1,
})`
border: ${p => `1px solid ${rgba(p.theme.colors.alertRed, 0.1)}`};
`
class ThrowBlock extends PureComponent<Props, State> { class ThrowBlock extends PureComponent<Props, State> {
state = { state = {
error: null, error: null,
@ -54,59 +21,13 @@ class ThrowBlock extends PureComponent<Props, State> {
this.setState({ error }) this.setState({ error })
} }
handleCreateIssue = () => {
const { error } = this.state
if (!error) {
return
}
const q = qs.stringify({
title: `Error: ${error.message}`,
body: `Error was thrown:
\`\`\`
${error.stack}
\`\`\`
`,
})
shell.openExternal(`https://github.com/LedgerHQ/ledger-live-desktop/issues/new?${q}`)
}
handleRestart = () => {
remote.app.relaunch()
remote.app.exit()
}
handleReset = () => {
db.resetAll()
this.handleRestart()
}
render() { render() {
const { error } = this.state const { error } = this.state
const { t } = this.props
if (error) { if (error) {
return ( return <RenderError error={error} />
<Container>
<Inner>
<TranslatedError error={error} />
</Inner>
<Box horizontal flow={2}>
<Button primary onClick={this.handleRestart}>
{t('app:crash.restart')}
</Button>
<Button danger onClick={this.handleReset}>
{t('app:crash.reset')}
</Button>
<ExportLogsBtn />
<Button primary onClick={this.handleCreateIssue}>
{t('app:crash.createTicket')}
</Button>
</Box>
</Container>
)
} }
return this.props.children return this.props.children
} }
} }
export default translate()(ThrowBlock) export default ThrowBlock

13
src/components/TopBar/ActivityIndicator.js

@ -10,7 +10,6 @@ import type { T } from 'types/common'
import type { AsyncState } from 'reducers/bridgeSync' import type { AsyncState } from 'reducers/bridgeSync'
import { globalSyncStateSelector } from 'reducers/bridgeSync' import { globalSyncStateSelector } from 'reducers/bridgeSync'
import { hasAccountsSelector } from 'reducers/accounts'
import { BridgeSyncConsumer } from 'bridge/BridgeSyncContext' import { BridgeSyncConsumer } from 'bridge/BridgeSyncContext'
import CounterValues from 'helpers/countervalues' import CounterValues from 'helpers/countervalues'
@ -23,7 +22,6 @@ import ItemContainer from './ItemContainer'
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
globalSyncState: globalSyncStateSelector, globalSyncState: globalSyncStateSelector,
hasAccounts: hasAccountsSelector,
}) })
type Props = { type Props = {
@ -128,16 +126,7 @@ class ActivityIndicatorInner extends PureComponent<Props, State> {
} }
} }
const ActivityIndicator = ({ const ActivityIndicator = ({ globalSyncState, t }: { globalSyncState: AsyncState, t: T }) => (
globalSyncState,
hasAccounts,
t,
}: {
globalSyncState: AsyncState,
hasAccounts: boolean,
t: T,
}) =>
!hasAccounts ? null : (
<BridgeSyncConsumer> <BridgeSyncConsumer>
{setSyncBehavior => ( {setSyncBehavior => (
<CounterValues.PollingConsumer> <CounterValues.PollingConsumer>

9
src/components/TopBar/index.js

@ -12,6 +12,7 @@ import type { T } from 'types/common'
import { lock } from 'reducers/application' import { lock } from 'reducers/application'
import { hasPassword } from 'reducers/settings' import { hasPassword } from 'reducers/settings'
import { hasAccountsSelector } from 'reducers/accounts'
import { openModal } from 'reducers/modals' import { openModal } from 'reducers/modals'
import IconLock from 'icons/Lock' import IconLock from 'icons/Lock'
@ -54,6 +55,7 @@ const Bar = styled.div`
const mapStateToProps = state => ({ const mapStateToProps = state => ({
hasPassword: hasPassword(state), hasPassword: hasPassword(state),
hasAccounts: hasAccountsSelector(state),
}) })
const mapDispatchToProps = { const mapDispatchToProps = {
@ -63,6 +65,7 @@ const mapDispatchToProps = {
type Props = { type Props = {
hasPassword: boolean, hasPassword: boolean,
hasAccounts: boolean,
history: RouterHistory, history: RouterHistory,
location: Location, location: Location,
lock: Function, lock: Function,
@ -91,17 +94,21 @@ class TopBar extends PureComponent<Props> {
} }
} }
render() { render() {
const { hasPassword, t } = this.props const { hasPassword, hasAccounts, t } = this.props
return ( return (
<Container bg="lightGrey" color="graphite"> <Container bg="lightGrey" color="graphite">
<Inner> <Inner>
<Box grow horizontal> <Box grow horizontal>
<GlobalSearch t={t} isHidden /> <GlobalSearch t={t} isHidden />
{hasAccounts && (
<Fragment>
<ActivityIndicator /> <ActivityIndicator />
<Box justifyContent="center"> <Box justifyContent="center">
<Bar /> <Bar />
</Box> </Box>
</Fragment>
)}
<Tooltip render={() => t('app:settings.title')}> <Tooltip render={() => t('app:settings.title')}>
<ItemContainer isInteractive onClick={this.navigateToSettings}> <ItemContainer isInteractive onClick={this.navigateToSettings}>
<IconSettings size={16} /> <IconSettings size={16} />

8
src/components/TranslatedError.js

@ -4,6 +4,7 @@
// - an error can have parameters, to use them, just use field of the Error object, that's what we give to `t()` // - an error can have parameters, to use them, just use field of the Error object, that's what we give to `t()`
// - returned value is intentially not styled (is universal). wrap this in whatever you need // - returned value is intentially not styled (is universal). wrap this in whatever you need
import logger from 'logger'
import { PureComponent } from 'react' import { PureComponent } from 'react'
import { translate } from 'react-i18next' import { translate } from 'react-i18next'
import type { T } from 'types/common' import type { T } from 'types/common'
@ -18,7 +19,12 @@ class TranslatedError extends PureComponent<Props> {
const { t, error } = this.props const { t, error } = this.props
if (!error) return null if (!error) return null
if (typeof error === 'string') return error if (typeof error === 'string') return error
return t(`errors:${error.name}`, error) const translation = t(`errors:${error.name}`, error)
if (translation) {
return translation
}
logger.warn(`TranslatedError: no translation for '${error.name}'`, error)
return error.message || error.name || t('errors:generic')
} }
} }

16
src/components/TriggerAppReady.js

@ -0,0 +1,16 @@
// @flow
import { PureComponent } from 'react'
export default class TriggerAppReady extends PureComponent<{}> {
componentDidMount() {
window.requestAnimationFrame(() => (this._timeout = setTimeout(() => window.onAppReady(), 300)))
}
componentWillUnmount() {
clearTimeout(this._timeout)
}
_timeout: *
render() {
return null
}
}

15
src/components/TriggerOnMount/index.js

@ -1,15 +0,0 @@
// @flow
import { PureComponent } from 'react'
type Props = {
callback: () => void,
}
class TriggerOnMount extends PureComponent<Props> {
componentDidMount() {
const { callback } = this.props
callback()
}
}
export default TriggerOnMount

5
src/components/Workflow/EnsureGenuine.js

@ -1,7 +1,9 @@
// @flow // @flow
import { timeout } from 'rxjs/operators/timeout'
import { PureComponent } from 'react' import { PureComponent } from 'react'
import isEqual from 'lodash/isEqual' import isEqual from 'lodash/isEqual'
import { GENUINE_TIMEOUT } from 'config/constants'
import type { Device } from 'types/common' import type { Device } from 'types/common'
import getIsGenuine from 'commands/getIsGenuine' import getIsGenuine from 'commands/getIsGenuine'
@ -59,7 +61,8 @@ class EnsureGenuine extends PureComponent<Props, State> {
this._checking = true this._checking = true
try { try {
const res = await getIsGenuine const res = await getIsGenuine
.send({ devicePath: device.path, targetId: infos.targetId }) .send({ devicePath: device.path, targetId: infos.targetId, version: infos.version })
.pipe(timeout(GENUINE_TIMEOUT))
.toPromise() .toPromise()
if (this._unmounting) return if (this._unmounting) return
const isGenuine = res === '0000' const isGenuine = res === '0000'

30
src/components/Workflow/index.js

@ -31,9 +31,10 @@ type Props = {
genuineError: ?Error, genuineError: ?Error,
}, },
) => Node, ) => Node,
renderMcuUpdate?: (deviceInfo: DeviceInfo) => Node, renderMcuUpdate?: (device: Device, deviceInfo: DeviceInfo) => Node,
renderFinalUpdate?: (deviceInfo: DeviceInfo) => Node, renderFinalUpdate?: (device: Device, deviceInfo: DeviceInfo) => Node,
renderDashboard?: (device: Device, deviceInfo: DeviceInfo, isGenuine: boolean) => Node, renderDashboard?: (device: Device, deviceInfo: DeviceInfo, isGenuine: boolean) => Node,
onGenuineCheck?: (isGenuine: boolean) => void,
renderError?: (dashboardError: ?Error, genuineError: ?Error) => Node, renderError?: (dashboardError: ?Error, genuineError: ?Error) => Node,
} }
type State = {} type State = {}
@ -47,12 +48,22 @@ class Workflow extends PureComponent<Props, State> {
renderMcuUpdate, renderMcuUpdate,
renderError, renderError,
renderDefault, renderDefault,
onGenuineCheck,
} = this.props } = this.props
return ( return (
<EnsureDevice> <EnsureDevice>
{(device: Device) => ( {(device: Device) => (
<EnsureDashboard device={device}> <EnsureDashboard device={device}>
{(deviceInfo: ?DeviceInfo, dashboardError: ?Error) => ( {(deviceInfo: ?DeviceInfo, dashboardError: ?Error) => {
if (deviceInfo && deviceInfo.mcu && renderMcuUpdate) {
return renderMcuUpdate(device, deviceInfo)
}
if (deviceInfo && deviceInfo.final && renderFinalUpdate) {
return renderFinalUpdate(device, deviceInfo)
}
return (
<EnsureGenuine device={device} infos={deviceInfo}> <EnsureGenuine device={device} infos={deviceInfo}>
{(isGenuine: ?boolean, genuineError: ?Error) => { {(isGenuine: ?boolean, genuineError: ?Error) => {
if (dashboardError || genuineError) { if (dashboardError || genuineError) {
@ -64,15 +75,9 @@ class Workflow extends PureComponent<Props, State> {
}) })
} }
if (deviceInfo && deviceInfo.mcu && renderMcuUpdate) {
return renderMcuUpdate(deviceInfo)
}
if (deviceInfo && deviceInfo.final && renderFinalUpdate) {
return renderFinalUpdate(deviceInfo)
}
if (isGenuine && deviceInfo && device && !dashboardError && !genuineError) { if (isGenuine && deviceInfo && device && !dashboardError && !genuineError) {
if (onGenuineCheck) onGenuineCheck(isGenuine)
if (renderDashboard) return renderDashboard(device, deviceInfo, isGenuine) if (renderDashboard) return renderDashboard(device, deviceInfo, isGenuine)
} }
@ -82,7 +87,8 @@ class Workflow extends PureComponent<Props, State> {
}) })
}} }}
</EnsureGenuine> </EnsureGenuine>
)} )
}}
</EnsureDashboard> </EnsureDashboard>
)} )}
</EnsureDevice> </EnsureDevice>

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

@ -16,8 +16,12 @@ type Style = any // FIXME
const buttonStyles: { [_: string]: Style } = { const buttonStyles: { [_: string]: Style } = {
default: { default: {
default: noop, default: noop,
active: noop, active: p => `
hover: noop, background: ${rgba(p.theme.colors.fog, 0.3)};
`,
hover: p => `
background: ${rgba(p.theme.colors.fog, 0.2)};
`,
focus: () => ` focus: () => `
box-shadow: ${focusedShadowStyle}; box-shadow: ${focusedShadowStyle};
`, `,

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

@ -30,6 +30,7 @@ const Back = styled(Box).attrs({
})` })`
cursor: pointer; cursor: pointer;
position: absolute; position: absolute;
line-height: 1;
top: 0; top: 0;
left: 0; left: 0;

16
src/components/base/Spoiler/index.js

@ -17,12 +17,12 @@ type State = {
} }
const Title = styled(Text).attrs({ const Title = styled(Text).attrs({
ff: 'Museo Sans|Bold', ff: p => (p.ff ? p.ff : 'Museo Sans|Bold'),
fontSize: 2, fontSize: p => (p.fontSize ? p.fontSize : 2),
color: 'dark', color: p => (p.color ? p.color : 'dark'),
tabIndex: 0, tabIndex: 0,
})` })`
text-transform: uppercase; text-transform: ${p => (!p.textTransform ? 'auto' : 'uppercase')};
letter-spacing: 1px; letter-spacing: 1px;
cursor: pointer; cursor: pointer;
outline: none; outline: none;
@ -41,15 +41,17 @@ class Spoiler extends PureComponent<Props, State> {
toggle = () => this.setState({ isOpened: !this.state.isOpened }) toggle = () => this.setState({ isOpened: !this.state.isOpened })
render() { render() {
const { title, children } = this.props const { title, children, ...p } = this.props
const { isOpened } = this.state const { isOpened } = this.state
return ( return (
<Fragment> <Fragment>
<Box horizontal flow={1} color="dark" align="center"> <Box horizontal flow={1} color="dark" align="center" {...p}>
<IconContainer isOpened={isOpened}> <IconContainer isOpened={isOpened}>
<IconChevronRight size={12} /> <IconChevronRight size={12} />
</IconContainer> </IconContainer>
<Title onClick={this.toggle}>{title}</Title> <Title {...p} onClick={this.toggle}>
{title}
</Title>
</Box> </Box>
{isOpened && children} {isOpened && children}
</Fragment> </Fragment>

10
src/components/layout/Default.js

@ -17,6 +17,9 @@ import DashboardPage from 'components/DashboardPage'
import ManagerPage from 'components/ManagerPage' import ManagerPage from 'components/ManagerPage'
import ExchangePage from 'components/ExchangePage' import ExchangePage from 'components/ExchangePage'
import SettingsPage from 'components/SettingsPage' import SettingsPage from 'components/SettingsPage'
import LibcoreBusyIndicator from 'components/LibcoreBusyIndicator'
import DeviceBusyIndicator from 'components/DeviceBusyIndicator'
import TriggerAppReady from 'components/TriggerAppReady'
import AppRegionDrag from 'components/AppRegionDrag' import AppRegionDrag from 'components/AppRegionDrag'
import IsUnlocked from 'components/IsUnlocked' import IsUnlocked from 'components/IsUnlocked'
@ -39,7 +42,6 @@ type Props = {
class Default extends Component<Props> { class Default extends Component<Props> {
componentDidMount() { componentDidMount() {
window.requestAnimationFrame(() => (this._timeout = setTimeout(() => window.onAppReady(), 300)))
window.addEventListener('keydown', this.kbShortcut) window.addEventListener('keydown', this.kbShortcut)
} }
@ -57,7 +59,6 @@ class Default extends Component<Props> {
} }
componentWillUnmount() { componentWillUnmount() {
clearTimeout(this._timeout)
window.removeEventListener('keydown', this.kbShortcut) // Prevents adding multiple listeners when hot reloading window.removeEventListener('keydown', this.kbShortcut) // Prevents adding multiple listeners when hot reloading
} }
@ -67,12 +68,12 @@ class Default extends Component<Props> {
} }
} }
_timeout = undefined
_scrollContainer = null _scrollContainer = null
render() { render() {
return ( return (
<Fragment> <Fragment>
<TriggerAppReady />
{process.platform === 'darwin' && <AppRegionDrag />} {process.platform === 'darwin' && <AppRegionDrag />}
<IsUnlocked> <IsUnlocked>
@ -96,6 +97,9 @@ class Default extends Component<Props> {
</Main> </Main>
</Box> </Box>
</Box> </Box>
<LibcoreBusyIndicator />
<DeviceBusyIndicator />
</IsUnlocked> </IsUnlocked>
</Fragment> </Fragment>
) )

3
src/components/modals/AccountSettingRenderBody.js

@ -22,6 +22,7 @@ import Box from 'components/base/Box'
import Button from 'components/base/Button' import Button from 'components/base/Button'
import Input from 'components/base/Input' import Input from 'components/base/Input'
import Select from 'components/base/Select' import Select from 'components/base/Select'
import SyncAgo from 'components/SyncAgo'
import { import {
ModalBody, ModalBody,
@ -258,12 +259,12 @@ class HelperComp extends PureComponent<Props, State> {
</Container> </Container>
) : null} ) : null}
<Spoiler title={t('app:account.settings.advancedLogs')}> <Spoiler title={t('app:account.settings.advancedLogs')}>
<SyncAgo date={account.lastSyncDate} />
<textarea <textarea
readOnly readOnly
style={{ style={{
userSelect: 'text', userSelect: 'text',
border: '1px dashed #f9f9f9', border: '1px dashed #f9f9f9',
marginTop: '20px',
backgroundColor: '#f9f9f9', backgroundColor: '#f9f9f9',
color: '#000', color: '#000',
fontFamily: 'monospace', fontFamily: 'monospace',

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

@ -11,7 +11,7 @@ import Box from 'components/base/Box'
import CurrencyBadge from 'components/base/CurrencyBadge' import CurrencyBadge from 'components/base/CurrencyBadge'
import Button from 'components/base/Button' import Button from 'components/base/Button'
import AccountsList from 'components/base/AccountsList' import AccountsList from 'components/base/AccountsList'
import IconExchange from 'icons/Exchange' import IconExclamationCircleThin from 'icons/ExclamationCircleThin'
import type { StepProps } from '../index' import type { StepProps } from '../index'
@ -20,14 +20,30 @@ class StepImport extends PureComponent<StepProps> {
this.startScanAccountsDevice() this.startScanAccountsDevice()
} }
componentDidUpdate(prevProps: StepProps) {
// handle case when we click on stop sync
if (prevProps.scanStatus !== 'finished' && this.props.scanStatus === 'finished') {
this.unsub()
}
// handle case when we click on retry sync
if (prevProps.scanStatus !== 'scanning' && this.props.scanStatus === 'scanning') {
this.startScanAccountsDevice()
}
}
componentWillUnmount() { componentWillUnmount() {
this.unsub()
}
scanSubscription = null
unsub = () => {
if (this.scanSubscription) { if (this.scanSubscription) {
this.scanSubscription.unsubscribe() this.scanSubscription.unsubscribe()
} }
} }
scanSubscription = null
translateName(account: Account) { translateName(account: Account) {
const { t } = this.props const { t } = this.props
let { name } = account let { name } = account
@ -45,6 +61,7 @@ class StepImport extends PureComponent<StepProps> {
} }
startScanAccountsDevice() { startScanAccountsDevice() {
this.unsub()
const { currency, currentDevice, setState } = this.props const { currency, currentDevice, setState } = this.props
try { try {
invariant(currency, 'No currency to scan') invariant(currency, 'No currency to scan')
@ -82,10 +99,7 @@ class StepImport extends PureComponent<StepProps> {
} }
handleRetry = () => { handleRetry = () => {
if (this.scanSubscription) { this.unsub()
this.scanSubscription.unsubscribe()
this.scanSubscription = null
}
this.handleResetState() this.handleResetState()
this.startScanAccountsDevice() this.startScanAccountsDevice()
} }
@ -131,6 +145,17 @@ class StepImport extends PureComponent<StepProps> {
handleUnselectAll = () => this.props.setState({ checkedAccountsIds: [] }) handleUnselectAll = () => this.props.setState({ checkedAccountsIds: [] })
renderError() {
const { err, t } = this.props
invariant(err, 'Trying to render inexisting error')
return (
<Box style={{ height: 200 }} align="center" justify="center" color="alertRed">
<IconExclamationCircleThin size={43} />
<Box mt={4}>{t('app:addAccounts.somethingWentWrong')}</Box>
</Box>
)
}
render() { render() {
const { const {
scanStatus, scanStatus,
@ -142,6 +167,12 @@ class StepImport extends PureComponent<StepProps> {
t, t,
} = this.props } = this.props
if (err) {
return this.renderError()
}
const currencyName = currency ? currency.name : ''
const importableAccounts = scannedAccounts.filter(acc => { const importableAccounts = scannedAccounts.filter(acc => {
if (acc.operations.length <= 0) { if (acc.operations.length <= 0) {
return false return false
@ -160,9 +191,8 @@ class StepImport extends PureComponent<StepProps> {
count: importableAccounts.length, count: importableAccounts.length,
}) })
const importableAccountsEmpty = t('app:addAccounts.noAccountToImport', { const importableAccountsEmpty = t('app:addAccounts.noAccountToImport', { currencyName })
currencyName: currency ? ` ${currency.name}}` : '', const hasAlreadyEmptyAccount = scannedAccounts.some(a => a.operations.length === 0)
})
return ( return (
<Fragment> <Fragment>
@ -180,7 +210,11 @@ class StepImport extends PureComponent<StepProps> {
/> />
<AccountsList <AccountsList
title={t('app:addAccounts.createNewAccount.title')} title={t('app:addAccounts.createNewAccount.title')}
emptyText={t('app:addAccounts.createNewAccount.noOperationOnLastAccount')} emptyText={
hasAlreadyEmptyAccount
? t('app:addAccounts.createNewAccount.noOperationOnLastAccount')
: t('app:addAccounts.createNewAccount.noAccountToCreate', { currencyName })
}
accounts={creatableAccounts} accounts={creatableAccounts}
checkedIds={checkedAccountsIds} checkedIds={checkedAccountsIds}
onToggleAccount={this.handleToggleAccount} onToggleAccount={this.handleToggleAccount}
@ -189,17 +223,7 @@ class StepImport extends PureComponent<StepProps> {
/> />
</Box> </Box>
{err && ( {err && <Box shrink>{err.message}</Box>}
<Box shrink>
{err.message}
<Button small outline onClick={this.handleRetry}>
<Box horizontal flow={2} align="center">
<IconExchange size={13} />
<span>{t('app:addAccounts.retrySync')}</span>
</Box>
</Button>
</Box>
)}
</Fragment> </Fragment>
) )
} }
@ -208,6 +232,7 @@ class StepImport extends PureComponent<StepProps> {
export default StepImport export default StepImport
export const StepImportFooter = ({ export const StepImportFooter = ({
setState,
scanStatus, scanStatus,
onClickAdd, onClickAdd,
onCloseModal, onCloseModal,
@ -250,7 +275,21 @@ export const StepImportFooter = ({
return ( return (
<Fragment> <Fragment>
{currency && <CurrencyBadge mr="auto" currency={currency} />} {currency && <CurrencyBadge mr="auto" currency={currency} />}
<Button primary disabled={scanStatus !== 'finished'} onClick={onClick}> {scanStatus === 'error' && (
<Button mr={2} onClick={() => setState({ scanStatus: 'scanning', err: null })}>
{t('app:common.retry')}
</Button>
)}
{scanStatus === 'scanning' && (
<Button mr={2} onClick={() => setState({ scanStatus: 'finished' })}>
{t('app:common.stop')}
</Button>
)}
<Button
primary
disabled={scanStatus !== 'finished' && scanStatus !== 'error'}
onClick={onClick}
>
{ctaWording} {ctaWording}
</Button> </Button>
</Fragment> </Fragment>

163
src/components/modals/OperationDetails.js

@ -17,6 +17,7 @@ import { MODAL_OPERATION_DETAILS } from 'config/constants'
import { getMarketColor } from 'styles/helpers' import { getMarketColor } from 'styles/helpers'
import Box from 'components/base/Box' import Box from 'components/base/Box'
import Spoiler from 'components/base/Spoiler'
import Button from 'components/base/Button' import Button from 'components/base/Button'
import Bar from 'components/base/Bar' import Bar from 'components/base/Bar'
import FormattedVal from 'components/base/FormattedVal' import FormattedVal from 'components/base/FormattedVal'
@ -28,26 +29,23 @@ import { currencySettingsForAccountSelector, marketIndicatorSelector } from 'red
import CounterValue from 'components/CounterValue' import CounterValue from 'components/CounterValue'
import ConfirmationCheck from 'components/OperationsList/ConfirmationCheck' import ConfirmationCheck from 'components/OperationsList/ConfirmationCheck'
import Ellipsis from '../base/Ellipsis'
const Line = styled(Box).attrs({
horizontal: true, const OpDetailsTitle = styled(Box).attrs({
})`` ff: 'Museo Sans|ExtraBold',
fontSize: 2,
const ColLeft = styled(Box).attrs({ color: 'black',
color: 'smoke', textTransform: 'uppercase',
ff: 'Open Sans', mb: 1,
fontSize: 4,
})` })`
width: 95px; letter-spacing: 2px;
` `
const ColRight = styled(Box).attrs({
fontSize: 4, const OpDetailsData = styled(Box).attrs({
ff: 'Open Sans', ff: 'Open Sans',
color: 'dark', color: 'smoke',
shrink: true, fontSize: 4,
})` })``
word-break: break-all;
`
const CanSelect = styled.div` const CanSelect = styled.div`
user-select: text; user-select: text;
@ -91,7 +89,8 @@ type Props = {
const OperationDetails = connect(mapStateToProps)((props: Props) => { const OperationDetails = connect(mapStateToProps)((props: Props) => {
const { t, onClose, operation, account, currencySettings, marketIndicator } = props const { t, onClose, operation, account, currencySettings, marketIndicator } = props
if (!operation || !account || !currencySettings) return null if (!operation || !account || !currencySettings) return null
const { hash, date, senders, recipients, type, fee } = operation const { hash, date, senders, type, fee, recipients } = operation
const { name, unit, currency } = account const { name, unit, currency } = account
const amount = getOperationAmountNumber(operation) const amount = getOperationAmountNumber(operation)
const isNegative = operation.type === 'OUT' const isNegative = operation.type === 'OUT'
@ -108,21 +107,21 @@ const OperationDetails = connect(mapStateToProps)((props: Props) => {
return ( return (
<ModalBody onClose={onClose}> <ModalBody onClose={onClose}>
<ModalTitle>{t('app:operationDetails.title')}</ModalTitle> <ModalTitle>{t('app:operationDetails.title')}</ModalTitle>
<ModalContent flow={4}> <ModalContent flow={3}>
<Box alignItems="center" mt={3}> <Box alignItems="center" mt={1}>
<ConfirmationCheck <ConfirmationCheck
marketColor={marketColor} marketColor={marketColor}
isConfirmed={isConfirmed} isConfirmed={isConfirmed}
style={{ style={{
transform: 'scale(2)', transform: 'scale(1.5)',
}} }}
t={t} t={t}
type={type} type={type}
withTooltip={false} withTooltip={false}
/> />
<Box mt={5} alignItems="center"> <Box my={4} alignItems="center">
<Box> <Box>
<FormattedVal unit={unit} alwaysShowSign showCode val={amount} fontSize={8} /> <FormattedVal unit={unit} alwaysShowSign showCode val={amount} fontSize={6} />
</Box> </Box>
<Box mt={1}> <Box mt={1}>
<CounterValue <CounterValue
@ -135,60 +134,67 @@ const OperationDetails = connect(mapStateToProps)((props: Props) => {
</Box> </Box>
</Box> </Box>
</Box> </Box>
<Line mt={4}> <Box horizontal flow={2}>
<ColLeft>{t('app:operationDetails.account')}</ColLeft> <Box flex={1}>
<ColRight>{name}</ColRight> <OpDetailsTitle>{t('app:operationDetails.account')}</OpDetailsTitle>
</Line> <OpDetailsData>{name}</OpDetailsData>
<B /> </Box>
<Line> <Box flex={1}>
<ColLeft>{t('app:operationDetails.date')}</ColLeft> <OpDetailsTitle>{t('app:operationDetails.date')}</OpDetailsTitle>
<ColRight>{moment(date).format('LLL')}</ColRight> <OpDetailsData>{moment(date).format('LLL')}</OpDetailsData>
</Line> </Box>
</Box>
<B /> <B />
<Line> <Box horizontal flow={2}>
<ColLeft>{t('app:operationDetails.status')}</ColLeft> <Box flex={1}>
<ColRight color={isConfirmed ? 'positiveGreen' : null} horizontal flow={1}> <OpDetailsTitle>{t('app:operationDetails.fees')}</OpDetailsTitle>
{fee ? (
<Fragment>
<OpDetailsData>
<FormattedVal unit={unit} showCode val={fee} color="dark" />
</OpDetailsData>
</Fragment>
) : null}
</Box>
<Box flex={1}>
<OpDetailsTitle>{t('app:operationDetails.status')}</OpDetailsTitle>
<OpDetailsData color={isConfirmed ? 'positiveGreen' : null} horizontal>
<Box> <Box>
{isConfirmed {isConfirmed
? t('app:operationDetails.confirmed') ? t('app:operationDetails.confirmed')
: t('app:operationDetails.notConfirmed')} : t('app:operationDetails.notConfirmed')}
</Box> </Box>
<Box>{`(${confirmations})`}</Box> <Box>{`(${confirmations})`}</Box>
</ColRight> </OpDetailsData>
</Line> </Box>
<B /> </Box>
{fee ? (
<Fragment>
<Line>
<ColLeft>{t('app:operationDetails.fees')}</ColLeft>
<ColRight>
<FormattedVal unit={unit} showCode val={fee} color="dark" />
</ColRight>
</Line>
<B /> <B />
</Fragment> <Box>
) : null} <OpDetailsTitle>{t('app:operationDetails.from')}</OpDetailsTitle>
<Line> <OpDetailsData>{uniqSenders.map(v => <CanSelect key={v}>{v}</CanSelect>)}</OpDetailsData>
<ColLeft>{t('app:operationDetails.from')}</ColLeft> </Box>
<ColRight>{uniqSenders.map(v => <CanSelect key={v}>{v}</CanSelect>)}</ColRight>
</Line>
<B /> <B />
<Line> <Box>
<ColLeft>{t('app:operationDetails.to')}</ColLeft> <OpDetailsTitle>{t('app:operationDetails.to')}</OpDetailsTitle>
<ColRight>{recipients.map(v => <CanSelect key={v}> {v} </CanSelect>)}</ColRight> <RenderRecipients recipients={recipients} t={t} />
</Line> </Box>
<B /> <B />
<Line> <Box>
<ColLeft>{t('app:operationDetails.identifier')}</ColLeft> <OpDetailsTitle>{t('app:operationDetails.identifier')}</OpDetailsTitle>
<ColRight> <OpDetailsData>
<CanSelect>{hash}</CanSelect> <CanSelect>
</ColRight> <Ellipsis>{hash}</Ellipsis>
</Line> </CanSelect>
</OpDetailsData>
</Box>
</ModalContent> </ModalContent>
<ModalFooter horizontal justify="flex-end" flow={2}> <ModalFooter horizontal justify="flex-end" flow={2}>
<Button onClick={onClose}>{t('app:common.cancel')}</Button> <Button padded onClick={onClose}>
{t('app:common.cancel')}
</Button>
{url ? ( {url ? (
<Button primary onClick={() => shell.openExternal(url)}> <Button ml="auto" primary padded onClick={() => shell.openExternal(url)}>
{t('app:operationDetails.viewOperation')} {t('app:operationDetails.viewOperation')}
</Button> </Button>
) : null} ) : null}
@ -216,3 +222,32 @@ const OperationDetailsWrapper = ({ t }: { t: T }) => (
) )
export default translate()(OperationDetailsWrapper) export default translate()(OperationDetailsWrapper)
export function RenderRecipients({ recipients, t }: { recipients: *, t: T }) {
// Hardcoded for now
const numToShow = 2
return (
<Box>
<OpDetailsData>
{recipients
.slice(0, numToShow)
.map(recipient => <CanSelect key={recipient}>{recipient}</CanSelect>)}
</OpDetailsData>
{recipients.length > numToShow && (
<Spoiler
title={t('app:operationDetails.showMore', { recipients: recipients.length - numToShow })}
color="wallet"
ff="Open Sans|SemiBold"
fontSize={4}
mt={1}
>
<OpDetailsData>
{recipients
.slice(numToShow)
.map(recipient => <CanSelect key={recipient}>{recipient}</CanSelect>)}
</OpDetailsData>
</Spoiler>
)}
</Box>
)
}

2
src/components/modals/ReleaseNotes.js

@ -233,7 +233,7 @@ class ReleaseNotes extends PureComponent<Props, State> {
) )
} }
return <Modal name={MODAL_RELEASES_NOTES} render={renderBody} width="600px" /> return <Modal name={MODAL_RELEASES_NOTES} render={renderBody} />
} }
} }

7
src/config/constants.js

@ -20,6 +20,8 @@ export const GET_CALLS_RETRY = intFromEnv('GET_CALLS_RETRY', 2)
export const SYNC_MAX_CONCURRENT = intFromEnv('LEDGER_SYNC_MAX_CONCURRENT', 6) export const SYNC_MAX_CONCURRENT = intFromEnv('LEDGER_SYNC_MAX_CONCURRENT', 6)
export const SYNC_BOOT_DELAY = 2 * 1000 export const SYNC_BOOT_DELAY = 2 * 1000
export const SYNC_ALL_INTERVAL = 60 * 1000 export const SYNC_ALL_INTERVAL = 60 * 1000
export const GENUINE_TIMEOUT = intFromEnv('GENUINE_TIMEOUT', 60 * 1000)
export const SYNC_TIMEOUT = intFromEnv('SYNC_TIMEOUT', 30 * 1000)
export const CHECK_APP_INTERVAL_WHEN_INVALID = 600 export const CHECK_APP_INTERVAL_WHEN_INVALID = 600
export const CHECK_APP_INTERVAL_WHEN_VALID = 1200 export const CHECK_APP_INTERVAL_WHEN_VALID = 1200
@ -29,6 +31,10 @@ export const DEVICE_DISCONNECT_DEBOUNCE = intFromEnv('LEDGER_DEVICE_DISCONNECT_D
// Endpoints... // Endpoints...
export const LEDGER_COUNTERVALUES_API = stringFromEnv(
'LEDGER_COUNTERVALUES_API',
'https://ledger-countervalue-poc.herokuapp.com',
)
export const LEDGER_REST_API_BASE = stringFromEnv( export const LEDGER_REST_API_BASE = stringFromEnv(
'LEDGER_REST_API_BASE', 'LEDGER_REST_API_BASE',
'https://api.ledgerwallet.com/', 'https://api.ledgerwallet.com/',
@ -59,6 +65,7 @@ export const SKIP_GENUINE = boolFromEnv('SKIP_GENUINE')
export const SKIP_ONBOARDING = boolFromEnv('SKIP_ONBOARDING') export const SKIP_ONBOARDING = boolFromEnv('SKIP_ONBOARDING')
export const SHOW_LEGACY_NEW_ACCOUNT = boolFromEnv('SHOW_LEGACY_NEW_ACCOUNT') export const SHOW_LEGACY_NEW_ACCOUNT = boolFromEnv('SHOW_LEGACY_NEW_ACCOUNT')
export const HIGHLIGHT_I18N = boolFromEnv('HIGHLIGHT_I18N') export const HIGHLIGHT_I18N = boolFromEnv('HIGHLIGHT_I18N')
export const DISABLE_ACTIVITY_INDICATORS = boolFromEnv('DISABLE_ACTIVITY_INDICATORS')
// Other constants // Other constants

17
src/helpers/apps/installApp.js

@ -1,8 +1,10 @@
// @flow // @flow
import qs from 'qs'
import type Transport from '@ledgerhq/hw-transport' import type Transport from '@ledgerhq/hw-transport'
import { createSocketDialog } from 'helpers/common' import { BASE_SOCKET_URL_SECURE } from 'config/constants'
import { createDeviceSocket } from 'helpers/socket'
import type { LedgerScriptParams } from 'helpers/common' import type { LedgerScriptParams } from 'helpers/common'
/** /**
@ -10,7 +12,14 @@ import type { LedgerScriptParams } from 'helpers/common'
*/ */
export default async function installApp( export default async function installApp(
transport: Transport<*>, transport: Transport<*>,
{ appParams }: { appParams: LedgerScriptParams }, targetId: string | number,
{ app }: { app: LedgerScriptParams },
): Promise<*> { ): Promise<*> {
return createSocketDialog(transport, '/install', appParams) const params = {
targetId,
...app,
firmwareKey: app.firmware_key,
}
const url = `${BASE_SOCKET_URL_SECURE}/install?${qs.stringify(params)}`
return createDeviceSocket(transport, url).toPromise()
} }

26
src/helpers/apps/listApps.js

@ -1,20 +1,24 @@
// @flow // @flow
import axios from 'axios' import axios from 'axios'
import { MANAGER_API_BASE } from 'config/constants' import { APPLICATIONS_BY_DEVICE } from 'helpers/urls'
import getDeviceVersion from 'helpers/devices/getDeviceVersion'
import getCurrentFirmware from 'helpers/devices/getCurrentFirmware'
export default async (targetId: string | number) => { export default async (targetId: string | number, version: string) => {
try { try {
const { data: deviceData } = await axios.get( const provider = 1
`${MANAGER_API_BASE}/device_versions_target_id/${targetId}`, const deviceData = await getDeviceVersion(targetId)
) const firmwareData = await getCurrentFirmware({ deviceId: deviceData.id, version })
const { data } = await axios.get('https://api.ledgerwallet.com/update/applications') const params = {
provider,
if (deviceData.name in data) { current_se_firmware_final_version: firmwareData.id,
return data[deviceData.name] device_version: deviceData.id,
} }
const {
return data['nanos-1.4'] data: { application_versions },
} = await axios.post(APPLICATIONS_BY_DEVICE, params)
return application_versions.length > 0 ? application_versions : []
} catch (err) { } catch (err) {
const error = Error(err.message) const error = Error(err.message)
error.stack = err.stack error.stack = err.stack

19
src/helpers/apps/uninstallApp.js

@ -1,8 +1,10 @@
// @flow // @flow
import qs from 'qs'
import type Transport from '@ledgerhq/hw-transport' import type Transport from '@ledgerhq/hw-transport'
import { createSocketDialog } from 'helpers/common' import { BASE_SOCKET_URL_SECURE } from 'config/constants'
import { createDeviceSocket } from 'helpers/socket'
import type { LedgerScriptParams } from 'helpers/common' import type { LedgerScriptParams } from 'helpers/common'
/** /**
@ -10,12 +12,15 @@ import type { LedgerScriptParams } from 'helpers/common'
*/ */
export default async function uninstallApp( export default async function uninstallApp(
transport: Transport<*>, transport: Transport<*>,
{ appParams }: { appParams: LedgerScriptParams }, targetId: string | number,
{ app }: { app: LedgerScriptParams },
): Promise<*> { ): Promise<*> {
const params = { const params = {
...appParams, targetId,
firmware: appParams.delete, ...app,
firmwareKey: appParams.deleteKey, firmware: app.delete,
firmwareKey: app.delete_key,
} }
return createSocketDialog(transport, '/install', params) const url = `${BASE_SOCKET_URL_SECURE}/install?${qs.stringify(params)}`
return createDeviceSocket(transport, url).toPromise()
} }

49
src/helpers/common.js

@ -1,11 +1,7 @@
// @flow // @flow
// FIXME remove this file! 'helpers/common.js' RLY? :P // FIXME remove this file! 'helpers/common.js' RLY? :P
import qs from 'qs'
import type Transport from '@ledgerhq/hw-transport' import type Transport from '@ledgerhq/hw-transport'
import { BASE_SOCKET_URL, BASE_SOCKET_URL_SECURE } from 'config/constants'
import { createDeviceSocket } from './socket'
const APDUS = { const APDUS = {
GET_FIRMWARE: [0xe0, 0x01, 0x00, 0x00], GET_FIRMWARE: [0xe0, 0x01, 0x00, 0x00],
@ -16,35 +12,14 @@ const APDUS = {
export type LedgerScriptParams = { export type LedgerScriptParams = {
firmware?: string, firmware?: string,
firmwareKey?: string, firmware_key?: string,
delete?: string, delete?: string,
deleteKey?: string, delete_key?: string,
targetId?: string | number, targetId?: string | number,
} name: string,
version: string,
type FirmwareUpdateType = 'osu' | 'final' icon: string,
app?: number,
export async function getMemInfos(transport: Transport<*>): Promise<Object> {
const { targetId } = await getFirmwareInfo(transport)
// Dont ask me about this `perso_11`: I don't know. But we need it.
return createSocketDialog(transport, '/get-mem-infos', { targetId, perso: 'perso_11' })
}
/**
* Open socket connection with firmware api, and init a dialog
* with the device
*/
export async function createSocketDialog(
transport: Transport<*>,
endpoint: string,
params: LedgerScriptParams,
managerUrl: boolean = false,
): Promise<string> {
console.warn('DEPRECATED createSocketDialog: use createDeviceSocket') // eslint-disable-line
const url = `${managerUrl ? BASE_SOCKET_URL_SECURE : BASE_SOCKET_URL}${endpoint}?${qs.stringify(
params,
)}`
return createDeviceSocket(transport, url).toPromise()
} }
/** /**
@ -66,15 +41,3 @@ export async function getFirmwareInfo(transport: Transport<*>) {
throw error throw error
} }
} }
/**
* Helpers to build OSU and Final firmware params
*/
export const buildParamsFromFirmware = (type: FirmwareUpdateType): Function => (
data: any,
): LedgerScriptParams => ({
firmware: data[`${type}_firmware`],
firmwareKey: data[`${type}_firmware_key`],
perso: data[`${type}_perso`],
targetId: data[`${type}_target_id`],
})

3
src/helpers/countervalues.js

@ -1,6 +1,7 @@
// @flow // @flow
import { createSelector } from 'reselect' import { createSelector } from 'reselect'
import { LEDGER_COUNTERVALUES_API } from 'config/constants'
import createCounterValues from '@ledgerhq/live-common/lib/countervalues' import createCounterValues from '@ledgerhq/live-common/lib/countervalues'
import { setExchangePairsAction } from 'actions/settings' import { setExchangePairsAction } from 'actions/settings'
import { currenciesSelector } from 'reducers/accounts' import { currenciesSelector } from 'reducers/accounts'
@ -53,7 +54,7 @@ const addExtraPollingHooks = (schedulePoll, cancelPoll) => {
const CounterValues = createCounterValues({ const CounterValues = createCounterValues({
log: (...args) => logger.log('CounterValues:', ...args), log: (...args) => logger.log('CounterValues:', ...args),
getAPIBaseURL: () => 'https://ledger-countervalue-poc.herokuapp.com', getAPIBaseURL: () => LEDGER_COUNTERVALUES_API,
storeSelector: state => state.countervalues, storeSelector: state => state.countervalues,
pairsSelector, pairsSelector,
setExchangePairsAction, setExchangePairsAction,

19
src/helpers/deviceAccess.js

@ -18,7 +18,7 @@ export const withDevice: WithDevice = devicePath => {
semaphorePerDevice[devicePath] || (semaphorePerDevice[devicePath] = createSemaphore(1)) semaphorePerDevice[devicePath] || (semaphorePerDevice[devicePath] = createSemaphore(1))
return job => return job =>
takeSemaphorePromise(sem, async () => { takeSemaphorePromise(sem, devicePath, async () => {
const t = await retry(() => TransportNodeHid.open(devicePath), { maxRetry: 1 }) const t = await retry(() => TransportNodeHid.open(devicePath), { maxRetry: 1 })
if (DEBUG_DEVICE) t.setDebugMode(true) if (DEBUG_DEVICE) t.setDebugMode(true)
@ -32,17 +32,32 @@ export const withDevice: WithDevice = devicePath => {
}) })
} }
function takeSemaphorePromise<T>(sem, f: () => Promise<T>): Promise<T> { function takeSemaphorePromise<T>(sem, devicePath, f: () => Promise<T>): Promise<T> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
sem.take(() => { sem.take(() => {
process.send({
type: 'setDeviceBusy',
busy: true,
devicePath,
})
f().then( f().then(
r => { r => {
sem.leave() sem.leave()
resolve(r) resolve(r)
process.send({
type: 'setDeviceBusy',
busy: false,
devicePath,
})
}, },
e => { e => {
sem.leave() sem.leave()
reject(e) reject(e)
process.send({
type: 'setDeviceBusy',
busy: false,
devicePath,
})
}, },
) )
}) })

26
src/helpers/devices/getCurrentFirmware.js

@ -0,0 +1,26 @@
// @flow
import axios from 'axios'
import { GET_CURRENT_FIRMWARE } from 'helpers/urls'
type Input = {
version: string,
deviceId: string | number,
}
let error
export default async (input: Input): Promise<*> => {
try {
const provider = 1
const { data } = await axios.post(GET_CURRENT_FIRMWARE, {
device_version: input.deviceId,
version_name: input.version,
provider,
})
return data
} catch (err) {
error = Error(err.message)
error.stack = err.stack
throw error
}
}

19
src/helpers/devices/getDeviceVersion.js

@ -0,0 +1,19 @@
// @flow
import axios from 'axios'
import { GET_DEVICE_VERSION } from 'helpers/urls'
export default async (targetId: string | number): Promise<*> => {
try {
const provider = 1
const { data } = await axios.post(GET_DEVICE_VERSION, {
provider,
target_id: targetId,
})
return data
} catch (err) {
const error = Error(err.message)
error.stack = err.stack
throw err
}
}

34
src/helpers/devices/getFirmwareInfo.js

@ -1,34 +0,0 @@
// @flow
import axios from 'axios'
import isEmpty from 'lodash/isEmpty'
import { MANAGER_API_BASE } from 'config/constants'
type Input = {
version: string,
targetId: string | number,
}
let error
export default async (data: Input) => {
try {
const { data: seFirmwareVersion } = await axios.post(
`${MANAGER_API_BASE}/firmware_versions_name`,
{
se_firmware_name: data.version,
target_id: data.targetId,
},
)
if (!isEmpty(seFirmwareVersion)) {
return seFirmwareVersion
}
error = Error('could not retrieve firmware informations, try again later')
throw error
} catch (err) {
error = Error(err.message)
error.stack = err.stack
throw error
}
}

24
src/helpers/devices/getIsGenuine.js

@ -1,12 +1,26 @@
// @flow // @flow
import type Transport from '@ledgerhq/hw-transport' import type Transport from '@ledgerhq/hw-transport'
import { createSocketDialog } from 'helpers/common'
import { SKIP_GENUINE } from 'config/constants' import { SKIP_GENUINE } from 'config/constants'
import { WS_GENUINE } from 'helpers/urls'
import { createDeviceSocket } from 'helpers/socket'
import getCurrentFirmware from './getCurrentFirmware'
import getDeviceVersion from './getDeviceVersion'
export default async ( export default async (
transport: Transport<*>, transport: Transport<*>,
{ targetId }: { targetId: string | number }, app: { targetId: string | number, version: string },
): Promise<string> => ): Promise<string> => {
SKIP_GENUINE const { targetId, version } = app
const device = await getDeviceVersion(app.targetId)
const firmware = await getCurrentFirmware({ deviceId: device.id, version })
const params = {
targetId,
version,
perso: firmware.perso,
}
const url = WS_GENUINE(params)
return SKIP_GENUINE
? new Promise(resolve => setTimeout(() => resolve('0000'), 1000)) ? new Promise(resolve => setTimeout(() => resolve('0000'), 1000))
: createSocketDialog(transport, '/genuine', { targetId }, true) : createDeviceSocket(transport, url).toPromise()
}

37
src/helpers/devices/getLatestFirmwareForDevice.js

@ -1,39 +1,38 @@
// @flow // @flow
import axios from 'axios' import axios from 'axios'
import isEmpty from 'lodash/isEmpty' import { GET_LATEST_FIRMWARE } from 'helpers/urls'
import { MANAGER_API_BASE } from 'config/constants'
import getFirmwareInfo from './getFirmwareInfo' import getCurrentFirmware from './getCurrentFirmware'
import getDeviceVersion from './getDeviceVersion'
type Input = { type Input = {
targetId: string | number,
version: string, version: string,
targetId: string | number,
} }
export default async (data: Input) => { export default async (input: Input) => {
try { try {
// Get firmware infos with firmware name and device version const provider = 1
const seFirmwareVersion = await getFirmwareInfo(data) const { targetId, version } = input
// Get device infos from targetId // Get device infos from targetId
const { data: deviceVersion } = await axios.get( const deviceVersion = await getDeviceVersion(targetId)
`${MANAGER_API_BASE}/device_versions_target_id/${data.targetId}`,
) // Get firmware infos with firmware name and device version
const seFirmwareVersion = await getCurrentFirmware({ version, deviceId: deviceVersion.id })
// Fetch next possible firmware // Fetch next possible firmware
const { data: serverData } = await axios.post(`${MANAGER_API_BASE}/get_latest_firmware`, { const { data } = await axios.post(GET_LATEST_FIRMWARE, {
current_se_firmware_version: seFirmwareVersion.id, current_se_firmware_final_version: seFirmwareVersion.id,
device_version: deviceVersion.id, device_version: deviceVersion.id,
providers: [1], provider,
}) })
const { se_firmware_version } = serverData if (data.result === 'null') {
return null
if (!isEmpty(se_firmware_version)) {
return se_firmware_version
} }
return null const { se_firmware_osu_version } = data
return se_firmware_osu_version
} catch (err) { } catch (err) {
const error = Error(err.message) const error = Error(err.message)
error.stack = err.stack error.stack = err.stack

7
src/helpers/devices/getMemInfo.js

@ -2,10 +2,9 @@
import type Transport from '@ledgerhq/hw-transport' import type Transport from '@ledgerhq/hw-transport'
import { getFirmwareInfo, createSocketDialog } from 'helpers/common' import { getFirmwareInfo } from 'helpers/common'
export default async function getMemInfos(transport: Transport<*>): Promise<Object> { export default async function getMemInfos(transport: Transport<*>): Promise<Object> {
const { targetId } = await getFirmwareInfo(transport) const { targetId } = await getFirmwareInfo(transport) // eslint-disable-line
// Dont ask me about this `perso_11`: I don't know. But we need it. return new Promise(resolve => setTimeout(() => resolve({}), 1000))
return createSocketDialog(transport, '/get-mem-infos', { targetId, perso: 'perso_11' })
} }

26
src/helpers/devices/getNextMCU.js

@ -0,0 +1,26 @@
// @flow
import axios from 'axios'
import { GET_NEXT_MCU } from 'helpers/urls'
import createCustomErrorClass from 'helpers/createCustomErrorClass'
const LatestMCUInstalledError = createCustomErrorClass('LatestMCUInstalledError')
export default async (bootloaderVersion: string): Promise<*> => {
try {
const { data } = await axios.post(GET_NEXT_MCU, {
bootloader_version: bootloaderVersion,
})
// FIXME: nextVersion will not be able to "default" when Error
// handling is standardize on the API side
if (data === 'default' || !data.name) {
throw new LatestMCUInstalledError('there is no next mcu version to install')
}
return data
} catch (err) {
const error = Error(err.message)
error.stack = err.stack
throw err
}
}

10
src/helpers/firmware/installFinalFirmware.js

@ -1,18 +1,16 @@
// @flow // @flow
import type Transport from '@ledgerhq/hw-transport' import type Transport from '@ledgerhq/hw-transport'
import { createSocketDialog, buildParamsFromFirmware } from 'helpers/common' import { WS_INSTALL } from 'helpers/urls'
import { createDeviceSocket } from 'helpers/socket'
type Input = Object type Input = Object
type Result = * type Result = *
const buildOsuParams = buildParamsFromFirmware('final')
export default async (transport: Transport<*>, firmware: Input): Result => { export default async (transport: Transport<*>, firmware: Input): Result => {
try { try {
const osuData = buildOsuParams(firmware) const url = WS_INSTALL(firmware)
await createSocketDialog(transport, '/install', osuData) await createDeviceSocket(transport, url).toPromise()
return { success: true } return { success: true }
} catch (err) { } catch (err) {
const error = Error(err.message) const error = Error(err.message)

25
src/helpers/firmware/installMcu.js

@ -1,8 +1,23 @@
// @flow // @flow
import type Transport from '@ledgerhq/hw-transport'
type Result = Promise<boolean> import { WS_MCU } from 'helpers/urls'
import { createDeviceSocket } from 'helpers/socket'
import getNextMCU from 'helpers/devices/getNextMCU'
// TODO: IMPLEMENTATION FOR FLASHING FIRMWARE type Result = Promise<*>
// GETTING APDUS FROM SERVER
// SEND THE APDUS TO DEVICE export default async (
export default async (): Result => new Promise(resolve => resolve(true)) transport: Transport<*>,
args: { targetId: string | number, version: string },
): Result => {
const { version } = args
const nextVersion = await getNextMCU(version)
const params = {
targetId: args.targetId,
version: nextVersion.name,
}
const url = WS_MCU(params)
return createDeviceSocket(transport, url).toPromise()
}

23
src/helpers/firmware/installOsuFirmware.js

@ -1,19 +1,26 @@
// @flow // @flow
import type Transport from '@ledgerhq/hw-transport' import type Transport from '@ledgerhq/hw-transport'
import { createSocketDialog, buildParamsFromFirmware } from 'helpers/common' import { WS_INSTALL } from 'helpers/urls'
import { createDeviceSocket } from 'helpers/socket'
type Input = Object import type { LedgerScriptParams } from 'helpers/common'
type Result = Promise<{ success: boolean, error?: any }> type Result = Promise<{ success: boolean, error?: any }>
const buildOsuParams = buildParamsFromFirmware('osu') export default async (
transport: Transport<*>,
export default async (transport: Transport<*>, firmware: Input): Result => { targetId: string | number,
firmware: LedgerScriptParams,
): Result => {
try { try {
const osuData = buildOsuParams(firmware) const params = {
await createSocketDialog(transport, '/install', osuData) targetId,
...firmware,
firmwareKey: firmware.firmware_key,
}
const url = WS_INSTALL(params)
await createDeviceSocket(transport, url).toPromise()
return { success: true } return { success: true }
} catch (err) { } catch (err) {
const error = Error(err.message) const error = Error(err.message)

27
src/helpers/urls.js

@ -0,0 +1,27 @@
// @flow
import qs from 'qs'
import { MANAGER_API_BASE, BASE_SOCKET_URL_SECURE } from 'config/constants'
import type { LedgerScriptParams } from 'helpers/common'
const urlBuilder = (base: string) => (endpoint: string): string => `${base}/${endpoint}`
const managerUrlbuilder = urlBuilder(MANAGER_API_BASE)
const wsURLBuilder = (endpoint: string) => (params?: Object) =>
`${BASE_SOCKET_URL_SECURE}/${endpoint}${params ? `?${qs.stringify(params)}` : ''}`
// const wsURLBuilderProxy = (endpoint: string) => (params?: Object) =>
// `ws://manager.ledger.fr:3501/${endpoint}${params ? `?${qs.stringify(params)}` : ''}`
export const GET_DEVICE_VERSION: string = managerUrlbuilder('get_device_version')
export const APPLICATIONS_BY_DEVICE: string = managerUrlbuilder('get_apps')
export const GET_CURRENT_FIRMWARE: string = managerUrlbuilder('get_firmware_version')
export const GET_LATEST_FIRMWARE: string = managerUrlbuilder('get_latest_firmware')
export const GET_NEXT_MCU: string = managerUrlbuilder('mcu_versions_bootloader')
export const WS_INSTALL: (arg: LedgerScriptParams) => string = wsURLBuilder('install')
export const WS_GENUINE: (arg: { targetId: string | number }) => string = wsURLBuilder('genuine')
export const WS_MCU: (arg: { targetId: string | number, version: string }) => string = wsURLBuilder(
'mcu',
)

18
src/helpers/withLibcore.js

@ -3,8 +3,24 @@
// TODO: `core` should be typed // TODO: `core` should be typed
type Job<A> = Object => Promise<A> type Job<A> = Object => Promise<A>
export default function withLibcore<A>(job: Job<A>): Promise<A> { let counter = 0
export default async function withLibcore<A>(job: Job<A>): Promise<A> {
const core = require('./init-libcore').default const core = require('./init-libcore').default
core.getPoolInstance() core.getPoolInstance()
try {
if (counter++ === 0) {
process.send({
type: 'setLibcoreBusy',
busy: true,
})
}
return job(core) return job(core)
} finally {
if (--counter === 0) {
process.send({
type: 'setLibcoreBusy',
busy: false,
})
}
}
} }

59
src/index.ejs

@ -7,10 +7,14 @@
<%= __GLOBAL_STYLES__ %> <%= __GLOBAL_STYLES__ %>
body {
background: #f9f9f9;
}
#preload { #preload {
-webkit-app-region: drag; -webkit-app-region: drag;
align-items: center; align-items: center;
background: white; background: #f9f9f9;
bottom: 0; bottom: 0;
display: none; display: none;
flex-direction: column; flex-direction: column;
@ -20,13 +24,38 @@
position: fixed; position: fixed;
right: 0; right: 0;
top: 0; top: 0;
transition: opacity 0.4s ease-in-out; transition: opacity 0.5s ease-in-out;
z-index: 100; z-index: 100;
} }
#preload video { #preload .logo {
height: 144px; height: 80px;
width: 256px; width: 80px;
animation: logo 4s infinite 0.5s;
transform-origin: 50% 50%;
opacity: 0;
transition: opacity 0.5s ease-in-out;
}
@keyframes logo {
0% {
transform: rotate(0deg);
}
20% {
transform: rotate(360deg);
}
30% {
transform: rotate(360deg);
}
50% {
transform: rotate(720deg);
}
60% {
transform: rotate(720deg);
}
100% {
transform: rotate(0deg);
}
} }
</style> </style>
@ -41,7 +70,7 @@
</head> </head>
<body> <body>
<div id="preload"> <div id="preload">
<video autoplay loop muted src="<%= __DEV__ ? '.' : '../static' %>/videos/loader.mp4" /> <img class="logo" src="<%= __DEV__ ? '.' : '../static' %>/images/ledgerlive-logo.svg" alt="" />
</div> </div>
<div id="app"></div> <div id="app"></div>
<script> <script>
@ -50,6 +79,7 @@
const preloadEl = document.getElementById('preload') const preloadEl = document.getElementById('preload')
const appEl = document.getElementById('app') const appEl = document.getElementById('app')
const logoEl = preloadEl.querySelector('.logo')
const initApp = (options = {}) => { const initApp = (options = {}) => {
const { force = false } = options const { force = false } = options
@ -64,18 +94,15 @@
} }
if (name === 'MainWindow') { if (name === 'MainWindow') {
setTimeout(() => {
logoEl.style.opacity = 1
}, 50)
preloadEl.style.display = 'flex' preloadEl.style.display = 'flex'
const startTime = Date.now()
let waitTime = 0 const PRELOAD_WAIT_TIME_MIN = 2000
const PRELOAD_WAIT_TIME_MIN = 2e3
const interval = setInterval(() => (waitTime += 250), 250)
window.onAppReady = () => { window.onAppReady = () => {
const delay = PRELOAD_WAIT_TIME_MIN - waitTime const delay = Math.max(0, PRELOAD_WAIT_TIME_MIN - (Date.now() - startTime))
setTimeout(initApp, delay)
clearInterval(interval)
setTimeout(initApp, delay > 0 ? delay : 1)
} }
} else { } else {
initApp({ force: true }) initApp({ force: true })

15
src/main/bridge.js

@ -97,16 +97,19 @@ ipcMainListenReceiveCommands({
}) })
function handleGlobalInternalMessage(payload) { function handleGlobalInternalMessage(payload) {
if (payload.type === 'executeHttpQueryOnRenderer') { switch (payload.type) {
case 'setLibcoreBusy':
case 'setDeviceBusy':
case 'executeHttpQueryOnRenderer': {
const win = getMainWindow && getMainWindow() const win = getMainWindow && getMainWindow()
if (!win) { if (!win) {
logger.warn("can't executeHttpQueryOnRenderer because no renderer") logger.warn(`can't ${payload.type} because no renderer`)
return return
} }
win.webContents.send('executeHttpQuery', { win.webContents.send(payload.type, payload)
id: payload.id, break
networkArg: payload.networkArg, }
}) default:
} }
} }

14
src/reducers/onboarding.js

@ -24,6 +24,7 @@ export type OnboardingState = {
isGenuineFail: boolean, isGenuineFail: boolean,
isDeviceGenuine: boolean, isDeviceGenuine: boolean,
genuineCheckUnavailable: ?Error, genuineCheckUnavailable: ?Error,
displayErrorScreen: boolean,
}, },
isLedgerNano: boolean | null, isLedgerNano: boolean | null,
flowType: string, flowType: string,
@ -38,6 +39,7 @@ const state: OnboardingState = {
isGenuineFail: false, isGenuineFail: false,
isDeviceGenuine: false, isDeviceGenuine: false,
genuineCheckUnavailable: null, genuineCheckUnavailable: null,
displayErrorScreen: false,
}, },
isLedgerNano: null, isLedgerNano: null,
flowType: '', flowType: '',
@ -71,7 +73,7 @@ const state: OnboardingState = {
}, },
{ {
name: 'selectDevice', name: 'selectDevice',
label: 'Select Device', label: 'onboarding:breadcrumb.selectDevice',
options: { options: {
showFooter: false, showFooter: false,
showBackground: true, showBackground: true,
@ -80,7 +82,7 @@ const state: OnboardingState = {
}, },
{ {
name: 'selectPIN', name: 'selectPIN',
label: 'Select PIN', label: 'onboarding:breadcrumb.selectPIN',
options: { options: {
showFooter: false, showFooter: false,
showBackground: true, showBackground: true,
@ -89,7 +91,7 @@ const state: OnboardingState = {
}, },
{ {
name: 'writeSeed', name: 'writeSeed',
label: 'Write Seed', label: 'onboarding:breadcrumb.writeSeed',
options: { options: {
showFooter: false, showFooter: false,
showBackground: true, showBackground: true,
@ -98,7 +100,7 @@ const state: OnboardingState = {
}, },
{ {
name: 'genuineCheck', name: 'genuineCheck',
label: 'Genuine Check', label: 'onboarding:breadcrumb.genuineCheck',
options: { options: {
showFooter: false, showFooter: false,
showBackground: true, showBackground: true,
@ -107,7 +109,7 @@ const state: OnboardingState = {
}, },
{ {
name: 'setPassword', name: 'setPassword',
label: 'Set Password', label: 'onboarding:breadcrumb.setPassword',
options: { options: {
showFooter: false, showFooter: false,
showBackground: true, showBackground: true,
@ -116,7 +118,7 @@ const state: OnboardingState = {
}, },
{ {
name: 'analytics', name: 'analytics',
label: 'Analytics & Bug report', label: 'onboarding:breadcrumb.analytics',
options: { options: {
showFooter: false, showFooter: false,
showBackground: true, showBackground: true,

16
src/renderer/events.js

@ -15,7 +15,9 @@ import network from 'api/network'
import { ipcRenderer } from 'electron' import { ipcRenderer } from 'electron'
import debug from 'debug' import debug from 'debug'
import { CHECK_UPDATE_DELAY } from 'config/constants' import { CHECK_UPDATE_DELAY, DISABLE_ACTIVITY_INDICATORS } from 'config/constants'
import { onSetDeviceBusy } from 'components/DeviceBusyIndicator'
import { onSetLibcoreBusy } from 'components/LibcoreBusyIndicator'
import { hasPassword } from 'reducers/settings' import { hasPassword } from 'reducers/settings'
import { lock } from 'reducers/application' import { lock } from 'reducers/application'
@ -87,7 +89,7 @@ export default ({ store }: { store: Object }) => {
} }
}) })
ipcRenderer.on('executeHttpQuery', (event: any, { networkArg, id }) => { ipcRenderer.on('executeHttpQueryOnRenderer', (event: any, { networkArg, id }) => {
network(networkArg).then( network(networkArg).then(
result => { result => {
ipcRenderer.send('executeHttpQueryPayload', { type: 'success', id, result }) ipcRenderer.send('executeHttpQueryPayload', { type: 'success', id, result })
@ -98,6 +100,16 @@ export default ({ store }: { store: Object }) => {
) )
}) })
if (!DISABLE_ACTIVITY_INDICATORS) {
ipcRenderer.on('setLibcoreBusy', (event: any, { busy }) => {
onSetLibcoreBusy(busy)
})
ipcRenderer.on('setDeviceBusy', (event: any, { busy, devicePath }) => {
onSetDeviceBusy(devicePath, busy)
})
}
if (__PROD__) { if (__PROD__) {
// TODO move this to "command" pattern // TODO move this to "command" pattern
const updaterHandlers = { const updaterHandlers = {

3
src/renderer/init.js

@ -27,6 +27,7 @@ import hardReset from 'helpers/hardReset'
import sentry from 'sentry/browser' import sentry from 'sentry/browser'
import App from 'components/App' import App from 'components/App'
import AppError from 'components/AppError'
import 'styles/global' import 'styles/global'
@ -99,5 +100,5 @@ function r(Comp) {
init().catch(e => { init().catch(e => {
// for now we make the app crash instead of pending forever. later we can render the error OR try to recover, but probably this is unrecoverable cases. // for now we make the app crash instead of pending forever. later we can render the error OR try to recover, but probably this is unrecoverable cases.
logger.error(e) logger.error(e)
process.exit(1) r(<AppError error={e} language="en" />)
}) })

22
static/i18n/en/app.yml

@ -11,7 +11,11 @@ common:
chooseWalletPlaceholder: Choose a wallet... chooseWalletPlaceholder: Choose a wallet...
currency: Currency currency: Currency
selectAccount: Select an account selectAccount: Select an account
selectAccountNoOption: 'No account matching "{{accountName}}"'
selectCurrency: Select a currency selectCurrency: Select a currency
selectCurrencyNoOption: 'No currency matching "{{currencyName}}"'
selectExchange: Select an exchange
selectExchangeNoOption: 'No exchange matching "{{exchangeName}}"'
sortBy: Sort by sortBy: Sort by
search: Search search: Search
save: Save save: Save
@ -23,6 +27,7 @@ common:
next: Next next: Next
back: Back back: Back
retry: Retry retry: Retry
stop: Stop
close: Close close: Close
eastern: Eastern eastern: Eastern
western: Western western: Western
@ -37,6 +42,7 @@ common:
upToDate: Up to date upToDate: Up to date
error: Sync error. error: Sync error.
refresh: Refresh refresh: Refresh
ago: Synced {{time}}
error: error:
load: Unable to load load: Unable to load
noResults: No results noResults: No results
@ -138,12 +144,13 @@ addAccounts:
editName: Edit name editName: Edit name
newAccount: New account newAccount: New account
legacyAccount: '{{accountName}} (legacy)' legacyAccount: '{{accountName}} (legacy)'
noAccountToImport: We didnt find any {{currencyName}}} account to import. noAccountToImport: We didnt find any {{currencyName}} account to import.
success: Great success! success: Great success!
createNewAccount: createNewAccount:
title: Create new account title: Create new account
noOperationOnLastAccount: You cannot create a new account because your last account has no operations noOperationOnLastAccount: You cannot create a new account because your last account has no operations
retrySync: Retry sync noAccountToCreate: We didnt find any {{currencyName}} account to create.
somethingWentWrong: Something went wrong during synchronization.
cta: cta:
create: 'Create account' create: 'Create account'
import: 'Import account' import: 'Import account'
@ -158,8 +165,9 @@ operationDetails:
fees: Fees fees: Fees
from: From from: From
to: To to: To
identifier: Identifier identifier: Hash
viewOperation: View operation viewOperation: View operation
showMore: See {{recipients}} more
operationList: operationList:
noMoreOperations: No more operations noMoreOperations: No more operations
manager: manager:
@ -361,6 +369,10 @@ update:
newVersionReady: A new update is available. newVersionReady: A new update is available.
relaunch: Update now relaunch: Update now
crash: crash:
oops: Oops, something went wrong.
uselessText: You may try again by restarting Ledger Live. Please export your logs and contact Ledger Support if the problem persists.
restart: Restart app restart: Restart app
reset: Reset app files reset: Hard reset
createTicket: Create ticket createTicket: Create issue
showDetails: Show details
showError: Show error

11
static/i18n/en/errors.yml

@ -1,7 +1,9 @@
RangeError: {{message}} generic: An error occurred
Error: {{message}} RangeError: '{{message}}'
LedgerAPIErrorWithMessage: {{message}} Error: '{{message}}'
TransportStatusError: {{message}} LedgerAPIErrorWithMessage: '{{message}}'
TransportStatusError: '{{message}}'
TimeoutError: 'Timeout reached'
FeeEstimationFailed: 'fee estimation failed (status: {{status}})' FeeEstimationFailed: 'fee estimation failed (status: {{status}})'
NotEnoughBalance: 'Not enough balance' NotEnoughBalance: 'Not enough balance'
BtcUnmatchedApp: 'You must open application ‘{{currencyName}}’ on the device' BtcUnmatchedApp: 'You must open application ‘{{currencyName}}’ on the device'
@ -17,3 +19,4 @@ WebsocketConnectionFailed: Failed to establish a socket connection
DeviceSocketFail: Device socket failure DeviceSocketFail: Device socket failure
DeviceSocketNoBulkStatus: Device socket failure (bulk) DeviceSocketNoBulkStatus: Device socket failure (bulk)
DeviceSocketNoHandler: Device socket failure (handler {{query}}) DeviceSocketNoHandler: Device socket failure (handler {{query}})
LatestMCUInstalledError: The latest MCU is already installed on the Device

142
static/i18n/en/onboarding.yml

@ -1,20 +1,23 @@
breadcrumb:
selectDevice: Select device
selectPIN: Choose PIN
writeSeed: Recovery phrase
genuineCheck: Security check
setPassword: Encrypt data
analytics: Analytics
start: start:
title: Welcome to Ledger Live title: Welcome to Ledger Live
startBtn: Get Started startBtn: Get started
init: init:
title: Welcome to Ledger Live, the computer companion app to your Ledger device title: Get started with your Ledger device
newDevice: newDevice:
title: Initialize your new Ledger device title: Initialize a new Ledger device
desc: Please replace it with the final wording once it’s done.
restoreDevice: restoreDevice:
title: Restore a Ledger device title: Restore a Ledger device
desc: Please replace it with the final wording once it’s done.
initializedDevice: initializedDevice:
title: I have already initialized my device title: Use a device that's already initialized
desc: Please replace it with the final wording once it’s done.
noDevice: noDevice:
title: Do not have a Ledger device yet? title: Do not have a Ledger device yet?
desc: Please replace it with the final wording once it’s done.
noDevice: noDevice:
title: Do not have a Ledger device yet? title: Do not have a Ledger device yet?
buyNew: buyNew:
@ -24,100 +27,113 @@ noDevice:
learnMore: learnMore:
title: Learn about Ledger Live title: Learn about Ledger Live
selectDevice: selectDevice:
title: To get started, select your device title: Select your device
ledgerNanoCard: ledgerNanoCard:
title: Ledger Nano S title: Ledger Nano S
desc: Please replace it with the final wording once it’s done.
ledgerBlueCard: ledgerBlueCard:
title: Ledger Blue title: Ledger Blue
desc: Please replace it with the final wording once it’s done.
selectPIN: selectPIN:
title: Start initialization & choose your PIN code # initialize:
title: Start initialization - Choose your PIN code
instructions: instructions:
ledgerNano: ledgerNano:
step1: Connect the Ledger Nano S to your computer. step1: Connect the Ledger Nano S to your computer.
step2: Press both buttons simultaneously as instructed on the screen. step2: Press both buttons simultaneously as instructed on the screen.
step3: Press the right button to select Configure as new device. step3: Press the right button to select Configure as new device?. # <bold>Configure as new device?<bold>.
step4: Choose a PIN code between 4 and 8 digits long. step4: 'Choose a PIN code between 4 and 8 digits long. Then select the checkmark (✓).'
ledgerBlue: ledgerBlue:
step1: Connect the Ledger Blue to your computer. step1: Connect the Ledger Blue to your computer.
step2: Tap on Configure as new device. step2: Tap on Configure as new device.
step3: Choose a PIN code between 4 and 8 digits long. step3: Choose a PIN code between 4 and 8 digits long.
# restore:
# title: Start restoration - Choose your PIN code
# instructions:
# nano:
# step1: Connect the Ledger Nano S to your computer.
# step2: Press both buttons simultaneously as instructed on the screen.
# step3: Press the left button to cancel Initialize as new device?. Press the right button to select Restore configuration?. # <bold>Initialize as new device?</bold> <bold>Restore configuration?</bold>.
# step4: 'Choose a PIN code between 4 and 8 digits long. Then select the checkmark (✓).'
# blue:
# step1: Connect the Ledger Blue to your computer.
# step2: Tap on Restore configuration. # <bold>Restore configuration</bold>.
# step3: Choose a PIN code between 4 and 8 digits long.
disclaimer: disclaimer:
note1: Choose your own PIN code. This code unlocks your device. note1: Choose your own PIN code. This code will unlock your device.
note2: An 8-digit PIN code offers an optimum level of security. note2: An 8-digit PIN code offers an optimum level of security.
note3: Never use a device supplied with a PIN code and/or a 24-word recovery phrase. note3: Never use a device supplied with a PIN code or a 24-word recovery phrase.
writeSeed: writeSeed:
restore:
title: Save your recovery phrase title: Save your recovery phrase
desc: Your recovery phrase is formed by 24 words. They will be displayed only once. desc: Your device will generate a recovery phrase of 24 words, displayed only once.
step1: Press the right button to select the length of your recovery phrase. Press both buttons to confirm.
step2: 'Select the first letters of Word #1 by pressing the right or left button. Press both buttons to confirm each letter.'
step3: 'Select Word #1 from the suggested words. Press both buttons to continue.'
step4: Repeat the process until the last word.
nano: nano:
title: Save your recovery phrase step1: 'Copy the word displayed below Word #1 in position 1 on a blank Recovery sheet.' # <bold>Word #1</bold> <italic>Recovery sheet</italic>
desc: Your recovery phrase is formed by 24 words. They will be displayed only once. step2: 'Press the right button to display Word #2 and repeat the process until all 24 words are copied on the Recovery sheet.' # <bold>Word #2</bold> <italic>Recovery sheet</italic>
step1: 'Copy the first word (Word #1) in position 1 on the blank Recovery sheet.' step3: 'Confirm your recovery phrase: select each requested word and press both buttons to validate it.'
step2: 'Press the right button to display Word #2 and repeat the process until all 24 words are copied on the Recovery sheet.'
step3: Confirm your recovery phrase press both buttons to validate each word displayed on the screen.
blue: blue:
title: Save your recovery phrase step1: Copy each word of the recovery phrase on a blank Recovery sheet. Copy the words in the same order. # <i>Recovery sheet</i>
desc: Your recovery phrase is formed by 24 words. They will be displayed only once. step2: Tap Next to move to the next words. Repeat the process until the Confirmation screen appears. # <bold>Next</bold> <bold>Confirmation</bold>
step1: Copy each word of the recovery phrase on the blank Recovery sheet. Make sure to copy the words in the same order. step3: Type each requested word to confirm your recovery phrase.
step2: Tap NEXT to display the following words. Tap PREVIOUS to go back. restore:
step3: Enter the requested words to confirm your recovery phrase. title: Enter your recovery phrase
desc: Copy the 24-word recovery phrase from your Recovery sheet on your device.
nano:
step1: Select the length of your recovery phrase. Press both buttons to continue.
step2: 'Select the first letters of Word #1 by pressing the right or left button. Press both buttons to confirm each letter.' # <bold>Word #1</bold>
step3: 'Select Word #1 from the suggested words. Press both buttons to continue.' # <bold>Word #1</bold>
step4: Repeat the process until the last word.
ledgerBlue:
step1: Select the length of your recovery phrase.
step2: Type the first word of your recovery phrase. Select the word when it appears.
step3: Repeat the process until the last word.
disclaimer: disclaimer:
note1: Carefully secure your 24 words out of sight. note1: Carefully secure your 24-word recovery phrase out of sight.
note2: Ledger does not keep any backup of your 24 words. note2: Ledger does not keep any backup of your recovery phrase.
note3: Make sure you are the sole holder of the 24-word recovery phrase. note3: Make sure you are the sole holder of your recovery phrase.
note4: Never use a device supplied with a recovery phrase and/or a PIN code. note4: Never use a device supplied with a recovery phrase or a PIN code.
genuineCheck: genuineCheck:
title: Final security check title: Final security check
descNano: Your Ledger Nano S should now display Your device is now ready. Before getting started, please confirm that descNano: Your Ledger Nano S should now display Your device is now ready. Before getting started, please confirm that
descBlue: Your Ledger Blue should now display Your device is now ready. Before getting started, please confirm that descBlue: #Your Ledger Blue should now display Your device is now ready. Before getting started, please confirm that
steps: steps:
step1: step1:
title: Did you choose your PIN code by yourself? title: Did you choose your PIN code by yourself?
desc: This is a long text, please replace it with the final wording once it’s done. Lorem ipsum dolor amet ledger lorem dolor
step2: step2:
title: Did you save your recovery phrase by yourself? title: Did you save your recovery phrase by yourself?
desc: This is a long text, please replace it with the final wording once it’s done. Lorem ipsum dolor amet ledger lorem dolor desc:
step3: step3:
title: Check if your Ledger device is genuine title: Check if your Ledger device is genuine
desc: This is a long text, please replace it with the final wording once it’s done. Lorem ipsum dolor amet ledger lorem dolor desc:
isGenuinePassed: Your Nano S is genuine isGenuinePassed: 'Genuine'
buttons: buttons:
genuineCheck: Genuine check genuineCheck: Genuine check
contactSupport: Contact Support contactSupport: Ledger Support
errorPage: errorPage:
ledgerNano: ledgerNano:
title: Something is wrong with your Ledger Nano S title: Oops, something went wrong...
desc: A problem occurred with your Ledger Nano S. Contact Ledger Support to get assistance or go back to the security check. desc: Go back to the security check or request Ledger Support assistance.
ledgerBlue: ledgerBlue:
title: Something is wrong with your Ledger Blue title: Oops, something went wrong...
desc: A problem occurred with your Ledger Blue. Contact Ledger Support to get assistance or go back to the security check. desc: Go back to the security check or request Ledger Support assistance.
setPassword: setPassword:
title: Protect your privacy (optional) title: Encrypt Ledger Live data
desc: Set a password to prevent unauthorized access to Ledger Live data stored on your computer, including account names, balances, transactions and public addresses. desc: Enhance your privacy. Set a password to encrypt Ledger Live data stored on your computer, including account names, balances, transactions and public addresses.
disclaimer: disclaimer:
note1: Make sure to remember your password and do not share it. note1: Make sure to remember your password. Do not share it.
note2: Losing your password requires resetting Ledger Live and re-adding accounts. note2: Losing your password requires resetting Ledger Live and re-adding accounts.
note3: Loss of password doesn’t affect your crypto-assets. note3: Resetting Ledger Live does not affect your crypto-assets.
password: Password
confirmPassword: Confirm password
skipThisStep: Skip this step
analytics: analytics:
title: Help Ledger to improve its products and services title: Analytics and bug reports
desc: This is a long text, please replace it with the final wording once it’s done.
Lorem ipsum dolor amet ledger lorem dolor ipsum amet desc: Share anonymous usage and diagnostics data to help improve Ledger products, services and security features.
shareAnalytics: shareAnalytics:
title: Share analytics title: Share usage data
desc: Help Ledger improve its products and services by automatically sending diagnostics and usage data. desc: Enable analytics of anonymous data to help Ledger improve its user's experience. This includes the operating system, language, firmware versions and the number of added accounts.
termsConditions:
title: Terms and Conditions
desc: Please accept terms and conditions to proceed
sentryLogs: sentryLogs:
title: Sentry Logs title: Report bugs
desc: Help Ledger improve its products and services by automatically sending diagnostics and usage data. desc: Automatically send bug reports to help Ledger developers diagnose issues and improve Ledger Live performance.
finish: finish:
title: This is the title of the screen. 1 line is the maximum title: 'Ready for launch!'
desc: This is a long text, please replace it with the final wording once it’s done.
Lorem ipsum dolor amet ledger lorem dolor ipsum amet desc: The value of crypto assets can go up or down. Balances shown in your portfolio may involve double conversions and are for indicative purposes only!
openAppButton: Open app openAppButton: Launch
followUsLabel: Follow us to stay updated followUsLabel:

22
static/i18n/fr/app.yml

@ -12,7 +12,11 @@ common:
chooseWalletPlaceholder: Choose a wallet... chooseWalletPlaceholder: Choose a wallet...
currency: Currency currency: Currency
selectAccount: Select an account selectAccount: Select an account
selectAccountNoOption: 'No account matching "{{accountName}}"'
selectCurrency: Select a currency selectCurrency: Select a currency
selectCurrencyNoOption: 'No currency matching "{{currencyName}}"'
selectExchange: Select an exchange
selectExchangeNoOption: 'No exchange matching "{{exchangeName}}"'
sortBy: Sort by sortBy: Sort by
search: Search search: Search
save: Save save: Save
@ -24,6 +28,7 @@ common:
next: Next next: Next
back: Back back: Back
retry: Retry retry: Retry
stop: Stop
close: Close close: Close
eastern: Eastern eastern: Eastern
western: Western western: Western
@ -38,6 +43,7 @@ common:
upToDate: Up to date upToDate: Up to date
error: Sync error. error: Sync error.
refresh: Refresh refresh: Refresh
ago: Synced {{time}}
error: error:
load: Unable to load load: Unable to load
noResults: No results noResults: No results
@ -139,12 +145,13 @@ addAccounts:
editName: Edit name editName: Edit name
newAccount: New account newAccount: New account
legacyAccount: '{{accountName}} (legacy)' legacyAccount: '{{accountName}} (legacy)'
noAccountToImport: We didnt find any {{currencyName}}} account to import. noAccountToImport: We didnt find any {{currencyName}} account to import.
success: Great success! success: Great success!
createNewAccount: createNewAccount:
title: Create new account title: Create new account
noOperationOnLastAccount: You cannot create a new account because your last account has no operations noOperationOnLastAccount: You cannot create a new account because your last account has no operations
retrySync: Retry sync noAccountToCreate: We didnt find any {{currencyName}} account to create.
somethingWentWrong: Something went wrong during synchronization.
cta: cta:
create: 'Create account' create: 'Create account'
import: 'Import account' import: 'Import account'
@ -159,8 +166,9 @@ operationDetails:
fees: Fees fees: Fees
from: From from: From
to: To to: To
identifier: Identifier identifier: Hash
viewOperation: View operation viewOperation: View operation
showMore: See {{recipients}} more
operationList: operationList:
noMoreOperations: No more operations noMoreOperations: No more operations
manager: manager:
@ -362,6 +370,10 @@ update:
newVersionReady: A new update is available. newVersionReady: A new update is available.
relaunch: Update now relaunch: Update now
crash: crash:
oops: Oops, something went wrong.
uselessText: You may try again by restarting Ledger Live. Please export your logs and contact Ledger Support if the problem persists.
restart: Restart app restart: Restart app
reset: Reset app files reset: Hard reset
createTicket: Create ticket createTicket: Create issue
showDetails: Show details
showError: Show error

15
static/i18n/fr/errors.yml

@ -1,12 +1,10 @@
--- ---
RangeError: generic: An error occurred
- message RangeError: '{{message}}'
Error: Error: '{{message}}'
- message LedgerAPIErrorWithMessage: '{{message}}'
LedgerAPIErrorWithMessage: TransportStatusError: '{{message}}'
- message TimeoutError: 'Timeout reached'
TransportStatusError:
- message
FeeEstimationFailed: 'fee estimation failed (status: {{status}})' FeeEstimationFailed: 'fee estimation failed (status: {{status}})'
NotEnoughBalance: 'Not enough balance' NotEnoughBalance: 'Not enough balance'
BtcUnmatchedApp: 'You must open application ‘{{currencyName}}’ on the device' BtcUnmatchedApp: 'You must open application ‘{{currencyName}}’ on the device'
@ -22,3 +20,4 @@ WebsocketConnectionFailed: Failed to establish a socket connection
DeviceSocketFail: Device socket failure DeviceSocketFail: Device socket failure
DeviceSocketNoBulkStatus: Device socket failure (bulk) DeviceSocketNoBulkStatus: Device socket failure (bulk)
DeviceSocketNoHandler: Device socket failure (handler {{query}}) DeviceSocketNoHandler: Device socket failure (handler {{query}})
LatestMCUInstalledError: The latest MCU is already installed on the Device

4
static/i18n/fr/language.yml

@ -1,3 +1,3 @@
--- ---
en: Anglais en: English
fr: Français fr: French

127
static/i18n/fr/onboarding.yml

@ -1,21 +1,24 @@
--- ---
breadcrumb:
selectDevice: Select device
selectPIN: Choose PIN
writeSeed: Recovery phrase
genuineCheck: Security check
setPassword: Encrypt data
analytics: Analytics
start: start:
title: Welcome to Ledger Live title: Welcome to Ledger Live
startBtn: Get Started startBtn: Get started
init: init:
title: Welcome to Ledger Live, the computer companion app to your Ledger device title: Get started with your Ledger device
newDevice: newDevice:
title: Initialize your new Ledger device title: Initialize a new Ledger device
desc: Please replace it with the final wording once it’s done.
restoreDevice: restoreDevice:
title: Restore a Ledger device title: Restore a Ledger device
desc: Please replace it with the final wording once it’s done.
initializedDevice: initializedDevice:
title: I have already initialized my device title: Use a device that's already initialized
desc: Please replace it with the final wording once it’s done.
noDevice: noDevice:
title: Do not have a Ledger device yet? title: Do not have a Ledger device yet?
desc: Please replace it with the final wording once it’s done.
noDevice: noDevice:
title: Do not have a Ledger device yet? title: Do not have a Ledger device yet?
buyNew: buyNew:
@ -25,100 +28,100 @@ noDevice:
learnMore: learnMore:
title: Learn about Ledger Live title: Learn about Ledger Live
selectDevice: selectDevice:
title: To get started, select your device title: Select your device
ledgerNanoCard: ledgerNanoCard:
title: Ledger Nano S title: Ledger Nano S
desc: Please replace it with the final wording once it’s done.
ledgerBlueCard: ledgerBlueCard:
title: Ledger Blue title: Ledger Blue
desc: Please replace it with the final wording once it’s done.
selectPIN: selectPIN:
title: Start initialization & choose your PIN code title: Start initialization - Choose your PIN code
instructions: instructions:
ledgerNano: ledgerNano:
step1: Connect the Ledger Nano S to your computer. step1: Connect the Ledger Nano S to your computer.
step2: Press both buttons simultaneously as instructed on the screen. step2: Press both buttons simultaneously as instructed on the screen.
step3: Press the right button to select Configure as new device. step3: Press the right button to select Configure as new device?.
step4: Choose a PIN code between 4 and 8 digits long. step4: 'Choose a PIN code between 4 and 8 digits long. Then select the checkmark (✓).'
ledgerBlue: ledgerBlue:
step1: Connect the Ledger Blue to your computer. step1: Connect the Ledger Blue to your computer.
step2: Tap on Configure as new device. step2: Tap on Configure as new device.
step3: Choose a PIN code between 4 and 8 digits long. step3: Choose a PIN code between 4 and 8 digits long.
disclaimer: disclaimer:
note1: Choose your own PIN code. This code unlocks your device. note1: Choose your own PIN code. This code will unlock your device.
note2: An 8-digit PIN code offers an optimum level of security. note2: An 8-digit PIN code offers an optimum level of security.
note3: Never use a device supplied with a PIN code and/or a 24-word recovery phrase. note3: Never use a device supplied with a PIN code or a 24-word recovery phrase.
writeSeed: writeSeed:
restore:
title: Save your recovery phrase title: Save your recovery phrase
desc: Your recovery phrase is formed by 24 words. They will be displayed only once. desc: Your device will generate a recovery phrase of 24 words, displayed only once.
step1: Press the right button to select the length of your recovery phrase. Press both buttons to confirm. nano:
step1: 'Copy the word displayed below Word #1 in position 1 on a blank Recovery sheet.'
step2: 'Press the right button to display Word #2 and repeat the process until all 24 words are copied on the Recovery sheet.'
step3: 'Confirm your recovery phrase: select each requested word and press both buttons to validate it.'
ledgerBlue:
step1: Copy each word of the recovery phrase on a blank Recovery sheet. Copy the words in the same order.
step2: Tap Next to move to the next words. Repeat the process until the Confirmation screen appears.
step3: Type each requested word to confirm your recovery phrase.
restore:
title: Enter your recovery phrase
desc: Copy the 24-word recovery phrase from your Recovery sheet on your device.
nano:
step1: Select the length of your recovery phrase. Press both buttons to continue.
step2: 'Select the first letters of Word #1 by pressing the right or left button. Press both buttons to confirm each letter.' step2: 'Select the first letters of Word #1 by pressing the right or left button. Press both buttons to confirm each letter.'
step3: 'Select Word #1 from the suggested words. Press both buttons to continue.' step3: 'Select Word #1 from the suggested words. Press both buttons to continue.'
step4: Repeat the process until the last word. step4: Repeat the process until the last word.
nano: ledgerBlue:
title: Save your recovery phrase step1: Select the length of your recovery phrase.
desc: Your recovery phrase is formed by 24 words. They will be displayed only once. step2: Type the first word of your recovery phrase. Select the word when it appears.
step1: 'Copy the first word (Word #1) in position 1 on the blank Recovery sheet.' step3: Repeat the process until the last word.
step2: 'Press the right button to display Word #2 and repeat the process until all 24 words are copied on the Recovery sheet.'
step3: Confirm your recovery phrase press both buttons to validate each word displayed on the screen.
blue:
title: Save your recovery phrase
desc: Your recovery phrase is formed by 24 words. They will be displayed only once.
step1: Copy each word of the recovery phrase on the blank Recovery sheet. Make sure to copy the words in the same order.
step2: Tap NEXT to display the following words. Tap PREVIOUS to go back.
step3: Enter the requested words to confirm your recovery phrase.
disclaimer: disclaimer:
note1: Carefully secure your 24 words out of sight. note1: Carefully secure your 24-word recovery phrase out of sight.
note2: Ledger does not keep any backup of your 24 words. note2: Ledger does not keep any backup of your recovery phrase.
note3: Make sure you are the sole holder of the 24-word recovery phrase. note3: Make sure you are the sole holder of your recovery phrase.
note4: Never use a device supplied with a recovery phrase and/or a PIN code. note4: Never use a device supplied with a recovery phrase or a PIN code.
genuineCheck: genuineCheck:
title: Final security check title: Final security check
descNano: Your Ledger Nano S should now display Your device is now ready. Before getting started, please confirm that descNano: Your Ledger Nano S should now display Your device is now ready. Before getting started, please confirm that
descBlue: Your Ledger Blue should now display Your device is now ready. Before getting started, please confirm that descBlue:
steps: steps:
step1: step1:
title: Did you choose your PIN code by yourself? title: Did you choose your PIN code by yourself?
desc: This is a long text, please replace it with the final wording once it’s done. Lorem ipsum dolor amet ledger lorem dolor
step2: step2:
title: Did you save your recovery phrase by yourself? title: Did you save your recovery phrase by yourself?
desc: This is a long text, please replace it with the final wording once it’s done. Lorem ipsum dolor amet ledger lorem dolor desc:
step3: step3:
title: Check if your Ledger device is genuine title: Check if your Ledger device is genuine
desc: This is a long text, please replace it with the final wording once it’s done. Lorem ipsum dolor amet ledger lorem dolor desc:
isGenuinePassed: Your Nano S is genuine isGenuinePassed: 'Genuine'
buttons: buttons:
genuineCheck: Genuine check genuineCheck: Genuine check
contactSupport: Contact Support contactSupport: Ledger Support
errorPage: errorPage:
ledgerNano: ledgerNano:
title: Something is wrong with your Ledger Nano S title: Oops, something went wrong...
desc: A problem occurred with your Ledger Nano S. Contact Ledger Support to get assistance or go back to the security check. desc: Go back to the security check or request Ledger Support assistance.
ledgerBlue: ledgerBlue:
title: Something is wrong with your Ledger Blue title: Oops, something went wrong...
desc: A problem occurred with your Ledger Blue. Contact Ledger Support to get assistance or go back to the security check. desc: Go back to the security check or request Ledger Support assistance.
setPassword: setPassword:
title: Protect your privacy (optional) title: Encrypt Ledger Live data
desc: Set a password to prevent unauthorized access to Ledger Live data stored on your computer, including account names, balances, transactions and public addresses. desc: Enhance your privacy. Set a password to encrypt Ledger Live data stored on your computer, including account names, balances, transactions and public addresses.
disclaimer: disclaimer:
note1: Make sure to remember your password and do not share it. note1: Make sure to remember your password. Do not share it.
note2: Losing your password requires resetting Ledger Live and re-adding accounts. note2: Losing your password requires resetting Ledger Live and re-adding accounts.
note3: Loss of password doesn’t affect your crypto-assets. note3: Resetting Ledger Live does not affect your crypto-assets.
password: Password
confirmPassword: Confirm password
skipThisStep: Skip this step
analytics: analytics:
title: Help Ledger to improve its products and services title: Analytics and bug reports
desc: This is a long text, please replace it with the final wording once it’s done.
Lorem ipsum dolor amet ledger lorem dolor ipsum amet desc: Share anonymous usage and diagnostics data to help improve Ledger products, services and security features.
shareAnalytics: shareAnalytics:
title: Share analytics title: Share usage data
desc: Help Ledger improve its products and services by automatically sending diagnostics and usage data. desc: Enable analytics of anonymous data to help Ledger improve its user's experience. This includes the operating system, language, firmware versions and the number of added accounts.
termsConditions:
title: Terms and Conditions
desc: Please accept terms and conditions to proceed
sentryLogs: sentryLogs:
title: Sentry Logs title: Report bugs
desc: Help Ledger improve its products and services by automatically sending diagnostics and usage data. desc: Automatically send bug reports to help Ledger developers diagnose issues and improve Ledger Live performance.
finish: finish:
title: This is the title of the screen. 1 line is the maximum title: 'Ready for launch!'
desc: This is a long text, please replace it with the final wording once it’s done.
Lorem ipsum dolor amet ledger lorem dolor ipsum amet desc: The value of crypto assets can go up or down. Balances shown in your portfolio may involve double conversions and are for indicative purposes only!
openAppButton: Open app openAppButton: Launch
followUsLabel: Follow us to stay updated followUsLabel:

1
static/images/crash-screen.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 28 KiB

28
static/images/ledgerlive-logo.svg

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="80px" height="80px" viewBox="0 0 80 80" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
<title>Icon</title>
<desc>Created with Sketch.</desc>
<defs>
<linearGradient x1="50%" y1="100.845352%" x2="50%" y2="2.10398089%" id="linearGradient-1">
<stop stop-color="#165EDB" offset="0%"></stop>
<stop stop-color="#99B9FF" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="1-copy" transform="translate(-472.000000, -344.000000)">
<g id="Icon" transform="translate(472.000000, 344.000000)">
<g id="Content" transform="translate(0.523092, 0.523092)">
<g id="Left">
<path d="M11.627774,11.627774 C18.1063804,5.1491675 26.4524748,1.22166415 35.3024827,0.242191779 C35.9187763,0.173983645 36.7320017,0.116916287 37.742159,0.0709897041 C38.8171483,0.0221163811 39.7282196,0.853947022 39.7770937,1.92893627 C39.7772154,2.92085812 39.7756841,3.65154836 39.7745064,4.21358474 L39.7606716,10.8155806 C39.7585189,11.8422703 38.9600575,12.6911342 37.9354198,12.7560494 C36.8385386,12.8255415 35.9664768,12.9129995 35.3192345,13.0184235 C29.808626,13.916001 24.6612519,16.5052205 20.5832362,20.5832362 C11.5847878,29.5816845 10.2557955,43.345322 16.5962594,53.751324 L25.7416776,44.6059058 C26.5025953,43.844988 27.7362867,43.844988 28.4972044,44.6059058 L34.6971398,50.8058411 C35.4580575,51.5667589 35.4580575,52.8004502 34.6971398,53.561368 L20.5337322,67.7247755 C18.0607496,70.1977581 14.0512526,70.1977581 11.57827,67.7247755 C11.5344014,67.6809069 11.491311,67.6365548 11.4489987,67.5917363 C-3.87569382,52.0740949 -3.81610208,27.07165 11.627774,11.627774 Z" fill="url(#linearGradient-1)" fill-rule="nonzero"></path>
<path d="M11.2499149,67.370816 L10.8613061,66.9840734 C9.62152199,65.6750214 8.48787594,64.3022341 7.46036802,62.8766244 L7.46892358,62.8681099 C5.74101341,60.4778085 4.31327483,57.9385703 3.18570784,55.3021417 C4.21813598,57.414571 6.38779952,58.8717088 8.89881752,58.8717088 C10.4057047,58.8717088 11.7896592,58.3469467 12.8768666,57.4707167 L25.7416776,44.6059058 C26.5025953,43.844988 27.7362867,43.844988 28.4972044,44.6059058 L34.6971398,50.8058411 C35.4580575,51.5667589 35.4580575,52.8004502 34.6971398,53.561368 L20.5337322,67.7247755 C18.0607496,70.1977581 14.0512526,70.1977581 11.57827,67.7247755 C11.463491,67.6099965 11.3540393,67.4919076 11.2499149,67.370816 Z" fill="#6490F1"></path>
</g>
<g id="Right" transform="translate(59.427777, 44.570833) scale(-1, -1) translate(-59.427777, -44.570833) translate(39.456147, 9.742259)">
<path d="M11.627774,11.627774 C18.1063804,5.1491675 26.4524748,1.22166415 35.3024827,0.242191779 C35.9187763,0.173983645 36.7320017,0.116916287 37.742159,0.0709897041 C38.8171483,0.0221163811 39.7282196,0.853947022 39.7770937,1.92893627 C39.7772154,2.92085812 39.7756841,3.65154836 39.7745064,4.21358474 L39.7606716,10.8155806 C39.7585189,11.8422703 38.9600575,12.6911342 37.9354198,12.7560494 C36.8385386,12.8255415 35.9664768,12.9129995 35.3192345,13.0184235 C29.808626,13.916001 24.6612519,16.5052205 20.5832362,20.5832362 C11.5847878,29.5816845 10.2557955,43.345322 16.5962594,53.751324 L25.7416776,44.6059058 C26.5025953,43.844988 27.7362867,43.844988 28.4972044,44.6059058 L34.6971398,50.8058411 C35.4580575,51.5667589 35.4580575,52.8004502 34.6971398,53.561368 L20.5337322,67.7247755 C18.0607496,70.1977581 14.0512526,70.1977581 11.57827,67.7247755 C11.5344014,67.6809069 11.491311,67.6365548 11.4489987,67.5917363 C-3.87569382,52.0740949 -3.81610208,27.07165 11.627774,11.627774 Z" id="Left" fill="url(#linearGradient-1)" fill-rule="nonzero"></path>
<path d="M11.2499149,67.370816 L10.8613061,66.9840734 C9.62152199,65.6750214 8.48787594,64.3022341 7.46036802,62.8766244 L7.46892358,62.8681099 C5.74101341,60.4778085 4.31327483,57.9385703 3.18570784,55.3021417 C4.21813598,57.414571 6.38779952,58.8717088 8.89881752,58.8717088 C10.4057047,58.8717088 11.7896592,58.3469467 12.8768666,57.4707167 L25.7416776,44.6059058 C26.5025953,43.844988 27.7362867,43.844988 28.4972044,44.6059058 L34.6971398,50.8058411 C35.4580575,51.5667589 35.4580575,52.8004502 34.6971398,53.561368 L20.5337322,67.7247755 C18.0607496,70.1977581 14.0512526,70.1977581 11.57827,67.7247755 C11.463491,67.6099965 11.3540393,67.4919076 11.2499149,67.370816 Z" id="Left" fill="#6490F1"></path>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
static/videos/loader.mp4

Binary file not shown.
Loading…
Cancel
Save