Browse Source

Merge pull request #663 from LedgerHQ/develop

Prepare for alpha.14
master
Gaëtan Renaudeau 7 years ago
committed by GitHub
parent
commit
341a2a7c0c
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 13
      .circleci/config.yml
  2. 1
      electron-builder.yml
  3. 8
      package.json
  4. 12
      scripts/compile.sh
  5. 4
      scripts/dist.sh
  6. 26
      scripts/hash-utils.sh
  7. 13
      scripts/install-ci-deps.sh
  8. 4
      scripts/live-cli.js
  9. 46
      scripts/postinstall.sh
  10. 4
      scripts/release.sh
  11. 71
      scripts/trans.js
  12. 13
      src/bridge/EthereumJSBridge.js
  13. 3
      src/bridge/LibcoreBridge.js
  14. 15
      src/bridge/RippleJSBridge.js
  15. 8
      src/bridge/UnsupportedBridge.js
  16. 11
      src/bridge/makeMockBridge.js
  17. 6
      src/bridge/types.js
  18. 12
      src/commands/installFinalFirmware.js
  19. 17
      src/commands/libcoreHardReset.js
  20. 2
      src/components/Breadcrumb/Step.js
  21. 52
      src/components/DashboardPage/index.js
  22. 17
      src/components/DeviceConnect/index.js
  23. 14
      src/components/ExportLogsBtn.js
  24. 14
      src/components/GradientBox.js
  25. 101
      src/components/ManagerPage/AppsList.js
  26. 3
      src/components/ManagerPage/Dashboard.js
  27. 30
      src/components/ManagerPage/FirmwareFinalUpdate.js
  28. 65
      src/components/ManagerPage/FirmwareUpdate.js
  29. 4
      src/components/ManagerPage/UpdateFirmwareButton.js
  30. 9
      src/components/ManagerPage/index.js
  31. 18
      src/components/Onboarding/steps/GenuineCheck.js
  32. 6
      src/components/Onboarding/steps/SelectPIN/SelectPINblue.js
  33. 8
      src/components/Onboarding/steps/SelectPIN/SelectPINnano.js
  34. 77
      src/components/Onboarding/steps/SelectPIN/SelectPINrestoreBlue.js
  35. 77
      src/components/Onboarding/steps/SelectPIN/SelectPINrestoreNano.js
  36. 13
      src/components/Onboarding/steps/SelectPIN/index.js
  37. 10
      src/components/Onboarding/steps/WriteSeed/WriteSeedBlue.js
  38. 10
      src/components/Onboarding/steps/WriteSeed/WriteSeedNano.js
  39. 39
      src/components/Onboarding/steps/WriteSeed/WriteSeedRestore.js
  40. 2
      src/components/Onboarding/steps/WriteSeed/index.js
  41. 29
      src/components/TopBar/ActivityIndicator.js
  42. 7
      src/components/Workflow/EnsureDashboard.js
  43. 23
      src/components/Workflow/EnsureDevice.js
  44. 9
      src/components/Workflow/WorkflowDefault.js
  45. 11
      src/components/Workflow/WorkflowWithIcon.js
  46. 4
      src/components/base/Ellipsis.js
  47. 5
      src/components/base/GrowScroll/index.js
  48. 19
      src/components/base/Modal/index.js
  49. 17
      src/components/base/Tooltip/index.js
  50. 2
      src/components/layout/Default.js
  51. 4
      src/components/modals/AddAccounts/steps/02-step-connect-device.js
  52. 14
      src/components/modals/AddAccounts/steps/03-step-import.js
  53. 96
      src/components/modals/OperationDetails.js
  54. 8
      src/components/modals/Receive/index.js
  55. 22
      src/components/modals/ReleaseNotes.js
  56. 20
      src/components/modals/Send/index.js
  57. 10
      src/config/constants.js
  58. 15
      src/helpers/apps/installApp.js
  59. 4
      src/helpers/apps/listApps.js
  60. 14
      src/helpers/apps/uninstallApp.js
  61. 8
      src/helpers/devices/getCurrentFirmware.js
  62. 15
      src/helpers/devices/getDeviceVersion.js
  63. 8
      src/helpers/devices/getLatestFirmwareForDevice.js
  64. 8
      src/helpers/devices/getNextMCU.js
  65. 23
      src/helpers/devices/getOsuFirmware.js
  66. 8
      src/helpers/firmware/getFinalFirmwareById.js
  67. 28
      src/helpers/firmware/installFinalFirmware.js
  68. 1
      src/helpers/firmware/installMcu.js
  69. 4
      src/helpers/firmware/installOsuFirmware.js
  70. 4
      src/helpers/hardReset.js
  71. 2
      src/helpers/urls.js
  72. 1
      src/icons/Trash.js
  73. 15
      src/index.ejs
  74. 317
      static/i18n/en/app.yml
  75. 39
      static/i18n/en/errors.yml
  76. 61
      static/i18n/en/onboarding.yml
  77. 2
      static/images/empty-account-tile.svg
  78. 17
      yarn.lock

13
.circleci/config.yml

@ -12,9 +12,18 @@ jobs:
- gh-pages - gh-pages
steps: steps:
- checkout - checkout
- restore_cache:
name: Restore Yarn Package Cache
keys:
- v1-yarn-packages-{{ .Branch }}-{{ checksum "yarn.lock" }}
- run: - run:
name: Dependencies name: Install Dependencies
command: SKIP_REBUILD=1 yarn command: bash scripts/install-ci-deps.sh
- save_cache:
name: Save Yarn Package Cache
key: v1-yarn-packages-{{ .Branch }}-{{ checksum "yarn.lock" }}
paths:
- node_modules/
- run: - run:
name: Lint name: Lint
command: yarn lint command: yarn lint

1
electron-builder.yml

@ -1,4 +1,5 @@
appId: com.ledger.live appId: com.ledger.live
npmRebuild: false
protocols: protocols:
name: Ledger Live name: Ledger Live

8
package.json

@ -7,9 +7,9 @@
"author": "Ledger", "author": "Ledger",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"compile": "bash ./scripts/dist.sh", "compile": "bash ./scripts/compile.sh",
"dist:dir": "yarn dist --dir -c.compression=store -c.mac.identity=null", "dist:dir": "yarn dist --dir -c.compression=store -c.mac.identity=null",
"dist": "yarn compile && electron-builder", "dist": "bash ./scripts/dist.sh",
"test": "jest", "test": "jest",
"flow": "flow", "flow": "flow",
"lint": "eslint src webpack .storybook", "lint": "eslint src webpack .storybook",
@ -41,7 +41,7 @@
"@ledgerhq/hw-app-xrp": "^4.13.0", "@ledgerhq/hw-app-xrp": "^4.13.0",
"@ledgerhq/hw-transport": "^4.13.0", "@ledgerhq/hw-transport": "^4.13.0",
"@ledgerhq/hw-transport-node-hid": "^4.13.0", "@ledgerhq/hw-transport-node-hid": "^4.13.0",
"@ledgerhq/ledger-core": "2.0.0-rc.1", "@ledgerhq/ledger-core": "2.0.0-rc.3",
"@ledgerhq/live-common": "2.31.0", "@ledgerhq/live-common": "2.31.0",
"async": "^2.6.1", "async": "^2.6.1",
"axios": "^0.18.0", "axios": "^0.18.0",
@ -65,6 +65,7 @@
"invariant": "^2.2.4", "invariant": "^2.2.4",
"lodash": "^4.17.5", "lodash": "^4.17.5",
"lru-cache": "^4.1.3", "lru-cache": "^4.1.3",
"measure-scrollbar": "^1.1.0",
"moment": "^2.22.2", "moment": "^2.22.2",
"qrcode": "^1.2.0", "qrcode": "^1.2.0",
"qrcode-reader": "^1.0.4", "qrcode-reader": "^1.0.4",
@ -74,6 +75,7 @@
"react": "^16.4.1", "react": "^16.4.1",
"react-dom": "^16.4.1", "react-dom": "^16.4.1",
"react-i18next": "^7.7.0", "react-i18next": "^7.7.0",
"react-key-handler": "^1.0.1",
"react-markdown": "^3.3.2", "react-markdown": "^3.3.2",
"react-mortal": "^3.2.0", "react-mortal": "^3.2.0",
"react-motion": "^0.5.2", "react-motion": "^0.5.2",

12
scripts/compile.sh

@ -0,0 +1,12 @@
#/bin/bash
set -e
export GIT_REVISION=`git rev-parse HEAD`
export SENTRY_URL=https://db8f5b9b021048d4a401f045371701cb@sentry.io/274561
rm -rf ./node_modules/.cache dist
yarn
rm -rf dist &&
NODE_ENV=production yarn run webpack-cli --mode production --config webpack/internals.config.js &&
NODE_ENV=production yarn run electron-webpack

4
scripts/dist.sh

@ -1,5 +1,3 @@
#/bin/bash #/bin/bash
rm -rf dist && yarn compile && DEBUG=electron-builder electron-builder
NODE_ENV=production webpack-cli --mode production --config webpack/internals.config.js &&
NODE_ENV=production electron-webpack

26
scripts/hash-utils.sh

@ -0,0 +1,26 @@
#/bin/bash
function GET_HASH_PATH {
HASH_NAME=$1
echo "./node_modules/.cache/LEDGER_HASH_$HASH_NAME.hash"
}
function GET_HASH {
HASH_NAME=$1
HASH_PATH=`GET_HASH_PATH $HASH_NAME`
if [ ! -e "$HASH_PATH" ]; then
echo ''
else
HASH_CONTENT=`cat "$HASH_PATH"`
echo $HASH_CONTENT
fi
}
function SET_HASH {
HASH_NAME=$1
HASH_CONTENT=$2
echo "setting hash $HASH_NAME to $HASH_CONTENT"
HASH_PATH=`GET_HASH_PATH $HASH_NAME`
mkdir -p ./node_modules/.cache
echo $HASH_CONTENT > $HASH_PATH
}

13
scripts/install-ci-deps.sh

@ -0,0 +1,13 @@
#/bin/bash
source scripts/hash-utils.sh
PACKAGE_JSON_HASH=`md5sum package.json | cut -d ' ' -f 1`
CACHED_PACKAGE_JSON_HASH=`GET_HASH 'package.json'`
if [ "$CACHED_PACKAGE_JSON_HASH" == "$PACKAGE_JSON_HASH" ]; then
echo "> Skipping yarn install"
else
yarn install
SET_HASH 'package.json' $PACKAGE_JSON_HASH
fi

4
scripts/hey.js → scripts/live-cli.js

@ -1,3 +1,7 @@
// This is a work in progress
// The goal is to provide a cli which allow interact
// with device & libcore for faster iterations
require('babel-polyfill') require('babel-polyfill')
require('babel-register') require('babel-register')

46
scripts/postinstall.sh

@ -1,8 +1,44 @@
#/bin/bash #/bin/bash
flow-typed install -s --overwrite source scripts/hash-utils.sh
rm flow-typed/npm/{react-i18next_v7.x.x.js,styled-components_v3.x.x.js,redux_*}
if [ "$SKIP_REBUILD" != "1" ]; then function MAIN {
electron-builder install-app-deps if ! $CI; then
fi REBUILD_ELECTRON_NATIVE_DEPS
fi
INSTALL_FLOW_TYPED
}
function INSTALL_FLOW_TYPED {
LATEST_FLOW_TYPED_COMMIT_HASH=`curl --silent --header "Accept: application/vnd.github.VERSION.sha" https://api.github.com/repos/flowtype/flow-typed/commits/master`
CURRENT_FLOW_TYPED_HASH=`GET_HASH 'flow-typed'`
if [ "$LATEST_FLOW_TYPED_COMMIT_HASH" == "$CURRENT_FLOW_TYPED_HASH" ]; then
echo "> Flow-typed definitions are up to date. Skipping"
else
echo "> Installing flow-typed defs"
flow-typed install -s --overwrite
echo "> Removing broken flow definitions"
rm flow-typed/npm/{react-i18next_v7.x.x.js,styled-components_v3.x.x.js,redux_*}
SET_HASH 'flow-typed' $LATEST_FLOW_TYPED_COMMIT_HASH
fi
}
function REBUILD_ELECTRON_NATIVE_DEPS {
# for strange/fancy os-es
if [[ `uname` == 'Darwin' ]]; then
PACKAGE_JSON_HASH=`md5 package.json | cut -d ' ' -f 1`
else
# for normal os-es
PACKAGE_JSON_HASH=`md5sum package.json | cut -d ' ' -f 1`
fi
CACHED_PACKAGE_JSON_HASH=`GET_HASH 'package.json'`
if [ "$CACHED_PACKAGE_JSON_HASH" == "$PACKAGE_JSON_HASH" ]; then
echo "> Electron native deps are up to date. Skipping"
else
echo "> Installing electron native deps"
DEBUG=electron-builder electron-builder install-app-deps
SET_HASH 'package.json' $PACKAGE_JSON_HASH
fi
}
MAIN

4
scripts/release.sh

@ -20,9 +20,5 @@ fi
# TODO check if version is not already there # TODO check if version is not already there
# TODO check if local git HEAD is EXACTLY our remote master HEAD # TODO check if local git HEAD is EXACTLY our remote master HEAD
export GIT_REVISION=`git rev-parse HEAD`
export SENTRY_URL=https://db8f5b9b021048d4a401f045371701cb@sentry.io/274561
rm -rf ./node_modules/.cache
yarn
yarn compile yarn compile
build build

71
scripts/trans.js

@ -1,71 +0,0 @@
#!/usr/bin/env node
/* eslint-disable no-console */
/* eslint-disable no-use-before-define */
require('dotenv').config()
const path = require('path')
const fs = require('fs')
const axios = require('axios')
const querystring = require('querystring')
const forEach = require('lodash/forEach')
const objectPath = require('object-path')
const yaml = require('js-yaml')
const chalk = require('chalk')
const { LOKALISE_TOKEN, LOKALISE_PROJECT } = process.env
const BASE = 'https://api.lokalise.co/api'
const stats = {
nb: 0,
}
main()
async function main() {
try {
console.log(`${chalk.blue('[>]')} ${chalk.dim('Fetching translations...')}`)
const url = `${BASE}/string/list`
const { data } = await axios.post(
url,
querystring.stringify({
api_token: LOKALISE_TOKEN,
id: LOKALISE_PROJECT,
}),
)
if (data.response.status === 'error') {
throw new Error(JSON.stringify(data.response))
}
const { strings } = data
forEach(strings, syncLanguage)
console.log(
`${chalk.blue('[>]')} ${chalk.dim('Successfully imported')} ${stats.nb} ${chalk.dim(
'translations',
)}`,
)
} catch (err) {
console.log(err)
console.log(`${chalk.red('[x] Error in the process')}`)
process.exit(1)
}
}
function syncLanguage(translations, language) {
const folderPath = getLanguageFolderPath(language)
const filePath = path.resolve(folderPath, 'translation.yml')
if (!fs.existsSync(folderPath)) {
fs.mkdirSync(folderPath)
}
const obj = translations.reduce((acc, cur) => {
objectPath.set(acc, cur.key, cur.translation)
console.log(`${chalk.green('[✓]')} ${language} ${chalk.dim(cur.key)}`)
++stats.nb
return acc
}, {})
fs.writeFileSync(filePath, yaml.dump(obj))
}
function getLanguageFolderPath(language) {
return path.resolve(__dirname, `../static/i18n/${language}`)
}

13
src/bridge/EthereumJSBridge.js

