Browse Source

Merge branch 'develop' into auto-lock

gre-patch-1
Juan Cortés Ross 6 years ago
committed by GitHub
parent
commit
36871534dc
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 13
      .circleci/config.yml
  2. 1
      .eslintrc
  3. 2
      .github/ISSUE_TEMPLATE/feature_request.md
  4. 3
      .gitignore
  5. 1
      .prettierignore
  6. 82
      babel.config.js
  7. 58
      package.json
  8. 50
      scripts/check-wordings.js
  9. 9
      scripts/cli/cli.sh
  10. 21
      scripts/cli/getDevice.js
  11. 104
      scripts/cli/txBetweenAccounts.js
  12. 85
      scripts/create-draft-release.js
  13. 17
      scripts/download-xliffs.sh
  14. 8
      scripts/helpers/display-env.sh
  15. 49
      scripts/patch-appimage.sh
  16. 78
      scripts/release.sh
  17. 5
      scripts/shasums/patch-appimage-sums.txt
  18. 61
      scripts/upload-github-release-asset.sh
  19. 2
      src/__mocks__/storybook-state.js
  20. 2
      src/actions/general.js
  21. 5
      src/actions/settings.js
  22. 2
      src/api/Ripple.js
  23. 4
      src/api/network.js
  24. 9
      src/bridge/BridgeSyncContext.js
  25. 103
      src/bridge/EthereumJSBridge.js
  26. 78
      src/bridge/LibcoreBridge.js
  27. 150
      src/bridge/RippleJSBridge.js
  28. 4
      src/bridge/UnsupportedBridge.js
  29. 1
      src/bridge/index.js
  30. 12
      src/bridge/makeMockBridge.js
  31. 7
      src/bridge/types.js
  32. 2
      src/commands/debugAppInfosForCurrency.js
  33. 2
      src/commands/getAddress.js
  34. 9
      src/commands/index.js
  35. 18
      src/commands/killInternalProcess.js
  36. 39
      src/commands/libcoreGetFees.js
  37. 19
      src/commands/libcoreHardReset.js
  38. 29
      src/commands/libcoreScanFromXPUB.js
  39. 123
      src/commands/libcoreSignAndBroadcast.js
  40. 20
      src/commands/libcoreSyncAccount.js
  41. 8
      src/components/AccountPage/AccountHeaderActions.js
  42. 6
      src/components/AccountPage/EmptyStateAccount.js
  43. 4
      src/components/AccountPage/index.js
  44. 8
      src/components/AdvancedOptions/BitcoinKind.js
  45. 4
      src/components/AdvancedOptions/EthereumKind.js
  46. 53
      src/components/AdvancedOptions/RippleKind.js
  47. 14
      src/components/BalanceSummary/BalanceInfos.js
  48. 2
      src/components/BalanceSummary/index.js
  49. 2
      src/components/BalanceSummary/stories.js
  50. 2
      src/components/CalculateBalance.js
  51. 2
      src/components/CounterValue/stories.js
  52. 181
      src/components/CurrenciesStatusBanner.js
  53. 68
      src/components/CurrencyDownStatusAlert.js
  54. 57
      src/components/CurrentAddress/index.js
  55. 2
      src/components/DashboardPage/AccountCard/index.js
  56. 1
      src/components/DashboardPage/AccountCardList.js
  57. 4
      src/components/DashboardPage/AccountCardListHeader.js
  58. 6
      src/components/DashboardPage/AccountCardPlaceholder.js
  59. 8
      src/components/DashboardPage/AccountsOrder.js
  60. 8
      src/components/DashboardPage/CurrentGreetings.js
  61. 8
      src/components/DashboardPage/EmptyState.js
  62. 9
      src/components/DashboardPage/SummaryDesc.js
  63. 2
      src/components/DashboardPage/index.js
  64. 287
      src/components/DevToolsPage/AccountImporter.js
  65. 11
      src/components/DevToolsPage/index.js
  66. 4
      src/components/DeviceInteraction/components.js
  67. 21
      src/components/EnsureDeviceApp.js
  68. 4
      src/components/ExchangePage/ExchangeCard.js
  69. 46
      src/components/ExchangePage/index.js
  70. 2
      src/components/ExportLogsBtn.js
  71. 34
      src/components/FeesField/BitcoinKind.js
  72. 9
      src/components/FeesField/EthereumKind.js
  73. 2
      src/components/FeesField/GenericContainer.js
  74. 9
      src/components/FeesField/RippleKind.js
  75. 8
      src/components/GenuineCheck.js
  76. 2
      src/components/GenuineCheckModal.js
  77. 2
      src/components/GlobalSearch.js
  78. 18
      src/components/IsUnlocked.js
  79. 2
      src/components/MainSideBar/AddAccountButton.js
  80. 72
      src/components/MainSideBar/index.js
  81. 2
      src/components/ManagerPage/AppSearchBar.js
  82. 79
      src/components/ManagerPage/AppsList.js
  83. 6
      src/components/ManagerPage/Dashboard.js
  84. 8
      src/components/ManagerPage/FirmwareUpdate.js
  85. 26
      src/components/ManagerPage/ManagerApp.js
  86. 4
      src/components/ManagerPage/ManagerGenuineCheck.js
  87. 6
      src/components/ManagerPage/PlugYourDevice.js
  88. 4
      src/components/ManagerPage/UpdateFirmwareButton.js
  89. 7
      src/components/ManagerPage/index.js
  90. 4
      src/components/Onboarding/OnboardingFooter.js
  91. 13
      src/components/Onboarding/helperComponents.js
  92. 68
      src/components/Onboarding/steps/Analytics.js
  93. 6
      src/components/Onboarding/steps/Finish.js
  94. 16
      src/components/Onboarding/steps/GenuineCheck/GenuineCheckErrorPage.js
  95. 8
      src/components/Onboarding/steps/GenuineCheck/GenuineCheckUnavailable.js
  96. 20
      src/components/Onboarding/steps/GenuineCheck/index.js
  97. 10
      src/components/Onboarding/steps/Init.js
  98. 10
      src/components/Onboarding/steps/NoDevice.js
  99. 6
      src/components/Onboarding/steps/SelectDevice.js
  100. 12
      src/components/Onboarding/steps/SelectPIN/SelectPINblue.js

13
.circleci/config.yml

@ -9,18 +9,25 @@ jobs:
build: build:
<<: *defaults <<: *defaults
steps: steps:
- run: sudo apt-get update
- run: sudo apt-get install -y libudev-dev - run: sudo apt-get install -y libudev-dev
- run:
name: Install latest yarn
command: |
curl -sS http://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - ;
echo "deb http://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list ;
sudo apt-get update && sudo apt-get install yarn
sudo rm /usr/local/bin/yarn # remove docker yarn
- checkout - checkout
- restore_cache: - restore_cache:
keys: keys:
- v7-yarn-packages-{{ checksum "yarn.lock" }} - v12-yarn-packages-{{ checksum "yarn.lock" }}
- run: yarn install - run: yarn install
- save_cache: - save_cache:
key: v7-yarn-packages-{{ checksum "yarn.lock" }} key: v12-yarn-packages-{{ checksum "yarn.lock" }}
paths: paths:
- node_modules - node_modules
- run: yarn lint - run: yarn lint
- run: ./node_modules/.bin/prettier -l "{src,webpack,.storybook,static/i18n}/**/*.js" - run: ./node_modules/.bin/prettier -l "{src,webpack,.storybook,static/i18n}/**/*.js"
- run: yarn flow --quiet - run: yarn flow --quiet
- run: yarn test - run: yarn test
- run: yarn release

1
.eslintrc

@ -60,6 +60,7 @@
"react/jsx-no-literals": [1, {"noStrings": false}], "react/jsx-no-literals": [1, {"noStrings": false}],
"react/prefer-stateless-function": 0, "react/prefer-stateless-function": 0,
"react/require-default-props": 0, "react/require-default-props": 0,
"react/no-multi-comp": 0,
"react/sort-comp": [1, { "react/sort-comp": [1, {
order: [ order: [
'static-methods', 'static-methods',

2
.github/ISSUE_TEMPLATE/feature_request.md

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

3
.gitignore

@ -1,3 +1,4 @@
xliffs/
/.env /.env
/dist/ /dist/
/flow-typed/ /flow-typed/
@ -10,3 +11,5 @@
/build/linux/arch/src /build/linux/arch/src
/build/linux/arch/*.tar.gz /build/linux/arch/*.tar.gz
/build/linux/arch/*.tar.xz /build/linux/arch/*.tar.xz
/test-e2e/sync/data/actual_app.json

1
.prettierignore

@ -1 +1,2 @@
package.json package.json
test-e2e/**/*.json

82
babel.config.js

@ -1,32 +1,62 @@
const { NODE_ENV } = process.env const { NODE_ENV, CLI } = process.env
const __TEST__ = NODE_ENV === 'test' const __TEST__ = NODE_ENV === 'test'
const __CLI__ = !!CLI
module.exports = () => ({ module.exports = (api) => {
presets: [
[ if (api) {
require('@babel/preset-env'), api.cache(true);
{ }
loose: true,
modules: __TEST__ ? 'commonjs' : false, return {
targets: { presets: [
electron: '1.8', [
node: 'current', require('@babel/preset-env'),
{
loose: true,
modules: __TEST__ || __CLI__ ? 'commonjs' : false,
targets: {
electron: '1.8',
node: 'current',
},
}, },
}, ],
require('@babel/preset-flow'),
require('@babel/preset-react'),
], ],
require('@babel/preset-flow'), plugins: [
require('@babel/preset-react'), [require('babel-plugin-module-resolver'), { root: ['src'] }],
require('@babel/preset-stage-0'), [
], require('babel-plugin-styled-components'),
plugins: [ {
[require('babel-plugin-module-resolver'), { root: ['src'] }], displayName: true,
[ ssr: __TEST__,
require('babel-plugin-styled-components'), },
{ ],
displayName: true, // Stage 0
ssr: __TEST__, "@babel/plugin-proposal-function-bind",
},
// Stage 1
"@babel/plugin-proposal-export-default-from",
"@babel/plugin-proposal-logical-assignment-operators",
["@babel/plugin-proposal-optional-chaining", { "loose": false }],
["@babel/plugin-proposal-pipeline-operator", { "proposal": "minimal" }],
["@babel/plugin-proposal-nullish-coalescing-operator", { "loose": false }],
"@babel/plugin-proposal-do-expressions",
// Stage 2
["@babel/plugin-proposal-decorators", { "legacy": true }],
"@babel/plugin-proposal-function-sent",
"@babel/plugin-proposal-export-namespace-from",
"@babel/plugin-proposal-numeric-separator",
"@babel/plugin-proposal-throw-expressions",
// Stage 3
"@babel/plugin-syntax-dynamic-import",
"@babel/plugin-syntax-import-meta",
["@babel/plugin-proposal-class-properties", { "loose": false }],
"@babel/plugin-proposal-json-strings"
], ],
], }
}) }

58
package.json

@ -3,7 +3,7 @@
"productName": "Ledger Live", "productName": "Ledger Live",
"description": "Ledger Live - Desktop", "description": "Ledger Live - Desktop",
"repository": "https://github.com/LedgerHQ/ledger-live-desktop", "repository": "https://github.com/LedgerHQ/ledger-live-desktop",
"version": "1.1.5", "version": "1.2.6",
"author": "Ledger", "author": "Ledger",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
@ -13,10 +13,12 @@
"dist": "bash ./scripts/dist.sh", "dist": "bash ./scripts/dist.sh",
"dist:dir": "bash ./scripts/dist-dir.sh", "dist:dir": "bash ./scripts/dist-dir.sh",
"compile": "bash ./scripts/compile.sh", "compile": "bash ./scripts/compile.sh",
"cli": "bash ./scripts/cli/cli.sh",
"lint": "eslint src webpack .storybook test-e2e", "lint": "eslint src webpack .storybook test-e2e",
"flow": "flow", "flow": "flow",
"test": "jest src", "test": "jest src",
"test-e2e": "jest test-e2e", "test-e2e": "jest test-e2e",
"test-sync": "bash test-e2e/sync/launch.sh",
"prettier": "prettier --write \"{src,webpack,.storybook,test-e2e}/**/*.{js,json}\"", "prettier": "prettier --write \"{src,webpack,.storybook,test-e2e}/**/*.{js,json}\"",
"ci": "yarn lint && yarn flow && yarn prettier && yarn test", "ci": "yarn lint && yarn flow && yarn prettier && yarn test",
"storybook": "NODE_ENV=development STORYBOOK_ENV=1 start-storybook -s ./static -p 4444", "storybook": "NODE_ENV=development STORYBOOK_ENV=1 start-storybook -s ./static -p 4444",
@ -33,13 +35,13 @@
} }
}, },
"dependencies": { "dependencies": {
"@ledgerhq/hw-app-btc": "4.21.0", "@ledgerhq/hw-app-btc": "^4.27.0",
"@ledgerhq/hw-app-eth": "^4.14.0", "@ledgerhq/hw-app-eth": "^4.24.0",
"@ledgerhq/hw-app-xrp": "^4.13.0", "@ledgerhq/hw-app-xrp": "^4.25.0",
"@ledgerhq/hw-transport": "^4.13.0", "@ledgerhq/hw-transport": "^4.24.0",
"@ledgerhq/hw-transport-node-hid": "4.22.0", "@ledgerhq/hw-transport-node-hid": "4.24.0",
"@ledgerhq/ledger-core": "2.0.0-rc.5", "@ledgerhq/ledger-core": "2.0.0-rc.11",
"@ledgerhq/live-common": "3.0.0", "@ledgerhq/live-common": "4.4.2",
"animated": "^0.2.2", "animated": "^0.2.2",
"async": "^2.6.1", "async": "^2.6.1",
"axios": "^0.18.0", "axios": "^0.18.0",
@ -62,16 +64,17 @@
"i18next": "^11.2.2", "i18next": "^11.2.2",
"i18next-node-fs-backend": "^1.0.0", "i18next-node-fs-backend": "^1.0.0",
"invariant": "^2.2.4", "invariant": "^2.2.4",
"jsqr": "^1.1.1",
"lodash": "^4.17.5", "lodash": "^4.17.5",
"lru-cache": "^4.1.3", "lru-cache": "^4.1.3",
"measure-scrollbar": "^1.1.0", "measure-scrollbar": "^1.1.0",
"moment": "^2.22.2", "moment": "^2.22.2",
"qrcode": "^1.2.0", "qrcode": "^1.2.0",
"qrcode-reader": "^1.0.4", "qrloop": "0.8.1",
"qs": "^6.5.1", "qs": "^6.5.1",
"raven": "^2.5.0", "raven": "^2.5.0",
"raven-js": "^3.24.2", "raven-js": "^3.24.2",
"react": "^16.4.1", "react": "^16.6.1",
"react-dom": "^16.4.1", "react-dom": "^16.4.1",
"react-i18next": "^7.7.0", "react-i18next": "^7.7.0",
"react-key-handler": "^1.0.1", "react-key-handler": "^1.0.1",
@ -113,12 +116,29 @@
"zxcvbn": "^4.4.2" "zxcvbn": "^4.4.2"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.0.0-beta.42", "@babel/core": "7.1.2",
"@babel/polyfill": "7.0.0-beta.42", "@babel/plugin-proposal-class-properties": "7.1.0",
"@babel/preset-env": "7.0.0-beta.42", "@babel/plugin-proposal-decorators": "7.1.2",
"@babel/preset-flow": "7.0.0-beta.42", "@babel/plugin-proposal-do-expressions": "7.0.0",
"@babel/preset-react": "7.0.0-beta.42", "@babel/plugin-proposal-export-default-from": "7.0.0",
"@babel/preset-stage-0": "7.0.0-beta.42", "@babel/plugin-proposal-export-namespace-from": "7.0.0",
"@babel/plugin-proposal-function-bind": "7.0.0",
"@babel/plugin-proposal-function-sent": "7.1.0",
"@babel/plugin-proposal-json-strings": "7.0.0",
"@babel/plugin-proposal-logical-assignment-operators": "7.0.0",
"@babel/plugin-proposal-nullish-coalescing-operator": "7.0.0",
"@babel/plugin-proposal-numeric-separator": "7.0.0",
"@babel/plugin-proposal-optional-chaining": "7.0.0",
"@babel/plugin-proposal-pipeline-operator": "7.0.0",
"@babel/plugin-proposal-throw-expressions": "7.0.0",
"@babel/plugin-syntax-dynamic-import": "7.0.0",
"@babel/plugin-syntax-import-meta": "7.0.0",
"@babel/polyfill": "7.0.0",
"@babel/preset-env": "7.1.0",
"@babel/preset-flow": "7.0.0",
"@babel/preset-react": "7.0.0",
"@babel/register": "7.0.0",
"@octokit/rest": "^15.10.0",
"@storybook/addon-actions": "^3.4.7", "@storybook/addon-actions": "^3.4.7",
"@storybook/addon-knobs": "^3.4.7", "@storybook/addon-knobs": "^3.4.7",
"@storybook/addon-links": "^3.4.7", "@storybook/addon-links": "^3.4.7",
@ -135,7 +155,7 @@
"chance": "^1.0.13", "chance": "^1.0.13",
"concurrently": "3.5.1", "concurrently": "3.5.1",
"dotenv": "^5.0.1", "dotenv": "^5.0.1",
"electron": "1.8.7", "electron": "1.8.8",
"electron-builder": "20.14.7", "electron-builder": "20.14.7",
"electron-devtools-installer": "^2.2.3", "electron-devtools-installer": "^2.2.3",
"electron-rebuild": "^1.7.3", "electron-rebuild": "^1.7.3",
@ -165,5 +185,9 @@
"webpack-cli": "^2.0.14", "webpack-cli": "^2.0.14",
"yaml-loader": "^0.5.0" "yaml-loader": "^0.5.0"
}, },
"engines": {
"node": ">=8.9.0 <=8.12.0",
"yarn": "^1.10.1"
},
"private": true "private": true
} }

50
scripts/check-wordings.js

