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. 37
      src/bridge/BridgeSyncContext.js
  12. 4
      src/bridge/EthereumJSBridge.js
  13. 41
      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. 18
      src/components/AccountPage/index.js
  24. 23
      src/components/AppError.js
  25. 95
      src/components/BalanceSummary/BalanceInfos.js
  26. 108
      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. 74
      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. 55
      src/components/TopBar/ActivityIndicator.js
  51. 17
      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. 64
      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. 175
      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. 20
      src/helpers/withLibcore.js
  84. 101
      src/index.ejs
  85. 21
      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. 144
      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. 131
      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 const addAccount: AddAccount = payload => ({
type: 'ADD_ACCOUNT',
type: 'DB:ADD_ACCOUNT',
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
import invariant from 'invariant'
import LRU from 'lru-cache'
import type { Currency } from '@ledgerhq/live-common/lib/types'
import createCustomErrorClass from 'helpers/createCustomErrorClass'
import { blockchainBaseURL } from './Ledger'
@ -11,10 +12,20 @@ export type Fees = {
[_: string]: number,
}
const cache = LRU({
maxAge: 5 * 60 * 1000,
})
export const getEstimatedFees = async (currency: Currency): Promise<Fees> => {
const key = currency.id
let promise = cache.get(key)
if (promise) return promise.then(r => r.data)
const baseURL = blockchainBaseURL(currency)
invariant(baseURL, `Fees for ${currency.id} are not supported`)
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) {
return data
}

37
src/bridge/BridgeSyncContext.js

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

4
src/bridge/EthereumJSBridge.js

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

41
src/bridge/RippleJSBridge.js

@ -256,14 +256,15 @@ const RippleJSBridge: WalletBridge<Transaction> = {
const derivations = getDerivations(currency)
for (const derivation of derivations) {
const legacy = derivation !== derivations[derivations.length - 1]
for (let index = 0; index < 255; index++) {
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 })
.toPromise()
if (finished) return
const accountId = `ripplejs:${currency.id}:${address}`
const accountId = `ripplejs:${currency.id}:${address}:${publicKey}`
let info
try {
@ -280,22 +281,24 @@ const RippleJSBridge: WalletBridge<Transaction> = {
if (!info) {
// account does not exist in Ripple server
// we are generating a new account locally
next({
id: accountId,
xpub: '',
name: getNewAccountPlaceholderName(currency, index),
freshAddress,
freshAddressPath,
balance: 0,
blockHeight: maxLedgerVersion,
index,
currency,
operations: [],
pendingOperations: [],
unit: currency.units[0],
archived: false,
lastSyncDate: new Date(),
})
if (!legacy) {
next({
id: accountId,
xpub: '',
name: getNewAccountPlaceholderName(currency, index),
freshAddress,
freshAddressPath,
balance: 0,
blockHeight: maxLedgerVersion,
index,
currency,
operations: [],
pendingOperations: [],
unit: currency.units[0],
archived: false,
lastSyncDate: new Date(),
})
}
break
}
@ -315,7 +318,7 @@ const RippleJSBridge: WalletBridge<Transaction> = {
const account: $Exact<Account> = {
id: accountId,
xpub: '',
name: getAccountPlaceholderName(currency, index),
name: getAccountPlaceholderName(currency, index, legacy),
freshAddress,
freshAddressPath,
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 { withDevice } from 'helpers/deviceAccess'
type Input = * // FIXME !
type Input = {
devicePath: string,
targetId: string | number,
version: string,
}
type Result = string
const cmd: Command<Input, Result> = createCommand('getIsGenuine', ({ devicePath, targetId }) =>
fromPromise(withDevice(devicePath)(transport => getIsGenuine(transport, { targetId }))),
const cmd: Command<Input, Result> = createCommand(
'getIsGenuine',
({ devicePath, targetId, version }) =>
fromPromise(
withDevice(devicePath)(transport => getIsGenuine(transport, { targetId, version })),
),
)
export default cmd

4
src/commands/index.js

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

9
src/commands/installApp.js

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

25
src/commands/installMcu.js

@ -3,24 +3,21 @@
import { createCommand, Command } from 'helpers/ipc'
import { fromPromise } from 'rxjs/observable/fromPromise'
// import { withDevice } from 'helpers/deviceAccess'
import { withDevice } from 'helpers/deviceAccess'
import installMcu from 'helpers/firmware/installMcu'
// type Input = {
// devicePath: string,
// firmware: Object,
// }
type Input = {
devicePath: string,
targetId: string | number,
version: string,
}
// type Result = {
// targetId: number | string,
// version: string,
// final: boolean,
// mcu: boolean,
// }
type Input = *
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

11
src/commands/installOsuFirmware.js

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

5
src/commands/listApps.js

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

9
src/commands/uninstallApp.js

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

18
src/components/AccountPage/index.js

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

95
src/components/BalanceSummary/BalanceInfos.js

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

108
src/components/BalanceSummary/index.js

@ -21,6 +21,7 @@ type Props = {
totalBalance: number,
sinceBalance: number,
refBalance: number,
isAvailable: boolean,
}) => *,
}
@ -37,54 +38,67 @@ const BalanceSummary = ({
return (
<Card p={0} py={5}>
<CalculateBalance accounts={accounts} daysCount={daysCount}>
{({ isAvailable, balanceHistory, balanceStart, balanceEnd }) =>
!isAvailable ? null : (
<Fragment>
{renderHeader ? (
<Box px={6}>
{renderHeader({
selectedTimeRange,
// FIXME refactor these
totalBalance: balanceEnd,
sinceBalance: balanceStart,
refBalance: balanceStart,
})}
</Box>
) : null}
<Box ff="Open Sans" fontSize={4} color="graphite" pt={6}>
<Chart
id={chartId}
unit={account ? account.unit : null}
color={chartColor}
data={balanceHistory}
height={200}
currency={counterValue}
tickXScale={selectedTimeRange}
renderTickY={val => formatShort(counterValue.units[0], val)}
renderTooltip={
isAvailable && !account
? d => (
<Fragment>
<FormattedVal
alwaysShowSign={false}
fontSize={5}
color="dark"
showCode
unit={counterValue.units[0]}
val={d.value}
/>
<Box ff="Open Sans|Regular" color="grey" fontSize={3} mt={2}>
{d.date.toISOString().substr(0, 10)}
</Box>
</Fragment>
)
: undefined
}
/>
{({ isAvailable, balanceHistory, balanceStart, balanceEnd }) => (
<Fragment>
{renderHeader ? (
<Box px={6}>
{renderHeader({
isAvailable,
selectedTimeRange,
// FIXME refactor these
totalBalance: balanceEnd,
sinceBalance: balanceStart,
refBalance: balanceStart,
})}
</Box>
</Fragment>
)
}
) : null}
<Box ff="Open Sans" fontSize={4} color="graphite" pt={6}>
<Chart
id={chartId}
unit={account ? account.unit : null}
color={!isAvailable ? '#eee' : chartColor}
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}
currency={counterValue}
tickXScale={selectedTimeRange}
renderTickY={
isAvailable ? val => formatShort(counterValue.units[0], val) : () => ''
}
isInteractive={isAvailable}
renderTooltip={
isAvailable && !account
? d => (
<Fragment>
<FormattedVal
alwaysShowSign={false}
fontSize={5}
color="dark"
showCode
unit={counterValue.units[0]}
val={d.value}
/>
<Box ff="Open Sans|Regular" color="grey" fontSize={3} mt={2}>
{d.date.toISOString().substr(0, 10)}
</Box>
</Fragment>
)
: undefined
}
/>
</Box>
</Fragment>
)}
</CalculateBalance>
</Card>
)

9
src/components/DashboardPage/index.js

@ -120,10 +120,17 @@ class DashboardPage extends PureComponent<Props> {
accounts={accounts}
selectedTimeRange={selectedTimeRange}
daysCount={daysCount}
renderHeader={({ totalBalance, selectedTimeRange, sinceBalance, refBalance }) => (
renderHeader={({
isAvailable,
totalBalance,
selectedTimeRange,
sinceBalance,
refBalance,
}) => (
<BalanceInfos
t={t}
counterValue={counterValue}
isAvailable={isAvailable}
totalBalance={totalBalance}
since={selectedTimeRange}
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 fontFamily from 'styles/styled/fontFamily'
import type { LedgerScriptParams } from 'helpers/common'
import { ff } from 'styles/helpers'
import Box from 'components/base/Box'
@ -12,20 +14,9 @@ import Search from 'components/base/Search'
import SearchIcon from 'icons/Search'
import CrossIcon from 'icons/Cross'
type LedgerApp = {
name: string,
version: string,
icon: string,
app: Object,
bolos_version: {
min: number,
max: number,
},
}
type Props = {
list: Array<LedgerApp>,
children: (list: Array<LedgerApp>) => React$Node,
list: Array<LedgerScriptParams>,
children: (list: Array<LedgerScriptParams>) => React$Node,
}
type State = {

41
src/components/ManagerPage/AppsList.js

@ -6,6 +6,7 @@ import styled from 'styled-components'
import { translate } from 'react-i18next'
import type { Device, T } from 'types/common'
import type { LedgerScriptParams } from 'helpers/common'
import listApps from 'commands/listApps'
import installApp from 'commands/installApp'
@ -43,27 +44,17 @@ const ICONS_FALLBACK = {
type Status = 'loading' | 'idle' | 'busy' | 'success' | 'error'
type Mode = 'home' | 'installing' | 'uninstalling'
type LedgerApp = {
name: string,
version: string,
icon: string,
app: Object,
bolos_version: {
min: number,
max: number,
},
}
type Props = {
device: Device,
targetId: string | number,
t: T,
version: string,
}
type State = {
status: Status,
error: string | null,
appsList: LedgerApp[],
appsList: LedgerScriptParams[] | Array<*>,
app: string,
mode: Mode,
}
@ -89,8 +80,8 @@ class AppsList extends PureComponent<Props, State> {
async fetchAppList() {
try {
const { targetId } = this.props
const appsList = CACHED_APPS || (await listApps.send({ targetId }).toPromise())
const { targetId, version } = this.props
const appsList = CACHED_APPS || (await listApps.send({ targetId, version }).toPromise())
CACHED_APPS = appsList
if (!this._unmounted) {
this.setState({ appsList, status: 'idle' })
@ -100,14 +91,14 @@ class AppsList extends PureComponent<Props, State> {
}
}
handleInstallApp = (args: { app: any, name: string }) => async () => {
const { app: appParams, name } = args
this.setState({ status: 'busy', app: name, mode: 'installing' })
handleInstallApp = (app: LedgerScriptParams) => async () => {
this.setState({ status: 'busy', app: app.name, mode: 'installing' })
try {
const {
device: { path: devicePath },
targetId,
} = this.props
const data = { appParams, devicePath }
const data = { app, devicePath, targetId }
await installApp.send(data).toPromise()
this.setState({ status: 'success', app: '' })
} catch (err) {
@ -115,14 +106,14 @@ class AppsList extends PureComponent<Props, State> {
}
}
handleUninstallApp = (args: { app: any, name: string }) => async () => {
const { app: appParams, name } = args
this.setState({ status: 'busy', app: name, mode: 'uninstalling' })
handleUninstallApp = (app: LedgerScriptParams) => async () => {
this.setState({ status: 'busy', app: app.name, mode: 'uninstalling' })
try {
const {
device: { path: devicePath },
targetId,
} = this.props
const data = { appParams, devicePath }
const data = { app, devicePath, targetId }
await uninstallApp.send(data).toPromise()
this.setState({ status: 'success', app: '' })
} catch (err) {
@ -184,15 +175,15 @@ class AppsList extends PureComponent<Props, State> {
}
renderList() {
const { appsList } = this.state
return appsList.length > 0 ? (
const { appsList, status } = this.state
return status === 'idle' ? (
<Box>
<AppSearchBar list={appsList}>
{items => (
<List>
{items.map(c => (
<ManagerApp
key={`${c.name}_${c.version}_${c.bolos_version.min}`}
key={`${c.name}_${c.version}`}
name={c.name}
version={`Version ${c.version}`}
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 mt={5}>
<AppsList device={device} targetId={deviceInfo.targetId} />
<AppsList device={device} targetId={deviceInfo.targetId} version={deviceInfo.version} />
</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 { LedgerScriptParams } from 'helpers/common'
import getLatestFirmwareForDevice from 'commands/getLatestFirmwareForDevice'
import installOsuFirmware from 'commands/installOsuFirmware'
@ -23,11 +25,6 @@ import UpdateFirmwareButton from './UpdateFirmwareButton'
let CACHED_LATEST_FIRMWARE = null
type FirmwareInfos = {
name: string,
notes: string,
}
type DeviceInfos = {
targetId: number | string,
version: string,
@ -40,7 +37,7 @@ type Props = {
}
type State = {
latestFirmware: ?FirmwareInfos,
latestFirmware: ?LedgerScriptParams,
}
class FirmwareUpdate extends PureComponent<Props, State> {
@ -84,12 +81,13 @@ class FirmwareUpdate extends PureComponent<Props, State> {
installFirmware = async () => {
try {
const { latestFirmware } = this.state
const { infos } = this.props
invariant(latestFirmware, 'did not find a new firmware or firmware is not set')
const {
device: { path: devicePath },
} = this.props
const { success } = await installOsuFirmware
.send({ devicePath, firmware: latestFirmware })
.send({ devicePath, firmware: latestFirmware, targetId: infos.targetId })
.toPromise()
if (success) {
this.fetchLatestFirmware()
@ -119,7 +117,9 @@ class FirmwareUpdate extends PureComponent<Props, State> {
</Box>
</Box>
<Text ff="Open Sans|SemiBold" fontSize={2}>
{t('app:manager.firmware.installed', { version: infos.version })}
{t('app:manager.firmware.installed', {
version: infos.version,
})}
</Text>
</Box>
<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,
}
const getCleanVersion = (input: string): string =>
input.endsWith('-osu') ? input.replace('-osu', '') : input
const UpdateFirmwareButton = ({ t, firmware, installFirmware }: Props) =>
firmware ? (
<Fragment>
<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>
<Button primary onClick={installFirmware}>
{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 WorkflowWithIcon from 'components/Workflow/WorkflowWithIcon'
import Dashboard from './Dashboard'
import FlashMcu from './FlashMcu'
type DeviceInfo = {
targetId: number | string,
@ -25,11 +26,11 @@ type Error = {
function ManagerPage(): Node {
return (
<Workflow
renderFinalUpdate={(deviceInfo: DeviceInfo) => (
renderFinalUpdate={(device: Device, deviceInfo: DeviceInfo) => (
<p>UPDATE FINAL FIRMARE (TEMPLATE + ACTION WIP) {deviceInfo.final}</p>
)}
renderMcuUpdate={(deviceInfo: DeviceInfo) => (
<p>FLASH MCU (TEMPLATE + ACTION WIP) {deviceInfo.mcu}</p>
renderMcuUpdate={(device: Device, deviceInfo: DeviceInfo) => (
<FlashMcu device={device} deviceInfo={deviceInfo} />
)}
renderDashboard={(device: Device, deviceInfo: DeviceInfo) => (
<Dashboard device={device} deviceInfo={deviceInfo} />

8
src/components/Onboarding/OnboardingBreadcrumb.js

@ -3,6 +3,7 @@
import React from 'react'
import { connect } from 'react-redux'
import findIndex from 'lodash/findIndex'
import { translate } from 'react-i18next'
import type { OnboardingState } from 'reducers/onboarding'
@ -14,15 +15,16 @@ const mapStateToProps = state => ({
type Props = {
onboarding: OnboardingState,
t: *,
}
function OnboardingBreadcrumb(props: Props) {
const { onboarding } = props
const { onboarding, t } = props
const { stepName, genuine } = onboarding
const filteredSteps = onboarding.steps
.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 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))

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

@ -82,10 +82,12 @@ class GenuineCheck extends PureComponent<StepProps, State> {
if (!item.pass) {
this.setState(INITIAL_STATE)
this.props.updateGenuineCheck({
isGenuineFail: true,
recoveryStepPass: false,
displayErrorScreen: true,
pinStepPass: false,
recoveryStepPass: false,
isGenuineFail: false,
isDeviceGenuine: false,
genuineCheckUnavailable: null,
})
}
}
@ -116,6 +118,7 @@ class GenuineCheck extends PureComponent<StepProps, State> {
isGenuineFail: true,
isDeviceGenuine: false,
genuineCheckUnavailable: null,
displayErrorScreen: true,
})
})
}
@ -125,12 +128,13 @@ class GenuineCheck extends PureComponent<StepProps, State> {
this.props.updateGenuineCheck({
isDeviceGenuine: false,
genuineCheckUnavailable: error,
displayErrorScreen: false,
})
})
}
redoGenuineCheck = () => {
this.props.updateGenuineCheck({ isGenuineFail: false })
this.props.updateGenuineCheck({ displayErrorScreen: false })
}
contactSupport = () => {
@ -153,7 +157,7 @@ class GenuineCheck extends PureComponent<StepProps, State> {
const { genuine } = onboarding
const { cachedPinStepButton, cachedRecoveryStepButton, isGenuineCheckModalOpened } = this.state
if (genuine.isGenuineFail) {
if (genuine.displayErrorScreen) {
return this.renderGenuineFail()
}
@ -225,17 +229,22 @@ class GenuineCheck extends PureComponent<StepProps, State> {
</GenuineSuccessText>
</Box>
) : genuine.genuineCheckUnavailable ? (
<Box horizontal 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
color="alertRed"
underline
onClick={this.handleOpenGenuineCheckModal}
>
{t('app:common.retry')}
</FakeLink>
<Box align="center" flow={1} color={colors.alertRed}>
<FakeLink
ff="Open Sans|Regular"
fontSize={4}
underline
onClick={this.handleOpenGenuineCheckModal}
>
{t('app:common.retry')}
</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>
) : (
@ -252,15 +261,32 @@ class GenuineCheck extends PureComponent<StepProps, State> {
</CardWrapper>
</Box>
</StepContainerInner>
<OnboardingFooter
horizontal
align="center"
flow={2}
t={t}
nextStep={nextStep}
prevStep={prevStep}
isContinueDisabled={!genuine.isDeviceGenuine}
/>
{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
horizontal
align="center"
flow={2}
t={t}
nextStep={nextStep}
prevStep={prevStep}
isContinueDisabled={!genuine.isDeviceGenuine}
/>
)}
<GenuineCheckModal
isOpened={isGenuineCheckModalOpened}
onClose={this.handleCloseGenuineCheckModal}

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

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

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

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

2
src/components/OperationsList/index.js

@ -119,7 +119,7 @@ export class OperationsList extends PureComponent<Props, State> {
<OperationC
operation={operation}
account={account}
key={operation.id}
key={`${account.id}_${operation.id}`}
onOperationClick={this.handleClickOperation}
t={t}
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}
renderOption={renderOption}
placeholder={t('app:common.selectAccount')}
noOptionsMessage={({ inputValue }) =>
t('app:common.selectAccountNoOption', { accountName: inputValue })
}
onChange={onChange}
/>
)

3
src/components/SelectCurrency/index.js

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

19
src/components/SelectExchange.js

@ -1,6 +1,7 @@
// @flow
import React, { Component } from 'react'
import { translate } from 'react-i18next'
import LRU from 'lru-cache'
import type { Currency } from '@ledgerhq/live-common/lib/types'
import type { Exchange } from '@ledgerhq/live-common/lib/countervalues/types'
import logger from 'logger'
@ -10,6 +11,18 @@ import Text from 'components/base/Text'
import CounterValues from 'helpers/countervalues'
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<
{
from: Currency,
@ -65,7 +78,7 @@ class SelectExchange extends Component<
const { _loadId } = this
const { from, to } = this.props
try {
const exchanges = await CounterValues.fetchExchangesForPair(from, to)
const exchanges = await getExchanges(from, to)
if (!this._unmounted && this._loadId === _loadId) {
this.setState({ exchanges })
}
@ -93,6 +106,10 @@ class SelectExchange extends Component<
options={options}
onChange={onChange}
isLoading={options.length === 0}
placeholder={t('app:common.selectExchange')}
noOptionsMessage={({ inputValue }) =>
t('app:common.selectExchangeNoOption', { exchangeName: inputValue })
}
{...props}
/>
)

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

@ -169,7 +169,7 @@ class TabProfile extends PureComponent<Props, State> {
to={counterValueCurrency}
exchangeId={counterValueExchange}
onChange={this.handleChangeExchange}
minWidth={150}
minWidth={200}
/>
</Box>
</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
import logger from 'logger'
import React, { PureComponent } from 'react'
import styled from 'styled-components'
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'
import RenderError from 'components/RenderError'
type Props = {
children: any,
t: T,
}
type State = {
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> {
state = {
error: null,
@ -54,59 +21,13 @@ class ThrowBlock extends PureComponent<Props, State> {
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() {
const { error } = this.state
const { t } = this.props
if (error) {
return (
<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 <RenderError error={error} />
}
return this.props.children
}
}
export default translate()(ThrowBlock)
export default ThrowBlock

55
src/components/TopBar/ActivityIndicator.js

@ -10,7 +10,6 @@ import type { T } from 'types/common'
import type { AsyncState } from 'reducers/bridgeSync'
import { globalSyncStateSelector } from 'reducers/bridgeSync'
import { hasAccountsSelector } from 'reducers/accounts'
import { BridgeSyncConsumer } from 'bridge/BridgeSyncContext'
import CounterValues from 'helpers/countervalues'
@ -23,7 +22,6 @@ import ItemContainer from './ItemContainer'
const mapStateToProps = createStructuredSelector({
globalSyncState: globalSyncStateSelector,
hasAccounts: hasAccountsSelector,
})
type Props = {
@ -128,37 +126,28 @@ class ActivityIndicatorInner extends PureComponent<Props, State> {
}
}
const ActivityIndicator = ({
globalSyncState,
hasAccounts,
t,
}: {
globalSyncState: AsyncState,
hasAccounts: boolean,
t: T,
}) =>
!hasAccounts ? null : (
<BridgeSyncConsumer>
{setSyncBehavior => (
<CounterValues.PollingConsumer>
{cvPolling => {
const isPending = cvPolling.pending || globalSyncState.pending
const isError = cvPolling.error || globalSyncState.error
return (
<ActivityIndicatorInner
t={t}
isPending={isPending}
isGlobalSyncStatePending={globalSyncState.pending}
isError={!!isError && !isPending}
cvPoll={cvPolling.poll}
setSyncBehavior={setSyncBehavior}
/>
)
}}
</CounterValues.PollingConsumer>
)}
</BridgeSyncConsumer>
)
const ActivityIndicator = ({ globalSyncState, t }: { globalSyncState: AsyncState, t: T }) => (
<BridgeSyncConsumer>
{setSyncBehavior => (
<CounterValues.PollingConsumer>
{cvPolling => {
const isPending = cvPolling.pending || globalSyncState.pending
const isError = cvPolling.error || globalSyncState.error
return (
<ActivityIndicatorInner
t={t}
isPending={isPending}
isGlobalSyncStatePending={globalSyncState.pending}
isError={!!isError && !isPending}
cvPoll={cvPolling.poll}
setSyncBehavior={setSyncBehavior}
/>
)
}}
</CounterValues.PollingConsumer>
)}
</BridgeSyncConsumer>
)
export default compose(
translate(),

17
src/components/TopBar/index.js

@ -12,6 +12,7 @@ import type { T } from 'types/common'
import { lock } from 'reducers/application'
import { hasPassword } from 'reducers/settings'
import { hasAccountsSelector } from 'reducers/accounts'
import { openModal } from 'reducers/modals'
import IconLock from 'icons/Lock'
@ -54,6 +55,7 @@ const Bar = styled.div`
const mapStateToProps = state => ({
hasPassword: hasPassword(state),
hasAccounts: hasAccountsSelector(state),
})
const mapDispatchToProps = {
@ -63,6 +65,7 @@ const mapDispatchToProps = {
type Props = {
hasPassword: boolean,
hasAccounts: boolean,
history: RouterHistory,
location: Location,
lock: Function,
@ -91,17 +94,21 @@ class TopBar extends PureComponent<Props> {
}
}
render() {
const { hasPassword, t } = this.props
const { hasPassword, hasAccounts, t } = this.props
return (
<Container bg="lightGrey" color="graphite">
<Inner>
<Box grow horizontal>
<GlobalSearch t={t} isHidden />
<ActivityIndicator />
<Box justifyContent="center">
<Bar />
</Box>
{hasAccounts && (
<Fragment>
<ActivityIndicator />
<Box justifyContent="center">
<Bar />
</Box>
</Fragment>
)}
<Tooltip render={() => t('app:settings.title')}>
<ItemContainer isInteractive onClick={this.navigateToSettings}>
<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()`
// - returned value is intentially not styled (is universal). wrap this in whatever you need
import logger from 'logger'
import { PureComponent } from 'react'
import { translate } from 'react-i18next'
import type { T } from 'types/common'
@ -18,7 +19,12 @@ class TranslatedError extends PureComponent<Props> {
const { t, error } = this.props
if (!error) return null
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
import { timeout } from 'rxjs/operators/timeout'
import { PureComponent } from 'react'
import isEqual from 'lodash/isEqual'
import { GENUINE_TIMEOUT } from 'config/constants'
import type { Device } from 'types/common'
import getIsGenuine from 'commands/getIsGenuine'
@ -59,7 +61,8 @@ class EnsureGenuine extends PureComponent<Props, State> {
this._checking = true
try {
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()
if (this._unmounting) return
const isGenuine = res === '0000'

64
src/components/Workflow/index.js

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

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

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

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

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

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

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

10
src/components/layout/Default.js

@ -17,6 +17,9 @@ import DashboardPage from 'components/DashboardPage'
import ManagerPage from 'components/ManagerPage'
import ExchangePage from 'components/ExchangePage'
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 IsUnlocked from 'components/IsUnlocked'
@ -39,7 +42,6 @@ type Props = {
class Default extends Component<Props> {
componentDidMount() {
window.requestAnimationFrame(() => (this._timeout = setTimeout(() => window.onAppReady(), 300)))
window.addEventListener('keydown', this.kbShortcut)
}
@ -57,7 +59,6 @@ class Default extends Component<Props> {
}
componentWillUnmount() {
clearTimeout(this._timeout)
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
render() {
return (
<Fragment>
<TriggerAppReady />
{process.platform === 'darwin' && <AppRegionDrag />}
<IsUnlocked>
@ -96,6 +97,9 @@ class Default extends Component<Props> {
</Main>
</Box>
</Box>
<LibcoreBusyIndicator />
<DeviceBusyIndicator />
</IsUnlocked>
</Fragment>
)

3
src/components/modals/AccountSettingRenderBody.js

@ -22,6 +22,7 @@ import Box from 'components/base/Box'
import Button from 'components/base/Button'
import Input from 'components/base/Input'
import Select from 'components/base/Select'
import SyncAgo from 'components/SyncAgo'
import {
ModalBody,
@ -258,12 +259,12 @@ class HelperComp extends PureComponent<Props, State> {
</Container>
) : null}
<Spoiler title={t('app:account.settings.advancedLogs')}>
<SyncAgo date={account.lastSyncDate} />
<textarea
readOnly
style={{
userSelect: 'text',
border: '1px dashed #f9f9f9',
marginTop: '20px',
backgroundColor: '#f9f9f9',
color: '#000',
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 Button from 'components/base/Button'
import AccountsList from 'components/base/AccountsList'
import IconExchange from 'icons/Exchange'
import IconExclamationCircleThin from 'icons/ExclamationCircleThin'
import type { StepProps } from '../index'
@ -20,14 +20,30 @@ class StepImport extends PureComponent<StepProps> {
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() {
this.unsub()
}
scanSubscription = null
unsub = () => {
if (this.scanSubscription) {
this.scanSubscription.unsubscribe()
}
}
scanSubscription = null
translateName(account: Account) {
const { t } = this.props
let { name } = account
@ -45,6 +61,7 @@ class StepImport extends PureComponent<StepProps> {
}
startScanAccountsDevice() {
this.unsub()
const { currency, currentDevice, setState } = this.props
try {
invariant(currency, 'No currency to scan')
@ -82,10 +99,7 @@ class StepImport extends PureComponent<StepProps> {
}
handleRetry = () => {
if (this.scanSubscription) {
this.scanSubscription.unsubscribe()
this.scanSubscription = null
}
this.unsub()
this.handleResetState()
this.startScanAccountsDevice()
}
@ -131,6 +145,17 @@ class StepImport extends PureComponent<StepProps> {
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() {
const {
scanStatus,
@ -142,6 +167,12 @@ class StepImport extends PureComponent<StepProps> {
t,
} = this.props
if (err) {
return this.renderError()
}
const currencyName = currency ? currency.name : ''
const importableAccounts = scannedAccounts.filter(acc => {
if (acc.operations.length <= 0) {
return false
@ -160,9 +191,8 @@ class StepImport extends PureComponent<StepProps> {
count: importableAccounts.length,
})
const importableAccountsEmpty = t('app:addAccounts.noAccountToImport', {
currencyName: currency ? ` ${currency.name}}` : '',
})
const importableAccountsEmpty = t('app:addAccounts.noAccountToImport', { currencyName })
const hasAlreadyEmptyAccount = scannedAccounts.some(a => a.operations.length === 0)
return (
<Fragment>
@ -180,7 +210,11 @@ class StepImport extends PureComponent<StepProps> {
/>
<AccountsList
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}
checkedIds={checkedAccountsIds}
onToggleAccount={this.handleToggleAccount}
@ -189,17 +223,7 @@ class StepImport extends PureComponent<StepProps> {
/>
</Box>
{err && (
<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>
)}
{err && <Box shrink>{err.message}</Box>}
</Fragment>
)
}
@ -208,6 +232,7 @@ class StepImport extends PureComponent<StepProps> {
export default StepImport
export const StepImportFooter = ({
setState,
scanStatus,
onClickAdd,
onCloseModal,
@ -250,7 +275,21 @@ export const StepImportFooter = ({
return (
<Fragment>
{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}
</Button>
</Fragment>

175
src/components/modals/OperationDetails.js

@ -17,6 +17,7 @@ import { MODAL_OPERATION_DETAILS } from 'config/constants'
import { getMarketColor } from 'styles/helpers'
import Box from 'components/base/Box'
import Spoiler from 'components/base/Spoiler'
import Button from 'components/base/Button'
import Bar from 'components/base/Bar'
import FormattedVal from 'components/base/FormattedVal'
@ -28,26 +29,23 @@ import { currencySettingsForAccountSelector, marketIndicatorSelector } from 'red
import CounterValue from 'components/CounterValue'
import ConfirmationCheck from 'components/OperationsList/ConfirmationCheck'
const Line = styled(Box).attrs({
horizontal: true,
})``
const ColLeft = styled(Box).attrs({
color: 'smoke',
ff: 'Open Sans',
fontSize: 4,
import Ellipsis from '../base/Ellipsis'
const OpDetailsTitle = styled(Box).attrs({
ff: 'Museo Sans|ExtraBold',
fontSize: 2,
color: 'black',
textTransform: 'uppercase',
mb: 1,
})`
width: 95px;
letter-spacing: 2px;
`
const ColRight = styled(Box).attrs({
fontSize: 4,
const OpDetailsData = styled(Box).attrs({
ff: 'Open Sans',
color: 'dark',
shrink: true,
})`
word-break: break-all;
`
color: 'smoke',
fontSize: 4,
})``
const CanSelect = styled.div`
user-select: text;
@ -91,7 +89,8 @@ type Props = {
const OperationDetails = connect(mapStateToProps)((props: Props) => {
const { t, onClose, operation, account, currencySettings, marketIndicator } = props
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 amount = getOperationAmountNumber(operation)
const isNegative = operation.type === 'OUT'
@ -108,21 +107,21 @@ const OperationDetails = connect(mapStateToProps)((props: Props) => {
return (
<ModalBody onClose={onClose}>
<ModalTitle>{t('app:operationDetails.title')}</ModalTitle>
<ModalContent flow={4}>
<Box alignItems="center" mt={3}>
<ModalContent flow={3}>
<Box alignItems="center" mt={1}>
<ConfirmationCheck
marketColor={marketColor}
isConfirmed={isConfirmed}
style={{
transform: 'scale(2)',
transform: 'scale(1.5)',
}}
t={t}
type={type}
withTooltip={false}
/>
<Box mt={5} alignItems="center">
<Box my={4} alignItems="center">
<Box>
<FormattedVal unit={unit} alwaysShowSign showCode val={amount} fontSize={8} />
<FormattedVal unit={unit} alwaysShowSign showCode val={amount} fontSize={6} />
</Box>
<Box mt={1}>
<CounterValue
@ -135,60 +134,67 @@ const OperationDetails = connect(mapStateToProps)((props: Props) => {
</Box>
</Box>
</Box>
<Line mt={4}>
<ColLeft>{t('app:operationDetails.account')}</ColLeft>
<ColRight>{name}</ColRight>
</Line>
<B />
<Line>
<ColLeft>{t('app:operationDetails.date')}</ColLeft>
<ColRight>{moment(date).format('LLL')}</ColRight>
</Line>
<Box horizontal flow={2}>
<Box flex={1}>
<OpDetailsTitle>{t('app:operationDetails.account')}</OpDetailsTitle>
<OpDetailsData>{name}</OpDetailsData>
</Box>
<Box flex={1}>
<OpDetailsTitle>{t('app:operationDetails.date')}</OpDetailsTitle>
<OpDetailsData>{moment(date).format('LLL')}</OpDetailsData>
</Box>
</Box>
<B />
<Line>
<ColLeft>{t('app:operationDetails.status')}</ColLeft>
<ColRight color={isConfirmed ? 'positiveGreen' : null} horizontal flow={1}>
<Box>
{isConfirmed
? t('app:operationDetails.confirmed')
: t('app:operationDetails.notConfirmed')}
</Box>
<Box>{`(${confirmations})`}</Box>
</ColRight>
</Line>
<Box horizontal flow={2}>
<Box flex={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>
{isConfirmed
? t('app:operationDetails.confirmed')
: t('app:operationDetails.notConfirmed')}
</Box>
<Box>{`(${confirmations})`}</Box>
</OpDetailsData>
</Box>
</Box>
<B />
{fee ? (
<Fragment>
<Line>
<ColLeft>{t('app:operationDetails.fees')}</ColLeft>
<ColRight>
<FormattedVal unit={unit} showCode val={fee} color="dark" />
</ColRight>
</Line>
<B />
</Fragment>
) : null}
<Line>
<ColLeft>{t('app:operationDetails.from')}</ColLeft>
<ColRight>{uniqSenders.map(v => <CanSelect key={v}>{v}</CanSelect>)}</ColRight>
</Line>
<Box>
<OpDetailsTitle>{t('app:operationDetails.from')}</OpDetailsTitle>
<OpDetailsData>{uniqSenders.map(v => <CanSelect key={v}>{v}</CanSelect>)}</OpDetailsData>
</Box>
<B />
<Line>
<ColLeft>{t('app:operationDetails.to')}</ColLeft>
<ColRight>{recipients.map(v => <CanSelect key={v}> {v} </CanSelect>)}</ColRight>
</Line>
<Box>
<OpDetailsTitle>{t('app:operationDetails.to')}</OpDetailsTitle>
<RenderRecipients recipients={recipients} t={t} />
</Box>
<B />
<Line>
<ColLeft>{t('app:operationDetails.identifier')}</ColLeft>
<ColRight>
<CanSelect>{hash}</CanSelect>
</ColRight>
</Line>
<Box>
<OpDetailsTitle>{t('app:operationDetails.identifier')}</OpDetailsTitle>
<OpDetailsData>
<CanSelect>
<Ellipsis>{hash}</Ellipsis>
</CanSelect>
</OpDetailsData>
</Box>
</ModalContent>
<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 ? (
<Button primary onClick={() => shell.openExternal(url)}>
<Button ml="auto" primary padded onClick={() => shell.openExternal(url)}>
{t('app:operationDetails.viewOperation')}
</Button>
) : null}
@ -216,3 +222,32 @@ const OperationDetailsWrapper = ({ t }: { t: T }) => (
)
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_BOOT_DELAY = 2 * 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_VALID = 1200
@ -29,6 +31,10 @@ export const DEVICE_DISCONNECT_DEBOUNCE = intFromEnv('LEDGER_DEVICE_DISCONNECT_D
// Endpoints...
export const LEDGER_COUNTERVALUES_API = stringFromEnv(
'LEDGER_COUNTERVALUES_API',
'https://ledger-countervalue-poc.herokuapp.com',
)
export const LEDGER_REST_API_BASE = stringFromEnv(
'LEDGER_REST_API_BASE',
'https://api.ledgerwallet.com/',
@ -59,6 +65,7 @@ export const SKIP_GENUINE = boolFromEnv('SKIP_GENUINE')
export const SKIP_ONBOARDING = boolFromEnv('SKIP_ONBOARDING')
export const SHOW_LEGACY_NEW_ACCOUNT = boolFromEnv('SHOW_LEGACY_NEW_ACCOUNT')
export const HIGHLIGHT_I18N = boolFromEnv('HIGHLIGHT_I18N')
export const DISABLE_ACTIVITY_INDICATORS = boolFromEnv('DISABLE_ACTIVITY_INDICATORS')
// Other constants

17
src/helpers/apps/installApp.js

@ -1,8 +1,10 @@
// @flow
import qs from 'qs'
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'
/**
@ -10,7 +12,14 @@ import type { LedgerScriptParams } from 'helpers/common'
*/
export default async function installApp(
transport: Transport<*>,
{ appParams }: { appParams: LedgerScriptParams },
targetId: string | number,
{ app }: { app: LedgerScriptParams },
): 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
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 {
const { data: deviceData } = await axios.get(
`${MANAGER_API_BASE}/device_versions_target_id/${targetId}`,
)
const { data } = await axios.get('https://api.ledgerwallet.com/update/applications')
if (deviceData.name in data) {
return data[deviceData.name]
const provider = 1
const deviceData = await getDeviceVersion(targetId)
const firmwareData = await getCurrentFirmware({ deviceId: deviceData.id, version })
const params = {
provider,
current_se_firmware_final_version: firmwareData.id,
device_version: deviceData.id,
}
return data['nanos-1.4']
const {
data: { application_versions },
} = await axios.post(APPLICATIONS_BY_DEVICE, params)
return application_versions.length > 0 ? application_versions : []
} catch (err) {
const error = Error(err.message)
error.stack = err.stack

19
src/helpers/apps/uninstallApp.js

@ -1,8 +1,10 @@
// @flow
import qs from 'qs'
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'
/**
@ -10,12 +12,15 @@ import type { LedgerScriptParams } from 'helpers/common'
*/
export default async function uninstallApp(
transport: Transport<*>,
{ appParams }: { appParams: LedgerScriptParams },
targetId: string | number,
{ app }: { app: LedgerScriptParams },
): Promise<*> {
const params = {
...appParams,
firmware: appParams.delete,
firmwareKey: appParams.deleteKey,
targetId,
...app,
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
// FIXME remove this file! 'helpers/common.js' RLY? :P
import qs from 'qs'
import type Transport from '@ledgerhq/hw-transport'
import { BASE_SOCKET_URL, BASE_SOCKET_URL_SECURE } from 'config/constants'
import { createDeviceSocket } from './socket'
const APDUS = {
GET_FIRMWARE: [0xe0, 0x01, 0x00, 0x00],
@ -16,35 +12,14 @@ const APDUS = {
export type LedgerScriptParams = {
firmware?: string,
firmwareKey?: string,
firmware_key?: string,
delete?: string,
deleteKey?: string,
delete_key?: string,
targetId?: string | number,
}
type FirmwareUpdateType = 'osu' | 'final'
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()
name: string,
version: string,
icon: string,
app?: number,
}
/**
@ -66,15 +41,3 @@ export async function getFirmwareInfo(transport: Transport<*>) {
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
import { createSelector } from 'reselect'
import { LEDGER_COUNTERVALUES_API } from 'config/constants'
import createCounterValues from '@ledgerhq/live-common/lib/countervalues'
import { setExchangePairsAction } from 'actions/settings'
import { currenciesSelector } from 'reducers/accounts'
@ -53,7 +54,7 @@ const addExtraPollingHooks = (schedulePoll, cancelPoll) => {
const CounterValues = createCounterValues({
log: (...args) => logger.log('CounterValues:', ...args),
getAPIBaseURL: () => 'https://ledger-countervalue-poc.herokuapp.com',
getAPIBaseURL: () => LEDGER_COUNTERVALUES_API,
storeSelector: state => state.countervalues,
pairsSelector,
setExchangePairsAction,

19
src/helpers/deviceAccess.js

@ -18,7 +18,7 @@ export const withDevice: WithDevice = devicePath => {
semaphorePerDevice[devicePath] || (semaphorePerDevice[devicePath] = createSemaphore(1))
return job =>
takeSemaphorePromise(sem, async () => {
takeSemaphorePromise(sem, devicePath, async () => {
const t = await retry(() => TransportNodeHid.open(devicePath), { maxRetry: 1 })
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) => {
sem.take(() => {
process.send({
type: 'setDeviceBusy',
busy: true,
devicePath,
})
f().then(
r => {
sem.leave()
resolve(r)
process.send({
type: 'setDeviceBusy',
busy: false,
devicePath,
})
},
e => {
sem.leave()
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
import type Transport from '@ledgerhq/hw-transport'
import { createSocketDialog } from 'helpers/common'
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 (
transport: Transport<*>,
{ targetId }: { targetId: string | number },
): Promise<string> =>
SKIP_GENUINE
app: { targetId: string | number, version: string },
): Promise<string> => {
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))
: createSocketDialog(transport, '/genuine', { targetId }, true)
: createDeviceSocket(transport, url).toPromise()
}

37
src/helpers/devices/getLatestFirmwareForDevice.js

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

7
src/helpers/devices/getMemInfo.js

@ -2,10 +2,9 @@
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> {
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' })
const { targetId } = await getFirmwareInfo(transport) // eslint-disable-line
return new Promise(resolve => setTimeout(() => resolve({}), 1000))
}

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
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 Result = *
const buildOsuParams = buildParamsFromFirmware('final')
export default async (transport: Transport<*>, firmware: Input): Result => {
try {
const osuData = buildOsuParams(firmware)
await createSocketDialog(transport, '/install', osuData)
const url = WS_INSTALL(firmware)
await createDeviceSocket(transport, url).toPromise()
return { success: true }
} catch (err) {
const error = Error(err.message)

25
src/helpers/firmware/installMcu.js

@ -1,8 +1,23 @@
// @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
// GETTING APDUS FROM SERVER
// SEND THE APDUS TO DEVICE
export default async (): Result => new Promise(resolve => resolve(true))
type Result = Promise<*>
export default async (
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
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 }>
const buildOsuParams = buildParamsFromFirmware('osu')
export default async (transport: Transport<*>, firmware: Input): Result => {
export default async (
transport: Transport<*>,
targetId: string | number,
firmware: LedgerScriptParams,
): Result => {
try {
const osuData = buildOsuParams(firmware)
await createSocketDialog(transport, '/install', osuData)
const params = {
targetId,
...firmware,
firmwareKey: firmware.firmware_key,
}
const url = WS_INSTALL(params)
await createDeviceSocket(transport, url).toPromise()
return { success: true }
} catch (err) {
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',
)

20
src/helpers/withLibcore.js

@ -3,8 +3,24 @@
// TODO: `core` should be typed
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
core.getPoolInstance()
return job(core)
try {
if (counter++ === 0) {
process.send({
type: 'setLibcoreBusy',
busy: true,
})
}
return job(core)
} finally {
if (--counter === 0) {
process.send({
type: 'setLibcoreBusy',
busy: false,
})
}
}
}

101
src/index.ejs

@ -7,10 +7,14 @@
<%= __GLOBAL_STYLES__ %>
body {
background: #f9f9f9;
}
#preload {
-webkit-app-region: drag;
align-items: center;
background: white;
background: #f9f9f9;
bottom: 0;
display: none;
flex-direction: column;
@ -20,13 +24,38 @@
position: fixed;
right: 0;
top: 0;
transition: opacity 0.4s ease-in-out;
transition: opacity 0.5s ease-in-out;
z-index: 100;
}
#preload video {
height: 144px;
width: 256px;
#preload .logo {
height: 80px;
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>
@ -41,45 +70,43 @@
</head>
<body>
<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 id="app"></div>
<script>
const { remote } = require('electron')
const { name } = remote.getCurrentWindow()
const { remote } = require('electron')
const { name } = remote.getCurrentWindow()
const preloadEl = document.getElementById('preload')
const appEl = document.getElementById('app')
const preloadEl = document.getElementById('preload')
const appEl = document.getElementById('app')
const logoEl = preloadEl.querySelector('.logo')
const initApp = (options = {}) => {
const { force = false } = options
const initApp = (options = {}) => {
const { force = false } = options
if (force) {
preloadEl.remove()
} else {
preloadEl.style.opacity = 0
if (force) {
preloadEl.remove()
} else {
preloadEl.style.opacity = 0
preloadEl.addEventListener('transitionend', () => preloadEl.remove())
}
}
if (name === 'MainWindow') {
preloadEl.style.display = 'flex'
let waitTime = 0
const PRELOAD_WAIT_TIME_MIN = 2e3
const interval = setInterval(() => (waitTime += 250), 250)
preloadEl.addEventListener('transitionend', () => preloadEl.remove())
}
}
window.onAppReady = () => {
const delay = PRELOAD_WAIT_TIME_MIN - waitTime
clearInterval(interval)
setTimeout(initApp, delay > 0 ? delay : 1)
}
} else {
initApp({ force: true })
}
</script>
if (name === 'MainWindow') {
setTimeout(() => {
logoEl.style.opacity = 1
}, 50)
preloadEl.style.display = 'flex'
const startTime = Date.now()
const PRELOAD_WAIT_TIME_MIN = 2000
window.onAppReady = () => {
const delay = Math.max(0, PRELOAD_WAIT_TIME_MIN - (Date.now() - startTime))
setTimeout(initApp, delay)
}
} else {
initApp({ force: true })
}
</script>
</body>
</html>

21
src/main/bridge.js

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

14
src/reducers/onboarding.js

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

16
src/renderer/events.js

@ -15,7 +15,9 @@ import network from 'api/network'
import { ipcRenderer } from 'electron'
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 { 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(
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__) {
// TODO move this to "command" pattern
const updaterHandlers = {

3
src/renderer/init.js

@ -27,6 +27,7 @@ import hardReset from 'helpers/hardReset'
import sentry from 'sentry/browser'
import App from 'components/App'
import AppError from 'components/AppError'
import 'styles/global'
@ -99,5 +100,5 @@ function r(Comp) {
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.
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...
currency: Currency
selectAccount: Select an account
selectAccountNoOption: 'No account matching "{{accountName}}"'
selectCurrency: Select a currency
selectCurrencyNoOption: 'No currency matching "{{currencyName}}"'
selectExchange: Select an exchange
selectExchangeNoOption: 'No exchange matching "{{exchangeName}}"'
sortBy: Sort by
search: Search
save: Save
@ -23,6 +27,7 @@ common:
next: Next
back: Back
retry: Retry
stop: Stop
close: Close
eastern: Eastern
western: Western
@ -37,6 +42,7 @@ common:
upToDate: Up to date
error: Sync error.
refresh: Refresh
ago: Synced {{time}}
error:
load: Unable to load
noResults: No results
@ -138,12 +144,13 @@ addAccounts:
editName: Edit name
newAccount: New account
legacyAccount: '{{accountName}} (legacy)'
noAccountToImport: We didnt find any {{currencyName}}} account to import.
noAccountToImport: We didnt find any {{currencyName}} account to import.
success: Great success!
createNewAccount:
title: Create new account
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:
create: 'Create account'
import: 'Import account'
@ -158,8 +165,9 @@ operationDetails:
fees: Fees
from: From
to: To
identifier: Identifier
identifier: Hash
viewOperation: View operation
showMore: See {{recipients}} more
operationList:
noMoreOperations: No more operations
manager:
@ -361,6 +369,10 @@ update:
newVersionReady: A new update is available.
relaunch: Update now
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
reset: Reset app files
createTicket: Create ticket
reset: Hard reset
createTicket: Create issue
showDetails: Show details
showError: Show error

11
static/i18n/en/errors.yml

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

144
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:
title: Welcome to Ledger Live
startBtn: Get Started
startBtn: Get started
init:
title: Welcome to Ledger Live, the computer companion app to your Ledger device
title: Get started with your Ledger device
newDevice:
title: Initialize your new Ledger device
desc: Please replace it with the final wording once it’s done.
title: Initialize a new Ledger device
restoreDevice:
title: Restore a Ledger device
desc: Please replace it with the final wording once it’s done.
initializedDevice:
title: I have already initialized my device
desc: Please replace it with the final wording once it’s done.
title: Use a device that's already initialized
noDevice:
title: Do not have a Ledger device yet?
desc: Please replace it with the final wording once it’s done.
noDevice:
title: Do not have a Ledger device yet?
buyNew:
@ -24,100 +27,113 @@ noDevice:
learnMore:
title: Learn about Ledger Live
selectDevice:
title: To get started, select your device
title: Select your device
ledgerNanoCard:
title: Ledger Nano S
desc: Please replace it with the final wording once it’s done.
ledgerBlueCard:
title: Ledger Blue
desc: Please replace it with the final wording once it’s done.
selectPIN:
title: Start initialization & choose your PIN code
# initialize:
title: Start initialization - Choose your PIN code
instructions:
ledgerNano:
step1: Connect the Ledger Nano S to your computer.
step2: Press both buttons simultaneously as instructed on the screen.
step3: Press the right button to select Configure as new device.
step4: Choose a PIN code between 4 and 8 digits long.
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. Then select the checkmark (✓).'
ledgerBlue:
step1: Connect the Ledger Blue to your computer.
step2: Tap on Configure as new device.
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:
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.
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:
restore:
title: Save your recovery phrase
desc: Your recovery phrase is formed by 24 words. They will be 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.
title: Save your recovery phrase
desc: Your device will generate a recovery phrase of 24 words, displayed only once.
nano:
title: Save your recovery phrase
desc: Your recovery phrase is formed by 24 words. They will be displayed only once.
step1: 'Copy the first word (Word #1) in position 1 on the 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 press both buttons to validate each word displayed on the screen.
step1: 'Copy the word displayed below Word #1 in position 1 on a blank Recovery sheet.' # <bold>Word #1</bold> <italic>Recovery sheet</italic>
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>
step3: 'Confirm your recovery phrase: select each requested word and press both buttons to validate it.'
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.
step1: Copy each word of the recovery phrase on a blank Recovery sheet. Copy the words in the same order. # <i>Recovery sheet</i>
step2: Tap Next to move to the next words. Repeat the process until the Confirmation screen appears. # <bold>Next</bold> <bold>Confirmation</bold>
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.' # <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:
note1: Carefully secure your 24 words out of sight.
note2: Ledger does not keep any backup of your 24 words.
note3: Make sure you are the sole holder of the 24-word recovery phrase.
note4: Never use a device supplied with a recovery phrase and/or a PIN code.
note1: Carefully secure your 24-word recovery phrase out of sight.
note2: Ledger does not keep any backup of your recovery phrase.
note3: Make sure you are the sole holder of your recovery phrase.
note4: Never use a device supplied with a recovery phrase or a PIN code.
genuineCheck:
title: Final security check
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:
step1:
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:
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:
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
isGenuinePassed: Your Nano S is genuine
desc:
isGenuinePassed: 'Genuine'
buttons:
genuineCheck: Genuine check
contactSupport: Contact Support
contactSupport: Ledger Support
errorPage:
ledgerNano:
title: Something is wrong with your Ledger Nano S
desc: A problem occurred with your Ledger Nano S. Contact Ledger Support to get assistance or go back to the security check.
title: Oops, something went wrong...
desc: Go back to the security check or request Ledger Support assistance.
ledgerBlue:
title: Something is wrong with your Ledger Blue
desc: A problem occurred with your Ledger Blue. Contact Ledger Support to get assistance or go back to the security check.
title: Oops, something went wrong...
desc: Go back to the security check or request Ledger Support assistance.
setPassword:
title: Protect your privacy (optional)
desc: Set a password to prevent unauthorized access to Ledger Live data stored on your computer, including account names, balances, transactions and public addresses.
title: Encrypt Ledger Live data
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:
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.
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:
title: Help Ledger to improve its products and services
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
title: Analytics and bug reports
desc: Share anonymous usage and diagnostics data to help improve Ledger products, services and security features.
shareAnalytics:
title: Share analytics
desc: Help Ledger improve its products and services by automatically sending diagnostics and usage data.
termsConditions:
title: Terms and Conditions
desc: Please accept terms and conditions to proceed
title: Share 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.
sentryLogs:
title: Sentry Logs
desc: Help Ledger improve its products and services by automatically sending diagnostics and usage data.
title: Report bugs
desc: Automatically send bug reports to help Ledger developers diagnose issues and improve Ledger Live performance.
finish:
title: This is the title of the screen. 1 line is the maximum
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
openAppButton: Open app
followUsLabel: Follow us to stay updated
title: 'Ready for launch!'
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: Launch
followUsLabel:

22
static/i18n/fr/app.yml

@ -12,7 +12,11 @@ common:
chooseWalletPlaceholder: Choose a wallet...
currency: Currency
selectAccount: Select an account
selectAccountNoOption: 'No account matching "{{accountName}}"'
selectCurrency: Select a currency
selectCurrencyNoOption: 'No currency matching "{{currencyName}}"'
selectExchange: Select an exchange
selectExchangeNoOption: 'No exchange matching "{{exchangeName}}"'
sortBy: Sort by
search: Search
save: Save
@ -24,6 +28,7 @@ common:
next: Next
back: Back
retry: Retry
stop: Stop
close: Close
eastern: Eastern
western: Western
@ -38,6 +43,7 @@ common:
upToDate: Up to date
error: Sync error.
refresh: Refresh
ago: Synced {{time}}
error:
load: Unable to load
noResults: No results
@ -139,12 +145,13 @@ addAccounts:
editName: Edit name
newAccount: New account
legacyAccount: '{{accountName}} (legacy)'
noAccountToImport: We didnt find any {{currencyName}}} account to import.
noAccountToImport: We didnt find any {{currencyName}} account to import.
success: Great success!
createNewAccount:
title: Create new account
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:
create: 'Create account'
import: 'Import account'
@ -159,8 +166,9 @@ operationDetails:
fees: Fees
from: From
to: To
identifier: Identifier
identifier: Hash
viewOperation: View operation
showMore: See {{recipients}} more
operationList:
noMoreOperations: No more operations
manager:
@ -362,6 +370,10 @@ update:
newVersionReady: A new update is available.
relaunch: Update now
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
reset: Reset app files
createTicket: Create ticket
reset: Hard reset
createTicket: Create issue
showDetails: Show details
showError: Show error

15
static/i18n/fr/errors.yml

@ -1,12 +1,10 @@
---
RangeError:
- message
Error:
- message
LedgerAPIErrorWithMessage:
- message
TransportStatusError:
- message
generic: An error occurred
RangeError: '{{message}}'
Error: '{{message}}'
LedgerAPIErrorWithMessage: '{{message}}'
TransportStatusError: '{{message}}'
TimeoutError: 'Timeout reached'
FeeEstimationFailed: 'fee estimation failed (status: {{status}})'
NotEnoughBalance: 'Not enough balance'
BtcUnmatchedApp: 'You must open application ‘{{currencyName}}’ on the device'
@ -22,3 +20,4 @@ WebsocketConnectionFailed: Failed to establish a socket connection
DeviceSocketFail: Device socket failure
DeviceSocketNoBulkStatus: Device socket failure (bulk)
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
fr: Français
en: English
fr: French

131
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:
title: Welcome to Ledger Live
startBtn: Get Started
startBtn: Get started
init:
title: Welcome to Ledger Live, the computer companion app to your Ledger device
title: Get started with your Ledger device
newDevice:
title: Initialize your new Ledger device
desc: Please replace it with the final wording once it’s done.
title: Initialize a new Ledger device
restoreDevice:
title: Restore a Ledger device
desc: Please replace it with the final wording once it’s done.
initializedDevice:
title: I have already initialized my device
desc: Please replace it with the final wording once it’s done.
title: Use a device that's already initialized
noDevice:
title: Do not have a Ledger device yet?
desc: Please replace it with the final wording once it’s done.
noDevice:
title: Do not have a Ledger device yet?
buyNew:
@ -25,100 +28,100 @@ noDevice:
learnMore:
title: Learn about Ledger Live
selectDevice:
title: To get started, select your device
title: Select your device
ledgerNanoCard:
title: Ledger Nano S
desc: Please replace it with the final wording once it’s done.
ledgerBlueCard:
title: Ledger Blue
desc: Please replace it with the final wording once it’s done.
selectPIN:
title: Start initialization & choose your PIN code
title: Start initialization - Choose your PIN code
instructions:
ledgerNano:
step1: Connect the Ledger Nano S to your computer.
step2: Press both buttons simultaneously as instructed on the screen.
step3: Press the right button to select Configure as new device.
step4: Choose a PIN code between 4 and 8 digits long.
step3: Press the right button to select Configure as new device?.
step4: 'Choose a PIN code between 4 and 8 digits long. Then select the checkmark (✓).'
ledgerBlue:
step1: Connect the Ledger Blue to your computer.
step2: Tap on Configure as new device.
step3: Choose a PIN code between 4 and 8 digits long.
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.
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:
restore:
title: Save your recovery phrase
desc: Your recovery phrase is formed by 24 words. They will be 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.
title: Save your recovery phrase
desc: Your device will generate a recovery phrase of 24 words, displayed only once.
nano:
title: Save your recovery phrase
desc: Your recovery phrase is formed by 24 words. They will be displayed only once.
step1: 'Copy the first word (Word #1) in position 1 on the blank Recovery sheet.'
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 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.
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.'
step3: 'Select Word #1 from the suggested words. Press both buttons to continue.'
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:
note1: Carefully secure your 24 words out of sight.
note2: Ledger does not keep any backup of your 24 words.
note3: Make sure you are the sole holder of the 24-word recovery phrase.
note4: Never use a device supplied with a recovery phrase and/or a PIN code.
note1: Carefully secure your 24-word recovery phrase out of sight.
note2: Ledger does not keep any backup of your recovery phrase.
note3: Make sure you are the sole holder of your recovery phrase.
note4: Never use a device supplied with a recovery phrase or a PIN code.
genuineCheck:
title: Final security check
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:
step1:
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:
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:
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
isGenuinePassed: Your Nano S is genuine
desc:
isGenuinePassed: 'Genuine'
buttons:
genuineCheck: Genuine check
contactSupport: Contact Support
contactSupport: Ledger Support
errorPage:
ledgerNano:
title: Something is wrong with your Ledger Nano S
desc: A problem occurred with your Ledger Nano S. Contact Ledger Support to get assistance or go back to the security check.
title: Oops, something went wrong...
desc: Go back to the security check or request Ledger Support assistance.
ledgerBlue:
title: Something is wrong with your Ledger Blue
desc: A problem occurred with your Ledger Blue. Contact Ledger Support to get assistance or go back to the security check.
title: Oops, something went wrong...
desc: Go back to the security check or request Ledger Support assistance.
setPassword:
title: Protect your privacy (optional)
desc: Set a password to prevent unauthorized access to Ledger Live data stored on your computer, including account names, balances, transactions and public addresses.
title: Encrypt Ledger Live data
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:
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.
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:
title: Help Ledger to improve its products and services
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
title: Analytics and bug reports
desc: Share anonymous usage and diagnostics data to help improve Ledger products, services and security features.
shareAnalytics:
title: Share analytics
desc: Help Ledger improve its products and services by automatically sending diagnostics and usage data.
termsConditions:
title: Terms and Conditions
desc: Please accept terms and conditions to proceed
title: Share 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.
sentryLogs:
title: Sentry Logs
desc: Help Ledger improve its products and services by automatically sending diagnostics and usage data.
title: Report bugs
desc: Automatically send bug reports to help Ledger developers diagnose issues and improve Ledger Live performance.
finish:
title: This is the title of the screen. 1 line is the maximum
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
openAppButton: Open app
followUsLabel: Follow us to stay updated
title: 'Ready for launch!'
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: Launch
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