@ -156,7 +156,8 @@ const fetchCurrentBlock = (perCurrencyId => currency => {
})({}) })({})
const EthereumBridge: WalletBridge<Transaction> = { const EthereumBridge: WalletBridge<Transaction> = {
scanAccountsOnDevice(currency, deviceId, { next, complete, error }) { scanAccountsOnDevice: (currency, deviceId) =>
Observable.create(o => {
let finished = false let finished = false
const unsubscribe = () => { const unsubscribe = () => {
finished = true finished = true
@ -251,22 +252,22 @@ const EthereumBridge: WalletBridge<Transaction> = {
.send({ currencyId: currency.id, devicePath: deviceId, path: freshAddressPath }) .send({ currencyId: currency.id, devicePath: deviceId, path: freshAddressPath })
.toPromise() .toPromise()
const r = await stepAddress(index, res, isStandard) const r = await stepAddress(index, res, isStandard)
if (r.account) next(r.account) if (r.account) o.next(r.account)
if (r.complete) { if (r.complete) {
break break
} }
} }
} }
complete() o.complete()
} catch (e) { } catch (e) {
error(e) o.error(e)
} }
} }
main() main()
return { unsubscribe } return unsubscribe
}, }),
synchronize: ({ freshAddress, blockHeight, currency, operations }) => synchronize: ({ freshAddress, blockHeight, currency, operations }) =>
Observable.create(o => { Observable.create(o => {

3
src/bridge/LibcoreBridge.js

@ -79,14 +79,13 @@ const getFees = async (a, transaction) => {
} }
const LibcoreBridge: WalletBridge<Transaction> = { const LibcoreBridge: WalletBridge<Transaction> = {
scanAccountsOnDevice(currency, devicePath, observer) { scanAccountsOnDevice(currency, devicePath) {
return libcoreScanAccounts return libcoreScanAccounts
.send({ .send({
devicePath, devicePath,
currencyId: currency.id, currencyId: currency.id,
}) })
.pipe(map(decodeAccount)) .pipe(map(decodeAccount))
.subscribe(observer)
}, },
synchronize: account => synchronize: account =>

15
src/bridge/RippleJSBridge.js

@ -239,7 +239,8 @@ const getServerInfo = (map => endpointConfig => {
})({}) })({})
const RippleJSBridge: WalletBridge<Transaction> = { const RippleJSBridge: WalletBridge<Transaction> = {
scanAccountsOnDevice(currency, deviceId, { next, complete, error }) { scanAccountsOnDevice: (currency, deviceId) =>
Observable.create(o => {
let finished = false let finished = false
const unsubscribe = () => { const unsubscribe = () => {
finished = true finished = true
@ -282,7 +283,7 @@ const RippleJSBridge: WalletBridge<Transaction> = {
// account does not exist in Ripple server // account does not exist in Ripple server
// we are generating a new account locally // we are generating a new account locally
if (!legacy) { if (!legacy) {
next({ o.next({
id: accountId, id: accountId,
xpub: '', xpub: '',
name: getNewAccountPlaceholderName(currency, index), name: getNewAccountPlaceholderName(currency, index),
@ -331,12 +332,12 @@ const RippleJSBridge: WalletBridge<Transaction> = {
lastSyncDate: new Date(), lastSyncDate: new Date(),
} }
account.operations = transactions.map(txToOperation(account)) account.operations = transactions.map(txToOperation(account))
next(account) o.next(account)
} }
} }
complete() o.complete()
} catch (e) { } catch (e) {
error(e) o.error(e)
} finally { } finally {
api.disconnect() api.disconnect()
} }
@ -344,8 +345,8 @@ const RippleJSBridge: WalletBridge<Transaction> = {
main() main()
return { unsubscribe } return unsubscribe
}, }),
synchronize: ({ endpointConfig, freshAddress, blockHeight }) => synchronize: ({ endpointConfig, freshAddress, blockHeight }) =>
Observable.create(o => { Observable.create(o => {

8
src/bridge/UnsupportedBridge.js

@ -10,10 +10,10 @@ const UnsupportedBridge: WalletBridge<*> = {
o.error(genericError) o.error(genericError)
}), }),
scanAccountsOnDevice(currency, deviceId, { error }) { scanAccountsOnDevice: () =>
Promise.resolve(genericError).then(error) Observable.create(o => {
return { unsubscribe() {} } o.error(genericError)
}, }),
pullMoreOperations: () => Promise.reject(genericError), pullMoreOperations: () => Promise.reject(genericError),

11
src/bridge/makeMockBridge.js

@ -75,13 +75,14 @@ function makeMockBridge(opts?: Opts): WalletBridge<*> {
} }
}), }),
scanAccountsOnDevice(currency, deviceId, { next, complete, error }) { scanAccountsOnDevice: currency =>
Observable.create(o => {
let unsubscribed = false let unsubscribed = false
async function job() { async function job() {
if (Math.random() > scanAccountDeviceSuccessRate) { if (Math.random() > scanAccountDeviceSuccessRate) {
await delay(1000) await delay(1000)
if (!unsubscribed) error(new Error('scan failed')) if (!unsubscribed) o.error(new Error('scan failed'))
return return
} }
const nbAccountToGen = 3 const nbAccountToGen = 3
@ -92,9 +93,9 @@ function makeMockBridge(opts?: Opts): WalletBridge<*> {
currency, currency,
}) })
account.unit = currency.units[0] account.unit = currency.units[0]
if (!unsubscribed) next(account) if (!unsubscribed) o.next(account)
} }
if (!unsubscribed) complete() if (!unsubscribed) o.complete()
} }
job() job()
@ -104,7 +105,7 @@ function makeMockBridge(opts?: Opts): WalletBridge<*> {
unsubscribed = true unsubscribed = true
}, },
} }
}, }),
pullMoreOperations: async (_accountId, _desiredCount) => { pullMoreOperations: async (_accountId, _desiredCount) => {
await delay(1000) await delay(1000)

6
src/bridge/types.js

@ -33,11 +33,7 @@ export interface WalletBridge<Transaction> {
// the scan can stop once all accounts are discovered. // the scan can stop once all accounts are discovered.
// the function returns a Subscription and you MUST stop everything if it is unsubscribed. // the function returns a Subscription and you MUST stop everything if it is unsubscribed.
// TODO return Observable // TODO return Observable
scanAccountsOnDevice( scanAccountsOnDevice(currency: Currency, deviceId: DeviceId): Observable<Account>;
currency: Currency,
deviceId: DeviceId,
observer: Observer<Account>,
): Subscription;
// synchronize an account. meaning updating the account object with latest state. // synchronize an account. meaning updating the account object with latest state.
// function receives the initialAccount object so you can actually know what the user side currently have // function receives the initialAccount object so you can actually know what the user side currently have

12
src/commands/installFinalFirmware.js

@ -8,20 +8,18 @@ import installFinalFirmware from 'helpers/firmware/installFinalFirmware'
type Input = { type Input = {
devicePath: string, devicePath: string,
firmware: Object, targetId: string | number,
version: string,
} }
type Result = { type Result = {
targetId: number | string, success: boolean,
version: string,
final: boolean,
mcu: boolean,
} }
const cmd: Command<Input, Result> = createCommand( const cmd: Command<Input, Result> = createCommand(
'installFinalFirmware', 'installFinalFirmware',
({ devicePath, firmware }) => ({ devicePath, ...rest }) =>
fromPromise(withDevice(devicePath)(transport => installFinalFirmware(transport, firmware))), fromPromise(withDevice(devicePath)(transport => installFinalFirmware(transport, { ...rest }))),
) )
export default cmd export default cmd

17
src/commands/libcoreHardReset.js

@ -1,20 +1,21 @@
// @flow // @flow
import { createCommand } from 'helpers/ipc' import { createCommand } from 'helpers/ipc'
import { Observable } from 'rxjs' import { fromPromise } from 'rxjs/observable/fromPromise'
import withLibcore from 'helpers/withLibcore' import withLibcore from 'helpers/withLibcore'
import createCustomErrorClass from 'helpers/createCustomErrorClass'
const HardResetFail = createCustomErrorClass('HardResetFail')
const cmd = createCommand('libcoreHardReset', () => const cmd = createCommand('libcoreHardReset', () =>
Observable.create(o => { fromPromise(
withLibcore(async core => { withLibcore(async core => {
try { const result = await core.getPoolInstance().eraseDataSince(new Date(0))
core.getPoolInstance().eraseDataSince(new Date(0)) if (result !== core.ERROR_CODE.FUTURE_WAS_SUCCESSFULL) {
o.complete() throw new HardResetFail(`Hard reset fail with ${result} (check core.ERROR_CODE)`)
} catch (e) {
o.error(e)
} }
})
}), }),
),
) )
export default cmd export default cmd

2
src/components/Breadcrumb/Step.js

@ -29,7 +29,7 @@ const Wrapper = styled(Box).attrs({
const StepNumber = styled(Box).attrs({ const StepNumber = styled(Box).attrs({
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
color: p => (['active', 'valid'].includes(p.status) ? 'white' : 'fog'), color: p => (['active', 'valid', 'error'].includes(p.status) ? 'white' : 'fog'),
bg: p => bg: p =>
['active', 'valid'].includes(p.status) ? 'wallet' : p.status === 'error' ? 'alertRed' : 'white', ['active', 'valid'].includes(p.status) ? 'wallet' : p.status === 'error' ? 'alertRed' : 'white',
ff: 'Rubik|Regular', ff: 'Rubik|Regular',

52
src/components/DashboardPage/index.js

@ -4,15 +4,17 @@ import React, { PureComponent, Fragment } from 'react'
import { compose } from 'redux' import { compose } from 'redux'
import { translate } from 'react-i18next' import { translate } from 'react-i18next'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import styled from 'styled-components'
import { push } from 'react-router-redux' import { push } from 'react-router-redux'
import { createStructuredSelector } from 'reselect' import { createStructuredSelector } from 'reselect'
import type { Account, Currency } from '@ledgerhq/live-common/lib/types' import type { Account, Currency } from '@ledgerhq/live-common/lib/types'
import type { T } from 'types/common' import type { T } from 'types/common'
import { colors } from 'styles/theme' import { colors } from 'styles/theme'
import { accountsSelector } from 'reducers/accounts' import { accountsSelector } from 'reducers/accounts'
import { openModal } from 'reducers/modals'
import { MODAL_ADD_ACCOUNTS } from 'config/constants'
import { import {
counterValueCurrencySelector, counterValueCurrencySelector,
localeSelector, localeSelector,
@ -28,11 +30,12 @@ import UpdateNotifier from 'components/UpdateNotifier'
import BalanceInfos from 'components/BalanceSummary/BalanceInfos' import BalanceInfos from 'components/BalanceSummary/BalanceInfos'
import BalanceSummary from 'components/BalanceSummary' import BalanceSummary from 'components/BalanceSummary'
import Box from 'components/base/Box' import Box from 'components/base/Box'
import { i } from 'helpers/staticPath'
import PillsDaysCount from 'components/PillsDaysCount' import PillsDaysCount from 'components/PillsDaysCount'
import Text from 'components/base/Text' import Text from 'components/base/Text'
import OperationsList from 'components/OperationsList' import OperationsList from 'components/OperationsList'
import StickyBackToTop from 'components/StickyBackToTop' import StickyBackToTop from 'components/StickyBackToTop'
import Button from 'components/base/Button'
import AccountCard from './AccountCard' import AccountCard from './AccountCard'
import AccountsOrder from './AccountsOrder' import AccountsOrder from './AccountsOrder'
import EmptyState from './EmptyState' import EmptyState from './EmptyState'
@ -48,6 +51,7 @@ const mapDispatchToProps = {
push, push,
reorderAccounts, reorderAccounts,
saveSettings, saveSettings,
openModal,
} }
type Props = { type Props = {
@ -57,6 +61,7 @@ type Props = {
counterValue: Currency, counterValue: Currency,
selectedTimeRange: TimeRange, selectedTimeRange: TimeRange,
saveSettings: ({ selectedTimeRange: TimeRange }) => *, saveSettings: ({ selectedTimeRange: TimeRange }) => *,
openModal: string => void,
} }
class DashboardPage extends PureComponent<Props> { class DashboardPage extends PureComponent<Props> {
@ -82,11 +87,11 @@ class DashboardPage extends PureComponent<Props> {
_cacheBalance = null _cacheBalance = null
render() { render() {
const { accounts, t, counterValue, selectedTimeRange } = this.props const { accounts, t, counterValue, selectedTimeRange, openModal } = this.props
const daysCount = timeRangeDaysByKey[selectedTimeRange] const daysCount = timeRangeDaysByKey[selectedTimeRange]
const timeFrame = this.handleGreeting() const timeFrame = this.handleGreeting()
const imagePath = i('empty-account-tile.svg')
const totalAccounts = accounts.length const totalAccounts = accounts.length
const displayOperationsHelper = (account: Account) => account.operations.length > 0 const displayOperationsHelper = (account: Account) => account.operations.length > 0
const displayOperations = accounts.some(displayOperationsHelper) const displayOperations = accounts.some(displayOperationsHelper)
@ -155,10 +160,19 @@ class DashboardPage extends PureComponent<Props> {
style={{ margin: '0 -16px' }} style={{ margin: '0 -16px' }}
> >
{accounts {accounts
.concat(Array(3 - (accounts.length % 3)).fill(null)) .concat(
Array(3 - (accounts.length % 3))
.fill(null)
.map((_, i) => i === 0),
)
.map((account, i) => ( .map((account, i) => (
<Box key={account ? account.id : `placeholder_${i}`} flex="33%" p={16}> <Box
key={typeof account === 'object' ? account.id : `placeholder_${i}`}
flex="33%"
p={16}
>
{account ? ( {account ? (
typeof account === 'object' ? (
<AccountCard <AccountCard
key={account.id} key={account.id}
counterValue={counterValue} counterValue={counterValue}
@ -166,6 +180,23 @@ class DashboardPage extends PureComponent<Props> {
daysCount={daysCount} daysCount={daysCount}
onClick={this.onAccountClick} onClick={this.onAccountClick}
/> />
) : (
<Wrapper>
<img alt="" src={imagePath} />
<Box
ff="Open Sans"
fontSize={3}
color="graphite"
pb={2}
textAlign="center"
>
{t('app:dashboard.emptyAccountTile.desc')}
</Box>
<Button primary onClick={() => openModal(MODAL_ADD_ACCOUNTS)}>
{t('app:dashboard.emptyAccountTile.createAccount')}
</Button>
</Wrapper>
)
) : null} ) : null}
</Box> </Box>
))} ))}
@ -198,3 +229,12 @@ export default compose(
), ),
translate(), translate(),
)(DashboardPage) )(DashboardPage)
const Wrapper = styled(Box).attrs({
p: 4,
flex: 1,
alignItems: 'center',
})`
border: 1px dashed ${p => p.theme.colors.fog};
border-radius: 4px;
`

17
src/components/DeviceConnect/index.js

@ -204,7 +204,7 @@ class DeviceConnect extends PureComponent<Props> {
const hasDevice = devices.length > 0 const hasDevice = devices.length > 0
const hasMultipleDevices = devices.length > 1 const hasMultipleDevices = devices.length > 1
// TODO: place custom wording in trans tags into yml file
/* eslint-disable react/jsx-no-literals */ /* eslint-disable react/jsx-no-literals */
return ( return (
<Box flow={4} ff="Open Sans"> <Box flow={4} ff="Open Sans">
@ -215,8 +215,7 @@ class DeviceConnect extends PureComponent<Props> {
</StepIcon> </StepIcon>
<Box grow shrink> <Box grow shrink>
<Trans i18nKey="app:deviceConnect.step1.connect" parent="div"> <Trans i18nKey="app:deviceConnect.step1.connect" parent="div">
Connect your <strong>Ledger device</strong> to your computer and enter your{' '} Connect and unlock your <strong>Ledger device</strong>
<strong>PIN code</strong> on your device
</Trans> </Trans>
</Box> </Box>
<StepCheck checked={hasDevice} /> <StepCheck checked={hasDevice} />
@ -260,9 +259,9 @@ class DeviceConnect extends PureComponent<Props> {
</StepIcon> </StepIcon>
<Box grow shrink> <Box grow shrink>
<Trans i18nKey="deviceConnect:step2.open" parent="div"> <Trans i18nKey="deviceConnect:step2.open" parent="div">
{'Open '} {'Open the '}
<strong>{currency.name}</strong> <strong>{currency.name}</strong>
{' App on your device'} {' app on your device'}
</Trans> </Trans>
</Box> </Box>
<StepCheck checked={appState.success} hasErrors={appState.fail} /> <StepCheck checked={appState.success} hasErrors={appState.fail} />
@ -275,8 +274,8 @@ class DeviceConnect extends PureComponent<Props> {
</WrapperIconCurrency> </WrapperIconCurrency>
</StepIcon> </StepIcon>
<Box grow shrink> <Box grow shrink>
<Trans i18nKey="deviceConnect:dashboard.open" parent="div"> <Trans i18nKey="app:dashboard.open" parent="div">
{'Go to the '} {'Navigate to the '}
<strong>{'dashboard'}</strong> <strong>{'dashboard'}</strong>
{' on your device'} {' on your device'}
</Trans> </Trans>
@ -299,8 +298,8 @@ class DeviceConnect extends PureComponent<Props> {
</StepIcon> </StepIcon>
<Box grow shrink> <Box grow shrink>
<Trans i18nKey="deviceConnect:stepGenuine.open" parent="div"> <Trans i18nKey="deviceConnect:stepGenuine.open" parent="div">
{'Confirm '} {'Allow the '}
<strong>{'authentication'}</strong> <strong>{'Ledger Manager'}</strong>
{' on your device'} {' on your device'}
</Trans> </Trans>
</Box> </Box>

14
src/components/ExportLogsBtn.js

@ -6,6 +6,7 @@ import { webFrame, remote } from 'electron'
import React, { Component } from 'react' import React, { Component } from 'react'
import { translate } from 'react-i18next' import { translate } from 'react-i18next'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import KeyHandler from 'react-key-handler'
import { createStructuredSelector, createSelector } from 'reselect' import { createStructuredSelector, createSelector } from 'reselect'
import { accountsSelector, encodeAccountsModel } from 'reducers/accounts' import { accountsSelector, encodeAccountsModel } from 'reducers/accounts'
import { storeSelector as settingsSelector } from 'reducers/settings' import { storeSelector as settingsSelector } from 'reducers/settings'
@ -20,6 +21,7 @@ class ExportLogsBtn extends Component<{
t: *, t: *,
settings: *, settings: *,
accounts: *, accounts: *,
hookToShortcut?: boolean,
}> { }> {
handleExportLogs = () => { handleExportLogs = () => {
const { accounts, settings } = this.props const { accounts, settings } = this.props
@ -49,9 +51,17 @@ class ExportLogsBtn extends Component<{
} }
} }
onKeyHandle = e => {
if (e.ctrlKey) {
this.handleExportLogs()
}
}
render() { render() {
const { t } = this.props const { t, hookToShortcut } = this.props
return ( return hookToShortcut ? (
<KeyHandler keyValue="e" onKeyHandle={this.onKeyHandle} />
) : (
<Button primary onClick={this.handleExportLogs}> <Button primary onClick={this.handleExportLogs}>
{t('app:settings.exportLogs.btn')} {t('app:settings.exportLogs.btn')}
</Button> </Button>

14
src/components/GradientBox.js

@ -0,0 +1,14 @@
// @flow
import styled from 'styled-components'
export default styled.div`
width: 100%;
height: 60px;
position: absolute;
bottom: 68px;
left: 0;
right: 0;
background: linear-gradient(rgba(255, 255, 255, 0), #ffffff 70%);
z-index: 2;
pointer-events: none;
`

101
src/components/ManagerPage/AppsList.js

@ -1,7 +1,7 @@
// @flow // @flow
/* eslint-disable react/jsx-no-literals */ // FIXME /* eslint-disable react/jsx-no-literals */ // FIXME
import React, { PureComponent } from 'react' import React, { PureComponent, Fragment } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { translate } from 'react-i18next' import { translate } from 'react-i18next'
@ -13,14 +13,16 @@ import installApp from 'commands/installApp'
import uninstallApp from 'commands/uninstallApp' import uninstallApp from 'commands/uninstallApp'
import Box from 'components/base/Box' import Box from 'components/base/Box'
import Modal, { ModalBody } from 'components/base/Modal' import Modal, { ModalBody, ModalFooter, ModalTitle, ModalContent } from 'components/base/Modal'
import Tooltip from 'components/base/Tooltip' import Tooltip from 'components/base/Tooltip'
import Text from 'components/base/Text' import Text from 'components/base/Text'
import Progress from 'components/base/Progress' import Progress from 'components/base/Progress'
import Spinner from 'components/base/Spinner' import Spinner from 'components/base/Spinner'
import Button from 'components/base/Button' import Button from 'components/base/Button'
import TranslatedError from 'components/TranslatedError'
import ExclamationCircle from 'icons/ExclamationCircle' import ExclamationCircle from 'icons/ExclamationCircle'
import ExclamationCircleThin from 'icons/ExclamationCircleThin'
import Update from 'icons/Update' import Update from 'icons/Update'
import Trash from 'icons/Trash' import Trash from 'icons/Trash'
import CheckCircle from 'icons/CheckCircle' import CheckCircle from 'icons/CheckCircle'
@ -53,8 +55,9 @@ type Props = {
type State = { type State = {
status: Status, status: Status,
error: string | null, error: ?Error,
appsList: LedgerScriptParams[] | Array<*>, appsList: LedgerScriptParams[],
appsLoaded: boolean,
app: string, app: string,
mode: Mode, mode: Mode,
} }
@ -64,6 +67,7 @@ class AppsList extends PureComponent<Props, State> {
status: 'loading', status: 'loading',
error: null, error: null,
appsList: [], appsList: [],
appsLoaded: false,
app: '', app: '',
mode: 'home', mode: 'home',
} }
@ -84,10 +88,10 @@ class AppsList extends PureComponent<Props, State> {
const appsList = CACHED_APPS || (await listApps.send({ targetId, version }).toPromise()) const appsList = CACHED_APPS || (await listApps.send({ targetId, version }).toPromise())
CACHED_APPS = appsList CACHED_APPS = appsList
if (!this._unmounted) { if (!this._unmounted) {
this.setState({ appsList, status: 'idle' }) this.setState({ appsList, status: 'idle', appsLoaded: true })
} }
} catch (err) { } catch (err) {
this.setState({ status: 'error', error: err.message }) this.setState({ status: 'error', error: err })
} }
} }
@ -100,9 +104,9 @@ class AppsList extends PureComponent<Props, State> {
} = this.props } = this.props
const data = { app, devicePath, targetId } const data = { app, devicePath, targetId }
await installApp.send(data).toPromise() await installApp.send(data).toPromise()
this.setState({ status: 'success', app: '' }) this.setState({ status: 'success' })
} catch (err) { } catch (err) {
this.setState({ status: 'error', error: err.message, app: '', mode: 'home' }) this.setState({ status: 'error', error: err, mode: 'home' })
} }
} }
@ -117,7 +121,7 @@ class AppsList extends PureComponent<Props, State> {
await uninstallApp.send(data).toPromise() await uninstallApp.send(data).toPromise()
this.setState({ status: 'success', app: '' }) this.setState({ status: 'success', app: '' })
} catch (err) { } catch (err) {
this.setState({ status: 'error', error: err.message, app: '', mode: 'home' }) this.setState({ status: 'error', error: err, app: '', mode: 'home' })
} }
} }
@ -126,47 +130,84 @@ class AppsList extends PureComponent<Props, State> {
renderModal = () => { renderModal = () => {
const { t } = this.props const { t } = this.props
const { app, status, error, mode } = this.state const { app, status, error, mode } = this.state
return ( return (
<Modal <Modal
isOpened={status !== 'idle' && status !== 'loading'} isOpened={status !== 'idle' && status !== 'loading'}
render={() => ( render={() => (
<ModalBody p={6} align="center" justify="center" style={{ height: 300 }}> <ModalBody align="center" justify="center" style={{ height: 300 }}>
{status === 'busy' || status === 'idle' ? ( {status === 'busy' || status === 'idle' ? (
<Box align="center" justify="center" flow={3}> <Fragment>
{mode === 'installing' ? <Update size={30} /> : <Trash size={30} />} <ModalTitle>
{mode === 'installing' ? (
<Box color="grey">
<Update size={30} />
</Box>
) : (
<Box color="grey">
<Trash size={30} />
</Box>
)}
</ModalTitle>
<ModalContent>
<Text ff="Museo Sans|Regular" fontSize={6} color="dark"> <Text ff="Museo Sans|Regular" fontSize={6} color="dark">
{t(`app:manager.apps.${mode}`, { app })} {t(`app:manager.apps.${mode}`, { app })}
</Text> </Text>
<Box my={5} style={{ width: 250 }}> <Box mt={6}>
<Progress style={{ width: '100%' }} infinite /> <Progress style={{ width: '100%' }} infinite />
</Box> </Box>
</Box> </ModalContent>
</Fragment>
) : status === 'error' ? ( ) : status === 'error' ? (
<Box align="center" justify="center" flow={3}> <Fragment>
<div>{'error happened'}</div> <ModalContent grow align="center" justify="center" mt={3}>
{error} <Box color="alertRed">
<Button primary onClick={this.handleCloseModal}> <ExclamationCircleThin size={44} />
close
</Button>
</Box> </Box>
<Box
color="black"
mt={4}
fontSize={6}
ff="Museo Sans|Regular"
textAlign="center"
style={{ maxWidth: 350 }}
>
<TranslatedError error={error} />
</Box>
</ModalContent>
<ModalFooter horizontal justifyContent="flex-end" style={{ width: '100%' }}>
<Button primary padded onClick={this.handleCloseModal}>
{t('app:common.close')}
</Button>
</ModalFooter>
</Fragment>
) : status === 'success' ? ( ) : status === 'success' ? (
<Box align="center" justify="center" flow={3}> <Fragment>
<ModalContent grow align="center" justify="center" mt={3}>
<Box color="positiveGreen"> <Box color="positiveGreen">
<CheckCircle size={30} /> <CheckCircle size={44} />
</Box> </Box>
<Text ff="Museo Sans|Regular" fontSize={6} color="dark"> <Box
color="black"
mt={4}
fontSize={6}
ff="Museo Sans|Regular"
textAlign="center"
style={{ maxWidth: 350 }}
>
{t( {t(
`app:manager.apps.${ `app:manager.apps.${
mode === 'installing' ? 'installSuccess' : 'uninstallSuccess' mode === 'installing' ? 'installSuccess' : 'uninstallSuccess'
}`, }`,
{ app }, { app },
)} )}
</Text>
<Button primary onClick={this.handleCloseModal}>
close
</Button>
</Box> </Box>
</ModalContent>
<ModalFooter horizontal justifyContent="flex-end" style={{ width: '100%' }}>
<Button primary padded onClick={this.handleCloseModal}>
{t('app:common.close')}
</Button>
</ModalFooter>
</Fragment>
) : null} ) : null}
</ModalBody> </ModalBody>
)} )}
@ -175,8 +216,8 @@ class AppsList extends PureComponent<Props, State> {
} }
renderList() { renderList() {
const { appsList, status } = this.state const { appsList, appsLoaded } = this.state
return status === 'idle' ? ( return appsLoaded ? (
<Box> <Box>
<AppSearchBar list={appsList}> <AppSearchBar list={appsList}>
{items => ( {items => (

3
src/components/ManagerPage/Dashboard.js

@ -2,6 +2,7 @@
import React from 'react' import React from 'react'
import { translate } from 'react-i18next' import { translate } from 'react-i18next'
import { EXPERIMENTAL_FIRMWARE_UPDATE } from 'config/constants'
import type { T, Device } from 'types/common' import type { T, Device } from 'types/common'
import Box from 'components/base/Box' import Box from 'components/base/Box'
@ -34,6 +35,7 @@ const Dashboard = ({ device, deviceInfo, t }: Props) => (
</Text> </Text>
</Box> </Box>
<Box mt={5}> <Box mt={5}>
{EXPERIMENTAL_FIRMWARE_UPDATE ? (
<FirmwareUpdate <FirmwareUpdate
infos={{ infos={{
targetId: deviceInfo.targetId, targetId: deviceInfo.targetId,
@ -41,6 +43,7 @@ const Dashboard = ({ device, deviceInfo, t }: Props) => (
}} }}
device={device} device={device}
/> />
) : null}
</Box> </Box>
<Box mt={5}> <Box mt={5}>
<AppsList device={device} targetId={deviceInfo.targetId} version={deviceInfo.version} /> <AppsList device={device} targetId={deviceInfo.targetId} version={deviceInfo.version} />

30
src/components/ManagerPage/FinalFirmwareUpdate.js → src/components/ManagerPage/FirmwareFinalUpdate.js

@ -2,8 +2,12 @@
import React, { PureComponent } from 'react' import React, { PureComponent } from 'react'
import { translate } from 'react-i18next' import { translate } from 'react-i18next'
import logger from 'logger'
import type { Device, T } from 'types/common' import type { Device, T } from 'types/common'
import installFinalFirmware from 'commands/installFinalFirmware'
import Box, { Card } from 'components/base/Box' import Box, { Card } from 'components/base/Box'
// import Button from 'components/base/Button' // import Button from 'components/base/Button'
@ -18,15 +22,9 @@ type Props = {
infos: DeviceInfos, infos: DeviceInfos,
} }
type State = { type State = {}
// latestFirmware: ?FirmwareInfos,
}
class FirmwareUpdate extends PureComponent<Props, State> {
state = {
// latestFirmware: null,
}
class FirmwareFinalUpdate extends PureComponent<Props, State> {
componentDidMount() {} componentDidMount() {}
componentWillUnmount() { componentWillUnmount() {
@ -35,6 +33,20 @@ class FirmwareUpdate extends PureComponent<Props, State> {
_unmounting = false _unmounting = false
installFinalFirmware = async () => {
try {
const { device, infos } = this.props
const { success } = await installFinalFirmware
.send({ devicePath: device.path, targetId: infos.targetId, version: infos.version })
.toPromise()
if (success) {
this.setState()
}
} catch (err) {
logger.log(err)
}
}
render() { render() {
const { t, ...props } = this.props const { t, ...props } = this.props
@ -51,4 +63,4 @@ class FirmwareUpdate extends PureComponent<Props, State> {
} }
} }
export default translate()(FirmwareUpdate) export default translate()(FirmwareFinalUpdate)

65
src/components/ManagerPage/FirmwareUpdate.js

@ -1,8 +1,8 @@
// @flow // @flow
/* eslint-disable react/jsx-no-literals */ // FIXME /* eslint-disable react/jsx-no-literals */ // FIXME
import React, { PureComponent } from 'react' import React, { PureComponent, Fragment } from 'react'
import { translate } from 'react-i18next' import { translate, Trans } from 'react-i18next'
import isEqual from 'lodash/isEqual' import isEqual from 'lodash/isEqual'
import isEmpty from 'lodash/isEmpty' import isEmpty from 'lodash/isEmpty'
import invariant from 'invariant' import invariant from 'invariant'
@ -17,19 +17,28 @@ import installOsuFirmware from 'commands/installOsuFirmware'
import Box, { Card } from 'components/base/Box' import Box, { Card } from 'components/base/Box'
import Text from 'components/base/Text' import Text from 'components/base/Text'
import Modal, { ModalBody, ModalFooter, ModalTitle, ModalContent } from 'components/base/Modal'
import Button from 'components/base/Button'
// import Progress from 'components/base/Progress'
import NanoS from 'icons/device/NanoS' import NanoS from 'icons/device/NanoS'
import CheckFull from 'icons/CheckFull' import CheckFull from 'icons/CheckFull'
import { PreventDeviceChangeRecheck } from '../Workflow/EnsureDevice'
import UpdateFirmwareButton from './UpdateFirmwareButton' import UpdateFirmwareButton from './UpdateFirmwareButton'
let CACHED_LATEST_FIRMWARE = null let CACHED_LATEST_FIRMWARE = null
export const getCleanVersion = (input: string): string =>
input.endsWith('-osu') ? input.replace('-osu', '') : input
type DeviceInfos = { type DeviceInfos = {
targetId: number | string, targetId: number | string,
version: string, version: string,
} }
type ModalStatus = 'closed' | 'disclaimer' | 'installing' | 'error' | 'success'
type Props = { type Props = {
t: T, t: T,
device: Device, device: Device,
@ -38,11 +47,13 @@ type Props = {
type State = { type State = {
latestFirmware: ?LedgerScriptParams, latestFirmware: ?LedgerScriptParams,
modal: ModalStatus,
} }
class FirmwareUpdate extends PureComponent<Props, State> { class FirmwareUpdate extends PureComponent<Props, State> {
state = { state = {
latestFirmware: null, latestFirmware: null,
modal: 'closed',
} }
componentDidMount() { componentDidMount() {
@ -86,6 +97,7 @@ class FirmwareUpdate extends PureComponent<Props, State> {
const { const {
device: { path: devicePath }, device: { path: devicePath },
} = this.props } = this.props
this.setState({ modal: 'installing' })
const { success } = await installOsuFirmware const { success } = await installOsuFirmware
.send({ devicePath, firmware: latestFirmware, targetId: infos.targetId }) .send({ devicePath, firmware: latestFirmware, targetId: infos.targetId })
.toPromise() .toPromise()
@ -97,9 +109,49 @@ class FirmwareUpdate extends PureComponent<Props, State> {
} }
} }
handleCloseModal = () => this.setState({ modal: 'closed' })
handleInstallModal = () => this.setState({ modal: 'disclaimer' })
renderModal = () => {
const { t } = this.props
const { modal, latestFirmware } = this.state
return (
<Modal
isOpened={modal !== 'closed'}
render={() => (
<ModalBody grow align="center" justify="center" mt={3}>
<Fragment>
<ModalTitle>{t('app:manager.firmware.update')}</ModalTitle>
<ModalContent>
<Text ff="Open Sans|Regular" fontSize={4} color="graphite" align="center">
<Trans i18nKey="app:manager.firmware.disclaimerTitle">
You are about to install the latest
<Text ff="Open Sans|SemiBold" color="dark">
{`firmware ${latestFirmware ? getCleanVersion(latestFirmware.name) : ''}`}
</Text>
</Trans>
</Text>
<Text ff="Open Sans|Regular" fontSize={4} color="graphite" align="center">
{t('app:manager.firmware.disclaimerAppDelete')}
{t('app:manager.firmware.disclaimerAppReinstall')}
</Text>
</ModalContent>
<ModalFooter horizontal justifyContent="flex-end" style={{ width: '100%' }}>
<Button primary padded onClick={this.installFirmware}>
{t('app:manager.firmware.continue')}
</Button>
</ModalFooter>
</Fragment>
</ModalBody>
)}
/>
)
}
render() { render() {
const { infos, t } = this.props const { infos, t } = this.props
const { latestFirmware } = this.state const { latestFirmware, modal } = this.state
return ( return (
<Card p={4}> <Card p={4}>
@ -122,8 +174,13 @@ class FirmwareUpdate extends PureComponent<Props, State> {
})} })}
</Text> </Text>
</Box> </Box>
<UpdateFirmwareButton firmware={latestFirmware} installFirmware={this.installFirmware} /> <UpdateFirmwareButton
firmware={latestFirmware}
installFirmware={this.handleInstallModal}
/>
</Box> </Box>
{modal !== 'closed' ? <PreventDeviceChangeRecheck /> : null}
{this.renderModal()}
</Card> </Card>
) )
} }

4
src/components/ManagerPage/UpdateFirmwareButton.js

@ -6,6 +6,7 @@ import type { T } from 'types/common'
import Button from 'components/base/Button' import Button from 'components/base/Button'
import Text from 'components/base/Text' import Text from 'components/base/Text'
import { getCleanVersion } from 'components/ManagerPage/FirmwareUpdate'
type FirmwareInfos = { type FirmwareInfos = {
name: string, name: string,
@ -18,9 +19,6 @@ type Props = {
installFirmware: () => void, installFirmware: () => void,
} }
const getCleanVersion = (input: string): string =>
input.endsWith('-osu') ? input.replace('-osu', '') : input
const UpdateFirmwareButton = ({ t, firmware, installFirmware }: Props) => const UpdateFirmwareButton = ({ t, firmware, installFirmware }: Props) =>
firmware ? ( firmware ? (
<Fragment> <Fragment>

9
src/components/ManagerPage/index.js

@ -1,9 +1,8 @@
// @flow // @flow
/* eslint-disable react/jsx-no-literals */ // FIXME /* eslint-disable react/jsx-no-literals */ // FIXME: remove
import React from 'react' import React, { PureComponent } from 'react'
import type { Node } from 'react'
import type { Device } from 'types/common' import type { Device } from 'types/common'
import Workflow from 'components/Workflow' import Workflow from 'components/Workflow'
@ -23,7 +22,8 @@ type Error = {
stack: string, stack: string,
} }
function ManagerPage(): Node { class ManagerPage extends PureComponent<*, *> {
render() {
return ( return (
<Workflow <Workflow
renderFinalUpdate={(device: Device, deviceInfo: DeviceInfo) => ( renderFinalUpdate={(device: Device, deviceInfo: DeviceInfo) => (
@ -53,6 +53,7 @@ function ManagerPage(): Node {
)} )}
/> />
) )
}
} }
export default ManagerPage export default ManagerPage

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

@ -156,7 +156,6 @@ class GenuineCheck extends PureComponent<StepProps, State> {
const { nextStep, prevStep, t, onboarding } = this.props const { nextStep, prevStep, t, onboarding } = this.props
const { genuine } = onboarding const { genuine } = onboarding
const { cachedPinStepButton, cachedRecoveryStepButton, isGenuineCheckModalOpened } = this.state const { cachedPinStepButton, cachedRecoveryStepButton, isGenuineCheckModalOpened } = this.state
if (genuine.displayErrorScreen) { if (genuine.displayErrorScreen) {
return this.renderGenuineFail() return this.renderGenuineFail()
} }
@ -165,18 +164,21 @@ class GenuineCheck extends PureComponent<StepProps, State> {
<FixedTopContainer> <FixedTopContainer>
<StepContainerInner> <StepContainerInner>
<Title>{t('onboarding:genuineCheck.title')}</Title> <Title>{t('onboarding:genuineCheck.title')}</Title>
{onboarding.isLedgerNano ? ( {onboarding.flowType === 'restoreDevice' ? (
<Description>{t('onboarding:genuineCheck.descNano')}</Description> <Description>{t('onboarding:genuineCheck.descRestore')}</Description>
) : ( ) : (
<Description>{t('onboarding:genuineCheck.descBlue')}</Description> <Description>
{onboarding.isLedgerNano
? t('onboarding:genuineCheck.descNano')
: t('onboarding:genuineCheck.descBlue')}
</Description>
)} )}
<Box mt={5}> <Box mt={5}>
<CardWrapper> <CardWrapper>
<Box justify="center"> <Box justify="center">
<Box horizontal> <Box horizontal>
<IconOptionRow>{'1.'}</IconOptionRow> <IconOptionRow>{'1.'}</IconOptionRow>
<CardTitle>{t('onboarding:genuineCheck.steps.step1.title')}</CardTitle> <CardTitle>{t('onboarding:genuineCheck.step1.title')}</CardTitle>
</Box> </Box>
</Box> </Box>
<Box justify="center"> <Box justify="center">
@ -195,7 +197,7 @@ class GenuineCheck extends PureComponent<StepProps, State> {
<IconOptionRow color={!genuine.pinStepPass ? 'grey' : 'wallet'}> <IconOptionRow color={!genuine.pinStepPass ? 'grey' : 'wallet'}>
{'2.'} {'2.'}
</IconOptionRow> </IconOptionRow>
<CardTitle>{t('onboarding:genuineCheck.steps.step2.title')}</CardTitle> <CardTitle>{t('onboarding:genuineCheck.step2.title')}</CardTitle>
</Box> </Box>
</Box> </Box>
<Box justify="center"> <Box justify="center">
@ -216,7 +218,7 @@ class GenuineCheck extends PureComponent<StepProps, State> {
<IconOptionRow color={!genuine.recoveryStepPass ? 'grey' : 'wallet'}> <IconOptionRow color={!genuine.recoveryStepPass ? 'grey' : 'wallet'}>
{'3.'} {'3.'}
</IconOptionRow> </IconOptionRow>
<CardTitle>{t('onboarding:genuineCheck.steps.step3.title')}</CardTitle> <CardTitle>{t('onboarding:genuineCheck.step3.title')}</CardTitle>
</Box> </Box>
</Box> </Box>
{genuine.recoveryStepPass && ( {genuine.recoveryStepPass && (

6
src/components/Onboarding/steps/SelectPIN/SelectPINblue.js

@ -24,17 +24,17 @@ class SelectPIN extends PureComponent<Props, *> {
{ {
key: 'step1', key: 'step1',
icon: <IconOptionRow>{'1.'}</IconOptionRow>, icon: <IconOptionRow>{'1.'}</IconOptionRow>,
desc: t('onboarding:selectPIN.instructions.ledgerBlue.step1'), desc: t('onboarding:selectPIN.initialize.instructions.blue.step1'),
}, },
{ {
key: 'step2', key: 'step2',
icon: <IconOptionRow>{'2.'}</IconOptionRow>, icon: <IconOptionRow>{'2.'}</IconOptionRow>,
desc: t('onboarding:selectPIN.instructions.ledgerBlue.step2'), desc: t('onboarding:selectPIN.initialize.instructions.blue.step2'),
}, },
{ {
key: 'step3', key: 'step3',
icon: <IconOptionRow>{'3.'}</IconOptionRow>, icon: <IconOptionRow>{'3.'}</IconOptionRow>,
desc: t('onboarding:selectPIN.instructions.ledgerBlue.step3'), desc: t('onboarding:selectPIN.initialize.instructions.blue.step3'),
}, },
] ]

8
src/components/Onboarding/steps/SelectPIN/SelectPINnano.js

@ -24,22 +24,22 @@ class SelectPINnano extends PureComponent<Props, *> {
{ {
key: 'step1', key: 'step1',
icon: <IconOptionRow>{'1.'}</IconOptionRow>, icon: <IconOptionRow>{'1.'}</IconOptionRow>,
desc: t('onboarding:selectPIN.instructions.ledgerNano.step1'), desc: t('onboarding:selectPIN.initialize.instructions.nano.step1'),
}, },
{ {
key: 'step2', key: 'step2',
icon: <IconOptionRow>{'2.'}</IconOptionRow>, icon: <IconOptionRow>{'2.'}</IconOptionRow>,
desc: t('onboarding:selectPIN.instructions.ledgerNano.step2'), desc: t('onboarding:selectPIN.initialize.instructions.nano.step2'),
}, },
{ {
key: 'step3', key: 'step3',
icon: <IconOptionRow>{'3.'}</IconOptionRow>, icon: <IconOptionRow>{'3.'}</IconOptionRow>,
desc: t('onboarding:selectPIN.instructions.ledgerNano.step3'), desc: t('onboarding:selectPIN.initialize.instructions.nano.step3'),
}, },
{ {
key: 'step4', key: 'step4',
icon: <IconOptionRow>{'4.'}</IconOptionRow>, icon: <IconOptionRow>{'4.'}</IconOptionRow>,
desc: t('onboarding:selectPIN.instructions.ledgerNano.step4'), desc: t('onboarding:selectPIN.initialize.instructions.nano.step4'),
}, },
] ]
const disclaimerNotes = [ const disclaimerNotes = [

77
src/components/Onboarding/steps/SelectPIN/SelectPINrestoreBlue.js

@ -0,0 +1,77 @@
// @flow
import React, { PureComponent } from 'react'
import { translate } from 'react-i18next'
import { colors } from 'styles/theme'
import Box from 'components/base/Box'
import type { T } from 'types/common'
import IconLedgerBlueSelectPIN from 'icons/illustrations/LedgerBlueSelectPIN'
import IconChevronRight from 'icons/ChevronRight'
import { IconOptionRow, DisclaimerBox, OptionRow, Inner } from '../../helperComponents'
type Props = {
t: T,
}
class SelectPINrestoreBlue extends PureComponent<Props, *> {
render() {
const { t } = this.props
const stepsLedgerBlue = [
{
key: 'step1',
icon: <IconOptionRow>{'1.'}</IconOptionRow>,
desc: t('onboarding:selectPIN.restore.instructions.blue.step1'),
},
{
key: 'step2',
icon: <IconOptionRow>{'2.'}</IconOptionRow>,
desc: t('onboarding:selectPIN.restore.instructions.blue.step2'),
},
{
key: 'step3',
icon: <IconOptionRow>{'3.'}</IconOptionRow>,
desc: t('onboarding:selectPIN.restore.instructions.blue.step3'),
},
]
const disclaimerNotes = [
{
key: 'note1',
icon: <IconChevronRight size={12} style={{ color: colors.smoke }} />,
desc: t('onboarding:selectPIN.disclaimer.note1'),
},
{
key: 'note2',
icon: <IconChevronRight size={12} style={{ color: colors.smoke }} />,
desc: t('onboarding:selectPIN.disclaimer.note2'),
},
{
key: 'note3',
icon: <IconChevronRight size={12} style={{ color: colors.smoke }} />,
desc: t('onboarding:selectPIN.disclaimer.note3'),
},
]
return (
<Box align="center">
<Inner style={{ width: 550 }}>
<Box style={{ width: 180, justifyContent: 'center', alignItems: 'center' }}>
<IconLedgerBlueSelectPIN />
</Box>
<Box>
<Box shrink grow flow={4}>
{stepsLedgerBlue.map(step => <OptionRow key={step.key} step={step} />)}
</Box>
</Box>
</Inner>
<DisclaimerBox mt={6} disclaimerNotes={disclaimerNotes} />
</Box>
)
}
}
export default translate()(SelectPINrestoreBlue)

77
src/components/Onboarding/steps/SelectPIN/SelectPINrestoreNano.js

@ -0,0 +1,77 @@
// @flow
import React, { PureComponent } from 'react'
import { translate } from 'react-i18next'
import { colors } from 'styles/theme'
import Box from 'components/base/Box'
import type { T } from 'types/common'
import IconLedgerNanoSelectPIN from 'icons/illustrations/LedgerNanoSelectPIN'
import IconChevronRight from 'icons/ChevronRight'
import { IconOptionRow, DisclaimerBox, OptionRow, Inner } from '../../helperComponents'
type Props = {
t: T,
}
class SelectPINrestoreNano extends PureComponent<Props, *> {
render() {
const { t } = this.props
const stepsLedgerNano = [
{
key: 'step1',
icon: <IconOptionRow>{'1.'}</IconOptionRow>,
desc: t('onboarding:selectPIN.restore.instructions.nano.step1'),
},
{
key: 'step2',
icon: <IconOptionRow>{'2.'}</IconOptionRow>,
desc: t('onboarding:selectPIN.restore.instructions.nano.step2'),
},
{
key: 'step3',
icon: <IconOptionRow>{'3.'}</IconOptionRow>,
desc: t('onboarding:selectPIN.restore.instructions.nano.step3'),
},
{
key: 'step4',
icon: <IconOptionRow>{'4.'}</IconOptionRow>,
desc: t('onboarding:selectPIN.restore.instructions.nano.step4'),
},
]
const disclaimerNotes = [
{
key: 'note1',
icon: <IconChevronRight size={12} style={{ color: colors.smoke }} />,
desc: t('onboarding:selectPIN.disclaimer.note1'),
},
{
key: 'note2',
icon: <IconChevronRight size={12} style={{ color: colors.smoke }} />,
desc: t('onboarding:selectPIN.disclaimer.note2'),
},
{
key: 'note3',
icon: <IconChevronRight size={12} style={{ color: colors.smoke }} />,
desc: t('onboarding:selectPIN.disclaimer.note3'),
},
]
return (
<Box align="center" mt={3}>
<Inner style={{ width: 700 }}>
<IconLedgerNanoSelectPIN />
<Box shrink grow flow={4} style={{ marginLeft: 40 }}>
{stepsLedgerNano.map(step => <OptionRow key={step.key} step={step} />)}
</Box>
</Inner>
<DisclaimerBox mt={6} disclaimerNotes={disclaimerNotes} />
</Box>
)
}
}
export default translate()(SelectPINrestoreNano)

13
src/components/Onboarding/steps/SelectPIN/index.js

@ -8,6 +8,8 @@ import { Title, FixedTopContainer } from '../../helperComponents'
import OnboardingFooter from '../../OnboardingFooter' import OnboardingFooter from '../../OnboardingFooter'
import SelectPINnano from './SelectPINnano' import SelectPINnano from './SelectPINnano'
import SelectPINblue from './SelectPINblue' import SelectPINblue from './SelectPINblue'
import SelectPINrestoreNano from './SelectPINrestoreNano'
import SelectPINrestoreBlue from './SelectPINrestoreBlue'
import type { StepProps } from '../..' import type { StepProps } from '../..'
@ -16,12 +18,21 @@ export default (props: StepProps) => {
return ( return (
<FixedTopContainer> <FixedTopContainer>
{onboarding.flowType === 'restoreDevice' ? (
<Box grow alignItems="center"> <Box grow alignItems="center">
<Title>{t('onboarding:selectPIN.title')}</Title> <Title>{t('onboarding:selectPIN.restore.title')}</Title>
<Box align="center" mt={7}>
{onboarding.isLedgerNano ? <SelectPINrestoreNano /> : <SelectPINrestoreBlue />}
</Box>
</Box>
) : (
<Box grow alignItems="center">
<Title>{t('onboarding:selectPIN.initialize.title')}</Title>
<Box align="center" mt={7}> <Box align="center" mt={7}>
{onboarding.isLedgerNano ? <SelectPINnano /> : <SelectPINblue />} {onboarding.isLedgerNano ? <SelectPINnano /> : <SelectPINblue />}
</Box> </Box>
</Box> </Box>
)}
<OnboardingFooter horizontal flow={2} t={t} nextStep={nextStep} prevStep={prevStep} /> <OnboardingFooter horizontal flow={2} t={t} nextStep={nextStep} prevStep={prevStep} />
</FixedTopContainer> </FixedTopContainer>
) )

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

@ -30,17 +30,17 @@ class WriteSeedBlue extends PureComponent<Props, *> {
{ {
key: 'step1', key: 'step1',
icon: <IconOptionRow>{'1.'}</IconOptionRow>, icon: <IconOptionRow>{'1.'}</IconOptionRow>,
desc: t('onboarding:writeSeed.blue.step1'), desc: t('onboarding:writeSeed.initialize.blue.step1'),
}, },
{ {
key: 'step2', key: 'step2',
icon: <IconOptionRow>{'2.'}</IconOptionRow>, icon: <IconOptionRow>{'2.'}</IconOptionRow>,
desc: t('onboarding:writeSeed.blue.step2'), desc: t('onboarding:writeSeed.initialize.blue.step2'),
}, },
{ {
key: 'step3', key: 'step3',
icon: <IconOptionRow>{'3.'}</IconOptionRow>, icon: <IconOptionRow>{'3.'}</IconOptionRow>,
desc: t('onboarding:writeSeed.blue.step3'), desc: t('onboarding:writeSeed.initialize.blue.step3'),
}, },
] ]
const disclaimerNotes = [ const disclaimerNotes = [
@ -69,8 +69,8 @@ class WriteSeedBlue extends PureComponent<Props, *> {
return ( return (
<Fragment> <Fragment>
<Box mb={3}> <Box mb={3}>
<Title>{t('onboarding:writeSeed.title')}</Title> <Title>{t('onboarding:writeSeed.initialize.title')}</Title>
<Description>{t('onboarding:writeSeed.desc')}</Description> <Description>{t('onboarding:writeSeed.initialize.desc')}</Description>
</Box> </Box>
<Box align="center"> <Box align="center">
<Inner style={{ width: 760 }}> <Inner style={{ width: 760 }}>

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

@ -30,17 +30,17 @@ class WriteSeedNano extends PureComponent<Props, *> {
{ {
key: 'step1', key: 'step1',
icon: <IconOptionRow>{'1.'}</IconOptionRow>, icon: <IconOptionRow>{'1.'}</IconOptionRow>,
desc: t('onboarding:writeSeed.nano.step1'), desc: t('onboarding:writeSeed.initialize.nano.step1'),
}, },
{ {
key: 'step2', key: 'step2',
icon: <IconOptionRow>{'2.'}</IconOptionRow>, icon: <IconOptionRow>{'2.'}</IconOptionRow>,
desc: t('onboarding:writeSeed.nano.step2'), desc: t('onboarding:writeSeed.initialize.nano.step2'),
}, },
{ {
key: 'step3', key: 'step3',
icon: <IconOptionRow>{'3.'}</IconOptionRow>, icon: <IconOptionRow>{'3.'}</IconOptionRow>,
desc: t('onboarding:writeSeed.nano.step3'), desc: t('onboarding:writeSeed.initialize.nano.step3'),
}, },
] ]
const disclaimerNotes = [ const disclaimerNotes = [
@ -69,8 +69,8 @@ class WriteSeedNano extends PureComponent<Props, *> {
return ( return (
<Fragment> <Fragment>
<Box mb={3}> <Box mb={3}>
<Title>{t('onboarding:writeSeed.title')}</Title> <Title>{t('onboarding:writeSeed.initialize.title')}</Title>
<Description>{t('onboarding:writeSeed.desc')}</Description> <Description>{t('onboarding:writeSeed.initialize.desc')}</Description>
</Box> </Box>
<Box align="center" mt={3}> <Box align="center" mt={3}>
<Inner style={{ width: 700 }}> <Inner style={{ width: 700 }}>

39
src/components/Onboarding/steps/WriteSeed/WriteSeedRestore.js

@ -7,6 +7,7 @@ import Box from 'components/base/Box'
import type { T } from 'types/common' import type { T } from 'types/common'
import IconWriteSeed from 'icons/illustrations/WriteSeed' import IconWriteSeed from 'icons/illustrations/WriteSeed'
import type { OnboardingState } from 'reducers/onboarding'
import IconChevronRight from 'icons/ChevronRight' import IconChevronRight from 'icons/ChevronRight'
@ -21,32 +22,50 @@ import {
type Props = { type Props = {
t: T, t: T,
onboarding: OnboardingState,
} }
class WriteSeedRestore extends PureComponent<Props, *> { class WriteSeedRestore extends PureComponent<Props, *> {
render() { render() {
const { t } = this.props const { t, onboarding } = this.props
const steps = [ const stepsNano = [
{ {
key: 'step1', key: 'step1',
icon: <IconOptionRow>{'1.'}</IconOptionRow>, icon: <IconOptionRow>{'1.'}</IconOptionRow>,
desc: t('onboarding:writeSeed.restore.step1'), desc: t('onboarding:writeSeed.restore.nano.step1'),
}, },
{ {
key: 'step2', key: 'step2',
icon: <IconOptionRow>{'2.'}</IconOptionRow>, icon: <IconOptionRow>{'2.'}</IconOptionRow>,
desc: t('onboarding:writeSeed.restore.step2'), desc: t('onboarding:writeSeed.restore.nano.step2'),
}, },
{ {
key: 'step3', key: 'step3',
icon: <IconOptionRow>{'3.'}</IconOptionRow>, icon: <IconOptionRow>{'3.'}</IconOptionRow>,
desc: t('onboarding:writeSeed.restore.step3'), desc: t('onboarding:writeSeed.restore.nano.step3'),
}, },
{ {
key: 'step4', key: 'step4',
icon: <IconOptionRow>{'4.'}</IconOptionRow>, icon: <IconOptionRow>{'4.'}</IconOptionRow>,
desc: t('onboarding:writeSeed.restore.step4'), desc: t('onboarding:writeSeed.restore.nano.step4'),
},
]
const stepsBlue = [
{
key: 'step1',
icon: <IconOptionRow>{'1.'}</IconOptionRow>,
desc: t('onboarding:writeSeed.restore.blue.step1'),
},
{
key: 'step2',
icon: <IconOptionRow>{'2.'}</IconOptionRow>,
desc: t('onboarding:writeSeed.restore.blue.step2'),
},
{
key: 'step3',
icon: <IconOptionRow>{'3.'}</IconOptionRow>,
desc: t('onboarding:writeSeed.restore.blue.step3'),
}, },
] ]
const disclaimerNotes = [ const disclaimerNotes = [
@ -83,9 +102,15 @@ class WriteSeedRestore extends PureComponent<Props, *> {
<Box style={{ width: 260, justifyContent: 'center', alignItems: 'center' }}> <Box style={{ width: 260, justifyContent: 'center', alignItems: 'center' }}>
<IconWriteSeed /> <IconWriteSeed />
</Box> </Box>
{onboarding.isLedgerNano ? (
<Box shrink flow={2} m={0}>
{stepsNano.map(step => <OptionRow key={step.key} step={step} />)}
</Box>
) : (
<Box shrink flow={2} m={0}> <Box shrink flow={2} m={0}>
{steps.map(step => <OptionRow key={step.key} step={step} />)} {stepsBlue.map(step => <OptionRow key={step.key} step={step} />)}
</Box> </Box>
)}
</Inner> </Inner>
<DisclaimerBox mt={6} disclaimerNotes={disclaimerNotes} /> <DisclaimerBox mt={6} disclaimerNotes={disclaimerNotes} />
</Box> </Box>

2
src/components/Onboarding/steps/WriteSeed/index.js

@ -19,7 +19,7 @@ export default (props: StepProps) => {
<FixedTopContainer> <FixedTopContainer>
<Box grow alignItems="center"> <Box grow alignItems="center">
{onboarding.flowType === 'restoreDevice' ? ( {onboarding.flowType === 'restoreDevice' ? (
<WriteSeedRestore /> <WriteSeedRestore onboarding={onboarding} />
) : onboarding.isLedgerNano ? ( ) : onboarding.isLedgerNano ? (
<WriteSeedNano /> <WriteSeedNano />
) : ( ) : (

29
src/components/TopBar/ActivityIndicator.js

@ -14,6 +14,8 @@ import { BridgeSyncConsumer } from 'bridge/BridgeSyncContext'
import CounterValues from 'helpers/countervalues' import CounterValues from 'helpers/countervalues'
import { Rotating } from 'components/base/Spinner' import { Rotating } from 'components/base/Spinner'
import Tooltip from 'components/base/Tooltip'
import TranslatedError from 'components/TranslatedError'
import Box from 'components/base/Box' import Box from 'components/base/Box'
import IconRefresh from 'icons/Refresh' import IconRefresh from 'icons/Refresh'
import IconExclamationCircle from 'icons/ExclamationCircle' import IconExclamationCircle from 'icons/ExclamationCircle'
@ -28,6 +30,7 @@ type Props = {
// FIXME: eslint should see that it is used in static method // FIXME: eslint should see that it is used in static method
isGlobalSyncStatePending: boolean, // eslint-disable-line react/no-unused-prop-types isGlobalSyncStatePending: boolean, // eslint-disable-line react/no-unused-prop-types
error: ?Error,
isPending: boolean, isPending: boolean,
isError: boolean, isError: boolean,
t: T, t: T,
@ -75,12 +78,12 @@ class ActivityIndicatorInner extends PureComponent<Props, State> {
} }
render() { render() {
const { isPending, isError, t } = this.props const { isPending, isError, error, t } = this.props
const { hasClicked, isFirstSync } = this.state const { hasClicked, isFirstSync } = this.state
const isDisabled = isError || (isPending && (isFirstSync || hasClicked)) const isDisabled = isError || (isPending && (isFirstSync || hasClicked))
const isRotating = isPending && (hasClicked || isFirstSync) const isRotating = isPending && (hasClicked || isFirstSync)
return ( const content = (
<ItemContainer disabled={isDisabled} onClick={isDisabled ? undefined : this.handleRefresh}> <ItemContainer disabled={isDisabled} onClick={isDisabled ? undefined : this.handleRefresh}>
<Rotating <Rotating
size={16} size={16}
@ -123,6 +126,23 @@ class ActivityIndicatorInner extends PureComponent<Props, State> {
</Box> </Box>
</ItemContainer> </ItemContainer>
) )
if (error) {
return (
<Tooltip
tooltipBg="alertRed"
render={() => (
<Box fontSize={4} p={2} style={{ maxWidth: 250 }}>
<TranslatedError error={error} />
</Box>
)}
>
{content}
</Tooltip>
)
}
return content
} }
} }
@ -132,13 +152,14 @@ const ActivityIndicator = ({ globalSyncState, t }: { globalSyncState: AsyncState
<CounterValues.PollingConsumer> <CounterValues.PollingConsumer>
{cvPolling => { {cvPolling => {
const isPending = cvPolling.pending || globalSyncState.pending const isPending = cvPolling.pending || globalSyncState.pending
const isError = cvPolling.error || globalSyncState.error const isError = !isPending && (cvPolling.error || globalSyncState.error)
return ( return (
<ActivityIndicatorInner <ActivityIndicatorInner
t={t} t={t}
isPending={isPending} isPending={isPending}
isGlobalSyncStatePending={globalSyncState.pending} isGlobalSyncStatePending={globalSyncState.pending}
isError={!!isError && !isPending} isError={!!isError}
error={isError ? globalSyncState.error : null}
cvPoll={cvPolling.poll} cvPoll={cvPolling.poll}
setSyncBehavior={setSyncBehavior} setSyncBehavior={setSyncBehavior}
/> />

7
src/components/Workflow/EnsureDashboard.js

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

23
src/components/Workflow/EnsureDevice.js

@ -1,5 +1,7 @@
// @flow // @flow
import { PureComponent } from 'react' /* eslint-disable react/no-multi-comp */
import { Component, PureComponent } from 'react'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import type { Node } from 'react' import type { Node } from 'react'
@ -12,9 +14,24 @@ type Props = {
children: (device: Device) => Node, children: (device: Device) => Node,
} }
type State = {} let prevents = 0
export class PreventDeviceChangeRecheck extends PureComponent<{}> {
componentDidMount() {
prevents++
}
componentWillUnmount() {
prevents--
}
render() {
return null
}
}
class EnsureDevice extends PureComponent<Props, State> { class EnsureDevice extends Component<Props> {
shouldComponentUpdate(nextProps) {
if (prevents > 0) return false
return nextProps.device !== this.props.device
}
render() { render() {
const { device, children } = this.props const { device, children } = this.props
return children(device) return children(device)

9
src/components/Workflow/WorkflowDefault.js

@ -111,8 +111,7 @@ const WorkflowDefault = ({ device, deviceInfo, errors, isGenuine }: Props) => (
</StepIcon> </StepIcon>
<Box grow shrink> <Box grow shrink>
<Trans i18nKey="app:deviceConnect.step1.connect" parent="div"> <Trans i18nKey="app:deviceConnect.step1.connect" parent="div">
Connect your <strong>Ledger device</strong> to your computer and enter your{' '} Connect and unlock your <strong>Ledger device</strong> <strong />
<strong>PIN code</strong> on your device
</Trans> </Trans>
</Box> </Box>
<StepCheck checked={!!device} /> <StepCheck checked={!!device} />
@ -128,7 +127,7 @@ const WorkflowDefault = ({ device, deviceInfo, errors, isGenuine }: Props) => (
</StepIcon> </StepIcon>
<Box grow shrink> <Box grow shrink>
<Trans i18nKey="deviceConnect:dashboard.open" parent="div"> <Trans i18nKey="deviceConnect:dashboard.open" parent="div">
{'Go to the '} {'Navigate to the '}
<strong>{'dashboard'}</strong> <strong>{'dashboard'}</strong>
{' on your device'} {' on your device'}
</Trans> </Trans>
@ -155,8 +154,8 @@ const WorkflowDefault = ({ device, deviceInfo, errors, isGenuine }: Props) => (
</StepIcon> </StepIcon>
<Box grow shrink> <Box grow shrink>
<Trans i18nKey="deviceConnect:stepGenuine.open" parent="div"> <Trans i18nKey="deviceConnect:stepGenuine.open" parent="div">
{'Confirm '} {'Allow the '}
<strong>{'authentication'}</strong> <strong>{'Ledger Manager'}</strong>
{' on your device'} {' on your device'}
</Trans> </Trans>
</Box> </Box>

11
src/components/Workflow/WorkflowWithIcon.js

@ -129,11 +129,8 @@ const WorkflowWithIcon = ({ device, deviceInfo, errors, isGenuine, t }: Props) =
</StepIcon> </StepIcon>
<Box grow shrink> <Box grow shrink>
<Trans i18nKey="deviceConnect:step1.connect" parent="div"> <Trans i18nKey="deviceConnect:step1.connect" parent="div">
{'Connect your '} {'Connect and unlock your '}
<strong>Ledger device</strong> <strong>Ledger device</strong>
{' to your computer and enter your '}
<strong>PIN code</strong>
{' on your device'}
</Trans> </Trans>
</Box> </Box>
<StepCheck checked={!!device} /> <StepCheck checked={!!device} />
@ -150,7 +147,7 @@ const WorkflowWithIcon = ({ device, deviceInfo, errors, isGenuine, t }: Props) =
</StepIcon> </StepIcon>
<Box grow shrink> <Box grow shrink>
<Trans i18nKey="deviceConnect:dashboard.open" parent="div"> <Trans i18nKey="deviceConnect:dashboard.open" parent="div">
{'Go to the '} {'Navigate to the '}
<strong>{'dashboard'}</strong> <strong>{'dashboard'}</strong>
{' on your device'} {' on your device'}
</Trans> </Trans>
@ -179,8 +176,8 @@ const WorkflowWithIcon = ({ device, deviceInfo, errors, isGenuine, t }: Props) =
</StepIcon> </StepIcon>
<Box grow shrink> <Box grow shrink>
<Trans i18nKey="deviceConnect:stepGenuine.open" parent="div"> <Trans i18nKey="deviceConnect:stepGenuine.open" parent="div">
{'Confirm '} {'Allow the '}
<strong>{'authentication'}</strong> <strong>{'Ledger Manager'}</strong>
{' on your device'} {' on your device'}
</Trans> </Trans>
</Box> </Box>

4
src/components/base/Ellipsis.js

@ -7,10 +7,10 @@ import Text from 'components/base/Text'
const outerStyle = { width: 0 } const outerStyle = { width: 0 }
const innerStyle = { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } const innerStyle = { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }
export default ({ children, ...p }: { children: any }) => ( export default ({ children, canSelect, ...p }: { children: any, canSelect?: boolean }) => (
<Box grow horizontal> <Box grow horizontal>
<Box grow {...p} style={outerStyle}> <Box grow {...p} style={outerStyle}>
<Text style={innerStyle}>{children}</Text> <Text style={{ ...innerStyle, userSelect: canSelect ? 'text' : 'none' }}>{children}</Text>
</Box> </Box>
</Box> </Box>
) )

5
src/components/base/GrowScroll/index.js

@ -2,6 +2,7 @@
import React, { PureComponent } from 'react' import React, { PureComponent } from 'react'
import Box from 'components/base/Box' import Box from 'components/base/Box'
import measureScrollbar from 'measure-scrollbar/commonjs'
type Props = { type Props = {
children: any, children: any,
@ -11,6 +12,8 @@ type Props = {
export const GrowScrollContext = React.createContext() export const GrowScrollContext = React.createContext()
const scrollbarWidth = measureScrollbar()
class GrowScroll extends PureComponent<Props> { class GrowScroll extends PureComponent<Props> {
static defaultProps = { static defaultProps = {
full: false, full: false,
@ -47,7 +50,7 @@ class GrowScroll extends PureComponent<Props> {
const scrollContainerStyles = { const scrollContainerStyles = {
overflowY: 'scroll', overflowY: 'scroll',
marginRight: `-80px`, marginRight: `-${80 + scrollbarWidth}px`,
paddingRight: `80px`, paddingRight: `80px`,
...(maxHeight ...(maxHeight
? { ? {

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

@ -10,6 +10,7 @@ import { connect } from 'react-redux'
import Mortal from 'react-mortal' import Mortal from 'react-mortal'
import styled from 'styled-components' import styled from 'styled-components'
import noop from 'lodash/noop' import noop from 'lodash/noop'
import { EXPERIMENTAL_CENTER_MODAL } from 'config/constants'
import { rgba } from 'styles/helpers' import { rgba } from 'styles/helpers'
import { radii } from 'styles/theme' import { radii } from 'styles/theme'
@ -133,6 +134,18 @@ function stopPropagation(e) {
e.stopPropagation() e.stopPropagation()
} }
const wrap = EXPERIMENTAL_CENTER_MODAL
? children => (
<Box alignItems="center" justifyContent="center" grow>
{children}
</Box>
)
: children => (
<GrowScroll alignItems="center" full pt={8}>
{children}
</GrowScroll>
)
export class Modal extends Component<Props> { export class Modal extends Component<Props> {
static defaultProps = { static defaultProps = {
isOpened: false, isOpened: false,
@ -198,7 +211,7 @@ export class Modal extends Component<Props> {
<Container isVisible={isVisible} onClick={preventBackdropClick ? undefined : onClose}> <Container isVisible={isVisible} onClick={preventBackdropClick ? undefined : onClose}>
<Backdrop op={m.opacity} /> <Backdrop op={m.opacity} />
<NonClickableHeadArea onClick={stopPropagation} /> <NonClickableHeadArea onClick={stopPropagation} />
<GrowScroll alignItems="center" full py={8}> {wrap(
<Wrapper <Wrapper
tabIndex={-1} tabIndex={-1}
op={m.opacity} op={m.opacity}
@ -208,8 +221,8 @@ export class Modal extends Component<Props> {
width={width} width={width}
> >
<Pure isAnimated={isAnimated} render={render} data={data} onClose={onClose} /> <Pure isAnimated={isAnimated} render={render} data={data} onClose={onClose} />
</Wrapper> </Wrapper>,
</GrowScroll> )}
</Container> </Container>
)} )}
</Mortal> </Mortal>

17
src/components/base/Tooltip/index.js

@ -20,15 +20,17 @@ export const TooltipContainer = ({
children, children,
innerRef, innerRef,
style, style,
tooltipBg,
}: { }: {
children: any, children: any,
innerRef?: Function, innerRef?: Function,
style?: Object, style?: Object,
tooltipBg?: string,
}) => ( }) => (
<div <div
ref={innerRef} ref={innerRef}
style={{ style={{
background: colors.dark, background: colors[tooltipBg || 'dark'],
borderRadius: 4, borderRadius: 4,
color: 'white', color: 'white',
fontFamily: 'Open Sans', fontFamily: 'Open Sans',
@ -51,6 +53,7 @@ type Props = {
offset?: Array<number>, offset?: Array<number>,
children: any, children: any,
render: Function, render: Function,
tooltipBg?: string,
} }
class Tooltip extends PureComponent<Props> { class Tooltip extends PureComponent<Props> {
@ -59,7 +62,7 @@ class Tooltip extends PureComponent<Props> {
} }
componentDidMount() { componentDidMount() {
const { offset } = this.props const { offset, tooltipBg } = this.props
if (this._node && this._template) { if (this._node && this._template) {
tippy(this._node, { tippy(this._node, {
@ -76,7 +79,9 @@ class Tooltip extends PureComponent<Props> {
if (this._node && this._node._tippy) { if (this._node && this._node._tippy) {
this._node._tippy.popper.querySelector('.tippy-roundarrow').innerHTML = ` this._node._tippy.popper.querySelector('.tippy-roundarrow').innerHTML = `
<svg viewBox="0 0 24 8"> <svg viewBox="0 0 24 8">
<path d="M5 8l5.5-5.6c.8-.8 2-.8 2.8 0L19 8" /> <path${
tooltipBg ? ` fill="${colors[tooltipBg]}"` : ''
} d="M5 8l5.5-5.6c.8-.8 2-.8 2.8 0L19 8" />
</svg>` </svg>`
} }
} }
@ -86,12 +91,14 @@ class Tooltip extends PureComponent<Props> {
_template = undefined _template = undefined
render() { render() {
const { children, render, ...props } = this.props const { children, render, tooltipBg, ...props } = this.props
return ( return (
<Container innerRef={n => (this._node = n)} {...props}> <Container innerRef={n => (this._node = n)} {...props}>
<Template> <Template>
<TooltipContainer innerRef={n => (this._template = n)}>{render()}</TooltipContainer> <TooltipContainer tooltipBg={tooltipBg} innerRef={n => (this._template = n)}>
{render()}
</TooltipContainer>
</Template> </Template>
{children} {children}
</Container> </Container>

2
src/components/layout/Default.js

@ -20,6 +20,7 @@ import SettingsPage from 'components/SettingsPage'
import LibcoreBusyIndicator from 'components/LibcoreBusyIndicator' import LibcoreBusyIndicator from 'components/LibcoreBusyIndicator'
import DeviceBusyIndicator from 'components/DeviceBusyIndicator' import DeviceBusyIndicator from 'components/DeviceBusyIndicator'
import TriggerAppReady from 'components/TriggerAppReady' import TriggerAppReady from 'components/TriggerAppReady'
import ExportLogsBtn from 'components/ExportLogsBtn'
import AppRegionDrag from 'components/AppRegionDrag' import AppRegionDrag from 'components/AppRegionDrag'
import IsUnlocked from 'components/IsUnlocked' import IsUnlocked from 'components/IsUnlocked'
@ -75,6 +76,7 @@ class Default extends Component<Props> {
<Fragment> <Fragment>
<TriggerAppReady /> <TriggerAppReady />
{process.platform === 'darwin' && <AppRegionDrag />} {process.platform === 'darwin' && <AppRegionDrag />}
<ExportLogsBtn hookToShortcut />
<IsUnlocked> <IsUnlocked>
{Object.entries(modals).map(([name, ModalComponent]: [string, any]) => ( {Object.entries(modals).map(([name, ModalComponent]: [string, any]) => (

4
src/components/modals/AddAccounts/steps/02-step-connect-device.js

@ -20,11 +20,11 @@ function StepConnectDevice({ t, currency, currentDevice, setState }: StepProps)
<CurrencyCircleIcon mb={3} size={40} currency={currency} /> <CurrencyCircleIcon mb={3} size={40} currency={currency} />
<Box ff="Open Sans" fontSize={4} color="dark" textAlign="center" style={{ width: 370 }}> <Box ff="Open Sans" fontSize={4} color="dark" textAlign="center" style={{ width: 370 }}>
<Trans i18nKey="app:addAccounts.connectDevice.desc" parent="div"> <Trans i18nKey="app:addAccounts.connectDevice.desc" parent="div">
{`You're about to import your `} {`Follow the steps below to add `}
<strong style={{ fontWeight: 'bold' }}>{`${currency.name} (${ <strong style={{ fontWeight: 'bold' }}>{`${currency.name} (${
currency.ticker currency.ticker
})`}</strong> })`}</strong>
{` account(s) from your Ledger device. Please follow the steps below:`} {` accounts from your Ledger device.`}
</Trans> </Trans>
</Box> </Box>
</Box> </Box>

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

@ -17,7 +17,7 @@ import type { StepProps } from '../index'
class StepImport extends PureComponent<StepProps> { class StepImport extends PureComponent<StepProps> {
componentDidMount() { componentDidMount() {
this.startScanAccountsDevice() this.props.setState({ scanStatus: 'scanning' })
} }
componentDidUpdate(prevProps: StepProps) { componentDidUpdate(prevProps: StepProps) {
@ -72,9 +72,7 @@ class StepImport extends PureComponent<StepProps> {
// TODO: use the real device // TODO: use the real device
const devicePath = currentDevice.path const devicePath = currentDevice.path
setState({ scanStatus: 'scanning' }) this.scanSubscription = bridge.scanAccountsOnDevice(currency, devicePath).subscribe({
this.scanSubscription = bridge.scanAccountsOnDevice(currency, devicePath, {
next: account => { next: account => {
const { scannedAccounts, checkedAccountsIds, existingAccounts } = this.props const { scannedAccounts, checkedAccountsIds, existingAccounts } = this.props
const hasAlreadyBeenScanned = !!scannedAccounts.find(a => account.id === a.id) const hasAlreadyBeenScanned = !!scannedAccounts.find(a => account.id === a.id)
@ -192,7 +190,7 @@ class StepImport extends PureComponent<StepProps> {
}) })
const importableAccountsEmpty = t('app:addAccounts.noAccountToImport', { currencyName }) const importableAccountsEmpty = t('app:addAccounts.noAccountToImport', { currencyName })
const hasAlreadyEmptyAccount = scannedAccounts.some(a => a.operations.length === 0) const alreadyEmptyAccount = scannedAccounts.find(a => a.operations.length === 0)
return ( return (
<Fragment> <Fragment>
@ -211,8 +209,10 @@ class StepImport extends PureComponent<StepProps> {
<AccountsList <AccountsList
title={t('app:addAccounts.createNewAccount.title')} title={t('app:addAccounts.createNewAccount.title')}
emptyText={ emptyText={
hasAlreadyEmptyAccount alreadyEmptyAccount
? t('app:addAccounts.createNewAccount.noOperationOnLastAccount') ? t('app:addAccounts.createNewAccount.noOperationOnLastAccount', {
accountName: alreadyEmptyAccount.name,
})
: t('app:addAccounts.createNewAccount.noAccountToCreate', { currencyName }) : t('app:addAccounts.createNewAccount.noAccountToCreate', { currencyName })
} }
accounts={creatableAccounts} accounts={creatableAccounts}

96
src/components/modals/OperationDetails.js

@ -1,7 +1,6 @@
// @flow // @flow
import React, { Fragment } from 'react' import React, { Fragment, Component } from 'react'
import uniq from 'lodash/uniq'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { shell } from 'electron' import { shell } from 'electron'
import { translate } from 'react-i18next' import { translate } from 'react-i18next'
@ -17,16 +16,19 @@ import { MODAL_OPERATION_DETAILS } from 'config/constants'
import { getMarketColor } from 'styles/helpers' import { getMarketColor } from 'styles/helpers'
import Box from 'components/base/Box' import Box from 'components/base/Box'
import Spoiler from 'components/base/Spoiler' import GradientBox from 'components/GradientBox'
import GrowScroll from 'components/base/GrowScroll'
import Button from 'components/base/Button' import Button from 'components/base/Button'
import Bar from 'components/base/Bar' import Bar from 'components/base/Bar'
import FormattedVal from 'components/base/FormattedVal' import FormattedVal from 'components/base/FormattedVal'
import Modal, { ModalBody, ModalTitle, ModalFooter, ModalContent } from 'components/base/Modal' import Modal, { ModalBody, ModalTitle, ModalFooter, ModalContent } from 'components/base/Modal'
import Text from 'components/base/Text'
import { createStructuredSelector, createSelector } from 'reselect' import { createStructuredSelector, createSelector } from 'reselect'
import { accountSelector } from 'reducers/accounts' import { accountSelector } from 'reducers/accounts'
import { currencySettingsForAccountSelector, marketIndicatorSelector } from 'reducers/settings' import { currencySettingsForAccountSelector, marketIndicatorSelector } from 'reducers/settings'
import IconChevronRight from 'icons/ChevronRight'
import CounterValue from 'components/CounterValue' import CounterValue from 'components/CounterValue'
import ConfirmationCheck from 'components/OperationsList/ConfirmationCheck' import ConfirmationCheck from 'components/OperationsList/ConfirmationCheck'
import Ellipsis from '../base/Ellipsis' import Ellipsis from '../base/Ellipsis'
@ -102,12 +104,13 @@ const OperationDetails = connect(mapStateToProps)((props: Props) => {
const isConfirmed = confirmations >= currencySettings.confirmationsNb const isConfirmed = confirmations >= currencySettings.confirmationsNb
const url = getAccountOperationExplorer(account, operation) const url = getAccountOperationExplorer(account, operation)
const uniqSenders = uniq(senders)
return ( return (
<ModalBody onClose={onClose}> <ModalBody onClose={onClose}>
<ModalTitle>{t('app:operationDetails.title')}</ModalTitle> <ModalTitle>{t('app:operationDetails.title')}</ModalTitle>
<ModalContent flow={3}> <ModalContent style={{ height: 500 }} mx={-5} pb={0}>
<GrowScroll px={5} pb={8}>
<Box flow={3}>
<Box alignItems="center" mt={1}> <Box alignItems="center" mt={1}>
<ConfirmationCheck <ConfirmationCheck
marketColor={marketColor} marketColor={marketColor}
@ -121,7 +124,7 @@ const OperationDetails = connect(mapStateToProps)((props: Props) => {
/> />
<Box my={4} alignItems="center"> <Box my={4} alignItems="center">
<Box> <Box>
<FormattedVal unit={unit} alwaysShowSign showCode val={amount} fontSize={6} /> <FormattedVal unit={unit} alwaysShowSign showCode val={amount} fontSize={7} />
</Box> </Box>
<Box mt={1}> <Box mt={1}>
<CounterValue <CounterValue
@ -170,23 +173,24 @@ const OperationDetails = connect(mapStateToProps)((props: Props) => {
</Box> </Box>
<B /> <B />
<Box> <Box>
<OpDetailsTitle>{t('app:operationDetails.from')}</OpDetailsTitle> <OpDetailsTitle>{t('app:operationDetails.identifier')}</OpDetailsTitle>
<OpDetailsData>{uniqSenders.map(v => <CanSelect key={v}>{v}</CanSelect>)}</OpDetailsData> <OpDetailsData>
<Ellipsis canSelect>{hash}</Ellipsis>
</OpDetailsData>
</Box> </Box>
<B /> <B />
<Box> <Box>
<OpDetailsTitle>{t('app:operationDetails.to')}</OpDetailsTitle> <OpDetailsTitle>{t('app:operationDetails.from')}</OpDetailsTitle>
<RenderRecipients recipients={recipients} t={t} /> <Recipients recipients={senders} t={t} />
</Box> </Box>
<B /> <B />
<Box> <Box>
<OpDetailsTitle>{t('app:operationDetails.identifier')}</OpDetailsTitle> <OpDetailsTitle>{t('app:operationDetails.to')}</OpDetailsTitle>
<OpDetailsData> <Recipients recipients={recipients} t={t} />
<CanSelect>
<Ellipsis>{hash}</Ellipsis>
</CanSelect>
</OpDetailsData>
</Box> </Box>
</Box>
</GrowScroll>
<GradientBox />
</ModalContent> </ModalContent>
<ModalFooter horizontal justify="flex-end" flow={2}> <ModalFooter horizontal justify="flex-end" flow={2}>
@ -194,7 +198,7 @@ const OperationDetails = connect(mapStateToProps)((props: Props) => {
{t('app:common.cancel')} {t('app:common.cancel')}
</Button> </Button>
{url ? ( {url ? (
<Button ml="auto" primary padded onClick={() => shell.openExternal(url)}> <Button primary padded onClick={() => shell.openExternal(url)}>
{t('app:operationDetails.viewOperation')} {t('app:operationDetails.viewOperation')}
</Button> </Button>
) : null} ) : null}
@ -223,31 +227,63 @@ const OperationDetailsWrapper = ({ t }: { t: T }) => (
export default translate()(OperationDetailsWrapper) export default translate()(OperationDetailsWrapper)
export function RenderRecipients({ recipients, t }: { recipients: *, t: T }) { const More = styled(Text).attrs({
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: ${p => (!p.textTransform ? 'auto' : 'uppercase')};
cursor: pointer;
outline: none;
`
export class Recipients extends Component<{ recipients: Array<*>, t: T }, *> {
state = {
showMore: false,
}
onClick = () => {
this.setState(({ showMore }) => ({ showMore: !showMore }))
}
render() {
const { recipients, t } = this.props
const { showMore } = this.state
// Hardcoded for now // Hardcoded for now
const numToShow = 2 const numToShow = 2
const shouldShowMore = recipients.length > 3
return ( return (
<Box> <Box>
<OpDetailsData> <OpDetailsData>
{recipients {(shouldShowMore ? recipients.slice(0, numToShow) : recipients).map(recipient => (
.slice(0, numToShow) <CanSelect key={recipient}>{recipient}</CanSelect>
.map(recipient => <CanSelect key={recipient}>{recipient}</CanSelect>)} ))}
</OpDetailsData> </OpDetailsData>
{recipients.length > numToShow && ( {shouldShowMore &&
<Spoiler !showMore && (
title={t('app:operationDetails.showMore', { recipients: recipients.length - numToShow })} <Box onClick={this.onClick} py={1}>
color="wallet" <More fontSize={4} color="wallet" ff="Open Sans|SemiBold" mt={1}>
ff="Open Sans|SemiBold" <IconChevronRight size={12} style={{ marginRight: 5 }} />
fontSize={4} {t('app:operationDetails.showMore', { recipients: recipients.length - numToShow })}
mt={1} </More>
> </Box>
)}
{showMore && (
<OpDetailsData> <OpDetailsData>
{recipients {recipients
.slice(numToShow) .slice(numToShow)
.map(recipient => <CanSelect key={recipient}>{recipient}</CanSelect>)} .map(recipient => <CanSelect key={recipient}>{recipient}</CanSelect>)}
</OpDetailsData> </OpDetailsData>
</Spoiler> )}
{shouldShowMore &&
showMore && (
<Box onClick={this.onClick} py={1}>
<More fontSize={4} color="wallet" ff="Open Sans|SemiBold" mt={1}>
<IconChevronRight size={12} style={{ marginRight: 5 }} />
{t('app:operationDetails.showLess')}
</More>
</Box>
)} )}
</Box> </Box>
) )
}
} }

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

@ -61,6 +61,8 @@ const INITIAL_STATE = {
stepIndex: 0, stepIndex: 0,
stepsDisabled: [], stepsDisabled: [],
stepsErrors: [], stepsErrors: [],
// FIXME the two above can be derivated from other info (if we keep error etc)
// we can get rid of it after a big refactoring (see how done in Send)
} }
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
@ -232,12 +234,12 @@ class ReceiveModal extends PureComponent<Props, State> {
}) })
} }
this.setState({ addressVerified: true, stepIndex: 3 }) this.handleCheckAddress(true)
} else { } else {
this.setState({ addressVerified: false }) this.handleCheckAddress(false)
} }
} catch (err) { } catch (err) {
this.setState({ addressVerified: false }) this.handleCheckAddress(false)
} }
} }

22
src/components/modals/ReleaseNotes.js

@ -3,7 +3,7 @@ import React, { PureComponent } from 'react'
import { translate } from 'react-i18next' import { translate } from 'react-i18next'
import ReactMarkdown from 'react-markdown' import ReactMarkdown from 'react-markdown'
import styled from 'styled-components' import styled from 'styled-components'
import axios from 'axios' import network from 'api/network'
import { MODAL_RELEASES_NOTES } from 'config/constants' import { MODAL_RELEASES_NOTES } from 'config/constants'
import Modal, { ModalBody, ModalTitle, ModalContent, ModalFooter } from 'components/base/Modal' import Modal, { ModalBody, ModalTitle, ModalContent, ModalFooter } from 'components/base/Modal'
@ -13,6 +13,7 @@ import Box from 'components/base/Box'
import GrowScroll from 'components/base/GrowScroll' import GrowScroll from 'components/base/GrowScroll'
import Text from 'components/base/Text' import Text from 'components/base/Text'
import Spinner from 'components/base/Spinner' import Spinner from 'components/base/Spinner'
import GradientBox from 'components/GradientBox'
import type { T } from 'types/common' import type { T } from 'types/common'
@ -159,8 +160,10 @@ class ReleaseNotes extends PureComponent<Props, State> {
if (!this.loading) { if (!this.loading) {
this.loading = true this.loading = true
axios network({
.get(`https://api.github.com/repos/LedgerHQ/ledger-live-desktop/releases/tags/v${version}`) method: 'GET',
url: `https://api.github.com/repos/LedgerHQ/ledger-live-desktop/releases/tags/v${version}`,
})
.then(response => { .then(response => {
const { body } = response.data const { body } = response.data
@ -218,7 +221,7 @@ class ReleaseNotes extends PureComponent<Props, State> {
return ( return (
<ModalBody onClose={onClose}> <ModalBody onClose={onClose}>
<ModalTitle>{t('app:releaseNotes.title')}</ModalTitle> <ModalTitle>{t('app:releaseNotes.title')}</ModalTitle>
<ModalContent style={{ height: 400 }} mx={-5} pb={0}> <ModalContent style={{ height: 500 }} mx={-5} pb={0}>
<GrowScroll px={5} pb={8}> <GrowScroll px={5} pb={8}>
{content} {content}
</GrowScroll> </GrowScroll>
@ -237,15 +240,4 @@ class ReleaseNotes extends PureComponent<Props, State> {
} }
} }
const GradientBox = styled.div`
width: 100%;
height: 60px;
position: absolute;
bottom: 68px;
left: 0;
right: 0;
background: linear-gradient(rgba(255, 255, 255, 0), #ffffff 70%);
z-index: 2;
`
export default translate()(ReleaseNotes) export default translate()(ReleaseNotes)

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

@ -57,6 +57,7 @@ type Step = {
canNext: (State<*>) => boolean, canNext: (State<*>) => boolean,
canPrev: (State<*>) => boolean, canPrev: (State<*>) => boolean,
canClose: (State<*>) => boolean, canClose: (State<*>) => boolean,
hasError: (State<*>) => boolean,
prevStep?: number, prevStep?: number,
} }
@ -91,6 +92,7 @@ class SendModal extends Component<Props, State<*>> {
bridge && account && transaction bridge && account && transaction
? bridge.isValidTransaction(account, transaction) ? bridge.isValidTransaction(account, transaction)
: false, : false,
hasError: () => false,
}, },
{ {
label: t('app:send.steps.connectDevice.title'), label: t('app:send.steps.connectDevice.title'),
@ -99,6 +101,7 @@ class SendModal extends Component<Props, State<*>> {
deviceSelected !== null && appStatus === 'success', deviceSelected !== null && appStatus === 'success',
prevStep: 0, prevStep: 0,
canPrev: () => true, canPrev: () => true,
hasError: () => false,
}, },
{ {
label: t('app:send.steps.verification.title'), label: t('app:send.steps.verification.title'),
@ -106,6 +109,7 @@ class SendModal extends Component<Props, State<*>> {
canNext: () => true, canNext: () => true,
canPrev: ({ error }) => !!error, canPrev: ({ error }) => !!error,
prevStep: 0, prevStep: 0,
hasError: ({ error }) => (error && error.name === 'UserRefusedOnDevice') || false,
}, },
{ {
label: t('app:send.steps.confirmation.title'), label: t('app:send.steps.confirmation.title'),
@ -113,6 +117,7 @@ class SendModal extends Component<Props, State<*>> {
canClose: () => true, canClose: () => true,
canPrev: () => true, canPrev: () => true,
canNext: () => false, canNext: () => false,
hasError: ({ error }) => (error && error.name !== 'UserRefusedOnDevice') || false,
}, },
] ]
} }
@ -273,6 +278,13 @@ class SendModal extends Component<Props, State<*>> {
const canNext = step.canNext(this.state) const canNext = step.canNext(this.state)
const canPrev = step.canPrev(this.state) const canPrev = step.canPrev(this.state)
const stepsErrors = []
this.steps.forEach((s, i) => {
if (s.hasError(this.state)) {
stepsErrors.push(i)
}
})
return ( return (
<Modal <Modal
name={MODAL_SEND} name={MODAL_SEND}
@ -290,7 +302,13 @@ class SendModal extends Component<Props, State<*>> {
</ModalTitle> </ModalTitle>
<ModalContent> <ModalContent>
<Breadcrumb t={t} mb={6} currentStep={stepIndex} items={this.steps} /> <Breadcrumb
t={t}
mb={6}
currentStep={stepIndex}
stepsErrors={stepsErrors}
items={this.steps}
/>
<ChildSwitch index={stepIndex}> <ChildSwitch index={stepIndex}>
<StepAmount <StepAmount

10
src/config/constants.js

@ -19,21 +19,21 @@ export const GET_CALLS_RETRY = intFromEnv('GET_CALLS_RETRY', 2)
export const SYNC_MAX_CONCURRENT = intFromEnv('LEDGER_SYNC_MAX_CONCURRENT', 6) export const SYNC_MAX_CONCURRENT = intFromEnv('LEDGER_SYNC_MAX_CONCURRENT', 6)
export const SYNC_BOOT_DELAY = 2 * 1000 export const SYNC_BOOT_DELAY = 2 * 1000
export const SYNC_ALL_INTERVAL = 60 * 1000 export const SYNC_ALL_INTERVAL = 120 * 1000
export const GENUINE_TIMEOUT = intFromEnv('GENUINE_TIMEOUT', 60 * 1000) export const GENUINE_TIMEOUT = intFromEnv('GENUINE_TIMEOUT', 120 * 1000)
export const SYNC_TIMEOUT = intFromEnv('SYNC_TIMEOUT', 30 * 1000) export const SYNC_TIMEOUT = intFromEnv('SYNC_TIMEOUT', 30 * 1000)
export const CHECK_APP_INTERVAL_WHEN_INVALID = 600 export const CHECK_APP_INTERVAL_WHEN_INVALID = 600
export const CHECK_APP_INTERVAL_WHEN_VALID = 1200 export const CHECK_APP_INTERVAL_WHEN_VALID = 1200
export const CHECK_UPDATE_DELAY = 5e3 export const CHECK_UPDATE_DELAY = 5e3
export const DEVICE_DISCONNECT_DEBOUNCE = intFromEnv('LEDGER_DEVICE_DISCONNECT_DEBOUNCE', 500) export const DEVICE_DISCONNECT_DEBOUNCE = intFromEnv('LEDGER_DEVICE_DISCONNECT_DEBOUNCE', 1000)
// Endpoints... // Endpoints...
export const LEDGER_COUNTERVALUES_API = stringFromEnv( export const LEDGER_COUNTERVALUES_API = stringFromEnv(
'LEDGER_COUNTERVALUES_API', 'LEDGER_COUNTERVALUES_API',
'https://ledger-countervalue-poc.herokuapp.com', 'https://beta.manager.live.ledger.fr/countervalues',
) )
export const LEDGER_REST_API_BASE = stringFromEnv( export const LEDGER_REST_API_BASE = stringFromEnv(
'LEDGER_REST_API_BASE', 'LEDGER_REST_API_BASE',
@ -66,6 +66,8 @@ export const SKIP_ONBOARDING = boolFromEnv('SKIP_ONBOARDING')
export const SHOW_LEGACY_NEW_ACCOUNT = boolFromEnv('SHOW_LEGACY_NEW_ACCOUNT') export const SHOW_LEGACY_NEW_ACCOUNT = boolFromEnv('SHOW_LEGACY_NEW_ACCOUNT')
export const HIGHLIGHT_I18N = boolFromEnv('HIGHLIGHT_I18N') export const HIGHLIGHT_I18N = boolFromEnv('HIGHLIGHT_I18N')
export const DISABLE_ACTIVITY_INDICATORS = boolFromEnv('DISABLE_ACTIVITY_INDICATORS') export const DISABLE_ACTIVITY_INDICATORS = boolFromEnv('DISABLE_ACTIVITY_INDICATORS')
export const EXPERIMENTAL_CENTER_MODAL = boolFromEnv('EXPERIMENTAL_CENTER_MODAL')
export const EXPERIMENTAL_FIRMWARE_UPDATE = boolFromEnv('EXPERIMENTAL_FIRMWARE_UPDATE')
// Other constants // Other constants

15
src/helpers/apps/installApp.js

@ -7,6 +7,19 @@ import { createDeviceSocket } from 'helpers/socket'
import type { LedgerScriptParams } from 'helpers/common' import type { LedgerScriptParams } from 'helpers/common'
import createCustomErrorClass from '../createCustomErrorClass'
const CannotInstall = createCustomErrorClass('CannotInstall')
function remapError(promise) {
return promise.catch((e: Error) => {
if (e.message.endsWith('6982')) {
throw new CannotInstall()
}
throw e
})
}
/** /**
* Install an app on the device * Install an app on the device
*/ */
@ -21,5 +34,5 @@ export default async function installApp(
firmwareKey: app.firmware_key, firmwareKey: app.firmware_key,
} }
const url = `${BASE_SOCKET_URL_SECURE}/install?${qs.stringify(params)}` const url = `${BASE_SOCKET_URL_SECURE}/install?${qs.stringify(params)}`
return createDeviceSocket(transport, url).toPromise() return remapError(createDeviceSocket(transport, url).toPromise())
} }

4
src/helpers/apps/listApps.js

@ -1,5 +1,5 @@
// @flow // @flow
import axios from 'axios' import network from 'api/network'
import { APPLICATIONS_BY_DEVICE } from 'helpers/urls' import { APPLICATIONS_BY_DEVICE } from 'helpers/urls'
import getDeviceVersion from 'helpers/devices/getDeviceVersion' import getDeviceVersion from 'helpers/devices/getDeviceVersion'
@ -17,7 +17,7 @@ export default async (targetId: string | number, version: string) => {
} }
const { const {
data: { application_versions }, data: { application_versions },
} = await axios.post(APPLICATIONS_BY_DEVICE, params) } = await network({ method: 'POST', url: APPLICATIONS_BY_DEVICE, data: params })
return application_versions.length > 0 ? application_versions : [] return application_versions.length > 0 ? application_versions : []
} catch (err) { } catch (err) {
const error = Error(err.message) const error = Error(err.message)

14
src/helpers/apps/uninstallApp.js

@ -6,6 +6,18 @@ import { BASE_SOCKET_URL_SECURE } from 'config/constants'
import { createDeviceSocket } from 'helpers/socket' import { createDeviceSocket } from 'helpers/socket'
import type { LedgerScriptParams } from 'helpers/common' import type { LedgerScriptParams } from 'helpers/common'
import createCustomErrorClass from '../createCustomErrorClass'
const CannotUninstall = createCustomErrorClass('CannotUninstall')
function remapError(promise) {
return promise.catch((e: Error) => {
if (e.message.endsWith('6a83')) {
throw new CannotUninstall()
}
throw e
})
}
/** /**
* Install an app on the device * Install an app on the device
@ -22,5 +34,5 @@ export default async function uninstallApp(
firmwareKey: app.delete_key, firmwareKey: app.delete_key,
} }
const url = `${BASE_SOCKET_URL_SECURE}/install?${qs.stringify(params)}` const url = `${BASE_SOCKET_URL_SECURE}/install?${qs.stringify(params)}`
return createDeviceSocket(transport, url).toPromise() return remapError(createDeviceSocket(transport, url).toPromise())
} }

8
src/helpers/devices/getCurrentFirmware.js

@ -1,5 +1,5 @@
// @flow // @flow
import axios from 'axios' import network from 'api/network'
import { GET_CURRENT_FIRMWARE } from 'helpers/urls' import { GET_CURRENT_FIRMWARE } from 'helpers/urls'
@ -12,10 +12,14 @@ let error
export default async (input: Input): Promise<*> => { export default async (input: Input): Promise<*> => {
try { try {
const provider = 1 const provider = 1
const { data } = await axios.post(GET_CURRENT_FIRMWARE, { const { data } = await network({
method: 'POST',
url: GET_CURRENT_FIRMWARE,
data: {
device_version: input.deviceId, device_version: input.deviceId,
version_name: input.version, version_name: input.version,
provider, provider,
},
}) })
return data return data
} catch (err) { } catch (err) {

15
src/helpers/devices/getDeviceVersion.js

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

8
src/helpers/devices/getLatestFirmwareForDevice.js

@ -1,5 +1,5 @@
// @flow // @flow
import axios from 'axios' import network from 'api/network'
import { GET_LATEST_FIRMWARE } from 'helpers/urls' import { GET_LATEST_FIRMWARE } from 'helpers/urls'
import getCurrentFirmware from './getCurrentFirmware' import getCurrentFirmware from './getCurrentFirmware'
@ -21,10 +21,14 @@ export default async (input: Input) => {
const seFirmwareVersion = await getCurrentFirmware({ version, deviceId: deviceVersion.id }) const seFirmwareVersion = await getCurrentFirmware({ version, deviceId: deviceVersion.id })
// Fetch next possible firmware // Fetch next possible firmware
const { data } = await axios.post(GET_LATEST_FIRMWARE, { const { data } = await network({
method: 'POST',
url: GET_LATEST_FIRMWARE,
data: {
current_se_firmware_final_version: seFirmwareVersion.id, current_se_firmware_final_version: seFirmwareVersion.id,
device_version: deviceVersion.id, device_version: deviceVersion.id,
provider, provider,
},
}) })
if (data.result === 'null') { if (data.result === 'null') {

8
src/helpers/devices/getNextMCU.js

@ -1,5 +1,5 @@
// @flow // @flow
import axios from 'axios' import network from 'api/network'
import { GET_NEXT_MCU } from 'helpers/urls' import { GET_NEXT_MCU } from 'helpers/urls'
import createCustomErrorClass from 'helpers/createCustomErrorClass' import createCustomErrorClass from 'helpers/createCustomErrorClass'
@ -8,8 +8,12 @@ const LatestMCUInstalledError = createCustomErrorClass('LatestMCUInstalledError'
export default async (bootloaderVersion: string): Promise<*> => { export default async (bootloaderVersion: string): Promise<*> => {
try { try {
const { data } = await axios.post(GET_NEXT_MCU, { const { data } = await network({
method: 'POST',
url: GET_NEXT_MCU,
data: {
bootloader_version: bootloaderVersion, bootloader_version: bootloaderVersion,
},
}) })
// FIXME: nextVersion will not be able to "default" when Error // FIXME: nextVersion will not be able to "default" when Error

23
src/helpers/devices/getOsuFirmware.js

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

8
src/helpers/firmware/getFinalFirmwareById.js

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

28
src/helpers/firmware/installFinalFirmware.js

@ -3,18 +3,34 @@ import type Transport from '@ledgerhq/hw-transport'
import { WS_INSTALL } from 'helpers/urls' import { WS_INSTALL } from 'helpers/urls'
import { createDeviceSocket } from 'helpers/socket' import { createDeviceSocket } from 'helpers/socket'
import getDeviceVersion from 'helpers/devices/getDeviceVersion'
import getOsuFirmware from 'helpers/devices/getOsuFirmware'
import getFinalFirmwareById from './getFinalFirmwareById'
type Input = Object type Input = {
targetId: number | string,
version: string,
}
type Result = * type Result = *
export default async (transport: Transport<*>, firmware: Input): Result => { export default async (transport: Transport<*>, app: Input): Result => {
try { try {
const url = WS_INSTALL(firmware) const { targetId, version } = app
const device = await getDeviceVersion(targetId)
const firmware = await getOsuFirmware({ deviceId: device.id, version })
const { next_se_firmware_final_version } = firmware
const nextFirmware = await getFinalFirmwareById(next_se_firmware_final_version)
const params = {
targetId,
...nextFirmware,
firmwareKey: nextFirmware.firmware_key,
}
const url = WS_INSTALL(params)
await createDeviceSocket(transport, url).toPromise() await createDeviceSocket(transport, url).toPromise()
return { success: true } return { success: true }
} catch (err) { } catch (error) {
const error = Error(err.message)
error.stack = err.stack
const result = { success: false, error } const result = { success: false, error }
throw result throw result
} }

1
src/helpers/firmware/installMcu.js

@ -13,7 +13,6 @@ export default async (
): Result => { ): Result => {
const { version } = args const { version } = args
const nextVersion = await getNextMCU(version) const nextVersion = await getNextMCU(version)
const params = { const params = {
targetId: args.targetId, targetId: args.targetId,
version: nextVersion.name, version: nextVersion.name,

4
src/helpers/firmware/installOsuFirmware.js

@ -22,9 +22,7 @@ export default async (
const url = WS_INSTALL(params) const url = WS_INSTALL(params)
await createDeviceSocket(transport, url).toPromise() await createDeviceSocket(transport, url).toPromise()
return { success: true } return { success: true }
} catch (err) { } catch (error) {
const error = Error(err.message)
error.stack = err.stack
const result = { success: false, error } const result = { success: false, error }
throw result throw result
} }

4
src/helpers/hardReset.js

@ -5,9 +5,7 @@ import db from 'helpers/db'
import { delay } from 'helpers/promise' import { delay } from 'helpers/promise'
export default async function hardReset() { export default async function hardReset() {
// TODO: wait for the libcoreHardReset to be finished await libcoreHardReset.send()
// actually, libcore doesnt goes back to js thread
await Promise.race([libcoreHardReset.send().toPromise(), delay(500)])
disableDBMiddleware() disableDBMiddleware()
db.resetAll() db.resetAll()
await delay(500) await delay(500)

2
src/helpers/urls.js

@ -14,9 +14,11 @@ const wsURLBuilder = (endpoint: string) => (params?: Object) =>
// const wsURLBuilderProxy = (endpoint: string) => (params?: Object) => // const wsURLBuilderProxy = (endpoint: string) => (params?: Object) =>
// `ws://manager.ledger.fr:3501/${endpoint}${params ? `?${qs.stringify(params)}` : ''}` // `ws://manager.ledger.fr:3501/${endpoint}${params ? `?${qs.stringify(params)}` : ''}`
export const GET_FINAL_FIRMWARE: string = managerUrlbuilder('firmware_final_versions')
export const GET_DEVICE_VERSION: string = managerUrlbuilder('get_device_version') export const GET_DEVICE_VERSION: string = managerUrlbuilder('get_device_version')
export const APPLICATIONS_BY_DEVICE: string = managerUrlbuilder('get_apps') export const APPLICATIONS_BY_DEVICE: string = managerUrlbuilder('get_apps')
export const GET_CURRENT_FIRMWARE: string = managerUrlbuilder('get_firmware_version') export const GET_CURRENT_FIRMWARE: string = managerUrlbuilder('get_firmware_version')
export const GET_CURRENT_OSU: string = managerUrlbuilder('get_osu_version')
export const GET_LATEST_FIRMWARE: string = managerUrlbuilder('get_latest_firmware') export const GET_LATEST_FIRMWARE: string = managerUrlbuilder('get_latest_firmware')
export const GET_NEXT_MCU: string = managerUrlbuilder('mcu_versions_bootloader') export const GET_NEXT_MCU: string = managerUrlbuilder('mcu_versions_bootloader')

1
src/icons/Trash.js

@ -5,6 +5,7 @@ import React from 'react'
const path = ( const path = (
<g transform="translate(670.57 190.38)"> <g transform="translate(670.57 190.38)">
<path <path
fill="currentColor"
d="m-658.54-187.18h3.2002a0.80037 0.80037 0 0 1 0 1.5993h-0.80049v10.4a2.3999 2.3999 0 0 1-2.3999 2.3999h-8.0001a2.3999 2.3999 0 0 1-2.3999-2.3999v-10.4h-0.79878a0.80037 0.80037 0 1 1 0-1.5993h3.1991v-0.80049a2.3999 2.3999 0 0 1 2.3999-2.3999h3.2003a2.3999 2.3999 0 0 1 2.3999 2.3999zm-1.5993 0v-0.80049a0.80037 0.80037 0 0 0-0.80049-0.80049h-3.2003a0.80037 0.80037 0 0 0-0.79878 0.80049v0.80049zm0.80049 1.5993a0.84357 0.84357 0 0 1-1e-3 0h-6.3995a0.84357 0.84357 0 0 1-2e-3 0h-1.5976v10.4c0 0.44224 0.35825 0.79877 0.79878 0.79877h8.0001a0.80037 0.80037 0 0 0 0.8005-0.79877v-10.4zm-5.6004 3.2003a0.80037 0.80037 0 1 1 1.5993 0v4.7997a0.80037 0.80037 0 0 1-1.5993 0zm3.1992 0a0.80049 0.80049 0 1 1 1.601 0v4.7997a0.80049 0.80049 0 0 1-1.601 0z" d="m-658.54-187.18h3.2002a0.80037 0.80037 0 0 1 0 1.5993h-0.80049v10.4a2.3999 2.3999 0 0 1-2.3999 2.3999h-8.0001a2.3999 2.3999 0 0 1-2.3999-2.3999v-10.4h-0.79878a0.80037 0.80037 0 1 1 0-1.5993h3.1991v-0.80049a2.3999 2.3999 0 0 1 2.3999-2.3999h3.2003a2.3999 2.3999 0 0 1 2.3999 2.3999zm-1.5993 0v-0.80049a0.80037 0.80037 0 0 0-0.80049-0.80049h-3.2003a0.80037 0.80037 0 0 0-0.79878 0.80049v0.80049zm0.80049 1.5993a0.84357 0.84357 0 0 1-1e-3 0h-6.3995a0.84357 0.84357 0 0 1-2e-3 0h-1.5976v10.4c0 0.44224 0.35825 0.79877 0.79878 0.79877h8.0001a0.80037 0.80037 0 0 0 0.8005-0.79877v-10.4zm-5.6004 3.2003a0.80037 0.80037 0 1 1 1.5993 0v4.7997a0.80037 0.80037 0 0 1-1.5993 0zm3.1992 0a0.80049 0.80049 0 1 1 1.601 0v4.7997a0.80049 0.80049 0 0 1-1.601 0z"
strokeWidth="1.2" strokeWidth="1.2"
/> />

15
src/index.ejs

@ -33,8 +33,6 @@
width: 80px; width: 80px;
animation: logo 4s infinite 0.5s; animation: logo 4s infinite 0.5s;
transform-origin: 50% 50%; transform-origin: 50% 50%;
opacity: 0;
transition: opacity 0.5s ease-in-out;
} }
@keyframes logo { @keyframes logo {
@ -42,16 +40,16 @@
transform: rotate(0deg); transform: rotate(0deg);
} }
20% { 20% {
transform: rotate(360deg); transform: rotate(-360deg);
} }
30% { 30% {
transform: rotate(360deg); transform: rotate(-360deg);
} }
50% { 50% {
transform: rotate(720deg); transform: rotate(-720deg);
} }
60% { 60% {
transform: rotate(720deg); transform: rotate(-720deg);
} }
100% { 100% {
transform: rotate(0deg); transform: rotate(0deg);
@ -94,12 +92,9 @@ const initApp = (options = {}) => {
} }
if (name === 'MainWindow') { if (name === 'MainWindow') {
setTimeout(() => {
logoEl.style.opacity = 1
}, 50)
preloadEl.style.display = 'flex' preloadEl.style.display = 'flex'
const startTime = Date.now() const startTime = Date.now()
const PRELOAD_WAIT_TIME_MIN = 2000 const PRELOAD_WAIT_TIME_MIN = 3000
window.onAppReady = () => { window.onAppReady = () => {
const delay = Math.max(0, PRELOAD_WAIT_TIME_MIN - (Date.now() - startTime)) const delay = Math.max(0, PRELOAD_WAIT_TIME_MIN - (Date.now() - startTime))
setTimeout(initApp, delay) setTimeout(initApp, delay)

317
static/i18n/en/app.yml

@ -1,5 +1,5 @@
common: common:
ok: Okay ok: OK
yes: Yes yes: Yes
no: No no: No
apply: Apply apply: Apply
@ -7,12 +7,12 @@ common:
cancel: Cancel cancel: Cancel
delete: Delete delete: Delete
continue: Continue continue: Continue
skipThisStep: Skip This Step skipThisStep: Skip this step
chooseWalletPlaceholder: Choose a wallet... chooseWalletPlaceholder: Choose a wallet...
currency: Currency currency: Currency
selectAccount: Select an account selectAccount: Select an account
selectAccountNoOption: 'No account matching "{{accountName}}"' selectAccountNoOption: 'No account matching "{{accountName}}"'
selectCurrency: Select a currency selectCurrency: Choose a crypto asset
selectCurrencyNoOption: 'No currency matching "{{currencyName}}"' selectCurrencyNoOption: 'No currency matching "{{currencyName}}"'
selectExchange: Select an exchange selectExchange: Select an exchange
selectExchangeNoOption: 'No exchange matching "{{exchangeName}}"' selectExchangeNoOption: 'No exchange matching "{{exchangeName}}"'
@ -21,7 +21,7 @@ common:
save: Save save: Save
password: Password password: Password
editProfile: Edit profile editProfile: Edit profile
lockApplication: Lock application lockApplication: Lock Ledger Live
showMore: Show more showMore: Show more
max: Max max: Max
next: Next next: Next
@ -32,15 +32,15 @@ common:
eastern: Eastern eastern: Eastern
western: Western western: Western
lockScreen: lockScreen:
title: Welcome Back title: Welcome back
subTitle: Your application is locked subTitle: Ledger Live is locked
description: Please enter your password to continue description: Enter your password to continue
inputPlaceholder: Type your password inputPlaceholder:
lostPassword: I lost my password lostPassword: I lost my password
sync: sync:
syncing: Syncing... syncing: Synchronizing...
upToDate: Up to date upToDate: Live
error: Sync error. error: Synchronization error
refresh: Refresh refresh: Refresh
ago: Synced {{time}} ago: Synced {{time}}
error: error:
@ -48,8 +48,12 @@ common:
noResults: No results noResults: No results
operation: operation:
type: type:
IN: Receive funds IN: Received
OUT: Sent funds # conf: Received
# unconf: Receiving...
OUT: Sent
# conf: Sent
# unconf: Sending...
time: time:
day: Day day: Day
week: Week week: Week
@ -70,57 +74,61 @@ account:
receive: Receive receive: Receive
lastOperations: Last operations lastOperations: Last operations
emptyState: emptyState:
title: This is a title, use it with caution title: No funds yet?
desc: Please create a new account or recover an old account from your Ledger device. desc: Make sure the [cryptocurrency] app is installed to receive funds. # replace [cryptocurrency] and make it bold
buttons: buttons:
receiveFunds: Receive Funds receiveFunds: Receive funds
settings: settings:
title: Edit Account title: Edit account
advancedLogs: Advanced logs advancedLogs: Advanced logs
accountName: accountName:
title: Account name title: Account name
desc: Lorem ipsum dolort amet desc: Describe this account.
error: Name is required error: An account name is required.
unit: unit:
title: Unit title: Unit
desc: Lorem ipsum dolort amet desc: Choose the unit to display.
endpointConfig: endpointConfig:
title: Node title: Node
desc: The API node to use desc: The API node to use
error: Invalid endpoint error: Invalid endpoint
dashboard: dashboard:
title: Dashboard title: Portfolio
emptyAccountTile:
desc: Lorem ipsum dolor sit amet, consectetur adipiscing elit
createAccount: Create Account
accounts: accounts:
title: Accounts ({{count}}) title: Accounts ({{count}})
greeting: greeting:
morning: "Good Morning!" morning: "Good morning"
evening: "Good Evening!" evening: "Good evening"
afternoon: "Good Afternoon!" afternoon: "Good afternoon"
summary: here is the summary of your account summary: "Here's the summary of your account."
summary_plural: 'here is the summary of your {{count}} accounts' summary_plural: "Here's the summary of your {{count}} accounts."
noAccounts: no accounts noAccounts: No accounts yet
recentActivity: Recent activity recentActivity: Recent activity
totalBalance: Total balance totalBalance: Total balance
accountsOrder: accountsOrder:
name: Alphabetic name: name
balance: Balance balance: balance
currentAddress: currentAddress:
title: Current address title: Current address
for: Address for <1><0>{{accountName}}</0></1> for: Address for <1><0>{{accountName}}</0></1>
message: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam blandit velit egestas leo tincidunt message: Your receive address has not been confirmed on your Ledger device. Verify the address for optimal security.
deviceConnect: deviceConnect:
step1: step1:
choose: "We detected {{count}} devices connected, please select one:" choose: "We detected {{count}} connected devices, please select one:"
connect: Connect your <1>Ledger device</1> to your computer and enter your <3>PIN code</3> on your device connect: Connect and unlock your <1>Ledger device</1> # remove key: <3>PIN code</3>
dashboard: test
emptyState: emptyState:
sidebar: sidebar:
text: You don’t have any accounts at the moment. Press the + button to create an account text: Press the + button to add an account to your portfolio.
dashboard: dashboard:
title: This is a title, use it with caution title: 'Let’s set up your portfolio!'
desc: Please create a new account or recover an old account from your Ledger device. desc: Open the Manager to install apps on your device or add accounts if your device already has apps installed.
buttons: buttons:
addAccount: Add Account addAccount: Add accounts
installApp: Install App installApp: Open Manager
exchange: exchange:
title: Exchange title: Exchange
visitWebsite: Visit website visitWebsite: Visit website
@ -133,28 +141,29 @@ genuinecheck:
addAccounts: addAccounts:
title: Add accounts title: Add accounts
breadcrumb: breadcrumb:
informations: Informations informations: Choose asset
connectDevice: Connect device connectDevice: Connect device
import: Import import: Select accounts
finish: End finish: Confirmation
accountToImportSubtitle: Account to import accountToImportSubtitle: Select existing accounts
accountToImportSubtitle_plural: 'Accounts to import ({{count}})' accountToImportSubtitle_plural: 'Select ({{count}}) existing accounts'
selectAll: Select all selectAll: Select all
unselectAll: Unselect all unselectAll: Deselect all
editName: Edit name editName: Edit name
newAccount: New account newAccount: New account
legacyAccount: '{{accountName}} (legacy)' legacyAccount: '{{accountName}} (legacy)'
noAccountToImport: We didnt find any {{currencyName}} account to import. noAccountToImport: All {{currencyName}} accounts found are already in your portfolio.
success: Great success! success: Account successfully added to your portfolio.
# success_plural: Accounts successfully added to your portfolio.
createNewAccount: createNewAccount:
title: Create new account title: Create new account
noOperationOnLastAccount: You cannot create a new account because your last account has no operations noOperationOnLastAccount: 'You have to receive funds on {{accountName}} before you can create a new account.'
noAccountToCreate: We didnt find any {{currencyName}} account to create. noAccountToCreate: No {{currencyName}} account was found to create.
somethingWentWrong: Something went wrong during synchronization. somethingWentWrong: Something went wrong during synchronization, please try again.
cta: cta:
create: 'Create account' create: 'Add account'
import: 'Import account' import: 'Add account' # Remove
import_plural: 'Import accounts' import_plural: 'Add accounts'
operationDetails: operationDetails:
title: Operation details title: Operation details
account: Account account: Account
@ -165,11 +174,12 @@ operationDetails:
fees: Fees fees: Fees
from: From from: From
to: To to: To
identifier: Hash identifier: Transaction ID
viewOperation: View operation viewOperation: View in explorer
showMore: See {{recipients}} more showMore: Show {{recipients}} more
showLess: Show less
operationList: operationList:
noMoreOperations: No more operations noMoreOperations: That's all!
manager: manager:
tabs: tabs:
apps: Apps apps: Apps
@ -182,50 +192,54 @@ manager:
installSuccess: '{{app}} app successfully installed' installSuccess: '{{app}} app successfully installed'
uninstallSuccess: '{{app}} app successfully uninstalled' uninstallSuccess: '{{app}} app successfully uninstalled'
alreadyInstalled: '{{app}} app is already installed' alreadyInstalled: '{{app}} app is already installed'
help: To update an app, you have to uninstall the app and re install it. help: Remove and reinstall to update apps
firmware: firmware:
installed: 'Firmware {{version}}' installed: 'Firmware version {{version}}'
update: Update firmware update: Update firmware
updateTitle: Firmware update updateTitle: Firmware update
latest: 'A new firmware {{version}} is available' continue: Continue update
latest: 'Firmware version {{version}} is available.'
disclaimerTitle: 'You are about to install the latest <1><0>firmware {{version}}</0></1>'
disclaimerAppDelete: Please note that all the apps installed on your device will be deleted.
disclaimerAppReinstall: You will be able to re-install your apps after the firmware update
title: Manager title: Manager
subtitle: Get all your apps here subtitle: Install apps or update your device.
device: device:
title: Plug your device title: Connect your device
desc: Please connect your Ledger device and follow the steps below to access the manager desc: Follow the steps below to use the Manager
cta: Plug my device cta: Connect my device
errors: errors:
noDevice: Please make sur your device is connected (TEMPLATE NEEDED) noDevice: No device is connected (TEMPLATE NEEDED)
noDashboard: Please make sure your device is on the dashboard screen (TEMPLATED NEEDED) noDashboard: Navigate to the dashboard on your device (TEMPLATED NEEDED)
noGenuine: You did not approve request on your device or your device is not genuine (TEMPLATE NEEDED) noGenuine: Allow the Manager to continue (TEMPLATE NEEDED)
receive: receive:
title: Receive funds title: Receive funds
steps: steps:
chooseAccount: chooseAccount:
title: Choose Account title: Choose account
label: Account label: Account
connectDevice: connectDevice:
title: Connect Device title: Connect device
withoutDevice: I don't have my device withoutDevice: Proceed without device
confirmAddress: confirmAddress:
title: Confirm Address title: Confirm address
action: Confirm address on device action: Confirm address on device
text: To receive funds, confirm the address on your device. text: To receive funds, confirm the address on your device.
support: Contact Support support: Ledger Support
error: error:
title: Houston, we have a problem! title: Receive address rejected
text: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer non nibh diam. In eget ipsum arcu donec finibus text: Please try again or request Ledger Support assistance when in doubt.
receiveFunds: receiveFunds:
title: Receive Funds title: Receive funds
label: Amount (Optional) label: Amount (optional)
send: send:
title: Send funds title: Send funds
totalSpent: Total spent totalSpent: Total
steps: steps:
amount: amount:
title: Informations title: Create payment
selectAccountDebit: Select an account to debit selectAccountDebit: Select an account to debit
recipientAddress: Recipient address recipientAddress: Recipient address # can't control the tooltip!
amount: Amount amount: Amount
max: Max max: Max
fees: Fees fees: Fees
@ -235,25 +249,24 @@ send:
rippleTag: Tag rippleTag: Tag
ethereumGasLimit: Gas limit ethereumGasLimit: Gas limit
unitPerByte: '{{unit}} per byte' unitPerByte: '{{unit}} per byte'
feePerByte: Fee per byte feePerByte: Fees per byte
connectDevice: connectDevice:
title: Connect device title: Connect device
verification: verification:
title: Verification title: Verification
warning: | warning: |
You are about to validate a transaction. Carefully verify the transaction details on your device. Press the left button on your device to cancel.
Be careful, we strongly recommand you to verify that the informations on your Ledger device are correct. body: Press the right button to confirm the transaction.
body: Once you have checked everything is ok, you can validate securely the transaction on your device.
confirmation: confirmation:
title: Confirmation title: Confirmation
success: success:
title: Transaction successfully broadcasted title: Transaction sent
text: | text: |
with the following transaction id: The transaction has been signed and sent to the network. Your account balance will update once the blockchain has confirmed the transaction. It has the following Transaction ID:
cta: View operation details cta: View operation details
error: error:
title: Transaction error title: Transaction canceled
cta: Retry operation cta: Retry
pending: pending:
title: Broadcasting transaction... title: Broadcasting transaction...
releaseNotes: releaseNotes:
@ -265,73 +278,73 @@ settings:
display: Display display: Display
currencies: Currencies currencies: Currencies
profile: Profile profile: Profile
about: About about: Help
display: display:
desc: Lorem ipsum dolor sit amet desc:
language: Interface language language: Language
languageDesc: Lorem ipsum dolor sit amet languageDesc: Choose the language to display.
counterValue: Countervalue counterValue: Base currency
counterValueDesc: Lorem ipsum dolor sit amet counterValueDesc: Choose the currency to display next to your balance and operations.
exchange: Exchange ({{ticker}}) exchange: Rate provider ({{ticker}})
exchangeDesc: The exchange to use for countervalue conversion exchangeDesc: Choose the provider of the base currency exchange rates.
region: Region region: Region
regionDesc: Lorem ipsum dolor sit amet regionDesc: Choose the region in which you’re located to set the application’s time zone.
stock: Stock market indicators stock: Regional market indicator
stockDesc: Lorem ipsum dolor sit amet stockDesc: Choose Western to display an increase in market value in blue. Choose Eastern to display an increase in market value in red.
currencies: currencies:
desc: Lorem ipsum dolor sit amet desc: Select a cryptocurrency to edit its settings.
exchange: Exchange ({{ticker}}) exchange: Rate provider ({{ticker}})
exchangeDesc: The exchange to use for countervalue conversion exchangeDesc: Choose the provider of the base currency exchange rates.
confirmationsToSpend: Confirmations to spend confirmationsToSpend: Number of confirmations required to spend
confirmationsToSpendDesc: Lorem ipsum dolor sit amet confirmationsToSpendDesc: Set the number of confirmations required for your funds to be spendable. # A higher number of confirmations decreases the probability that a transaction is rejected.
confirmationsNb: Number of confirmations confirmationsNb: Number of confirmations
confirmationsNbDesc: Lorem ipsum dolor sit amet confirmationsNbDesc: Set the number of blocks a transaction needs to be included in to consider it as confirmed. # A higher number of confirmations increases the certainty that a transaction cannot be reversed.
transactionsFees: Transactions fees transactionsFees: Default transaction fees
transactionsFeesDesc: Lorem ipsum dolor sit amet transactionsFeesDesc: Select your default transaction fees. The higher the fee, the quicker the transaction will be processed.
explorer: Blockchain explorer explorer: Blockchain explorer
explorerDesc: Lorem ipsum dolor sit amet explorerDesc: Which service to use to look up an operation in the blockchain.
profile: profile:
desc: Lorem ipsum dolor sit amet desc:
password: Password password: Data encryption
passwordDesc: Lorem ipsum dolor sit amet passwordDesc: Enhance your privacy. Set a password to encrypt Ledger Live data stored on your computer, including account names, balances, transactions and public addresses.
changePassword: Change password changePassword: Change password
sync: Sync accounts sync: Synchronize accounts
syncDesc: Lorem ipsum dolor sit amet syncDesc: Resynchronize your accounts with the blockchain.
export: Export logs export: Export logs
exportDesc: Lorem ipsum dolor sit amet exportDesc: Exporting Ledger Live logs may be necessary for troubleshooting purposes.
softResetTitle: Clean application cache softResetTitle: Clear cache
softResetDesc: Lorem ipsum dolor sit amet softResetDesc: Clear the Ledger Live cache to force resynchronization with the blockchain.
softReset: Clean cache softReset: Clear
hardResetTitle: Reset application hardResetTitle: Reset Ledger Live
hardResetDesc: Lorem ipsum dolor sit amet hardResetDesc: Erase all Ledger Live data stored on your computer, including your profile, accounts, transaction history and settings. The private keys that manage your crypto assets remain secure on your Ledger device.
hardReset: Hard reset hardReset: Reset
developerMode: Developer Mode developerMode: Developer mode
developerModeDesc: Enable visibility of developer apps & currencies like Bitcoin Testnet developerModeDesc: Show developer apps in the Manager.
analytics: Share analytics analytics: Analytics
analyticsDesc: Help Ledger improve its products and services by automatically sending diagnostics and usage data. analyticsDesc: Enable analytics of anonymous data to help Ledger improve the user experience. This includes the operating system, language, firmware versions and the number of added accounts.
reportErrors: Sentry Logs reportErrors: Usage and diagnostics
reportErrorsDesc: Help Ledger improve its products and services by automatically sending diagnostics and usage data. reportErrorsDesc: Share anonymous usage and diagnostics data to help improve Ledger products, services and security features.
about: about:
desc: Lorem ipsum dolor sit amet desc:
version: Version version: Ledger Live version
releaseNotesBtn: Show release notes releaseNotesBtn: Show release notes # Close button instead of continue.
faq: FAQ faq: Ledger Support
faqDesc: Lorem ipsum dolor sit amet faqDesc: A problem? Learn about Ledger Live, Ledger devices, supported crypto assets and apps.
contactUs: Contact us contactUs: Contact us
contactUsDesc: Lorem ipsum dolor sit amet contactUsDesc: Need help? Request assistance from Ledger Support by email or chat.
terms: Terms and Privacy policy terms: --- Terms and Privacy policy ---
termsDesc: Lorem ipsum dolor sit amet termsDesc: --- Check with Legal ---
hardResetModal: hardResetModal:
title: Reset Ledger Live title: Reset Ledger Live
desc: Resetting will erase all Ledger Live data stored on your computer, including your profile, accounts, transaction history and application settings. The keys to access your crypto assets in the blockchain remain secure on your Ledger device. desc: Resetting will erase all Ledger Live data stored on your computer, including your profile, accounts, transaction history and application settings. The keys to access your crypto assets in the blockchain remain secure on your Ledger device.
softResetModal: softResetModal:
title: Clean application cache title: Clear cache
subTitle: Are you sure houston? subTitle: Are you sure?
desc: Lorem ipsum dolor sit amet desc: Clearing the Ledger Live cache forces resynchronization with the blockchain.
removeAccountModal: removeAccountModal:
title: Delete this account title: Remove account
subTitle: Are you sure houston? subTitle: Are you sure?
desc: Lorem ipsum dolor sit amet desc: The account will no longer be included in your portfolio. Accounts can always be re-added.
exportLogs: exportLogs:
title: Export Logs title: Export Logs
desc: Export Logs desc: Export Logs
@ -347,32 +360,32 @@ password:
inputFields: inputFields:
newPassword: newPassword:
label: Password label: Password
placeholder: Password placeholder:
confirmPassword: confirmPassword:
label: Confirm Password label: Confirm password
placeholder: Confirm Password placeholder:
currentPassword: currentPassword:
label: Current Password label: Current password
placeholder: Current Password placeholder:
changePassword: changePassword:
title: Edit Password title: Data encryption
subTitle: Change your password subTitle: Change your password
desc: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer non nibh diam. In eget ipsum arcu donec finibus desc: Make sure to remember your password. Losing your password requires resetting Ledger Live and re-adding accounts.
setPassword: setPassword:
title: Set Password title: Enable data encryption
subTitle: Set a password to lock your application subTitle: Set a password
desc: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer non nibh diam. In eget ipsum arcu donec finibus desc: Make sure to remember your password. Losing your password requires resetting Ledger Live and re-adding accounts.
disablePassword: disablePassword:
title: Disable Password title: Disable data encryption
desc: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer non nibh diam. desc: Ledger Live data will be stored unencrypted on your computer. This includes account names, balances, transactions and public addresses.
update: update:
newVersionReady: A new update is available. newVersionReady: A new update is available.
relaunch: Update now relaunch: Update now
crash: crash:
oops: Oops, something went wrong. 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. uselessText: You may try again by restarting Ledger Live. Please export your logs and contact Ledger Support if the problem persists.
restart: Restart app restart: Restart
reset: Hard reset reset: Hard reset
createTicket: Create issue createTicket: Ledger Support
showDetails: Show details showDetails: Show details
showError: Show error showError: Show error

39
static/i18n/en/errors.yml

@ -1,22 +1,25 @@
generic: An error occurred generic: Oops, an unknown error occurred. Please try again or contact Ledger Support.
RangeError: '{{message}}' RangeError: '{{message}}'
Error: '{{message}}' Error: '{{message}}'
LedgerAPIErrorWithMessage: '{{message}}' LedgerAPIErrorWithMessage: '{{message}}'
TransportStatusError: '{{message}}' TransportStatusError: '{{message}}'
TimeoutError: 'Timeout reached' TimeoutError: 'The request timed out. Please try again or contact Ledger Support.'
FeeEstimationFailed: 'fee estimation failed (status: {{status}})' FeeEstimationFailed: 'The fee could not be estimated. Please try again or set a custom fee (status: {{status}})'
NotEnoughBalance: 'Not enough balance' NotEnoughBalance: 'The account has insufficient funds to proceed.'
BtcUnmatchedApp: 'You must open application ‘{{currencyName}}’ on the device' BtcUnmatchedApp: 'Open the ‘{{currencyName}}’ app on your Ledger device to proceed.'
WrongAppOpened: 'You must open application ‘{{currencyName}}’ on the device' WrongAppOpened: 'Open the ‘{{currencyName}}’ app on your Ledger device to proceed.'
WrongDeviceForAccount: 'You must use the device associated to the account ‘{{accountName}}’' WrongDeviceForAccount: 'Use the device associated with the account ‘{{accountName}}’.'
LedgerAPINotAvailable: 'Ledger API is not available for currency {{currencyName}}' LedgerAPINotAvailable: 'The Ledger API is not available for {{currencyName}}. Please check status.ledger.fr.'
LedgerAPIError: 'A problem occurred with Ledger API. Please try again later. (HTTP {{status}})' LedgerAPIError: 'A problem occurred with the Ledger API. Please try again later. (HTTP {{status}})'
NetworkDown: 'Your internet connection seems down. Please try again later.' NetworkDown: 'Your internet connection seems down. Please try again.'
NoAddressesFound: 'No accounts found' NoAddressesFound: 'No accounts were found.'
UserRefusedOnDevice: Transaction have been aborted UserRefusedOnDevice: Please try again or request Ledger Support assistance when in doubt.
WebsocketConnectionError: An error occurred with the socket connection WebsocketConnectionError: An error occurred with the websocket connection. Please try again or contact Ledger Support.
WebsocketConnectionFailed: Failed to establish a socket connection WebsocketConnectionFailed: Oops, could not establish a websocket connection. Please try again or contact Ledger Support.
DeviceSocketFail: Device socket failure DeviceSocketFail: Oops. a device socket failure occurred. Please try again or contact Ledger Support.
DeviceSocketNoBulkStatus: Device socket failure (bulk) DeviceSocketNoBulkStatus: Oops, the device socket failed (bulk). Please try again or contact Ledger Support.
DeviceSocketNoHandler: Device socket failure (handler {{query}}) DeviceSocketNoHandler: Oops, the device socket failed (handler {{query}}). Please try again or contact Ledger Support.
LatestMCUInstalledError: The latest MCU is already installed on the Device LatestMCUInstalledError: The MCU on the device is already up to date.
HardResetFail: Could not reset Ledger Live. Please try again or contact Ledger Support.
CannotUninstall: Cannot uninstall app
CannotInstall: Cannot install app

61
static/i18n/en/onboarding.yml

@ -33,35 +33,36 @@ selectDevice:
ledgerBlueCard: ledgerBlueCard:
title: Ledger Blue title: Ledger Blue
selectPIN: selectPIN:
# initialize: disclaimer:
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 or a 24-word recovery phrase.
initialize:
title: Start initialization - Choose your PIN code title: Start initialization - Choose your PIN code
instructions: instructions:
ledgerNano: nano:
step1: Connect the Ledger Nano S to your computer. step1: Connect the Ledger Nano S to your computer.
step2: Press both buttons simultaneously as instructed on the screen. step2: Press both buttons simultaneously as instructed on the screen.
step3: Press the right button to select Configure as new device?. # <bold>Configure as new device?<bold>. 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 (✓).' step4: 'Choose a PIN code between 4 and 8 digits long. Then select the checkmark (✓).'
ledgerBlue: blue:
step1: Connect the Ledger Blue to your computer. step1: Connect the Ledger Blue to your computer.
step2: Tap on Configure as new device. step2: Tap on Configure as new device.
step3: Choose a PIN code between 4 and 8 digits long. step3: Choose a PIN code between 4 and 8 digits long.
# restore: restore:
# title: Start restoration - Choose your PIN code title: Start restoration - Choose your PIN code
# instructions: instructions:
# nano: nano:
# step1: Connect the Ledger Nano S to your computer. step1: Connect the Ledger Nano S to your computer.
# step2: Press both buttons simultaneously as instructed on the screen. step2: Press both buttons simultaneously as instructed on the screen.
# step3: Press the 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>. 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 (✓).' step4: 'Choose a PIN code between 4 and 8 digits long. Then select the checkmark (✓).'
# blue: blue:
# step1: Connect the Ledger Blue to your computer. step1: Connect the Ledger Blue to your computer.
# step2: Tap on Restore configuration. # <bold>Restore configuration</bold>. step2: Tap on Restore configuration. # <bold>Restore configuration</bold>.
# step3: Choose a PIN code between 4 and 8 digits long. step3: Choose a PIN code between 4 and 8 digits long.
disclaimer:
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 or a 24-word recovery phrase.
writeSeed: writeSeed:
initialize:
title: Save your recovery phrase title: Save your recovery phrase
desc: Your device will generate a recovery phrase of 24 words, displayed only once. desc: Your device will generate a recovery phrase of 24 words, displayed only once.
nano: nano:
@ -80,7 +81,7 @@ writeSeed:
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> 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> 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. step4: Repeat the process until the last word.
ledgerBlue: blue:
step1: Select the length of your recovery phrase. step1: Select the length of your recovery phrase.
step2: Type the first word of your recovery phrase. Select the word when it appears. step2: Type the first word of your recovery phrase. Select the word when it appears.
step3: Repeat the process until the last word. step3: Repeat the process until the last word.
@ -92,17 +93,15 @@ writeSeed:
genuineCheck: genuineCheck:
title: Final security check title: Final security check
descNano: Your Ledger Nano S should now display Your device is now ready. Before getting started, please confirm that descNano: Your Ledger Nano S should now display Your device is now ready. Before getting started, please confirm that
descBlue: #Your Ledger Blue should now display Your device is now ready. Before getting started, please confirm that descBlue: Your Ledger Blue should now display Your device is now ready. Before getting started, please confirm that
steps: descRestore: Before getting started, please confirm that
step1: step1:
title: Did you choose your PIN code by yourself? title: Did you choose your PIN code by yourself?
step2: step2:
title: Did you save your recovery phrase by yourself? title: Did you save your recovery phrase by yourself?
desc:
step3: step3:
title: Check if your Ledger device is genuine title: Do you have a genuine Ledger device?
desc: isGenuinePassed: Your device is genuine
isGenuinePassed: 'Genuine'
buttons: buttons:
genuineCheck: Genuine check genuineCheck: Genuine check
contactSupport: Ledger Support contactSupport: Ledger Support
@ -119,7 +118,7 @@ setPassword:
disclaimer: disclaimer:
note1: Make sure to remember your password. Do not share it. note1: Make sure to remember your password. Do not share it.
note2: Losing your password requires resetting Ledger Live and re-adding accounts. note2: Losing your password requires resetting Ledger Live and re-adding accounts.
note3: Resetting Ledger Live does not affect your crypto-assets. note3: Resetting Ledger Live does not affect your crypto assets.
password: Password password: Password
confirmPassword: Confirm password confirmPassword: Confirm password
skipThisStep: Skip this step skipThisStep: Skip this step
@ -128,12 +127,12 @@ analytics:
desc: Share anonymous usage and diagnostics data to help improve Ledger products, services and security features. desc: Share anonymous usage and diagnostics data to help improve Ledger products, services and security features.
shareAnalytics: shareAnalytics:
title: Share usage data 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. desc: Enable analytics of anonymous data to help Ledger improve the user experience. This includes the operating system, language, firmware versions and the number of added accounts.
sentryLogs: sentryLogs:
title: Report bugs title: Report bugs
desc: Automatically send bug reports to help Ledger developers diagnose issues and improve Ledger Live performance. desc: Automatically send bug reports to help Ledger developers diagnose issues and improve Ledger Live performance.
finish: finish:
title: 'Ready for launch!' title: Welcome to Ledger Live
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! desc: The unified crypto portfolio, backed by the security of your Ledger device.
openAppButton: Launch openAppButton: Launch
followUsLabel: followUsLabel: Follow us

2
static/images/empty-account-tile.svg

@ -0,0 +1,2 @@
<svg width="224" height="94" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><rect id="b" x="20" y="12" width="142" height="40" rx="4"/><filter x="-28.9%" y="-55%" width="157.7%" height="305%" filterUnits="objectBoundingBox" id="a"><feOffset dy="19" in="SourceAlpha" result="shadowOffsetOuter1"/><feGaussianBlur stdDeviation="10.5" in="shadowOffsetOuter1" result="shadowBlurOuter1"/><feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.03 0" in="shadowBlurOuter1"/></filter><rect id="d" x="10" y="9" width="162" height="38" rx="4"/><filter x="-25.3%" y="-57.9%" width="150.6%" height="315.8%" filterUnits="objectBoundingBox" id="c"><feOffset dy="19" in="SourceAlpha" result="shadowOffsetOuter1"/><feGaussianBlur stdDeviation="10.5" in="shadowOffsetOuter1" result="shadowBlurOuter1"/><feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.03 0" in="shadowBlurOuter1"/></filter><rect id="f" width="182" height="42" rx="4"/><filter x="-22.5%" y="-52.4%" width="145.1%" height="295.2%" filterUnits="objectBoundingBox" id="e"><feOffset dy="19" in="SourceAlpha" result="shadowOffsetOuter1"/><feGaussianBlur stdDeviation="10.5" in="shadowOffsetOuter1" result="shadowBlurOuter1"/><feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.03 0" in="shadowBlurOuter1"/></filter><path d="M5.62 0h1.7v3.058h-1.7V0zm1.7 0v3.058h-1.7V0h1.7zM5.813 12.942h1.7V16h-1.7v-3.058zm1.7 0V16h-1.7v-3.058h1.7zM2.406 0h1.7v3.058h-1.7V0zm1.7 0v3.058h-1.7V0h1.7zM2.6 12.942h1.7V16H2.6v-3.058zm1.7 0V16H2.6v-3.058h1.7zM.666 8.813V1.95h.85l6.139.002c1.807.11 3.212 1.566 3.118 3.254l-.002.279c.111 1.744-1.298 3.217-3.168 3.328H.666zm.85 0l.85-.85v4.32h5.608c.95-.025 1.676-.727 1.659-1.557v-.37c.019-.814-.707-1.518-1.637-1.543h-6.48zm7.557-3.275l.001-.378c.042-.77-.62-1.457-1.471-1.51H2.366v3.463h5.205c.899-.063 1.552-.753 1.502-1.575zM2.366 7.113l5.186.002.467-.002c1.86.05 3.355 1.5 3.314 3.262v.334c.036 1.779-1.458 3.224-3.337 3.273H.666V7.113h1.7zm0 0v.85l-.85-.85h.85z" id="g"/></defs><g fill="none" fill-rule="evenodd"><g transform="translate(21 2)"><use fill="#000" filter="url(#a)" xlink:href="#b"/><use fill="#FFF" xlink:href="#b"/></g><g transform="translate(21 2)"><use fill="#000" filter="url(#c)" xlink:href="#d"/><use fill="#FFF" xlink:href="#d"/></g><g transform="translate(21 2)"><use fill="#000" filter="url(#e)" xlink:href="#f"/><use fill="#FFF" xlink:href="#f"/><rect fill="#999" x="39" y="13" width="120" height="5" rx="2.5"/><rect fill="#D8D8D8" x="39" y="23" width="70" height="5" rx="2.5"/><g transform="translate(17 13)"><use fill="#FCB653" fill-rule="nonzero" xlink:href="#g"/></g></g></g></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

17
yarn.lock

@ -1502,9 +1502,9 @@
dependencies: dependencies:
events "^2.0.0" events "^2.0.0"
"@ledgerhq/ledger-core@2.0.0-rc.1": "@ledgerhq/ledger-core@2.0.0-rc.3":
version "2.0.0-rc.1" version "2.0.0-rc.3"
resolved "https://registry.yarnpkg.com/@ledgerhq/ledger-core/-/ledger-core-2.0.0-rc.1.tgz#0b31f7d2c693b9c11d4093dbb0896f13c33bf141" resolved "https://registry.yarnpkg.com/@ledgerhq/ledger-core/-/ledger-core-2.0.0-rc.3.tgz#21b04239e9ba6b7fdcb89958eea8ad47a4a28a88"
dependencies: dependencies:
"@ledgerhq/hw-app-btc" "^4.7.3" "@ledgerhq/hw-app-btc" "^4.7.3"
"@ledgerhq/hw-transport-node-hid" "^4.7.6" "@ledgerhq/hw-transport-node-hid" "^4.7.6"
@ -9322,6 +9322,10 @@ meant@~1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/meant/-/meant-1.0.1.tgz#66044fea2f23230ec806fb515efea29c44d2115d" resolved "https://registry.yarnpkg.com/meant/-/meant-1.0.1.tgz#66044fea2f23230ec806fb515efea29c44d2115d"
measure-scrollbar@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/measure-scrollbar/-/measure-scrollbar-1.1.0.tgz#986890d22866255ec5b212480f097c55a82d1231"
media-typer@0.3.0: media-typer@0.3.0:
version "0.3.0" version "0.3.0"
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
@ -11549,6 +11553,13 @@ react-is@^16.4.1:
version "16.4.1" version "16.4.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.4.1.tgz#d624c4650d2c65dbd52c72622bbf389435d9776e" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.4.1.tgz#d624c4650d2c65dbd52c72622bbf389435d9776e"
react-key-handler@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/react-key-handler/-/react-key-handler-1.0.1.tgz#1fc0f4f4855f506a192c2cbe9fe8cb78fc553191"
dependencies:
exenv "^1.2.0"
prop-types "^15.5.7"
react-lifecycles-compat@^3.0.0, react-lifecycles-compat@^3.0.4: react-lifecycles-compat@^3.0.0, react-lifecycles-compat@^3.0.4:
version "3.0.4" version "3.0.4"
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"

Loading…
Cancel
Save