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
steps:
- checkout
- restore_cache:
name: Restore Yarn Package Cache
keys:
- v1-yarn-packages-{{ .Branch }}-{{ checksum "yarn.lock" }}
- run:
name: Dependencies
command: SKIP_REBUILD=1 yarn
name: Install Dependencies
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:
name: Lint
command: yarn lint

1
electron-builder.yml

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

8
package.json

@ -7,9 +7,9 @@
"author": "Ledger",
"license": "MIT",
"scripts": {
"compile": "bash ./scripts/dist.sh",
"compile": "bash ./scripts/compile.sh",
"dist:dir": "yarn dist --dir -c.compression=store -c.mac.identity=null",
"dist": "yarn compile && electron-builder",
"dist": "bash ./scripts/dist.sh",
"test": "jest",
"flow": "flow",
"lint": "eslint src webpack .storybook",
@ -41,7 +41,7 @@
"@ledgerhq/hw-app-xrp": "^4.13.0",
"@ledgerhq/hw-transport": "^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",
"async": "^2.6.1",
"axios": "^0.18.0",
@ -65,6 +65,7 @@
"invariant": "^2.2.4",
"lodash": "^4.17.5",
"lru-cache": "^4.1.3",
"measure-scrollbar": "^1.1.0",
"moment": "^2.22.2",
"qrcode": "^1.2.0",
"qrcode-reader": "^1.0.4",
@ -74,6 +75,7 @@
"react": "^16.4.1",
"react-dom": "^16.4.1",
"react-i18next": "^7.7.0",
"react-key-handler": "^1.0.1",
"react-markdown": "^3.3.2",
"react-mortal": "^3.2.0",
"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
rm -rf dist &&
NODE_ENV=production webpack-cli --mode production --config webpack/internals.config.js &&
NODE_ENV=production electron-webpack
yarn compile && DEBUG=electron-builder electron-builder

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-register')

46
scripts/postinstall.sh

@ -1,8 +1,44 @@
#/bin/bash
flow-typed install -s --overwrite
rm flow-typed/npm/{react-i18next_v7.x.x.js,styled-components_v3.x.x.js,redux_*}
source scripts/hash-utils.sh
if [ "$SKIP_REBUILD" != "1" ]; then
electron-builder install-app-deps
fi
function MAIN {
if ! $CI; then
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 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
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> = {
scanAccountsOnDevice(currency, deviceId, { next, complete, error }) {
scanAccountsOnDevice: (currency, deviceId) =>
Observable.create(o => {
let finished = false
const unsubscribe = () => {
finished = true
@ -251,22 +252,22 @@ const EthereumBridge: WalletBridge<Transaction> = {
.send({ currencyId: currency.id, devicePath: deviceId, path: freshAddressPath })
.toPromise()
const r = await stepAddress(index, res, isStandard)
if (r.account) next(r.account)
if (r.account) o.next(r.account)
if (r.complete) {
break
}
}
}
complete()
o.complete()
} catch (e) {
error(e)
o.error(e)
}
}
main()
return { unsubscribe }
},
return unsubscribe
}),
synchronize: ({ freshAddress, blockHeight, currency, operations }) =>
Observable.create(o => {

3
src/bridge/LibcoreBridge.js

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

15
src/bridge/RippleJSBridge.js

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

8
src/bridge/UnsupportedBridge.js

@ -10,10 +10,10 @@ const UnsupportedBridge: WalletBridge<*> = {
o.error(genericError)
}),
scanAccountsOnDevice(currency, deviceId, { error }) {
Promise.resolve(genericError).then(error)
return { unsubscribe() {} }
},
scanAccountsOnDevice: () =>
Observable.create(o => {
o.error(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
async function job() {
if (Math.random() > scanAccountDeviceSuccessRate) {
await delay(1000)
if (!unsubscribed) error(new Error('scan failed'))
if (!unsubscribed) o.error(new Error('scan failed'))
return
}
const nbAccountToGen = 3
@ -92,9 +93,9 @@ function makeMockBridge(opts?: Opts): WalletBridge<*> {
currency,
})
account.unit = currency.units[0]
if (!unsubscribed) next(account)
if (!unsubscribed) o.next(account)
}
if (!unsubscribed) complete()
if (!unsubscribed) o.complete()
}
job()
@ -104,7 +105,7 @@ function makeMockBridge(opts?: Opts): WalletBridge<*> {
unsubscribed = true
},
}
},
}),
pullMoreOperations: async (_accountId, _desiredCount) => {
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 function returns a Subscription and you MUST stop everything if it is unsubscribed.
// TODO return Observable
scanAccountsOnDevice(
currency: Currency,
deviceId: DeviceId,
observer: Observer<Account>,
): Subscription;
scanAccountsOnDevice(currency: Currency, deviceId: DeviceId): Observable<Account>;
// 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

12
src/commands/installFinalFirmware.js

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

17
src/commands/libcoreHardReset.js

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

2
src/components/Breadcrumb/Step.js

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

52
src/components/DashboardPage/index.js

@ -4,15 +4,17 @@ import React, { PureComponent, Fragment } from 'react'
import { compose } from 'redux'
import { translate } from 'react-i18next'
import { connect } from 'react-redux'
import styled from 'styled-components'
import { push } from 'react-router-redux'
import { createStructuredSelector } from 'reselect'
import type { Account, Currency } from '@ledgerhq/live-common/lib/types'
import type { T } from 'types/common'
import { colors } from 'styles/theme'
import { accountsSelector } from 'reducers/accounts'
import { openModal } from 'reducers/modals'
import { MODAL_ADD_ACCOUNTS } from 'config/constants'
import {
counterValueCurrencySelector,
localeSelector,
@ -28,11 +30,12 @@ import UpdateNotifier from 'components/UpdateNotifier'
import BalanceInfos from 'components/BalanceSummary/BalanceInfos'
import BalanceSummary from 'components/BalanceSummary'
import Box from 'components/base/Box'
import { i } from 'helpers/staticPath'
import PillsDaysCount from 'components/PillsDaysCount'
import Text from 'components/base/Text'
import OperationsList from 'components/OperationsList'
import StickyBackToTop from 'components/StickyBackToTop'
import Button from 'components/base/Button'
import AccountCard from './AccountCard'
import AccountsOrder from './AccountsOrder'
import EmptyState from './EmptyState'
@ -48,6 +51,7 @@ const mapDispatchToProps = {
push,
reorderAccounts,
saveSettings,
openModal,
}
type Props = {
@ -57,6 +61,7 @@ type Props = {
counterValue: Currency,
selectedTimeRange: TimeRange,
saveSettings: ({ selectedTimeRange: TimeRange }) => *,
openModal: string => void,
}
class DashboardPage extends PureComponent<Props> {
@ -82,11 +87,11 @@ class DashboardPage extends PureComponent<Props> {
_cacheBalance = null
render() {
const { accounts, t, counterValue, selectedTimeRange } = this.props
const { accounts, t, counterValue, selectedTimeRange, openModal } = this.props
const daysCount = timeRangeDaysByKey[selectedTimeRange]
const timeFrame = this.handleGreeting()
const imagePath = i('empty-account-tile.svg')
const totalAccounts = accounts.length
const displayOperationsHelper = (account: Account) => account.operations.length > 0
const displayOperations = accounts.some(displayOperationsHelper)
@ -155,10 +160,19 @@ class DashboardPage extends PureComponent<Props> {
style={{ margin: '0 -16px' }}
>
{accounts
.concat(Array(3 - (accounts.length % 3)).fill(null))
.concat(
Array(3 - (accounts.length % 3))
.fill(null)
.map((_, i) => i === 0),
)
.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 ? (
typeof account === 'object' ? (
<AccountCard
key={account.id}
counterValue={counterValue}
@ -166,6 +180,23 @@ class DashboardPage extends PureComponent<Props> {
daysCount={daysCount}
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}
</Box>
))}
@ -198,3 +229,12 @@ export default compose(
),
translate(),
)(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 hasMultipleDevices = devices.length > 1
// TODO: place custom wording in trans tags into yml file
/* eslint-disable react/jsx-no-literals */
return (
<Box flow={4} ff="Open Sans">
@ -215,8 +215,7 @@ class DeviceConnect extends PureComponent<Props> {
</StepIcon>
<Box grow shrink>
<Trans i18nKey="app:deviceConnect.step1.connect" parent="div">
Connect your <strong>Ledger device</strong> to your computer and enter your{' '}
<strong>PIN code</strong> on your device
Connect and unlock your <strong>Ledger device</strong>
</Trans>
</Box>
<StepCheck checked={hasDevice} />
@ -260,9 +259,9 @@ class DeviceConnect extends PureComponent<Props> {
</StepIcon>
<Box grow shrink>
<Trans i18nKey="deviceConnect:step2.open" parent="div">
{'Open '}
{'Open the '}
<strong>{currency.name}</strong>
{' App on your device'}
{' app on your device'}
</Trans>
</Box>
<StepCheck checked={appState.success} hasErrors={appState.fail} />
@ -275,8 +274,8 @@ class DeviceConnect extends PureComponent<Props> {
</WrapperIconCurrency>
</StepIcon>
<Box grow shrink>
<Trans i18nKey="deviceConnect:dashboard.open" parent="div">
{'Go to the '}
<Trans i18nKey="app:dashboard.open" parent="div">
{'Navigate to the '}
<strong>{'dashboard'}</strong>
{' on your device'}
</Trans>
@ -299,8 +298,8 @@ class DeviceConnect extends PureComponent<Props> {
</StepIcon>
<Box grow shrink>
<Trans i18nKey="deviceConnect:stepGenuine.open" parent="div">
{'Confirm '}
<strong>{'authentication'}</strong>
{'Allow the '}
<strong>{'Ledger Manager'}</strong>
{' on your device'}
</Trans>
</Box>

14
src/components/ExportLogsBtn.js

@ -6,6 +6,7 @@ import { webFrame, remote } from 'electron'
import React, { Component } from 'react'
import { translate } from 'react-i18next'
import { connect } from 'react-redux'
import KeyHandler from 'react-key-handler'
import { createStructuredSelector, createSelector } from 'reselect'
import { accountsSelector, encodeAccountsModel } from 'reducers/accounts'
import { storeSelector as settingsSelector } from 'reducers/settings'
@ -20,6 +21,7 @@ class ExportLogsBtn extends Component<{
t: *,
settings: *,
accounts: *,
hookToShortcut?: boolean,
}> {
handleExportLogs = () => {
const { accounts, settings } = this.props
@ -49,9 +51,17 @@ class ExportLogsBtn extends Component<{
}
}
onKeyHandle = e => {
if (e.ctrlKey) {
this.handleExportLogs()
}
}
render() {
const { t } = this.props
return (
const { t, hookToShortcut } = this.props
return hookToShortcut ? (
<KeyHandler keyValue="e" onKeyHandle={this.onKeyHandle} />
) : (
<Button primary onClick={this.handleExportLogs}>
{t('app:settings.exportLogs.btn')}
</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
/* eslint-disable react/jsx-no-literals */ // FIXME
import React, { PureComponent } from 'react'
import React, { PureComponent, Fragment } from 'react'
import styled from 'styled-components'
import { translate } from 'react-i18next'
@ -13,14 +13,16 @@ import installApp from 'commands/installApp'
import uninstallApp from 'commands/uninstallApp'
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 Text from 'components/base/Text'
import Progress from 'components/base/Progress'
import Spinner from 'components/base/Spinner'
import Button from 'components/base/Button'
import TranslatedError from 'components/TranslatedError'
import ExclamationCircle from 'icons/ExclamationCircle'
import ExclamationCircleThin from 'icons/ExclamationCircleThin'
import Update from 'icons/Update'
import Trash from 'icons/Trash'
import CheckCircle from 'icons/CheckCircle'
@ -53,8 +55,9 @@ type Props = {
type State = {
status: Status,
error: string | null,
appsList: LedgerScriptParams[] | Array<*>,
error: ?Error,
appsList: LedgerScriptParams[],
appsLoaded: boolean,
app: string,
mode: Mode,
}
@ -64,6 +67,7 @@ class AppsList extends PureComponent<Props, State> {
status: 'loading',
error: null,
appsList: [],
appsLoaded: false,
app: '',
mode: 'home',
}
@ -84,10 +88,10 @@ class AppsList extends PureComponent<Props, State> {
const appsList = CACHED_APPS || (await listApps.send({ targetId, version }).toPromise())
CACHED_APPS = appsList
if (!this._unmounted) {
this.setState({ appsList, status: 'idle' })
this.setState({ appsList, status: 'idle', appsLoaded: true })
}
} 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
const data = { app, devicePath, targetId }
await installApp.send(data).toPromise()
this.setState({ status: 'success', app: '' })
this.setState({ status: 'success' })
} 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()
this.setState({ status: 'success', app: '' })
} 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 = () => {
const { t } = this.props
const { app, status, error, mode } = this.state
return (
<Modal
isOpened={status !== 'idle' && status !== 'loading'}
render={() => (
<ModalBody p={6} align="center" justify="center" style={{ height: 300 }}>
<ModalBody align="center" justify="center" style={{ height: 300 }}>
{status === 'busy' || status === 'idle' ? (
<Box align="center" justify="center" flow={3}>
{mode === 'installing' ? <Update size={30} /> : <Trash size={30} />}
<Fragment>
<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">
{t(`app:manager.apps.${mode}`, { app })}
</Text>
<Box my={5} style={{ width: 250 }}>
<Box mt={6}>
<Progress style={{ width: '100%' }} infinite />
</Box>
</Box>
</ModalContent>
</Fragment>
) : status === 'error' ? (
<Box align="center" justify="center" flow={3}>
<div>{'error happened'}</div>
{error}
<Button primary onClick={this.handleCloseModal}>
close
</Button>
<Fragment>
<ModalContent grow align="center" justify="center" mt={3}>
<Box color="alertRed">
<ExclamationCircleThin size={44} />
</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' ? (
<Box align="center" justify="center" flow={3}>
<Fragment>
<ModalContent grow align="center" justify="center" mt={3}>
<Box color="positiveGreen">
<CheckCircle size={30} />
<CheckCircle size={44} />
</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(
`app:manager.apps.${
mode === 'installing' ? 'installSuccess' : 'uninstallSuccess'
}`,
{ app },
)}
</Text>
<Button primary onClick={this.handleCloseModal}>
close
</Button>
</Box>
</ModalContent>
<ModalFooter horizontal justifyContent="flex-end" style={{ width: '100%' }}>
<Button primary padded onClick={this.handleCloseModal}>
{t('app:common.close')}
</Button>
</ModalFooter>
</Fragment>
) : null}
</ModalBody>
)}
@ -175,8 +216,8 @@ class AppsList extends PureComponent<Props, State> {
}
renderList() {
const { appsList, status } = this.state
return status === 'idle' ? (
const { appsList, appsLoaded } = this.state
return appsLoaded ? (
<Box>
<AppSearchBar list={appsList}>
{items => (

3
src/components/ManagerPage/Dashboard.js

@ -2,6 +2,7 @@
import React from 'react'
import { translate } from 'react-i18next'
import { EXPERIMENTAL_FIRMWARE_UPDATE } from 'config/constants'
import type { T, Device } from 'types/common'
import Box from 'components/base/Box'
@ -34,6 +35,7 @@ const Dashboard = ({ device, deviceInfo, t }: Props) => (
</Text>
</Box>
<Box mt={5}>
{EXPERIMENTAL_FIRMWARE_UPDATE ? (
<FirmwareUpdate
infos={{
targetId: deviceInfo.targetId,
@ -41,6 +43,7 @@ const Dashboard = ({ device, deviceInfo, t }: Props) => (
}}
device={device}
/>
) : null}
</Box>
<Box mt={5}>
<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 { translate } from 'react-i18next'
import logger from 'logger'
import type { Device, T } from 'types/common'
import installFinalFirmware from 'commands/installFinalFirmware'
import Box, { Card } from 'components/base/Box'
// import Button from 'components/base/Button'
@ -18,15 +22,9 @@ type Props = {
infos: DeviceInfos,
}
type State = {
// latestFirmware: ?FirmwareInfos,
}
class FirmwareUpdate extends PureComponent<Props, State> {
state = {
// latestFirmware: null,
}
type State = {}
class FirmwareFinalUpdate extends PureComponent<Props, State> {
componentDidMount() {}
componentWillUnmount() {
@ -35,6 +33,20 @@ class FirmwareUpdate extends PureComponent<Props, State> {
_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() {
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
/* eslint-disable react/jsx-no-literals */ // FIXME
import React, { PureComponent } from 'react'
import { translate } from 'react-i18next'
import React, { PureComponent, Fragment } from 'react'
import { translate, Trans } from 'react-i18next'
import isEqual from 'lodash/isEqual'
import isEmpty from 'lodash/isEmpty'
import invariant from 'invariant'
@ -17,19 +17,28 @@ import installOsuFirmware from 'commands/installOsuFirmware'
import Box, { Card } from 'components/base/Box'
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 CheckFull from 'icons/CheckFull'
import { PreventDeviceChangeRecheck } from '../Workflow/EnsureDevice'
import UpdateFirmwareButton from './UpdateFirmwareButton'
let CACHED_LATEST_FIRMWARE = null
export const getCleanVersion = (input: string): string =>
input.endsWith('-osu') ? input.replace('-osu', '') : input
type DeviceInfos = {
targetId: number | string,
version: string,
}
type ModalStatus = 'closed' | 'disclaimer' | 'installing' | 'error' | 'success'
type Props = {
t: T,
device: Device,
@ -38,11 +47,13 @@ type Props = {
type State = {
latestFirmware: ?LedgerScriptParams,
modal: ModalStatus,
}
class FirmwareUpdate extends PureComponent<Props, State> {
state = {
latestFirmware: null,
modal: 'closed',
}
componentDidMount() {
@ -86,6 +97,7 @@ class FirmwareUpdate extends PureComponent<Props, State> {
const {
device: { path: devicePath },
} = this.props
this.setState({ modal: 'installing' })
const { success } = await installOsuFirmware
.send({ devicePath, firmware: latestFirmware, targetId: infos.targetId })
.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() {
const { infos, t } = this.props
const { latestFirmware } = this.state
const { latestFirmware, modal } = this.state
return (
<Card p={4}>
@ -122,8 +174,13 @@ class FirmwareUpdate extends PureComponent<Props, State> {
})}
</Text>
</Box>
<UpdateFirmwareButton firmware={latestFirmware} installFirmware={this.installFirmware} />
<UpdateFirmwareButton
firmware={latestFirmware}
installFirmware={this.handleInstallModal}
/>
</Box>
{modal !== 'closed' ? <PreventDeviceChangeRecheck /> : null}
{this.renderModal()}
</Card>
)
}

4
src/components/ManagerPage/UpdateFirmwareButton.js

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

9
src/components/ManagerPage/index.js

@ -1,9 +1,8 @@
// @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 Workflow from 'components/Workflow'
@ -23,7 +22,8 @@ type Error = {
stack: string,
}
function ManagerPage(): Node {
class ManagerPage extends PureComponent<*, *> {
render() {
return (
<Workflow
renderFinalUpdate={(device: Device, deviceInfo: DeviceInfo) => (
@ -53,6 +53,7 @@ function ManagerPage(): Node {
)}
/>
)
}
}
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 { genuine } = onboarding
const { cachedPinStepButton, cachedRecoveryStepButton, isGenuineCheckModalOpened } = this.state
if (genuine.displayErrorScreen) {
return this.renderGenuineFail()
}
@ -165,18 +164,21 @@ class GenuineCheck extends PureComponent<StepProps, State> {
<FixedTopContainer>
<StepContainerInner>
<Title>{t('onboarding:genuineCheck.title')}</Title>
{onboarding.isLedgerNano ? (
<Description>{t('onboarding:genuineCheck.descNano')}</Description>
{onboarding.flowType === 'restoreDevice' ? (
<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}>
<CardWrapper>
<Box justify="center">
<Box horizontal>
<IconOptionRow>{'1.'}</IconOptionRow>
<CardTitle>{t('onboarding:genuineCheck.steps.step1.title')}</CardTitle>
<CardTitle>{t('onboarding:genuineCheck.step1.title')}</CardTitle>
</Box>
</Box>
<Box justify="center">
@ -195,7 +197,7 @@ class GenuineCheck extends PureComponent<StepProps, State> {
<IconOptionRow color={!genuine.pinStepPass ? 'grey' : 'wallet'}>
{'2.'}
</IconOptionRow>
<CardTitle>{t('onboarding:genuineCheck.steps.step2.title')}</CardTitle>
<CardTitle>{t('onboarding:genuineCheck.step2.title')}</CardTitle>
</Box>
</Box>
<Box justify="center">
@ -216,7 +218,7 @@ class GenuineCheck extends PureComponent<StepProps, State> {
<IconOptionRow color={!genuine.recoveryStepPass ? 'grey' : 'wallet'}>
{'3.'}
</IconOptionRow>
<CardTitle>{t('onboarding:genuineCheck.steps.step3.title')}</CardTitle>
<CardTitle>{t('onboarding:genuineCheck.step3.title')}</CardTitle>
</Box>
</Box>
{genuine.recoveryStepPass && (

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

@ -24,17 +24,17 @@ class SelectPIN extends PureComponent<Props, *> {
{
key: 'step1',
icon: <IconOptionRow>{'1.'}</IconOptionRow>,
desc: t('onboarding:selectPIN.instructions.ledgerBlue.step1'),
desc: t('onboarding:selectPIN.initialize.instructions.blue.step1'),
},
{
key: 'step2',
icon: <IconOptionRow>{'2.'}</IconOptionRow>,
desc: t('onboarding:selectPIN.instructions.ledgerBlue.step2'),
desc: t('onboarding:selectPIN.initialize.instructions.blue.step2'),
},
{
key: 'step3',
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',
icon: <IconOptionRow>{'1.'}</IconOptionRow>,
desc: t('onboarding:selectPIN.instructions.ledgerNano.step1'),
desc: t('onboarding:selectPIN.initialize.instructions.nano.step1'),
},
{
key: 'step2',
icon: <IconOptionRow>{'2.'}</IconOptionRow>,
desc: t('onboarding:selectPIN.instructions.ledgerNano.step2'),
desc: t('onboarding:selectPIN.initialize.instructions.nano.step2'),
},
{
key: 'step3',
icon: <IconOptionRow>{'3.'}</IconOptionRow>,
desc: t('onboarding:selectPIN.instructions.ledgerNano.step3'),
desc: t('onboarding:selectPIN.initialize.instructions.nano.step3'),
},
{
key: 'step4',
icon: <IconOptionRow>{'4.'}</IconOptionRow>,
desc: t('onboarding:selectPIN.instructions.ledgerNano.step4'),
desc: t('onboarding:selectPIN.initialize.instructions.nano.step4'),
},
]
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 SelectPINnano from './SelectPINnano'
import SelectPINblue from './SelectPINblue'
import SelectPINrestoreNano from './SelectPINrestoreNano'
import SelectPINrestoreBlue from './SelectPINrestoreBlue'
import type { StepProps } from '../..'
@ -16,12 +18,21 @@ export default (props: StepProps) => {
return (
<FixedTopContainer>
{onboarding.flowType === 'restoreDevice' ? (
<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}>
{onboarding.isLedgerNano ? <SelectPINnano /> : <SelectPINblue />}
</Box>
</Box>
)}
<OnboardingFooter horizontal flow={2} t={t} nextStep={nextStep} prevStep={prevStep} />
</FixedTopContainer>
)

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

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

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

@ -30,17 +30,17 @@ class WriteSeedNano extends PureComponent<Props, *> {
{
key: 'step1',
icon: <IconOptionRow>{'1.'}</IconOptionRow>,
desc: t('onboarding:writeSeed.nano.step1'),
desc: t('onboarding:writeSeed.initialize.nano.step1'),
},
{
key: 'step2',
icon: <IconOptionRow>{'2.'}</IconOptionRow>,
desc: t('onboarding:writeSeed.nano.step2'),
desc: t('onboarding:writeSeed.initialize.nano.step2'),
},
{
key: 'step3',
icon: <IconOptionRow>{'3.'}</IconOptionRow>,
desc: t('onboarding:writeSeed.nano.step3'),
desc: t('onboarding:writeSeed.initialize.nano.step3'),
},
]
const disclaimerNotes = [
@ -69,8 +69,8 @@ class WriteSeedNano extends PureComponent<Props, *> {
return (
<Fragment>
<Box mb={3}>
<Title>{t('onboarding:writeSeed.title')}</Title>
<Description>{t('onboarding:writeSeed.desc')}</Description>
<Title>{t('onboarding:writeSeed.initialize.title')}</Title>
<Description>{t('onboarding:writeSeed.initialize.desc')}</Description>
</Box>
<Box align="center" mt={3}>
<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 IconWriteSeed from 'icons/illustrations/WriteSeed'
import type { OnboardingState } from 'reducers/onboarding'
import IconChevronRight from 'icons/ChevronRight'
@ -21,32 +22,50 @@ import {
type Props = {
t: T,
onboarding: OnboardingState,
}
class WriteSeedRestore extends PureComponent<Props, *> {
render() {
const { t } = this.props
const { t, onboarding } = this.props
const steps = [
const stepsNano = [
{
key: 'step1',
icon: <IconOptionRow>{'1.'}</IconOptionRow>,
desc: t('onboarding:writeSeed.restore.step1'),
desc: t('onboarding:writeSeed.restore.nano.step1'),
},
{
key: 'step2',
icon: <IconOptionRow>{'2.'}</IconOptionRow>,
desc: t('onboarding:writeSeed.restore.step2'),
desc: t('onboarding:writeSeed.restore.nano.step2'),
},
{
key: 'step3',
icon: <IconOptionRow>{'3.'}</IconOptionRow>,
desc: t('onboarding:writeSeed.restore.step3'),
desc: t('onboarding:writeSeed.restore.nano.step3'),
},
{
key: 'step4',
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 = [
@ -83,9 +102,15 @@ class WriteSeedRestore extends PureComponent<Props, *> {
<Box style={{ width: 260, justifyContent: 'center', alignItems: 'center' }}>
<IconWriteSeed />
</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}>
{steps.map(step => <OptionRow key={step.key} step={step} />)}
{stepsBlue.map(step => <OptionRow key={step.key} step={step} />)}
</Box>
)}
</Inner>
<DisclaimerBox mt={6} disclaimerNotes={disclaimerNotes} />
</Box>

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

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

29
src/components/TopBar/ActivityIndicator.js

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

7
src/components/Workflow/EnsureDashboard.js

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

23
src/components/Workflow/EnsureDevice.js

@ -1,5 +1,7 @@
// @flow
import { PureComponent } from 'react'
/* eslint-disable react/no-multi-comp */
import { Component, PureComponent } from 'react'
import { connect } from 'react-redux'
import type { Node } from 'react'
@ -12,9 +14,24 @@ type Props = {
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() {
const { device, children } = this.props
return children(device)

9
src/components/Workflow/WorkflowDefault.js

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

11
src/components/Workflow/WorkflowWithIcon.js

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

4
src/components/base/Ellipsis.js

@ -7,10 +7,10 @@ import Text from 'components/base/Text'
const outerStyle = { width: 0 }
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 {...p} style={outerStyle}>
<Text style={innerStyle}>{children}</Text>
<Text style={{ ...innerStyle, userSelect: canSelect ? 'text' : 'none' }}>{children}</Text>
</Box>
</Box>
)

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

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

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

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

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

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

2
src/components/layout/Default.js

@ -20,6 +20,7 @@ import SettingsPage from 'components/SettingsPage'
import LibcoreBusyIndicator from 'components/LibcoreBusyIndicator'
import DeviceBusyIndicator from 'components/DeviceBusyIndicator'
import TriggerAppReady from 'components/TriggerAppReady'
import ExportLogsBtn from 'components/ExportLogsBtn'
import AppRegionDrag from 'components/AppRegionDrag'
import IsUnlocked from 'components/IsUnlocked'
@ -75,6 +76,7 @@ class Default extends Component<Props> {
<Fragment>
<TriggerAppReady />
{process.platform === 'darwin' && <AppRegionDrag />}
<ExportLogsBtn hookToShortcut />
<IsUnlocked>
{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} />
<Box ff="Open Sans" fontSize={4} color="dark" textAlign="center" style={{ width: 370 }}>
<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} (${
currency.ticker
})`}</strong>
{` account(s) from your Ledger device. Please follow the steps below:`}
{` accounts from your Ledger device.`}
</Trans>
</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> {
componentDidMount() {
this.startScanAccountsDevice()
this.props.setState({ scanStatus: 'scanning' })
}
componentDidUpdate(prevProps: StepProps) {
@ -72,9 +72,7 @@ class StepImport extends PureComponent<StepProps> {
// TODO: use the real device
const devicePath = currentDevice.path
setState({ scanStatus: 'scanning' })
this.scanSubscription = bridge.scanAccountsOnDevice(currency, devicePath, {
this.scanSubscription = bridge.scanAccountsOnDevice(currency, devicePath).subscribe({
next: account => {
const { scannedAccounts, checkedAccountsIds, existingAccounts } = this.props
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 hasAlreadyEmptyAccount = scannedAccounts.some(a => a.operations.length === 0)
const alreadyEmptyAccount = scannedAccounts.find(a => a.operations.length === 0)
return (
<Fragment>
@ -211,8 +209,10 @@ class StepImport extends PureComponent<StepProps> {
<AccountsList
title={t('app:addAccounts.createNewAccount.title')}
emptyText={
hasAlreadyEmptyAccount
? t('app:addAccounts.createNewAccount.noOperationOnLastAccount')
alreadyEmptyAccount
? t('app:addAccounts.createNewAccount.noOperationOnLastAccount', {
accountName: alreadyEmptyAccount.name,
})
: t('app:addAccounts.createNewAccount.noAccountToCreate', { currencyName })
}
accounts={creatableAccounts}

96
src/components/modals/OperationDetails.js

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

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

@ -61,6 +61,8 @@ const INITIAL_STATE = {
stepIndex: 0,
stepsDisabled: [],
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({
@ -232,12 +234,12 @@ class ReceiveModal extends PureComponent<Props, State> {
})
}
this.setState({ addressVerified: true, stepIndex: 3 })
this.handleCheckAddress(true)
} else {
this.setState({ addressVerified: false })
this.handleCheckAddress(false)
}
} 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 ReactMarkdown from 'react-markdown'
import styled from 'styled-components'
import axios from 'axios'
import network from 'api/network'
import { MODAL_RELEASES_NOTES } from 'config/constants'
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 Text from 'components/base/Text'
import Spinner from 'components/base/Spinner'
import GradientBox from 'components/GradientBox'
import type { T } from 'types/common'
@ -159,8 +160,10 @@ class ReleaseNotes extends PureComponent<Props, State> {
if (!this.loading) {
this.loading = true
axios
.get(`https://api.github.com/repos/LedgerHQ/ledger-live-desktop/releases/tags/v${version}`)
network({
method: 'GET',
url: `https://api.github.com/repos/LedgerHQ/ledger-live-desktop/releases/tags/v${version}`,
})
.then(response => {
const { body } = response.data
@ -218,7 +221,7 @@ class ReleaseNotes extends PureComponent<Props, State> {
return (
<ModalBody onClose={onClose}>
<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}>
{content}
</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)

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

@ -57,6 +57,7 @@ type Step = {
canNext: (State<*>) => boolean,
canPrev: (State<*>) => boolean,
canClose: (State<*>) => boolean,
hasError: (State<*>) => boolean,
prevStep?: number,
}
@ -91,6 +92,7 @@ class SendModal extends Component<Props, State<*>> {
bridge && account && transaction
? bridge.isValidTransaction(account, transaction)
: false,
hasError: () => false,
},
{
label: t('app:send.steps.connectDevice.title'),
@ -99,6 +101,7 @@ class SendModal extends Component<Props, State<*>> {
deviceSelected !== null && appStatus === 'success',
prevStep: 0,
canPrev: () => true,
hasError: () => false,
},
{
label: t('app:send.steps.verification.title'),
@ -106,6 +109,7 @@ class SendModal extends Component<Props, State<*>> {
canNext: () => true,
canPrev: ({ error }) => !!error,
prevStep: 0,
hasError: ({ error }) => (error && error.name === 'UserRefusedOnDevice') || false,
},
{
label: t('app:send.steps.confirmation.title'),
@ -113,6 +117,7 @@ class SendModal extends Component<Props, State<*>> {
canClose: () => true,
canPrev: () => true,
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 canPrev = step.canPrev(this.state)
const stepsErrors = []
this.steps.forEach((s, i) => {
if (s.hasError(this.state)) {
stepsErrors.push(i)
}
})
return (
<Modal
name={MODAL_SEND}
@ -290,7 +302,13 @@ class SendModal extends Component<Props, State<*>> {
</ModalTitle>
<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}>
<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_BOOT_DELAY = 2 * 1000
export const SYNC_ALL_INTERVAL = 60 * 1000
export const GENUINE_TIMEOUT = intFromEnv('GENUINE_TIMEOUT', 60 * 1000)
export const SYNC_ALL_INTERVAL = 120 * 1000
export const GENUINE_TIMEOUT = intFromEnv('GENUINE_TIMEOUT', 120 * 1000)
export const SYNC_TIMEOUT = intFromEnv('SYNC_TIMEOUT', 30 * 1000)
export const CHECK_APP_INTERVAL_WHEN_INVALID = 600
export const CHECK_APP_INTERVAL_WHEN_VALID = 1200
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...
export const LEDGER_COUNTERVALUES_API = stringFromEnv(
'LEDGER_COUNTERVALUES_API',
'https://ledger-countervalue-poc.herokuapp.com',
'https://beta.manager.live.ledger.fr/countervalues',
)
export const LEDGER_REST_API_BASE = stringFromEnv(
'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 HIGHLIGHT_I18N = boolFromEnv('HIGHLIGHT_I18N')
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

15
src/helpers/apps/installApp.js

@ -7,6 +7,19 @@ import { createDeviceSocket } from 'helpers/socket'
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
*/
@ -21,5 +34,5 @@ export default async function installApp(
firmwareKey: app.firmware_key,
}
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
import axios from 'axios'
import network from 'api/network'
import { APPLICATIONS_BY_DEVICE } from 'helpers/urls'
import getDeviceVersion from 'helpers/devices/getDeviceVersion'
@ -17,7 +17,7 @@ export default async (targetId: string | number, version: string) => {
}
const {
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 : []
} catch (err) {
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 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
@ -22,5 +34,5 @@ export default async function uninstallApp(
firmwareKey: app.delete_key,
}
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
import axios from 'axios'
import network from 'api/network'
import { GET_CURRENT_FIRMWARE } from 'helpers/urls'
@ -12,10 +12,14 @@ let error
export default async (input: Input): Promise<*> => {
try {
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,
version_name: input.version,
provider,
},
})
return data
} catch (err) {

15
src/helpers/devices/getDeviceVersion.js

@ -1,19 +1,16 @@
// @flow
import axios from 'axios'
import { GET_DEVICE_VERSION } from 'helpers/urls'
import network from 'api/network'
export default async (targetId: string | number): Promise<*> => {
try {
const provider = 1
const { data } = await axios.post(GET_DEVICE_VERSION, {
const { data } = await network({
method: 'POST',
url: GET_DEVICE_VERSION,
data: {
provider,
target_id: targetId,
},
})
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
import axios from 'axios'
import network from 'api/network'
import { GET_LATEST_FIRMWARE } from 'helpers/urls'
import getCurrentFirmware from './getCurrentFirmware'
@ -21,10 +21,14 @@ export default async (input: Input) => {
const seFirmwareVersion = await getCurrentFirmware({ version, deviceId: deviceVersion.id })
// 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,
device_version: deviceVersion.id,
provider,
},
})
if (data.result === 'null') {

8
src/helpers/devices/getNextMCU.js

@ -1,5 +1,5 @@
// @flow
import axios from 'axios'
import network from 'api/network'
import { GET_NEXT_MCU } from 'helpers/urls'
import createCustomErrorClass from 'helpers/createCustomErrorClass'
@ -8,8 +8,12 @@ const LatestMCUInstalledError = createCustomErrorClass('LatestMCUInstalledError'
export default async (bootloaderVersion: string): Promise<*> => {
try {
const { data } = await axios.post(GET_NEXT_MCU, {
const { data } = await network({
method: 'POST',
url: GET_NEXT_MCU,
data: {
bootloader_version: bootloaderVersion,
},
})
// 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 { 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 = *
export default async (transport: Transport<*>, firmware: Input): Result => {
export default async (transport: Transport<*>, app: Input): Result => {
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()
return { success: true }
} catch (err) {
const error = Error(err.message)
error.stack = err.stack
} catch (error) {
const result = { success: false, error }
throw result
}

1
src/helpers/firmware/installMcu.js

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

4
src/helpers/firmware/installOsuFirmware.js

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

4
src/helpers/hardReset.js

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

2
src/helpers/urls.js

@ -14,9 +14,11 @@ const wsURLBuilder = (endpoint: string) => (params?: Object) =>
// const wsURLBuilderProxy = (endpoint: string) => (params?: Object) =>
// `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 APPLICATIONS_BY_DEVICE: string = managerUrlbuilder('get_apps')
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_NEXT_MCU: string = managerUrlbuilder('mcu_versions_bootloader')

1
src/icons/Trash.js

@ -5,6 +5,7 @@ import React from 'react'
const path = (
<g transform="translate(670.57 190.38)">
<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"
strokeWidth="1.2"
/>

15
src/index.ejs

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

317
static/i18n/en/app.yml

@ -1,5 +1,5 @@
common:
ok: Okay
ok: OK
yes: Yes
no: No
apply: Apply
@ -7,12 +7,12 @@ common:
cancel: Cancel
delete: Delete
continue: Continue
skipThisStep: Skip This Step
skipThisStep: Skip this step
chooseWalletPlaceholder: Choose a wallet...
currency: Currency
selectAccount: Select an account
selectAccountNoOption: 'No account matching "{{accountName}}"'
selectCurrency: Select a currency
selectCurrency: Choose a crypto asset
selectCurrencyNoOption: 'No currency matching "{{currencyName}}"'
selectExchange: Select an exchange
selectExchangeNoOption: 'No exchange matching "{{exchangeName}}"'
@ -21,7 +21,7 @@ common:
save: Save
password: Password
editProfile: Edit profile
lockApplication: Lock application
lockApplication: Lock Ledger Live
showMore: Show more
max: Max
next: Next
@ -32,15 +32,15 @@ common:
eastern: Eastern
western: Western
lockScreen:
title: Welcome Back
subTitle: Your application is locked
description: Please enter your password to continue
inputPlaceholder: Type your password
title: Welcome back
subTitle: Ledger Live is locked
description: Enter your password to continue
inputPlaceholder:
lostPassword: I lost my password
sync:
syncing: Syncing...
upToDate: Up to date
error: Sync error.
syncing: Synchronizing...
upToDate: Live
error: Synchronization error
refresh: Refresh
ago: Synced {{time}}
error:
@ -48,8 +48,12 @@ common:
noResults: No results
operation:
type:
IN: Receive funds
OUT: Sent funds
IN: Received
# conf: Received
# unconf: Receiving...
OUT: Sent
# conf: Sent
# unconf: Sending...
time:
day: Day
week: Week
@ -70,57 +74,61 @@ account:
receive: Receive
lastOperations: Last operations
emptyState:
title: This is a title, use it with caution
desc: Please create a new account or recover an old account from your Ledger device.
title: No funds yet?
desc: Make sure the [cryptocurrency] app is installed to receive funds. # replace [cryptocurrency] and make it bold
buttons:
receiveFunds: Receive Funds
receiveFunds: Receive funds
settings:
title: Edit Account
title: Edit account
advancedLogs: Advanced logs
accountName:
title: Account name
desc: Lorem ipsum dolort amet
error: Name is required
desc: Describe this account.
error: An account name is required.
unit:
title: Unit
desc: Lorem ipsum dolort amet
desc: Choose the unit to display.
endpointConfig:
title: Node
desc: The API node to use
error: Invalid endpoint
dashboard:
title: Dashboard
title: Portfolio
emptyAccountTile:
desc: Lorem ipsum dolor sit amet, consectetur adipiscing elit
createAccount: Create Account
accounts:
title: Accounts ({{count}})
greeting:
morning: "Good Morning!"
evening: "Good Evening!"
afternoon: "Good Afternoon!"
summary: here is the summary of your account
summary_plural: 'here is the summary of your {{count}} accounts'
noAccounts: no accounts
morning: "Good morning"
evening: "Good evening"
afternoon: "Good afternoon"
summary: "Here's the summary of your account."
summary_plural: "Here's the summary of your {{count}} accounts."
noAccounts: No accounts yet
recentActivity: Recent activity
totalBalance: Total balance
accountsOrder:
name: Alphabetic
balance: Balance
name: name
balance: balance
currentAddress:
title: Current address
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:
step1:
choose: "We detected {{count}} devices connected, please select one:"
connect: Connect your <1>Ledger device</1> to your computer and enter your <3>PIN code</3> on your device
choose: "We detected {{count}} connected devices, please select one:"
connect: Connect and unlock your <1>Ledger device</1> # remove key: <3>PIN code</3>
dashboard: test
emptyState:
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:
title: This is a title, use it with caution
desc: Please create a new account or recover an old account from your Ledger device.
title: 'Let’s set up your portfolio!'
desc: Open the Manager to install apps on your device or add accounts if your device already has apps installed.
buttons:
addAccount: Add Account
installApp: Install App
addAccount: Add accounts
installApp: Open Manager
exchange:
title: Exchange
visitWebsite: Visit website
@ -133,28 +141,29 @@ genuinecheck:
addAccounts:
title: Add accounts
breadcrumb:
informations: Informations
informations: Choose asset
connectDevice: Connect device
import: Import
finish: End
accountToImportSubtitle: Account to import
accountToImportSubtitle_plural: 'Accounts to import ({{count}})'
import: Select accounts
finish: Confirmation
accountToImportSubtitle: Select existing accounts
accountToImportSubtitle_plural: 'Select ({{count}}) existing accounts'
selectAll: Select all
unselectAll: Unselect all
unselectAll: Deselect all
editName: Edit name
newAccount: New account
legacyAccount: '{{accountName}} (legacy)'
noAccountToImport: We didnt find any {{currencyName}} account to import.
success: Great success!
noAccountToImport: All {{currencyName}} accounts found are already in your portfolio.
success: Account successfully added to your portfolio.
# success_plural: Accounts successfully added to your portfolio.
createNewAccount:
title: Create new account
noOperationOnLastAccount: You cannot create a new account because your last account has no operations
noAccountToCreate: We didnt find any {{currencyName}} account to create.
somethingWentWrong: Something went wrong during synchronization.
noOperationOnLastAccount: 'You have to receive funds on {{accountName}} before you can create a new account.'
noAccountToCreate: No {{currencyName}} account was found to create.
somethingWentWrong: Something went wrong during synchronization, please try again.
cta:
create: 'Create account'
import: 'Import account'
import_plural: 'Import accounts'
create: 'Add account'
import: 'Add account' # Remove
import_plural: 'Add accounts'
operationDetails:
title: Operation details
account: Account
@ -165,11 +174,12 @@ operationDetails:
fees: Fees
from: From
to: To
identifier: Hash
viewOperation: View operation
showMore: See {{recipients}} more
identifier: Transaction ID
viewOperation: View in explorer
showMore: Show {{recipients}} more
showLess: Show less
operationList:
noMoreOperations: No more operations
noMoreOperations: That's all!
manager:
tabs:
apps: Apps
@ -182,50 +192,54 @@ manager:
installSuccess: '{{app}} app successfully installed'
uninstallSuccess: '{{app}} app successfully uninstalled'
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:
installed: 'Firmware {{version}}'
installed: 'Firmware version {{version}}'
update: Update firmware
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
subtitle: Get all your apps here
subtitle: Install apps or update your device.
device:
title: Plug your device
desc: Please connect your Ledger device and follow the steps below to access the manager
cta: Plug my device
title: Connect your device
desc: Follow the steps below to use the Manager
cta: Connect my device
errors:
noDevice: Please make sur your device is connected (TEMPLATE NEEDED)
noDashboard: Please make sure your device is on the dashboard screen (TEMPLATED NEEDED)
noGenuine: You did not approve request on your device or your device is not genuine (TEMPLATE NEEDED)
noDevice: No device is connected (TEMPLATE NEEDED)
noDashboard: Navigate to the dashboard on your device (TEMPLATED NEEDED)
noGenuine: Allow the Manager to continue (TEMPLATE NEEDED)
receive:
title: Receive funds
steps:
chooseAccount:
title: Choose Account
title: Choose account
label: Account
connectDevice:
title: Connect Device
withoutDevice: I don't have my device
title: Connect device
withoutDevice: Proceed without device
confirmAddress:
title: Confirm Address
title: Confirm address
action: Confirm address on device
text: To receive funds, confirm the address on your device.
support: Contact Support
support: Ledger Support
error:
title: Houston, we have a problem!
text: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer non nibh diam. In eget ipsum arcu donec finibus
title: Receive address rejected
text: Please try again or request Ledger Support assistance when in doubt.
receiveFunds:
title: Receive Funds
label: Amount (Optional)
title: Receive funds
label: Amount (optional)
send:
title: Send funds
totalSpent: Total spent
totalSpent: Total
steps:
amount:
title: Informations
title: Create payment
selectAccountDebit: Select an account to debit
recipientAddress: Recipient address
recipientAddress: Recipient address # can't control the tooltip!
amount: Amount
max: Max
fees: Fees
@ -235,25 +249,24 @@ send:
rippleTag: Tag
ethereumGasLimit: Gas limit
unitPerByte: '{{unit}} per byte'
feePerByte: Fee per byte
feePerByte: Fees per byte
connectDevice:
title: Connect device
verification:
title: Verification
warning: |
You are about to validate a transaction.
Be careful, we strongly recommand you to verify that the informations on your Ledger device are correct.
body: Once you have checked everything is ok, you can validate securely the transaction on your device.
Carefully verify the transaction details on your device. Press the left button on your device to cancel.
body: Press the right button to confirm the transaction.
confirmation:
title: Confirmation
success:
title: Transaction successfully broadcasted
title: Transaction sent
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
error:
title: Transaction error
cta: Retry operation
title: Transaction canceled
cta: Retry
pending:
title: Broadcasting transaction...
releaseNotes:
@ -265,73 +278,73 @@ settings:
display: Display
currencies: Currencies
profile: Profile
about: About
about: Help
display:
desc: Lorem ipsum dolor sit amet
language: Interface language
languageDesc: Lorem ipsum dolor sit amet
counterValue: Countervalue
counterValueDesc: Lorem ipsum dolor sit amet
exchange: Exchange ({{ticker}})
exchangeDesc: The exchange to use for countervalue conversion
desc:
language: Language
languageDesc: Choose the language to display.
counterValue: Base currency
counterValueDesc: Choose the currency to display next to your balance and operations.
exchange: Rate provider ({{ticker}})
exchangeDesc: Choose the provider of the base currency exchange rates.
region: Region
regionDesc: Lorem ipsum dolor sit amet
stock: Stock market indicators
stockDesc: Lorem ipsum dolor sit amet
regionDesc: Choose the region in which you’re located to set the application’s time zone.
stock: Regional market indicator
stockDesc: Choose Western to display an increase in market value in blue. Choose Eastern to display an increase in market value in red.
currencies:
desc: Lorem ipsum dolor sit amet
exchange: Exchange ({{ticker}})
exchangeDesc: The exchange to use for countervalue conversion
confirmationsToSpend: Confirmations to spend
confirmationsToSpendDesc: Lorem ipsum dolor sit amet
desc: Select a cryptocurrency to edit its settings.
exchange: Rate provider ({{ticker}})
exchangeDesc: Choose the provider of the base currency exchange rates.
confirmationsToSpend: Number of confirmations required to spend
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
confirmationsNbDesc: Lorem ipsum dolor sit amet
transactionsFees: Transactions fees
transactionsFeesDesc: 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: Default transaction fees
transactionsFeesDesc: Select your default transaction fees. The higher the fee, the quicker the transaction will be processed.
explorer: Blockchain explorer
explorerDesc: Lorem ipsum dolor sit amet
explorerDesc: Which service to use to look up an operation in the blockchain.
profile:
desc: Lorem ipsum dolor sit amet
password: Password
passwordDesc: Lorem ipsum dolor sit amet
desc:
password: Data encryption
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
sync: Sync accounts
syncDesc: Lorem ipsum dolor sit amet
sync: Synchronize accounts
syncDesc: Resynchronize your accounts with the blockchain.
export: Export logs
exportDesc: Lorem ipsum dolor sit amet
softResetTitle: Clean application cache
softResetDesc: Lorem ipsum dolor sit amet
softReset: Clean cache
hardResetTitle: Reset application
hardResetDesc: Lorem ipsum dolor sit amet
hardReset: Hard reset
developerMode: Developer Mode
developerModeDesc: Enable visibility of developer apps & currencies like Bitcoin Testnet
analytics: Share analytics
analyticsDesc: Help Ledger improve its products and services by automatically sending diagnostics and usage data.
reportErrors: Sentry Logs
reportErrorsDesc: Help Ledger improve its products and services by automatically sending diagnostics and usage data.
exportDesc: Exporting Ledger Live logs may be necessary for troubleshooting purposes.
softResetTitle: Clear cache
softResetDesc: Clear the Ledger Live cache to force resynchronization with the blockchain.
softReset: Clear
hardResetTitle: Reset Ledger Live
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: Reset
developerMode: Developer mode
developerModeDesc: Show developer apps in the Manager.
analytics: Analytics
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: Usage and diagnostics
reportErrorsDesc: Share anonymous usage and diagnostics data to help improve Ledger products, services and security features.
about:
desc: Lorem ipsum dolor sit amet
version: Version
releaseNotesBtn: Show release notes
faq: FAQ
faqDesc: Lorem ipsum dolor sit amet
desc:
version: Ledger Live version
releaseNotesBtn: Show release notes # Close button instead of continue.
faq: Ledger Support
faqDesc: A problem? Learn about Ledger Live, Ledger devices, supported crypto assets and apps.
contactUs: Contact us
contactUsDesc: Lorem ipsum dolor sit amet
terms: Terms and Privacy policy
termsDesc: Lorem ipsum dolor sit amet
contactUsDesc: Need help? Request assistance from Ledger Support by email or chat.
terms: --- Terms and Privacy policy ---
termsDesc: --- Check with Legal ---
hardResetModal:
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.
softResetModal:
title: Clean application cache
subTitle: Are you sure houston?
desc: Lorem ipsum dolor sit amet
title: Clear cache
subTitle: Are you sure?
desc: Clearing the Ledger Live cache forces resynchronization with the blockchain.
removeAccountModal:
title: Delete this account
subTitle: Are you sure houston?
desc: Lorem ipsum dolor sit amet
title: Remove account
subTitle: Are you sure?
desc: The account will no longer be included in your portfolio. Accounts can always be re-added.
exportLogs:
title: Export Logs
desc: Export Logs
@ -347,32 +360,32 @@ password:
inputFields:
newPassword:
label: Password
placeholder: Password
placeholder:
confirmPassword:
label: Confirm Password
placeholder: Confirm Password
label: Confirm password
placeholder:
currentPassword:
label: Current Password
placeholder: Current Password
label: Current password
placeholder:
changePassword:
title: Edit Password
title: Data encryption
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:
title: Set Password
subTitle: Set a password to lock your application
desc: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer non nibh diam. In eget ipsum arcu donec finibus
title: Enable data encryption
subTitle: Set a password
desc: Make sure to remember your password. Losing your password requires resetting Ledger Live and re-adding accounts.
disablePassword:
title: Disable Password
desc: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer non nibh diam.
title: Disable data encryption
desc: Ledger Live data will be stored unencrypted on your computer. This includes account names, balances, transactions and public addresses.
update:
newVersionReady: A new update is available.
relaunch: Update now
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.
restart: Restart app
restart: Restart
reset: Hard reset
createTicket: Create issue
createTicket: Ledger Support
showDetails: Show details
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}}'
Error: '{{message}}'
LedgerAPIErrorWithMessage: '{{message}}'
TransportStatusError: '{{message}}'
TimeoutError: 'Timeout reached'
FeeEstimationFailed: 'fee estimation failed (status: {{status}})'
NotEnoughBalance: 'Not enough balance'
BtcUnmatchedApp: 'You must open application ‘{{currencyName}}’ on the device'
WrongAppOpened: 'You must open application ‘{{currencyName}}’ on the device'
WrongDeviceForAccount: 'You must use the device associated to the account ‘{{accountName}}’'
LedgerAPINotAvailable: 'Ledger API is not available for currency {{currencyName}}'
LedgerAPIError: 'A problem occurred with Ledger API. Please try again later. (HTTP {{status}})'
NetworkDown: 'Your internet connection seems down. Please try again later.'
NoAddressesFound: 'No accounts found'
UserRefusedOnDevice: Transaction have been aborted
WebsocketConnectionError: An error occurred with the socket connection
WebsocketConnectionFailed: Failed to establish a socket connection
DeviceSocketFail: Device socket failure
DeviceSocketNoBulkStatus: Device socket failure (bulk)
DeviceSocketNoHandler: Device socket failure (handler {{query}})
LatestMCUInstalledError: The latest MCU is already installed on the Device
TimeoutError: 'The request timed out. Please try again or contact Ledger Support.'
FeeEstimationFailed: 'The fee could not be estimated. Please try again or set a custom fee (status: {{status}})'
NotEnoughBalance: 'The account has insufficient funds to proceed.'
BtcUnmatchedApp: 'Open the ‘{{currencyName}}’ app on your Ledger device to proceed.'
WrongAppOpened: 'Open the ‘{{currencyName}}’ app on your Ledger device to proceed.'
WrongDeviceForAccount: 'Use the device associated with the account ‘{{accountName}}’.'
LedgerAPINotAvailable: 'The Ledger API is not available for {{currencyName}}. Please check status.ledger.fr.'
LedgerAPIError: 'A problem occurred with the Ledger API. Please try again later. (HTTP {{status}})'
NetworkDown: 'Your internet connection seems down. Please try again.'
NoAddressesFound: 'No accounts were found.'
UserRefusedOnDevice: Please try again or request Ledger Support assistance when in doubt.
WebsocketConnectionError: An error occurred with the websocket connection. Please try again or contact Ledger Support.
WebsocketConnectionFailed: Oops, could not establish a websocket connection. Please try again or contact Ledger Support.
DeviceSocketFail: Oops. a device socket failure occurred. Please try again or contact Ledger Support.
DeviceSocketNoBulkStatus: Oops, the device socket failed (bulk). Please try again or contact Ledger Support.
DeviceSocketNoHandler: Oops, the device socket failed (handler {{query}}). Please try again or contact Ledger Support.
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:
title: Ledger Blue
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
instructions:
ledgerNano:
nano:
step1: Connect the Ledger Nano S to your computer.
step2: Press both buttons simultaneously as instructed on the screen.
step3: Press the right button to select Configure as new device?. # <bold>Configure as new device?<bold>.
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.
step2: Tap on Configure as new device.
step3: Choose a PIN code between 4 and 8 digits long.
# restore:
# title: Start restoration - Choose your PIN code
# instructions:
# nano:
# step1: Connect the Ledger Nano S to your computer.
# step2: Press both buttons simultaneously as instructed on the screen.
# step3: Press the left button to cancel Initialize as new device?. Press the right button to select Restore configuration?. # <bold>Initialize as new device?</bold> <bold>Restore configuration?</bold>.
# step4: 'Choose a PIN code between 4 and 8 digits long. Then select the checkmark (✓).'
# blue:
# step1: Connect the Ledger Blue to your computer.
# step2: Tap on Restore configuration. # <bold>Restore configuration</bold>.
# step3: Choose a PIN code between 4 and 8 digits long.
disclaimer:
note1: Choose your own PIN code. This code 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.
restore:
title: Start restoration - Choose your PIN code
instructions:
nano:
step1: Connect the Ledger Nano S to your computer.
step2: Press both buttons simultaneously as instructed on the screen.
step3: Press the left button to cancel Initialize as new device?. Press the right button to select Restore configuration?. # <bold>Initialize as new device?</bold> <bold>Restore configuration?</bold>.
step4: 'Choose a PIN code between 4 and 8 digits long. Then select the checkmark (✓).'
blue:
step1: Connect the Ledger Blue to your computer.
step2: Tap on Restore configuration. # <bold>Restore configuration</bold>.
step3: Choose a PIN code between 4 and 8 digits long.
writeSeed:
initialize:
title: Save your recovery phrase
desc: Your device will generate a recovery phrase of 24 words, displayed only once.
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>
step3: 'Select Word #1 from the suggested words. Press both buttons to continue.' # <bold>Word #1</bold>
step4: Repeat the process until the last word.
ledgerBlue:
blue:
step1: Select the length of your recovery phrase.
step2: Type the first word of your recovery phrase. Select the word when it appears.
step3: Repeat the process until the last word.
@ -92,17 +93,15 @@ writeSeed:
genuineCheck:
title: Final security check
descNano: Your Ledger Nano S should now display Your device is now ready. Before getting started, please confirm that
descBlue: #Your Ledger Blue should now display Your device is now ready. Before getting started, please confirm that
steps:
descBlue: Your Ledger Blue should now display Your device is now ready. Before getting started, please confirm that
descRestore: Before getting started, please confirm that
step1:
title: Did you choose your PIN code by yourself?
step2:
title: Did you save your recovery phrase by yourself?
desc:
step3:
title: Check if your Ledger device is genuine
desc:
isGenuinePassed: 'Genuine'
title: Do you have a genuine Ledger device?
isGenuinePassed: Your device is genuine
buttons:
genuineCheck: Genuine check
contactSupport: Ledger Support
@ -119,7 +118,7 @@ setPassword:
disclaimer:
note1: Make sure to remember your password. Do not share it.
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
confirmPassword: Confirm password
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.
shareAnalytics:
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:
title: Report bugs
desc: Automatically send bug reports to help Ledger developers diagnose issues and improve Ledger Live performance.
finish:
title: 'Ready for launch!'
desc: The value of crypto assets can go up or down. Balances shown in your portfolio may involve double conversions and are for indicative purposes only!
title: Welcome to Ledger Live
desc: The unified crypto portfolio, backed by the security of your Ledger device.
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:
events "^2.0.0"
"@ledgerhq/ledger-core@2.0.0-rc.1":
version "2.0.0-rc.1"
resolved "https://registry.yarnpkg.com/@ledgerhq/ledger-core/-/ledger-core-2.0.0-rc.1.tgz#0b31f7d2c693b9c11d4093dbb0896f13c33bf141"
"@ledgerhq/ledger-core@2.0.0-rc.3":
version "2.0.0-rc.3"
resolved "https://registry.yarnpkg.com/@ledgerhq/ledger-core/-/ledger-core-2.0.0-rc.3.tgz#21b04239e9ba6b7fdcb89958eea8ad47a4a28a88"
dependencies:
"@ledgerhq/hw-app-btc" "^4.7.3"
"@ledgerhq/hw-transport-node-hid" "^4.7.6"
@ -9322,6 +9322,10 @@ meant@~1.0.1:
version "1.0.1"
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:
version "0.3.0"
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"
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:
version "3.0.4"
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"

Loading…
Cancel
Save