Browse Source

Merge pull request #1106 from LedgerHQ/develop

Prepare for 1.0.2
master
Gaëtan Renaudeau 7 years ago
committed by GitHub
parent
commit
e3730288ef
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      .github/ISSUE_TEMPLATE/bug_report.md
  2. 8
      .github/ISSUE_TEMPLATE/enhancement.md
  3. 8
      .github/ISSUE_TEMPLATE/feature_request.md
  4. BIN
      docs/screenshot.png
  5. 3
      electron-builder.yml
  6. 1
      package.json
  7. 3
      scripts/compile.sh
  8. 4
      scripts/dist.sh
  9. 6
      scripts/download-analytics.sh
  10. 10
      scripts/hash-utils.sh
  11. 7
      scripts/install-ci-deps.sh
  12. 17
      scripts/postinstall.sh
  13. 12
      scripts/reset-files.sh
  14. 20
      src/bridge/BridgeSyncContext.js
  15. 13
      src/bridge/EthereumJSBridge.js
  16. 3
      src/bridge/LibcoreBridge.js
  17. 6
      src/bridge/RippleJSBridge.js
  18. 4
      src/commands/libcoreScanAccounts.js
  19. 4
      src/commands/libcoreSignAndBroadcast.js
  20. 2
      src/commands/libcoreSyncAccount.js
  21. 2
      src/components/AccountPage/EmptyStateAccount.js
  22. 1
      src/components/BalanceSummary/index.js
  23. 4
      src/components/CalculateBalance.js
  24. 9
      src/components/DeviceBusyIndicator.js
  25. 8
      src/components/EnsureDeviceApp.js
  26. 21
      src/components/ExchangePage/index.js
  27. 46
      src/components/ExchangePage/logos/paybis.js
  28. 27
      src/components/ExchangePage/logos/simplex.js
  29. 38
      src/components/ExportLogsBtn.js
  30. 2
      src/components/FeesField/GenericContainer.js
  31. 3
      src/components/GenuineCheck.js
  32. 1
      src/components/ManagerPage/ManagerApp.js
  33. 2
      src/components/ManagerPage/index.js
  34. 21
      src/components/Onboarding/OnboardingBreadcrumb.js
  35. 2
      src/components/Onboarding/steps/Finish.js
  36. 15
      src/components/Onboarding/steps/GenuineCheck/index.js
  37. 2
      src/components/Onboarding/steps/NoDevice.js
  38. 30
      src/components/OpenUserDataDirectoryBtn.js
  39. 30
      src/components/RenderError.js
  40. 16
      src/components/SelectExchange.js
  41. 1
      src/components/SettingsPage/DisablePasswordModal.js
  42. 50
      src/components/SettingsPage/LaunchOnboardingBtn.js
  43. 3
      src/components/SettingsPage/PasswordForm.js
  44. 2
      src/components/SettingsPage/sections/About.js
  45. 39
      src/components/SettingsPage/sections/Help.js
  46. 36
      src/components/SyncBackground.js
  47. 22
      src/components/TranslatedError.js
  48. 4
      src/components/base/Chart/Tooltip.js
  49. 2
      src/components/base/Chart/handleMouseEvents.js
  50. 1
      src/components/base/Chart/index.js
  51. 2
      src/components/base/Input/index.js
  52. 5
      src/components/base/InputPassword/index.js
  53. 2
      src/components/layout/Default.js
  54. 6
      src/components/modals/AccountSettingRenderBody.js
  55. 14
      src/components/modals/AddAccounts/steps/03-step-import.js
  56. 4
      src/components/modals/Receive/index.js
  57. 47
      src/components/modals/ReleaseNotes/ReleaseNotesBody.js
  58. 13
      src/components/modals/Send/fields/RecipientField.js
  59. 67
      src/components/modals/Send/steps/01-step-amount.js
  60. 19
      src/config/urls.js
  61. 2
      src/helpers/SettingsDefaults.js
  62. 2
      src/helpers/derivations.js
  63. 17
      src/helpers/deviceAccess.js
  64. 4
      src/helpers/getAddressForCurrency/btc.js
  65. 113
      src/helpers/libcore.js
  66. 12
      src/helpers/pname.js
  67. 17
      src/helpers/resolveLogsDirectory.js
  68. 10
      src/helpers/resolveUserDataDirectory.js
  69. 8
      src/helpers/socket.js
  70. 3
      src/helpers/withLibcore.js
  71. 2
      src/index.ejs
  72. 7
      src/internals/index.js
  73. 1
      src/logger/logger-storybook.js
  74. 180
      src/logger/logger.js
  75. 14
      src/main/app.js
  76. 3
      src/main/bridge.js
  77. 51
      src/reducers/onboarding.js
  78. 4
      src/renderer/events.js
  79. 19
      src/renderer/init.js
  80. 4
      src/sentry/browser.js
  81. 32
      src/sentry/install.js
  82. 4
      src/sentry/node.js
  83. 46
      static/i18n/en/app.yml
  84. 45
      static/i18n/en/errors.yml
  85. 1
      static/i18n/en/language.yml
  86. 29
      static/i18n/en/onboarding.yml
  87. 24
      static/i18n/fr/app.yml
  88. 31
      static/i18n/fr/errors.yml
  89. 4
      webpack/internals.config.js
  90. 3
      webpack/main.config.js
  91. 2
      webpack/renderer.config.js
  92. 97
      yarn.lock

4
.github/ISSUE_TEMPLATE/bug_report.md

@ -7,8 +7,8 @@ about: Report a bug in Ledger Live Desktop or a regression.
<!-- Precise the app version (Settings > About or bottom-left corner on a crash screen) -->
- Ledger Live **version_here**
- Platform: **windows OR mac OR linux**
- tested on Ledger Live **version_here**
- Platform and version: **e.g. Mac 10.13.5 / Windows 10 / ..**
#### Expected behavior

8
.github/ISSUE_TEMPLATE/enhancement.md

@ -3,6 +3,10 @@ name: 🗣 Start a Discussion
about: Discuss to propose changes to improve the state of Ledger Live.
---
<!-- DESCRIPTION: Explain precisely what you think should be improved and how you think it should work -->
#### Ledger Live Version
<!-- Precise your current app version (Settings > About or bottom-left corner on a crash screen) -->
@ -12,7 +16,3 @@ about: Discuss to propose changes to improve the state of Ledger Live.
#### Part of the application to improve
<!-- which part is to improve? e.g. Send > Step 1 -->
#### Description
<!-- Explain precisely what you think should be improved and how you think it should work -->

8
.github/ISSUE_TEMPLATE/feature_request.md

@ -5,10 +5,10 @@ about: Any feature you find missing in Ledger Live? Discuss to suggest feature r
- [ ] I have checked this feature was not yet requested.
#### Part of the application
<!-- DESCRIPTION: Explain precisely what is the feature about -->
<!-- what part of the application would be impacted by this feature? -->
#### Description
<!-- Explain precisely what is the feature about -->
#### Part of the application
<!-- what part of the application would be impacted by this feature? -->

BIN
docs/screenshot.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 346 KiB

After

Width:  |  Height:  |  Size: 371 KiB

3
electron-builder.yml

@ -36,9 +36,6 @@ win:
- target: nsis
arch:
- x64
- target: zip
arch:
- x64
nsis:
oneClick: false

1
package.json

@ -55,6 +55,7 @@
"debug": "^3.1.0",
"downshift": "^1.31.16",
"eip55": "^1.0.3",
"electron-context-menu": "^0.10.0",
"electron-store": "^1.3.0",
"electron-updater": "^2.21.8",
"ethereumjs-tx": "^1.3.4",

3
scripts/compile.sh

@ -2,7 +2,8 @@
set -e
export GIT_REVISION=`git rev-parse HEAD`
GIT_REVISION=$(git rev-parse HEAD)
export GIT_REVISION
export SENTRY_URL=https://db8f5b9b021048d4a401f045371701cb@sentry.io/274561
export JOBS=max

4
scripts/dist.sh

@ -4,13 +4,13 @@
# some context:
# - https://github.com/electron-userland/electron-builder/issues/2577
# - https://github.com/electron-userland/electron-builder/issues/2269
if [[ `uname` == 'Linux' ]]; then
if [[ $(uname) == 'Linux' ]]; then
mv build/icon.png /tmp
fi
yarn compile && DEBUG=electron-builder electron-builder
# hilarious fix continuation: put back the icon where it was
if [[ `uname` == 'Linux' ]]; then
if [[ $(uname) == 'Linux' ]]; then
mv /tmp/icon.png build
fi

6
scripts/download-analytics.sh

@ -1,10 +1,10 @@
#!/bin/bash
if [ -z $ANALYTICS_KEY ]; then
if [ -z "$ANALYTICS_KEY" ]; then
echo 'ANALYTICS_KEY must be set'
exit 1
fi
cd `dirname $0`/..
cd "$(dirname "$0")/.." || exit
wget https://cdn.segment.com/analytics.js/v1/$ANALYTICS_KEY/analytics.min.js -O static/analytics.min.js
wget https://cdn.segment.com/analytics.js/v1/"$ANALYTICS_KEY"/analytics.min.js -O static/analytics.min.js

10
scripts/hash-utils.sh