@ -5,34 +5,34 @@ const { spawn } = require('child_process')
// those wordings are dynamically created, so they are detected // those wordings are dynamically created, so they are detected
// as false positive // as false positive
const WHITELIST = [ const WHITELIST = [
'app:operation.type.IN', 'operation.type.IN',
'app:operation.type.OUT', 'operation.type.OUT',
'app:exchange.coinhouse', 'exchange.coinhouse',
'app:exchange.changelly', 'exchange.changelly',
'app:exchange.coinmama', 'exchange.coinmama',
'app:exchange.simplex', 'exchange.simplex',
'app:exchange.paybis', 'exchange.paybis',
'app:addAccounts.accountToImportSubtitle_plural', 'addAccounts.accountToImportSubtitle_plural',
'app:dashboard.summary_plural', 'dashboard.summary_plural',
'app:addAccounts.success_plural', 'addAccounts.success_plural',
'app:addAccounts.successDescription_plural', 'addAccounts.successDescription_plural',
'app:time.since.day', 'time.since.day',
'app:time.since.week', 'time.since.week',
'app:time.since.month', 'time.since.month',
'app:time.since.year', 'time.since.year',
'app:time.day', 'time.day',
'app:time.week', 'time.week',
'app:time.month', 'time.month',
'app:time.year', 'time.year',
'app:addAccounts.cta.add_plural', 'addAccounts.cta.add_plural',
'app:manager.apps.installing', 'manager.apps.installing',
'app:manager.apps.uninstalling', 'manager.apps.uninstalling',
'app:manager.apps.installSuccess', 'manager.apps.installSuccess',
'app:manager.apps.uninstallSuccess', 'manager.apps.uninstallSuccess',
] ]
const WORDINGS = { const WORDINGS = {
app: require('../static/i18n/en/app.json'), require('../static/i18n/en/app.json'),
onboarding: require('../static/i18n/en/onboarding.json'), onboarding: require('../static/i18n/en/onboarding.json'),
// errors: require('../static/i18n/en/errors.json'), // errors: require('../static/i18n/en/errors.json'),
// language: require('../static/i18n/en/language.json'), // language: require('../static/i18n/en/language.json'),

9
scripts/cli/cli.sh

@ -0,0 +1,9 @@
#!/bin/bash
# TODO: os specific
export LEDGER_DATA_DIR="$HOME/.config/Electron"
export LEDGER_LOGS_DIRECTORY="$LEDGER_DATA_DIR/logs"
export LEDGER_LIVE_SQLITE_PATH="$LEDGER_DATA_DIR/sqlite"
export CLI=1
node -r @babel/register -r @babel/polyfill scripts/cli/txBetweenAccounts.js

21
scripts/cli/getDevice.js

@ -0,0 +1,21 @@
import CommNodeHid from '@ledgerhq/hw-transport-node-hid'
export default function getDevice() {
return new Promise((resolve, reject) => {
const sub = CommNodeHid.listen({
error: err => {
sub.unsubscribe()
reject(err)
},
next: async e => {
if (!e.device) {
return
}
if (e.type === 'add') {
sub.unsubscribe()
resolve(e.device)
}
},
})
})
}

104
scripts/cli/txBetweenAccounts.js

@ -0,0 +1,104 @@
/* eslint-disable no-console */
import chalk from 'chalk'
import path from 'path'
import fs from 'fs'
import inquirer from 'inquirer'
import { formatCurrencyUnit } from '@ledgerhq/live-common/lib/currencies'
import 'globals'
import withLibcore from 'helpers/withLibcore'
import accountModel from 'helpers/accountModel'
import { doSignAndBroadcast } from 'commands/libcoreSignAndBroadcast'
import getDevice from './getDevice'
async function main() {
try {
// GET ACCOUNTS
const app = await parseAppFile()
const accounts = app.accounts.map(accountModel.decode)
// GET SENDER ACCOUNT
const senderAccount = await chooseAccount(accounts, 'Choose sender account')
// GET RECIPIENT ACCOUNT
const recipientAccount = await chooseAccount(accounts, 'Choose recipient account')
// GET AMOUNT & FEE
const { amount, feePerByte } = await inquirer.prompt([
{
type: 'input',
name: 'amount',
message: 'Amount',
default: 0,
},
{
type: 'input',
name: 'feePerByte',
message: 'Fee per byte',
default: 0,
},
])
// GET DEVICE
console.log(chalk.blue(`Waiting for device...`))
const device = await getDevice()
console.log(chalk.blue(`Using device with path [${device.path}]`))
await withLibcore(async core =>
doSignAndBroadcast({
accountId: senderAccount.id,
currencyId: senderAccount.currency.id,
xpub: senderAccount.xpub,
freshAddress: senderAccount.freshAddress,
freshAddressPath: senderAccount.freshAddressPath,
index: senderAccount.index,
transaction: {
amount,
feePerByte,
recipient: recipientAccount.freshAddress,
},
deviceId: device.path,
core,
isCancelled: () => false,
onSigned: () => {
console.log(`>> signed`)
},
onOperationBroadcasted: operation => {
console.log(`>> broadcasted`, operation)
},
}),
)
} catch (err) {
console.log(`[ERROR]`, err)
}
}
async function parseAppFile() {
const appFilePath = path.resolve(process.env.LEDGER_DATA_DIR, 'app.json')
const appFileContent = fs.readFileSync(appFilePath, 'utf-8')
const parsedApp = JSON.parse(appFileContent)
return parsedApp.data
}
async function chooseAccount(accounts, msg) {
const { account } = await inquirer.prompt([
{
type: 'list',
choices: accounts.map(account => ({
name: `${account.name} | ${chalk.green(
formatCurrencyUnit(account.unit, account.balance, {
showCode: true,
}),
)}`,
value: account,
})),
name: 'account',
message: msg,
},
])
return account
}
main()

85
scripts/create-draft-release.js

@ -0,0 +1,85 @@
#!/usr/bin/env node
/* eslint-disable no-console */
const util = require('util')
const exec = util.promisify(require('child_process').exec)
const octokit = require('@octokit/rest')()
const repo = {
owner: 'LedgerHQ',
repo: 'ledger-live-desktop',
}
async function getTag() {
const { stdout } = await exec('git tag --points-at HEAD')
const tag = stdout.replace('\n', '')
if (!tag) {
throw new Error(`Unable to get current tag. Is your HEAD on a tagged commit?`)
}
return tag
}
async function checkDraft(tag) {
const { status, data } = await octokit.repos.getReleases(repo)
if (status !== 200) {
throw new Error(`Got HTTP status ${status} when trying to fetch releases list.`)
}
for (const release of data) {
if (release.tag_name === tag) {
if (release.draft) {
return true
}
throw new Error(`A release tagged ${tag} exists but is not a draft.`)
}
}
return false
}
async function createDraft(tag) {
const params = {
...repo,
tag_name: tag,
name: tag,
draft: true,
prerelease: true,
}
const { status } = await octokit.repos.createRelease(params)
if (status !== 201) {
throw new Error(`Got HTTP status ${status} when trying to create the release draft.`)
}
}
async function main() {
try {
const token = process.env.GH_TOKEN
const tag = await getTag()
octokit.authenticate({
type: 'token',
token,
})
const existingDraft = await checkDraft(tag)
if (!existingDraft) {
console.log(`No draft exists for ${tag}, creating...`)
createDraft(tag)
} else {
console.log(`A draft already exists for ${tag}, nothing to do.`)
}
} catch (e) {
console.error(e)
process.exit(1)
}
}
main()

17
scripts/download-xliffs.sh

@ -0,0 +1,17 @@
#!/bin/sh
if [ -z "$CROWDIN_TOKEN" ]; then
echo "CROWDIN_TOKEN env required" >&2
exit 1
fi
rm -rf xliffs
mkdir xliffs
cd xliffs
for lang in fr es-ES zh-CN ja ko ru; do
curl "https://api.crowdin.com/api/project/ledger-wallet/export-file?file=develop/static/i18n/en/app.json&language=$lang&format=xliff&key=$CROWDIN_TOKEN" > en-$lang.xliff
done
zip -r ledger-live-langs.zip *.xliff

8
scripts/helpers/display-env.sh

@ -7,9 +7,15 @@ if [ "$GIT_REVISION" == "" ]; then
GIT_REVISION=$(git rev-parse HEAD) GIT_REVISION=$(git rev-parse HEAD)
fi fi
if [[ $(uname) == 'Darwin' ]]; then
osVersion="$(sw_vers -productName) $(sw_vers -productVersion)"
else
osVersion="$(uname -srmo)"
fi
echo echo
printf " │ \\e[4;1m%s\\e[0;0m\\n" "Ledger Live Desktop - ${GIT_REVISION}" printf " │ \\e[4;1m%s\\e[0;0m\\n" "Ledger Live Desktop - ${GIT_REVISION}"
printf " │ \\e[1;30m%s\\e[1;0m\\n" "$(uname -srmo)" printf " │ \\e[1;30m%s\\e[1;0m\\n" "${osVersion}"
printf " │ \\e[2;1mcommit \\e[0;33m%s\\e[0;0m\\n" "$(git rev-parse HEAD)" printf " │ \\e[2;1mcommit \\e[0;33m%s\\e[0;0m\\n" "$(git rev-parse HEAD)"
echo echo

49
scripts/patch-appimage.sh

@ -0,0 +1,49 @@
#!/bin/bash
# Patch .AppImage to address libcore crash on some
# distributions, due to loading system libraries
# instead of embedded ones.
#
# see https://github.com/LedgerHQ/ledger-live-desktop/issues/1010
set -e
BASE_URL=http://mirrors.kernel.org/ubuntu/pool/main/k/krb5
PACKAGE_SUFFIX=-2build1_amd64.deb
TMP_DIR=$(mktemp -d)
LEDGER_LIVE_VERSION=$(grep version package.json | sed -E 's/.*: "(.*)",/\1/g')
cp "dist/ledger-live-desktop-$LEDGER_LIVE_VERSION-linux-x86_64.AppImage" "$TMP_DIR"
pushd "$TMP_DIR"
declare -a LIBRARIES=(
"libgssapi-krb5-2_1.16"
"libk5crypto3_1.16"
"libkrb5-3_1.16"
"libkrb5support0_1.16"
)
for PACKAGE in "${LIBRARIES[@]}"; do
curl -fOL "$BASE_URL/$PACKAGE$PACKAGE_SUFFIX"
ar p "$PACKAGE$PACKAGE_SUFFIX" data.tar.xz | tar xvJf >/dev/null - ./usr/lib/x86_64-linux-gnu/
rm "$PACKAGE$PACKAGE_SUFFIX"
done
curl -fOL "https://s3-eu-west-1.amazonaws.com/ledger-ledgerlive-resources-dev/public_resources/appimagetool-x86_64.AppImage"
cp "$OLDPWD/scripts/shasums/patch-appimage-sums.txt" .
sha512sum --quiet --check patch-appimage-sums.txt || exit 1
./ledger-live-desktop-"$LEDGER_LIVE_VERSION"-linux-x86_64.AppImage --appimage-extract
cp -a usr/lib/x86_64-linux-gnu/*.so.* squashfs-root/usr/lib
chmod +x appimagetool-x86_64.AppImage
./appimagetool-x86_64.AppImage squashfs-root "$OLDPWD/dist/ledger-live-desktop-$LEDGER_LIVE_VERSION-linux-x86_64.AppImage"
popd
MD5_SUM=$(sha512sum "dist/ledger-live-desktop-$LEDGER_LIVE_VERSION-linux-x86_64.AppImage" | cut -f1 -d\ | xxd -r -p | base64 | paste -sd "")
sed -i "s|sha512: .*|sha512: ${MD5_SUM}|g" dist/latest-linux.yml
SIZE=$(stat --printf="%s" "dist/ledger-live-desktop-$LEDGER_LIVE_VERSION-linux-x86_64.AppImage")
sed -i "s|size: .*|size: ${SIZE}|g" dist/latest-linux.yml

78
scripts/release.sh

@ -13,7 +13,9 @@ if [ "$(git rev-parse --abbrev-ref HEAD)" != "master" ]; then
exit 0 exit 0
fi fi
if ! git describe --exact-match --tags 2>/dev/null >/dev/null; then GH_TAG=$(git describe --exact-match --tags 2>/dev/null || echo '')
if [[ $GH_TAG == "" ]]; then
echo "You are not on a tag. Exiting properly. (CI)" echo "You are not on a tag. Exiting properly. (CI)"
exit 0 exit 0
fi fi
@ -25,6 +27,11 @@ fi
if [ ! -d "static/fonts/museosans" ]; then if [ ! -d "static/fonts/museosans" ]; then
if ! command -v aws ; then if ! command -v aws ; then
if ! command -v apt ; then
echo "Museo Sans is missing, and I can't fetch it (no aws, no apt)" >&2
exit 1
fi
runJob "sudo apt install awscli" "installing aws cli..." "installed aws cli" "failed to install aws cli" runJob "sudo apt install awscli" "installing aws cli..." "installed aws cli" "failed to install aws cli"
fi fi
@ -52,11 +59,68 @@ fi
# exit 1 # exit 1
# fi # fi
if [[ $(uname) == 'Linux' ]]; then # only run it on one target, to prevent race conditions
runJob \
"node scripts/create-draft-release.js" \
"creating a draft release on GitHub (if needed)..." \
"draft release ready" \
"failed to create a draft release"
fi
runJob "yarn compile" "compiling..." "compiled" "failed to compile" "verbose" runJob "yarn compile" "compiling..." "compiled" "failed to compile" "verbose"
runJob \ if [[ $(uname) == 'Linux' ]]; then
"DEBUG=electron-builder electron-builder build --publish always" \ # --------------------------------------------------------------------
"building, packaging and publishing app..." \ # Linux: Internal process error (null)
"app built, packaged and published successfully" \ #
"failed to build app" \ # context: https://github.com/LedgerHQ/ledger-live-desktop/issues/1010
"verbose" # Linux: Internal process error (null)
#
# The "fix" is not optimal, as it doesn't really solve the problem
# (electron loading system openssl before we can load our embedded one)
# Quick summary:
#
# - build without publishing
# - unpack the .AppImage
# - download reported working libs from ubuntu mirrors, put it inside
# - re-pack the .AppImage
# - checksum stuff
# - upload to gh
runJob \
"DEBUG=electron-builder electron-builder build --publish never" \
"building and packaging app..." \
"app built and packaged successfully" \
"failed to build app" \
"verbose"
runJob \
"scripts/patch-appimage.sh" \
"patching AppImage..." \
"AppImage patched successfully" \
"failed to patch AppImage"
LEDGER_LIVE_VERSION=$(grep version package.json | sed -E 's/.*: "(.*)",/\1/g')
scripts/upload-github-release-asset.sh \
github_api_token="$GH_TOKEN" \
owner=LedgerHQ \
repo=ledger-live-desktop \
tag="$GH_TAG" \
filename="dist/ledger-live-desktop-$LEDGER_LIVE_VERSION-linux-x86_64.AppImage"
scripts/upload-github-release-asset.sh \
github_api_token="$GH_TOKEN" \
owner=LedgerHQ \
repo=ledger-live-desktop \
tag="$GH_TAG" \
filename="dist/latest-linux.yml"
else
runJob \
"DEBUG=electron-builder electron-builder build --publish always" \
"building and packaging app..." \
"app built and packaged successfully" \
"failed to build app" \
"verbose"
fi

5
scripts/shasums/patch-appimage-sums.txt

@ -0,0 +1,5 @@
bebb42401a43971cfe3e31f2c9ee4efee352ce0d29a8ccc95ca1356a58463afd4876b133d9f4295697f96b76eb21b50c1909a073db753569e8969065eb40b306 usr/lib/x86_64-linux-gnu/libgssapi_krb5.so.2.2
d7d2b38a46d65a06560241b226f61d81c4df28d56c6841dd34bb428802ace0fc80cf94de1e5117f0b85b2c69b550df61ac999184d5cfe8ecd3bea4d8394d1d21 usr/lib/x86_64-linux-gnu/libkrb5.so.3.3
b025b755eb9a64f0d03a8e92c9e4b4f95c2c506bf070cf037841ef8cdb9013e16390d0e17330f2ce8c98c3b1f05b917a3018109acfde7aab50bc9d9fa70ea12b usr/lib/x86_64-linux-gnu/libk5crypto.so.3.1
f181e41f306819c10054ff8ceebf4943858f2cd34dea5206b51141877e2f651be3c6435bb02538cbde2cc0415f38e476423a9fd6a428ca9d425e9c662483b9af usr/lib/x86_64-linux-gnu/libkrb5support.so.0.1
dd8d81d4c1485209a65a1446225428a1b919478a74fd5698aff64cb8a67992544e62455f849ad73392505707cb94739de00af5ab340a22a87bb752c3808a55d2 appimagetool-x86_64.AppImage

61
scripts/upload-github-release-asset.sh

@ -0,0 +1,61 @@
#!/usr/bin/env bash
#
# Author: Stefan Buck
# License: MIT
# https://gist.github.com/stefanbuck/ce788fee19ab6eb0b4447a85fc99f447
#
#
# This script accepts the following parameters:
#
# * owner
# * repo
# * filename
# * github_api_token
#
# Script to upload a release asset using the GitHub API v3.
#
# Example:
#
# upload-github-release-asset.sh github_api_token=TOKEN owner=stefanbuck repo=playground filename=./build.zip
#
# Check dependencies.
set -e
# Validate settings.
[ "$TRACE" ] && set -x
# shellcheck disable=SC2124
CONFIG=$@
for line in $CONFIG; do
eval "$line"
done
# Define variables.
GH_API="https://api.github.com"
# shellcheck disable=SC2154
GH_REPO="$GH_API/repos/$owner/$repo"
# shellcheck disable=SC2154
AUTH="Authorization: token $github_api_token"
# github_api_token=$GH_TOKEN owner=LedgerHQ repo=ledger-live-desktop tag=v1.2.2 filename=./dist/electron-builder-debug.yml
LATEST_RELEASE_ID=$(curl -sH "$AUTH" "$GH_API/repos/LedgerHQ/ledger-live-desktop/releases" | grep '"id":' | head -n 1 | sed -E 's/.*: (.*),/\1/')
# Validate token.
curl -o /dev/null -sH "$AUTH" "$GH_REPO" || { echo "Error: Invalid repo, token or network issue!"; exit 1; }
# Get ID of the asset based on given filename.
# shellcheck disable=SC2154
[ "$LATEST_RELEASE_ID" ] || { echo "Error: Failed to get release id"; exit 1; }
# Upload asset
echo "Uploading asset... "
# Construct url
# shellcheck disable=SC2154
GH_ASSET="https://uploads.github.com/repos/$owner/$repo/releases/$LATEST_RELEASE_ID/assets?name=$(basename "$filename")"
curl --data-binary @"$filename" -H "Authorization: token $github_api_token" -H "Content-Type: application/octet-stream" "$GH_ASSET"

2
src/__mocks__/storybook-state.js

@ -2,7 +2,7 @@ import { genStoreState } from '@ledgerhq/live-common/lib/countervalues/mock'
import { import {
getCryptoCurrencyById, getCryptoCurrencyById,
getFiatCurrencyByTicker, getFiatCurrencyByTicker,
} from '@ledgerhq/live-common/lib/helpers/currencies' } from '@ledgerhq/live-common/lib/currencies'
export default { export default {
countervalues: genStoreState([ countervalues: genStoreState([

2
src/actions/general.js

@ -8,7 +8,7 @@ import {
getOrderAccounts, getOrderAccounts,
} from 'reducers/settings' } from 'reducers/settings'
import { accountsSelector } from 'reducers/accounts' import { accountsSelector } from 'reducers/accounts'
import { sortAccounts } from 'helpers/accountOrdering' import { sortAccounts } from '@ledgerhq/live-common/lib/account'
const accountsBtcBalanceSelector = createSelector( const accountsBtcBalanceSelector = createSelector(
accountsSelector, accountsSelector,

5
src/actions/settings.js

@ -46,3 +46,8 @@ export const setExchangePairsAction: SetExchangePairs = pairs => ({
type: 'SETTINGS_SET_PAIRS', type: 'SETTINGS_SET_PAIRS',
pairs, pairs,
}) })
export const dismissBanner = (bannerId: string) => ({
type: 'SETTINGS_DISMISS_BANNER',
payload: bannerId,
})

2
src/api/Ripple.js

@ -6,7 +6,7 @@ import {
parseCurrencyUnit, parseCurrencyUnit,
getCryptoCurrencyById, getCryptoCurrencyById,
formatCurrencyUnit, formatCurrencyUnit,
} from '@ledgerhq/live-common/lib/helpers/currencies' } from '@ledgerhq/live-common/lib/currencies'
const rippleUnit = getCryptoCurrencyById('ripple').units[0] const rippleUnit = getCryptoCurrencyById('ripple').units[0]

4
src/api/network.js

@ -6,7 +6,7 @@ import logger from 'logger'
import { LedgerAPIErrorWithMessage, LedgerAPIError, NetworkDown } from 'config/errors' import { LedgerAPIErrorWithMessage, LedgerAPIError, NetworkDown } from 'config/errors'
import anonymizer from 'helpers/anonymizer' import anonymizer from 'helpers/anonymizer'
const userFriendlyError = <A>(p: Promise<A>, { url, method, startTime }): Promise<A> => const userFriendlyError = <A>(p: Promise<A>, { url, method, startTime, ...rest }): Promise<A> =>
p.catch(error => { p.catch(error => {
let errorToThrow let errorToThrow
if (error.response) { if (error.response) {
@ -47,6 +47,7 @@ const userFriendlyError = <A>(p: Promise<A>, { url, method, startTime }): Promis
}) })
} }
logger.networkError({ logger.networkError({
...rest,
status, status,
url, url,
method, method,
@ -80,6 +81,7 @@ let implementation = (arg: Object) => {
const meta = { const meta = {
url: arg.url, url: arg.url,
method: arg.method, method: arg.method,
data: arg.data,
startTime: Date.now(), startTime: Date.now(),
} }
logger.network(meta) logger.network(meta)

9
src/bridge/BridgeSyncContext.js

@ -16,7 +16,9 @@ import { setAccountSyncState } from 'actions/bridgeSync'
import { bridgeSyncSelector, syncStateLocalSelector } from 'reducers/bridgeSync' import { bridgeSyncSelector, syncStateLocalSelector } from 'reducers/bridgeSync'
import type { BridgeSyncState } from 'reducers/bridgeSync' import type { BridgeSyncState } from 'reducers/bridgeSync'
import { accountsSelector, isUpToDateSelector } from 'reducers/accounts' import { accountsSelector, isUpToDateSelector } from 'reducers/accounts'
import { currenciesStatusSelector, currencyDownStatusLocal } from 'reducers/currenciesStatus'
import { SYNC_MAX_CONCURRENT, SYNC_TIMEOUT } from 'config/constants' import { SYNC_MAX_CONCURRENT, SYNC_TIMEOUT } from 'config/constants'
import type { CurrencyStatus } from 'reducers/currenciesStatus'
import { getBridgeForCurrency } from '.' import { getBridgeForCurrency } from '.'
type BridgeSyncProviderProps = { type BridgeSyncProviderProps = {
@ -29,6 +31,7 @@ type BridgeSyncProviderOwnProps = BridgeSyncProviderProps & {
isUpToDate: boolean, isUpToDate: boolean,
updateAccountWithUpdater: (string, (Account) => Account) => void, updateAccountWithUpdater: (string, (Account) => Account) => void,
setAccountSyncState: (string, AsyncState) => *, setAccountSyncState: (string, AsyncState) => *,
currenciesStatus: CurrencyStatus[],
} }
type AsyncState = { type AsyncState = {
@ -48,6 +51,7 @@ export type Sync = (action: BehaviorAction) => void
const BridgeSyncContext = React.createContext((_: BehaviorAction) => {}) const BridgeSyncContext = React.createContext((_: BehaviorAction) => {})
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
currenciesStatus: currenciesStatusSelector,
accounts: accountsSelector, accounts: accountsSelector,
bridgeSync: bridgeSyncSelector, bridgeSync: bridgeSyncSelector,
isUpToDate: isUpToDateSelector, isUpToDate: isUpToDateSelector,
@ -73,6 +77,11 @@ class Provider extends Component<BridgeSyncProviderOwnProps, Sync> {
return return
} }
if (currencyDownStatusLocal(this.props.currenciesStatus, account.currency)) {
next()
return
}
const bridge = getBridgeForCurrency(account.currency) const bridge = getBridgeForCurrency(account.currency)
this.props.setAccountSyncState(accountId, { pending: true, error: null }) this.props.setAccountSyncState(accountId, { pending: true, error: null })

103
src/bridge/EthereumJSBridge.js

@ -8,28 +8,37 @@ import AdvancedOptions from 'components/AdvancedOptions/EthereumKind'
import throttle from 'lodash/throttle' import throttle from 'lodash/throttle'
import flatMap from 'lodash/flatMap' import flatMap from 'lodash/flatMap'
import uniqBy from 'lodash/uniqBy' import uniqBy from 'lodash/uniqBy'
import {
getDerivationModesForCurrency,
getDerivationScheme,
runDerivationScheme,
isIterableDerivationMode,
getMandatoryEmptyAccountSkip,
} from '@ledgerhq/live-common/lib/derivation'
import {
getAccountPlaceholderName,
getNewAccountPlaceholderName,
} from '@ledgerhq/live-common/lib/account'
import type { Account, Operation } from '@ledgerhq/live-common/lib/types' import type { Account, Operation } from '@ledgerhq/live-common/lib/types'
import eip55 from 'eip55' import eip55 from 'eip55'
import { apiForCurrency } from 'api/Ethereum' import { apiForCurrency } from 'api/Ethereum'
import type { Tx } from 'api/Ethereum' import type { Tx } from 'api/Ethereum'
import { getDerivations } from 'helpers/derivations'
import getAddressCommand from 'commands/getAddress' import getAddressCommand from 'commands/getAddress'
import signTransactionCommand from 'commands/signTransaction' import signTransactionCommand from 'commands/signTransaction'
import { getAccountPlaceholderName, getNewAccountPlaceholderName } from 'helpers/accountName' import { NotEnoughBalance, FeeNotLoaded, ETHAddressNonEIP } from 'config/errors'
import { NotEnoughBalance, ETHAddressNonEIP } from 'config/errors'
import type { EditProps, WalletBridge } from './types' import type { EditProps, WalletBridge } from './types'
type Transaction = { type Transaction = {
recipient: string, recipient: string,
amount: BigNumber, amount: BigNumber,
gasPrice: BigNumber, gasPrice: ?BigNumber,
gasLimit: BigNumber, gasLimit: BigNumber,
} }
const serializeTransaction = t => ({ const serializeTransaction = t => ({
recipient: t.recipient, recipient: t.recipient,
amount: `0x${BigNumber(t.amount).toString(16)}`, amount: `0x${BigNumber(t.amount).toString(16)}`,
gasPrice: `0x${BigNumber(t.gasPrice).toString(16)}`, gasPrice: !t.gasPrice ? '0x00' : `0x${BigNumber(t.gasPrice).toString(16)}`,
gasLimit: `0x${BigNumber(t.gasLimit).toString(16)}`, gasLimit: `0x${BigNumber(t.gasLimit).toString(16)}`,
}) })
@ -64,7 +73,7 @@ const txToOps = (account: Account) => (tx: Tx): Operation[] => {
const value = BigNumber(tx.value) const value = BigNumber(tx.value)
const fee = BigNumber(tx.gas_price * tx.gas_used) const fee = BigNumber(tx.gas_price * tx.gas_used)
if (sending) { if (sending) {
ops.push({ const op: $Exact<Operation> = {
id: `${account.id}-${tx.hash}-OUT`, id: `${account.id}-${tx.hash}-OUT`,
hash: tx.hash, hash: tx.hash,
type: 'OUT', type: 'OUT',
@ -76,10 +85,12 @@ const txToOps = (account: Account) => (tx: Tx): Operation[] => {
senders: [tx.from], senders: [tx.from],
recipients: [tx.to], recipients: [tx.to],
date: new Date(tx.received_at), date: new Date(tx.received_at),
}) extra: {},
}
ops.push(op)
} }
if (receiving) { if (receiving) {
ops.push({ const op: $Exact<Operation> = {
id: `${account.id}-${tx.hash}-IN`, id: `${account.id}-${tx.hash}-IN`,
hash: tx.hash, hash: tx.hash,
type: 'IN', type: 'IN',
@ -91,7 +102,9 @@ const txToOps = (account: Account) => (tx: Tx): Operation[] => {
senders: [tx.from], senders: [tx.from],
recipients: [tx.to], recipients: [tx.to],
date: new Date(new Date(tx.received_at) + 1), // hack: make the IN appear after the OUT in history. date: new Date(new Date(tx.received_at) + 1), // hack: make the IN appear after the OUT in history.
}) extra: {},
}
ops.push(op)
} }
return ops return ops
} }
@ -140,6 +153,8 @@ const signAndBroadcast = async ({
onSigned, onSigned,
onOperationBroadcasted, onOperationBroadcasted,
}) => { }) => {
const { gasPrice, amount, gasLimit } = t
if (!gasPrice) throw new FeeNotLoaded()
const api = apiForCurrency(a.currency) const api = apiForCurrency(a.currency)
const nonce = await api.getAccountNonce(a.freshAddress) const nonce = await api.getAccountNonce(a.freshAddress)
@ -158,12 +173,12 @@ const signAndBroadcast = async ({
const hash = await api.broadcastTransaction(transaction) const hash = await api.broadcastTransaction(transaction)
onOperationBroadcasted({ const op: $Exact<Operation> = {
id: `${a.id}-${hash}-OUT`, id: `${a.id}-${hash}-OUT`,
hash, hash,
type: 'OUT', type: 'OUT',
value: t.amount, value: amount,
fee: t.gasPrice.times(t.gasLimit), fee: gasPrice.times(gasLimit),
blockHeight: null, blockHeight: null,
blockHash: null, blockHash: null,
accountId: a.id, accountId: a.id,
@ -171,7 +186,10 @@ const signAndBroadcast = async ({
recipients: [t.recipient], recipients: [t.recipient],
transactionSequenceNumber: nonce, transactionSequenceNumber: nonce,
date: new Date(), date: new Date(),
}) extra: {},
}
onOperationBroadcasted(op)
} }
} }
@ -208,8 +226,8 @@ const EthereumBridge: WalletBridge<Transaction> = {
async function stepAddress( async function stepAddress(
index, index,
{ address, path: freshAddressPath, publicKey }, { address, path: freshAddressPath },
isStandard, derivationMode,
shouldSkipEmpty, shouldSkipEmpty,
): { account?: Account, complete?: boolean } { ): { account?: Account, complete?: boolean } {
const balance = await api.getAccountBalance(address) const balance = await api.getAccountBalance(address)
@ -220,19 +238,21 @@ const EthereumBridge: WalletBridge<Transaction> = {
if (finished) return { complete: true } if (finished) return { complete: true }
const freshAddress = address const freshAddress = address
const accountId = `ethereumjs:${currency.id}:${address}:${publicKey}` const accountId = `ethereumjs:2:${currency.id}:${address}:${derivationMode}`
if (txs.length === 0 && balance.isZero()) { if (txs.length === 0 && balance.isZero()) {
// this is an empty account // this is an empty account
if (isStandard) { if (derivationMode === '') {
// is standard derivation
if (newAccountCount === 0) { if (newAccountCount === 0) {
// first zero account will emit one account as opportunity to create a new account.. // first zero account will emit one account as opportunity to create a new account..
const account: $Exact<Account> = { const account: $Exact<Account> = {
id: accountId, id: accountId,
xpub: '', seedIdentifier: freshAddress,
freshAddress, freshAddress,
freshAddressPath, freshAddressPath,
name: getNewAccountPlaceholderName(currency, index), derivationMode,
name: getNewAccountPlaceholderName({ currency, index, derivationMode }),
balance, balance,
blockHeight: currentBlock.height, blockHeight: currentBlock.height,
index, index,
@ -256,10 +276,11 @@ const EthereumBridge: WalletBridge<Transaction> = {
const account: $Exact<Account> = { const account: $Exact<Account> = {
id: accountId, id: accountId,
xpub: '', seedIdentifier: freshAddress,
freshAddress, freshAddress,
freshAddressPath, freshAddressPath,
name: getAccountPlaceholderName(currency, index, !isStandard), derivationMode,
name: getAccountPlaceholderName({ currency, index, derivationMode }),
balance, balance,
blockHeight: currentBlock.height, blockHeight: currentBlock.height,
index, index,
@ -286,21 +307,23 @@ const EthereumBridge: WalletBridge<Transaction> = {
async function main() { async function main() {
try { try {
const derivations = getDerivations(currency) const derivationModes = getDerivationModesForCurrency(currency)
const last = derivations[derivations.length - 1] for (const derivationMode of derivationModes) {
for (const derivation of derivations) {
const isStandard = last === derivation
let emptyCount = 0 let emptyCount = 0
const mandatoryEmptyAccountSkip = derivation.mandatoryEmptyAccountSkip || 0 const mandatoryEmptyAccountSkip = getMandatoryEmptyAccountSkip(derivationMode)
for (let index = 0; index < 255; index++) { const derivationScheme = getDerivationScheme({ derivationMode, currency })
const freshAddressPath = derivation({ currency, x: index, segwit: false }) const stopAt = isIterableDerivationMode(derivationMode) ? 255 : 1
for (let index = 0; index < stopAt; index++) {
const freshAddressPath = runDerivationScheme(derivationScheme, currency, {
account: index,
})
const res = await getAddressCommand const res = await getAddressCommand
.send({ currencyId: currency.id, devicePath: deviceId, path: freshAddressPath }) .send({ currencyId: currency.id, devicePath: deviceId, path: freshAddressPath })
.toPromise() .toPromise()
const r = await stepAddress( const r = await stepAddress(
index, index,
res, res,
isStandard, derivationMode,
emptyCount < mandatoryEmptyAccountSkip, emptyCount < mandatoryEmptyAccountSkip,
) )
logger.log( logger.log(
@ -402,7 +425,7 @@ const EthereumBridge: WalletBridge<Transaction> = {
createTransaction: () => ({ createTransaction: () => ({
amount: BigNumber(0), amount: BigNumber(0),
recipient: '', recipient: '',
gasPrice: BigNumber(0), gasPrice: null,
gasLimit: BigNumber(0x5208), gasLimit: BigNumber(0x5208),
}), }),
@ -420,23 +443,27 @@ const EthereumBridge: WalletBridge<Transaction> = {
getTransactionRecipient: (a, t) => t.recipient, getTransactionRecipient: (a, t) => t.recipient,
isValidTransaction: (a, t) => (!t.amount.isZero() && t.recipient && true) || false,
EditFees, EditFees,
EditAdvancedOptions, EditAdvancedOptions,
checkCanBeSpent: (a, t) => checkValidTransaction: (a, t) =>
t.amount.isLessThanOrEqualTo(a.balance) !t.gasPrice
? Promise.resolve() ? Promise.reject(new FeeNotLoaded())
: Promise.reject(new NotEnoughBalance()), : t.amount.isLessThanOrEqualTo(a.balance)
? Promise.resolve(true)
: Promise.reject(new NotEnoughBalance()),
getTotalSpent: (a, t) => getTotalSpent: (a, t) =>
t.amount.isGreaterThan(0) && t.gasPrice.isGreaterThan(0) && t.gasLimit.isGreaterThan(0) t.amount.isGreaterThan(0) &&
t.gasPrice &&
t.gasPrice.isGreaterThan(0) &&
t.gasLimit.isGreaterThan(0)
? Promise.resolve(t.amount.plus(t.gasPrice.times(t.gasLimit))) ? Promise.resolve(t.amount.plus(t.gasPrice.times(t.gasLimit)))
: Promise.resolve(BigNumber(0)), : Promise.resolve(BigNumber(0)),
getMaxAmount: (a, t) => Promise.resolve(a.balance.minus(t.gasPrice.times(t.gasLimit))), getMaxAmount: (a, t) =>
Promise.resolve(a.balance.minus((t.gasPrice || BigNumber(0)).times(t.gasLimit))),
signAndBroadcast: (a, t, deviceId) => signAndBroadcast: (a, t, deviceId) =>
Observable.create(o => { Observable.create(o => {

78
src/bridge/LibcoreBridge.js

@ -9,9 +9,9 @@ import FeesBitcoinKind from 'components/FeesField/BitcoinKind'
import libcoreScanAccounts from 'commands/libcoreScanAccounts' import libcoreScanAccounts from 'commands/libcoreScanAccounts'
import libcoreSyncAccount from 'commands/libcoreSyncAccount' import libcoreSyncAccount from 'commands/libcoreSyncAccount'
import libcoreSignAndBroadcast from 'commands/libcoreSignAndBroadcast' import libcoreSignAndBroadcast from 'commands/libcoreSignAndBroadcast'
import libcoreGetFees from 'commands/libcoreGetFees' import libcoreGetFees, { extractGetFeesInputFromAccount } from 'commands/libcoreGetFees'
import libcoreValidAddress from 'commands/libcoreValidAddress' import libcoreValidAddress from 'commands/libcoreValidAddress'
import { NotEnoughBalance } from 'config/errors' import { NotEnoughBalance, FeeNotLoaded } from 'config/errors'
import type { WalletBridge, EditProps } from './types' import type { WalletBridge, EditProps } from './types'
const NOT_ENOUGH_FUNDS = 52 const NOT_ENOUGH_FUNDS = 52
@ -20,15 +20,18 @@ const notImplemented = new Error('LibcoreBridge: not implemented')
type Transaction = { type Transaction = {
amount: BigNumber, amount: BigNumber,
feePerByte: BigNumber, feePerByte: ?BigNumber,
recipient: string, recipient: string,
} }
const serializeTransaction = t => ({ const serializeTransaction = t => {
recipient: t.recipient, const { feePerByte } = t
amount: t.amount.toString(), return {
feePerByte: t.feePerByte.toString(), recipient: t.recipient,
}) amount: t.amount.toString(),
feePerByte: (feePerByte && feePerByte.toString()) || '0',
}
}
const decodeOperation = (encodedAccount, rawOp) => const decodeOperation = (encodedAccount, rawOp) =>
decodeAccount({ ...encodedAccount, operations: [rawOp] }).operations[0] decodeAccount({ ...encodedAccount, operations: [rawOp] }).operations[0]
@ -75,7 +78,9 @@ const isRecipientValid = (currency, recipient) => {
const feesLRU = LRU({ max: 100 }) const feesLRU = LRU({ max: 100 })
const getFeesKey = (a, t) => const getFeesKey = (a, t) =>
`${a.id}_${a.blockHeight || 0}_${t.amount.toString()}_${t.recipient}_${t.feePerByte.toString()}` `${a.id}_${a.blockHeight || 0}_${t.amount.toString()}_${t.recipient}_${
t.feePerByte ? t.feePerByte.toString() : ''
}`
const getFees = async (a, transaction) => { const getFees = async (a, transaction) => {
const isValid = await isRecipientValid(a.currency, transaction.recipient) const isValid = await isRecipientValid(a.currency, transaction.recipient)
@ -83,10 +88,10 @@ const getFees = async (a, transaction) => {
const key = getFeesKey(a, transaction) const key = getFeesKey(a, transaction)
let promise = feesLRU.get(key) let promise = feesLRU.get(key)
if (promise) return promise if (promise) return promise
promise = libcoreGetFees promise = libcoreGetFees
.send({ .send({
accountId: a.id, ...extractGetFeesInputFromAccount(a),
accountIndex: a.index,
transaction: serializeTransaction(transaction), transaction: serializeTransaction(transaction),
}) })
.toPromise() .toPromise()
@ -95,18 +100,20 @@ const getFees = async (a, transaction) => {
return promise return promise
} }
const checkCanBeSpent = (a, t) => const checkValidTransaction = (a, t) =>
!t.amount !t.feePerByte
? Promise.resolve() ? Promise.reject(new FeeNotLoaded())
: getFees(a, t) : !t.amount
.then(() => {}) ? Promise.resolve(true)
.catch(e => { : getFees(a, t)
if (e.code === NOT_ENOUGH_FUNDS) { .then(() => true)
throw new NotEnoughBalance() .catch(e => {
} if (e.code === NOT_ENOUGH_FUNDS) {
feesLRU.del(getFeesKey(a, t)) throw new NotEnoughBalance()
throw e }
}) feesLRU.del(getFeesKey(a, t))
throw e
})
const LibcoreBridge: WalletBridge<Transaction> = { const LibcoreBridge: WalletBridge<Transaction> = {
scanAccountsOnDevice(currency, devicePath) { scanAccountsOnDevice(currency, devicePath) {
@ -122,13 +129,15 @@ const LibcoreBridge: WalletBridge<Transaction> = {
libcoreSyncAccount libcoreSyncAccount
.send({ .send({
accountId: account.id, accountId: account.id,
freshAddressPath: account.freshAddressPath, derivationMode: account.derivationMode,
xpub: account.xpub || '',
seedIdentifier: account.seedIdentifier,
index: account.index, index: account.index,
currencyId: account.currency.id, currencyId: account.currency.id,
}) })
.pipe( .pipe(
map(rawSyncedAccount => { map(({ rawAccount, requiresCacheFlush }) => {
const syncedAccount = decodeAccount(rawSyncedAccount) const syncedAccount = decodeAccount(rawAccount)
return account => { return account => {
const accountOps = account.operations const accountOps = account.operations
const syncedOps = syncedAccount.operations const syncedOps = syncedAccount.operations
@ -142,11 +151,11 @@ const LibcoreBridge: WalletBridge<Transaction> = {
} }
const hasChanged = const hasChanged =
requiresCacheFlush ||
accountOps.length !== syncedOps.length || // size change, we do a full refresh for now... accountOps.length !== syncedOps.length || // size change, we do a full refresh for now...
(accountOps.length > 0 && (accountOps.length > 0 &&
syncedOps.length > 0 && syncedOps.length > 0 &&
(accountOps[0].accountId !== syncedOps[0].accountId || (accountOps[0].id !== syncedOps[0].id || // if same size, only check if the last item has changed.
accountOps[0].id !== syncedOps[0].id || // if same size, only check if the last item has changed.
accountOps[0].blockHeight !== syncedOps[0].blockHeight)) accountOps[0].blockHeight !== syncedOps[0].blockHeight))
if (hasChanged) { if (hasChanged) {
@ -170,7 +179,7 @@ const LibcoreBridge: WalletBridge<Transaction> = {
createTransaction: () => ({ createTransaction: () => ({
amount: BigNumber(0), amount: BigNumber(0),
recipient: '', recipient: '',
feePerByte: BigNumber(0), feePerByte: null,
isRBF: false, isRBF: false,
}), }),
@ -192,9 +201,7 @@ const LibcoreBridge: WalletBridge<Transaction> = {
// EditAdvancedOptions, // EditAdvancedOptions,
isValidTransaction: (a, t) => (!t.amount.isZero() && t.recipient && true) || false, checkValidTransaction,
checkCanBeSpent,
getTotalSpent: (a, t) => getTotalSpent: (a, t) =>
t.amount.isZero() t.amount.isZero()
@ -213,9 +220,10 @@ const LibcoreBridge: WalletBridge<Transaction> = {
.send({ .send({
accountId: account.id, accountId: account.id,
currencyId: account.currency.id, currencyId: account.currency.id,
xpub: account.xpub, blockHeight: account.blockHeight,
freshAddress: account.freshAddress, xpub: account.xpub || '', // FIXME only reason is to build the op id. we need to consider another id for making op id.
freshAddressPath: account.freshAddressPath, derivationMode: account.derivationMode,
seedIdentifier: account.seedIdentifier,
index: account.index, index: account.index,
transaction: serializeTransaction(transaction), transaction: serializeTransaction(transaction),
deviceId, deviceId,

150
src/bridge/RippleJSBridge.js

@ -7,7 +7,16 @@ import bs58check from 'ripple-bs58check'
import { computeBinaryTransactionHash } from 'ripple-hashes' import { computeBinaryTransactionHash } from 'ripple-hashes'
import throttle from 'lodash/throttle' import throttle from 'lodash/throttle'
import type { Account, Operation } from '@ledgerhq/live-common/lib/types' import type { Account, Operation } from '@ledgerhq/live-common/lib/types'
import { getDerivations } from 'helpers/derivations' import {
getDerivationModesForCurrency,
getDerivationScheme,
runDerivationScheme,
isIterableDerivationMode,
} from '@ledgerhq/live-common/lib/derivation'
import {
getAccountPlaceholderName,
getNewAccountPlaceholderName,
} from '@ledgerhq/live-common/lib/account'
import getAddress from 'commands/getAddress' import getAddress from 'commands/getAddress'
import signTransaction from 'commands/signTransaction' import signTransaction from 'commands/signTransaction'
import { import {
@ -19,14 +28,17 @@ import {
} from 'api/Ripple' } from 'api/Ripple'
import FeesRippleKind from 'components/FeesField/RippleKind' import FeesRippleKind from 'components/FeesField/RippleKind'
import AdvancedOptionsRippleKind from 'components/AdvancedOptions/RippleKind' import AdvancedOptionsRippleKind from 'components/AdvancedOptions/RippleKind'
import { getAccountPlaceholderName, getNewAccountPlaceholderName } from 'helpers/accountName' import {
import { NotEnoughBalance } from 'config/errors' NotEnoughBalance,
FeeNotLoaded,
NotEnoughBalanceBecauseDestinationNotCreated,
} from 'config/errors'
import type { WalletBridge, EditProps } from './types' import type { WalletBridge, EditProps } from './types'
type Transaction = { type Transaction = {
amount: BigNumber, amount: BigNumber,
recipient: string, recipient: string,
fee: BigNumber, fee: ?BigNumber,
tag: ?number, tag: ?number,
} }
@ -51,6 +63,8 @@ const EditAdvancedOptions = ({ onChange, value }: EditProps<Transaction>) => (
async function signAndBroadcast({ a, t, deviceId, isCancelled, onSigned, onOperationBroadcasted }) { async function signAndBroadcast({ a, t, deviceId, isCancelled, onSigned, onOperationBroadcasted }) {
const api = apiForEndpointConfig(a.endpointConfig) const api = apiForEndpointConfig(a.endpointConfig)
const { fee } = t
if (!fee) throw new FeeNotLoaded()
try { try {
await api.connect() await api.connect()
const amount = formatAPICurrencyXRP(t.amount) const amount = formatAPICurrencyXRP(t.amount)
@ -66,7 +80,7 @@ async function signAndBroadcast({ a, t, deviceId, isCancelled, onSigned, onOpera
}, },
} }
const instruction = { const instruction = {
fee: formatAPICurrencyXRP(t.fee).value, fee: formatAPICurrencyXRP(fee).value,
maxLedgerVersionOffset: 12, maxLedgerVersionOffset: 12,
} }
@ -91,13 +105,13 @@ async function signAndBroadcast({ a, t, deviceId, isCancelled, onSigned, onOpera
const hash = computeBinaryTransactionHash(transaction) const hash = computeBinaryTransactionHash(transaction)
onOperationBroadcasted({ const op: $Exact<Operation> = {
id: `${a.id}-${hash}-OUT`, id: `${a.id}-${hash}-OUT`,
hash, hash,
accountId: a.id, accountId: a.id,
type: 'OUT', type: 'OUT',
value: t.amount, value: t.amount,
fee: t.fee, fee,
blockHash: null, blockHash: null,
blockHeight: null, blockHeight: null,
senders: [a.freshAddress], senders: [a.freshAddress],
@ -107,14 +121,16 @@ async function signAndBroadcast({ a, t, deviceId, isCancelled, onSigned, onOpera
transactionSequenceNumber: transactionSequenceNumber:
(a.operations.length > 0 ? a.operations[0].transactionSequenceNumber : 0) + (a.operations.length > 0 ? a.operations[0].transactionSequenceNumber : 0) +
a.pendingOperations.length, a.pendingOperations.length,
}) extra: {},
}
onOperationBroadcasted(op)
} }
} finally { } finally {
api.disconnect() api.disconnect()
} }
} }
function isRecipientValid(currency, recipient) { function isRecipientValid(recipient) {
try { try {
bs58check.decode(recipient) bs58check.decode(recipient)
return true return true
@ -205,7 +221,7 @@ const txToOperation = (account: Account) => ({
} }
const op: $Exact<Operation> = { const op: $Exact<Operation> = {
id, id: `${account.id}-${id}-${type}`,
hash: id, hash: id,
accountId: account.id, accountId: account.id,
type, type,
@ -217,6 +233,7 @@ const txToOperation = (account: Account) => ({
recipients: [destination.address], recipients: [destination.address],
date: new Date(timestamp), date: new Date(timestamp),
transactionSequenceNumber: sequence, transactionSequenceNumber: sequence,
extra: {},
} }
return op return op
} }
@ -241,6 +258,31 @@ const getServerInfo = (map => endpointConfig => {
return f() return f()
})({}) })({})
const recipientIsNew = async (endpointConfig, recipient) => {
if (!isRecipientValid(recipient)) return false
const api = apiForEndpointConfig(endpointConfig)
try {
await api.connect()
try {
await api.getAccountInfo(recipient)
return false
} catch (e) {
if (e.message !== 'actNotFound') {
throw e
}
return true
}
} finally {
api.disconnect()
}
}
const cacheRecipientsNew = {}
const cachedRecipientIsNew = (endpointConfig, recipient) => {
if (recipient in cacheRecipientsNew) return cacheRecipientsNew[recipient]
return (cacheRecipientsNew[recipient] = recipientIsNew(endpointConfig, recipient))
}
const RippleJSBridge: WalletBridge<Transaction> = { const RippleJSBridge: WalletBridge<Transaction> = {
scanAccountsOnDevice: (currency, deviceId) => scanAccountsOnDevice: (currency, deviceId) =>
Observable.create(o => { Observable.create(o => {
@ -258,17 +300,20 @@ const RippleJSBridge: WalletBridge<Transaction> = {
const minLedgerVersion = Number(ledgers[0]) const minLedgerVersion = Number(ledgers[0])
const maxLedgerVersion = Number(ledgers[1]) const maxLedgerVersion = Number(ledgers[1])
const derivations = getDerivations(currency) const derivationModes = getDerivationModesForCurrency(currency)
for (const derivation of derivations) { for (const derivationMode of derivationModes) {
const legacy = derivation !== derivations[derivations.length - 1] const derivationScheme = getDerivationScheme({ derivationMode, currency })
for (let index = 0; index < 255; index++) { const stopAt = isIterableDerivationMode(derivationMode) ? 255 : 1
const freshAddressPath = derivation({ currency, x: index, segwit: false }) for (let index = 0; index < stopAt; index++) {
const { address, publicKey } = await await getAddress const freshAddressPath = runDerivationScheme(derivationScheme, currency, {
account: index,
})
const { address } = await await getAddress
.send({ currencyId: currency.id, devicePath: deviceId, path: freshAddressPath }) .send({ currencyId: currency.id, devicePath: deviceId, path: freshAddressPath })
.toPromise() .toPromise()
if (finished) return if (finished) return
const accountId = `ripplejs:${currency.id}:${address}:${publicKey}` const accountId = `ripplejs:2:${currency.id}:${address}:${derivationMode}`
let info let info
try { try {
@ -285,11 +330,12 @@ const RippleJSBridge: WalletBridge<Transaction> = {
if (!info) { if (!info) {
// account does not exist in Ripple server // account does not exist in Ripple server
// we are generating a new account locally // we are generating a new account locally
if (!legacy) { if (derivationMode === '') {
o.next({ o.next({
id: accountId, id: accountId,
xpub: '', seedIdentifier: freshAddress,
name: getNewAccountPlaceholderName(currency, index), derivationMode,
name: getNewAccountPlaceholderName({ currency, index, derivationMode }),
freshAddress, freshAddress,
freshAddressPath, freshAddressPath,
balance: BigNumber(0), balance: BigNumber(0),
@ -322,8 +368,9 @@ const RippleJSBridge: WalletBridge<Transaction> = {
const account: $Exact<Account> = { const account: $Exact<Account> = {
id: accountId, id: accountId,
xpub: '', seedIdentifier: freshAddress,
name: getAccountPlaceholderName(currency, index, legacy), derivationMode,
name: getAccountPlaceholderName({ currency, index, derivationMode }),
freshAddress, freshAddress,
freshAddressPath, freshAddressPath,
balance, balance,
@ -446,13 +493,13 @@ const RippleJSBridge: WalletBridge<Transaction> = {
pullMoreOperations: () => Promise.resolve(a => a), // FIXME not implemented pullMoreOperations: () => Promise.resolve(a => a), // FIXME not implemented
isRecipientValid: (currency, recipient) => Promise.resolve(isRecipientValid(currency, recipient)), isRecipientValid: (currency, recipient) => Promise.resolve(isRecipientValid(recipient)),
getRecipientWarning: () => Promise.resolve(null), getRecipientWarning: () => Promise.resolve(null),
createTransaction: () => ({ createTransaction: () => ({
amount: BigNumber(0), amount: BigNumber(0),
recipient: '', recipient: '',
fee: BigNumber(0), fee: null,
tag: undefined, tag: undefined,
}), }),
@ -463,10 +510,30 @@ const RippleJSBridge: WalletBridge<Transaction> = {
getTransactionAmount: (a, t) => t.amount, getTransactionAmount: (a, t) => t.amount,
editTransactionRecipient: (account, t, recipient) => ({ editTransactionRecipient: (account, t, recipient) => {
...t, const parts = recipient.split('?')
recipient, const params = new URLSearchParams(parts[1])
}), recipient = parts[0]
// Extract parameters we may need
for (const [key, value] of params.entries()) {
switch (key) {
case 'dt':
t.tag = parseInt(value, 10) || 0
break
case 'amount':
t.amount = parseAPIValue(value || '0')
break
default:
// do nothing
}
}
return {
...t,
recipient,
}
},
EditFees, EditFees,
@ -474,27 +541,38 @@ const RippleJSBridge: WalletBridge<Transaction> = {
getTransactionRecipient: (a, t) => t.recipient, getTransactionRecipient: (a, t) => t.recipient,
isValidTransaction: (a, t) => (!t.amount.isZero() && t.recipient && true) || false, checkValidTransaction: async (a, t) => {
if (!t.fee) throw new FeeNotLoaded()
checkCanBeSpent: async (a, t) => {
const r = await getServerInfo(a.endpointConfig) const r = await getServerInfo(a.endpointConfig)
const reserveBaseXRP = parseAPIValue(r.validatedLedger.reserveBaseXRP)
if (t.recipient) {
if (await cachedRecipientIsNew(a.endpointConfig, t.recipient)) {
if (t.amount.lt(reserveBaseXRP)) {
const f = formatAPICurrencyXRP(reserveBaseXRP)
throw new NotEnoughBalanceBecauseDestinationNotCreated('', {
minimalAmount: `${f.currency} ${f.value}`,
})
}
}
}
if ( if (
t.amount t.amount
.plus(t.fee) .plus(t.fee || 0)
.plus(parseAPIValue(r.validatedLedger.reserveBaseXRP)) .plus(reserveBaseXRP)
.isLessThanOrEqualTo(a.balance) .isLessThanOrEqualTo(a.balance)
) { ) {
return return true
} }
throw new NotEnoughBalance() throw new NotEnoughBalance()
}, },
getTotalSpent: (a, t) => Promise.resolve(t.amount.plus(t.fee)), getTotalSpent: (a, t) => Promise.resolve(t.amount.plus(t.fee || 0)),
getMaxAmount: (a, t) => Promise.resolve(a.balance.minus(t.fee)), getMaxAmount: (a, t) => Promise.resolve(a.balance.minus(t.fee || 0)),
signAndBroadcast: (a, t, deviceId) => signAndBroadcast: (a, t, deviceId) =>
Observable.create(o => { Observable.create(o => {
delete cacheRecipientsNew[t.recipient]
let cancelled = false let cancelled = false
const isCancelled = () => cancelled const isCancelled = () => cancelled
const onSigned = () => { const onSigned = () => {

4
src/bridge/UnsupportedBridge.js

@ -27,13 +27,11 @@ const UnsupportedBridge: WalletBridge<*> = {
getTransactionAmount: () => BigNumber(0), getTransactionAmount: () => BigNumber(0),
isValidTransaction: () => false,
editTransactionRecipient: () => null, editTransactionRecipient: () => null,
getTransactionRecipient: () => '', getTransactionRecipient: () => '',
checkCanBeSpent: () => Promise.resolve(), checkValidTransaction: () => Promise.resolve(false),
getTotalSpent: () => Promise.resolve(BigNumber(0)), getTotalSpent: () => Promise.resolve(BigNumber(0)),

1
src/bridge/index.js

@ -12,6 +12,7 @@ const perFamily = {
bitcoin: LibcoreBridge, bitcoin: LibcoreBridge,
ripple: RippleJSBridge, ripple: RippleJSBridge,
ethereum: EthereumJSBridge, ethereum: EthereumJSBridge,
stellar: null,
} }
if (USE_MOCK_DATA) { if (USE_MOCK_DATA) {
const mockBridge = makeMockBridge() const mockBridge = makeMockBridge()

12
src/bridge/makeMockBridge.js

@ -6,11 +6,11 @@ import {
genAddingOperationsInAccount, genAddingOperationsInAccount,
genOperation, genOperation,
} from '@ledgerhq/live-common/lib/mock/account' } from '@ledgerhq/live-common/lib/mock/account'
import { getOperationAmountNumber } from '@ledgerhq/live-common/lib/helpers/operation' import { getOperationAmountNumber } from '@ledgerhq/live-common/lib/operation'
import Prando from 'prando' import Prando from 'prando'
import { BigNumber } from 'bignumber.js' import { BigNumber } from 'bignumber.js'
import type { Operation } from '@ledgerhq/live-common/lib/types' import type { Operation } from '@ledgerhq/live-common/lib/types'
import { validateNameEdition } from 'helpers/accountName' import { validateNameEdition } from '@ledgerhq/live-common/lib/account'
import { MOCK_DATA_SEED } from 'config/constants' import { MOCK_DATA_SEED } from 'config/constants'
import type { WalletBridge } from './types' import type { WalletBridge } from './types'
@ -18,7 +18,7 @@ const defaultOpts = {
scanAccountDeviceSuccessRate: 0.8, scanAccountDeviceSuccessRate: 0.8,
transactionsSizeTarget: 100, transactionsSizeTarget: 100,
extraInitialTransactionProps: () => null, extraInitialTransactionProps: () => null,
checkCanBeSpent: () => Promise.resolve(), checkValidTransaction: () => Promise.resolve(),
getTotalSpent: (a, t) => Promise.resolve(t.amount), getTotalSpent: (a, t) => Promise.resolve(t.amount),
getMaxAmount: a => Promise.resolve(a.balance), getMaxAmount: a => Promise.resolve(a.balance),
} }
@ -36,7 +36,7 @@ function makeMockBridge(opts?: Opts): WalletBridge<*> {
extraInitialTransactionProps, extraInitialTransactionProps,
getTotalSpent, getTotalSpent,
getMaxAmount, getMaxAmount,
checkCanBeSpent, checkValidTransaction,
} = { } = {
...defaultOpts, ...defaultOpts,
...opts, ...opts,
@ -155,9 +155,7 @@ function makeMockBridge(opts?: Opts): WalletBridge<*> {
EditAdvancedOptions, EditAdvancedOptions,
isValidTransaction: (a, t) => (t.amount > 0 && t.recipient && true) || false, checkValidTransaction,
checkCanBeSpent,
getTotalSpent, getTotalSpent,

7
src/bridge/types.js

@ -76,15 +76,16 @@ export interface WalletBridge<Transaction> {
getTransactionRecipient(account: Account, transaction: Transaction): string; getTransactionRecipient(account: Account, transaction: Transaction): string;
isValidTransaction(account: Account, transaction: Transaction): boolean;
// render the whole Fees section of the form // render the whole Fees section of the form
EditFees?: *; // React$ComponentType<EditProps<Transaction>>; EditFees?: *; // React$ComponentType<EditProps<Transaction>>;
// render the whole advanced part of the form // render the whole advanced part of the form
EditAdvancedOptions?: *; // React$ComponentType<EditProps<Transaction>>; EditAdvancedOptions?: *; // React$ComponentType<EditProps<Transaction>>;
checkCanBeSpent(account: Account, transaction: Transaction): Promise<void>; // validate the transaction and all currency specific validations here, we can return false
// to disable the button without throwing an error if we are handling the error on a different
// input or throw an error that will highlight the issue on the amount field
checkValidTransaction(account: Account, transaction: Transaction): Promise<boolean>;
getTotalSpent(account: Account, transaction: Transaction): Promise<BigNumber>; getTotalSpent(account: Account, transaction: Transaction): Promise<BigNumber>;

2
src/commands/debugAppInfosForCurrency.js

@ -1,6 +1,6 @@
// @flow // @flow
import { getCryptoCurrencyById } from '@ledgerhq/live-common/lib/helpers/currencies' import { getCryptoCurrencyById } from '@ledgerhq/live-common/lib/currencies'
import { createCommand, Command } from 'helpers/ipc' import { createCommand, Command } from 'helpers/ipc'
import { fromPromise } from 'rxjs/observable/fromPromise' import { fromPromise } from 'rxjs/observable/fromPromise'
import { withDevice } from 'helpers/deviceAccess' import { withDevice } from 'helpers/deviceAccess'

2
src/commands/getAddress.js

@ -1,6 +1,6 @@
// @flow // @flow
import { getCryptoCurrencyById } from '@ledgerhq/live-common/lib/helpers/currencies' import { getCryptoCurrencyById } from '@ledgerhq/live-common/lib/currencies'
import { createCommand, Command } from 'helpers/ipc' import { createCommand, Command } from 'helpers/ipc'
import { fromPromise } from 'rxjs/observable/fromPromise' import { fromPromise } from 'rxjs/observable/fromPromise'
import { withDevice } from 'helpers/deviceAccess' import { withDevice } from 'helpers/deviceAccess'

9
src/commands/index.js

@ -15,10 +15,11 @@ import installFinalFirmware from 'commands/installFinalFirmware'
import installMcu from 'commands/installMcu' import installMcu from 'commands/installMcu'
import installOsuFirmware from 'commands/installOsuFirmware' import installOsuFirmware from 'commands/installOsuFirmware'
import isDashboardOpen from 'commands/isDashboardOpen' import isDashboardOpen from 'commands/isDashboardOpen'
import killInternalProcess from 'commands/killInternalProcess'
import libcoreGetFees from 'commands/libcoreGetFees' import libcoreGetFees from 'commands/libcoreGetFees'
import libcoreGetVersion from 'commands/libcoreGetVersion' import libcoreGetVersion from 'commands/libcoreGetVersion'
import libcoreHardReset from 'commands/libcoreHardReset'
import libcoreScanAccounts from 'commands/libcoreScanAccounts' import libcoreScanAccounts from 'commands/libcoreScanAccounts'
import libcoreScanFromXPUB from 'commands/libcoreScanFromXPUB'
import libcoreSignAndBroadcast from 'commands/libcoreSignAndBroadcast' import libcoreSignAndBroadcast from 'commands/libcoreSignAndBroadcast'
import libcoreSyncAccount from 'commands/libcoreSyncAccount' import libcoreSyncAccount from 'commands/libcoreSyncAccount'
import libcoreValidAddress from 'commands/libcoreValidAddress' import libcoreValidAddress from 'commands/libcoreValidAddress'
@ -47,10 +48,11 @@ const all: Array<Command<any, any>> = [
installMcu, installMcu,
installOsuFirmware, installOsuFirmware,
isDashboardOpen, isDashboardOpen,
killInternalProcess,
libcoreGetFees, libcoreGetFees,
libcoreGetVersion, libcoreGetVersion,
libcoreHardReset,
libcoreScanAccounts, libcoreScanAccounts,
libcoreScanFromXPUB,
libcoreSignAndBroadcast, libcoreSignAndBroadcast,
libcoreSyncAccount, libcoreSyncAccount,
libcoreValidAddress, libcoreValidAddress,
@ -67,8 +69,11 @@ const all: Array<Command<any, any>> = [
uninstallApp, uninstallApp,
] ]
export const commandsById = {}
all.forEach(cmd => { all.forEach(cmd => {
invariant(!all.some(c => c !== cmd && c.id === cmd.id), `duplicate command '${cmd.id}'`) invariant(!all.some(c => c !== cmd && c.id === cmd.id), `duplicate command '${cmd.id}'`)
commandsById[cmd.id] = cmd
}) })
export default all export default all

18
src/commands/killInternalProcess.js

@ -0,0 +1,18 @@
// @flow
import { createCommand, Command } from 'helpers/ipc'
import { of } from 'rxjs'
type Input = void
type Result = boolean
const cmd: Command<Input, Result> = createCommand('killInternalProcess', () => {
setTimeout(() => {
// we assume commands are run on the internal process
// special exit code for better identification
process.exit(42)
})
return of(true)
})
export default cmd

39
src/commands/libcoreGetFees.js

@ -4,8 +4,15 @@ import { Observable } from 'rxjs'
import { BigNumber } from 'bignumber.js' import { BigNumber } from 'bignumber.js'
import withLibcore from 'helpers/withLibcore' import withLibcore from 'helpers/withLibcore'
import { createCommand, Command } from 'helpers/ipc' import { createCommand, Command } from 'helpers/ipc'
import * as accountIdHelper from 'helpers/accountId' import { getCryptoCurrencyById } from '@ledgerhq/live-common/lib/currencies'
import { isValidAddress, libcoreAmountToBigNumber, bigNumberToLibcoreAmount } from 'helpers/libcore' import { getWalletName } from '@ledgerhq/live-common/lib/account'
import type { Account, DerivationMode } from '@ledgerhq/live-common/lib/types'
import {
isValidAddress,
libcoreAmountToBigNumber,
bigNumberToLibcoreAmount,
getOrCreateWallet,
} from 'helpers/libcore'
import { InvalidAddress } from 'config/errors' import { InvalidAddress } from 'config/errors'
type BitcoinLikeTransaction = { type BitcoinLikeTransaction = {
@ -16,23 +23,43 @@ type BitcoinLikeTransaction = {
} }
type Input = { type Input = {
accountId: string,
accountIndex: number, accountIndex: number,
transaction: BitcoinLikeTransaction, transaction: BitcoinLikeTransaction,
currencyId: string,
derivationMode: DerivationMode,
seedIdentifier: string,
}
export const extractGetFeesInputFromAccount = (a: Account) => {
const currencyId = a.currency.id
return {
accountIndex: a.index,
currencyId,
derivationMode: a.derivationMode,
seedIdentifier: a.seedIdentifier,
}
} }
type Result = { totalFees: string } type Result = { totalFees: string }
const cmd: Command<Input, Result> = createCommand( const cmd: Command<Input, Result> = createCommand(
'libcoreGetFees', 'libcoreGetFees',
({ accountId, accountIndex, transaction }) => ({ currencyId, derivationMode, seedIdentifier, accountIndex, transaction }) =>
Observable.create(o => { Observable.create(o => {
let unsubscribed = false let unsubscribed = false
const isCancelled = () => unsubscribed const isCancelled = () => unsubscribed
const currency = getCryptoCurrencyById(currencyId)
withLibcore(async core => { withLibcore(async core => {
const { walletName } = accountIdHelper.decode(accountId) const walletName = getWalletName({
const njsWallet = await core.getPoolInstance().getWallet(walletName) currency,
derivationMode,
seedIdentifier,
})
const njsWallet = await getOrCreateWallet(core, walletName, {
currency,
derivationMode,
})
if (isCancelled()) return if (isCancelled()) return
const njsAccount = await njsWallet.getAccount(accountIndex) const njsAccount = await njsWallet.getAccount(accountIndex)
if (isCancelled()) return if (isCancelled()) return

19
src/commands/libcoreHardReset.js

@ -1,19 +0,0 @@
// @flow
import { createCommand } from 'helpers/ipc'
import { fromPromise } from 'rxjs/observable/fromPromise'
import withLibcore from 'helpers/withLibcore'
import { HardResetFail } from 'config/errors'
const cmd = createCommand('libcoreHardReset', () =>
fromPromise(
withLibcore(async core => {
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

29
src/commands/libcoreScanFromXPUB.js

@ -0,0 +1,29 @@
// @flow
import { fromPromise } from 'rxjs/observable/fromPromise'
import type { AccountRaw, DerivationMode } from '@ledgerhq/live-common/lib/types'
import { createCommand, Command } from 'helpers/ipc'
import withLibcore from 'helpers/withLibcore'
import { scanAccountsFromXPUB } from 'helpers/libcore'
type Input = {
currencyId: string,
xpub: string,
derivationMode: DerivationMode,
seedIdentifier: string,
}
type Result = AccountRaw
const cmd: Command<Input, Result> = createCommand(
'libcoreScanFromXPUB',
({ currencyId, xpub, derivationMode, seedIdentifier }) =>
fromPromise(
withLibcore(async core =>
scanAccountsFromXPUB({ core, currencyId, xpub, derivationMode, seedIdentifier }),
),
),
)
export default cmd

123
src/commands/libcoreSignAndBroadcast.js

@ -2,17 +2,23 @@
import logger from 'logger' import logger from 'logger'
import { BigNumber } from 'bignumber.js' import { BigNumber } from 'bignumber.js'
import type { OperationRaw } from '@ledgerhq/live-common/lib/types' import { StatusCodes } from '@ledgerhq/hw-transport'
import Btc from '@ledgerhq/hw-app-btc' import Btc from '@ledgerhq/hw-app-btc'
import { Observable } from 'rxjs' import { Observable } from 'rxjs'
import { getCryptoCurrencyById } from '@ledgerhq/live-common/lib/helpers/currencies' import { isSegwitDerivationMode } from '@ledgerhq/live-common/lib/derivation'
import { isSegwitPath } from 'helpers/bip32' import { getCryptoCurrencyById } from '@ledgerhq/live-common/lib/currencies'
import { libcoreAmountToBigNumber, bigNumberToLibcoreAmount } from 'helpers/libcore' import type { OperationRaw, DerivationMode, CryptoCurrency } from '@ledgerhq/live-common/lib/types'
import { getWalletName } from '@ledgerhq/live-common/lib/account'
import {
libcoreAmountToBigNumber,
bigNumberToLibcoreAmount,
getOrCreateWallet,
} from 'helpers/libcore'
import { UpdateYourApp } from 'config/errors'
import withLibcore from 'helpers/withLibcore' import withLibcore from 'helpers/withLibcore'
import { createCommand, Command } from 'helpers/ipc' import { createCommand, Command } from 'helpers/ipc'
import { withDevice } from 'helpers/deviceAccess' import { withDevice } from 'helpers/deviceAccess'
import * as accountIdHelper from 'helpers/accountId'
type BitcoinLikeTransaction = { type BitcoinLikeTransaction = {
amount: string, amount: string,
@ -22,10 +28,11 @@ type BitcoinLikeTransaction = {
type Input = { type Input = {
accountId: string, accountId: string,
blockHeight: number,
currencyId: string, currencyId: string,
derivationMode: DerivationMode,
seedIdentifier: string,
xpub: string, xpub: string,
freshAddress: string,
freshAddressPath: string,
index: number, index: number,
transaction: BitcoinLikeTransaction, transaction: BitcoinLikeTransaction,
deviceId: string, deviceId: string,
@ -37,17 +44,29 @@ type Result = { type: 'signed' } | { type: 'broadcasted', operation: OperationRa
const cmd: Command<Input, Result> = createCommand( const cmd: Command<Input, Result> = createCommand(
'libcoreSignAndBroadcast', 'libcoreSignAndBroadcast',
({ accountId, currencyId, xpub, freshAddress, freshAddressPath, index, transaction, deviceId }) => ({
accountId,
blockHeight,
currencyId,
derivationMode,
seedIdentifier,
xpub,
index,
transaction,
deviceId,
}) =>
Observable.create(o => { Observable.create(o => {
let unsubscribed = false let unsubscribed = false
const currency = getCryptoCurrencyById(currencyId)
const isCancelled = () => unsubscribed const isCancelled = () => unsubscribed
withLibcore(core => withLibcore(core =>
doSignAndBroadcast({ doSignAndBroadcast({
accountId, accountId,
currencyId, currency,
blockHeight,
derivationMode,
seedIdentifier,
xpub, xpub,
freshAddress,
freshAddressPath,
index, index,
transaction, transaction,
deviceId, deviceId,
@ -73,28 +92,33 @@ const cmd: Command<Input, Result> = createCommand(
async function signTransaction({ async function signTransaction({
hwApp, hwApp,
currencyId, currency,
blockHeight,
transaction, transaction,
derivationMode,
sigHashType, sigHashType,
supportsSegwit,
isSegwit,
hasTimestamp, hasTimestamp,
}: { }: {
hwApp: Btc, hwApp: Btc,
currencyId: string, currency: CryptoCurrency,
blockHeight: number,
transaction: *, transaction: *,
derivationMode: DerivationMode,
sigHashType: number, sigHashType: number,
supportsSegwit: boolean,
isSegwit: boolean,
hasTimestamp: boolean, hasTimestamp: boolean,
}) { }) {
const additionals = [] const additionals = []
let expiryHeight let expiryHeight
if (currencyId === 'bitcoin_cash' || currencyId === 'bitcoin_gold') additionals.push('bip143') if (currency.id === 'bitcoin_cash' || currency.id === 'bitcoin_gold') additionals.push('bip143')
if (currencyId === 'zcash') expiryHeight = Buffer.from([0x00, 0x00, 0x00, 0x00]) if (currency.id === 'zcash') {
expiryHeight = Buffer.from([0x00, 0x00, 0x00, 0x00])
if (blockHeight >= 419200) {
additionals.push('sapling')
}
}
const rawInputs = transaction.getInputs() const rawInputs = transaction.getInputs()
const hasExtraData = currencyId === 'zcash' const hasExtraData = currency.id === 'zcash'
const inputs = await Promise.all( const inputs = await Promise.all(
rawInputs.map(async input => { rawInputs.map(async input => {
@ -102,7 +126,7 @@ async function signTransaction({
const hexPreviousTransaction = Buffer.from(rawPreviousTransaction).toString('hex') const hexPreviousTransaction = Buffer.from(rawPreviousTransaction).toString('hex')
const previousTransaction = hwApp.splitTransaction( const previousTransaction = hwApp.splitTransaction(
hexPreviousTransaction, hexPreviousTransaction,
supportsSegwit, true, // set to true allow both segwit AND non-segwit
hasTimestamp, hasTimestamp,
hasExtraData, hasExtraData,
) )
@ -151,7 +175,7 @@ async function signTransaction({
outputScriptHex, outputScriptHex,
lockTime, lockTime,
sigHashType, sigHashType,
isSegwit, isSegwitDerivationMode(derivationMode),
initialTimestamp, initialTimestamp,
additionals, additionals,
expiryHeight, expiryHeight,
@ -162,10 +186,11 @@ async function signTransaction({
export async function doSignAndBroadcast({ export async function doSignAndBroadcast({
accountId, accountId,
currencyId, derivationMode,
blockHeight,
seedIdentifier,
currency,
xpub, xpub,
freshAddress,
freshAddressPath,
index, index,
transaction, transaction,
deviceId, deviceId,
@ -175,10 +200,11 @@ export async function doSignAndBroadcast({
onOperationBroadcasted, onOperationBroadcasted,
}: { }: {
accountId: string, accountId: string,
currencyId: string, derivationMode: DerivationMode,
seedIdentifier: string,
blockHeight: number,
currency: CryptoCurrency,
xpub: string, xpub: string,
freshAddress: string,
freshAddressPath: string,
index: number, index: number,
transaction: BitcoinLikeTransaction, transaction: BitcoinLikeTransaction,
deviceId: string, deviceId: string,
@ -187,8 +213,9 @@ export async function doSignAndBroadcast({
onSigned: () => void, onSigned: () => void,
onOperationBroadcasted: (optimisticOp: $Exact<OperationRaw>) => void, onOperationBroadcasted: (optimisticOp: $Exact<OperationRaw>) => void,
}): Promise<void> { }): Promise<void> {
const { walletName } = accountIdHelper.decode(accountId) const walletName = getWalletName({ currency, seedIdentifier, derivationMode })
const njsWallet = await core.getPoolInstance().getWallet(walletName)
const njsWallet = await getOrCreateWallet(core, walletName, { currency, derivationMode })
if (isCancelled()) return if (isCancelled()) return
const njsAccount = await njsWallet.getAccount(index) const njsAccount = await njsWallet.getAccount(index)
if (isCancelled()) return if (isCancelled()) return
@ -214,19 +241,22 @@ export async function doSignAndBroadcast({
const hasTimestamp = !!njsWalletCurrency.bitcoinLikeNetworkParameters.UsesTimestampedTransaction const hasTimestamp = !!njsWalletCurrency.bitcoinLikeNetworkParameters.UsesTimestampedTransaction
// TODO: const timestampDelay = njsWalletCurrency.bitcoinLikeNetworkParameters.TimestampDelay // TODO: const timestampDelay = njsWalletCurrency.bitcoinLikeNetworkParameters.TimestampDelay
const currency = getCryptoCurrencyById(currencyId)
const signedTransaction = await withDevice(deviceId)(async transport => const signedTransaction = await withDevice(deviceId)(async transport =>
signTransaction({ signTransaction({
hwApp: new Btc(transport), hwApp: new Btc(transport),
currencyId, currency,
blockHeight,
transaction: builded, transaction: builded,
sigHashType: parseInt(sigHashType, 16), sigHashType: parseInt(sigHashType, 16),
supportsSegwit: !!currency.supportsSegwit,
isSegwit: isSegwitPath(freshAddressPath),
hasTimestamp, hasTimestamp,
derivationMode,
}), }),
) ).catch(e => {
if (e && e.statusCode === StatusCodes.INCORRECT_P1_P2) {
throw new UpdateYourApp(`UpdateYourApp ${currency.id}`, currency)
}
throw e
})
if (!signedTransaction || isCancelled() || !njsAccount) return if (!signedTransaction || isCancelled() || !njsAccount) return
onSigned() onSigned()
@ -237,10 +267,20 @@ export async function doSignAndBroadcast({
.asBitcoinLikeAccount() .asBitcoinLikeAccount()
.broadcastRawTransaction(Array.from(Buffer.from(signedTransaction, 'hex'))) .broadcastRawTransaction(Array.from(Buffer.from(signedTransaction, 'hex')))
const senders = builded
.getInputs()
.map(input => input.getAddress())
.filter(a => a)
const recipients = builded
.getOutputs()
.map(output => output.getAddress())
.filter(a => a)
const fee = libcoreAmountToBigNumber(builded.getFees()) const fee = libcoreAmountToBigNumber(builded.getFees())
// NB we don't check isCancelled() because the broadcast is not cancellable now! // NB we don't check isCancelled() because the broadcast is not cancellable now!
onOperationBroadcasted({ const op: $Exact<OperationRaw> = {
id: `${xpub}-${txHash}-OUT`, id: `${xpub}-${txHash}-OUT`,
hash: txHash, hash: txHash,
type: 'OUT', type: 'OUT',
@ -250,12 +290,13 @@ export async function doSignAndBroadcast({
fee: fee.toString(), fee: fee.toString(),
blockHash: null, blockHash: null,
blockHeight: null, blockHeight: null,
// FIXME for senders and recipients, can we ask the libcore? senders,
senders: [freshAddress], recipients,
recipients: [transaction.recipient],
accountId, accountId,
date: new Date().toISOString(), date: new Date().toISOString(),
}) extra: {},
}
onOperationBroadcasted(op)
} }
export default cmd export default cmd

20
src/commands/libcoreSyncAccount.js

@ -1,6 +1,7 @@
// @flow // @flow
import type { AccountRaw } from '@ledgerhq/live-common/lib/types' import type { AccountRaw, DerivationMode } from '@ledgerhq/live-common/lib/types'
import { getCryptoCurrencyById } from '@ledgerhq/live-common/lib/currencies'
import { fromPromise } from 'rxjs/observable/fromPromise' import { fromPromise } from 'rxjs/observable/fromPromise'
import { createCommand, Command } from 'helpers/ipc' import { createCommand, Command } from 'helpers/ipc'
@ -9,15 +10,24 @@ import withLibcore from 'helpers/withLibcore'
type Input = { type Input = {
accountId: string, accountId: string,
freshAddressPath: string,
currencyId: string, currencyId: string,
xpub: string,
derivationMode: DerivationMode,
seedIdentifier: string,
index: number, index: number,
} }
type Result = AccountRaw type Result = { rawAccount: AccountRaw, requiresCacheFlush: boolean }
const cmd: Command<Input, Result> = createCommand('libcoreSyncAccount', accountInfos => const cmd: Command<Input, Result> = createCommand(
fromPromise(withLibcore(core => syncAccount({ ...accountInfos, core }))), 'libcoreSyncAccount',
({ currencyId, ...accountInfos }) =>
fromPromise(
withLibcore(core => {
const currency = getCryptoCurrencyById(currencyId)
return syncAccount({ ...accountInfos, currency, core })
}),
),
) )
export default cmd export default cmd

8
src/components/AccountPage/AccountHeaderActions.js

@ -7,7 +7,7 @@ import { translate } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
import type { Account } from '@ledgerhq/live-common/lib/types' import type { Account } from '@ledgerhq/live-common/lib/types'
import Tooltip from 'components/base/Tooltip' import Tooltip from 'components/base/Tooltip'
import isAccountEmpty from 'helpers/isAccountEmpty' import { isAccountEmpty } from '@ledgerhq/live-common/lib/account'
import { MODAL_SEND, MODAL_RECEIVE, MODAL_SETTINGS_ACCOUNT } from 'config/constants' import { MODAL_SEND, MODAL_RECEIVE, MODAL_SETTINGS_ACCOUNT } from 'config/constants'
@ -67,19 +67,19 @@ class AccountHeaderActions extends PureComponent<Props> {
<Button small primary onClick={() => openModal(MODAL_SEND, { account })}> <Button small primary onClick={() => openModal(MODAL_SEND, { account })}>
<Box horizontal flow={1} alignItems="center"> <Box horizontal flow={1} alignItems="center">
<IconSend size={12} /> <IconSend size={12} />
<Box>{t('app:send.title')}</Box> <Box>{t('send.title')}</Box>
</Box> </Box>
</Button> </Button>
<Button small primary onClick={() => openModal(MODAL_RECEIVE, { account })}> <Button small primary onClick={() => openModal(MODAL_RECEIVE, { account })}>
<Box horizontal flow={1} alignItems="center"> <Box horizontal flow={1} alignItems="center">
<IconReceive size={12} /> <IconReceive size={12} />
<Box>{t('app:receive.title')}</Box> <Box>{t('receive.title')}</Box>
</Box> </Box>
</Button> </Button>
</Fragment> </Fragment>
) : null} ) : null}
<Tooltip render={() => t('app:account.settings.title')}> <Tooltip render={() => t('account.settings.title')}>
<ButtonSettings onClick={() => openModal(MODAL_SETTINGS_ACCOUNT, { account })}> <ButtonSettings onClick={() => openModal(MODAL_SETTINGS_ACCOUNT, { account })}>
<Box justifyContent="center"> <Box justifyContent="center">
<IconAccountSettings size={16} /> <IconAccountSettings size={16} />

6
src/components/AccountPage/EmptyStateAccount.js

@ -40,9 +40,9 @@ class EmptyStateAccount extends PureComponent<Props, *> {
height="89" height="89"
/> />
<Box mt={5} alignItems="center"> <Box mt={5} alignItems="center">
<Title>{t('app:account.emptyState.title')}</Title> <Title>{t('account.emptyState.title')}</Title>
<Description mt={3} style={{ display: 'block' }}> <Description mt={3} style={{ display: 'block' }}>
<Trans i18nKey="app:account.emptyState.desc"> <Trans i18nKey="account.emptyState.desc">
{'Make sure the'} {'Make sure the'}
<Text ff="Open Sans|SemiBold" color="dark"> <Text ff="Open Sans|SemiBold" color="dark">
{account.currency.managerAppName} {account.currency.managerAppName}
@ -53,7 +53,7 @@ class EmptyStateAccount extends PureComponent<Props, *> {
<Button mt={5} primary onClick={() => openModal(MODAL_RECEIVE, { account })}> <Button mt={5} primary onClick={() => openModal(MODAL_RECEIVE, { account })}>
<Box horizontal flow={1} alignItems="center"> <Box horizontal flow={1} alignItems="center">
<IconReceive size={12} /> <IconReceive size={12} />
<Box>{t('app:account.emptyState.buttons.receiveFunds')}</Box> <Box>{t('account.emptyState.buttons.receiveFunds')}</Box>
</Box> </Box>
</Button> </Button>
</Box> </Box>

4
src/components/AccountPage/index.js

@ -8,7 +8,7 @@ import { Redirect } from 'react-router'
import type { Currency, Account } from '@ledgerhq/live-common/lib/types' import type { Currency, Account } from '@ledgerhq/live-common/lib/types'
import type { T } from 'types/common' import type { T } from 'types/common'
import { accountSelector } from 'reducers/accounts' import { accountSelector } from 'reducers/accounts'
import isAccountEmpty from 'helpers/isAccountEmpty' import { isAccountEmpty } from '@ledgerhq/live-common/lib/account'
import { import {
counterValueCurrencySelector, counterValueCurrencySelector,
localeSelector, localeSelector,
@ -97,7 +97,7 @@ class AccountPage extends PureComponent<Props> {
/> />
</Box> </Box>
<OperationsList account={account} title={t('app:account.lastOperations')} /> <OperationsList account={account} title={t('account.lastOperations')} />
<StickyBackToTop /> <StickyBackToTop />
</Fragment> </Fragment>

8
src/components/AdvancedOptions/BitcoinKind.js

@ -15,12 +15,12 @@ type Props = {
} }
export default translate()(({ isRBF, onChangeRBF, t }: Props) => ( export default translate()(({ isRBF, onChangeRBF, t }: Props) => (
<Spoiler title={t('app:send.steps.amount.advancedOptions')}> <Spoiler title={t('send.steps.amount.advancedOptions')}>
<Box horizontal align="center" flow={5}> <Box horizontal align="center" flow={5}>
<Box style={{ width: 200 }}> <Box style={{ width: 200 }}>
<Label> <Label>
<span>{t('app:send.steps.amount.useRBF')}</span> <span>{t('send.steps.amount.useRBF')}</span>
<LabelInfoTooltip ml={1} text={t('app:send.steps.amount.useRBF')} /> <LabelInfoTooltip ml={1} text={t('send.steps.amount.useRBF')} />
</Label> </Label>
</Box> </Box>
<Box grow> <Box grow>
@ -32,7 +32,7 @@ export default translate()(({ isRBF, onChangeRBF, t }: Props) => (
<Box horizontal align="flex-start" flow={5}> <Box horizontal align="flex-start" flow={5}>
<Box style={{ width: 200 }}> <Box style={{ width: 200 }}>
<Label> <Label>
<span>{t('app:send.steps.amount.message')}</span> <span>{t('send.steps.amount.message')}</span>
</Label> </Label>
</Box> </Box>
<Box grow> <Box grow>

4
src/components/AdvancedOptions/EthereumKind.js

@ -15,11 +15,11 @@ type Props = {
} }
export default translate()(({ gasLimit, onChangeGasLimit, t }: Props) => ( export default translate()(({ gasLimit, onChangeGasLimit, t }: Props) => (
<Spoiler title={t('app:send.steps.amount.advancedOptions')}> <Spoiler title={t('send.steps.amount.advancedOptions')}>
<Box horizontal align="center" flow={5}> <Box horizontal align="center" flow={5}>
<Box style={{ width: 200 }}> <Box style={{ width: 200 }}>
<Label> <Label>
<span>{t('app:send.steps.amount.ethereumGasLimit')}</span> <span>{t('send.steps.amount.ethereumGasLimit')}</span>
</Label> </Label>
</Box> </Box>
<Box grow> <Box grow>

53
src/components/AdvancedOptions/RippleKind.js

@ -1,11 +1,11 @@
// @flow // @flow
import React from 'react' import React, { Component } from 'react'
import { BigNumber } from 'bignumber.js'
import { translate } from 'react-i18next' import { translate } from 'react-i18next'
import Box from 'components/base/Box' import Box from 'components/base/Box'
import Input from 'components/base/Input' import Input from 'components/base/Input'
import Label from 'components/base/Label' import Label from 'components/base/Label'
import Spoiler from 'components/base/Spoiler'
type Props = { type Props = {
tag: ?number, tag: ?number,
@ -13,24 +13,33 @@ type Props = {
t: *, t: *,
} }
export default translate()(({ tag, onChangeTag, t }: Props) => ( const uint32maxPlus1 = BigNumber(2).pow(32)
<Spoiler title={t('app:send.steps.amount.advancedOptions')}>
<Box horizontal align="center" flow={5}> class RippleKind extends Component<Props> {
<Box style={{ width: 200 }}> onChange = str => {
<Label> const { onChangeTag } = this.props
<span>{t('app:send.steps.amount.rippleTag')}</span> const tag = BigNumber(str.replace(/[^0-9]/g, ''))
</Label> if (!tag.isNaN() && tag.isFinite()) {
</Box> if (tag.isInteger() && tag.isPositive() && tag.lt(uint32maxPlus1)) {
<Box grow> onChangeTag(tag.toNumber())
<Input }
value={String(tag || '')} } else {
onChange={str => { onChangeTag(undefined)
const tag = parseInt(str, 10) }
if (!isNaN(tag) && isFinite(tag)) onChangeTag(tag) }
else onChangeTag(undefined) render() {
}} const { tag, t } = this.props
/> return (
<Box vertical flow={5}>
<Box grow>
<Label>
<span>{t('send.steps.amount.rippleTag')}</span>
</Label>
<Input value={String(tag || '')} onChange={this.onChange} />
</Box>
</Box> </Box>
</Box> )
</Spoiler> }
)) }
export default translate()(RippleKind)

14
src/components/BalanceSummary/BalanceInfos.js

@ -56,11 +56,7 @@ export function BalanceSincePercent(props: BalanceSinceProps) {
withIcon withIcon
/> />
)} )}
{!isAvailable ? ( {!isAvailable ? <PlaceholderLine dark width={60} /> : <Sub>{t(`time.since.${since}`)}</Sub>}
<PlaceholderLine dark width={60} />
) : (
<Sub>{t(`app:time.since.${since}`)}</Sub>
)}
</Box> </Box>
) )
} }
@ -82,11 +78,7 @@ export function BalanceSinceDiff(props: Props) {
withIcon withIcon
/> />
)} )}
{!isAvailable ? ( {!isAvailable ? <PlaceholderLine dark width={60} /> : <Sub>{t(`time.since.${since}`)}</Sub>}
<PlaceholderLine dark width={60} />
) : (
<Sub>{t(`app:time.since.${since}`)}</Sub>
)}
</Box> </Box>
) )
} }
@ -127,7 +119,7 @@ function BalanceInfos(props: Props) {
isAvailable={isAvailable} isAvailable={isAvailable}
totalBalance={totalBalance} totalBalance={totalBalance}
> >
<Sub>{t('app:dashboard.totalBalance')}</Sub> <Sub>{t('dashboard.totalBalance')}</Sub>
</BalanceTotal> </BalanceTotal>
<BalanceSincePercent <BalanceSincePercent
alignItems="flex-end" alignItems="flex-end"

2
src/components/BalanceSummary/index.js

@ -3,7 +3,7 @@
import React, { Fragment } from 'react' import React, { Fragment } from 'react'
import { BigNumber } from 'bignumber.js' import { BigNumber } from 'bignumber.js'
import moment from 'moment' import moment from 'moment'
import { formatShort } from '@ledgerhq/live-common/lib/helpers/currencies' import { formatShort } from '@ledgerhq/live-common/lib/currencies'
import type { Currency, Account } from '@ledgerhq/live-common/lib/types' import type { Currency, Account } from '@ledgerhq/live-common/lib/types'
import Chart from 'components/base/Chart' import Chart from 'components/base/Chart'

2
src/components/BalanceSummary/stories.js

@ -4,7 +4,7 @@ import React from 'react'
import { storiesOf } from '@storybook/react' import { storiesOf } from '@storybook/react'
import { number } from '@storybook/addon-knobs' import { number } from '@storybook/addon-knobs'
import { translate } from 'react-i18next' import { translate } from 'react-i18next'
import { getFiatCurrencyByTicker } from '@ledgerhq/live-common/lib/helpers/currencies' import { getFiatCurrencyByTicker } from '@ledgerhq/live-common/lib/currencies'
import BalanceInfos from './BalanceInfos' import BalanceInfos from './BalanceInfos'

2
src/components/CalculateBalance.js

@ -6,7 +6,7 @@ import { connect } from 'react-redux'
import { BigNumber } from 'bignumber.js' import { BigNumber } from 'bignumber.js'
import type { Account } from '@ledgerhq/live-common/lib/types' import type { Account } from '@ledgerhq/live-common/lib/types'
import { getBalanceHistorySum } from '@ledgerhq/live-common/lib/helpers/account' import { getBalanceHistorySum } from '@ledgerhq/live-common/lib/account'
import CounterValues from 'helpers/countervalues' import CounterValues from 'helpers/countervalues'
import { import {
exchangeSettingsForAccountSelector, exchangeSettingsForAccountSelector,

2
src/components/CounterValue/stories.js

@ -1,7 +1,7 @@
// @flow // @flow
import React from 'react' import React from 'react'
import { getCryptoCurrencyById } from '@ledgerhq/live-common/lib/helpers/currencies' import { getCryptoCurrencyById } from '@ledgerhq/live-common/lib/currencies'
import { storiesOf } from '@storybook/react' import { storiesOf } from '@storybook/react'
import { number } from '@storybook/addon-knobs' import { number } from '@storybook/addon-knobs'

181
src/components/CurrenciesStatusBanner.js

@ -0,0 +1,181 @@
// @flow
import React, { PureComponent } from 'react'
import { compose } from 'redux'
import { translate } from 'react-i18next'
import { connect } from 'react-redux'
import { createStructuredSelector } from 'reselect'
import styled from 'styled-components'
import type { Currency } from '@ledgerhq/live-common/lib/types'
import { colors } from 'styles/theme'
import { openURL } from 'helpers/linking'
import { CHECK_CUR_STATUS_INTERVAL } from 'config/constants'
import IconCross from 'icons/Cross'
import IconTriangleWarning from 'icons/TriangleWarning'
import IconChevronRight from 'icons/ChevronRight'
import { dismissedBannersSelector } from 'reducers/settings'
import { currenciesStatusSelector, fetchCurrenciesStatus } from 'reducers/currenciesStatus'
import { currenciesSelector } from 'reducers/accounts'
import { dismissBanner } from 'actions/settings'
import type { CurrencyStatus } from 'reducers/currenciesStatus'
import Box from 'components/base/Box'
const mapStateToProps = createStructuredSelector({
dismissedBanners: dismissedBannersSelector,
accountsCurrencies: currenciesSelector,
currenciesStatus: currenciesStatusSelector,
})
const mapDispatchToProps = {
dismissBanner,
fetchCurrenciesStatus,
}
const getItemKey = (item: CurrencyStatus) => `${item.id}_${item.nonce}`
const CloseIconContainer = styled.div`
position: absolute;
top: 0;
right: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 10px;
border-bottom-left-radius: 4px;
opacity: 0.5;
&:hover {
opacity: 1;
}
`
const CloseIcon = (props: *) => (
<CloseIconContainer {...props}>
<IconCross size={16} color="white" />
</CloseIconContainer>
)
type Props = {
accountsCurrencies: Currency[],
dismissedBanners: string[],
dismissBanner: string => void,
currenciesStatus: CurrencyStatus[],
fetchCurrenciesStatus: () => Promise<void>,
t: *,
}
class CurrenciesStatusBanner extends PureComponent<Props> {
componentDidMount() {
this.pollStatus()
}
componentWillUnmount() {
this.unmounted = true
if (this.timeout) {
clearTimeout(this.timeout)
}
}
unmounted = false
timeout: *
pollStatus = async () => {
await this.props.fetchCurrenciesStatus()
if (this.unmounted) return
this.timeout = setTimeout(this.pollStatus, CHECK_CUR_STATUS_INTERVAL)
}
dismiss = item => this.props.dismissBanner(getItemKey(item))
render() {
const { dismissedBanners, accountsCurrencies, currenciesStatus, t } = this.props
const filtered = currenciesStatus.filter(
item =>
accountsCurrencies.find(cur => cur.id === item.id) &&
dismissedBanners.indexOf(getItemKey(item)) === -1,
)
if (!filtered.length) return null
return (
<Box flow={2} style={styles.container}>
{filtered.map(r => <BannerItem key={r.id} t={t} item={r} onItemDismiss={this.dismiss} />)}
</Box>
)
}
}
class BannerItem extends PureComponent<{
item: CurrencyStatus,
onItemDismiss: CurrencyStatus => void,
t: *,
}> {
onLinkClick = () => openURL(this.props.item.link)
dismiss = () => this.props.onItemDismiss(this.props.item)
render() {
const { item, t } = this.props
return (
<Box relative key={item.id} style={styles.banner}>
<CloseIcon onClick={this.dismiss} />
<Box horizontal flow={2}>
<IconTriangleWarning height={16} width={16} color="white" />
<Box shrink ff="Open Sans|SemiBold">
{item.message}
</Box>
</Box>
{item.link && <BannerItemLink t={t} onClick={this.onLinkClick} />}
</Box>
)
}
}
const UnderlinedLink = styled.span`
border-bottom: 1px solid transparent;
&:hover {
border-bottom-color: white;
}
`
const BannerItemLink = ({ t, onClick }: { t: *, onClick: void => * }) => (
<Box
mt={2}
ml={4}
flow={1}
horizontal
align="center"
cursor="pointer"
onClick={onClick}
color="white"
>
<IconChevronRight size={16} color="white" />
<UnderlinedLink>{t('common.learnMore')}</UnderlinedLink>
</Box>
)
const styles = {
container: {
position: 'fixed',
left: 32,
bottom: 32,
},
banner: {
background: colors.alertRed,
overflow: 'hidden',
borderRadius: 4,
fontSize: 13,
padding: 14,
color: 'white',
fontWeight: 'bold',
paddingRight: 50,
width: 350,
},
}
export default compose(
connect(
mapStateToProps,
mapDispatchToProps,
),
translate(),
)(CurrenciesStatusBanner)

68
src/components/CurrencyDownStatusAlert.js

@ -0,0 +1,68 @@
// @flow
import React, { PureComponent } from 'react'
import { translate } from 'react-i18next'
import styled from 'styled-components'
import { connect } from 'react-redux'
import { createStructuredSelector } from 'reselect'
import type { CurrencyStatus } from 'reducers/currenciesStatus'
import { currencyDownStatus } from 'reducers/currenciesStatus'
import { openURL } from 'helpers/linking'
import Box from 'components/base/Box'
import IconTriangleWarning from 'icons/TriangleWarning'
import IconExternalLink from 'icons/ExternalLink'
type Props = {
t: *,
status: ?CurrencyStatus,
}
const CurrencyDownBox = styled(Box).attrs({
horizontal: true,
align: 'center',
color: 'white',
borderRadius: 1,
fontSize: 1,
px: 4,
py: 2,
mb: 4,
})`
background-color: ${p => p.theme.colors.alertRed};
`
const Link = styled.span`
margin-left: 5px;
margin-right: 5px;
text-decoration: underline;
cursor: pointer;
`
class CurrencyDownStatusAlert extends PureComponent<Props> {
onClick = () => {
const { status } = this.props
if (status) openURL(status.link)
}
render() {
const { status, t } = this.props
if (!status) return null
return (
<CurrencyDownBox>
<Box mr={2}>
<IconTriangleWarning height={16} width={16} />
</Box>
<Box style={{ display: 'block' }} ff="Open Sans|SemiBold" fontSize={3} horizontal shrink>
{status.message}
<Link onClick={this.onClick}>{t('common.learnMore')}</Link>
<IconExternalLink size={12} />
</Box>
</CurrencyDownBox>
)
}
}
export default connect(
createStructuredSelector({
status: currencyDownStatus,
}),
)(translate()(CurrencyDownStatusAlert))

57
src/components/CurrentAddress/index.js

@ -3,7 +3,7 @@
import React, { PureComponent } from 'react' import React, { PureComponent } from 'react'
import { Trans, translate } from 'react-i18next' import { Trans, translate } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
import { encodeURIScheme } from '@ledgerhq/live-common/lib/helpers/currencies' import { encodeURIScheme } from '@ledgerhq/live-common/lib/currencies'
import type { Account } from '@ledgerhq/live-common/lib/types' import type { Account } from '@ledgerhq/live-common/lib/types'
import noop from 'lodash/noop' import noop from 'lodash/noop'
@ -143,7 +143,26 @@ class CurrentAddress extends PureComponent<Props, { copyFeedback: boolean }> {
copyFeedback: false, copyFeedback: false,
} }
_isUnmounted = false componentWillUnmount() {
if (this._timeout) clearTimeout(this._timeout)
}
renderCopy = copy => {
const { t } = this.props
return (
<FooterButton
icon={<IconCopy size={16} />}
label={t('common.copyAddress')}
onClick={() => {
this.setState({ copyFeedback: true })
this._timeout = setTimeout(() => this.setState({ copyFeedback: false }), 1e3)
copy()
}}
/>
)
}
_timeout: ?TimeoutID = null
render() { render() {
const { const {
@ -174,17 +193,17 @@ class CurrentAddress extends PureComponent<Props, { copyFeedback: boolean }> {
<Label> <Label>
<Box> <Box>
{accountName ? ( {accountName ? (
<Trans i18nKey="app:currentAddress.for" parent="div"> <Trans i18nKey="currentAddress.for" parent="div">
{'Address for '} {'Address for '}
<strong>{accountName}</strong> <strong>{accountName}</strong>
</Trans> </Trans>
) : ( ) : (
t('app:currentAddress.title') t('currentAddress.title')
)} )}
</Box> </Box>
</Label> </Label>
<Address> <Address>
{copyFeedback && <CopyFeedback>{t('app:common.addressCopied')}</CopyFeedback>} {copyFeedback && <CopyFeedback>{t('common.addressCopied')}</CopyFeedback>}
{address} {address}
</Address> </Address>
<Box horizontal flow={2} mt={2} alignItems="center" style={{ maxWidth: 320 }}> <Box horizontal flow={2} mt={2} alignItems="center" style={{ maxWidth: 320 }}>
@ -198,39 +217,21 @@ class CurrentAddress extends PureComponent<Props, { copyFeedback: boolean }> {
ff="Open Sans" ff="Open Sans"
> >
{isAddressVerified === null {isAddressVerified === null
? t('app:currentAddress.messageIfUnverified', { currencyName }) ? t('currentAddress.messageIfUnverified', { currencyName })
: isAddressVerified : isAddressVerified
? t('app:currentAddress.messageIfAccepted', { currencyName }) ? t('currentAddress.messageIfAccepted', { currencyName })
: t('app:currentAddress.messageIfSkipped', { currencyName })} : t('currentAddress.messageIfSkipped', { currencyName })}
</Box> </Box>
</Box> </Box>
<Footer> <Footer>
{isAddressVerified !== null ? ( {isAddressVerified !== null ? (
<FooterButton <FooterButton
icon={<IconRecheck size={16} />} icon={<IconRecheck size={16} />}
label={ label={isAddressVerified === false ? t('common.verify') : t('common.reverify')}
isAddressVerified === false ? t('app:common.verify') : t('app:common.reverify')
}
onClick={onVerify} onClick={onVerify}
/> />
) : null} ) : null}
<CopyToClipboard <CopyToClipboard data={address} render={this.renderCopy} />
data={address}
render={copy => (
<FooterButton
icon={<IconCopy size={16} />}
label={t('app:common.copyAddress')}
onClick={() => {
this.setState({ copyFeedback: true })
setTimeout(() => {
if (this._isUnmounted) return
this.setState({ copyFeedback: false })
}, 1e3)
copy()
}}
/>
)}
/>
</Footer> </Footer>
</Container> </Container>
) )

2
src/components/DashboardPage/AccountCard/index.js

@ -69,7 +69,7 @@ class AccountCard extends PureComponent<{
render() { render() {
const { counterValue, account, onClick, daysCount, ...props } = this.props const { counterValue, account, onClick, daysCount, ...props } = this.props
return ( return (
<Wrapper onClick={this.onClick} {...props}> <Wrapper onClick={this.onClick} {...props} data-e2e="dashboard_AccountCardWrapper">
<Box flow={4}> <Box flow={4}>
<AccountCardHeader accountName={account.name} currency={account.currency} /> <AccountCardHeader accountName={account.name} currency={account.currency} />
<Bar size={1} color="fog" /> <Bar size={1} color="fog" />

1
src/components/DashboardPage/AccountCardList.js

@ -28,6 +28,7 @@ class AccountCardList extends Component<Props> {
justifyContent="flex-start" justifyContent="flex-start"
alignItems="center" alignItems="center"
style={{ margin: '0 -16px' }} style={{ margin: '0 -16px' }}
data-e2e="dashboard_AccountList"
> >
{accounts {accounts
.map(account => ({ .map(account => ({

4
src/components/DashboardPage/AccountCardListHeader.js

@ -19,8 +19,8 @@ class AccountCardListHeader extends PureComponent<Props> {
return ( return (
<Box horizontal alignItems="flex-end"> <Box horizontal alignItems="flex-end">
<Text color="dark" ff="Museo Sans" fontSize={6}> <Text color="dark" ff="Museo Sans" fontSize={6} data-e2e="dashboard_AccountCount">
{t('app:dashboard.accounts.title', { count: accountsLength })} {t('dashboard.accounts.title', { count: accountsLength })}
</Text> </Text>
<Box ml="auto" horizontal flow={1}> <Box ml="auto" horizontal flow={1}>
<AccountsOrder /> <AccountsOrder />

6
src/components/DashboardPage/AccountCardPlaceholder.js

@ -31,7 +31,7 @@ class AccountCardPlaceholder extends PureComponent<{
render() { render() {
const { t } = this.props const { t } = this.props
return ( return (
<Wrapper> <Wrapper data-e2e="dashboard_AccountPlaceOrder">
<Box mt={2}> <Box mt={2}>
<img alt="" src={i('empty-account-tile.svg')} /> <img alt="" src={i('empty-account-tile.svg')} />
</Box> </Box>
@ -44,10 +44,10 @@ class AccountCardPlaceholder extends PureComponent<{
textAlign="center" textAlign="center"
style={{ maxWidth: 150 }} style={{ maxWidth: 150 }}
> >
{t('app:dashboard.emptyAccountTile.desc')} {t('dashboard.emptyAccountTile.desc')}
</Box> </Box>
<Button primary onClick={this.onAddAccounts}> <Button primary onClick={this.onAddAccounts}>
{t('app:dashboard.emptyAccountTile.createAccount')} {t('dashboard.emptyAccountTile.createAccount')}
</Button> </Button>
</Wrapper> </Wrapper>
) )

8
src/components/DashboardPage/AccountsOrder.js

@ -91,11 +91,11 @@ class AccountsOrder extends Component<Props> {
return [ return [
{ {
key: 'name', key: 'name',
label: t('app:dashboard.accountsOrder.name'), label: t('dashboard.accountsOrder.name'),
}, },
{ {
key: 'balance', key: 'balance',
label: t('app:dashboard.accountsOrder.balance'), label: t('dashboard.accountsOrder.balance'),
}, },
].map(item => ({ ].map(item => ({
...item, ...item,
@ -141,7 +141,7 @@ class AccountsOrder extends Component<Props> {
> >
<Track onUpdate event="ChangeSort" orderAccounts={orderAccounts} /> <Track onUpdate event="ChangeSort" orderAccounts={orderAccounts} />
<Text ff="Open Sans|SemiBold" fontSize={4}> <Text ff="Open Sans|SemiBold" fontSize={4}>
{t('app:common.sortBy')} {t('common.sortBy')}
</Text> </Text>
<Box <Box
alignItems="center" alignItems="center"
@ -152,7 +152,7 @@ class AccountsOrder extends Component<Props> {
horizontal horizontal
> >
<Text color="dark"> <Text color="dark">
{t(`app:dashboard.accountsOrder.${this.getCurrentValue() || 'balance'}`)} {t(`dashboard.accountsOrder.${this.getCurrentValue() || 'balance'}`)}
</Text> </Text>
<IconAngleDown size={16} /> <IconAngleDown size={16} />
</Box> </Box>

8
src/components/DashboardPage/CurrentGreetings.js

@ -10,18 +10,18 @@ const getCurrentGreetings = () => {
const afternoon_breakpoint = 12 const afternoon_breakpoint = 12
const evening_breakpoint = 17 const evening_breakpoint = 17
if (localTimeHour >= afternoon_breakpoint && localTimeHour < evening_breakpoint) { if (localTimeHour >= afternoon_breakpoint && localTimeHour < evening_breakpoint) {
return 'app:dashboard.greeting.afternoon' return 'dashboard.greeting.afternoon'
} else if (localTimeHour >= evening_breakpoint) { } else if (localTimeHour >= evening_breakpoint) {
return 'app:dashboard.greeting.evening' return 'dashboard.greeting.evening'
} }
return 'app:dashboard.greeting.morning' return 'dashboard.greeting.morning'
} }
class CurrentGettings extends PureComponent<{ t: T }> { class CurrentGettings extends PureComponent<{ t: T }> {
render() { render() {
const { t } = this.props const { t } = this.props
return ( return (
<Text color="dark" ff="Museo Sans" fontSize={7}> <Text color="dark" ff="Museo Sans" fontSize={7} data-e2e="dashboard_currentGettings">
{t(getCurrentGreetings())} {t(getCurrentGreetings())}
</Text> </Text>
) )

8
src/components/DashboardPage/EmptyState.js

@ -45,9 +45,9 @@ class EmptyState extends PureComponent<Props, *> {
height="157" height="157"
/> />
<Box mt={5} alignItems="center"> <Box mt={5} alignItems="center">
<Title data-e2e="dashboard_empty_title">{t('app:emptyState.dashboard.title')}</Title> <Title data-e2e="dashboard_empty_title">{t('emptyState.dashboard.title')}</Title>
<Description mt={3} style={{ maxWidth: 600 }}> <Description mt={3} style={{ maxWidth: 600 }}>
{t('app:emptyState.dashboard.desc')} {t('emptyState.dashboard.desc')}
</Description> </Description>
<Box mt={5} horizontal style={{ width: 300 }} flow={3} justify="center"> <Box mt={5} horizontal style={{ width: 300 }} flow={3} justify="center">
<Button <Button
@ -56,7 +56,7 @@ class EmptyState extends PureComponent<Props, *> {
onClick={this.handleInstallApp} onClick={this.handleInstallApp}
data-e2e="dashboard_empty_OpenManager" data-e2e="dashboard_empty_OpenManager"
> >
{t('app:emptyState.dashboard.buttons.installApp')} {t('emptyState.dashboard.buttons.installApp')}
</Button> </Button>
<Button <Button
outline outline
@ -64,7 +64,7 @@ class EmptyState extends PureComponent<Props, *> {
onClick={() => openModal(MODAL_ADD_ACCOUNTS)} onClick={() => openModal(MODAL_ADD_ACCOUNTS)}
data-e2e="dashboard_empty_AddAccounts" data-e2e="dashboard_empty_AddAccounts"
> >
{t('app:emptyState.dashboard.buttons.addAccount')} {t('emptyState.dashboard.buttons.addAccount')}
</Button> </Button>
</Box> </Box>
</Box> </Box>

9
src/components/DashboardPage/SummaryDesc.js

@ -12,8 +12,13 @@ class SummaryDesc extends PureComponent<{
render() { render() {
const { totalAccounts, t } = this.props const { totalAccounts, t } = this.props
return ( return (
<Text color="grey" fontSize={5} ff="Museo Sans|Light"> <Text
{t('app:dashboard.summary', { count: totalAccounts })} color="grey"
fontSize={5}
ff="Museo Sans|Light"
data-e2e="dashboard_accountsSummaryDesc"
>
{t('dashboard.summary', { count: totalAccounts })}
</Text> </Text>
) )
} }

2
src/components/DashboardPage/index.js

@ -129,7 +129,7 @@ class DashboardPage extends PureComponent<Props> {
<OperationsList <OperationsList
onAccountClick={this.onAccountClick} onAccountClick={this.onAccountClick}
accounts={accounts} accounts={accounts}
title={t('app:dashboard.recentActivity')} title={t('dashboard.recentActivity')}
withAccount withAccount
/> />
)} )}

287
src/components/DevToolsPage/AccountImporter.js

@ -0,0 +1,287 @@
// @flow
/* eslint-disable react/no-multi-comp */
import React, { PureComponent, Fragment } from 'react'
import invariant from 'invariant'
import { connect } from 'react-redux'
import type { Currency, Account, DerivationMode } from '@ledgerhq/live-common/lib/types'
import { decodeAccount } from 'reducers/accounts'
import { addAccount } from 'actions/accounts'
import FakeLink from 'components/base/FakeLink'
import Ellipsis from 'components/base/Ellipsis'
import Switch from 'components/base/Switch'
import Spinner from 'components/base/Spinner'
import Box, { Card } from 'components/base/Box'
import TranslatedError from 'components/TranslatedError'
import Button from 'components/base/Button'
import Input from 'components/base/Input'
import Label from 'components/base/Label'
import SelectCurrency from 'components/SelectCurrency'
import { CurrencyCircleIcon } from 'components/base/CurrencyBadge'
import { idleCallback } from 'helpers/promise'
import scanFromXPUB from 'commands/libcoreScanFromXPUB'
const mapDispatchToProps = {
addAccount,
}
type Props = {
addAccount: Account => void,
}
type ImportableAccountType = {
name: string,
currency: Currency,
derivationMode: DerivationMode,
xpub: string,
}
type State = {
status: string,
importableAccounts: ImportableAccountType[],
currency: ?Currency,
xpub: string,
name: string,
isSegwit: boolean,
isUnsplit: boolean,
error: ?Error,
}
const INITIAL_STATE = {
status: 'idle',
currency: null,
xpub: '',
name: 'dev',
isSegwit: true,
isUnsplit: false,
error: null,
importableAccounts: [],
}
class AccountImporter extends PureComponent<Props, State> {
state = INITIAL_STATE
onChangeCurrency = currency => {
if (currency.family !== 'bitcoin') return
this.setState({
currency,
isSegwit: !!currency.supportsSegwit,
isUnsplit: false,
})
}
onChangeXPUB = xpub => this.setState({ xpub })
onChangeSegwit = isSegwit => this.setState({ isSegwit })
onChangeUnsplit = isUnsplit => this.setState({ isUnsplit })
onChangeName = name => this.setState({ name })
isValid = () => {
const { currency, xpub, status } = this.state
return !!currency && !!xpub && status !== 'scanning'
}
scan = async () => {
this.setState({ status: 'scanning' })
const { importableAccounts } = this.state
try {
for (let i = 0; i < importableAccounts.length; i++) {
const a = importableAccounts[i]
const scanPayload = {
seedIdentifier: `dev_${a.xpub}`,
currencyId: a.currency.id,
xpub: a.xpub,
derivationMode: a.derivationMode,
}
const rawAccount = await scanFromXPUB.send(scanPayload).toPromise()
const account = decodeAccount(rawAccount)
await this.import({
...account,
name: a.name,
})
this.removeImportableAccount(a)
}
this.reset()
} catch (error) {
this.setState({ status: 'error', error })
}
}
addToScan = () => {
const { xpub, currency, isSegwit, isUnsplit, name } = this.state
const derivationMode = isSegwit
? isUnsplit
? 'segwit_unsplit'
: 'segwit'
: isUnsplit
? 'unsplit'
: ''
const importableAccount = { xpub, currency, derivationMode, name }
this.setState(({ importableAccounts }) => ({
importableAccounts: [...importableAccounts, importableAccount],
currency: null,
xpub: '',
name: 'dev',
isSegwit: true,
isUnsplit: false,
}))
}
removeImportableAccount = importableAccount => {
this.setState(({ importableAccounts }) => ({
importableAccounts: importableAccounts.filter(i => i.xpub !== importableAccount.xpub),
}))
}
import = async account => {
invariant(account, 'no account')
await idleCallback()
this.props.addAccount(account)
}
reset = () => this.setState(INITIAL_STATE)
render() {
const {
currency,
xpub,
name,
isSegwit,
isUnsplit,
status,
error,
importableAccounts,
} = this.state
const supportsSplit = !!currency && !!currency.forkedFrom
return (
<Fragment>
<Card title="Import from xpub" flow={3}>
{status === 'idle' || status === 'scanning' ? (
<Fragment>
<Box flow={1}>
<Label>{'currency'}</Label>
<SelectCurrency autoFocus value={currency} onChange={this.onChangeCurrency} />
</Box>
{currency && (currency.supportsSegwit || supportsSplit) ? (
<Box horizontal justify="flex-end" align="center" flow={3}>
{supportsSplit && (
<Box horizontal align="center" flow={1}>
<Box ff="Museo Sans|Bold" fontSize={4}>
{'unsplit'}
</Box>
<Switch isChecked={isUnsplit} onChange={this.onChangeUnsplit} />
</Box>
)}
{currency.supportsSegwit && (
<Box horizontal align="center" flow={1}>
<Box ff="Museo Sans|Bold" fontSize={4}>
{'segwit'}
</Box>
<Switch isChecked={isSegwit} onChange={this.onChangeSegwit} />
</Box>
)}
</Box>
) : null}
<Box flow={1}>
<Label>{'xpub'}</Label>
<Input
placeholder="xpub"
value={xpub}
onChange={this.onChangeXPUB}
onEnter={this.addToScan}
/>
</Box>
<Box flow={1}>
<Label>{'name'}</Label>
<Input
placeholder="name"
value={name}
onChange={this.onChangeName}
onEnter={this.addToScan}
/>
</Box>
<Box align="flex-end">
<Button primary small disabled={!this.isValid()} onClick={this.addToScan}>
{'add to scan'}
</Button>
</Box>
</Fragment>
) : status === 'error' ? (
<Box align="center" justify="center" p={5} flow={4}>
<Box>
<TranslatedError error={error} />
</Box>
<Button primary onClick={this.reset} small autoFocus>
{'Reset'}
</Button>
</Box>
) : null}
</Card>
{!!importableAccounts.length && (
<Card flow={2}>
{importableAccounts.map((acc, i) => (
<ImportableAccount
key={acc.xpub}
importableAccount={acc}
onRemove={this.removeImportableAccount}
isLoading={status === 'scanning' && i === 0}
>
{acc.xpub}
</ImportableAccount>
))}
{status !== 'scanning' && (
<Box mt={4} align="flex-start">
<Button primary onClick={this.scan}>
{'Launch scan'}
</Button>
</Box>
)}
</Card>
)}
</Fragment>
)
}
}
class ImportableAccount extends PureComponent<{
importableAccount: ImportableAccountType,
onRemove: ImportableAccountType => void,
isLoading: boolean,
}> {
remove = () => {
this.props.onRemove(this.props.importableAccount)
}
render() {
const { importableAccount, isLoading } = this.props
return (
<Box horizontal flow={2} align="center">
{isLoading && <Spinner size={16} color="rgba(0, 0, 0, 0.3)" />}
<CurrencyCircleIcon currency={importableAccount.currency} size={24} />
<Box grow ff="Rubik" fontSize={3}>
<Ellipsis>{`[${importableAccount.name}] ${importableAccount.derivationMode ||
'default'} ${importableAccount.xpub}`}</Ellipsis>
</Box>
{!isLoading && (
<FakeLink onClick={this.remove} fontSize={3}>
{'Remove'}
</FakeLink>
)}
</Box>
)
}
}
export default connect(
null,
mapDispatchToProps,
)(AccountImporter)

11
src/components/DevToolsPage/index.js

@ -0,0 +1,11 @@
import React from 'react'
import Box from 'components/base/Box'
import AccountImporter from './AccountImporter'
export default () => (
<Box flow={2}>
<AccountImporter />
</Box>
)

4
src/components/DeviceInteraction/components.js

@ -144,11 +144,11 @@ export const ErrorDescContainer = translate()(
<Box ml="auto" horizontal flow={2}> <Box ml="auto" horizontal flow={2}>
{!!errorHelpURL && ( {!!errorHelpURL && (
<FakeLink underline color="alertRed" onClick={() => openURL(errorHelpURL)}> <FakeLink underline color="alertRed" onClick={() => openURL(errorHelpURL)}>
{t('app:common.help')} {t('common.help')}
</FakeLink> </FakeLink>
)} )}
<FakeLink underline color="alertRed" onClick={onRetry}> <FakeLink underline color="alertRed" onClick={onRetry}>
{t('app:common.retry')} {t('common.retry')}
</FakeLink> </FakeLink>
</Box> </Box>
</Box> </Box>

21
src/components/EnsureDeviceApp.js

@ -10,8 +10,11 @@ import { getCryptoCurrencyIcon } from '@ledgerhq/live-common/lib/react'
import logger from 'logger' import logger from 'logger'
import getAddress from 'commands/getAddress' import getAddress from 'commands/getAddress'
import { createCancelablePolling } from 'helpers/promise' import { createCancelablePolling } from 'helpers/promise'
import { standardDerivation } from 'helpers/derivations' import {
import { isSegwitPath } from 'helpers/bip32' isSegwitDerivationMode,
getDerivationScheme,
runDerivationScheme,
} from '@ledgerhq/live-common/lib/derivation'
import DeviceInteraction from 'components/DeviceInteraction' import DeviceInteraction from 'components/DeviceInteraction'
import Text from 'components/base/Text' import Text from 'components/base/Text'
@ -20,7 +23,7 @@ import IconUsb from 'icons/Usb'
import type { Device } from 'types/common' import type { Device } from 'types/common'
import { WrongDeviceForAccount, CantOpenDevice, BtcUnmatchedApp } from 'config/errors' import { WrongDeviceForAccount, CantOpenDevice, UpdateYourApp } from 'config/errors'
import { getCurrentDevice } from 'reducers/devices' import { getCurrentDevice } from 'reducers/devices'
const usbIcon = <IconUsb size={16} /> const usbIcon = <IconUsb size={16} />
@ -61,10 +64,10 @@ class EnsureDeviceApp extends Component<{
}, },
{ {
shouldThrow: (err: Error) => { shouldThrow: (err: Error) => {
const isWrongApp = err instanceof BtcUnmatchedApp
const isWrongDevice = err instanceof WrongDeviceForAccount const isWrongDevice = err instanceof WrongDeviceForAccount
const isCantOpenDevice = err instanceof CantOpenDevice const isCantOpenDevice = err instanceof CantOpenDevice
return isWrongApp || isWrongDevice || isCantOpenDevice const isUpdateYourApp = err instanceof UpdateYourApp
return isWrongDevice || isCantOpenDevice || isUpdateYourApp
}, },
}, },
) )
@ -74,7 +77,7 @@ class EnsureDeviceApp extends Component<{
const cur = account ? account.currency : currency const cur = account ? account.currency : currency
invariant(cur, 'No currency given') invariant(cur, 'No currency given')
return ( return (
<Trans i18nKey="app:deviceConnect.step2.open" parent="div"> <Trans i18nKey="deviceConnect.step2" parent="div">
{'Open the '} {'Open the '}
<Bold>{cur.managerAppName}</Bold> <Bold>{cur.managerAppName}</Bold>
{' app on your device'} {' app on your device'}
@ -94,7 +97,7 @@ class EnsureDeviceApp extends Component<{
{ {
id: 'device', id: 'device',
title: ( title: (
<Trans i18nKey="app:deviceConnect.step1.connect" parent="div"> <Trans i18nKey="deviceConnect.step1" parent="div">
{'Connect and unlock your '} {'Connect and unlock your '}
<Bold>{'Ledger device'}</Bold> <Bold>{'Ledger device'}</Bold>
</Trans> </Trans>
@ -122,8 +125,8 @@ async function getAddressFromAccountOrCurrency(device, account, currency) {
currencyId: currency.id, currencyId: currency.id,
path: account path: account
? account.freshAddressPath ? account.freshAddressPath
: standardDerivation({ currency, segwit: false, x: 0 }), : runDerivationScheme(getDerivationScheme({ currency, derivationMode: '' }), currency),
segwit: account ? isSegwitPath(account.freshAddressPath) : false, segwit: account ? isSegwitDerivationMode(account.derivationMode) : false,
}) })
.toPromise() .toPromise()
return address return address

4
src/components/ExchangePage/ExchangeCard.js

@ -31,9 +31,9 @@ export default class ExchangeCard extends PureComponent<{ t: T, card: CardType }
{logo} {logo}
</Box> </Box>
<Box shrink ff="Open Sans|Regular" fontSize={4} flow={3}> <Box shrink ff="Open Sans|Regular" fontSize={4} flow={3}>
<Box>{t(`app:exchange.${id}`)}</Box> <Box>{t(`exchange.${id}`)}</Box>
<Box horizontal align="center" color="wallet" flow={1}> <Box horizontal align="center" color="wallet" flow={1}>
<FakeLink onClick={this.onClick}>{t('app:exchange.visitWebsite')}</FakeLink> <FakeLink onClick={this.onClick}>{t('exchange.visitWebsite')}</FakeLink>
<ExternalLinkIcon size={14} /> <ExternalLinkIcon size={14} />
</Box> </Box>
</Box> </Box>

46
src/components/ExchangePage/index.js

@ -2,9 +2,11 @@
import React, { PureComponent } from 'react' import React, { PureComponent } from 'react'
import { translate } from 'react-i18next' import { translate } from 'react-i18next'
import shuffle from 'lodash/shuffle'
import type { T } from 'types/common' import type { T } from 'types/common'
import { urls } from 'config/urls' import { urls } from 'config/urls'
import { i } from 'helpers/staticPath'
import TrackPage from 'analytics/TrackPage' import TrackPage from 'analytics/TrackPage'
import Box from 'components/base/Box' import Box from 'components/base/Box'
@ -20,7 +22,7 @@ type Props = {
t: T, t: T,
} }
const cards = [ const cards = shuffle([
{ {
key: 'coinhouse', key: 'coinhouse',
id: 'coinhouse', id: 'coinhouse',
@ -51,7 +53,43 @@ const cards = [
url: urls.paybis, url: urls.paybis,
logo: <PaybisLogo width={150} height={57} />, logo: <PaybisLogo width={150} height={57} />,
}, },
] {
key: 'luno',
id: 'luno',
url: urls.luno,
logo: <img src={i('logos/exchanges/luno.svg')} alt="Luno" width={150} />,
},
{
key: 'shapeshift',
id: 'shapeshift',
url: urls.shapeshift,
logo: <img src={i('logos/exchanges/shapeshift.svg')} alt="Shapeshift" width={150} />,
},
{
key: 'genesis',
id: 'genesis',
url: urls.genesis,
logo: <img src={i('logos/exchanges/genesis.svg')} alt="Genesis" width={150} />,
},
{
key: 'kyberSwap',
id: 'kyberSwap',
url: urls.kyberSwap,
logo: <img src={i('logos/exchanges/kyber-swap.png')} alt="KyberSwap" width={150} />,
},
{
key: 'changeNow',
id: 'changeNow',
url: urls.changeNow,
logo: <img src={i('logos/exchanges/change-now.png')} alt="ChangeNow" width={150} />,
},
{
key: 'thorSwap',
id: 'thorSwap',
url: urls.thorSwap,
logo: <img src={i('logos/exchanges/thor-swap.png')} alt="ThorSwap" width={150} />,
},
])
class ExchangePage extends PureComponent<Props> { class ExchangePage extends PureComponent<Props> {
render() { render() {
@ -60,10 +98,10 @@ class ExchangePage extends PureComponent<Props> {
<Box pb={6} selectable> <Box pb={6} selectable>
<TrackPage category="Exchange" /> <TrackPage category="Exchange" />
<Box ff="Museo Sans|Regular" fontSize={7} color="dark"> <Box ff="Museo Sans|Regular" fontSize={7} color="dark">
{t('app:exchange.title')} {t('exchange.title')}
</Box> </Box>
<Box ff="Museo Sans|Light" fontSize={5} mb={5}> <Box ff="Museo Sans|Light" fontSize={5} mb={5}>
{t('app:exchange.desc')} {t('exchange.desc')}
</Box> </Box>
<Box flow={3}>{cards.map(card => <ExchangeCard key={card.key} t={t} card={card} />)}</Box> <Box flow={3}>{cards.map(card => <ExchangeCard key={card.key} t={t} card={card} />)}</Box>
</Box> </Box>

2
src/components/ExportLogsBtn.js

@ -78,7 +78,7 @@ class ExportLogsBtn extends Component<{
<KeyHandler keyValue="e" onKeyHandle={this.onKeyHandle} /> <KeyHandler keyValue="e" onKeyHandle={this.onKeyHandle} />
) : ( ) : (
<Button small primary event="ExportLogs" onClick={this.handleExportLogs}> <Button small primary event="ExportLogs" onClick={this.handleExportLogs}>
{t('app:settings.exportLogs.btn')} {t('settings.exportLogs.btn')}
</Button> </Button>
) )
} }

34
src/components/FeesField/BitcoinKind.js

@ -8,6 +8,7 @@ import { translate } from 'react-i18next'
import type { T } from 'types/common' import type { T } from 'types/common'
import { FeeNotLoaded } from 'config/errors'
import InputCurrency from 'components/base/InputCurrency' import InputCurrency from 'components/base/InputCurrency'
import Select from 'components/base/Select' import Select from 'components/base/Select'
import type { Fees } from 'api/Fees' import type { Fees } from 'api/Fees'
@ -17,7 +18,7 @@ import Box from '../base/Box'
type Props = { type Props = {
account: Account, account: Account,
feePerByte: BigNumber, feePerByte: ?BigNumber,
onChange: BigNumber => void, onChange: BigNumber => void,
t: T, t: T,
} }
@ -50,6 +51,12 @@ const customItem = {
blockCount: 0, blockCount: 0,
feePerByte: BigNumber(0), feePerByte: BigNumber(0),
} }
const notLoadedItem = {
label: 'Standard',
value: 'standard',
blockCount: 0,
feePerByte: BigNumber(0),
}
type State = { isFocused: boolean, items: FeeItem[], selectedItem: FeeItem } type State = { isFocused: boolean, items: FeeItem[], selectedItem: FeeItem }
@ -57,13 +64,13 @@ type OwnProps = Props & { fees?: Fees, error?: Error }
class FeesField extends Component<OwnProps, State> { class FeesField extends Component<OwnProps, State> {
state = { state = {
items: [customItem], items: [notLoadedItem],
selectedItem: customItem, selectedItem: notLoadedItem,
isFocused: false, isFocused: false,
} }
static getDerivedStateFromProps(nextProps, prevState) { static getDerivedStateFromProps(nextProps, prevState) {
const { fees, feePerByte } = nextProps const { fees, feePerByte, error } = nextProps
let items: FeeItem[] = [] let items: FeeItem[] = []
if (fees) { if (fees) {
for (const key of Object.keys(fees)) { for (const key of Object.keys(fees)) {
@ -80,17 +87,18 @@ class FeesField extends Component<OwnProps, State> {
} }
items = items.sort((a, b) => a.blockCount - b.blockCount) items = items.sort((a, b) => a.blockCount - b.blockCount)
} }
items.push(customItem) items.push(!feePerByte && !error ? notLoadedItem : customItem)
const selectedItem = prevState.selectedItem.feePerByte.eq(feePerByte) const selectedItem =
? prevState.selectedItem !feePerByte && prevState.selectedItem.feePerByte.eq(feePerByte)
: items.find(f => f.feePerByte.eq(feePerByte)) || items[items.length - 1] ? prevState.selectedItem
: items.find(f => f.feePerByte.eq(feePerByte)) || items[items.length - 1]
return { items, selectedItem } return { items, selectedItem }
} }
componentDidUpdate({ fees: prevFees }: OwnProps) { componentDidUpdate({ fees: prevFees }: OwnProps) {
const { feePerByte, fees, onChange } = this.props const { feePerByte, fees, onChange } = this.props
const { items, isFocused } = this.state const { items, isFocused } = this.state
if (fees && fees !== prevFees && feePerByte.isZero() && !isFocused) { if (fees && fees !== prevFees && !feePerByte && !isFocused) {
// initialize with the median // initialize with the median
const feePerByte = (items.find(item => item.blockCount === defaultBlockCount) || items[0]) const feePerByte = (items.find(item => item.blockCount === defaultBlockCount) || items[0])
.feePerByte .feePerByte
@ -127,7 +135,7 @@ class FeesField extends Component<OwnProps, State> {
const satoshi = units[units.length - 1] const satoshi = units[units.length - 1]
return ( return (
<GenericContainer error={error}> <GenericContainer>
<Select width={156} options={items} value={selectedItem} onChange={this.onSelectChange} /> <Select width={156} options={items} value={selectedItem} onChange={this.onSelectChange} />
<InputCurrency <InputCurrency
ref={this.input} ref={this.input}
@ -137,10 +145,10 @@ class FeesField extends Component<OwnProps, State> {
value={feePerByte} value={feePerByte}
onChange={onChange} onChange={onChange}
onChangeFocus={this.onChangeFocus} onChangeFocus={this.onChangeFocus}
loading={!feePerByte && !error}
error={!feePerByte && error ? new FeeNotLoaded() : null}
renderRight={ renderRight={
<InputRight> <InputRight>{t('send.steps.amount.unitPerByte', { unit: satoshi.code })}</InputRight>
{t('app:send.steps.amount.unitPerByte', { unit: satoshi.code })}
</InputRight>
} }
allowZero allowZero
/> />

9
src/components/FeesField/EthereumKind.js

@ -4,6 +4,7 @@ import React, { Component } from 'react'
import { BigNumber } from 'bignumber.js' import { BigNumber } from 'bignumber.js'
import type { Account } from '@ledgerhq/live-common/lib/types' import type { Account } from '@ledgerhq/live-common/lib/types'
import { FeeNotLoaded } from 'config/errors'
import InputCurrency from 'components/base/InputCurrency' import InputCurrency from 'components/base/InputCurrency'
import type { Fees } from 'api/Fees' import type { Fees } from 'api/Fees'
import WithFeesAPI from '../WithFeesAPI' import WithFeesAPI from '../WithFeesAPI'
@ -11,7 +12,7 @@ import GenericContainer from './GenericContainer'
type Props = { type Props = {
account: Account, account: Account,
gasPrice: BigNumber, gasPrice: ?BigNumber,
onChange: BigNumber => void, onChange: BigNumber => void,
} }
@ -22,7 +23,7 @@ class FeesField extends Component<Props & { fees?: Fees, error?: Error }, *> {
componentDidUpdate() { componentDidUpdate() {
const { gasPrice, fees, onChange } = this.props const { gasPrice, fees, onChange } = this.props
const { isFocused } = this.state const { isFocused } = this.state
if (gasPrice.isZero() && fees && fees.gas_price && !isFocused) { if (!gasPrice && fees && fees.gas_price && !isFocused) {
onChange(BigNumber(fees.gas_price)) // we want to set the default to gas_price onChange(BigNumber(fees.gas_price)) // we want to set the default to gas_price
} }
} }
@ -33,12 +34,14 @@ class FeesField extends Component<Props & { fees?: Fees, error?: Error }, *> {
const { account, gasPrice, error, onChange } = this.props const { account, gasPrice, error, onChange } = this.props
const { units } = account.currency const { units } = account.currency
return ( return (
<GenericContainer error={error}> <GenericContainer>
<InputCurrency <InputCurrency
defaultUnit={units.length > 1 ? units[1] : units[0]} defaultUnit={units.length > 1 ? units[1] : units[0]}
units={units} units={units}
containerProps={{ grow: true }} containerProps={{ grow: true }}
value={gasPrice} value={gasPrice}
loading={!error && !gasPrice}
error={!gasPrice && error ? new FeeNotLoaded() : null}
onChange={onChange} onChange={onChange}
onChangeFocus={this.onChangeFocus} onChangeFocus={this.onChangeFocus}
/> />

2
src/components/FeesField/GenericContainer.js

@ -16,7 +16,7 @@ export default translate()(({ children, t }: { children: React$Node, t: * }) =>
openURL(urls.feesMoreInfo) openURL(urls.feesMoreInfo)
track('Send Flow Fees Help Requested') track('Send Flow Fees Help Requested')
}} }}
label={t('app:send.steps.amount.fees')} label={t('send.steps.amount.fees')}
/> />
<Box horizontal flow={5}> <Box horizontal flow={5}>
{children} {children}

9
src/components/FeesField/RippleKind.js

@ -4,12 +4,13 @@ import React, { Component } from 'react'
import type { BigNumber } from 'bignumber.js' import type { BigNumber } from 'bignumber.js'
import type { Account } from '@ledgerhq/live-common/lib/types' import type { Account } from '@ledgerhq/live-common/lib/types'
import { apiForEndpointConfig, parseAPIValue } from 'api/Ripple' import { apiForEndpointConfig, parseAPIValue } from 'api/Ripple'
import { FeeNotLoaded } from 'config/errors'
import InputCurrency from 'components/base/InputCurrency' import InputCurrency from 'components/base/InputCurrency'
import GenericContainer from './GenericContainer' import GenericContainer from './GenericContainer'
type Props = { type Props = {
account: Account, account: Account,
fee: BigNumber, fee: ?BigNumber,
onChange: BigNumber => void, onChange: BigNumber => void,
} }
@ -36,7 +37,7 @@ class FeesField extends Component<Props, State> {
const info = await api.getServerInfo() const info = await api.getServerInfo()
if (syncId !== this.syncId) return if (syncId !== this.syncId) return
const serverFee = parseAPIValue(info.validatedLedger.baseFeeXRP) const serverFee = parseAPIValue(info.validatedLedger.baseFeeXRP)
if (this.props.fee.isZero()) { if (!this.props.fee) {
this.props.onChange(serverFee) this.props.onChange(serverFee)
} }
} catch (error) { } catch (error) {
@ -50,11 +51,13 @@ class FeesField extends Component<Props, State> {
const { error } = this.state const { error } = this.state
const { units } = account.currency const { units } = account.currency
return ( return (
<GenericContainer error={error}> <GenericContainer>
<InputCurrency <InputCurrency
defaultUnit={units[0]} defaultUnit={units[0]}
units={units} units={units}
containerProps={{ grow: true }} containerProps={{ grow: true }}
loading={!error && !fee}
error={!fee && error ? new FeeNotLoaded() : null}
value={fee} value={fee}
onChange={onChange} onChange={onChange}
/> />

8
src/components/GenuineCheck.js

@ -124,7 +124,7 @@ class GenuineCheck extends PureComponent<Props> {
{ {
id: 'device', id: 'device',
title: ( title: (
<Trans i18nKey="app:deviceConnect.step1.connect" parent="div"> <Trans i18nKey="deviceConnect.step1" parent="div">
{'Connect and unlock your '} {'Connect and unlock your '}
<Bold>{'Ledger device'}</Bold> <Bold>{'Ledger device'}</Bold>
</Trans> </Trans>
@ -135,10 +135,10 @@ class GenuineCheck extends PureComponent<Props> {
{ {
id: 'deviceInfo', id: 'deviceInfo',
title: ( title: (
<Trans i18nKey="deviceConnect:dashboard.open" parent="div"> <Trans i18nKey="deviceConnect.step2" parent="div">
{'Navigate to the '} {'Navigate to the '}
<Bold>{'dashboard'}</Bold> <Bold>{'dashboard'}</Bold>
{' on your device'} {' app on your device'}
</Trans> </Trans>
), ),
icon: homeIcon, icon: homeIcon,
@ -147,7 +147,7 @@ class GenuineCheck extends PureComponent<Props> {
{ {
id: 'isGenuine', id: 'isGenuine',
title: ( title: (
<Trans i18nKey="deviceConnect:stepGenuine.open" parent="div"> <Trans i18nKey="deviceConnect.step3" parent="div">
{'Allow '} {'Allow '}
<Bold>{'Ledger Manager'}</Bold> <Bold>{'Ledger Manager'}</Bold>
{' on your device'} {' on your device'}

2
src/components/GenuineCheckModal.js

@ -20,7 +20,7 @@ class GenuineCheckModal extends PureComponent<Props> {
const { t, onSuccess, onFail, onUnavailable } = this.props const { t, onSuccess, onFail, onUnavailable } = this.props
return ( return (
<ModalBody onClose={onClose}> <ModalBody onClose={onClose}>
<ModalTitle>{t('app:genuinecheck.modal.title')}</ModalTitle> <ModalTitle>{t('genuinecheck.modal.title')}</ModalTitle>
<ModalContent> <ModalContent>
<GenuineCheck onSuccess={onSuccess} onFail={onFail} onUnavailable={onUnavailable} /> <GenuineCheck onSuccess={onSuccess} onFail={onFail} onUnavailable={onUnavailable} />
</ModalContent> </ModalContent>

2
src/components/GlobalSearch.js

@ -66,7 +66,7 @@ class GlobalSearch extends PureComponent<Props, State> {
<IconSearch size={16} /> <IconSearch size={16} />
</Box> </Box>
<Input <Input
placeholder={t('app:common.search')} placeholder={t('common.search')}
innerRef={input => (this._input = input)} innerRef={input => (this._input = input)}
onBlur={this.handleBlur} onBlur={this.handleBlur}
onFocus={this.handleFocus} onFocus={this.handleFocus}

18
src/components/IsUnlocked.js

@ -12,7 +12,7 @@ import { i } from 'helpers/staticPath'
import IconTriangleWarning from 'icons/TriangleWarning' import IconTriangleWarning from 'icons/TriangleWarning'
import db from 'helpers/db' import db from 'helpers/db'
import hardReset from 'helpers/hardReset' import { hardReset } from 'helpers/reset'
import { fetchAccounts } from 'actions/accounts' import { fetchAccounts } from 'actions/accounts'
import { isLocked, unlock } from 'reducers/application' import { isLocked, unlock } from 'reducers/application'
@ -161,17 +161,17 @@ class IsUnlocked extends Component<Props, State> {
/> />
} }
/> />
<PageTitle>{t('app:common.lockScreen.title')}</PageTitle> <PageTitle>{t('common.lockScreen.title')}</PageTitle>
<LockScreenDesc> <LockScreenDesc>
{t('app:common.lockScreen.subTitle')} {t('common.lockScreen.subTitle')}
<br /> <br />
{t('app:common.lockScreen.description')} {t('common.lockScreen.description')}
</LockScreenDesc> </LockScreenDesc>
<Box horizontal align="center"> <Box horizontal align="center">
<Box style={{ width: 280 }}> <Box style={{ width: 280 }}>
<InputPassword <InputPassword
autoFocus autoFocus
placeholder={t('app:common.lockScreen.inputPlaceholder')} placeholder={t('common.lockScreen.inputPlaceholder')}
type="password" type="password"
onChange={this.handleChangeInput('password')} onChange={this.handleChangeInput('password')}
value={inputValue.password} value={inputValue.password}
@ -187,7 +187,7 @@ class IsUnlocked extends Component<Props, State> {
</Box> </Box>
</Box> </Box>
<Button type="button" mt={3} small onClick={this.handleOpenHardResetModal}> <Button type="button" mt={3} small onClick={this.handleOpenHardResetModal}>
{t('app:common.lockScreen.lostPassword')} {t('common.lockScreen.lostPassword')}
</Button> </Button>
</Box> </Box>
</form> </form>
@ -199,9 +199,9 @@ class IsUnlocked extends Component<Props, State> {
onClose={this.handleCloseHardResetModal} onClose={this.handleCloseHardResetModal}
onReject={this.handleCloseHardResetModal} onReject={this.handleCloseHardResetModal}
onConfirm={this.handleHardReset} onConfirm={this.handleHardReset}
confirmText={t('app:common.reset')} confirmText={t('common.reset')}
title={t('app:settings.hardResetModal.title')} title={t('settings.hardResetModal.title')}
desc={t('app:settings.hardResetModal.desc')} desc={t('settings.hardResetModal.desc')}
renderIcon={this.hardResetIconRender} renderIcon={this.hardResetIconRender}
/> />
</Box> </Box>

2
src/components/MainSideBar/AddAccountButton.js

@ -33,7 +33,7 @@ export default class AddAccountButton extends PureComponent<{
return ( return (
<Tooltip render={() => tooltipText}> <Tooltip render={() => tooltipText}>
<PlusWrapper onClick={onClick}> <PlusWrapper onClick={onClick}>
<IconCirclePlus size={16} /> <IconCirclePlus size={16} data-e2e="menuAddAccount_button" />
</PlusWrapper> </PlusWrapper>
</Tooltip> </Tooltip>
) )

72
src/components/MainSideBar/index.js

@ -1,7 +1,7 @@
// @flow // @flow
import React, { PureComponent } from 'react' import React, { PureComponent } from 'react'
import { translate } from 'react-i18next' import { Trans, translate } from 'react-i18next'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { compose } from 'redux' import { compose } from 'redux'
import { withRouter } from 'react-router' import { withRouter } from 'react-router'
@ -19,6 +19,7 @@ import { i } from 'helpers/staticPath'
import { accountsSelector } from 'reducers/accounts' import { accountsSelector } from 'reducers/accounts'
import { openModal } from 'reducers/modals' import { openModal } from 'reducers/modals'
import { getUpdateStatus } from 'reducers/update' import { getUpdateStatus } from 'reducers/update'
import { developerModeSelector } from 'reducers/settings'
import { SideBarList, SideBarListItem } from 'components/base/SideBar' import { SideBarList, SideBarListItem } from 'components/base/SideBar'
import Box from 'components/base/Box' import Box from 'components/base/Box'
@ -34,10 +35,12 @@ import IconExchange from 'icons/Exchange'
import AccountListItem from './AccountListItem' import AccountListItem from './AccountListItem'
import AddAccountButton from './AddAccountButton' import AddAccountButton from './AddAccountButton'
import TopGradient from './TopGradient' import TopGradient from './TopGradient'
import KeyboardContent from '../KeyboardContent'
const mapStateToProps = state => ({ const mapStateToProps = state => ({
accounts: accountsSelector(state), accounts: accountsSelector(state),
updateStatus: getUpdateStatus(state), updateStatus: getUpdateStatus(state),
developerMode: developerModeSelector(state),
}) })
const mapDispatchToProps = { const mapDispatchToProps = {
@ -52,8 +55,26 @@ type Props = {
push: string => void, push: string => void,
openModal: string => void, openModal: string => void,
updateStatus: UpdateStatus, updateStatus: UpdateStatus,
developerMode: boolean,
} }
const IconDev = () => (
<div
style={{
width: 16,
height: 16,
fontSize: 10,
fontFamily: 'monospace',
fontWeight: 'bold',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
{'DEV'}
</div>
)
class MainSideBar extends PureComponent<Props> { class MainSideBar extends PureComponent<Props> {
push = (to: string) => { push = (to: string) => {
const { push } = this.props const { push } = this.props
@ -66,29 +87,27 @@ class MainSideBar extends PureComponent<Props> {
push(to) push(to)
} }
ADD_ACCOUNT_EMPTY_STATE = (
<Box relative pr={3}>
<img style={{ position: 'absolute', top: -10, right: 5 }} alt="" src={i('arrow-add.svg')} />
{this.props.t('app:emptyState.sidebar.text')}
</Box>
)
handleClickDashboard = () => this.push('/') handleClickDashboard = () => this.push('/')
handleOpenSendModal = () => this.props.openModal(MODAL_SEND) handleOpenSendModal = () => this.props.openModal(MODAL_SEND)
handleOpenReceiveModal = () => this.props.openModal(MODAL_RECEIVE) handleOpenReceiveModal = () => this.props.openModal(MODAL_RECEIVE)
handleClickManager = () => this.push('/manager') handleClickManager = () => this.push('/manager')
handleClickExchange = () => this.push('/exchange') handleClickExchange = () => this.push('/exchange')
handleClickDev = () => this.push('/dev')
handleOpenImportModal = () => this.props.openModal(MODAL_ADD_ACCOUNTS) handleOpenImportModal = () => this.props.openModal(MODAL_ADD_ACCOUNTS)
render() { render() {
const { t, accounts, location, updateStatus } = this.props const { t, accounts, location, updateStatus, developerMode } = this.props
const { pathname } = location const { pathname } = location
const addAccountButton = ( const addAccountButton = (
<AddAccountButton <AddAccountButton tooltipText={t('addAccounts.title')} onClick={this.handleOpenImportModal} />
tooltipText={t('app:addAccounts.title')} )
onClick={this.handleOpenImportModal}
/> const emptyState = (
<Box relative pr={3}>
<img style={{ position: 'absolute', top: -10, right: 5 }} alt="" src={i('arrow-add.svg')} />
<Trans i18nKey="emptyState.sidebar.text" />
</Box>
) )
return ( return (
@ -96,9 +115,9 @@ class MainSideBar extends PureComponent<Props> {
<TopGradient /> <TopGradient />
<GrowScroll> <GrowScroll>
<Space of={70} /> <Space of={70} />
<SideBarList title={t('app:sidebar.menu')}> <SideBarList title={t('sidebar.menu')}>
<SideBarListItem <SideBarListItem
label={t('app:dashboard.title')} label={t('dashboard.title')}
icon={IconPieChart} icon={IconPieChart}
iconActiveColor="wallet" iconActiveColor="wallet"
onClick={this.handleClickDashboard} onClick={this.handleClickDashboard}
@ -106,39 +125,50 @@ class MainSideBar extends PureComponent<Props> {
hasNotif={updateStatus === 'downloaded'} hasNotif={updateStatus === 'downloaded'}
/> />
<SideBarListItem <SideBarListItem
label={t('app:send.title')} label={t('send.title')}
icon={IconSend} icon={IconSend}
iconActiveColor="wallet" iconActiveColor="wallet"
onClick={this.handleOpenSendModal} onClick={this.handleOpenSendModal}
disabled={accounts.length === 0} disabled={accounts.length === 0}
/> />
<SideBarListItem <SideBarListItem
label={t('app:receive.title')} label={t('receive.title')}
icon={IconReceive} icon={IconReceive}
iconActiveColor="wallet" iconActiveColor="wallet"
onClick={this.handleOpenReceiveModal} onClick={this.handleOpenReceiveModal}
disabled={accounts.length === 0} disabled={accounts.length === 0}
/> />
<SideBarListItem <SideBarListItem
label={t('app:sidebar.manager')} label={t('sidebar.manager')}
icon={IconManager} icon={IconManager}
iconActiveColor="wallet" iconActiveColor="wallet"
onClick={this.handleClickManager} onClick={this.handleClickManager}
isActive={pathname === '/manager'} isActive={pathname === '/manager'}
/> />
<SideBarListItem <SideBarListItem
label={t('app:sidebar.exchange')} label={t('sidebar.exchange')}
icon={IconExchange} icon={IconExchange}
iconActiveColor="wallet" iconActiveColor="wallet"
onClick={this.handleClickExchange} onClick={this.handleClickExchange}
isActive={pathname === '/exchange'} isActive={pathname === '/exchange'}
/> />
{developerMode && (
<KeyboardContent sequence="DEVTOOLS">
<SideBarListItem
label={t('sidebar.developer')}
icon={IconDev}
iconActiveColor="wallet"
onClick={this.handleClickDev}
isActive={pathname === '/dev'}
/>
</KeyboardContent>
)}
</SideBarList> </SideBarList>
<Space of={40} /> <Space of={40} />
<SideBarList <SideBarList
title={t('app:sidebar.accounts', { count: accounts.length })} title={t('sidebar.accounts', { count: accounts.length })}
titleRight={addAccountButton} titleRight={addAccountButton}
emptyState={this.ADD_ACCOUNT_EMPTY_STATE} emptyState={emptyState}
> >
{accounts.map(account => ( {accounts.map(account => (
<AccountListItem <AccountListItem

2
src/components/ManagerPage/AppSearchBar.js

@ -81,7 +81,7 @@ class AppSearchBar extends PureComponent<Props, State> {
<Space of={30} /> <Space of={30} />
<Search <Search
fuseOptions={{ fuseOptions={{
threshold: 0.5, threshold: 0.1,
keys: ['name'], keys: ['name'],
}} }}
value={query} value={query}

79
src/components/ManagerPage/AppsList.js

@ -7,15 +7,13 @@ import { translate } from 'react-i18next'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { compose } from 'redux' import { compose } from 'redux'
import type { Device, T } from 'types/common' import type { Device, T } from 'types/common'
import type { Application, ApplicationVersion, DeviceInfo } from 'helpers/types' import type { ApplicationVersion, DeviceInfo } from 'helpers/types'
import { getFullListSortedCryptoCurrencies } from 'helpers/countervalues'
import { developerModeSelector } from 'reducers/settings' import { developerModeSelector } from 'reducers/settings'
import listApps from 'commands/listApps' import listApps from 'commands/listApps'
import listAppVersions from 'commands/listAppVersions' import listAppVersions from 'commands/listAppVersions'
import installApp from 'commands/installApp' import installApp from 'commands/installApp'
import uninstallApp from 'commands/uninstallApp' import uninstallApp from 'commands/uninstallApp'
import Box from 'components/base/Box' import Box from 'components/base/Box'
import Space from 'components/base/Space' import Space from 'components/base/Space'
import Modal, { ModalBody, ModalFooter, ModalTitle, ModalContent } from 'components/base/Modal' import Modal, { ModalBody, ModalFooter, ModalTitle, ModalContent } from 'components/base/Modal'
@ -26,13 +24,11 @@ import Spinner from 'components/base/Spinner'
import Button from 'components/base/Button' import Button from 'components/base/Button'
import TranslatedError from 'components/TranslatedError' import TranslatedError from 'components/TranslatedError'
import TrackPage from 'analytics/TrackPage' import TrackPage from 'analytics/TrackPage'
import IconInfoCircle from 'icons/InfoCircle' import IconInfoCircle from 'icons/InfoCircle'
import ExclamationCircleThin from 'icons/ExclamationCircleThin' import ExclamationCircleThin from 'icons/ExclamationCircleThin'
import Update from 'icons/Update' import Update from 'icons/Update'
import Trash from 'icons/Trash' import Trash from 'icons/Trash'
import CheckCircle from 'icons/CheckCircle' import CheckCircle from 'icons/CheckCircle'
import { FreezeDeviceChangeEvents } from './HookDeviceChange' import { FreezeDeviceChangeEvents } from './HookDeviceChange'
import ManagerApp, { Container as FakeManagerAppContainer } from './ManagerApp' import ManagerApp, { Container as FakeManagerAppContainer } from './ManagerApp'
import AppSearchBar from './AppSearchBar' import AppSearchBar from './AppSearchBar'
@ -71,6 +67,9 @@ type State = {
mode: Mode, mode: Mode,
} }
const oldAppsInstallDisabled = ['ZenCash', 'Ripple']
const canHandleInstall = c => !oldAppsInstallDisabled.includes(c.name)
const LoadingApp = () => ( const LoadingApp = () => (
<FakeManagerAppContainer noShadow align="center" justify="center" style={{ height: 90 }}> <FakeManagerAppContainer noShadow align="center" justify="center" style={{ height: 90 }}>
<Spinner size={16} color="rgba(0, 0, 0, 0.3)" /> <Spinner size={16} color="rgba(0, 0, 0, 0.3)" />
@ -99,29 +98,53 @@ class AppsList extends PureComponent<Props, State> {
_unmounted = false _unmounted = false
filterAppVersions = (applicationsList, compatibleAppVersionsList) => { prepareAppList = ({ applicationsList, compatibleAppVersionsList, sortedCryptoCurrencies }) => {
if (!this.props.isDevMode) { const filtered = this.props.isDevMode
return compatibleAppVersionsList.filter(version => { ? compatibleAppVersionsList.slice(0)
const app = applicationsList.find(e => e.id === version.app) : compatibleAppVersionsList.filter(version => {
if (app) { const app = applicationsList.find(e => e.id === version.app)
return app.category !== 2 if (app) {
} return app.category !== 2
}
return false return false
}) })
}
return compatibleAppVersionsList const sortedCryptoApps = []
// sort by crypto first
sortedCryptoCurrencies.forEach(crypto => {
const app = filtered.find(
item => item.name.toLowerCase() === crypto.managerAppName.toLowerCase(),
)
if (app) {
filtered.splice(filtered.indexOf(app), 1)
sortedCryptoApps.push(app)
}
})
return sortedCryptoApps.concat(filtered)
} }
async fetchAppList() { async fetchAppList() {
try { try {
const { deviceInfo } = this.props const { deviceInfo } = this.props
const applicationsList: Array<Application> = await listApps.send().toPromise()
const compatibleAppVersionsList = await listAppVersions.send(deviceInfo).toPromise() const [
const filteredAppVersionsList = this.filterAppVersions(
applicationsList, applicationsList,
compatibleAppVersionsList, compatibleAppVersionsList,
) sortedCryptoCurrencies,
] = await Promise.all([
listApps.send().toPromise(),
listAppVersions.send(deviceInfo).toPromise(),
getFullListSortedCryptoCurrencies(),
])
const filteredAppVersionsList = this.prepareAppList({
applicationsList,
compatibleAppVersionsList,
sortedCryptoCurrencies,
})
if (!this._unmounted) { if (!this._unmounted) {
this.setState({ this.setState({
@ -191,7 +214,7 @@ class AppsList extends PureComponent<Props, State> {
</ModalTitle> </ModalTitle>
<ModalContent> <ModalContent>
<Text ff="Museo Sans|Regular" fontSize={6} color="dark"> <Text ff="Museo Sans|Regular" fontSize={6} color="dark">
{t(`app:manager.apps.${mode}`, { app })} {t(`manager.apps.${mode}`, { app })}
</Text> </Text>
<Box mt={6}> <Box mt={6}>
<Progress style={{ width: '100%' }} infinite /> <Progress style={{ width: '100%' }} infinite />
@ -233,7 +256,7 @@ class AppsList extends PureComponent<Props, State> {
</ModalContent> </ModalContent>
<ModalFooter horizontal justifyContent="flex-end" style={{ width: '100%' }}> <ModalFooter horizontal justifyContent="flex-end" style={{ width: '100%' }}>
<Button primary onClick={this.handleCloseModal}> <Button primary onClick={this.handleCloseModal}>
{t('app:common.close')} {t('common.close')}
</Button> </Button>
</ModalFooter> </ModalFooter>
</Fragment> </Fragment>
@ -252,7 +275,7 @@ class AppsList extends PureComponent<Props, State> {
style={{ maxWidth: 350 }} style={{ maxWidth: 350 }}
> >
{t( {t(
`app:manager.apps.${ `manager.apps.${
mode === 'installing' ? 'installSuccess' : 'uninstallSuccess' mode === 'installing' ? 'installSuccess' : 'uninstallSuccess'
}`, }`,
{ app }, { app },
@ -261,7 +284,7 @@ class AppsList extends PureComponent<Props, State> {
</ModalContent> </ModalContent>
<ModalFooter horizontal justifyContent="flex-end" style={{ width: '100%' }}> <ModalFooter horizontal justifyContent="flex-end" style={{ width: '100%' }}>
<Button primary onClick={this.handleCloseModal}> <Button primary onClick={this.handleCloseModal}>
{t('app:common.close')} {t('common.close')}
</Button> </Button>
</ModalFooter> </ModalFooter>
</Fragment> </Fragment>
@ -285,7 +308,7 @@ class AppsList extends PureComponent<Props, State> {
name={c.name} name={c.name}
version={`Version ${c.version}`} version={`Version ${c.version}`}
icon={ICONS_FALLBACK[c.icon] || c.icon} icon={ICONS_FALLBACK[c.icon] || c.icon}
onInstall={this.handleInstallApp(c)} onInstall={canHandleInstall(c) ? this.handleInstallApp(c) : null}
onUninstall={this.handleUninstallApp(c)} onUninstall={this.handleUninstallApp(c)}
/> />
))} ))}
@ -319,11 +342,11 @@ class AppsList extends PureComponent<Props, State> {
<Box flow={6}> <Box flow={6}>
<Box> <Box>
<Box mb={4} color="dark" ff="Museo Sans" fontSize={5} flow={2} horizontal align="center"> <Box mb={4} color="dark" ff="Museo Sans" fontSize={5} flow={2} horizontal align="center">
<span style={{ lineHeight: 1 }}>{t('app:manager.apps.all')}</span> <span style={{ lineHeight: 1 }}>{t('manager.apps.all')}</span>
<Tooltip <Tooltip
render={() => ( render={() => (
<Box ff="Open Sans|SemiBold" fontSize={2}> <Box ff="Open Sans|SemiBold" fontSize={2}>
{t('app:manager.apps.help')} {t('manager.apps.help')}
</Box> </Box>
)} )}
> >

6
src/components/ManagerPage/Dashboard.js

@ -27,14 +27,14 @@ const Dashboard = ({ device, deviceInfo, t, handleHelpRequest }: Props) => (
<TrackPage category="Manager" name="Dashboard" /> <TrackPage category="Manager" name="Dashboard" />
<Box> <Box>
<Text ff="Museo Sans|Regular" fontSize={7} color="dark"> <Text ff="Museo Sans|Regular" fontSize={7} color="dark">
{t('app:manager.title')} {t('manager.title')}
</Text> </Text>
<Box horizontal> <Box horizontal>
<Text ff="Museo Sans|Light" fontSize={5}> <Text ff="Museo Sans|Light" fontSize={5}>
{t('app:manager.subtitle')} {t('manager.subtitle')}
</Text> </Text>
<HelpLink onClick={handleHelpRequest}> <HelpLink onClick={handleHelpRequest}>
<div style={{ textDecoration: 'underline' }}>{t('app:common.needHelp')}</div> <div style={{ textDecoration: 'underline' }}>{t('common.needHelp')}</div>
<IconExternalLink size={14} /> <IconExternalLink size={14} />
</HelpLink> </HelpLink>
</Box> </Box>

8
src/components/ManagerPage/FirmwareUpdate.js

@ -136,17 +136,17 @@ class FirmwareUpdate extends PureComponent<Props, State> {
<Box horizontal align="center"> <Box horizontal align="center">
<Text ff="Open Sans|SemiBold" fontSize={4} color="dark"> <Text ff="Open Sans|SemiBold" fontSize={4} color="dark">
{device.product === 'Blue' {device.product === 'Blue'
? t('app:manager.firmware.titleBlue') ? t('manager.firmware.titleBlue')
: t('app:manager.firmware.titleNano')} : t('manager.firmware.titleNano')}
</Text> </Text>
<Box color="wallet" ml={2}> <Box color="wallet" ml={2}>
<Tooltip render={() => t('app:manager.yourDeviceIsGenuine')}> <Tooltip render={() => t('manager.yourDeviceIsGenuine')}>
<CheckFull size={13} color="wallet" /> <CheckFull size={13} color="wallet" />
</Tooltip> </Tooltip>
</Box> </Box>
</Box> </Box>
<Text ff="Open Sans|SemiBold" fontSize={2}> <Text ff="Open Sans|SemiBold" fontSize={2}>
{t('app:manager.firmware.installed', { {t('manager.firmware.installed', {
version: deviceInfo.fullVersion, version: deviceInfo.fullVersion,
})} })}
</Text> </Text>

26
src/components/ManagerPage/ManagerApp.js

@ -49,7 +49,7 @@ type Props = {
name: string, name: string,
version: string, version: string,
icon: string, icon: string,
onInstall: Function, onInstall?: Function,
onUninstall: Function, onUninstall: Function,
} }
@ -64,17 +64,19 @@ function ManagerApp({ name, version, icon, onInstall, onUninstall, t }: Props) {
{version} {version}
</Text> </Text>
</Box> </Box>
<Button {onInstall ? (
outline <Button
onClick={onInstall} outline
event={'Manager Install Click'} onClick={onInstall}
eventProperties={{ event={'Manager Install Click'}
appName: name, eventProperties={{
appVersion: version, appName: name,
}} appVersion: version,
> }}
{t('app:manager.apps.install')} >
</Button> {t('manager.apps.install')}
</Button>
) : null}
<Button <Button
outline outline
onClick={onUninstall} onClick={onUninstall}

4
src/components/ManagerPage/ManagerGenuineCheck.js

@ -31,10 +31,10 @@ class ManagerGenuineCheck extends PureComponent<Props> {
style={{ marginBottom: 30, maxWidth: 362, width: '100%' }} style={{ marginBottom: 30, maxWidth: 362, width: '100%' }}
/> />
<Text ff="Museo Sans|Regular" fontSize={7} color="dark" style={{ marginBottom: 10 }}> <Text ff="Museo Sans|Regular" fontSize={7} color="dark" style={{ marginBottom: 10 }}>
{t('app:manager.device.title')} {t('manager.device.title')}
</Text> </Text>
<Text ff="Museo Sans|Light" fontSize={5} color="grey" align="center"> <Text ff="Museo Sans|Light" fontSize={5} color="grey" align="center">
{t('app:manager.device.desc')} {t('manager.device.desc')}
</Text> </Text>
</Box> </Box>
<Space of={40} /> <Space of={40} />

6
src/components/ManagerPage/PlugYourDevice.js

@ -22,12 +22,12 @@ function PlugYourDevice(props: Props) {
<Box align="center" style={{ width: 365 }}> <Box align="center" style={{ width: 365 }}>
<Box mb={5}>hey</Box> <Box mb={5}>hey</Box>
<Box textAlign="center" mb={1} ff="Museo Sans|Regular" color="dark" fontSize={6}> <Box textAlign="center" mb={1} ff="Museo Sans|Regular" color="dark" fontSize={6}>
{t('app:manager.device.title')} {t('manager.device.title')}
</Box> </Box>
<Box textAlign="center" mb={5} ff="Open Sans|Regular" color="smoke" fontSize={4}> <Box textAlign="center" mb={5} ff="Open Sans|Regular" color="smoke" fontSize={4}>
{t('app:manager.device.desc')} {t('manager.device.desc')}
</Box> </Box>
<Button primary>{t('app:manager.device.cta')}</Button> <Button primary>{t('manager.device.cta')}</Button>
</Box> </Box>
</Card> </Card>
) )

4
src/components/ManagerPage/UpdateFirmwareButton.js

@ -23,7 +23,7 @@ const UpdateFirmwareButton = ({ t, firmware, onClick }: Props) =>
firmware ? ( firmware ? (
<Fragment> <Fragment>
<Text ff="Open Sans|Regular" fontSize={4} style={{ marginLeft: 'auto', marginRight: 15 }}> <Text ff="Open Sans|Regular" fontSize={4} style={{ marginLeft: 'auto', marginRight: 15 }}>
{t('app:manager.firmware.latest', { version: getCleanVersion(firmware.name) })} {t('manager.firmware.latest', { version: getCleanVersion(firmware.name) })}
</Text> </Text>
<Button <Button
primary primary
@ -33,7 +33,7 @@ const UpdateFirmwareButton = ({ t, firmware, onClick }: Props) =>
firmwareName: firmware.name, firmwareName: firmware.name,
}} }}
> >
{t('app:manager.firmware.update')} {t('manager.firmware.update')}
</Button> </Button>
</Fragment> </Fragment>
) : null ) : null

7
src/components/ManagerPage/index.js

@ -4,12 +4,11 @@ import React, { PureComponent, Fragment } from 'react'
import invariant from 'invariant' import invariant from 'invariant'
import { openURL } from 'helpers/linking' import { openURL } from 'helpers/linking'
import { urls } from 'config/urls' import { urls } from 'config/urls'
import type { Device } from 'types/common' import type { Device } from 'types/common'
import type { DeviceInfo } from 'helpers/types' import type { DeviceInfo } from 'helpers/types'
import { getFullListSortedCryptoCurrencies } from 'helpers/countervalues'
import Dashboard from './Dashboard' import Dashboard from './Dashboard'
import ManagerGenuineCheck from './ManagerGenuineCheck' import ManagerGenuineCheck from './ManagerGenuineCheck'
import HookDeviceChange from './HookDeviceChange' import HookDeviceChange from './HookDeviceChange'
@ -30,6 +29,10 @@ const INITIAL_STATE = {
class ManagerPage extends PureComponent<Props, State> { class ManagerPage extends PureComponent<Props, State> {
state = INITIAL_STATE state = INITIAL_STATE
componentDidMount() {
getFullListSortedCryptoCurrencies() // start fetching the crypto currencies ordering
}
// prettier-ignore // prettier-ignore
handleSuccessGenuine = ({ device, deviceInfo }: { device: Device, deviceInfo: DeviceInfo }) => { // eslint-disable-line react/no-unused-prop-types handleSuccessGenuine = ({ device, deviceInfo }: { device: Device, deviceInfo: DeviceInfo }) => { // eslint-disable-line react/no-unused-prop-types
this.setState({ isGenuine: true, device, deviceInfo }) this.setState({ isGenuine: true, device, deviceInfo })

4
src/components/Onboarding/OnboardingFooter.js

@ -24,7 +24,7 @@ const OnboardingFooter = ({
}: Props) => ( }: Props) => (
<OnboardingFooterWrapper {...props}> <OnboardingFooterWrapper {...props}>
<Button outlineGrey onClick={() => prevStep()}> <Button outlineGrey onClick={() => prevStep()}>
{t('app:common.back')} {t('common.back')}
</Button> </Button>
<Button <Button
data-e2e="continue_button" data-e2e="continue_button"
@ -33,7 +33,7 @@ const OnboardingFooter = ({
onClick={() => nextStep()} onClick={() => nextStep()}
ml="auto" ml="auto"
> >
{t('app:common.continue')} {t('common.continue')}
</Button> </Button>
</OnboardingFooterWrapper> </OnboardingFooterWrapper>
) )

13
src/components/Onboarding/helperComponents.js

@ -69,6 +69,19 @@ export function OptionRow({ step, ...p }: { step: StepType }) {
</Box> </Box>
) )
} }
export function BulletRow({ step, ...p }: { step: StepType }) {
const { icon, desc } = step
return (
<Box horizontal my="7px">
<Box {...p} mr="7px">
{icon}
</Box>
<Box justify="center" shrink>
<OptionRowDesc>{desc}</OptionRowDesc>
</Box>
</Box>
)
}
export const OptionRowDesc = styled(Box).attrs({ export const OptionRowDesc = styled(Box).attrs({
ff: 'Open Sans|Regular', ff: 'Open Sans|Regular',
fontSize: 4, fontSize: 4,

68
src/components/Onboarding/steps/Analytics.js

@ -7,10 +7,13 @@ import { saveSettings } from 'actions/settings'
import Box from 'components/base/Box' import Box from 'components/base/Box'
import Switch from 'components/base/Switch' import Switch from 'components/base/Switch'
import FakeLink from 'components/base/FakeLink' import FakeLink from 'components/base/FakeLink'
import { Trans } from 'react-i18next'
import TrackPage from 'analytics/TrackPage' import TrackPage from 'analytics/TrackPage'
import Track from 'analytics/Track' import Track from 'analytics/Track'
import { openModal } from 'reducers/modals' import { openModal } from 'reducers/modals'
import { MODAL_SHARE_ANALYTICS, MODAL_TECHNICAL_DATA } from 'config/constants' import { MODAL_SHARE_ANALYTICS, MODAL_TECHNICAL_DATA } from 'config/constants'
import { openURL } from 'helpers/linking'
import { urls } from 'config/urls'
import ShareAnalytics from '../../modals/ShareAnalytics' import ShareAnalytics from '../../modals/ShareAnalytics'
import TechnicalData from '../../modals/TechnicalData' import TechnicalData from '../../modals/TechnicalData'
import { Title, Description, FixedTopContainer, StepContainerInner } from '../helperComponents' import { Title, Description, FixedTopContainer, StepContainerInner } from '../helperComponents'
@ -26,7 +29,7 @@ type State = {
} }
const INITIAL_STATE = { const INITIAL_STATE = {
analyticsToggle: false, analyticsToggle: true,
sentryLogsToggle: true, sentryLogsToggle: true,
} }
@ -46,6 +49,9 @@ class Analytics extends PureComponent<StepProps, State> {
}) })
} }
onClickTerms = () => openURL(urls.terms)
onClickPrivacy = () => openURL(urls.privacyPolicy)
handleNavBack = () => { handleNavBack = () => {
const { savePassword, prevStep } = this.props const { savePassword, prevStep } = this.props
savePassword(undefined) savePassword(undefined)
@ -70,13 +76,15 @@ class Analytics extends PureComponent<StepProps, State> {
deviceType={onboarding.isLedgerNano ? 'Nano S' : 'Blue'} deviceType={onboarding.isLedgerNano ? 'Nano S' : 'Blue'}
/> />
<StepContainerInner> <StepContainerInner>
<Title data-e2e="onboarding_title">{t('onboarding:analytics.title')}</Title> <Title data-e2e="onboarding_title">{t('onboarding.analytics.title')}</Title>
<Description>{t('onboarding:analytics.desc')}</Description> <Description>{t('onboarding.analytics.desc')}</Description>
<Box mt={5}> <Box mt={5}>
<Container> <Container>
<Box> <Box>
<Box horizontal mb={1}> <Box horizontal mb={1}>
<AnalyticsTitle>{t('onboarding:analytics.technicalData.title')}</AnalyticsTitle> <AnalyticsTitle data-e2e="analytics_techData">
{t('onboarding.analytics.technicalData.title')}
</AnalyticsTitle>
<LearnMoreWrapper> <LearnMoreWrapper>
<FakeLink <FakeLink
underline underline
@ -84,15 +92,16 @@ class Analytics extends PureComponent<StepProps, State> {
color="smoke" color="smoke"
ml={2} ml={2}
onClick={this.handleTechnicalDataModal} onClick={this.handleTechnicalDataModal}
data-e2e="analytics_techData_Link"
> >
{t('app:common.learnMore')} {t('common.learnMore')}
</FakeLink> </FakeLink>
</LearnMoreWrapper> </LearnMoreWrapper>
</Box> </Box>
<TechnicalData /> <TechnicalData />
<AnalyticsText>{t('onboarding:analytics.technicalData.desc')}</AnalyticsText> <AnalyticsText>{t('onboarding.analytics.technicalData.desc')}</AnalyticsText>
<MandatoryText> <MandatoryText>
{t('onboarding:analytics.technicalData.mandatoryText')} {t('onboarding.analytics.technicalData.mandatoryText')}
</MandatoryText> </MandatoryText>
</Box> </Box>
<Box justifyContent="center"> <Box justifyContent="center">
@ -102,7 +111,9 @@ class Analytics extends PureComponent<StepProps, State> {
<Container> <Container>
<Box> <Box>
<Box horizontal mb={1}> <Box horizontal mb={1}>
<AnalyticsTitle>{t('onboarding:analytics.shareAnalytics.title')}</AnalyticsTitle> <AnalyticsTitle data-e2e="analytics_shareAnalytics">
{t('onboarding.analytics.shareAnalytics.title')}
</AnalyticsTitle>
<LearnMoreWrapper> <LearnMoreWrapper>
<FakeLink <FakeLink
style={{ textDecoration: 'underline' }} style={{ textDecoration: 'underline' }}
@ -110,13 +121,14 @@ class Analytics extends PureComponent<StepProps, State> {
color="smoke" color="smoke"
ml={2} ml={2}
onClick={this.handleShareAnalyticsModal} onClick={this.handleShareAnalyticsModal}
data-e2e="analytics_shareAnalytics_Link"
> >
{t('app:common.learnMore')} {t('common.learnMore')}
</FakeLink> </FakeLink>
</LearnMoreWrapper> </LearnMoreWrapper>
<ShareAnalytics /> <ShareAnalytics />
</Box> </Box>
<AnalyticsText>{t('onboarding:analytics.shareAnalytics.desc')}</AnalyticsText> <AnalyticsText>{t('onboarding.analytics.shareAnalytics.desc')}</AnalyticsText>
</Box> </Box>
<Box justifyContent="center"> <Box justifyContent="center">
<Track <Track
@ -133,9 +145,11 @@ class Analytics extends PureComponent<StepProps, State> {
<Container> <Container>
<Box> <Box>
<Box mb={1}> <Box mb={1}>
<AnalyticsTitle>{t('onboarding:analytics.sentryLogs.title')}</AnalyticsTitle> <AnalyticsTitle data-e2e="analytics_reportBugs">
{t('onboarding.analytics.sentryLogs.title')}
</AnalyticsTitle>
</Box> </Box>
<AnalyticsText>{t('onboarding:analytics.sentryLogs.desc')}</AnalyticsText> <AnalyticsText>{t('onboarding.analytics.sentryLogs.desc')}</AnalyticsText>
</Box> </Box>
<Box justifyContent="center"> <Box justifyContent="center">
<Track <Track
@ -149,6 +163,26 @@ class Analytics extends PureComponent<StepProps, State> {
<Switch isChecked={sentryLogsToggle} onChange={this.handleSentryLogsToggle} /> <Switch isChecked={sentryLogsToggle} onChange={this.handleSentryLogsToggle} />
</Box> </Box>
</Container> </Container>
<Container>
<Box>
<Box mb={1}>
<AnalyticsTitle data-e2e="analytics_terms">
{t('onboarding.analytics.terms.title')}
</AnalyticsTitle>
</Box>
<AnalyticsText>
<div>
<Trans i18nKey="onboarding.analytics.terms.desc">
{'Accept the '}
<HoveredLink onClick={this.onClickTerms}>{'terms of license'}</HoveredLink>
{'and'}
<HoveredLink onClick={this.onClickPrivacy}>{'privacy'}</HoveredLink>
{'.'}
</Trans>
</div>
</AnalyticsText>
</Box>
</Container>
</Box> </Box>
</StepContainerInner> </StepContainerInner>
<OnboardingFooter <OnboardingFooter
@ -196,8 +230,16 @@ const Container = styled(Box).attrs({
width: 550px; width: 550px;
justify-content: space-between; justify-content: space-between;
` `
const LearnMoreWrapper = styled(Box).attrs({})` const LearnMoreWrapper = styled(Box)`
${FakeLink}:hover { ${FakeLink}:hover {
color: ${p => p.theme.colors.wallet}; color: ${p => p.theme.colors.wallet};
} }
` `
const HoveredLink = styled.span`
cursor: pointer;
text-decoration: underline;
&:hover {
color: ${p => p.theme.colors.wallet};
}
`

6
src/components/Onboarding/steps/Finish.js

@ -103,12 +103,12 @@ export default class Finish extends Component<StepProps, *> {
</Box> </Box>
<Box pt={5} align="center"> <Box pt={5} align="center">
<Title data-e2e="finish_title">{t('onboarding:finish.title')}</Title> <Title data-e2e="finish_title">{t('onboarding.finish.title')}</Title>
<Description>{t('onboarding:finish.desc')}</Description> <Description>{t('onboarding.finish.desc')}</Description>
</Box> </Box>
<Box p={5}> <Box p={5}>
<Button primary onClick={() => finish()} data-e2e="continue_button"> <Button primary onClick={() => finish()} data-e2e="continue_button">
{t('onboarding:finish.openAppButton')} {t('onboarding.finish.openAppButton')}
</Button> </Button>
</Box> </Box>
<Box horizontal mt={3} flow={5} color="grey"> <Box horizontal mt={3} flow={5} color="grey">

16
src/components/Onboarding/steps/GenuineCheck/GenuineCheckErrorPage.js

@ -40,21 +40,21 @@ class GenuineCheckErrorPage extends PureComponent<Props, *> {
{onboarding.genuine.isGenuineFail ? ( {onboarding.genuine.isGenuineFail ? (
<Fragment> <Fragment>
{this.trackErrorPage('Not Genuine')} {this.trackErrorPage('Not Genuine')}
<Title>{t('onboarding:genuineCheck.errorPage.title.isGenuineFail')}</Title> <Title>{t('onboarding.genuineCheck.errorPage.title.isGenuineFail')}</Title>
<Description>{t('onboarding:genuineCheck.errorPage.desc.isGenuineFail')}</Description> <Description>{t('onboarding.genuineCheck.errorPage.desc.isGenuineFail')}</Description>
</Fragment> </Fragment>
) : !onboarding.genuine.pinStepPass ? ( ) : !onboarding.genuine.pinStepPass ? (
<Fragment> <Fragment>
{this.trackErrorPage('PIN Step')} {this.trackErrorPage('PIN Step')}
<Title>{t('onboarding:genuineCheck.errorPage.title.pinFailed')}</Title> <Title>{t('onboarding.genuineCheck.errorPage.title.pinFailed')}</Title>
<Description>{t('onboarding:genuineCheck.errorPage.desc.pinFailed')}</Description> <Description>{t('onboarding.genuineCheck.errorPage.desc.pinFailed')}</Description>
</Fragment> </Fragment>
) : ( ) : (
<Fragment> <Fragment>
{this.trackErrorPage('Recovery Phase Step')} {this.trackErrorPage('Recovery Phase Step')}
<Title>{t('onboarding:genuineCheck.errorPage.title.recoveryPhraseFailed')}</Title> <Title>{t('onboarding.genuineCheck.errorPage.title.recoveryPhraseFailed')}</Title>
<Description> <Description>
{t('onboarding:genuineCheck.errorPage.desc.recoveryPhraseFailed')} {t('onboarding.genuineCheck.errorPage.desc.recoveryPhraseFailed')}
</Description> </Description>
</Fragment> </Fragment>
)} )}
@ -78,12 +78,12 @@ class GenuineCheckErrorPage extends PureComponent<Props, *> {
</Box> </Box>
<OnboardingFooterWrapper> <OnboardingFooterWrapper>
<Button outlineGrey onClick={() => redoGenuineCheck()}> <Button outlineGrey onClick={() => redoGenuineCheck()}>
{t('app:common.back')} {t('common.back')}
</Button> </Button>
<ExternalLinkButton <ExternalLinkButton
danger danger
ml="auto" ml="auto"
label={t('onboarding:genuineCheck.buttons.contactSupport')} label={t('onboarding.genuineCheck.buttons.contactSupport')}
url={urls.contactSupport} url={urls.contactSupport}
/> />
</OnboardingFooterWrapper> </OnboardingFooterWrapper>

8
src/components/Onboarding/steps/GenuineCheck/GenuineCheckUnavailable.js

@ -27,7 +27,7 @@ export function GenuineCheckUnavailableFooter({
return ( return (
<OnboardingFooterWrapper> <OnboardingFooterWrapper>
<Button outlineGrey onClick={() => prevStep()}> <Button outlineGrey onClick={() => prevStep()}>
{t('app:common.back')} {t('common.back')}
</Button> </Button>
<Box horizontal ml="auto"> <Box horizontal ml="auto">
<Button <Button
@ -36,10 +36,10 @@ export function GenuineCheckUnavailableFooter({
onClick={() => nextStep()} onClick={() => nextStep()}
mx={2} mx={2}
> >
{t('app:common.skipThisStep')} {t('common.skipThisStep')}
</Button> </Button>
<Button onClick={nextStep} disabled primary> <Button onClick={nextStep} disabled primary>
{t('app:common.continue')} {t('common.continue')}
</Button> </Button>
</Box> </Box>
</OnboardingFooterWrapper> </OnboardingFooterWrapper>
@ -81,7 +81,7 @@ export function GenuineCheckUnavailableMessage({
}) })
}} }}
> >
{t('app:common.retry')} {t('common.retry')}
</FakeLink> </FakeLink>
</Box> </Box>
) )

20
src/components/Onboarding/steps/GenuineCheck/index.js

@ -59,12 +59,12 @@ class GenuineCheck extends PureComponent<StepProps, State> {
const { t } = this.props const { t } = this.props
return [ return [
{ {
label: t('app:common.labelYes'), label: t('common.labelYes'),
key: 'yes', key: 'yes',
pass: true, pass: true,
}, },
{ {
label: t('app:common.labelNo'), label: t('common.labelNo'),
key: 'no', key: 'no',
pass: false, pass: false,
}, },
@ -174,18 +174,18 @@ class GenuineCheck extends PureComponent<StepProps, State> {
deviceType={onboarding.isLedgerNano ? 'Nano S' : 'Blue'} deviceType={onboarding.isLedgerNano ? 'Nano S' : 'Blue'}
/> />
<StepContainerInner> <StepContainerInner>
<Title>{t('onboarding:genuineCheck.title')}</Title> <Title>{t('onboarding.genuineCheck.title')}</Title>
{onboarding.flowType === 'restoreDevice' ? ( {onboarding.flowType === 'restoreDevice' ? (
<Description>{t('onboarding:genuineCheck.descRestore')}</Description> <Description>{t('onboarding.genuineCheck.descRestore')}</Description>
) : ( ) : (
<Description>{t('onboarding:genuineCheck.descGeneric')}</Description> <Description>{t('onboarding.genuineCheck.descGeneric')}</Description>
)} )}
<Box mt={5}> <Box mt={5}>
<GenuineCheckCardWrapper> <GenuineCheckCardWrapper>
<Box justify="center"> <Box justify="center">
<Box horizontal> <Box horizontal>
<IconOptionRow>{'1.'}</IconOptionRow> <IconOptionRow>{'1.'}</IconOptionRow>
<CardTitle>{t('onboarding:genuineCheck.step1.title')}</CardTitle> <CardTitle>{t('onboarding.genuineCheck.step1.title')}</CardTitle>
</Box> </Box>
</Box> </Box>
<Box justify="center"> <Box justify="center">
@ -204,7 +204,7 @@ class GenuineCheck extends PureComponent<StepProps, State> {
<IconOptionRow color={!genuine.pinStepPass ? 'grey' : 'wallet'}> <IconOptionRow color={!genuine.pinStepPass ? 'grey' : 'wallet'}>
{'2.'} {'2.'}
</IconOptionRow> </IconOptionRow>
<CardTitle>{t('onboarding:genuineCheck.step2.title')}</CardTitle> <CardTitle>{t('onboarding.genuineCheck.step2.title')}</CardTitle>
</Box> </Box>
</Box> </Box>
<Box justify="center"> <Box justify="center">
@ -228,7 +228,7 @@ class GenuineCheck extends PureComponent<StepProps, State> {
<IconOptionRow color={!genuine.recoveryStepPass ? 'grey' : 'wallet'}> <IconOptionRow color={!genuine.recoveryStepPass ? 'grey' : 'wallet'}>
{'3.'} {'3.'}
</IconOptionRow> </IconOptionRow>
<CardTitle>{t('onboarding:genuineCheck.step3.title')}</CardTitle> <CardTitle>{t('onboarding.genuineCheck.step3.title')}</CardTitle>
</Box> </Box>
</Box> </Box>
{genuine.recoveryStepPass && ( {genuine.recoveryStepPass && (
@ -237,7 +237,7 @@ class GenuineCheck extends PureComponent<StepProps, State> {
<Box horizontal align="center" flow={1} color={colors.wallet}> <Box horizontal align="center" flow={1} color={colors.wallet}>
<IconCheck size={16} /> <IconCheck size={16} />
<Box ff="Open Sans|SemiBold" fontSize={4}> <Box ff="Open Sans|SemiBold" fontSize={4}>
{t('onboarding:genuineCheck.isGenuinePassed')} {t('onboarding.genuineCheck.isGenuinePassed')}
</Box> </Box>
</Box> </Box>
) : genuine.genuineCheckUnavailable ? ( ) : genuine.genuineCheckUnavailable ? (
@ -250,7 +250,7 @@ class GenuineCheck extends PureComponent<StepProps, State> {
disabled={!genuine.recoveryStepPass} disabled={!genuine.recoveryStepPass}
onClick={this.handleOpenGenuineCheckModal} onClick={this.handleOpenGenuineCheckModal}
> >
{t('onboarding:genuineCheck.buttons.genuineCheck')} {t('onboarding.genuineCheck.buttons.genuineCheck')}
</Button> </Button>
)} )}
</Box> </Box>

10
src/components/Onboarding/steps/Init.js

@ -30,7 +30,7 @@ class Init extends PureComponent<StepProps, *> {
{ {
key: 'newDevice', key: 'newDevice',
icon: <IconPlus size={20} />, icon: <IconPlus size={20} />,
title: t('onboarding:init.newDevice.title'), title: t('onboarding.init.newDevice.title'),
onClick: () => { onClick: () => {
jumpStep('selectDevice') jumpStep('selectDevice')
flowType('newDevice') flowType('newDevice')
@ -39,7 +39,7 @@ class Init extends PureComponent<StepProps, *> {
{ {
key: 'restoreDevice', key: 'restoreDevice',
icon: <IconRecover size={20} />, icon: <IconRecover size={20} />,
title: t('onboarding:init.restoreDevice.title'), title: t('onboarding.init.restoreDevice.title'),
onClick: () => { onClick: () => {
jumpStep('selectDevice') jumpStep('selectDevice')
flowType('restoreDevice') flowType('restoreDevice')
@ -48,7 +48,7 @@ class Init extends PureComponent<StepProps, *> {
{ {
key: 'initializedDevice', key: 'initializedDevice',
icon: <IconCheck size={20} />, icon: <IconCheck size={20} />,
title: t('onboarding:init.initializedDevice.title'), title: t('onboarding.init.initializedDevice.title'),
onClick: () => { onClick: () => {
jumpStep('selectDevice') jumpStep('selectDevice')
flowType('initializedDevice') flowType('initializedDevice')
@ -57,7 +57,7 @@ class Init extends PureComponent<StepProps, *> {
{ {
key: 'noDevice', key: 'noDevice',
icon: <IconExternalLink size={20} />, icon: <IconExternalLink size={20} />,
title: t('onboarding:noDevice.title'), title: t('onboarding.noDevice.title'),
onClick: () => { onClick: () => {
jumpStep('noDevice') jumpStep('noDevice')
flowType('noDevice') flowType('noDevice')
@ -77,7 +77,7 @@ class Init extends PureComponent<StepProps, *> {
} }
/> />
<Box m={5} style={{ maxWidth: 480 }}> <Box m={5} style={{ maxWidth: 480 }}>
<Title>{t('onboarding:init.title')}</Title> <Title>{t('onboarding.init.title')}</Title>
</Box> </Box>
<Box pt={4} flow={4}> <Box pt={4} flow={4}>
{optionCards.map(card => <OptionFlowCard key={card.key} card={card} />)} {optionCards.map(card => <OptionFlowCard key={card.key} card={card} />)}

10
src/components/Onboarding/steps/NoDevice.js

@ -26,7 +26,7 @@ class NoDevice extends PureComponent<StepProps, *> {
{ {
key: 'buyNew', key: 'buyNew',
icon: <IconCart size={20} />, icon: <IconCart size={20} />,
title: t('onboarding:noDevice.buyNew.title'), title: t('onboarding.noDevice.buyNew.title'),
onClick: () => { onClick: () => {
openURL(urls.noDeviceBuyNew) openURL(urls.noDeviceBuyNew)
}, },
@ -34,7 +34,7 @@ class NoDevice extends PureComponent<StepProps, *> {
{ {
key: 'trackOrder', key: 'trackOrder',
icon: <IconTruck size={20} />, icon: <IconTruck size={20} />,
title: t('onboarding:noDevice.trackOrder.title'), title: t('onboarding.noDevice.trackOrder.title'),
onClick: () => { onClick: () => {
openURL(urls.noDeviceTrackOrder) openURL(urls.noDeviceTrackOrder)
}, },
@ -42,7 +42,7 @@ class NoDevice extends PureComponent<StepProps, *> {
{ {
key: 'learnMore', key: 'learnMore',
icon: <IconInfoCircle size={20} />, icon: <IconInfoCircle size={20} />,
title: t('onboarding:noDevice.learnMore.title'), title: t('onboarding.noDevice.learnMore.title'),
onClick: () => { onClick: () => {
openURL(urls.noDeviceLearnMore) openURL(urls.noDeviceLearnMore)
}, },
@ -68,7 +68,7 @@ class NoDevice extends PureComponent<StepProps, *> {
} }
/> />
<Box m={5} style={{ maxWidth: 480 }}> <Box m={5} style={{ maxWidth: 480 }}>
<Title>{t('onboarding:noDevice.title')}</Title> <Title>{t('onboarding.noDevice.title')}</Title>
</Box> </Box>
<Box pt={4} flow={4}> <Box pt={4} flow={4}>
{optionCards.map(card => <OptionFlowCard key={card.key} card={card} />)} {optionCards.map(card => <OptionFlowCard key={card.key} card={card} />)}
@ -77,7 +77,7 @@ class NoDevice extends PureComponent<StepProps, *> {
</GrowScroll> </GrowScroll>
<OnboardingFooterWrapper> <OnboardingFooterWrapper>
<Button outlineGrey onClick={() => prevStep()} mr="auto"> <Button outlineGrey onClick={() => prevStep()} mr="auto">
{t('app:common.back')} {t('common.back')}
</Button> </Button>
</OnboardingFooterWrapper> </OnboardingFooterWrapper>
</Box> </Box>

6
src/components/Onboarding/steps/SelectDevice.js

@ -41,7 +41,7 @@ class SelectDevice extends PureComponent<StepProps, {}> {
<TrackPage category="Onboarding" name="Select Device" flowType={onboarding.flowType} /> <TrackPage category="Onboarding" name="Select Device" flowType={onboarding.flowType} />
<StepContainerInner> <StepContainerInner>
<Box mb={5}> <Box mb={5}>
<Title>{t('onboarding:selectDevice.title')}</Title> <Title>{t('onboarding.selectDevice.title')}</Title>
</Box> </Box>
<Box pt={4}> <Box pt={4}>
<Inner> <Inner>
@ -53,7 +53,7 @@ class SelectDevice extends PureComponent<StepProps, {}> {
<DeviceIcon> <DeviceIcon>
<img alt="" src={i('ledger-nano-onb.svg')} /> <img alt="" src={i('ledger-nano-onb.svg')} />
</DeviceIcon> </DeviceIcon>
<BlockTitle>{t('onboarding:selectDevice.ledgerNanoCard.title')}</BlockTitle> <BlockTitle>{t('onboarding.selectDevice.ledgerNanoCard.title')}</BlockTitle>
</DeviceContainer> </DeviceContainer>
<DeviceContainer <DeviceContainer
isActive={!onboarding.isLedgerNano && onboarding.isLedgerNano !== null} isActive={!onboarding.isLedgerNano && onboarding.isLedgerNano !== null}
@ -63,7 +63,7 @@ class SelectDevice extends PureComponent<StepProps, {}> {
<DeviceIcon> <DeviceIcon>
<img alt="" src={i('ledger-blue-onb.svg')} /> <img alt="" src={i('ledger-blue-onb.svg')} />
</DeviceIcon> </DeviceIcon>
<BlockTitle>{t('onboarding:selectDevice.ledgerBlueCard.title')}</BlockTitle> <BlockTitle>{t('onboarding.selectDevice.ledgerBlueCard.title')}</BlockTitle>
</DeviceContainer> </DeviceContainer>
</Inner> </Inner>
</Box> </Box>

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

@ -25,14 +25,14 @@ class SelectPIN extends PureComponent<Props, *> {
{ {
key: 'step1', key: 'step1',
icon: <IconOptionRow>{'1.'}</IconOptionRow>, icon: <IconOptionRow>{'1.'}</IconOptionRow>,
desc: t('onboarding:selectPIN.initialize.instructions.blue.step1'), desc: t('onboarding.selectPIN.initialize.instructions.blue.step1'),
}, },
{ {
key: 'step2', key: 'step2',
icon: <IconOptionRow>{'2.'}</IconOptionRow>, icon: <IconOptionRow>{'2.'}</IconOptionRow>,
desc: ( desc: (
<Box style={{ display: 'block' }}> <Box style={{ display: 'block' }}>
<Trans i18nKey="onboarding:selectPIN.initialize.instructions.blue.step2"> <Trans i18nKey="onboarding.selectPIN.initialize.instructions.blue.step2">
{'Tap on'} {'Tap on'}
<Text ff="Open Sans|SemiBold" color="dark"> <Text ff="Open Sans|SemiBold" color="dark">
{'Configure as new device'} {'Configure as new device'}
@ -44,7 +44,7 @@ class SelectPIN extends PureComponent<Props, *> {
{ {
key: 'step3', key: 'step3',
icon: <IconOptionRow>{'3.'}</IconOptionRow>, icon: <IconOptionRow>{'3.'}</IconOptionRow>,
desc: t('onboarding:selectPIN.initialize.instructions.blue.step3'), desc: t('onboarding.selectPIN.initialize.instructions.blue.step3'),
}, },
] ]
@ -52,17 +52,17 @@ class SelectPIN extends PureComponent<Props, *> {
{ {
key: 'note1', key: 'note1',
icon: <IconChevronRight size={12} style={{ color: colors.smoke }} />, icon: <IconChevronRight size={12} style={{ color: colors.smoke }} />,
desc: t('onboarding:selectPIN.disclaimer.note1'), desc: t('onboarding.selectPIN.disclaimer.note1'),
}, },
{ {
key: 'note2', key: 'note2',
icon: <IconChevronRight size={12} style={{ color: colors.smoke }} />, icon: <IconChevronRight size={12} style={{ color: colors.smoke }} />,
desc: t('onboarding:selectPIN.disclaimer.note2'), desc: t('onboarding.selectPIN.disclaimer.note2'),
}, },
{ {
key: 'note3', key: 'note3',
icon: <IconChevronRight size={12} style={{ color: colors.smoke }} />, icon: <IconChevronRight size={12} style={{ color: colors.smoke }} />,
desc: t('onboarding:selectPIN.disclaimer.note3'), desc: t('onboarding.selectPIN.disclaimer.note3'),
}, },
] ]

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

Loading…
Cancel
Save