@ -7,12 +7,12 @@ function GET_HASH_PATH {
function GET_HASH {
HASH_NAME=$1
HASH_PATH=`GET_HASH_PATH $HASH_NAME`
HASH_PATH=$(GET_HASH_PATH "$HASH_NAME")
if [ ! -e "$HASH_PATH" ]; then
echo ''
else
HASH_CONTENT=`cat "$HASH_PATH"`
echo $HASH_CONTENT
HASH_CONTENT=$(cat "$HASH_PATH")
echo "$HASH_CONTENT"
fi
}
@ -20,7 +20,7 @@ function SET_HASH {
HASH_NAME=$1
HASH_CONTENT=$2
echo "setting hash $HASH_NAME to $HASH_CONTENT"
HASH_PATH=`GET_HASH_PATH $HASH_NAME`
HASH_PATH=$(GET_HASH_PATH "$HASH_NAME")
mkdir -p ./node_modules/.cache
echo $HASH_CONTENT > $HASH_PATH
echo "$HASH_CONTENT" > "$HASH_PATH"
}

7
scripts/install-ci-deps.sh

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

17
scripts/postinstall.sh

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

12
scripts/reset-files.sh

@ -4,24 +4,24 @@ set -e
echo "> Getting user data folder..."
TMP_FILE=`mktemp`
cat <<EOF > $TMP_FILE
TMP_FILE=$(mktemp)
cat <<EOF > "$TMP_FILE"
const { app } = require('electron')
console.log(app.getPath('userData'))
EOF
USER_DATA_FOLDER=`timeout 0.5 electron $TMP_FILE || echo` # echo used to ensure status 0
USER_DATA_FOLDER=$(timeout 0.5 electron "$TMP_FILE" || echo) # echo used to ensure status 0
if [ "$USER_DATA_FOLDER" == "" ]; then
echo "You probably are on a slow computer. Be patient..."
USER_DATA_FOLDER=`timeout 3 electron $TMP_FILE || echo` # echo used to ensure status 0
USER_DATA_FOLDER=$(timeout 3 electron "$TMP_FILE" || echo) # echo used to ensure status 0
fi
if [ "$USER_DATA_FOLDER" == "" ]; then
echo "Apparently, very very slow computer..."
USER_DATA_FOLDER=`timeout 6 electron $TMP_FILE || echo` # echo used to ensure status 0
USER_DATA_FOLDER=$(timeout 6 electron "$TMP_FILE" || echo) # echo used to ensure status 0
fi
rm $TMP_FILE
rm "$TMP_FILE"
if [ "$USER_DATA_FOLDER" == "" ]; then
echo "Could not find the data folder. Bye"

20
src/bridge/BridgeSyncContext.js

@ -17,12 +17,7 @@ import { setAccountSyncState } from 'actions/bridgeSync'
import { bridgeSyncSelector, syncStateLocalSelector } from 'reducers/bridgeSync'
import type { BridgeSyncState } from 'reducers/bridgeSync'
import { accountsSelector } from 'reducers/accounts'
import {
SYNC_BOOT_DELAY,
SYNC_ALL_INTERVAL,
SYNC_MAX_CONCURRENT,
SYNC_TIMEOUT,
} from 'config/constants'
import { SYNC_MAX_CONCURRENT, SYNC_TIMEOUT } from 'config/constants'
import { getBridgeForCurrency } from '.'
type BridgeSyncProviderProps = {
@ -151,19 +146,6 @@ class Provider extends Component<BridgeSyncProviderOwnProps, Sync> {
this.api = sync
}
componentDidMount() {
const syncLoop = async () => {
this.api({ type: 'BACKGROUND_TICK' })
this.syncTimeout = setTimeout(syncLoop, SYNC_ALL_INTERVAL)
}
this.syncTimeout = setTimeout(syncLoop, SYNC_BOOT_DELAY)
}
componentWillUnmount() {
clearTimeout(this.syncTimeout)
}
syncTimeout: *
api: Sync
render() {

13
src/bridge/EthereumJSBridge.js

@ -176,6 +176,7 @@ const EthereumBridge: WalletBridge<Transaction> = {
index,
{ address, path: freshAddressPath, publicKey },
isStandard,
mandatoryCount,
): { account?: Account, complete?: boolean } {
const balance = await api.getAccountBalance(address)
if (finished) return { complete: true }
@ -211,6 +212,10 @@ const EthereumBridge: WalletBridge<Transaction> = {
}
newAccountCount++
}
if (index < mandatoryCount) {
return {}
}
// NB for legacy addresses maybe we will continue at least for the first 10 addresses
return { complete: true }
}
@ -254,7 +259,13 @@ const EthereumBridge: WalletBridge<Transaction> = {
const res = await getAddressCommand
.send({ currencyId: currency.id, devicePath: deviceId, path: freshAddressPath })
.toPromise()
const r = await stepAddress(index, res, isStandard)
const r = await stepAddress(
index,
res,
isStandard,
// $FlowFixMe i know i know, not part of function
derivation.mandatoryCount || 0,
)
if (r.account) o.next(r.account)
if (r.complete) {
break

3
src/bridge/LibcoreBridge.js

@ -138,7 +138,8 @@ const LibcoreBridge: WalletBridge<Transaction> = {
(accountOps.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))
if (hasChanged) {
patch.operations = syncedAccount.operations

6
src/bridge/RippleJSBridge.js

@ -192,9 +192,11 @@ type Tx = {
const txToOperation = (account: Account) => ({
id,
sequence,
type: txType,
outcome: { fee, deliveredAmount, ledgerVersion, timestamp },
specification: { source, destination },
}: Tx): Operation => {
}: Tx): ?Operation => {
if (txType === 'trustline') return null
const type = source.address === account.freshAddress ? 'OUT' : 'IN'
let value = deliveredAmount ? parseAPICurrencyObject(deliveredAmount) : 0
const feeValue = parseAPIValue(fee)
@ -334,7 +336,7 @@ const RippleJSBridge: WalletBridge<Transaction> = {
unit: currency.units[0],
lastSyncDate: new Date(),
}
account.operations = transactions.map(txToOperation(account))
account.operations = transactions.map(txToOperation(account)).filter(Boolean)
o.next(account)
}
}

4
src/commands/libcoreScanAccounts.js

@ -17,6 +17,7 @@ const cmd: Command<Input, Result> = createCommand(
'libcoreScanAccounts',
({ devicePath, currencyId }) =>
Observable.create(o => {
let unsubscribed = false
// TODO scanAccountsOnDevice should directly return a Observable so we just have to pass-in
withLibcore(core =>
scanAccountsOnDevice({
@ -26,6 +27,7 @@ const cmd: Command<Input, Result> = createCommand(
onAccountScanned: account => {
o.next(account)
},
isUnsubscribed: () => unsubscribed,
}).then(
() => {
o.complete()
@ -37,7 +39,7 @@ const cmd: Command<Input, Result> = createCommand(
)
function unsubscribe() {
// FIXME not implemented
unsubscribed = true
}
return unsubscribe

4
src/commands/libcoreSignAndBroadcast.js

@ -19,13 +19,15 @@ type BitcoinLikeTransaction = {
}
type Input = {
account: AccountRaw,
account: AccountRaw, // FIXME there is no reason we send the whole AccountRaw
transaction: BitcoinLikeTransaction,
deviceId: string,
}
type Result = { type: 'signed' } | { type: 'broadcasted', operation: OperationRaw }
// FIXME this command should be unified with 'signTransaction' command
const cmd: Command<Input, Result> = createCommand(
'libcoreSignAndBroadcast',
({ account, transaction, deviceId }) =>

2
src/commands/libcoreSyncAccount.js

@ -8,7 +8,7 @@ import { syncAccount } from 'helpers/libcore'
import withLibcore from 'helpers/withLibcore'
type Input = {
rawAccount: AccountRaw,
rawAccount: AccountRaw, // FIXME there is no reason we send the whole AccountRaw
}
type Result = AccountRaw

2
src/components/AccountPage/EmptyStateAccount.js

@ -45,7 +45,7 @@ class EmptyStateAccount extends PureComponent<Props, *> {
<Trans i18nKey="app:account.emptyState.desc">
{'Make sure the'}
<Text ff="Open Sans|SemiBold" color="dark">
{account.currency.name}
{account.currency.managerAppName}
</Text>
{'app is installed to receive funds.'}
</Trans>

1
src/components/BalanceSummary/index.js

@ -73,6 +73,7 @@ const BalanceSummary = ({
}
height={200}
currency={counterValue}
cvCode={counterValue.ticker}
tickXScale={selectedTimeRange}
renderTickY={
isAvailable ? val => formatShort(counterValue.units[0], val) : () => ''

4
src/components/CalculateBalance.js

@ -79,7 +79,9 @@ const mapStateToProps = (state: State, props: OwnProps) => {
balanceHistory,
balanceStart: balanceHistory[0].value,
balanceEnd,
hash: `${balanceHistory.length}_${balanceEnd}_${isAvailable.toString()}`,
hash: `${props.accounts.length > 0 ? props.accounts[0].id : ''}_${
balanceHistory.length
}_${balanceEnd}_${isAvailable.toString()}`,
}
}

9
src/components/DeviceBusyIndicator.js

@ -14,10 +14,10 @@ const Indicator = styled.div`
`
// NB this is done like this to be extremely performant. we don't want redux for this..
const perPaths = {}
let globalBusy = false
const instances = []
export const onSetDeviceBusy = (path, busy) => {
perPaths[path] = busy
export const onSetDeviceBusy = busy => {
globalBusy = busy
instances.forEach(i => i.forceUpdate())
}
@ -30,8 +30,7 @@ class DeviceBusyIndicator extends PureComponent<{}> {
instances.splice(i, 1)
}
render() {
const busy = Object.values(perPaths).reduce((busy, b) => busy || b, false)
return <Indicator busy={busy} />
return <Indicator busy={globalBusy} />
}
}

8
src/components/EnsureDeviceApp.js

@ -24,7 +24,6 @@ import type { Device } from 'types/common'
import { createCustomErrorClass } from 'helpers/errors'
import { getCurrentDevice } from 'reducers/devices'
export const WrongAppOpened = createCustomErrorClass('WrongAppOpened')
export const WrongDeviceForAccount = createCustomErrorClass('WrongDeviceForAccount')
const usbIcon = <IconUsb size={16} />
@ -77,20 +76,21 @@ class EnsureDeviceApp extends Component<{
const cur = account ? account.currency : currency
invariant(cur, 'No currency given')
return (
<Trans i18nKey="deviceConnect:step2.open" parent="div">
<Trans i18nKey="app:deviceConnect.step2.open" parent="div">
{'Open the '}
<Bold>{cur.name}</Bold>
<Bold>{cur.managerAppName}</Bold>
{' app on your device'}
</Trans>
)
}
render() {
const { account, currency, ...props } = this.props
const { account, currency, device, ...props } = this.props
const cur = account ? account.currency : currency
const Icon = cur ? getCryptoCurrencyIcon(cur) : null
return (
<DeviceInteraction
key={device ? device.path : null}
shouldRenderRetry
steps={[
{

21
src/components/ExchangePage/index.js

@ -4,6 +4,7 @@ import React, { PureComponent } from 'react'
import { translate } from 'react-i18next'
import type { T } from 'types/common'
import { urls } from 'config/urls'
import TrackPage from 'analytics/TrackPage'
import Box from 'components/base/Box'
@ -12,6 +13,8 @@ import ExchangeCard from './ExchangeCard'
import CoinhouseLogo from './logos/coinhouse'
import ChangellyLogo from './logos/changelly'
import CoinmamaLogo from './logos/bigmama'
import SimplexLogo from './logos/simplex'
import PaybisLogo from './logos/paybis'
type Props = {
t: T,
@ -21,21 +24,33 @@ const cards = [
{
key: 'coinhouse',
id: 'coinhouse',
url: 'https://www.coinhouse.com/r/157530',
url: urls.coinhouse,
logo: <CoinhouseLogo width={150} />,
},
{
key: 'changelly',
id: 'changelly',
url: 'https://changelly.com/?ref_id=aac789605a01',
url: urls.changelly,
logo: <ChangellyLogo width={150} />,
},
{
key: 'coinmama',
id: 'coinmama',
url: 'http://go.coinmama.com/visit/?bta=51801&nci=5343',
url: urls.coinmama,
logo: <CoinmamaLogo width={150} />,
},
{
key: 'simplex',
id: 'simplex',
url: urls.simplex,
logo: <SimplexLogo width={160} height={57} />,
},
{
key: 'paybis',
id: 'paybis',
url: urls.paybis,
logo: <PaybisLogo width={150} height={57} />,
},
]
class ExchangePage extends PureComponent<Props> {

46
src/components/ExchangePage/logos/paybis.js

@ -0,0 +1,46 @@
// @flow
import React, { Fragment } from 'react'
const inner = (
<Fragment>
<g fill="none" fillRule="evenodd">
<path d="M18.615 13.763h24.359V38.91h-24.36z" />
<path
fill="#424644"
d="M27.15 26.634h2.682c1.626 0 2.927-.297 3.902-.946 1.003-.595 1.599-1.46 1.843-2.596.243-1.298.054-2.19-.596-2.731-.65-.54-1.897-.784-3.74-.784H28.56l-1.41 7.057zm-1.572-12.87h3.577l-.623 3h3.062l.596-3h3.55l-.597 3h1.3c2.493 0 4.255.542 5.339 1.623 1.056 1.055 1.381 2.65.975 4.705-.406 2.028-1.355 3.596-2.9 4.705-1.517 1.109-3.522 1.65-5.933 1.65H26.58l-.271 1.297-1.517 7.599v.027l-.136.513h-5.771c.054-.162.081-.351.135-.54l3.306-16.603c.19-.892.135-1.514-.136-1.812-.298-.297-.948-.46-1.95-.46h-1.626l.515-2.703h5.852l.596-3.002z"
/>
<path d="M.813-.027H127.35v56.621H.813z" />
<path
fill="#3BB0DF"
d="M35.441 33.502c0 .081-.027.163-.027.244-.27 1.298-.84 2.19-1.707 2.73-.84.542-2.195.812-4.037.812h-.57c-3.711 0-9.537.081-13.276.081l-1.328 2.704h5.826l-.542 2.731h3.55l.541-2.73h3.062l-.542 2.73h3.55l.542-2.73h2.628c2.439 0 4.417-.542 5.934-1.596 1.49-1.082 2.438-2.65 2.872-4.732.406-2.028.054-3.57-1.03-4.678l-.054-.054 1.68-1.38h-7.451l-1.49 7.41 1.87-1.542zm8.02 17.008a26.402 26.402 0 0 1-14.333 4.219c-14.632 0-26.473-11.817-26.473-26.445 0-14.629 11.841-26.445 26.473-26.445 10.025 0 19.13 5.624 23.654 14.547h2.06C50.208 6.408 40.182 0 29.128 0 13.494 0 .813 12.655.813 28.284s12.68 28.283 28.315 28.283a28.4 28.4 0 0 0 17.53-6.057h-3.197zm82.182-16.845c.65-2.894-1.572-3.678-3.116-4.273-.949-.351-1.653-.676-1.463-1.541.135-.649.84-1.109 1.842-1.109.867 0 2.005.433 2.52.758l1.897-2.84c-.976-.595-2.385-1-4.282-1-3.414 0-5.744 1.622-6.34 4.218-.623 2.84 1.572 3.678 3.089 4.272.921.352 1.626.703 1.436 1.488-.162.703-.867 1.162-1.842 1.162-.867 0-2.222-.378-2.873-.757l-.84 3.624c.895.27 1.951.432 3.09.432 3.928 0 6.286-1.703 6.882-4.434zm-12.6 4.137l3.116-13.845h-4.227l-3.116 13.845h4.227zm4.281-17.928c.271-1.217-.596-2.217-1.87-2.217-1.3 0-2.573 1-2.844 2.217-.271 1.217.569 2.19 1.842 2.19 1.3 0 2.601-.973 2.872-2.19zm-12.49 10.924c-.57 2.57-1.925 4.03-3.686 4.03-.488 0-.948-.082-1.192-.19l1.653-7.382c.38-.162.921-.297 1.409-.297 1.707 0 2.357 1.406 1.815 3.84zm4.335-.378c.948-4.218-.976-6.652-4.58-6.652-.731 0-1.68.19-2.276.433l1.463-6.571H99.55l-4.39 19.469c1.03.54 2.9 1 4.959 1 4.714 0 7.884-2.488 9.05-7.68z"
/>
<path d="M69.853 24.2H97.03v17.334H69.853z" />
<path
fill="#424644"
d="M96.975 24.471H92.45l-3.522 7.571a24.423 24.423 0 0 0-.894 2.217h-.027s.081-1.19 0-2.217l-.57-7.57h-4.524l2.384 13.167-2.222 3.867h4.227l9.673-17.035zM80.61 36.963l1.544-7.922c.677-3.488-.867-4.84-4.444-4.84-1.03 0-2.113.108-3.034.297-.271 1.298-.813 2.38-1.653 3.218 1.002-.352 2.466-.703 3.658-.703 1.355 0 1.978.487 1.788 1.541l-.108.514c-4.038.216-7.75 1.027-8.427 4.515-.515 2.786 1.22 4.354 5.338 4.354 2.14 0 4.254-.433 5.338-.974zm-3.387-2.028c-.271.109-.976.19-1.518.19-1.436 0-2.222-.568-2.032-1.65.325-1.54 2.005-1.784 4.2-1.892l-.65 3.352z"
/>
<path d="M53.704 19.144H73.51v18.522H53.704z" />
<path
fill="#424644"
d="M60.749 27.364h1.87c1.354 0 2.438-.27 3.25-.757.813-.514 1.328-1.244 1.518-2.19.217-1.082.054-1.839-.488-2.271-.542-.46-1.571-.676-3.089-.676h-1.896l-1.165 5.894zm7.37-8.22c2.059 0 3.522.433 4.389 1.325.894.92 1.165 2.217.84 3.948-.352 1.676-1.138 3.001-2.412 3.894-1.273.946-2.899 1.406-4.93 1.406h-5.718l-.217 1.081-1.246 6.355-.027.027-.082.432H53.92c.055-.135.082-.297.109-.46l2.764-13.844c.135-.784.108-1.27-.136-1.54-.217-.244-.759-.38-1.626-.38h-1.327l.433-2.244H68.12z"
/>
<path d="M50.344 42.94h75.895v6.813H50.344z" />
<path
fill="#424644"
d="M53.27 48.023l1.3-5.084h-1.11l-.434 1.677a1.544 1.544 0 0 0-.433-.081c-1.111 0-1.897.73-2.168 1.892-.298 1.19.163 1.866 1.382 1.866.569 0 1.138-.108 1.463-.27zm-.948-.622c-.054.027-.217.027-.325.027-.461 0-.624-.352-.461-1.028.163-.622.488-1.027.975-1.027.109 0 .271.027.326.054l-.515 1.974zm4.01-3.894c.081-.324-.136-.568-.488-.568-.325 0-.677.244-.758.568-.082.325.135.568.487.568.325 0 .678-.243.759-.568zm-1.274 4.705l.922-3.623h-1.084l-.948 3.623h1.11zm3.902-.081l.867-3.38c-.27-.135-.731-.243-1.273-.243-1.22 0-2.087.622-2.412 1.838-.27 1.082.19 1.73 1.111 1.73.217 0 .488-.053.65-.107l-.027.162c-.108.433-.379.649-.84.649-.433 0-.785-.081-1.002-.19l-.515.839c.298.162.759.297 1.382.297 1.002 0 1.788-.54 2.06-1.595zm-.84-.946c-.108.027-.27.08-.406.08-.407 0-.596-.324-.434-.973.163-.649.515-.946.949-.946.135 0 .27 0 .352.027l-.46 1.812zm3.93-3.678c.08-.324-.136-.568-.489-.568-.325 0-.677.244-.758.568-.082.325.135.568.487.568.326 0 .678-.243.76-.568zm-1.274 4.705l.92-3.623h-1.083l-.948 3.623h1.11zm2.71-.054l.243-.92a.976.976 0 0 1-.298.055c-.217 0-.325-.081-.271-.325l.406-1.568h.624l.216-.811h-.65l.271-1.082-1.138.298-.217.784h-.406l-.217.81h.434l-.434 1.65c-.19.812 0 1.217.759 1.217.27 0 .542-.054.677-.108zm3.603-.135l.569-2.19c.244-.974-.163-1.325-1.138-1.325a4.81 4.81 0 0 0-1.517.243l.08.73c.299-.108.76-.216 1.085-.216.379 0 .569.135.487.432l-.054.136c-1.11.054-2.14.297-2.384 1.27-.19.785.27 1.19 1.409 1.19.569 0 1.165-.108 1.463-.27zm-.894-.541a1.471 1.471 0 0 1-.407.054c-.406 0-.596-.162-.542-.46.109-.432.597-.486 1.193-.513l-.244.919zm2.71.73l1.327-5.273h-1.11l-1.329 5.273h1.111zm11.217-.162l-.136-.811a1.04 1.04 0 0 1-.514.135c-.38 0-.596-.379-.434-.974.163-.649.515-1 .948-1 .19 0 .353.027.434.135l.542-.784c-.19-.162-.46-.243-.921-.243-.895 0-1.816.594-2.14 1.892-.326 1.271.27 1.893 1.164 1.893.46 0 .786-.081 1.057-.243zm3.522-3.515c-.135-.027-.298-.027-.514-.027-.65 0-1.193.08-1.653.243l-.867 3.461h1.11l.705-2.785c.108-.054.244-.054.38-.054.162 0 .297.027.406.054l.433-.892zm4.038.054h-1.247l-1.084 2.082c-.162.297-.298.622-.298.622s.027-.325.027-.622l-.027-2.082h-1.246l.433 3.623-.894 1.433h1.138l3.198-5.056zm3.251 1.811c.298-1.243-.162-1.892-1.382-1.892-.569 0-1.138.108-1.49.243l-1.246 4.894h1.11l.407-1.46c.081.027.27.054.433.054 1.111 0 1.87-.73 2.168-1.839zm-1.11 0c-.163.595-.516 1.028-.976 1.028a.886.886 0 0 1-.325-.054l.514-1.974a.886.886 0 0 1 .326-.054c.46 0 .623.351.46 1.054zm3.17 1.758l.243-.92a.976.976 0 0 1-.298.055c-.217 0-.325-.081-.27-.325l.406-1.568h.623l.217-.811h-.65l.297-1.082-1.165.298-.216.784h-.407l-.217.81h.434l-.434 1.65c-.19.812 0 1.217.759 1.217.27 0 .542-.054.677-.108zm4.253-1.758c.271-1.108-.135-1.892-1.219-1.892s-1.897.784-2.168 1.892c-.298 1.082.136 1.893 1.193 1.893 1.083 0 1.923-.811 2.194-1.893zm-1.11 0c-.163.65-.461 1-.84 1-.407 0-.515-.35-.326-1 .163-.703.461-1.054.84-1.054.38 0 .515.351.325 1.054zm5.419 1.65l-.136-.811a1.04 1.04 0 0 1-.515.135c-.379 0-.596-.379-.433-.974.162-.649.515-1 .921-1 .19 0 .352.027.46.135l.543-.784c-.217-.162-.461-.243-.949-.243-.867 0-1.815.594-2.14 1.892-.325 1.271.298 1.893 1.192 1.893.46 0 .759-.081 1.057-.243zm3.63-.027l.895-3.434h-1.111l-.705 2.758a1.701 1.701 0 0 1-.433.054c-.325 0-.434-.135-.38-.46l.624-2.352h-1.111l-.597 2.325c-.243.974.19 1.38 1.247 1.38.569 0 1.192-.109 1.571-.271zm3.713-3.488c-.109-.027-.299-.027-.515-.027-.623 0-1.192.08-1.626.243l-.894 3.461h1.11l.705-2.785c.109-.054.244-.054.407-.054.135 0 .298.027.379.054l.434-.892zm2.6 0c-.135-.027-.324-.027-.514-.027-.65 0-1.22.08-1.653.243l-.867 3.461h1.11l.705-2.785c.109-.054.244-.054.38-.054.162 0 .298.027.406.054l.434-.892zm3.144 1.65c.244-.974-.136-1.677-1.192-1.677-1.084 0-1.951.757-2.222 1.892-.271 1.082.135 1.893 1.382 1.893.542 0 1.138-.108 1.49-.27l-.108-.811a2.732 2.732 0 0 1-1.057.216c-.46 0-.759-.19-.732-.568l2.33-.297c.028-.136.055-.27.109-.379zm-1.03-.217l-1.3.162c.135-.595.514-.865.894-.865s.515.243.406.703zm4.254 2.244l.623-2.488c.217-.784-.162-1.216-1.3-1.216-.65 0-1.138.08-1.599.243l-.867 3.461h1.111l.705-2.785a1.89 1.89 0 0 1 .433-.054c.325 0 .46.162.38.46l-.597 2.379h1.111zm3.306-.162l-.136-.811a1.04 1.04 0 0 1-.514.135c-.38 0-.597-.379-.434-.974.163-.649.515-1 .921-1 .19 0 .353.027.46.135l.543-.784c-.217-.162-.46-.243-.948-.243-.868 0-1.816.594-2.141 1.892-.298 1.271.298 1.893 1.192 1.893.46 0 .759-.081 1.057-.243zm5.013-3.461h-1.247l-1.11 2.082c-.136.297-.272.622-.272.622s.027-.325.027-.622l-.027-2.082h-1.246l.433 3.623-.92 1.433h1.164l3.198-5.056zM76.03 45.67l.082-.838-1.897.325c-.19.027-.325.027-.433.027-.38 0-.57-.19-.488-.514.081-.298.352-.487.786-.487.27 0 .542.081.704.19l.542-.866c-.244-.135-.65-.243-1.165-.243-1.057 0-1.761.622-1.951 1.325-.108.432.027.838.434 1.027-.515.19-1.03.595-1.193 1.271-.216.92.38 1.406 1.626 1.406.677 0 1.274-.108 1.626-.27l.569-2.245.758-.108zm-2.22 1.73a2.435 2.435 0 0 1-.488.055c-.57 0-.813-.216-.705-.649.109-.406.434-.676.922-.757l.623-.081-.352 1.433z"
/>
<path d="M50.317 49.78h29.399v6.814h-29.4z" />
<path
fill="#424644"
d="M53.812 53.025c.244-.973-.135-1.676-1.192-1.676-1.084 0-1.924.757-2.222 1.892-.244 1.082.135 1.893 1.409 1.893.542 0 1.11-.108 1.49-.27l-.108-.811a2.766 2.766 0 0 1-1.084.216c-.434 0-.732-.19-.732-.568l2.33-.297c.055-.135.082-.27.109-.379zm-1.003-.216l-1.327.162c.162-.595.542-.865.921-.865.352 0 .515.243.406.703zm4.471 2.244l-.758-1.866 1.571-1.757h-1.246l-.813 1.027h-.027l-.271-1.027h-1.274l.705 1.757-1.707 1.866h1.246l.949-1.136.352 1.136h1.273zm2.791-.162l-.135-.811a1.04 1.04 0 0 1-.515.135c-.407 0-.596-.379-.434-.974.163-.648.515-1 .922-1 .19 0 .352.027.46.135l.542-.784c-.217-.162-.488-.243-.948-.243-.867 0-1.816.595-2.14 1.892-.326 1.271.297 1.893 1.191 1.893.461 0 .76-.08 1.057-.243zm3.63.162l.624-2.46c.19-.812-.162-1.244-1.03-1.244-.243 0-.514.08-.731.162l.433-1.73h-1.11l-1.355 5.272h1.11l.705-2.758a1.23 1.23 0 0 1 .434-.081c.352 0 .487.162.406.487l-.596 2.352h1.11zm3.74-.19l.57-2.19c.243-.973-.163-1.324-1.166-1.324-.542 0-1.138.108-1.49.243l.081.73c.298-.108.732-.216 1.084-.216.38 0 .542.135.46.432l-.027.136c-1.11.054-2.167.297-2.411 1.27-.19.758.244 1.19 1.436 1.19.569 0 1.165-.108 1.463-.27zm-.894-.54a1.643 1.643 0 0 1-.406.054c-.407 0-.624-.162-.542-.46.108-.432.569-.486 1.192-.513l-.244.92zm4.715.73l.65-2.488c.19-.784-.19-1.216-1.328-1.216-.65 0-1.138.08-1.571.243l-.894 3.461h1.11l.705-2.785a1.89 1.89 0 0 1 .433-.054c.326 0 .461.162.38.46l-.596 2.38h1.11zm3.901-.081l.84-3.38c-.27-.135-.704-.243-1.273-.243-1.192 0-2.086.622-2.385 1.838-.27 1.082.19 1.73 1.111 1.73.217 0 .461-.053.65-.107l-.054.162c-.108.433-.379.649-.84.649-.406 0-.758-.081-.975-.19l-.542.839c.325.162.759.297 1.41.297 1.002 0 1.787-.54 2.058-1.595zm-.867-.946a1.324 1.324 0 0 1-.379.08c-.434 0-.623-.324-.46-.973.162-.649.541-.946.948-.946.162 0 .27 0 .352.027l-.46 1.812zm5.311-1c.244-.974-.135-1.677-1.192-1.677-1.084 0-1.924.757-2.222 1.892-.27 1.082.136 1.893 1.409 1.893.542 0 1.111-.108 1.49-.27l-.108-.811a2.87 2.87 0 0 1-1.084.216c-.46 0-.731-.19-.731-.568l2.33-.297c.054-.135.081-.27.108-.379zm-1.002-.217l-1.328.162c.162-.595.515-.865.921-.865.352 0 .515.243.407.703z"
/>
</g>
</Fragment>
)
export default ({ width, height }: { width: number, height: number }) => (
<svg width={width} height={height}>
{inner}
</svg>
)

27
src/components/ExchangePage/logos/simplex.js

@ -0,0 +1,27 @@
// @flow
import React, { Fragment } from 'react'
const inner = (
<Fragment>
<path
fill="#75787B"
d="M58.227 22.684c.971.451 1.969.558 3.587.584 4.234.08 5.771 1.087 5.771 3.528a3.62 3.62 0 0 1-.377 1.644c-.243.477-.756.981-1.484 1.485-.728.504-1.995.743-3.802.743-2.562 0-4.45-.61-5.718-1.857a.89.89 0 0 1-.242-.53c.027-.451.242-.664.647-.664.216 0 .378.054.512.187.944 1.034 2.644 1.617 4.72 1.617 2.805 0 4.369-.955 4.369-2.652.027-.77-.216-1.273-.728-1.538-.998-.53-2.077-.664-3.803-.69-4.1-.08-5.501-.981-5.501-3.236 0-2.387 2.184-3.899 5.555-3.899 2.077 0 3.776.583 5.043 1.671a.69.69 0 0 1 .216.53c-.027.398-.243.637-.62.664-.216 0-.405-.053-.54-.186-.97-.928-2.4-1.406-4.018-1.406-.647 0-1.24.053-1.753.186a6.21 6.21 0 0 0-1.645.716c-.566.345-.836.902-.863 1.724-.027.69.216 1.14.674 1.38m39.721.397v6.684c0 .478-.216.716-.674.743-.459-.027-.702-.265-.702-.743v-6.551c0-2.97-1.267-4.536-3.721-4.536-2.481 0-4.073 1.936-4.073 5.04v6.047c0 .478-.215.716-.674.743-.458-.027-.701-.265-.701-.743v-6.551c0-2.97-1.267-4.536-3.721-4.536-2.508 0-4.073 1.936-4.073 5.04v6.047c0 .478-.242.716-.7.743-.432-.027-.648-.265-.675-.743V18.334c.027-.504.243-.742.674-.769.459.027.701.265.701.77v1.379h.054c.863-1.486 2.347-2.308 4.261-2.308 2.158 0 3.614.955 4.342 2.732h.054c.863-1.698 2.427-2.732 4.72-2.732 3.263 0 4.908 1.99 4.908 5.676m17.036.955c0-3.183-2.13-5.358-5.312-5.358-3.156 0-5.367 2.175-5.367 5.358s2.211 5.358 5.367 5.358c3.182 0 5.312-2.175 5.312-5.358m-10.68-5.702v1.83h.055c.27-.451.593-.823.944-1.167.377-.345.917-.69 1.618-1.035.7-.371 1.672-.557 2.94-.557 1.941 0 3.56.663 4.719 1.83 1.726 1.91 1.726 3.448 1.807 4.801 0 .982-.135 1.777-.594 3.024-.701 1.83-3.074 3.713-5.933 3.607-1.267 0-2.238-.186-2.94-.53-1.402-.743-2.022-1.353-2.561-2.228h-.054v6.79c0 .477-.216.716-.675.742-.458-.026-.7-.265-.7-.742V18.335c0-.504.242-.743.7-.77.46.027.675.266.675.77m17.605-5.968v17.399c0 .477-.242.716-.7.743-.432-.027-.648-.266-.675-.743v-17.4c.027-.477.243-.715.674-.742.46.027.702.265.702.743m5.464 10.901h10.598c-.35-2.758-2.373-4.589-5.286-4.589-2.966 0-4.962 1.83-5.312 4.589m11.084 1.247h-11.111c.215 2.917 2.265 4.88 5.475 4.88 1.914 0 3.397-.77 4.368-1.963a.62.62 0 0 1 .513-.265c.458.026.7.265.7.716a.612.612 0 0 1-.188.45c-1.24 1.433-3.047 2.335-5.393 2.335-2.077 0-3.803-.664-5.017-1.83-1.78-1.91-1.834-3.448-1.887-4.8 0-.982.107-1.805.62-2.998.755-1.857 3.155-3.767 6.148-3.634 3.857 0 6.608 2.626 6.608 6.313-.027.503-.324.769-.836.796m15.116 4.747a.773.773 0 0 1 .243.584c-.027.424-.27.636-.728.663-.216 0-.405-.106-.567-.292l-4.88-5.358-4.936 5.384a.633.633 0 0 1-.54.266c-.43-.027-.647-.266-.674-.69a.63.63 0 0 1 .19-.477l5.015-5.358-4.827-5.172c-.162-.186-.243-.372-.243-.584.027-.424.27-.636.728-.663.216 0 .405.107.567.292l4.746 5.252 4.773-5.279a.706.706 0 0 1 .54-.265c.431.027.647.265.674.69a.634.634 0 0 1-.189.477l-4.854 5.226 4.962 5.304z"
/>
<path
fill="#43B02A"
d="M21.135 8.867l5.553-4.355-12.48 2.8 6.927 1.555zm4.32.971l2.097-4.281L22.84 9.25l2.613.587zm3.325.747l.001-4.438-1.958 3.998 1.957.44zm3.978.893l-2.609-5.326v4.74l2.609.586zm-15.084.103l2.092-1.64-5.862-1.318 3.77 2.958zm23.72 1.838l-10.016-7.86 3.086 6.303 6.93 1.557zm-26.387.254l1.57-1.231-3.527-2.767 1.957 3.998zm7.67 1.834l2.168-4.426-3.373-.757-2.7 2.118 3.906 3.065zm13.708.28l4.705-1.056-5.867-1.318 1.162 2.373zm-24.568.388l2.092-1.641-2.091-4.274-.001 5.915zm18.33 1.01l4.87-1.092-1.5-3.063-3.369-.757v4.912zm-4.144.93l2.776-.623v-5.526l-2.565-.577-2.44 4.978 2.23 1.748zm11.863.694l3.529-2.766-4.4.987.87 1.78zm-15.328.083l1.759-.394-1.131-.888-.628 1.282zm-4.485 1.006l2.779-.623 1.237-2.525-4.395-3.448-2.058 1.615 2.437 4.981zm10.725.394v-1.42l-1.407.316 1.407 1.104zm-16.962 1.005l4.869-1.092-2.167-4.427-2.702 2.118v3.401zm-6.925.31l5.557-4.36.001-6.988-5.558 11.347zm14.398.817l.783-1.599-1.41.317.627 1.282zm20.347.001l2.611-5.33-3.772 2.958 1.16 2.372zm-33.893.234l4.704-1.055V18.97l-4.704 3.69zm26.632.455l4.394-3.444-1.143-2.333-5.48 1.229v2.8l2.23 1.748zm-2.23 1.748l1.133-.887-1.132-.889v1.776zm-19.699.097l.001-1.975-4.399.987 4.398.988zm6.848 1.538l1.235-2.522-1.236-2.526-5.477 1.229-.001 2.588 5.479 1.23zm20.083.54l1.5-3.062-1.5-3.061-3.904 3.061 3.904 3.062zm-17.308.083l-.78-1.594-.627 1.278 1.407.316zm5.594 1.257l3.114-2.441v-3.923l-3.112-2.442-3.887.872-1.73 3.532 1.727 3.529 3.888.873zm-15.217.597V26.34l-4.704-1.057 4.704 3.691zm18.33.102v-1.419L27.37 28.76l1.406.316zm14.702 1.195l.002-12.592-3.085 6.297 3.083 6.295zm-20.316.067l1.133-.888-1.761-.396.628 1.284zm12.462.276l1.144-2.333-4.393-3.446-2.23 1.75v2.798l5.48 1.231zm6.623.244l-2.611-5.332-1.162 2.372 3.773 2.96zm-.853 1.052l-3.53-2.769-.871 1.78 4.4.99zm-26.878.258l2.168-4.427-4.87-1.094v3.4l2.702 2.121zm20.704 2.37l5.87-1.316-4.707-1.057-1.163 2.374zm-17.55.106l4.394-3.445-1.237-2.528-2.774-.623-2.44 4.981 2.057 1.615zm12.473 1.034l3.371-.756 1.5-3.064-4.87-1.094-.001 4.914zm-3.936.883l2.567-.576.002-5.528L26 29.833l-2.23 1.749 2.436 4.979zm-4.739 1.063l3.37-.756-2.165-4.426-3.906 3.063 2.701 2.119zm-11.023.061v-6.989l-5.555-4.36 5.555 11.35zm1.368 0l2.094-4.273-2.093-1.643-.001 5.917zm1.232.585l3.528-2.766-1.57-1.231-1.958 3.997zm.854 1.052l5.863-1.315-2.092-1.642-3.771 2.957zm14.874 2.483l.001-4.44-1.958.44 1.957 4zm1.369 0l2.612-5.333-2.611.586-.001 4.748zm-2.602.583l-2.093-4.276-2.61.585 4.703 3.691zm3.834.001l10.02-7.855-6.933 1.555-3.087 6.3zm-4.691 1.049L21.13 39.08l-6.928 1.554 12.48 2.804zm2.775 4.238a2.98 2.98 0 0 1-2.325-1.101 2.896 2.896 0 0 1-.646-1.801L13.788 41.92a2.97 2.97 0 0 1-2.662 1.63c-.91 0-1.757-.4-2.324-1.1a2.888 2.888 0 0 1 .454-4.092L3.608 26.82a3.02 3.02 0 0 1-.639.068c-.91 0-1.757-.4-2.322-1.098a2.893 2.893 0 0 1 .47-4.106 2.964 2.964 0 0 1 1.846-.635c.218 0 .435.023.648.07l5.653-11.54a2.867 2.867 0 0 1-1.082-2.595 2.89 2.89 0 0 1 1.1-1.958 2.97 2.97 0 0 1 1.85-.637c.91 0 1.758.4 2.325 1.1.13.16.244.34.34.534l12.697-2.848A2.88 2.88 0 0 1 27.61.913a2.97 2.97 0 0 1 1.85-.638c.91 0 1.758.401 2.325 1.101.705.87.843 2.063.363 3.066l10.185 7.992a2.97 2.97 0 0 1 1.831-.623c.911 0 1.758.401 2.325 1.1a2.893 2.893 0 0 1-.47 4.107 2.956 2.956 0 0 1-1.17.557l-.003 12.804c.647.15 1.227.51 1.64 1.022a2.891 2.891 0 0 1-.471 4.106 2.968 2.968 0 0 1-1.849.637 3.01 3.01 0 0 1-1.837-.623l-10.185 7.986a2.897 2.897 0 0 1-.835 3.531 2.97 2.97 0 0 1-1.85.638z"
mask="url(#b)"
/>
<path
fill="#43B02A"
d="M75.563 14.695c0-1.565-1.29-2.833-2.881-2.833-1.591 0-2.88 1.268-2.88 2.833 0 1.33.934 2.444 2.192 2.748v12.323c.027.477.243.716.675.743.458-.027.7-.266.7-.743V17.443c1.259-.304 2.194-1.417 2.194-2.748"
/>
</Fragment>
)
export default ({ width, height }: { width: number, height: number }) => (
<svg width={width} height={height}>
{inner}
</svg>
)

38
src/components/ExportLogsBtn.js

@ -6,15 +6,25 @@ import { webFrame, remote } from 'electron'
import React, { Component } from 'react'
import { translate } from 'react-i18next'
import KeyHandler from 'react-key-handler'
import { getCurrentLogFile } from 'helpers/resolveLogsDirectory'
import Button from './base/Button'
function writeToFile(file, data) {
return new Promise((resolve, reject) => {
fs.writeFile(file, data, error => {
if (error) {
reject(error)
} else {
resolve()
}
})
})
}
class ExportLogsBtn extends Component<{
t: *,
hookToShortcut?: boolean,
}> {
handleExportLogs = () => {
const srcLogFile = getCurrentLogFile()
export = async () => {
const resourceUsage = webFrame.getResourceUsage()
logger.log('exportLogsMeta', {
resourceUsage,
@ -23,23 +33,39 @@ class ExportLogsBtn extends Component<{
environment: __DEV__ ? 'development' : 'production',
userAgent: window.navigator.userAgent,
})
const date = new Date() // we don't want all the logs that happen after the Export was pressed ^^
const path = remote.dialog.showSaveDialog({
title: 'Export logs',
defaultPath: `ledgerlive-export-${moment().format(
'YYYY.MM.DD-HH.mm.ss',
)}-${__GIT_REVISION__ || 'unversionned'}.log`,
)}-${__GIT_REVISION__ || 'unversionned'}.json`,
filters: [
{
name: 'All Files',
extensions: ['log'],
extensions: ['json'],
},
],
})
if (path) {
fs.createReadStream(srcLogFile).pipe(fs.createWriteStream(path))
const logs = await logger.queryAllLogs(date)
const json = JSON.stringify(logs)
await writeToFile(path, json)
}
}
exporting = false
handleExportLogs = () => {
if (this.exporting) return
this.exporting = true
this.export()
.catch(e => {
logger.critical(e)
})
.then(() => {
this.exporting = false
})
}
onKeyHandle = e => {
if (e.ctrlKey) {
this.handleExportLogs()

2
src/components/FeesField/GenericContainer.js

@ -6,7 +6,7 @@ import Box from 'components/base/Box'
import LabelWithExternalIcon from 'components/base/LabelWithExternalIcon'
import { translate } from 'react-i18next'
import { openURL } from 'helpers/linking'
import { urls } from 'config/support'
import { urls } from 'config/urls'
import { track } from 'analytics/segment'
export default translate()(({ children, t }: { children: React$Node, t: * }) => (

3
src/components/GenuineCheck.js

@ -115,7 +115,7 @@ class GenuineCheck extends PureComponent<Props> {
}
render() {
const { onSuccess, ...props } = this.props
const { onSuccess, device, ...props } = this.props
const steps = [
{
id: 'device',
@ -156,6 +156,7 @@ class GenuineCheck extends PureComponent<Props> {
return (
<DeviceInteraction
key={device ? device.path : null}
{...props}
waitBeforeSuccess={500}
steps={steps}

1
src/components/ManagerPage/ManagerApp.js

@ -30,6 +30,7 @@ const AppIcon = styled.img`
display: block;
width: 36px;
height: 36px;
pointer-events: none;
`
const AppName = styled(Box).attrs({

2
src/components/ManagerPage/index.js

@ -3,7 +3,7 @@
import React, { PureComponent, Fragment } from 'react'
import invariant from 'invariant'
import { openURL } from 'helpers/linking'
import { urls } from 'config/support'
import { urls } from 'config/urls'
import type { Device } from 'types/common'
import type { DeviceInfo } from 'helpers/devices/getDeviceInfo'

21
src/components/Onboarding/OnboardingBreadcrumb.js

@ -20,18 +20,31 @@ type Props = {
function OnboardingBreadcrumb(props: Props) {
const { onboarding, t } = props
const { stepName, genuine } = onboarding
const { stepName, genuine, onboardingRelaunched } = onboarding
const isInitializedFlow = onboarding.flowType === 'initializedDevice'
const regularFilteredSteps = onboarding.steps
const regularSteps = onboarding.steps
.filter(step => !step.external)
.map(step => ({ ...step, label: t(step.label) }))
const alreadyInitializedSteps = onboarding.steps
.filter(step => !step.external && step.name !== 'writeSeed' && step.name !== 'selectPIN')
.filter(step => !step.external && !step.options.alreadyInitSkip)
.map(step => ({ ...step, label: t(step.label) }))
const filteredSteps = isInitializedFlow ? alreadyInitializedSteps : regularFilteredSteps
const onboardingRelaunchedSteps = onboarding.steps
.filter(
step =>
isInitializedFlow
? !step.options.alreadyInitSkip && !step.external && !step.options.relaunchSkip
: !step.external && !step.options.relaunchSkip,
)
.map(step => ({ ...step, label: t(step.label) }))
const filteredSteps = onboardingRelaunched
? onboardingRelaunchedSteps
: isInitializedFlow
? alreadyInitializedSteps
: regularSteps
const stepIndex = findIndex(filteredSteps, s => s.name === stepName)
const genuineStepIndex = findIndex(filteredSteps, s => s.name === 'genuineCheck')

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

@ -4,7 +4,7 @@ import React, { Component } from 'react'
import { openURL } from 'helpers/linking'
import styled from 'styled-components'
import { i } from 'helpers/staticPath'
import { urls } from 'config/support'
import { urls } from 'config/urls'
import Box from 'components/base/Box'
import Button from 'components/base/Button'

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

@ -5,7 +5,7 @@ import { openURL } from 'helpers/linking'
import { connect } from 'react-redux'
import styled from 'styled-components'
import { colors } from 'styles/theme'
import { urls } from 'config/support'
import { urls } from 'config/urls'
import { updateGenuineCheck } from 'reducers/onboarding'
@ -61,12 +61,12 @@ class GenuineCheck extends PureComponent<StepProps, State> {
const { t } = this.props
return [
{
label: t('app:common.yes'),
label: t('app:common.labelYes'),
key: 'yes',
pass: true,
},
{
label: t('app:common.no'),
label: t('app:common.labelNo'),
key: 'no',
pass: false,
},
@ -144,14 +144,17 @@ class GenuineCheck extends PureComponent<StepProps, State> {
}
contactSupport = () => {
openURL(urls.genuineCheckContactSupport)
openURL(urls.contactSupport)
}
handlePrevStep = () => {
const { prevStep, onboarding, jumpStep } = this.props
onboarding.flowType === 'initializedDevice' ? jumpStep('selectDevice') : prevStep()
}
handleNextStep = () => {
const { onboarding, jumpStep, nextStep } = this.props
onboarding.onboardingRelaunched ? jumpStep('finish') : nextStep()
}
renderGenuineFail = () => (
<GenuineCheckErrorPage
redoGenuineCheck={this.redoGenuineCheck}
@ -280,7 +283,7 @@ class GenuineCheck extends PureComponent<StepProps, State> {
) : (
<OnboardingFooter
t={t}
nextStep={nextStep}
nextStep={this.handleNextStep}
prevStep={this.handlePrevStep}
isContinueDisabled={!genuine.isDeviceGenuine}
/>

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

@ -7,7 +7,7 @@ import { i } from 'helpers/staticPath'
import Box from 'components/base/Box'
import GrowScroll from 'components/base/GrowScroll'
import TrackPage from 'analytics/TrackPage'
import { urls } from 'config/support'
import { urls } from 'config/urls'
import IconCart from 'icons/Cart'
import IconTruck from 'icons/Truck'
import IconInfoCircle from 'icons/InfoCircle'

30
src/components/OpenUserDataDirectoryBtn.js

@ -0,0 +1,30 @@
// @flow
import React, { Component } from 'react'
import logger from 'logger'
import { shell } from 'electron'
import { translate } from 'react-i18next'
import resolveUserDataDirectory from 'helpers/resolveUserDataDirectory'
import Button from 'components/base/Button'
class OpenUserDataDirectoryBtn extends Component<{
t: *,
}> {
handleOpenUserDataDirectory = async () => {
const userDataDirectory = resolveUserDataDirectory()
logger.log(`Opening user data directory: ${userDataDirectory}`)
shell.openItem(userDataDirectory)
}
render() {
const { t } = this.props
return (
<Button primary small onClick={this.handleOpenUserDataDirectory}>
{t('app:settings.openUserDataDirectory.btn')}
</Button>
)
}
}
export default translate()(OpenUserDataDirectoryBtn)

30
src/components/RenderError.js

@ -4,9 +4,9 @@ import React, { PureComponent } from 'react'
import styled from 'styled-components'
import { openURL } from 'helpers/linking'
import { remote } from 'electron'
import qs from 'querystring'
import { translate } from 'react-i18next'
import { urls } from 'config/urls'
import { i } from 'helpers/staticPath'
import hardReset from 'helpers/hardReset'
@ -47,21 +47,12 @@ class RenderError extends PureComponent<
</IconWrapperCircle>
)
handleCreateIssue = () => {
const { error } = this.props
if (!error) {
return
}
const q = qs.stringify({
title: `Error: ${error.message}`,
body: `Error was thrown:
\`\`\`
${error.stack}
\`\`\`
`,
})
openURL(`https://github.com/LedgerHQ/ledger-live-desktop/issues/new?${q}`)
github = () => {
openURL(urls.githubIssues)
}
contact = () => {
openURL(urls.contactSupport)
}
handleRestart = () => {
@ -105,8 +96,11 @@ ${error.stack}
{t('app:crash.restart')}
</Button>
<ExportLogsBtn withoutAppData={withoutAppData} />
<Button small primary onClick={this.handleCreateIssue}>
{t('app:crash.createTicket')}
<Button small primary onClick={this.contact}>
{t('app:crash.support')}
</Button>
<Button small primary onClick={this.github}>
{t('app:crash.github')}
</Button>
<Button small danger onClick={this.handleOpenHardResetModal}>
{t('app:crash.reset')}

16
src/components/SelectExchange.js

@ -106,13 +106,15 @@ class SelectExchange extends Component<
</Text>
) : (
<Fragment>
<Track
onUpdate
event="SelectExchange"
exchangeName={value && value.id}
fromCurrency={from.ticker}
toCurrency={to.ticker}
/>
{exchanges ? (
<Track
onUpdate
event="SelectExchange"
exchangeName={value && value.id}
fromCurrency={from.ticker}
toCurrency={to.ticker}
/>
) : null}
<Select
value={value}
options={options}

1
src/components/SettingsPage/DisablePasswordModal.js

@ -84,7 +84,6 @@ class DisablePasswordModal extends PureComponent<Props, State> {
<InputPassword
autoFocus
type="password"
placeholder={t('app:password.inputFields.currentPassword.placeholder')}
id="password"
onChange={this.handleInputChange('currentPassword')}
value={currentPassword}

50
src/components/SettingsPage/LaunchOnboardingBtn.js

@ -0,0 +1,50 @@
// @flow
import React, { Fragment, PureComponent } from 'react'
import { connect } from 'react-redux'
import { saveSettings } from 'actions/settings'
import { translate } from 'react-i18next'
import type { T } from 'types/common'
import type { SettingsState } from 'reducers/settings'
import type { OnboardingState } from 'reducers/onboarding'
import Track from 'analytics/Track'
import Onboarding from 'components/Onboarding'
import Button from 'components/base/Button/index'
import { relaunchOnboarding } from 'reducers/onboarding'
const mapDispatchToProps = {
saveSettings,
relaunchOnboarding,
}
type Props = {
saveSettings: ($Shape<SettingsState>) => void,
relaunchOnboarding: ($Shape<OnboardingState>) => void,
t: T,
}
class LaunchOnboardingBtn extends PureComponent<Props> {
handleLaunchOnboarding = () => {
this.props.saveSettings({ hasCompletedOnboarding: false })
this.props.relaunchOnboarding({ onboardingRelaunched: true })
return <Onboarding />
}
render() {
const { t } = this.props
return (
<Fragment>
<Track onUpdate event={'Launch Onboarding from Settings'} />
<Button primary small onClick={this.handleLaunchOnboarding}>
{t('app:common.launch')}
</Button>
</Fragment>
)
}
}
export default translate()(
connect(
null,
mapDispatchToProps,
)(LaunchOnboardingBtn),
)

3
src/components/SettingsPage/PasswordForm.js

@ -44,7 +44,6 @@ class PasswordForm extends PureComponent<Props> {
</Label>
<InputPassword
autoFocus
placeholder={t('app:password.inputFields.currentPassword.placeholder')}
id="currentPassword"
onChange={onChange('currentPassword')}
value={currentPassword}
@ -57,7 +56,6 @@ class PasswordForm extends PureComponent<Props> {
<InputPassword
style={{ mt: 4, width: 240 }}
autoFocus={!isPasswordEnabled}
placeholder={t('app:password.inputFields.newPassword.placeholder')}
id="newPassword"
onChange={onChange('newPassword')}
value={newPassword}
@ -69,7 +67,6 @@ class PasswordForm extends PureComponent<Props> {
</Label>
<InputPassword
style={{ width: 240 }}
placeholder={t('app:password.inputFields.confirmPassword.placeholder')}
id="confirmPassword"
onChange={onChange('confirmPassword')}
value={confirmPassword}

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

@ -6,7 +6,7 @@ import { translate } from 'react-i18next'
import type { T } from 'types/common'
import TrackPage from 'analytics/TrackPage'
import { urls } from 'config/support'
import { urls } from 'config/urls'
import IconLoader from 'icons/Loader'
import ReleaseNotesButton from '../ReleaseNotesButton'

39
src/components/SettingsPage/sections/Help.js

@ -6,12 +6,14 @@ import type { T } from 'types/common'
import TrackPage from 'analytics/TrackPage'
import IconHelp from 'icons/Help'
import resolveLogsDirectory from 'helpers/resolveLogsDirectory'
import { urls } from 'config/support'
import { urls } from 'config/urls'
import ExportLogsBtn from 'components/ExportLogsBtn'
import OpenUserDataDirectoryBtn from 'components/OpenUserDataDirectoryBtn'
import CleanButton from '../CleanButton'
import ResetButton from '../ResetButton'
import AboutRowItem from '../AboutRowItem'
import LaunchOnboardingBtn from '../LaunchOnboardingBtn'
import {
SettingsSection as Section,
@ -39,30 +41,41 @@ class SectionHelp extends PureComponent<Props> {
/>
<Body>
<AboutRowItem
title={t('app:settings.help.faq')}
desc={t('app:settings.help.faqDesc')}
url={urls.faq}
/>
<Row
title={t('app:settings.profile.softResetTitle')}
desc={t('app:settings.profile.softResetDesc')}
>
<CleanButton />
</Row>
<Row
title={t('app:settings.profile.hardResetTitle')}
desc={t('app:settings.profile.hardResetDesc')}
>
<ResetButton />
</Row>
<Row
title={t('app:settings.exportLogs.title')}
desc={t('app:settings.exportLogs.desc', { logsDirectory: resolveLogsDirectory() })}
>
<ExportLogsBtn />
</Row>
<AboutRowItem
title={t('app:settings.help.faq')}
desc={t('app:settings.help.faqDesc')}
url={urls.faq}
/>
<Row
title={t('app:settings.profile.launchOnboarding')}
desc={t('app:settings.profile.launchOnboardingDesc')}
>
<LaunchOnboardingBtn />
</Row>
<Row
title={t('app:settings.openUserDataDirectory.title')}
desc={t('app:settings.openUserDataDirectory.desc')}
>
<OpenUserDataDirectoryBtn />
</Row>
<Row
title={t('app:settings.profile.hardResetTitle')}
desc={t('app:settings.profile.hardResetDesc')}
>
<ResetButton />
</Row>
</Body>
</Section>
)

36
src/components/SyncBackground.js

@ -0,0 +1,36 @@
// @flow
import React, { PureComponent } from 'react'
import { BridgeSyncConsumer } from 'bridge/BridgeSyncContext'
import type { Sync } from 'bridge/BridgeSyncContext'
import { SYNC_BOOT_DELAY, SYNC_ALL_INTERVAL } from 'config/constants'
export class Effect extends PureComponent<{
sync: Sync,
}> {
componentDidMount() {
const syncLoop = async () => {
const { sync } = this.props
sync({ type: 'BACKGROUND_TICK' })
this.syncTimeout = setTimeout(syncLoop, SYNC_ALL_INTERVAL)
}
this.syncTimeout = setTimeout(syncLoop, SYNC_BOOT_DELAY)
}
componentWillUnmount() {
clearTimeout(this.syncTimeout)
}
syncTimeout: *
render() {
return null
}
}
const SyncBackground = () => (
<BridgeSyncConsumer>{sync => <Effect sync={sync} />}</BridgeSyncConsumer>
)
export default SyncBackground

22
src/components/TranslatedError.js

@ -22,14 +22,24 @@ class TranslatedError extends PureComponent<Props> {
render() {
const { t, error, field } = this.props
if (!error) return null
if (typeof error === 'string') return error
if (typeof error !== 'object') {
// this case should not happen (it is supposed to be a ?Error)
logger.critical(`TranslatedError invalid usage: ${String(error)}`)
if (typeof error === 'string') {
return error // TMP in case still used somewhere
}
return null
}
// $FlowFixMe
const arg: Object = Object.assign({ message: error.message }, error)
if (error.name) {
const translation = t(`errors:${error.name}.${field}`, error)
// FIXME in case the error don't exist in t we should not return and fallback code after. I just don't know how to check this. FIXME
return translation
const translation = t(`errors:${error.name}.${field}`, arg)
if (translation !== `${error.name}.${field}`) {
// It is translated
return translation
}
}
logger.warn(`TranslatedError: no translation for '${error.name}'`, error)
return error.message || error.name || t(`errors:generic.${field}`)
return t(`errors:generic.${field}`, arg)
}
}

4
src/components/base/Chart/Tooltip.js

@ -69,7 +69,9 @@ const Tooltip = ({
/>
)}
<Box ff="Open Sans|Regular" color="grey" fontSize={3} mt={2}>
{moment(item.date).format('LL')}
{moment(item.date)
.add(1, 'second')
.format('LL')}
</Box>
</Fragment>
)}

2
src/components/base/Chart/handleMouseEvents.js

@ -96,7 +96,7 @@ export default function handleMouseEvents({
unit={props.unit}
renderTooltip={renderTooltip}
item={d.ref}
counterValue={getFiatCurrencyByTicker('USD')}
counterValue={getFiatCurrencyByTicker(props.cvCode || 'USD')}
/>
</ThemeProvider>
</Provider>,

1
src/components/base/Chart/index.js

@ -48,6 +48,7 @@ import type { Data } from './types'
export type Props = {
data: Data, // eslint-disable-line react/no-unused-prop-types
unit?: ?Unit, // eslint-disable-line react/no-unused-prop-types
cvCode?: string, // eslint-disable-line react/no-unused-prop-types
id?: string, // eslint-disable-line react/no-unused-prop-types
height?: number,
tickXScale: string, // eslint-disable-line react/no-unused-prop-types

2
src/components/base/Input/index.js

@ -87,7 +87,7 @@ type Props = {
renderLeft?: any,
renderRight?: any,
containerProps?: Object,
error?: string | boolean,
error?: ?Error | boolean,
small?: boolean,
editInPlace?: boolean,
}

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

@ -47,7 +47,6 @@ type State = {
}
type Props = {
maxLength: number,
onChange: Function,
t: T,
value: string,
@ -58,7 +57,6 @@ class InputPassword extends PureComponent<Props, State> {
static defaultProps = {
onChange: noop,
value: '',
maxLength: 20,
}
state = {
@ -86,7 +84,7 @@ class InputPassword extends PureComponent<Props, State> {
}
render() {
const { t, value, maxLength, withStrength } = this.props
const { t, value, withStrength } = this.props
const { passwordStrength, inputType } = this.state
const hasValue = value.trim() !== ''
@ -96,7 +94,6 @@ class InputPassword extends PureComponent<Props, State> {
<Input
{...this.props}
type={inputType}
maxLength={maxLength}
onChange={this.handleChange}
renderRight={
<InputRight onClick={this.toggleInputType}>

2
src/components/layout/Default.js

@ -31,6 +31,7 @@ import AppRegionDrag from 'components/AppRegionDrag'
import IsUnlocked from 'components/IsUnlocked'
import SideBar from 'components/MainSideBar'
import TopBar from 'components/TopBar'
import SyncBackground from 'components/SyncBackground'
import SyncContinuouslyPendingOperations from '../SyncContinouslyPendingOperations'
const Main = styled(GrowScroll).attrs({
@ -94,6 +95,7 @@ class Default extends Component<Props> {
))}
<SyncContinuouslyPendingOperations priority={20} interval={SYNC_PENDING_INTERVAL} />
<SyncBackground />
<div id="sticky-back-to-top-root" />

6
src/components/modals/AccountSettingRenderBody.js

@ -214,7 +214,7 @@ class HelperComp extends PureComponent<Props, State> {
maxLength={MAX_ACCOUNT_NAME_SIZE}
onChange={this.handleChangeName}
onFocus={e => this.handleFocus(e, 'accountName')}
error={accountNameError && t('app:account.settings.accountName.error')}
error={accountNameError && new Error(t('app:account.settings.accountName.error'))}
/>
</Box>
</Container>
@ -251,7 +251,9 @@ class HelperComp extends PureComponent<Props, State> {
onChange={this.handleChangeEndpointConfig}
onFocus={e => this.handleFocus(e, 'endpointConfig')}
error={
endpointConfigError ? t('app:account.settings.endpointConfig.error') : false
endpointConfigError
? new Error(t('app:account.settings.endpointConfig.error'))
: null
}
/>
</Box>

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

@ -1,5 +1,6 @@
// @flow
import logger from 'logger'
import invariant from 'invariant'
import styled from 'styled-components'
import { Trans } from 'react-i18next'
@ -122,7 +123,10 @@ class StepImport extends PureComponent<StepProps> {
}
},
complete: () => setScanStatus('finished'),
error: err => setScanStatus('error', err),
error: err => {
logger.critical(err)
setScanStatus('error', err)
},
})
} catch (err) {
setScanStatus('error', err)
@ -160,7 +164,7 @@ class StepImport extends PureComponent<StepProps> {
}
renderError() {
const { err, t } = this.props
const { err } = this.props
invariant(err, 'Trying to render inexisting error')
return (
<Box
@ -172,9 +176,11 @@ class StepImport extends PureComponent<StepProps> {
color="alertRed"
>
<IconExclamationCircleThin size={43} />
<Box mt={4}>{t('app:addAccounts.somethingWentWrong')}</Box>
<Box mt={4}>
<TranslatedError error={err} />
<TranslatedError error={err} field="title" />
</Box>
<Box mt={4}>
<TranslatedError error={err} field="description" />
</Box>
</Box>
)

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

@ -13,7 +13,7 @@ import type { Account } from '@ledgerhq/live-common/lib/types'
import { MODAL_RECEIVE } from 'config/constants'
import { openURL } from 'helpers/linking'
import { urls } from 'config/support'
import { urls } from 'config/urls'
import type { T, Device } from 'types/common'
import type { StepProps as DefaultStepProps } from 'components/base/Stepper'
@ -133,7 +133,7 @@ class ReceiveModal extends PureComponent<Props, State> {
isAppOpened: false,
})
handleContactUs = () => {
openURL(urls.receiveFlowContactSupport)
openURL(urls.contactSupport)
}
handleReset = () => this.setState({ ...INITIAL_STATE })

47
src/components/modals/ReleaseNotes/ReleaseNotesBody.js

@ -2,7 +2,7 @@
import React, { PureComponent } from 'react'
import { translate } from 'react-i18next'
import styled from 'styled-components'
import semver from 'semver'
import network from 'api/network'
import Button from 'components/base/Button'
@ -24,7 +24,8 @@ type Props = {
}
type State = {
markdown: ?string,
notes: *,
error: ?Error,
}
const Title = styled(Text).attrs({
@ -35,7 +36,8 @@ const Title = styled(Text).attrs({
class ReleaseNotesBody extends PureComponent<Props, State> {
state = {
markdown: null,
notes: null,
error: null,
}
componentDidMount() {
@ -45,33 +47,46 @@ class ReleaseNotesBody extends PureComponent<Props, State> {
fetchNotes = async (version: string) => {
try {
const {
data: { body },
} = await network({
const { data } = await network({
method: 'GET',
url: `https://api.github.com/repos/LedgerHQ/ledger-live-desktop/releases/tags/v${version}`,
url: 'https://api.github.com/repos/LedgerHQ/ledger-live-desktop/releases',
// `https://api.github.com/repos/LedgerHQ/ledger-live-desktop/releases/tags/v${version}`,
})
if (body) {
this.setState({ markdown: body })
} else {
this.setState({ markdown: this.props.t('app:common.error.load') })
}
const v = semver.parse(version)
if (!v) throw new Error(`can't parse semver ${version}`)
const notes = data.filter(
d =>
semver.gte(
d.tag_name,
v.prerelease.length
? `${v.major}.${v.minor}.${v.patch}-${v.prerelease[0]}`
: `${v.major}.${v.minor}.0`,
) && semver.lte(d.tag_name, version),
)
this.setState({ notes })
} catch (error) {
this.setState({ markdown: this.props.t('app:common.error.load') })
this.setState({ error })
}
}
renderContent = () => {
const { markdown } = this.state
const { error, notes } = this.state
const { t } = this.props
const { version } = this.props
if (markdown) {
if (notes) {
return notes.map(note => (
<Notes mb={6}>
<Title>{t('app:releaseNotes.version', { versionNb: note.tag_name })}</Title>
<Markdow>{note.body}</Markdow>
</Notes>
))
} else if (error) {
return (
<Notes>
<Title>{t('app:releaseNotes.version', { versionNb: version })}</Title>
<Markdow>{markdown}</Markdow>
<Markdow>{t('app:common.error.load')}</Markdow>
</Notes>
)
}

13
src/components/modals/Send/fields/RecipientField.js

@ -4,11 +4,12 @@ import type { Account } from '@ledgerhq/live-common/lib/types'
import type { T } from 'types/common'
import type { WalletBridge } from 'bridge/types'
import { openURL } from 'helpers/linking'
import { urls } from 'config/support'
import { urls } from 'config/urls'
import Box from 'components/base/Box'
import LabelWithExternalIcon from 'components/base/LabelWithExternalIcon'
import RecipientAddress from 'components/RecipientAddress'
import { track } from 'analytics/segment'
import { createCustomErrorClass } from 'helpers/errors'
type Props<Transaction> = {
t: T,
@ -19,6 +20,8 @@ type Props<Transaction> = {
autoFocus?: boolean,
}
const InvalidAddress = createCustomErrorClass('InvalidAddress')
class RecipientField<Transaction> extends Component<Props<Transaction>, { isValid: boolean }> {
state = {
isValid: true,
@ -79,7 +82,13 @@ class RecipientField<Transaction> extends Component<Props<Transaction>, { isVali
<RecipientAddress
autoFocus={autoFocus}
withQrCode
error={!value || isValid ? null : `This is not a valid ${account.currency.name} address`}
error={
!value || isValid
? null
: new InvalidAddress(null, {
currencyName: account.currency.name,
})
}
value={value}
onChange={this.onChange}
/>

67
src/components/modals/Send/steps/01-step-amount.js

@ -45,45 +45,36 @@ export default ({
{account &&
bridge &&
transaction && (
<RecipientField
autoFocus={openedFromAccount}
account={account}
bridge={bridge}
transaction={transaction}
onChangeTransaction={onChangeTransaction}
t={t}
/>
)}
{account &&
bridge &&
transaction && (
<AmountField
key={account.id}
account={account}
bridge={bridge}
transaction={transaction}
onChangeTransaction={onChangeTransaction}
t={t}
/>
)}
{account &&
bridge &&
transaction &&
FeesField && (
<FeesField account={account} value={transaction} onChange={onChangeTransaction} />
)}
<Fragment key={account.id}>
<RecipientField
autoFocus={openedFromAccount}
account={account}
bridge={bridge}
transaction={transaction}
onChangeTransaction={onChangeTransaction}
t={t}
/>
<AmountField
account={account}
bridge={bridge}
transaction={transaction}
onChangeTransaction={onChangeTransaction}
t={t}
/>
{FeesField && (
<FeesField account={account} value={transaction} onChange={onChangeTransaction} />
)}
{account &&
bridge &&
transaction &&
AdvancedOptionsField && (
<AdvancedOptionsField
account={account}
value={transaction}
onChange={onChangeTransaction}
/>
{AdvancedOptionsField && (
<AdvancedOptionsField
account={account}
value={transaction}
onChange={onChangeTransaction}
/>
)}
</Fragment>
)}
</Box>
)

19
src/config/support.js → src/config/urls.js

@ -1,21 +1,30 @@
// @flow
export const urls = {
// Social
twitter: 'https://twitter.com/LedgerHQ',
github: 'https://github.com/LedgerHQ/ledger-live-desktop',
reddit: 'https://www.reddit.com/r/ledgerwallet/',
// Ledger support
faq: 'https://support.ledgerwallet.com/hc/en-us',
terms: 'https://www.ledgerwallet.com/terms',
noDeviceBuyNew: 'https://www.ledgerwallet.com/',
noDeviceTrackOrder: 'http://order.ledgerwallet.com/',
noDeviceLearnMore: 'https://www.ledgerwallet.com/',
managerHelpRequest: 'https://support.ledgerwallet.com/hc/en-us/articles/360006523674 ',
genuineCheckContactSupport:
'https://support.ledgerwallet.com/hc/en-us/requests/new?ticket_form_id=248165',
contactSupport: 'https://support.ledgerwallet.com/hc/en-us/requests/new?ticket_form_id=248165',
feesMoreInfo: 'https://support.ledgerwallet.com/hc/en-us/articles/360006535873',
recipientAddressInfo: 'https://support.ledgerwallet.com/hc/en-us/articles/360006433934',
// should join and generalize naming for the same urls once defined
receiveFlowContactSupport:
'https://support.ledgerwallet.com/hc/en-us/requests/new?ticket_form_id=248165',
privacyPolicy: 'https://www.ledgerwallet.com/privacy-policy',
githubIssues:
'https://github.com/LedgerHQ/ledger-live-desktop/issues?q=is%3Aissue+is%3Aopen+label%3Abug+sort%3Acomments-desc',
// Exchanges
coinhouse: 'https://www.coinhouse.com/r/157530',
changelly: 'https://changelly.com/?ref_id=aac789605a01',
coinmama: 'http://go.coinmama.com/visit/?bta=51801&nci=5343',
simplex: 'https://www.simplex.com/',
paybis: 'https://paybis.com/',
}

2
src/helpers/SettingsDefaults.js

@ -16,7 +16,7 @@ export const currencySettingsDefaults = ({
let confirmationsNb
if (blockAvgTime) {
const def = Math.ceil((30 * 60) / blockAvgTime) // 30 min approx validation
confirmationsNb = { min: 1, def, max: 2 * def }
confirmationsNb = { min: 1, def, max: 3 * def }
}
return {
confirmationsNb,

2
src/helpers/derivations.js

@ -8,8 +8,10 @@ type Derivation = ({
}) => string
const ethLegacyMEW: Derivation = ({ x }) => `44'/60'/0'/${x}`
ethLegacyMEW.mandatoryCount = 5
const etcLegacyMEW: Derivation = ({ x }) => `44'/60'/160720'/${x}'/0`
etcLegacyMEW.mandatoryCount = 5
const rippleLegacy: Derivation = ({ x }) => `44'/144'/0'/${x}'`

17
src/helpers/deviceAccess.js

@ -1,7 +1,8 @@
// @flow
import logger from 'logger'
import throttle from 'lodash/throttle'
import type Transport from '@ledgerhq/hw-transport'
import TransportNodeHid from '@ledgerhq/hw-transport-node-hid'
import { DEBUG_DEVICE } from 'config/constants'
import { retry } from './promise'
import { createCustomErrorClass } from './errors'
@ -26,14 +27,21 @@ let busy = false
TransportNodeHid.setListenDevicesPollingSkip(() => busy)
const refreshBusyUIState = throttle(() => {
process.send({
type: 'setDeviceBusy',
busy,
})
}, 100)
export const withDevice: WithDevice = devicePath => job => {
const p = queue.then(async () => {
busy = true
refreshBusyUIState()
try {
// FIXME: remove this retry
const t = await retry(() => TransportNodeHid.open(devicePath), { maxRetry: 1 })
if (DEBUG_DEVICE) {
t.setDebugMode(true)
}
t.setDebugMode(logger.apdu)
try {
const res = await job(t).catch(mapError)
return res
@ -42,6 +50,7 @@ export const withDevice: WithDevice = devicePath => job => {
}
} finally {
busy = false
refreshBusyUIState()
}
})

4
src/helpers/getAddressForCurrency/btc.js

@ -27,9 +27,7 @@ export default async (
if (bitcoinLikeInfo) {
const { P2SH, P2PKH } = await getBitcoinLikeInfo(transport)
if (P2SH !== bitcoinLikeInfo.P2SH || P2PKH !== bitcoinLikeInfo.P2PKH) {
throw new BtcUnmatchedApp(`BtcUnmatchedApp ${currency.id}`, {
currencyName: currency.name,
})
throw new BtcUnmatchedApp(`BtcUnmatchedApp ${currency.id}`, currency)
}
}

113
src/helpers/libcore.js

@ -36,65 +36,63 @@ type Props = {
devicePath: string,
currencyId: string,
onAccountScanned: AccountRaw => void,
isUnsubscribed: () => boolean,
}
export function scanAccountsOnDevice(props: Props): Promise<AccountRaw[]> {
const { devicePath, currencyId, onAccountScanned, core } = props
export async function scanAccountsOnDevice(props: Props): Promise<AccountRaw[]> {
const { devicePath, currencyId, onAccountScanned, core, isUnsubscribed } = props
const currency = getCryptoCurrencyById(currencyId)
return withDevice(devicePath)(async transport => {
const hwApp = new Btc(transport)
const commonParams = {
core,
currencyId,
onAccountScanned,
devicePath,
isUnsubscribed,
}
const commonParams = {
core,
currencyId,
onAccountScanned,
hwApp,
}
let allAccounts = []
let allAccounts = []
const nonSegwitAccounts = await scanAccountsOnDeviceBySegwit({
...commonParams,
showNewAccount: !!SHOW_LEGACY_NEW_ACCOUNT || !currency.supportsSegwit,
isSegwit: false,
isUnsplit: false,
})
allAccounts = allAccounts.concat(nonSegwitAccounts)
const nonSegwitAccounts = await scanAccountsOnDeviceBySegwit({
if (currency.supportsSegwit) {
const segwitAccounts = await scanAccountsOnDeviceBySegwit({
...commonParams,
showNewAccount: !!SHOW_LEGACY_NEW_ACCOUNT || !currency.supportsSegwit,
isSegwit: false,
showNewAccount: true,
isSegwit: true,
isUnsplit: false,
})
allAccounts = allAccounts.concat(nonSegwitAccounts)
allAccounts = allAccounts.concat(segwitAccounts)
}
// TODO: put that info inside currency itself
if (currencyId in SPLITTED_CURRENCIES) {
const splittedAccounts = await scanAccountsOnDeviceBySegwit({
...commonParams,
isSegwit: false,
showNewAccount: false,
isUnsplit: true,
})
allAccounts = allAccounts.concat(splittedAccounts)
if (currency.supportsSegwit) {
const segwitAccounts = await scanAccountsOnDeviceBySegwit({
...commonParams,
showNewAccount: true,
isSegwit: true,
isUnsplit: false,
})
allAccounts = allAccounts.concat(segwitAccounts)
}
// TODO: put that info inside currency itself
if (currencyId in SPLITTED_CURRENCIES) {
const splittedAccounts = await scanAccountsOnDeviceBySegwit({
...commonParams,
isSegwit: false,
showNewAccount: false,
isUnsplit: true,
isSegwit: true,
})
allAccounts = allAccounts.concat(splittedAccounts)
if (currency.supportsSegwit) {
const segwitAccounts = await scanAccountsOnDeviceBySegwit({
...commonParams,
showNewAccount: false,
isUnsplit: true,
isSegwit: true,
})
allAccounts = allAccounts.concat(segwitAccounts)
}
allAccounts = allAccounts.concat(segwitAccounts)
}
}
return allAccounts
})
return allAccounts
}
function encodeWalletName({
@ -114,17 +112,19 @@ function encodeWalletName({
async function scanAccountsOnDeviceBySegwit({
core,
hwApp,
devicePath,
currencyId,
onAccountScanned,
isUnsubscribed,
isSegwit,
isUnsplit,
showNewAccount,
}: {
core: *,
hwApp: Object,
devicePath: string,
currencyId: string,
onAccountScanned: AccountRaw => void,
isUnsubscribed: () => boolean,
isSegwit: boolean, // FIXME all segwit to change to 'purpose'
showNewAccount: boolean,
isUnsplit: boolean,
@ -135,7 +135,11 @@ async function scanAccountsOnDeviceBySegwit({
const path = `${isSegwit ? '49' : '44'}'/${coinType}'`
const { publicKey } = await hwApp.getWalletPublicKey(path, false, isSegwit)
const { publicKey } = await withDevice(devicePath)(async transport =>
new Btc(transport).getWalletPublicKey(path, false, isSegwit),
)
if (isUnsubscribed()) return []
const walletName = encodeWalletName({ publicKey, currencyId, isSegwit, isUnsplit })
@ -148,7 +152,7 @@ async function scanAccountsOnDeviceBySegwit({
const accounts = await scanNextAccount({
core,
wallet,
hwApp,
devicePath,
currencyId,
accountsCount,
accountIndex: 0,
@ -157,6 +161,7 @@ async function scanAccountsOnDeviceBySegwit({
isSegwit,
isUnsplit,
showNewAccount,
isUnsubscribed,
})
return accounts
@ -164,12 +169,14 @@ async function scanAccountsOnDeviceBySegwit({
const hexToBytes = str => Array.from(Buffer.from(str, 'hex'))
const createAccount = async (wallet, hwApp) => {
const createAccount = async (wallet, devicePath) => {
const accountCreationInfos = await wallet.getNextAccountCreationInfo()
await accountCreationInfos.derivations.reduce(
(promise, derivation) =>
promise.then(async () => {
const { publicKey, chainCode } = await hwApp.getWalletPublicKey(derivation)
const { publicKey, chainCode } = await withDevice(devicePath)(async transport =>
new Btc(transport).getWalletPublicKey(derivation),
)
accountCreationInfos.publicKeys.push(hexToBytes(publicKey))
accountCreationInfos.chainCodes.push(hexToBytes(chainCode))
}),
@ -222,7 +229,7 @@ async function scanNextAccount(props: {
// $FlowFixMe
wallet: NJSWallet,
core: *,
hwApp: Object,
devicePath: string,
currencyId: string,
accountsCount: number,
accountIndex: number,
@ -231,11 +238,12 @@ async function scanNextAccount(props: {
isSegwit: boolean,
isUnsplit: boolean,
showNewAccount: boolean,
isUnsubscribed: () => boolean,
}): Promise<AccountRaw[]> {
const {
core,
wallet,
hwApp,
devicePath,
currencyId,
accountsCount,
accountIndex,
@ -244,6 +252,7 @@ async function scanNextAccount(props: {
isSegwit,
isUnsplit,
showNewAccount,
isUnsubscribed,
} = props
// create account only if account has not been scanned yet
@ -252,13 +261,17 @@ async function scanNextAccount(props: {
const njsAccount = hasBeenScanned
? await wallet.getAccount(accountIndex)
: await createAccount(wallet, hwApp)
: await createAccount(wallet, devicePath)
if (isUnsubscribed()) return []
const shouldSyncAccount = true // TODO: let's sync everytime. maybe in the future we can optimize.
if (shouldSyncAccount) {
await coreSyncAccount(core, njsAccount)
}
if (isUnsubscribed()) return []
const query = njsAccount.queryOperations()
const ops = await query.complete().execute()
@ -273,6 +286,8 @@ async function scanNextAccount(props: {
ops,
})
if (isUnsubscribed()) return []
const isEmpty = ops.length === 0
if (!isEmpty || showNewAccount) {

12
src/helpers/pname.js

@ -0,0 +1,12 @@
// @flow
// Infer a "pname" aka short id version of process name
const pname =
typeof window === 'undefined'
? process.env.IS_INTERNAL_PROCESS
? 'internal'
: 'main'
: 'renderer'
export default pname

17
src/helpers/resolveLogsDirectory.js

@ -1,7 +1,6 @@
// @flow
import path from 'path'
import moment from 'moment'
const resolveLogsDirectory = () => {
const { LEDGER_LOGS_DIRECTORY } = process.env
@ -11,19 +10,3 @@ const resolveLogsDirectory = () => {
}
export default resolveLogsDirectory
export const RotatingLogFileParameters = {
filename: 'application-%DATE%.log',
datePattern: 'YYYY-MM-DD',
maxSize: '20m',
maxFiles: '14d',
}
export const getCurrentLogFile = () =>
path.resolve(
resolveLogsDirectory(),
RotatingLogFileParameters.filename.replace(
'%DATE%',
moment().format(RotatingLogFileParameters.datePattern),
),
)

10
src/helpers/resolveUserDataDirectory.js

@ -0,0 +1,10 @@
// @flow
const resolveUserDataDirectory = () => {
const { LEDGER_CONFIG_DIRECTORY } = process.env
if (LEDGER_CONFIG_DIRECTORY) return LEDGER_CONFIG_DIRECTORY
const electron = require('electron')
return (electron.app || electron.remote.app).getPath('userData')
}
export default resolveUserDataDirectory

8
src/helpers/socket.js

@ -31,11 +31,11 @@ export const createDeviceSocket = (transport: Transport<*>, url: string) =>
invariant(ws, 'websocket is available')
ws.on('open', () => {
logger.websocket('OPENED', url)
logger.websocket('OPENED', { url })
})
ws.on('error', e => {
logger.websocket('ERROR', e)
logger.websocket('ERROR', { message: e.message, stack: e.stack })
o.error(new WebsocketConnectionError(e.message, { url }))
})
@ -97,7 +97,7 @@ export const createDeviceSocket = (transport: Transport<*>, url: string) =>
},
error: msg => {
logger.websocket('ERROR', msg.data)
logger.websocket('ERROR', { data: msg.data })
throw new DeviceSocketFail(msg.data, { url })
},
}
@ -114,7 +114,7 @@ export const createDeviceSocket = (transport: Transport<*>, url: string) =>
logger.websocket('RECEIVE', msg)
await handlers[msg.query](msg)
} catch (err) {
logger.websocket('ERROR', err.toString())
logger.websocket('ERROR', { message: err.message, stack: err.stack })
o.error(err)
}
}

3
src/helpers/withLibcore.js

@ -14,7 +14,8 @@ export default async function withLibcore<A>(job: Job<A>): Promise<A> {
busy: true,
})
}
return job(core)
const res = await job(core)
return res
} finally {
if (--counter === 0) {
process.send({

2
src/index.ejs

@ -64,7 +64,7 @@
<script>
<% if (htmlWebpackPlugin.options.nodeModules) { %>
require('module').globalPaths.push(
'<%= htmlWebpackPlugin.options.nodeModules %>'.replace(/\\/g, '\\\\'),
'<%= htmlWebpackPlugin.options.nodeModules.replace(/\\/g, "\\\\") %>',
)
<% } %>
require('source-map-support/source-map-support.js').install()

7
src/internals/index.js

@ -7,8 +7,6 @@ import sentry from 'sentry/node'
import { EXPERIMENTAL_HTTP_ON_RENDERER } from 'config/constants'
import { serializeError } from 'helpers/errors'
logger.setProcessShortName('internal')
require('../env')
process.title = 'Ledger Live Internal'
@ -18,7 +16,10 @@ process.on('uncaughtException', err => {
type: 'uncaughtException',
error: serializeError(err),
})
process.exit(1)
// FIXME we should ideally do this:
// process.exit(1)
// but for now, until we kill all exceptions:
logger.critical(err, 'uncaughtException')
})
const defers = {}

1
src/logger/logger-storybook.js

@ -1,7 +1,6 @@
const noop = () => {}
module.exports = {
setProcessShortName: noop,
onCmd: noop,
onDB: noop,
onReduxAction: noop,

180
src/logger/logger.js

@ -2,10 +2,12 @@
import winston from 'winston'
import Transport from 'winston-transport'
import resolveLogsDirectory, { RotatingLogFileParameters } from 'helpers/resolveLogsDirectory'
import resolveLogsDirectory from 'helpers/resolveLogsDirectory'
import anonymizer from 'helpers/anonymizer'
import pname from 'helpers/pname'
import {
DEBUG_DEVICE,
DEBUG_NETWORK,
DEBUG_COMMANDS,
DEBUG_DB,
@ -18,8 +20,6 @@ import {
require('winston-daily-rotate-file')
let pname = '?'
const { format } = winston
const { combine, json, timestamp } = format
@ -28,12 +28,48 @@ const pinfo = format(info => {
return info
})
const transports = [
new winston.transports.DailyRotateFile({
function createDailyRotateFile(processName) {
return new winston.transports.DailyRotateFile({
dirname: resolveLogsDirectory(),
...RotatingLogFileParameters,
}),
]
json: true,
filename: `ledger-live-${processName}-%DATE%.log`,
datePattern: 'YYYY-MM-DD',
maxSize: '20m',
maxFiles: '7d',
})
}
const transports = [createDailyRotateFile(pname)]
const queryLogs = (processName: string, date: Date) =>
new Promise((resolve, reject) => {
const dailyRotateFile = createDailyRotateFile(processName)
const options = {
from: date - 10 * 60 * 1000,
until: date,
limit: 2000,
start: 0,
order: 'desc',
}
dailyRotateFile.query(options, (err, result) => {
if (err) {
reject(err)
return
}
resolve(result)
})
})
const queryAllLogs = async (date: Date = new Date()) => {
const internal = await queryLogs('internal', date)
const main = await queryLogs('main', date)
const renderer = await queryLogs('renderer', date)
const all = internal
.concat(main)
.concat(renderer)
.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp))
return all
}
if (process.env.NODE_ENV !== 'production' || process.env.DEV_TOOLS) {
let consoleT
@ -81,11 +117,25 @@ if (process.env.NODE_ENV !== 'production' || process.env.DEV_TOOLS) {
}
const logger = winston.createLogger({
level: 'info',
level: 'debug',
format: combine(pinfo(), timestamp(), json()),
transports,
})
const captureBreadcrumb = (breadcrumb: any) => {
if (!process.env.STORYBOOK_ENV) {
try {
if (typeof window !== 'undefined') {
require('sentry/browser').captureBreadcrumb(breadcrumb)
} else {
require('sentry/node').captureBreadcrumb(breadcrumb)
}
} catch (e) {
logger.log('warn', "Can't captureBreadcrumb", e)
}
}
}
const logCmds = !__DEV__ || DEBUG_COMMANDS
const logDb = !__DEV__ || DEBUG_DB
const logRedux = !__DEV__ || DEBUG_ACTION
@ -94,26 +144,48 @@ const logLibcore = !__DEV__ || DEBUG_LIBCORE
const logWS = !__DEV__ || DEBUG_WS
const logNetwork = !__DEV__ || DEBUG_NETWORK
const logAnalytics = !__DEV__ || DEBUG_ANALYTICS
const logApdu = !__DEV__ || DEBUG_DEVICE
export default {
setProcessShortName: (processShortName: string) => {
pname = processShortName
},
const blacklistTooVerboseCommandInput = ['libcoreSyncAccount', 'libcoreSignAndBroadcast']
const blacklistTooVerboseCommandResponse = [
'libcoreSyncAccount',
'libcoreScanAccounts',
'listApps',
'listAppVersions',
'listCategories',
]
export default {
onCmd: (type: string, id: string, spentTime: number, data?: any) => {
if (logCmds) {
switch (type) {
case 'cmd.START':
logger.log('info', 'info', `CMD ${id}.send()`, { type, data })
logger.log(
'info',
`CMD ${id}.send()`,
blacklistTooVerboseCommandInput.includes(id) ? { type } : { type, data },
)
break
case 'cmd.NEXT':
logger.log('info', `● CMD ${id}`, { type, data })
logger.log(
'info',
`● CMD ${id}`,
blacklistTooVerboseCommandResponse.includes(id) ? { type } : { type, data },
)
break
case 'cmd.COMPLETE':
logger.log('info', `✔ CMD ${id} finished in ${spentTime.toFixed(0)}ms`, { type })
captureBreadcrumb({
category: 'command',
message: `${id}`,
})
break
case 'cmd.ERROR':
logger.log('warn', `✖ CMD ${id} error`, { type, data })
captureBreadcrumb({
category: 'command',
message: `${id}`,
})
break
default:
}
@ -123,7 +195,7 @@ export default {
onDB: (way: 'read' | 'write' | 'clear', name: string) => {
const msg = `📁 ${way} ${name}`
if (logDb) {
logger.log('info', msg, { type: 'db' })
logger.log('debug', msg, { type: 'db' })
}
},
@ -131,7 +203,7 @@ export default {
onReduxAction: (action: Object) => {
if (logRedux) {
logger.log('info', `⚛️ ${action.type}`, { type: 'action', action })
logger.log('debug', `⚛️ ${action.type}`, { type: 'action' })
}
},
@ -142,19 +214,25 @@ export default {
const displayEl = `${tagName.toLowerCase()}${classList.length ? ` ${classList.item(0)}` : ''}`
const msg = `⇓ <TAB> - active element ${displayEl}`
if (logTabkey) {
logger.log('info', msg, { type: 'keydown' })
logger.log('debug', msg, { type: 'keydown' })
}
},
apdu: (log: string) => {
if (logApdu) {
logger.log('debug', log, { type: 'apdu' })
}
},
websocket: (type: string, msg: *) => {
websocket: (type: string, obj?: Object) => {
if (logWS) {
logger.log('info', `~ ${type}:`, msg, { type: 'ws' })
logger.log('debug', `~ ${type}`, { ...obj, type: 'ws' })
}
},
libcore: (level: string, msg: string) => {
if (logLibcore) {
logger.log('info', `🛠 ${level}: ${msg}`, { type: 'libcore' })
logger.log(level.toLowerCase(), `🛠 ${msg}`, { type: 'libcore' })
}
},
@ -176,12 +254,17 @@ export default {
status: number,
responseTime: number,
}) => {
const log = `✔📡 HTTP ${status} ${method} ${anonymizer.url(
url,
)} finished in ${responseTime.toFixed(0)}ms`
const anonymURL = anonymizer.url(url)
const log = `✔📡 HTTP ${status} ${method} ${url} – finished in ${responseTime.toFixed(0)}ms`
if (logNetwork) {
logger.log('info', log, { type: 'network-response' })
}
captureBreadcrumb({
category: 'network',
message: 'network success',
data: { url: anonymURL, status, method, responseTime },
})
},
networkError: ({
@ -197,12 +280,18 @@ export default {
error: string,
responseTime: number,
}) => {
const log = `✖📡 HTTP ${status} ${method} ${anonymizer.url(
url,
)} ${error} failed after ${responseTime.toFixed(0)}ms`
const anonymURL = anonymizer.url(url)
const log = `✖📡 HTTP ${status} ${method} ${url}${error} – failed after ${responseTime.toFixed(
0,
)}ms`
if (logNetwork) {
logger.log('info', log, { type: 'network-error', status, method })
}
captureBreadcrumb({
category: 'network',
message: 'network error',
data: { url: anonymURL, status, method, responseTime },
})
},
networkDown: ({
@ -214,12 +303,14 @@ export default {
url: string,
responseTime: number,
}) => {
const log = `✖📡 NETWORK DOWN – ${method} ${anonymizer.url(
url,
)} after ${responseTime.toFixed(0)}ms`
const log = `✖📡 NETWORK DOWN – ${method} ${url} – after ${responseTime.toFixed(0)}ms`
if (logNetwork) {
logger.log('info', log, { type: 'network-down' })
}
captureBreadcrumb({
category: 'network',
message: 'network down',
})
},
analyticsStart: (id: string) => {
@ -238,12 +329,23 @@ export default {
if (logAnalytics) {
logger.log('info', `△ track ${event}`, { type: 'anaytics-track', properties })
}
captureBreadcrumb({
category: 'track',
message: event,
data: properties,
})
},
analyticsPage: (category: string, name: ?string, properties: ?Object) => {
const message = name ? `${category} ${name}` : category
if (logAnalytics) {
logger.log('info', `△ page ${category} ${name || ''}`, { type: 'anaytics-page', properties })
logger.log('info', `△ page ${message}`, { type: 'anaytics-page', properties })
}
captureBreadcrumb({
category: 'page',
message,
data: properties,
})
},
// General functions in case the hooks don't apply
@ -260,8 +362,18 @@ export default {
logger.log('error', ...args)
},
critical: (error: Error) => {
logger.log('error', error)
critical: (error: Error, context?: string) => {
if (context) {
captureBreadcrumb({
category: 'context',
message: context,
})
}
// $FlowFixMe
logger.log('error', error.message, {
stack: error.stack,
...error,
})
if (!process.env.STORYBOOK_ENV) {
try {
if (typeof window !== 'undefined') {
@ -274,4 +386,6 @@ export default {
}
}
},
queryAllLogs,
}

14
src/main/app.js

@ -35,6 +35,20 @@ const { UPGRADE_EXTENSIONS, ELECTRON_WEBPACK_WDS_PORT, DEV_TOOLS, DEV_TOOLS_MODE
const devTools = __DEV__ || DEV_TOOLS
// context menu - see #978
require('electron-context-menu')({
showInspectElement: __DEV__ || DEV_TOOLS,
showCopyImageAddress: false,
// TODO: i18n for labels
labels: {
cut: 'Cut',
copy: 'Copy',
paste: 'Paste',
copyLink: 'Copy Link',
inspect: 'Inspect element',
},
})
const getWindowPosition = (height, width, display = screen.getPrimaryDisplay()) => {
const { bounds } = display

3
src/main/bridge.js

@ -17,8 +17,6 @@ import { setInternalProcessPID } from './terminator'
import { getMainWindow } from './app'
logger.setProcessShortName('main')
// sqlite files will be located in the app local data folder
const LEDGER_LIVE_SQLITE_PATH = path.resolve(app.getPath('userData'), 'sqlite')
const LEDGER_LOGS_DIRECTORY = process.env.LEDGER_LOGS_DIRECTORY || resolveLogsDirectory()
@ -51,6 +49,7 @@ const bootInternalProcess = () => {
internalProcess = fork(forkBundlePath, {
env: {
...process.env,
IS_INTERNAL_PROCESS: 1,
LEDGER_LOGS_DIRECTORY,
LEDGER_CONFIG_DIRECTORY,
LEDGER_LIVE_SQLITE_PATH,

51
src/reducers/onboarding.js

@ -8,9 +8,9 @@ type Step = {
external?: boolean,
label?: string,
options: {
showFooter: boolean,
showBackground: boolean,
showBreadcrumb: boolean,
relaunchSkip?: boolean,
alreadyInitSkip?: boolean,
},
}
@ -28,10 +28,11 @@ export type OnboardingState = {
},
isLedgerNano: boolean | null,
flowType: string,
onboardingRelaunched?: boolean,
}
const state: OnboardingState = {
stepIndex: 0, // FIXME is this used at all? dup with stepName?
const initialState: OnboardingState = {
stepIndex: 0,
stepName: SKIP_ONBOARDING ? 'analytics' : 'start',
genuine: {
pinStepPass: false,
@ -43,13 +44,12 @@ const state: OnboardingState = {
},
isLedgerNano: null,
flowType: '',
onboardingRelaunched: false,
steps: [
{
name: 'start',
external: true,
options: {
showFooter: false,
showBackground: true,
showBreadcrumb: false,
},
},
@ -57,8 +57,6 @@ const state: OnboardingState = {
name: 'init',
external: true,
options: {
showFooter: false,
showBackground: true,
showBreadcrumb: false,
},
},
@ -66,8 +64,6 @@ const state: OnboardingState = {
name: 'noDevice',
external: true,
options: {
showFooter: false,
showBackground: true,
showBreadcrumb: false,
},
},
@ -75,8 +71,6 @@ const state: OnboardingState = {
name: 'selectDevice',
label: 'onboarding:breadcrumb.selectDevice',
options: {
showFooter: false,
showBackground: true,
showBreadcrumb: true,
},
},
@ -84,26 +78,22 @@ const state: OnboardingState = {
name: 'selectPIN',
label: 'onboarding:breadcrumb.selectPIN',
options: {
showFooter: false,
showBackground: true,
showBreadcrumb: true,
alreadyInitSkip: true,
},
},
{
name: 'writeSeed',
label: 'onboarding:breadcrumb.writeSeed',
options: {
showFooter: false,
showBackground: true,
showBreadcrumb: true,
alreadyInitSkip: true,
},
},
{
name: 'genuineCheck',
label: 'onboarding:breadcrumb.genuineCheck',
options: {
showFooter: false,
showBackground: true,
showBreadcrumb: true,
},
},
@ -111,26 +101,22 @@ const state: OnboardingState = {
name: 'setPassword',
label: 'onboarding:breadcrumb.setPassword',
options: {
showFooter: false,
showBackground: true,
showBreadcrumb: true,
relaunchSkip: true,
},
},
{
name: 'analytics',
label: 'onboarding:breadcrumb.analytics',
options: {
showFooter: false,
showBackground: true,
showBreadcrumb: true,
relaunchSkip: true,
},
},
{
name: 'finish',
external: true,
options: {
showFooter: false,
showBackground: true,
showBreadcrumb: false,
},
},
@ -149,7 +135,7 @@ const handlers = {
}
return { ...state, stepName: state.steps[index + 1].name, stepIndex: index + 1 }
},
ONBOARDING_PREV_STEP: state => {
ONBOARDING_PREV_STEP: (state: OnboardingState) => {
const step = state.steps.find(step => step.name === state.stepName)
if (!step) {
return state
@ -160,7 +146,7 @@ const handlers = {
}
return { ...state, stepName: state.steps[index - 1].name, stepIndex: index - 1 }
},
ONBOARDING_JUMP_STEP: (state, { payload: stepName }) => {
ONBOARDING_JUMP_STEP: (state: OnboardingState, { payload: stepName }) => {
const step = state.steps.find(step => step.name === stepName)
if (!step) {
return state
@ -169,25 +155,30 @@ const handlers = {
return { ...state, stepName: step.name, stepIndex: index }
},
UPDATE_GENUINE_CHECK: (state, { payload: obj }) => ({
UPDATE_GENUINE_CHECK: (state: OnboardingState, { payload: obj }) => ({
...state,
genuine: {
...state.genuine,
...obj,
},
}),
ONBOARDING_SET_FLOW_TYPE: (state, { payload: flowType }) => ({
ONBOARDING_SET_FLOW_TYPE: (state: OnboardingState, { payload: flowType }) => ({
...state,
flowType,
}),
ONBOARDING_SET_DEVICE_TYPE: (state, { payload: isLedgerNano }) => ({
ONBOARDING_SET_DEVICE_TYPE: (state: OnboardingState, { payload: isLedgerNano }) => ({
...state,
isLedgerNano,
}),
ONBOARDING_RELAUNCH: (
state: OnboardingState,
{ payload: onboardingRelaunched }: { payload: $Shape<OnboardingState> },
) => ({ ...initialState, ...onboardingRelaunched }),
}
export default handleActions(handlers, state)
export default handleActions(handlers, initialState)
export const relaunchOnboarding = createAction('ONBOARDING_RELAUNCH')
export const nextStep = createAction('ONBOARDING_NEXT_STEP')
export const prevStep = createAction('ONBOARDING_PREV_STEP')
export const jumpStep = createAction('ONBOARDING_JUMP_STEP')

4
src/renderer/events.js

@ -105,8 +105,8 @@ export default ({ store }: { store: Object }) => {
onSetLibcoreBusy(busy)
})
ipcRenderer.on('setDeviceBusy', (event: any, { busy, devicePath }) => {
onSetDeviceBusy(devicePath, busy)
ipcRenderer.on('setDeviceBusy', (event: any, { busy }) => {
onSetDeviceBusy(busy)
})
}

19
src/renderer/init.js

@ -32,8 +32,6 @@ import AppError from 'components/AppError'
import 'styles/global'
logger.setProcessShortName('renderer')
const rootNode = document.getElementById('app')
const TAB_KEY = 9
@ -94,6 +92,23 @@ async function init() {
if (isGlobalTabEnabled()) disableGlobalTab()
})
}
document.addEventListener(
'dragover',
(event: Event) => {
event.preventDefault()
return false
},
false,
)
document.addEventListener(
'drop',
(event: Event) => {
event.preventDefault()
return false
},
false,
)
}
function r(Comp) {

4
src/sentry/browser.js

@ -11,3 +11,7 @@ export default (shouldSendCallback: () => boolean) => {
export const captureException = (e: Error) => {
Raven.captureException(e)
}
export const captureBreadcrumb = (o: *) => {
Raven.captureBreadcrumb(o)
}

32
src/sentry/install.js

@ -1,4 +1,5 @@
// @flow
import pname from 'helpers/pname'
import anonymizer from 'helpers/anonymizer'
/* eslint-disable no-continue */
@ -10,15 +11,28 @@ export default (Raven: any, shouldSendCallback: () => boolean, userId: string) =
captureUnhandledRejections: true,
allowSecretKey: true,
release: __APP_VERSION__,
tags: { git_commit: __GIT_REVISION__ },
tags: {
git_commit: __GIT_REVISION__,
},
environment: __DEV__ ? 'development' : 'production',
shouldSendCallback,
autoBreadcrumbs: {
xhr: false, // it is track anonymously from logger
console: false, // we don't track because not anonymized
dom: false, // user interactions like clicks. it's too cryptic to be exploitable.
location: false, // we don't really need location change because we use trackpage
sentry: true,
},
extra: {
process: pname,
},
dataCallback: (data: mixed) => {
// We are mutating the data to anonymize everything.
if (typeof data !== 'object' || !data) return data
delete data.server_name // hides the user machine name
if (typeof data.request === 'object' && data.request) {
const { request } = data
if (typeof request.url === 'string') {
@ -26,22 +40,6 @@ export default (Raven: any, shouldSendCallback: () => boolean, userId: string) =
}
}
if (data.breadcrumbs && typeof data.breadcrumbs === 'object') {
const { breadcrumbs } = data
if (Array.isArray(breadcrumbs.values)) {
const { values } = breadcrumbs
for (const b of values) {
if (!b || typeof b !== 'object') continue
if (b.category === 'xhr' && b.data && typeof b.data === 'object') {
const { data } = b
if (typeof data.url === 'string') {
data.url = anonymizer.url(data.url)
}
}
}
}
}
anonymizer.filepathRecursiveReplacer(data)
console.log('Sentry=>', data) // eslint-disable-line

4
src/sentry/node.js

@ -10,3 +10,7 @@ export default (shouldSendCallback: () => boolean, userId: string) => {
export const captureException = (e: Error) => {
Raven.captureException(e)
}
export const captureBreadcrumb = (o: *) => {
Raven.captureBreadcrumb(o)
}

46
static/i18n/en/app.yml

@ -1,11 +1,13 @@
---
common:
labelYes: 'Yes'
labelNo: 'No'
ok: OK
yes: Yes
no: No
apply: Apply
confirm: Confirm
cancel: Cancel
delete: Delete
launch: Launch
continue: Continue
learnMore: Learn more
skipThisStep: Skip this step
@ -63,11 +65,7 @@ buttons:
operation:
type:
IN: Received
# conf: Received
# unconf: Receiving...
OUT: Sent
# conf: Sent
# unconf: Sending...
time:
day: Day
week: Week
@ -89,7 +87,7 @@ account:
lastOperations: Last operations
emptyState:
title: No crypto assets yet?
desc: Make sure the <1><0>{{currency}}</0></1> app is installed and start receiving
desc: Make sure the <1><0>{{managerAppName}}</0></1> app is installed and start receiving
buttons:
receiveFunds: Receive
settings:
@ -134,8 +132,9 @@ currentAddress:
deviceConnect:
step1:
choose: "We detected {{count}} connected devices, please select one:"
connect: Connect and unlock your <1>Ledger device</1> # remove key: <3>PIN code</3>
dashboard: Not used. # This key is not used. Still managed in JS.
connect: Connect and unlock your <1>Ledger device</1>
step2:
open: 'Open the <1><0>{{managerAppName}}</0></1> app on your device'
emptyState:
sidebar:
text: Press this button to add accounts to your portfolio
@ -152,6 +151,8 @@ exchange:
coinhouse: 'Coinhouse is a trusted platform for individuals and institutional investors looking to analyze, acquire, sell and securely store crypto assets.'
changelly: 'Changelly is a popular instant crypto asset exchange with 100+ coins and tokens listed.'
coinmama: 'Coinmama is a financial service that makes it fast, safe and fun to buy digital assets, anywhere in the world.'
simplex: 'Simplex is a EU licensed financial institution, providing a fraudless credit card payment solution.'
paybis: 'it is safe and easy to Buy Bitcoin with credit card from PayBis. Service operates in US, Canada, Germany, Russia and Saudi Arabia.'
genuinecheck:
modal:
title: Genuine check
@ -181,7 +182,6 @@ addAccounts:
title: Add a new account
noOperationOnLastAccount: "No transactions found on your last new account <1><0>{{accountName}}</0></1>. You can add a new account after you've started transacting on that account."
noAccountToCreate: No <1><0>{{currencyName}}</0></1> account was found to create
somethingWentWrong: Something went wrong during synchronization, please try again.
cta:
addMore: 'Add more'
add: 'Add account'
@ -225,7 +225,7 @@ manager:
continue: Continue
latest: 'Firmware version {{version}} is available'
disclaimerTitle: 'You are about to install <1><0>firmware version {{version}}</0></1>'
disclaimerAppDelete: Apps installed on your device have to re-installed after the update.
disclaimerAppDelete: Apps installed on your device have to be re-installed after the update.
disclaimerAppReinstall: "This does not affect your crypto assets in the blockchain."
modal:
steps:
@ -276,7 +276,7 @@ send:
amount:
title: Details
selectAccountDebit: Select an account to debit
recipientAddress: Recipient address # can't control the tooltip!
recipientAddress: Recipient address
amount: Amount
max: Max
fees: Network fees
@ -309,7 +309,7 @@ send:
releaseNotes:
title: Release notes
version: Ledger Live {{versionNb}}
settings: # Always ensure descriptions carry full stops (.)
settings:
title: Settings
tabs:
display: General
@ -334,9 +334,9 @@ settings: # Always ensure descriptions carry full stops (.)
exchange: Rate provider ({{ticker}} → BTC)
exchangeDesc: Choose the provider of the rate between {{currencyName}} and Bitcoin. The indicative total value of your portfolio is calculated by converting your {{currencyName}} to Bitcoin, which is then converted to your base currency.
confirmationsToSpend: Number of confirmations required to spend
confirmationsToSpendDesc: Set the number of network confirmations required for your crypto assets to be spendable. # A higher number of confirmations decreases the probability that a transaction is rejected.
confirmationsToSpendDesc: Set the number of network confirmations required for your crypto assets to be spendable.
confirmationsNb: Number of confirmations
confirmationsNbDesc: Set the number of network confirmations for a transaction to be marked as confirmed. # A higher number of confirmations increases the certainty that a transaction cannot be reversed.
confirmationsNbDesc: Set the number of network confirmations for a transaction to be marked as confirmed.
transactionsFees: Default transaction fees
transactionsFeesDesc: Select your default transaction fees. The higher the fee, the faster the transaction will be processed.
explorer: Blockchain explorer
@ -360,12 +360,14 @@ settings: # Always ensure descriptions carry full stops (.)
analyticsDesc: Enable analytics of anonymous data to help Ledger improve the user experience. This includes the operating system, language, firmware versions and the number of added accounts.
reportErrors: Report bugs
reportErrorsDesc: Share anonymous usage and diagnostics data to help improve Ledger products, services and security features.
launchOnboarding: Onboarding
launchOnboardingDesc: Launch again the onboarding to add a new device to the Ledger Live application
about:
desc: Information about Ledger Live, terms and conditions, and privacy policy.
help:
desc: Learn about Ledger Live features or get help.
version: Version
releaseNotesBtn: Details # Close button instead of continue.
releaseNotesBtn: Details
faq: Ledger Support
faqDesc: A problem? Get help with Ledger Live, Ledger devices, supported crypto assets and apps.
terms: Terms and conditions
@ -383,6 +385,10 @@ settings: # Always ensure descriptions carry full stops (.)
title: Remove account
subTitle: Are you sure?
desc: The account will no longer be included in your portfolio. This operation does not affect your assets. Accounts can always be re-added.
openUserDataDirectory:
title: 'Open user data directory'
desc: 'Open the user data directory'
btn: 'Open'
exportLogs:
title: Export logs
desc: 'Exporting Ledger Live logs may be necessary for troubleshooting purposes.'
@ -398,13 +404,10 @@ password:
inputFields:
newPassword:
label: New password
placeholder: #remove
confirmPassword:
label: Confirm password
placeholder: #remove
currentPassword: #remove
currentPassword:
label: Current password
placeholder:
changePassword:
title: Password lock
subTitle: Change your password
@ -424,7 +427,8 @@ crash:
uselessText: You may try again by restarting Ledger Live. Please export your logs and contact Ledger Support if the problem persists.
restart: Restart
reset: Reset
createTicket: Contact us
support: Contact Support
github: GitHub
showDetails: Show details
showError: Show error
disclaimerModal:

45
static/i18n/en/errors.yml

@ -1,7 +1,10 @@
# the error codes are alphabetically sorted
---
generic:
title: '{{message}}'
description: Something went wrong. Please retry or contact us.
BtcUnmatchedApp:
title: That's the wrong app
description: Open the ‘{{currencyName}}’ app on your device
description: Open the ‘{{managerAppName}}’ app on your device
DeviceNotGenuine:
title: Possibly not genuine
description: 'Request Ledger Support assistance.'
@ -20,27 +23,9 @@ DeviceSocketNoHandler:
DisconnectedDevice:
title: Oops, device was disconnected
description: The connection to the device was lost, so please try again.
Error:
title: '{{message}}'
description: Something went wrong. Please retry or contact us.
"Invariant Violation":
title: '{{message}}'
description: Something went wrong. Please retry or contact us.
InternalError:
title: '{{message}}'
description: Something went wrong. Please retry or contact us.
TypeError:
title: '{{message}}'
description: Something went wrong. Please retry or contact us.
ReferenceError:
title: '{{message}}'
description: Something went wrong. Please retry or contact us.
FeeEstimationFailed:
title: Sorry, fee estimation failed
description: 'Try setting a custom fee (status: {{status}})'
generic:
title: Sorry, that's an unexpected bug
description: Please retry or contact Ledger Support.
HardResetFail:
title: Oops, could not reset
description: Please retry or contact Ledger Support.
@ -58,13 +43,13 @@ LedgerAPINotAvailable:
description: Please retry or contact Ledger Support.
ManagerAPIsFail:
title: Oops, Manager services unavailable.
description: Please check the network status. #link to status.ledger.fr ?
description: Please check the network status.
ManagerAppAlreadyInstalled:
title: Oops, that's already installed. # include {{currencyName}}
description: Check your device to see which apps are already installed.
title: Oops, that's already installed.
description: Check your device to see which apps are already installed.
ManagerAppRelyOnBTC:
title: Bitcoin app required
description: Install the Bitcoin app before installing this app. # include {{currencyName}}
description: Install the Bitcoin app before installing this app.
ManagerDeviceLocked:
title: Please unlock your device
description: Your device was locked. Please unlock it.
@ -72,7 +57,7 @@ ManagerNotEnoughSpace:
title: Sorry, insufficient device storage
description: Uninstall some apps to increase available storage and try again.
ManagerUninstallBTCDep:
title: Sorry, Bitcoin is required # include {{currencyName}}
title: Sorry, Bitcoin is required
description: First uninstall apps that depend on Bitcoin.
NetworkDown:
title: Oops, internet seems down
@ -83,9 +68,6 @@ NoAddressesFound:
NotEnoughBalance:
title: Oops, not enough balance
description: Make sure the account to debit has sufficient balance
RangeError:
title: '{{message}}'
description:
TimeoutError:
title: Oops, a time out occurred
description: It took too long for the server to respond.
@ -106,16 +88,15 @@ UserRefusedAddress:
description: Please try again or contact Ledger Support
WebsocketConnectionError:
title: Sorry, try again (websocket error).
description: #context
description:
WebsocketConnectionFailed:
title: Sorry, try again (websocket failed).
description:
WrongAppOpened:
title: Please open the ‘{{currencyName}}’ app
description: The wrong app was opened on your device. Please retry.
WrongDeviceForAccount:
title: Oops, wrong device for ‘{{accountName}}’.
description: The connected device is not associated with the account you selected. Please connect the right device.
DeviceAppVerifyNotSupported:
title: Open Manager to update this App
description: The app verification is not supported
InvalidAddress:
title: 'This is not a valid {{currencyName}} address'

1
static/i18n/en/language.yml

@ -1,3 +1,4 @@
---
system: Use system language
en: English
fr: French

29
static/i18n/en/onboarding.yml

@ -1,3 +1,4 @@
---
breadcrumb:
selectDevice: Device selection
selectPIN: PIN code
@ -35,7 +36,7 @@ selectDevice:
title: Ledger Blue
selectPIN:
disclaimer:
note1: 'Choose your own PIN code, this code will unlock your device.' # dotted line should be in red. Increase size of the hand (+50%?)
note1: 'Choose your own PIN code, this code will unlock your device.'
note2: An 8-digit PIN code offers an optimum level of security.
note3: Never use a device supplied with a PIN code or a 24-word recovery phrase.
initialize:
@ -68,7 +69,7 @@ writeSeed:
desc: Your device will generate a 24-word recovery phrase to back up your private keys
nano:
step1: 'Copy the word displayed below <1><0>Word #1</0></1> in position 1 on a blank Recovery sheet.'
step2: 'Press the right button to display <1><0>Word #2</0></1> and repeat the process until all 24 words are copied on the Recovery sheet.' #<italic>Recovery sheet</italic>
step2: 'Press the right button to display <1><0>Word #2</0></1> and repeat the process until all 24 words are copied on the Recovery sheet.'
step3: 'Confirm your recovery phrase: select each requested word and press both buttons to validate it.'
blue:
step1: Copy each word of the recovery phrase on a blank Recovery sheet. Copy the words in the same order.
@ -146,21 +147,17 @@ analytics:
title: Report bugs
desc: Automatically send reports to help Ledger fix bugs.
technicalData:
title: Technical data *
desc: Ledger will automatically collect technical information to get basic feedback on usage. This information is anonymous and does not contain personal data.
mandatoryText: '* mandatory'
mandatoryContextual:
title: Technical data
item1: Anonymous unique application ID
item2: OS name and version
item3: Ledger Live version
item4: Application language or region
item5: OS language or region
title: Technical data *
desc: Ledger will automatically collect technical information to get basic feedback on usage. This information is anonymous and does not contain personal data.
mandatoryText: '* mandatory'
mandatoryContextual:
title: Technical data
item1: Anonymous unique application ID
item2: OS name and version
item3: Ledger Live version
item4: Application language or region
item5: OS language or region
finish:
title: Your device is ready!
desc: Proceed to your portfolio and start adding your accounts...
openAppButton: Open Ledger Live
#tooltip:
# twitter: Twitter
# github: GitHub
# reddit: Reddit

24
static/i18n/fr/app.yml

@ -1,8 +1,8 @@
---
common:
labelYes: 'Yes'
labelNo: 'No'
ok: OK
yes: true
no: false
apply: Apply
confirm: Confirm
cancel: Cancel
@ -86,7 +86,7 @@ account:
lastOperations: Last operations
emptyState:
title: No crypto assets yet?
desc: Make sure the <1><0>{{currency}}</0></1> app is installed and start receiving
desc: Make sure the <1><0>{{managerAppName}}</0></1> app is installed and start receiving
buttons:
receiveFunds: Receive
settings:
@ -132,7 +132,8 @@ deviceConnect:
step1:
choose: "We detected {{count}} connected devices, please select one:"
connect: Connect and unlock your <1>Ledger device</1>
dashboard: Not used.
step2:
open: 'Open the <1><0>{{managerAppName}}</0></1> app on your device'
emptyState:
sidebar:
text: Press this button to add accounts to your portfolio
@ -149,6 +150,8 @@ exchange:
coinhouse: 'Coinhouse is a trusted platform for individuals and institutional investors looking to analyze, acquire, sell and securely store crypto assets.'
changelly: 'Changelly is a popular instant crypto asset exchange with 100+ coins and tokens listed.'
coinmama: 'Coinmama is a financial service that makes it fast, safe and fun to buy digital assets, anywhere in the world.'
simplex: 'Simplex is a EU licensed financial institution, providing a fraudless credit card payment solution.'
paybis: 'it is safe and easy to Buy Bitcoin with credit card from PayBis. Service operates in US, Canada, Germany, Russia and Saudi Arabia.'
genuinecheck:
modal:
title: Genuine check
@ -178,7 +181,6 @@ addAccounts:
title: Add a new account
noOperationOnLastAccount: "No transactions found on your last new account <1><0>{{accountName}}</0></1>. You can add a new account after you've started transacting on that account."
noAccountToCreate: No <1><0>{{currencyName}}</0></1> account was found to create
somethingWentWrong: Something went wrong during synchronization, please try again.
cta:
addMore: 'Add more'
add: 'Add account'
@ -222,7 +224,7 @@ manager:
continue: Continue
latest: 'Firmware version {{version}} is available'
disclaimerTitle: 'You are about to install <1><0>firmware version {{version}}</0></1>'
disclaimerAppDelete: Apps installed on your device have to re-installed after the update.
disclaimerAppDelete: Apps installed on your device have to be re-installed after the update.
disclaimerAppReinstall: "This does not affect your crypto assets in the blockchain."
modal:
steps:
@ -380,6 +382,10 @@ settings:
title: Remove account
subTitle: Are you sure?
desc: The account will no longer be included in your portfolio. This operation does not affect your assets. Accounts can always be re-added.
openUserDataDirectory:
title: 'Open user data directory'
desc: 'Open the user data directory'
btn: 'Open'
exportLogs:
title: Export logs
desc: 'Exporting Ledger Live logs may be necessary for troubleshooting purposes.'
@ -395,13 +401,10 @@ password:
inputFields:
newPassword:
label: New password
placeholder:
confirmPassword:
label: Confirm password
placeholder:
currentPassword:
label: Current password
placeholder:
changePassword:
title: Password lock
subTitle: Change your password
@ -421,7 +424,8 @@ crash:
uselessText: You may try again by restarting Ledger Live. Please export your logs and contact Ledger Support if the problem persists.
restart: Restart
reset: Reset
createTicket: Contact us
support: Contact Support
github: GitHub
showDetails: Show details
showError: Show error
disclaimerModal:

31
static/i18n/fr/errors.yml

@ -1,7 +1,10 @@
---
generic:
title: '{{message}}'
description: Something went wrong. Please retry or contact us.
BtcUnmatchedApp:
title: That's the wrong app
description: Open the ‘{{currencyName}}’ app on your device
description: Open the ‘{{managerAppName}}’ app on your device
DeviceNotGenuine:
title: Possibly not genuine
description: 'Request Ledger Support assistance.'
@ -20,27 +23,9 @@ DeviceSocketNoHandler:
DisconnectedDevice:
title: Oops, device was disconnected
description: The connection to the device was lost, so please try again.
Error:
title: '{{message}}'
description: Something went wrong. Please retry or contact us.
"Invariant Violation":
title: '{{message}}'
description: Something went wrong. Please retry or contact us.
InternalError:
title: '{{message}}'
description: Something went wrong. Please retry or contact us.
TypeError:
title: '{{message}}'
description: Something went wrong. Please retry or contact us.
ReferenceError:
title: '{{message}}'
description: Something went wrong. Please retry or contact us.
FeeEstimationFailed:
title: Sorry, fee estimation failed
description: 'Try setting a custom fee (status: {{status}})'
generic:
title: Sorry, that's an unexpected bug
description: Please retry or contact Ledger Support.
HardResetFail:
title: Oops, could not reset
description: Please retry or contact Ledger Support.
@ -83,9 +68,6 @@ NoAddressesFound:
NotEnoughBalance:
title: Oops, not enough balance
description: Make sure the account to debit has sufficient balance
RangeError:
title: '{{message}}'
description:
TimeoutError:
title: Oops, a time out occurred
description: It took too long for the server to respond.
@ -110,12 +92,11 @@ WebsocketConnectionError:
WebsocketConnectionFailed:
title: Sorry, try again (websocket failed).
description:
WrongAppOpened:
title: Please open the ‘{{currencyName}}’ app
description: The wrong app was opened on your device. Please retry.
WrongDeviceForAccount:
title: Oops, wrong device for ‘{{accountName}}’.
description: The connected device is not associated with the account you selected. Please connect the right device.
DeviceAppVerifyNotSupported:
title: Open Manager to update this App
description: The app verification is not supported
InvalidAddress:
title: 'This is not a valid {{currencyName}} address'

4
webpack/internals.config.js

@ -47,4 +47,8 @@ module.exports = webpackMain().then(config => ({
},
plugins: [...plugins('internals'), ...config.plugins],
optimization: {
minimize: false,
},
}))

3
webpack/main.config.js

@ -9,6 +9,9 @@ const config = {
module: {
rules,
},
optimization: {
minimize: false,
},
}
module.exports = config

2
webpack/renderer.config.js

@ -15,7 +15,7 @@ const config = {
historyApiFallback: true,
},
optimization: {
minimizer: [],
minimize: false,
},
}

97
yarn.lock

@ -1475,10 +1475,10 @@
resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-0.7.1.tgz#e44e596d03c9f16ba3b127ad333a8a072bcb5a0a"
"@ledgerhq/hw-app-btc@^4.13.0":
version "4.17.0"
resolved "https://registry.yarnpkg.com/@ledgerhq/hw-app-btc/-/hw-app-btc-4.17.0.tgz#8f9e138446ea4b4c19c1584160f0333bbdfd48ae"
version "4.19.0"
resolved "https://registry.yarnpkg.com/@ledgerhq/hw-app-btc/-/hw-app-btc-4.19.0.tgz#0fdce47ad71df7783c6bf881e6a9bc8b4c84de52"
dependencies:
"@ledgerhq/hw-transport" "^4.15.0"
"@ledgerhq/hw-transport" "^4.19.0"
create-hash "^1.1.3"
"@ledgerhq/hw-app-btc@^4.7.3":
@ -1489,23 +1489,23 @@
create-hash "^1.1.3"
"@ledgerhq/hw-app-eth@^4.14.0":
version "4.15.0"
resolved "https://registry.yarnpkg.com/@ledgerhq/hw-app-eth/-/hw-app-eth-4.15.0.tgz#16e520793d27d6daf9edfb6f08138aad14f2ce4f"
version "4.19.0"
resolved "https://registry.yarnpkg.com/@ledgerhq/hw-app-eth/-/hw-app-eth-4.19.0.tgz#2a76556172b4e9522a9b92357a48b39e10e003a6"
dependencies:
"@ledgerhq/hw-transport" "^4.15.0"
"@ledgerhq/hw-transport" "^4.19.0"
"@ledgerhq/hw-app-xrp@^4.13.0":
version "4.15.0"
resolved "https://registry.yarnpkg.com/@ledgerhq/hw-app-xrp/-/hw-app-xrp-4.15.0.tgz#a6d553ad89559465d7bcce3b34d56a131722166d"
version "4.19.0"
resolved "https://registry.yarnpkg.com/@ledgerhq/hw-app-xrp/-/hw-app-xrp-4.19.0.tgz#67fc53e5ab7791f28481ed5406adcf7d92c7ca9b"
dependencies:
"@ledgerhq/hw-transport" "^4.15.0"
"@ledgerhq/hw-transport" "^4.19.0"
bip32-path "0.4.2"
"@ledgerhq/hw-transport-node-hid@^4.13.0":
version "4.18.0"
resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport-node-hid/-/hw-transport-node-hid-4.18.0.tgz#39ab0f9300c755f6b33f28dda22e30677882b6be"
version "4.19.1"
resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport-node-hid/-/hw-transport-node-hid-4.19.1.tgz#bc2cda4db0b4e8a3e26682bf5b81011f8eb53d82"
dependencies:
"@ledgerhq/hw-transport" "^4.15.0"
"@ledgerhq/hw-transport" "^4.19.0"
node-hid "^0.7.2"
"@ledgerhq/hw-transport-node-hid@^4.7.6":
@ -1515,9 +1515,9 @@
"@ledgerhq/hw-transport" "^4.15.0"
node-hid "^0.7.2"
"@ledgerhq/hw-transport@^4.13.0", "@ledgerhq/hw-transport@^4.15.0":
version "4.15.0"
resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport/-/hw-transport-4.15.0.tgz#ec99436c2662e70fb6f9c72f7bb5d1f3a051c4e3"
"@ledgerhq/hw-transport@^4.13.0", "@ledgerhq/hw-transport@^4.15.0", "@ledgerhq/hw-transport@^4.19.0":
version "4.19.0"
resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport/-/hw-transport-4.19.0.tgz#19a804aee1bfc4abac1dc9a2a7a582e79273f991"
dependencies:
events "^2.0.0"
@ -1535,8 +1535,8 @@
prebuild-install "^2.2.2"
"@ledgerhq/live-common@^2.32.0":
version "2.32.0"
resolved "https://registry.yarnpkg.com/@ledgerhq/live-common/-/live-common-2.32.0.tgz#6c2108d58ec44335077d87442a6418e0ec4d3372"
version "2.34.0"
resolved "https://registry.yarnpkg.com/@ledgerhq/live-common/-/live-common-2.34.0.tgz#436ec0816c37f6aabcbca807090f152f1bd3b785"
dependencies:
axios "^0.18.0"
invariant "^2.2.2"
@ -5794,6 +5794,13 @@ electron-builder@20.14.7:
update-notifier "^2.5.0"
yargs "^11.0.0"
electron-context-menu@^0.10.0:
version "0.10.0"
resolved "https://registry.yarnpkg.com/electron-context-menu/-/electron-context-menu-0.10.0.tgz#97fce2b805e03ac2b1dae11eb6a68b064b78d633"
dependencies:
electron-dl "^1.2.0"
electron-is-dev "^0.3.0"
electron-devtools-installer@^2.2.3, electron-devtools-installer@^2.2.4:
version "2.2.4"
resolved "https://registry.yarnpkg.com/electron-devtools-installer/-/electron-devtools-installer-2.2.4.tgz#261a50337e37121d338b966f07922eb4939a8763"
@ -5803,6 +5810,14 @@ electron-devtools-installer@^2.2.3, electron-devtools-installer@^2.2.4:
rimraf "^2.5.2"
semver "^5.3.0"
electron-dl@^1.2.0:
version "1.12.0"
resolved "https://registry.yarnpkg.com/electron-dl/-/electron-dl-1.12.0.tgz#328c7f12d3e458ed4ddc773d8ffc28d59ab35d2e"
dependencies:
ext-name "^5.0.0"
pupa "^1.0.0"
unused-filename "^1.0.0"
electron-download-tf@4.3.4:
version "4.3.4"
resolved "https://registry.yarnpkg.com/electron-download-tf/-/electron-download-tf-4.3.4.tgz#b03740b2885aa2ad3f8784fae74df427f66d5165"
@ -6543,6 +6558,19 @@ express@^4.16.2, express@^4.16.3:
utils-merge "1.0.1"
vary "~1.1.2"
ext-list@^2.0.0:
version "2.2.2"
resolved "https://registry.yarnpkg.com/ext-list/-/ext-list-2.2.2.tgz#0b98e64ed82f5acf0f2931babf69212ef52ddd37"
dependencies:
mime-db "^1.28.0"
ext-name@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/ext-name/-/ext-name-5.0.0.tgz#70781981d183ee15d13993c8822045c506c8f0a6"
dependencies:
ext-list "^2.0.0"
sort-keys-length "^1.0.0"
extend-shallow@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f"
@ -8741,6 +8769,10 @@ js-tokens@^3.0.0, js-tokens@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
"js-tokens@^3.0.0 || ^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
js-yaml@3.5.4:
version "3.5.4"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.5.4.tgz#f64f16dcd78beb9ce8361068e733ebe47b079179"
@ -9361,7 +9393,13 @@ longest@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097"
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1:
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.1:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
dependencies:
js-tokens "^3.0.0 || ^4.0.0"
loose-envify@^1.2.0:
version "1.3.1"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848"
dependencies:
@ -9654,7 +9692,7 @@ miller-rabin@^4.0.0:
bn.js "^4.0.0"
brorand "^1.0.1"
"mime-db@>= 1.34.0 < 2":
"mime-db@>= 1.34.0 < 2", mime-db@^1.28.0:
version "1.34.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.34.0.tgz#452d0ecff5c30346a6dc1e64b1eaee0d3719ff9a"
@ -9809,6 +9847,10 @@ mkdirp@0.5.1, mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdi
dependencies:
minimist "0.0.8"
modify-filename@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/modify-filename/-/modify-filename-1.1.0.tgz#9a2dec83806fbb2d975f22beec859ca26b393aa1"
moment@^2.11.2, moment@^2.21.0, moment@^2.22.2:
version "2.22.2"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.22.2.tgz#3c257f9839fc0e93ff53149632239eb90783ff66"
@ -11500,6 +11542,10 @@ punycode@^2.1.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
pupa@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/pupa/-/pupa-1.0.0.tgz#9a9568a5af7e657b8462a6e9d5328743560ceff6"
pushdata-bitcoin@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/pushdata-bitcoin/-/pushdata-bitcoin-1.0.1.tgz#15931d3cd967ade52206f523aa7331aef7d43af7"
@ -13141,6 +13187,12 @@ socks@~2.2.0:
ip "^1.1.5"
smart-buffer "^4.0.1"
sort-keys-length@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/sort-keys-length/-/sort-keys-length-1.0.1.tgz#9cb6f4f4e9e48155a6aa0671edd336ff1479a188"
dependencies:
sort-keys "^1.0.0"
sort-keys@^1.0.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-1.1.2.tgz#441b6d4d346798f1b4e49e8920adfba0e543f9ad"
@ -14185,6 +14237,13 @@ untildify@^3.0.2:
version "3.0.3"
resolved "https://registry.yarnpkg.com/untildify/-/untildify-3.0.3.tgz#1e7b42b140bcfd922b22e70ca1265bfe3634c7c9"
unused-filename@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/unused-filename/-/unused-filename-1.0.0.tgz#d340880f71ae2115ebaa1325bef05cc6684469c6"
dependencies:
modify-filename "^1.1.0"
path-exists "^3.0.0"
unzip-response@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-2.0.1.tgz#d2f0f737d16b0615e72a6935ed04214572d56f97"

Loading…
Cancel
Save