Browse Source

Merge pull request #1804 from LedgerHQ/develop

Prepare 1.4.0
master
Gaëtan Renaudeau 6 years ago
committed by GitHub
parent
commit
db895a2dba
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      .github/ISSUE_TEMPLATE/feature_request.md
  2. 3
      README.md
  3. 26
      package.json
  4. 15
      scripts/check-no-dups.sh
  5. 51
      scripts/create-release-shasums.sh
  6. 1
      scripts/release.sh
  7. 91
      src/api/Ethereum.js
  8. 31
      src/api/Fees.js
  9. 6
      src/api/Ledger.js
  10. 47
      src/api/Ripple.js
  11. 1
      src/bridge/BridgeSyncContext.js
  12. 12
      src/bridge/EthereumJSBridge.js
  13. 8
      src/bridge/LibcoreBridge.js
  14. 35
      src/bridge/RippleJSBridge.js
  15. 4
      src/bridge/index.js
  16. 2
      src/bridge/makeMockBridge.js
  17. 8
      src/bridge/types.js
  18. BIN
      src/commands/.DS_Store
  19. 59
      src/commands/autoUpdate.js
  20. 4
      src/commands/index.js
  21. 2
      src/commands/libcoreSignAndBroadcast.js
  22. 30
      src/commands/quitAndInstallElectronUpdate.js
  23. 17
      src/components/App.js
  24. 4
      src/components/CounterValue/index.js
  25. 4
      src/components/CurrenciesStatusBanner.js
  26. 3
      src/components/CurrentAddress/index.js
  27. 39
      src/components/DashboardPage/index.js
  28. 9
      src/components/DevToolsPage/AccountImporter.js
  29. 2
      src/components/FeesField/BitcoinKind.js
  30. 2
      src/components/FeesField/EthereumKind.js
  31. 5
      src/components/FeesField/RippleKind.js
  32. 13
      src/components/GenuineCheckModal.js
  33. 4
      src/components/IsUnlocked.js
  34. 9
      src/components/MainSideBar/index.js
  35. 284
      src/components/ManagerPage/AppsList.js
  36. 3
      src/components/ManagerPage/ManagerApp.js
  37. 5
      src/components/Onboarding/steps/GenuineCheck/GenuineCheckUnavailable.js
  38. 4
      src/components/OperationsList/AccountCell.js
  39. 4
      src/components/OperationsList/AmountCell.js
  40. 2
      src/components/SelectExchange.js
  41. 3
      src/components/SettingsPage/CleanButton.js
  42. 2
      src/components/SettingsPage/CounterValueSelect.js
  43. 67
      src/components/SettingsPage/DisablePasswordModal.js
  44. 8
      src/components/SettingsPage/PasswordAutoLockSelect.js
  45. 1
      src/components/SettingsPage/PasswordForm.js
  46. 49
      src/components/SettingsPage/PasswordModal.js
  47. 16
      src/components/SettingsPage/RepairDeviceButton.js
  48. 3
      src/components/SettingsPage/ResetButton.js
  49. 2
      src/components/SettingsPage/ResetFallbackModal.js
  50. 94
      src/components/SettingsPage/SocketExport.js
  51. 4
      src/components/SettingsPage/sections/Display.js
  52. 71
      src/components/SettingsPage/sections/Export.js
  53. 130
      src/components/TopBanner.js
  54. 4
      src/components/TopBar/index.js
  55. 103
      src/components/UpdateNotifier/UpdateDownloaded.js
  56. 48
      src/components/UpdateNotifier/UpdateInstalled.js
  57. 17
      src/components/UpdateNotifier/index.js
  58. 74
      src/components/Updater/Banner.js
  59. 70
      src/components/Updater/DebugUpdater.js
  60. 37
      src/components/Updater/UpdateDot.js
  61. 95
      src/components/Updater/UpdaterContext.js
  62. 8
      src/components/WithFeesAPI.js
  63. 6
      src/components/base/CurrencyBadge.js
  64. 59
      src/components/base/Modal/ConfirmModal.js
  65. 143
      src/components/base/Modal/ModalBody.js
  66. 59
      src/components/base/Modal/ModalContent.js
  67. 18
      src/components/base/Modal/ModalFooter.js
  68. 93
      src/components/base/Modal/ModalHeader.js
  69. 75
      src/components/base/Modal/ModalTitle.js
  70. 95
      src/components/base/Modal/RepairModal.js
  71. 345
      src/components/base/Modal/index.js
  72. 75
      src/components/base/Modal/stories.js
  73. 13
      src/components/base/SideBar/SideBarListItem.js
  74. 1
      src/components/base/SideBar/stories.js
  75. 44
      src/components/base/Stepper/index.js
  76. 6
      src/components/layout/Default.js
  77. 90
      src/components/modals/AccountSettingRenderBody.js
  78. 16
      src/components/modals/AddAccounts/index.js
  79. 32
      src/components/modals/Debug.js
  80. 47
      src/components/modals/Disclaimer.js
  81. 215
      src/components/modals/OperationDetails.js
  82. 9
      src/components/modals/Receive/index.js
  83. 38
      src/components/modals/ReleaseNotes/ReleaseNotesBody.js
  84. 56
      src/components/modals/ReleaseNotes/index.js
  85. 10
      src/components/modals/Send/fields/RecipientField.js
  86. 5
      src/components/modals/Send/index.js
  87. 2
      src/components/modals/Send/steps/01-step-amount.js
  88. 5
      src/components/modals/SettingsAccount.js
  89. 62
      src/components/modals/ShareAnalytics.js
  90. 59
      src/components/modals/TechnicalData.js
  91. 78
      src/components/modals/UpdateFirmware/Disclaimer.js
  92. 7
      src/components/modals/UpdateFirmware/index.js
  93. 6
      src/config/constants.js
  94. 6
      src/config/urls.js
  95. 39
      src/icons/Donjon.js
  96. 13
      src/icons/TriangleWarning.js
  97. 30
      src/icons/device/NanoX.js
  98. 1
      src/icons/device/index.js
  99. 1
      src/index.ejs
  100. 63
      src/internals/index.js

2
.github/ISSUE_TEMPLATE/feature_request.md

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

3
README.md

@ -1,5 +1,7 @@
# Ledger Live (desktop) [![CircleCI](https://circleci.com/gh/LedgerHQ/ledger-live-desktop.svg?style=svg)](https://circleci.com/gh/LedgerHQ/ledger-live-desktop) [![Crowdin](https://d322cqt584bo4o.cloudfront.net/ledger-wallet/localized.svg)](https://crowdin.com/project/ledger-wallet) # Ledger Live (desktop) [![CircleCI](https://circleci.com/gh/LedgerHQ/ledger-live-desktop.svg?style=svg)](https://circleci.com/gh/LedgerHQ/ledger-live-desktop) [![Crowdin](https://d322cqt584bo4o.cloudfront.net/ledger-wallet/localized.svg)](https://crowdin.com/project/ledger-wallet)
- Related: [ledger-live-mobile](https://github.com/ledgerhq/ledger-live-mobile)
> Ledger Live is a new generation wallet desktop application providing a unique interface to maintain multiple cryptocurrencies for your Ledger Nano S / Blue. Manage your device, create accounts, receive and send cryptoassets, [...and many more](https://www.ledger.fr/2018/07/09/ledger-launches-ledger-live-the-all-in-one-companion-app-to-your-ledger-device/). > Ledger Live is a new generation wallet desktop application providing a unique interface to maintain multiple cryptocurrencies for your Ledger Nano S / Blue. Manage your device, create accounts, receive and send cryptoassets, [...and many more](https://www.ledger.fr/2018/07/09/ledger-launches-ledger-live-the-all-in-one-companion-app-to-your-ledger-device/).
<a href="https://github.com/LedgerHQ/ledger-live-desktop/releases"> <a href="https://github.com/LedgerHQ/ledger-live-desktop/releases">
@ -85,6 +87,7 @@ SKIP_GENUINE=1
SKIP_ONBOARDING=1 SKIP_ONBOARDING=1
SHOW_LEGACY_NEW_ACCOUNT=1 SHOW_LEGACY_NEW_ACCOUNT=1
HIGHLIGHT_I18N=1 HIGHLIGHT_I18N=1
EXPERIMENTAL_WS_EXPORT=0
## constants ## constants
GET_CALLS_TIMEOUT=30000 GET_CALLS_TIMEOUT=30000

26
package.json

@ -20,7 +20,7 @@
"test-e2e": "jest test-e2e", "test-e2e": "jest test-e2e",
"test-sync": "bash test-e2e/sync/launch.sh", "test-sync": "bash test-e2e/sync/launch.sh",
"prettier": "prettier --write \"{src,webpack,.storybook,test-e2e}/**/*.{js,json}\"", "prettier": "prettier --write \"{src,webpack,.storybook,test-e2e}/**/*.{js,json}\"",
"ci": "yarn lint && yarn flow && yarn prettier && yarn test", "ci": "yarn check --integrity && ./scripts/check-no-dups.sh && yarn lint && yarn flow && yarn prettier && yarn test",
"storybook": "NODE_ENV=development STORYBOOK_ENV=1 start-storybook -s ./static -p 4444", "storybook": "NODE_ENV=development STORYBOOK_ENV=1 start-storybook -s ./static -p 4444",
"publish-storybook": "bash ./scripts/legacy/publish-storybook.sh", "publish-storybook": "bash ./scripts/legacy/publish-storybook.sh",
"reset-files": "bash ./scripts/legacy/reset-files.sh" "reset-files": "bash ./scripts/legacy/reset-files.sh"
@ -35,13 +35,14 @@
} }
}, },
"dependencies": { "dependencies": {
"@ledgerhq/hw-app-btc": "^4.34.0", "@ledgerhq/errors": "^4.35.1",
"@ledgerhq/hw-app-eth": "^4.32.0", "@ledgerhq/hw-app-btc": "^4.35.0",
"@ledgerhq/hw-app-xrp": "^4.32.0", "@ledgerhq/hw-app-eth": "^4.35.0",
"@ledgerhq/hw-transport": "^4.32.0", "@ledgerhq/hw-app-xrp": "^4.35.0",
"@ledgerhq/hw-transport-node-hid": "^4.32.0", "@ledgerhq/hw-transport": "^4.35.0",
"@ledgerhq/hw-transport-node-hid": "^4.35.0",
"@ledgerhq/ledger-core": "2.0.0-rc.16", "@ledgerhq/ledger-core": "2.0.0-rc.16",
"@ledgerhq/live-common": "4.14.1", "@ledgerhq/live-common": "4.16.0",
"animated": "^0.2.2", "animated": "^0.2.2",
"async": "^2.6.1", "async": "^2.6.1",
"axios": "^0.18.0", "axios": "^0.18.0",
@ -64,18 +65,20 @@
"i18next": "^11.2.2", "i18next": "^11.2.2",
"i18next-node-fs-backend": "^1.0.0", "i18next-node-fs-backend": "^1.0.0",
"invariant": "^2.2.4", "invariant": "^2.2.4",
"ip": "^1.1.5",
"jsqr": "^1.1.1", "jsqr": "^1.1.1",
"lodash": "^4.17.5", "lodash": "^4.17.5",
"lru-cache": "^4.1.3", "lru-cache": "^4.1.3",
"measure-scrollbar": "^1.1.0", "measure-scrollbar": "^1.1.0",
"moment": "^2.22.2", "moment": "^2.22.2",
"openpgp": "^4.2.1",
"qrcode": "^1.2.0", "qrcode": "^1.2.0",
"qrloop": "0.8.1", "qrloop": "0.8.1",
"qs": "^6.5.1", "qs": "^6.5.1",
"raven": "^2.5.0", "raven": "^2.5.0",
"raven-js": "^3.24.2", "raven-js": "^3.24.2",
"react": "^16.6.1", "react": "^16.6.3",
"react-dom": "^16.4.1", "react-dom": "^16.6.3",
"react-i18next": "^7.7.0", "react-i18next": "^7.7.0",
"react-key-handler": "^1.0.1", "react-key-handler": "^1.0.1",
"react-markdown": "^3.3.2", "react-markdown": "^3.3.2",
@ -111,7 +114,7 @@
"winston": "^3.0.0", "winston": "^3.0.0",
"winston-transport": "^4.2.0", "winston-transport": "^4.2.0",
"write-file-atomic": "^2.3.0", "write-file-atomic": "^2.3.0",
"ws": "^5.1.1", "ws": "^6.1.3",
"zxcvbn": "^4.4.2" "zxcvbn": "^4.4.2"
}, },
"devDependencies": { "devDependencies": {
@ -182,7 +185,8 @@
"webpack": "^4.6.0", "webpack": "^4.6.0",
"webpack-bundle-analyzer": "^2.11.1", "webpack-bundle-analyzer": "^2.11.1",
"webpack-cli": "^2.0.14", "webpack-cli": "^2.0.14",
"yaml-loader": "^0.5.0" "yaml-loader": "^0.5.0",
"yarn-deduplicate": "^1.1.1"
}, },
"engines": { "engines": {
"node": ">=8.9.0 <=8.15.0", "node": ">=8.9.0 <=8.15.0",

15
scripts/check-no-dups.sh

@ -0,0 +1,15 @@
#!/bin/bash
yarn-deduplicate -l | grep \@ledgerhq
if [ $? -eq 0 ]; then
echo "Found duplicates in @ledgerhq/* – fix it with yarn-deduplicate"
exit 1
fi
yarn-deduplicate -l | grep \"react
if [ $? -eq 0 ]; then
echo "Found duplicates in some react packages – fix it with yarn-deduplicate"
exit 1
fi

51
scripts/create-release-shasums.sh

@ -0,0 +1,51 @@
#!/bin/env bash
# Fetch release binaries for all platforms
# and produce a .sha512sum file in the current folder
# exit on error
set -e
[[ "$GH_TOKEN" == "" ]] && echo "GH_TOKEN is unset" && exit 1
function main {
ASSETS_FILTER="(AppImage|zip|exe)"
PKG_VER=$(grep version package.json | sed -E 's/.*: "(.*)",/\1/g')
OUTPUT_FILE="ledger-live-desktop-$PKG_VER.sha512sum"
read -p "> release version ($PKG_VER): " -r RELEASE_VERSION
RELEASE_VERSION=${RELEASE_VERSION:-$PKG_VER}
RELEASES=$(do_request "/repos/LedgerHQ/ledger-live-desktop/releases")
printf """
console.log(
(%s).find(r => r.tag_name === 'v%s').assets
.filter(a => a.name.match(/\\.%s$/))
.map(a => a.browser_download_url)
.join('\\\n')
)
""" "$RELEASES" "$RELEASE_VERSION" "$ASSETS_FILTER" >"$TMP_FILE1"
node "$TMP_FILE1" | tee "$TMP_FILE2"
pushd "$TMP_DIR" >/dev/null
while IFS= read -r line ; do
curl -L -O "$line"
done < "$TMP_FILE2"
sha512sum -- * > "$OLDPWD/$OUTPUT_FILE"
popd >/dev/null
}
TMP_DIR=$(mktemp -d)
TMP_FILE1=$(mktemp)
TMP_FILE2=$(mktemp)
function cleanup {
rm -rf "$TMP_FILE1" "$TMP_FILE2" "$TMP_DIR"
}
function do_request {
curl -H "Authorization: token $GH_TOKEN" "https://api.github.com$1"
}
trap cleanup EXIT
main

1
scripts/release.sh

@ -67,6 +67,7 @@ if [[ $(uname) == 'Linux' ]]; then # only run it on one target, to prevent race
"failed to create a draft release" "failed to create a draft release"
fi fi
runJob "rm -rf ./node_modules/.cache" "Removing node modules cache..." "done" "fail"
runJob "yarn compile" "compiling..." "compiled" "failed to compile" "verbose" runJob "yarn compile" "compiling..." "compiled" "failed to compile" "verbose"
if [[ $(uname) == 'Linux' ]]; then if [[ $(uname) == 'Linux' ]]; then

91
src/api/Ethereum.js

@ -1,91 +0,0 @@
// @flow
import type { CryptoCurrency } from '@ledgerhq/live-common/lib/types'
import { BigNumber } from 'bignumber.js'
import { LedgerAPINotAvailable } from '@ledgerhq/errors'
import network from './network'
import { blockchainBaseURL } from './Ledger'
export type Block = { height: number } // TODO more fields actually
export type Tx = {
hash: string,
received_at: string,
nonce: string,
value: number,
gas: number,
gas_price: number,
cumulative_gas_used: number,
gas_used: number,
from: string,
to: string,
input: string,
index: number,
block?: {
hash: string,
height: number,
time: string,
},
confirmations: number,
}
export type API = {
getTransactions: (
address: string,
blockHash: ?string,
) => Promise<{
truncated: boolean,
txs: Tx[],
}>,
getCurrentBlock: () => Promise<Block>,
getAccountNonce: (address: string) => Promise<number>,
broadcastTransaction: (signedTransaction: string) => Promise<string>,
getAccountBalance: (address: string) => Promise<BigNumber>,
}
export const apiForCurrency = (currency: CryptoCurrency): API => {
const baseURL = blockchainBaseURL(currency)
if (!baseURL) {
throw new LedgerAPINotAvailable(`LedgerAPINotAvailable ${currency.id}`, {
currencyName: currency.name,
})
}
return {
async getTransactions(address, blockHash) {
const { data } = await network({
method: 'GET',
url: `${baseURL}/addresses/${address}/transactions`,
params: { blockHash, noToken: 1 },
})
return data
},
async getCurrentBlock() {
const { data } = await network({
method: 'GET',
url: `${baseURL}/blocks/current`,
})
return data
},
async getAccountNonce(address) {
const { data } = await network({
method: 'GET',
url: `${baseURL}/addresses/${address}/nonce`,
})
return data[0].nonce
},
async broadcastTransaction(tx) {
const { data } = await network({
method: 'POST',
url: `${baseURL}/transactions/send`,
data: { tx },
})
return data.result
},
async getAccountBalance(address) {
const { data } = await network({
method: 'GET',
url: `${baseURL}/addresses/${address}/balance`,
})
// FIXME precision lost here. nothing we can do easily
return BigNumber(data[0].balance)
},
}
}

31
src/api/Fees.js

@ -1,31 +0,0 @@
// @flow
import invariant from 'invariant'
import LRU from 'lru-cache'
import type { Currency } from '@ledgerhq/live-common/lib/types'
import { FeeEstimationFailed } from '@ledgerhq/errors'
import { blockchainBaseURL } from './Ledger'
import network from './network'
export type Fees = {
[_: string]: number,
}
const cache = LRU({
maxAge: 5 * 60 * 1000,
})
export const getEstimatedFees = async (currency: Currency): Promise<Fees> => {
const key = currency.id
let promise = cache.get(key)
if (promise) return promise.then(r => r.data)
const baseURL = blockchainBaseURL(currency)
invariant(baseURL, `Fees for ${currency.id} are not supported`)
promise = network({ method: 'GET', url: `${baseURL}/fees` })
cache.set(key, promise)
const { data, status } = await promise
if (status < 200 || status >= 300) cache.del(key)
if (data) {
return data
}
throw new FeeEstimationFailed(`FeeEstimationFailed ${status}`, { httpStatus: status })
}

6
src/api/Ledger.js

@ -1,6 +0,0 @@
// @flow
import type { Currency } from '@ledgerhq/live-common/lib/types'
import { LEDGER_REST_API_BASE } from 'config/constants'
export const blockchainBaseURL = ({ ledgerExplorerId }: Currency): ?string =>
ledgerExplorerId ? `${LEDGER_REST_API_BASE}/blockchain/v2/${ledgerExplorerId}` : null

47
src/api/Ripple.js

@ -1,47 +0,0 @@
// @flow
import logger from 'logger'
import { BigNumber } from 'bignumber.js'
import { RippleAPI } from 'ripple-lib'
import {
parseCurrencyUnit,
getCryptoCurrencyById,
formatCurrencyUnit,
} from '@ledgerhq/live-common/lib/currencies'
const rippleUnit = getCryptoCurrencyById('ripple').units[0]
export const defaultEndpoint = 'wss://s2.ripple.com'
export const apiForEndpointConfig = (endpointConfig: ?string = null) => {
const server = endpointConfig || defaultEndpoint
const api = new RippleAPI({ server })
api.on('error', (errorCode, errorMessage) => {
logger.warn(`Ripple API error: ${errorCode}: ${errorMessage}`)
})
return api
}
export const parseAPIValue = (value: string) => parseCurrencyUnit(rippleUnit, value)
export const parseAPICurrencyObject = ({
currency,
value,
}: {
currency: string,
value: string,
}) => {
if (currency !== 'XRP') {
logger.warn(`RippleJS: attempt to parse unknown currency ${currency}`)
return BigNumber(0)
}
return parseAPIValue(value)
}
export const formatAPICurrencyXRP = (amount: BigNumber) => {
const value = formatCurrencyUnit(rippleUnit, amount, {
showAllDigits: true,
disableRounding: true,
useGrouping: false,
})
return { currency: 'XRP', value }
}

1
src/bridge/BridgeSyncContext.js

@ -100,6 +100,7 @@ class Provider extends Component<BridgeSyncProviderOwnProps, Sync> {
next() next()
}, },
error: error => { error: error => {
logger.critical(error)
this.props.setAccountSyncState(accountId, { pending: false, error }) this.props.setAccountSyncState(accountId, { pending: false, error })
next() next()
}, },

12
src/bridge/EthereumJSBridge.js

@ -22,8 +22,8 @@ import {
} from '@ledgerhq/live-common/lib/account' } from '@ledgerhq/live-common/lib/account'
import type { Account, Operation } from '@ledgerhq/live-common/lib/types' import type { Account, Operation } from '@ledgerhq/live-common/lib/types'
import eip55 from 'eip55' import eip55 from 'eip55'
import { apiForCurrency } from 'api/Ethereum' import { apiForCurrency } from '@ledgerhq/live-common/lib/api/Ethereum'
import type { Tx } from 'api/Ethereum' import type { Tx } from '@ledgerhq/live-common/lib/api/Ethereum'
import getAddressCommand from 'commands/getAddress' import getAddressCommand from 'commands/getAddress'
import signTransactionCommand from 'commands/signTransaction' import signTransactionCommand from 'commands/signTransaction'
import { NotEnoughBalance, FeeNotLoaded, ETHAddressNonEIP } from '@ledgerhq/errors' import { NotEnoughBalance, FeeNotLoaded, ETHAddressNonEIP } from '@ledgerhq/errors'
@ -129,7 +129,7 @@ function isRecipientValid(currency, recipient) {
} }
// Returns a warning if we detect a non-eip address // Returns a warning if we detect a non-eip address
function getRecipientWarning(currency, recipient) { function getRecipientWarning(account, recipient) {
if (!recipient.match(/^0x[0-9a-fA-F]{40}$/)) return null if (!recipient.match(/^0x[0-9a-fA-F]{40}$/)) return null
const slice = recipient.substr(2) const slice = recipient.substr(2)
const isFullUpper = slice === slice.toUpperCase() const isFullUpper = slice === slice.toUpperCase()
@ -420,9 +420,9 @@ const EthereumBridge: WalletBridge<Transaction> = {
pullMoreOperations: () => Promise.resolve(a => a), // NOT IMPLEMENTED pullMoreOperations: () => Promise.resolve(a => a), // NOT IMPLEMENTED
isRecipientValid: (currency, recipient) => Promise.resolve(isRecipientValid(currency, recipient)), isRecipientValid: (account, recipient) => Promise.resolve(isRecipientValid(account, recipient)),
getRecipientWarning: (currency, recipient) => getRecipientWarning: (account, recipient) =>
Promise.resolve(getRecipientWarning(currency, recipient)), Promise.resolve(getRecipientWarning(account, recipient)),
createTransaction: () => ({ createTransaction: () => ({
amount: BigNumber(0), amount: BigNumber(0),

8
src/bridge/LibcoreBridge.js

@ -60,15 +60,15 @@ const EditAdvancedOptions = ({ onChange, value }: EditProps<Transaction>) => (
const recipientValidLRU = LRU({ max: 100 }) const recipientValidLRU = LRU({ max: 100 })
const isRecipientValid = (currency, recipient) => { const isRecipientValid = (account, recipient) => {
const key = `${currency.id}_${recipient}` const key = `${account.currency.id}_${recipient}`
let promise = recipientValidLRU.get(key) let promise = recipientValidLRU.get(key)
if (promise) return promise if (promise) return promise
if (!recipient) return Promise.resolve(false) if (!recipient) return Promise.resolve(false)
promise = libcoreValidAddress promise = libcoreValidAddress
.send({ .send({
address: recipient, address: recipient,
currencyId: currency.id, currencyId: account.currency.id,
}) })
.toPromise() .toPromise()
recipientValidLRU.set(key, promise) recipientValidLRU.set(key, promise)
@ -83,7 +83,7 @@ const getFeesKey = (a, t) =>
}` }`
const getFees = async (a, transaction) => { const getFees = async (a, transaction) => {
const isValid = await isRecipientValid(a.currency, transaction.recipient) const isValid = await isRecipientValid(a, transaction.recipient)
if (!isValid) return null if (!isValid) return null
const key = getFeesKey(a, transaction) const key = getFeesKey(a, transaction)
let promise = feesLRU.get(key) let promise = feesLRU.get(key)

35
src/bridge/RippleJSBridge.js

@ -3,6 +3,7 @@ import invariant from 'invariant'
import { BigNumber } from 'bignumber.js' import { BigNumber } from 'bignumber.js'
import { Observable } from 'rxjs' import { Observable } from 'rxjs'
import React from 'react' import React from 'react'
import { RippleAPI } from 'ripple-lib'
import bs58check from 'ripple-bs58check' import bs58check from 'ripple-bs58check'
import { computeBinaryTransactionHash } from 'ripple-hashes' import { computeBinaryTransactionHash } from 'ripple-hashes'
import throttle from 'lodash/throttle' import throttle from 'lodash/throttle'
@ -26,13 +27,14 @@ import {
parseAPIValue, parseAPIValue,
parseAPICurrencyObject, parseAPICurrencyObject,
formatAPICurrencyXRP, formatAPICurrencyXRP,
} from 'api/Ripple' } from '@ledgerhq/live-common/lib/api/Ripple'
import FeesRippleKind from 'components/FeesField/RippleKind' import FeesRippleKind from 'components/FeesField/RippleKind'
import AdvancedOptionsRippleKind from 'components/AdvancedOptions/RippleKind' import AdvancedOptionsRippleKind from 'components/AdvancedOptions/RippleKind'
import { import {
NotEnoughBalance, NotEnoughBalance,
FeeNotLoaded, FeeNotLoaded,
NotEnoughBalanceBecauseDestinationNotCreated, NotEnoughBalanceBecauseDestinationNotCreated,
InvalidAddressBecauseDestinationIsAlsoSource,
} from '@ledgerhq/errors' } from '@ledgerhq/errors'
import type { WalletBridge, EditProps } from './types' import type { WalletBridge, EditProps } from './types'
@ -63,7 +65,7 @@ const EditAdvancedOptions = ({ onChange, value }: EditProps<Transaction>) => (
) )
async function signAndBroadcast({ a, t, deviceId, isCancelled, onSigned, onOperationBroadcasted }) { async function signAndBroadcast({ a, t, deviceId, isCancelled, onSigned, onOperationBroadcasted }) {
const api = apiForEndpointConfig(a.endpointConfig) const api = apiForEndpointConfig(RippleAPI, a.endpointConfig)
const { fee } = t const { fee } = t
if (!fee) throw new FeeNotLoaded() if (!fee) throw new FeeNotLoaded()
try { try {
@ -135,15 +137,23 @@ async function signAndBroadcast({ a, t, deviceId, isCancelled, onSigned, onOpera
} }
} }
function isRecipientValid(recipient) { function isRecipientValid(account, recipient) {
try { try {
bs58check.decode(recipient) bs58check.decode(recipient)
return true
return !(account && account.freshAddress === recipient)
} catch (e) { } catch (e) {
return false return false
} }
} }
function getRecipientWarning(account, recipient) {
if (account.freshAddress === recipient) {
return new InvalidAddressBecauseDestinationIsAlsoSource()
}
return null
}
function mergeOps(existing: Operation[], newFetched: Operation[]) { function mergeOps(existing: Operation[], newFetched: Operation[]) {
const ids = existing.map(o => o.id) const ids = existing.map(o => o.id)
const all = existing.concat(newFetched.filter(o => !ids.includes(o.id))) const all = existing.concat(newFetched.filter(o => !ids.includes(o.id)))
@ -252,7 +262,7 @@ const getServerInfo = (map => endpointConfig => {
if (!endpointConfig) endpointConfig = '' if (!endpointConfig) endpointConfig = ''
if (map[endpointConfig]) return map[endpointConfig]() if (map[endpointConfig]) return map[endpointConfig]()
const f = throttle(async () => { const f = throttle(async () => {
const api = apiForEndpointConfig(endpointConfig) const api = apiForEndpointConfig(RippleAPI, endpointConfig)
try { try {
await api.connect() await api.connect()
const res = await api.getServerInfo() const res = await api.getServerInfo()
@ -269,8 +279,8 @@ const getServerInfo = (map => endpointConfig => {
})({}) })({})
const recipientIsNew = async (endpointConfig, recipient) => { const recipientIsNew = async (endpointConfig, recipient) => {
if (!isRecipientValid(recipient)) return false if (!isRecipientValid(null, recipient)) return false
const api = apiForEndpointConfig(endpointConfig) const api = apiForEndpointConfig(RippleAPI, endpointConfig)
try { try {
await api.connect() await api.connect()
try { try {
@ -302,7 +312,7 @@ const RippleJSBridge: WalletBridge<Transaction> = {
} }
async function main() { async function main() {
const api = apiForEndpointConfig() const api = apiForEndpointConfig(RippleAPI)
try { try {
await api.connect() await api.connect()
const serverInfo = await getServerInfo() const serverInfo = await getServerInfo()
@ -423,7 +433,7 @@ const RippleJSBridge: WalletBridge<Transaction> = {
} }
async function main() { async function main() {
const api = apiForEndpointConfig(endpointConfig) const api = apiForEndpointConfig(RippleAPI, endpointConfig)
try { try {
await api.connect() await api.connect()
if (finished) return if (finished) return
@ -504,8 +514,9 @@ const RippleJSBridge: WalletBridge<Transaction> = {
pullMoreOperations: () => Promise.resolve(a => a), // FIXME not implemented pullMoreOperations: () => Promise.resolve(a => a), // FIXME not implemented
isRecipientValid: (currency, recipient) => Promise.resolve(isRecipientValid(recipient)), isRecipientValid: (account, recipient) => Promise.resolve(isRecipientValid(account, recipient)),
getRecipientWarning: () => Promise.resolve(null), getRecipientWarning: (account, recipient) =>
Promise.resolve(getRecipientWarning(account, recipient)),
createTransaction: () => ({ createTransaction: () => ({
amount: BigNumber(0), amount: BigNumber(0),
@ -617,7 +628,7 @@ const RippleJSBridge: WalletBridge<Transaction> = {
getDefaultEndpointConfig: () => defaultEndpoint, getDefaultEndpointConfig: () => defaultEndpoint,
validateEndpointConfig: async endpointConfig => { validateEndpointConfig: async endpointConfig => {
const api = apiForEndpointConfig(endpointConfig) const api = apiForEndpointConfig(RippleAPI, endpointConfig)
await api.connect() await api.connect()
}, },
} }

4
src/bridge/index.js

@ -1,5 +1,5 @@
// @flow // @flow
import type { Currency } from '@ledgerhq/live-common/lib/types' import type { CryptoCurrency } from '@ledgerhq/live-common/lib/types'
import invariant from 'invariant' import invariant from 'invariant'
import { USE_MOCK_DATA } from 'config/constants' import { USE_MOCK_DATA } from 'config/constants'
import { WalletBridge } from './types' import { WalletBridge } from './types'
@ -20,7 +20,7 @@ if (USE_MOCK_DATA) {
perFamily.ethereum = mockBridge perFamily.ethereum = mockBridge
perFamily.ripple = mockBridge perFamily.ripple = mockBridge
} }
export const getBridgeForCurrency = (currency: Currency): WalletBridge<any> => { export const getBridgeForCurrency = (currency: CryptoCurrency): WalletBridge<any> => {
const bridge = perFamily[currency.family] const bridge = perFamily[currency.family]
invariant(bridge, `${currency.id} currency is not supported`) invariant(bridge, `${currency.id} currency is not supported`)
return bridge return bridge

2
src/bridge/makeMockBridge.js

@ -128,7 +128,7 @@ function makeMockBridge(opts?: Opts): WalletBridge<*> {
} }
}, },
isRecipientValid: (currency, recipient) => Promise.resolve(recipient.length > 0), isRecipientValid: (account, recipient) => Promise.resolve(recipient.length > 0),
getRecipientWarning: () => Promise.resolve(null), getRecipientWarning: () => Promise.resolve(null),
createTransaction: () => ({ createTransaction: () => ({

8
src/bridge/types.js

@ -2,7 +2,7 @@
import type { Observable } from 'rxjs' import type { Observable } from 'rxjs'
import type { BigNumber } from 'bignumber.js' import type { BigNumber } from 'bignumber.js'
import type { Account, Operation, Currency } from '@ledgerhq/live-common/lib/types' import type { Account, Operation, CryptoCurrency } from '@ledgerhq/live-common/lib/types'
// a WalletBridge is implemented on renderer side. // a WalletBridge is implemented on renderer side.
// this is an abstraction on top of libcore / ethereumjs / ripple js / ... // this is an abstraction on top of libcore / ethereumjs / ripple js / ...
@ -34,7 +34,7 @@ export interface WalletBridge<Transaction> {
// the scan can stop once all accounts are discovered. // the scan can stop once all accounts are discovered.
// the function returns a Subscription and you MUST stop everything if it is unsubscribed. // the function returns a Subscription and you MUST stop everything if it is unsubscribed.
// TODO return Observable // TODO return Observable
scanAccountsOnDevice(currency: Currency, deviceId: DeviceId): Observable<Account>; scanAccountsOnDevice(currency: CryptoCurrency, deviceId: DeviceId): Observable<Account>;
// synchronize an account. meaning updating the account object with latest state. // synchronize an account. meaning updating the account object with latest state.
// function receives the initialAccount object so you can actually know what the user side currently have // function receives the initialAccount object so you can actually know what the user side currently have
@ -52,8 +52,8 @@ export interface WalletBridge<Transaction> {
// count is user's desired number of ops to pull (but implementation can decide to ignore it or not) // count is user's desired number of ops to pull (but implementation can decide to ignore it or not)
pullMoreOperations(initialAccount: Account, count: number): Promise<(Account) => Account>; pullMoreOperations(initialAccount: Account, count: number): Promise<(Account) => Account>;
isRecipientValid(currency: Currency, recipient: string): Promise<boolean>; isRecipientValid(account: Account, recipient: string): Promise<boolean>;
getRecipientWarning(currency: Currency, recipient: string): Promise<?Error>; getRecipientWarning(account: Account, recipient: string): Promise<?Error>;
// Related to send funds: // Related to send funds:

BIN
src/commands/.DS_Store

Binary file not shown.

59
src/commands/autoUpdate.js

@ -0,0 +1,59 @@
// @flow
import { createCommand, Command } from 'helpers/ipc'
import { Observable } from 'rxjs'
// import { UPDATE_CHECK_IGNORE, UPDATE_CHECK_FEED } from 'config/constants'
import { UPDATE_CHECK_IGNORE } from 'config/constants'
// import createElectronAppUpdater from 'main/updater/createElectronAppUpdater'
import type { UpdateStatus } from 'components/Updater/UpdaterContext'
type Input = {}
type Result = {
status: UpdateStatus,
payload?: *,
}
const cmd: Command<Input, Result> = createCommand('main:autoUpdate', () =>
Observable.create(o => {
const { autoUpdater } = require('electron-updater')
const sendStatus = (status, payload) => {
o.next({ status, payload })
}
const handleDownload = async _ => {
try {
sendStatus('checking')
// const appUpdater = await createElectronAppUpdater({
// feedURL: UPDATE_CHECK_FEED,
// updateVersion: info.version,
// })
// await appUpdater.verify()
sendStatus('check-success')
} catch (err) {
// don't throw if the check fail for now. it's a white bullet.
if (UPDATE_CHECK_IGNORE) {
// TODO: track the error
sendStatus('check-success')
} else {
o.error(err)
}
}
}
autoUpdater.on('checking-for-update', () => sendStatus('checking-for-update'))
autoUpdater.on('update-available', info => sendStatus('update-available', info))
autoUpdater.on('update-not-available', info => sendStatus('update-not-available', info))
autoUpdater.on('download-progress', p => sendStatus('download-progress', p))
autoUpdater.on('update-downloaded', handleDownload)
autoUpdater.on('error', err => o.error(err))
autoUpdater.autoInstallOnAppQuit = false
autoUpdater.checkForUpdates()
return () => {}
}),
)
export default cmd

4
src/commands/index.js

@ -4,6 +4,7 @@ import invariant from 'invariant'
import type { Command } from 'helpers/ipc' import type { Command } from 'helpers/ipc'
import debugAppInfosForCurrency from 'commands/debugAppInfosForCurrency' import debugAppInfosForCurrency from 'commands/debugAppInfosForCurrency'
import autoUpdate from 'commands/autoUpdate'
import firmwarePrepare from 'commands/firmwarePrepare' import firmwarePrepare from 'commands/firmwarePrepare'
import firmwareMain from 'commands/firmwareMain' import firmwareMain from 'commands/firmwareMain'
import firmwareRepair from 'commands/firmwareRepair' import firmwareRepair from 'commands/firmwareRepair'
@ -22,6 +23,7 @@ import libcoreSyncAccount from 'commands/libcoreSyncAccount'
import libcoreValidAddress from 'commands/libcoreValidAddress' import libcoreValidAddress from 'commands/libcoreValidAddress'
import listenDevices from 'commands/listenDevices' import listenDevices from 'commands/listenDevices'
import ping from 'commands/ping' import ping from 'commands/ping'
import quitAndInstallElectronUpdate from 'commands/quitAndInstallElectronUpdate'
import signTransaction from 'commands/signTransaction' import signTransaction from 'commands/signTransaction'
import testApdu from 'commands/testApdu' import testApdu from 'commands/testApdu'
import testCrash from 'commands/testCrash' import testCrash from 'commands/testCrash'
@ -29,6 +31,7 @@ import testInterval from 'commands/testInterval'
import uninstallApp from 'commands/uninstallApp' import uninstallApp from 'commands/uninstallApp'
const all: Array<Command<any, any>> = [ const all: Array<Command<any, any>> = [
autoUpdate,
debugAppInfosForCurrency, debugAppInfosForCurrency,
firmwarePrepare, firmwarePrepare,
firmwareMain, firmwareMain,
@ -48,6 +51,7 @@ const all: Array<Command<any, any>> = [
libcoreValidAddress, libcoreValidAddress,
listenDevices, listenDevices,
ping, ping,
quitAndInstallElectronUpdate,
signTransaction, signTransaction,
testApdu, testApdu,
testCrash, testCrash,

2
src/commands/libcoreSignAndBroadcast.js

@ -129,7 +129,7 @@ async function signTransaction({
const hexPreviousTransaction = Buffer.from(rawPreviousTransaction).toString('hex') const hexPreviousTransaction = Buffer.from(rawPreviousTransaction).toString('hex')
const previousTransaction = hwApp.splitTransaction( const previousTransaction = hwApp.splitTransaction(
hexPreviousTransaction, hexPreviousTransaction,
true, // set to true allow both segwit AND non-segwit currency.supportsSegwit,
hasTimestamp, hasTimestamp,
hasExtraData, hasExtraData,
additionals, additionals,

30
src/commands/quitAndInstallElectronUpdate.js

@ -0,0 +1,30 @@
// @flow
import { createCommand, Command } from 'helpers/ipc'
import { Observable } from 'rxjs'
type Input = void
type Result = void
const cmd: Command<Input, Result> = createCommand('main:quitAndInstallElectronUpdate', () =>
Observable.create(o => {
const { app, BrowserWindow } = require('electron')
const { autoUpdater } = require('electron-updater')
const browserWindows = BrowserWindow.getAllWindows()
// Fixes quitAndInstall not quitting on macOS, as suggested on
// https://github.com/electron-userland/electron-builder/issues/1604#issuecomment-306709572
app.removeAllListeners('window-all-closed')
browserWindows.forEach(browserWindow => {
browserWindow.removeAllListeners('close')
})
// couldn't find a way to catch if fail ¯\_(ツ)_/¯
autoUpdater.quitAndInstall(false)
o.complete()
return () => {}
}),
)
export default cmd

17
src/components/App.js

@ -16,6 +16,7 @@ import ThrowBlock from 'components/ThrowBlock'
import Default from 'components/layout/Default' import Default from 'components/layout/Default'
import CounterValues from 'helpers/countervalues' import CounterValues from 'helpers/countervalues'
import { BridgeSyncProvider } from 'bridge/BridgeSyncContext' import { BridgeSyncProvider } from 'bridge/BridgeSyncContext'
import { UpdaterProvider } from 'components/Updater/UpdaterContext'
const App = ({ const App = ({
store, store,
@ -31,13 +32,15 @@ const App = ({
<CounterValues.PollingProvider> <CounterValues.PollingProvider>
<I18nextProvider i18n={i18n} initialLanguage={language}> <I18nextProvider i18n={i18n} initialLanguage={language}>
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<ThrowBlock> <UpdaterProvider>
<ConnectedRouter history={history}> <ThrowBlock>
<Switch> <ConnectedRouter history={history}>
<Route component={Default} /> <Switch>
</Switch> <Route component={Default} />
</ConnectedRouter> </Switch>
</ThrowBlock> </ConnectedRouter>
</ThrowBlock>
</UpdaterProvider>
</ThemeProvider> </ThemeProvider>
</I18nextProvider> </I18nextProvider>
</CounterValues.PollingProvider> </CounterValues.PollingProvider>

4
src/components/CounterValue/index.js

@ -3,7 +3,7 @@
import type { BigNumber } from 'bignumber.js' import type { BigNumber } from 'bignumber.js'
import React, { PureComponent } from 'react' import React, { PureComponent } from 'react'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import type { Currency } from '@ledgerhq/live-common/lib/types' import type { CryptoCurrency, Currency } from '@ledgerhq/live-common/lib/types'
import { import {
counterValueCurrencySelector, counterValueCurrencySelector,
@ -19,7 +19,7 @@ import type { State } from 'reducers'
type OwnProps = { type OwnProps = {
// wich market to query // wich market to query
currency: Currency, currency: CryptoCurrency,
// when? if not given: take latest // when? if not given: take latest
date?: Date, date?: Date,

4
src/components/CurrenciesStatusBanner.js

@ -6,7 +6,7 @@ import { translate } from 'react-i18next'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { createStructuredSelector } from 'reselect' import { createStructuredSelector } from 'reselect'
import styled from 'styled-components' import styled from 'styled-components'
import type { Currency } from '@ledgerhq/live-common/lib/types' import type { CryptoCurrency } from '@ledgerhq/live-common/lib/types'
import { colors } from 'styles/theme' import { colors } from 'styles/theme'
import { openURL } from 'helpers/linking' import { openURL } from 'helpers/linking'
@ -59,7 +59,7 @@ const CloseIcon = (props: *) => (
) )
type Props = { type Props = {
accountsCurrencies: Currency[], accountsCurrencies: CryptoCurrency[],
dismissedBanners: string[], dismissedBanners: string[],
dismissBanner: string => void, dismissBanner: string => void,
currenciesStatus: CurrencyStatus[], currenciesStatus: CurrencyStatus[],

3
src/components/CurrentAddress/index.js

@ -145,8 +145,11 @@ class CurrentAddress extends PureComponent<Props, { copyFeedback: boolean }> {
componentWillUnmount() { componentWillUnmount() {
if (this._timeout) clearTimeout(this._timeout) if (this._timeout) clearTimeout(this._timeout)
this._isUnmounted = true
} }
_isUnmounted = false
renderCopy = copy => { renderCopy = copy => {
const { t } = this.props const { t } = this.props
return ( return (

39
src/components/DashboardPage/index.js

@ -3,6 +3,7 @@
import React, { PureComponent, Fragment } from 'react' import React, { PureComponent, Fragment } from 'react'
import uniq from 'lodash/uniq' import uniq from 'lodash/uniq'
import { compose } from 'redux' import { compose } from 'redux'
import IconNanoX from 'icons/device/NanoX'
import { translate } from 'react-i18next' import { translate } from 'react-i18next'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { push } from 'react-router-redux' import { push } from 'react-router-redux'
@ -24,17 +25,21 @@ import { saveSettings } from 'actions/settings'
import TrackPage from 'analytics/TrackPage' import TrackPage from 'analytics/TrackPage'
import RefreshAccountsOrdering from 'components/RefreshAccountsOrdering' import RefreshAccountsOrdering from 'components/RefreshAccountsOrdering'
import UpdateNotifier from 'components/UpdateNotifier' import UpdateBanner from 'components/Updater/Banner'
import BalanceInfos from 'components/BalanceSummary/BalanceInfos' import BalanceInfos from 'components/BalanceSummary/BalanceInfos'
import BalanceSummary from 'components/BalanceSummary' import BalanceSummary from 'components/BalanceSummary'
import Box from 'components/base/Box' import Box from 'components/base/Box'
import PillsDaysCount from 'components/PillsDaysCount' import PillsDaysCount from 'components/PillsDaysCount'
import OperationsList from 'components/OperationsList' import OperationsList from 'components/OperationsList'
import StickyBackToTop from 'components/StickyBackToTop' import StickyBackToTop from 'components/StickyBackToTop'
import styled from 'styled-components'
import { openURL } from 'helpers/linking'
import EmptyState from './EmptyState' import EmptyState from './EmptyState'
import CurrentGreetings from './CurrentGreetings' import CurrentGreetings from './CurrentGreetings'
import SummaryDesc from './SummaryDesc' import SummaryDesc from './SummaryDesc'
import AccountCardList from './AccountCardList' import AccountCardList from './AccountCardList'
import TopBanner, { FakeLink } from '../TopBanner'
import { urls } from '../../config/urls'
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
accounts: accountsSelector, accounts: accountsSelector,
@ -84,7 +89,24 @@ class DashboardPage extends PureComponent<Props> {
return ( return (
<Fragment> <Fragment>
<UpdateNotifier /> <TopBannerContainer>
<UpdateBanner />
<TopBanner
content={{
message: t('banners.promoteMobile'),
Icon: IconNanoX,
right: (
<FakeLink onClick={() => openURL(urls.promoNanoX)}>
{t('common.learnMore')}
</FakeLink>
),
}}
status={'dark'}
bannerId={'promoNanoX'}
dismissable
/>
<SeparatorBar />
</TopBannerContainer>
<RefreshAccountsOrdering onMount /> <RefreshAccountsOrdering onMount />
<TrackPage <TrackPage
category="Portfolio" category="Portfolio"
@ -143,6 +165,19 @@ class DashboardPage extends PureComponent<Props> {
) )
} }
} }
// This forces only one visible top banner at a time
const TopBannerContainer = styled.div`
& > *:not(:first-child) {
display: none;
}
`
// If no banners are present, the SeparatorBar appears
const SeparatorBar = styled.div`
height: 1px;
border-bottom: 1px solid ${p => p.theme.colors.fog};
margin-bottom: 15px;
margin-top: -20px;
`
export default compose( export default compose(
connect( connect(

9
src/components/DevToolsPage/AccountImporter.js

@ -6,7 +6,7 @@ import React, { PureComponent, Fragment } from 'react'
import invariant from 'invariant' import invariant from 'invariant'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import type { Currency, Account, DerivationMode } from '@ledgerhq/live-common/lib/types' import type { CryptoCurrency, Account, DerivationMode } from '@ledgerhq/live-common/lib/types'
import { decodeAccount } from 'reducers/accounts' import { decodeAccount } from 'reducers/accounts'
import { addAccount } from 'actions/accounts' import { addAccount } from 'actions/accounts'
@ -37,7 +37,7 @@ type Props = {
type ImportableAccountType = { type ImportableAccountType = {
name: string, name: string,
currency: Currency, currency: CryptoCurrency,
derivationMode: DerivationMode, derivationMode: DerivationMode,
xpub: string, xpub: string,
} }
@ -47,7 +47,7 @@ type State = {
importableAccounts: ImportableAccountType[], importableAccounts: ImportableAccountType[],
currency: ?Currency, currency: ?CryptoCurrency,
xpub: string, xpub: string,
name: string, name: string,
isSegwit: boolean, isSegwit: boolean,
@ -72,7 +72,7 @@ const INITIAL_STATE = {
class AccountImporter extends PureComponent<Props, State> { class AccountImporter extends PureComponent<Props, State> {
state = INITIAL_STATE state = INITIAL_STATE
onChangeCurrency = currency => { onChangeCurrency = (currency: CryptoCurrency) => {
if (currency.family !== 'bitcoin') return if (currency.family !== 'bitcoin') return
this.setState({ this.setState({
currency, currency,
@ -119,6 +119,7 @@ class AccountImporter extends PureComponent<Props, State> {
addToScan = () => { addToScan = () => {
const { xpub, currency, isSegwit, isUnsplit, name } = this.state const { xpub, currency, isSegwit, isUnsplit, name } = this.state
if (!currency) return
const derivationMode = isSegwit const derivationMode = isSegwit
? isUnsplit ? isUnsplit
? 'segwit_unsplit' ? 'segwit_unsplit'

2
src/components/FeesField/BitcoinKind.js

@ -11,7 +11,7 @@ import type { T } from 'types/common'
import { FeeNotLoaded } from '@ledgerhq/errors' import { FeeNotLoaded } from '@ledgerhq/errors'
import InputCurrency from 'components/base/InputCurrency' import InputCurrency from 'components/base/InputCurrency'
import Select from 'components/base/Select' import Select from 'components/base/Select'
import type { Fees } from 'api/Fees' import type { Fees } from '@ledgerhq/live-common/lib/api/Fees'
import WithFeesAPI from '../WithFeesAPI' import WithFeesAPI from '../WithFeesAPI'
import GenericContainer from './GenericContainer' import GenericContainer from './GenericContainer'
import Box from '../base/Box' import Box from '../base/Box'

2
src/components/FeesField/EthereumKind.js

@ -6,7 +6,7 @@ import type { Account } from '@ledgerhq/live-common/lib/types'
import { FeeNotLoaded } from '@ledgerhq/errors' import { FeeNotLoaded } from '@ledgerhq/errors'
import InputCurrency from 'components/base/InputCurrency' import InputCurrency from 'components/base/InputCurrency'
import type { Fees } from 'api/Fees' import type { Fees } from '@ledgerhq/live-common/lib/api/Fees'
import WithFeesAPI from '../WithFeesAPI' import WithFeesAPI from '../WithFeesAPI'
import GenericContainer from './GenericContainer' import GenericContainer from './GenericContainer'

5
src/components/FeesField/RippleKind.js

@ -1,9 +1,10 @@
// @flow // @flow
import React, { Component } from 'react' import React, { Component } from 'react'
import { RippleAPI } from 'ripple-lib'
import type { BigNumber } from 'bignumber.js' import type { BigNumber } from 'bignumber.js'
import type { Account } from '@ledgerhq/live-common/lib/types' import type { Account } from '@ledgerhq/live-common/lib/types'
import { apiForEndpointConfig, parseAPIValue } from 'api/Ripple' import { apiForEndpointConfig, parseAPIValue } from '@ledgerhq/live-common/lib/api/Ripple'
import { FeeNotLoaded } from '@ledgerhq/errors' import { FeeNotLoaded } from '@ledgerhq/errors'
import InputCurrency from 'components/base/InputCurrency' import InputCurrency from 'components/base/InputCurrency'
import GenericContainer from './GenericContainer' import GenericContainer from './GenericContainer'
@ -30,7 +31,7 @@ class FeesField extends Component<Props, State> {
} }
syncId = 0 syncId = 0
async sync() { async sync() {
const api = apiForEndpointConfig(this.props.account.endpointConfig) const api = apiForEndpointConfig(RippleAPI, this.props.account.endpointConfig)
const syncId = ++this.syncId const syncId = ++this.syncId
try { try {
await api.connect() await api.connect()

13
src/components/GenuineCheckModal.js

@ -5,7 +5,7 @@ import { translate } from 'react-i18next'
import type { T } from 'types/common' import type { T } from 'types/common'
import Modal, { ModalBody, ModalTitle, ModalContent } from 'components/base/Modal' import Modal, { ModalBody } from 'components/base/Modal'
import GenuineCheck from 'components/GenuineCheck' import GenuineCheck from 'components/GenuineCheck'
type Props = { type Props = {
@ -19,12 +19,13 @@ class GenuineCheckModal extends PureComponent<Props> {
renderBody = ({ onClose }) => { renderBody = ({ onClose }) => {
const { t, onSuccess, onFail, onUnavailable } = this.props const { t, onSuccess, onFail, onUnavailable } = this.props
return ( return (
<ModalBody onClose={onClose}> <ModalBody
<ModalTitle>{t('genuinecheck.modal.title')}</ModalTitle> onClose={onClose}
<ModalContent> title={t('genuinecheck.modal.title')}
render={() => (
<GenuineCheck onSuccess={onSuccess} onFail={onFail} onUnavailable={onUnavailable} /> <GenuineCheck onSuccess={onSuccess} onFail={onFail} onUnavailable={onUnavailable} />
</ModalContent> )}
</ModalBody> />
) )
} }

4
src/components/IsUnlocked.js

@ -23,8 +23,8 @@ import Box from 'components/base/Box'
import InputPassword from 'components/base/InputPassword' import InputPassword from 'components/base/InputPassword'
import LedgerLiveLogo from 'components/base/LedgerLiveLogo' import LedgerLiveLogo from 'components/base/LedgerLiveLogo'
import IconArrowRight from 'icons/ArrowRight' import IconArrowRight from 'icons/ArrowRight'
import Button from './base/Button/index' import Button from 'components/base/Button/index'
import ConfirmModal from './base/Modal/ConfirmModal' import ConfirmModal from 'components/base/Modal/ConfirmModal'
type InputValue = { type InputValue = {
password: string, password: string,

9
src/components/MainSideBar/index.js

@ -11,20 +11,19 @@ import type { Location } from 'react-router'
import type { Account } from '@ledgerhq/live-common/lib/types' import type { Account } from '@ledgerhq/live-common/lib/types'
import type { T } from 'types/common' import type { T } from 'types/common'
import type { UpdateStatus } from 'reducers/update'
import { MODAL_RECEIVE, MODAL_SEND, MODAL_ADD_ACCOUNTS } from 'config/constants' import { MODAL_RECEIVE, MODAL_SEND, MODAL_ADD_ACCOUNTS } from 'config/constants'
import { i } from 'helpers/staticPath' import { i } from 'helpers/staticPath'
import { accountsSelector } from 'reducers/accounts' import { accountsSelector } from 'reducers/accounts'
import { openModal } from 'reducers/modals' import { openModal } from 'reducers/modals'
import { getUpdateStatus } from 'reducers/update'
import { developerModeSelector } from 'reducers/settings' import { developerModeSelector } from 'reducers/settings'
import { SideBarList, SideBarListItem } from 'components/base/SideBar' import { SideBarList, SideBarListItem } from 'components/base/SideBar'
import Box from 'components/base/Box' import Box from 'components/base/Box'
import GrowScroll from 'components/base/GrowScroll' import GrowScroll from 'components/base/GrowScroll'
import Space from 'components/base/Space' import Space from 'components/base/Space'
import UpdateDot from 'components/Updater/UpdateDot'
import IconManager from 'icons/Manager' import IconManager from 'icons/Manager'
import IconPieChart from 'icons/PieChart' import IconPieChart from 'icons/PieChart'
@ -39,7 +38,6 @@ import KeyboardContent from '../KeyboardContent'
const mapStateToProps = state => ({ const mapStateToProps = state => ({
accounts: accountsSelector(state), accounts: accountsSelector(state),
updateStatus: getUpdateStatus(state),
developerMode: developerModeSelector(state), developerMode: developerModeSelector(state),
}) })
@ -54,7 +52,6 @@ type Props = {
location: Location, location: Location,
push: string => void, push: string => void,
openModal: string => void, openModal: string => void,
updateStatus: UpdateStatus,
developerMode: boolean, developerMode: boolean,
} }
@ -96,7 +93,7 @@ class MainSideBar extends PureComponent<Props> {
handleOpenImportModal = () => this.props.openModal(MODAL_ADD_ACCOUNTS) handleOpenImportModal = () => this.props.openModal(MODAL_ADD_ACCOUNTS)
render() { render() {
const { t, accounts, location, updateStatus, developerMode } = this.props const { t, accounts, location, developerMode } = this.props
const { pathname } = location const { pathname } = location
const addAccountButton = ( const addAccountButton = (
@ -122,7 +119,7 @@ class MainSideBar extends PureComponent<Props> {
iconActiveColor="wallet" iconActiveColor="wallet"
onClick={this.handleClickDashboard} onClick={this.handleClickDashboard}
isActive={pathname === '/'} isActive={pathname === '/'}
hasNotif={updateStatus === 'downloaded'} NotifComponent={UpdateDot}
/> />
<SideBarListItem <SideBarListItem
label={t('send.title')} label={t('send.title')}

284
src/components/ManagerPage/AppsList.js

@ -1,7 +1,7 @@
// @flow // @flow
/* eslint-disable react/jsx-no-literals */ // FIXME /* eslint-disable react/jsx-no-literals */ // FIXME
import React, { PureComponent, Fragment } from 'react' import React, { PureComponent } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { translate } from 'react-i18next' import { translate } from 'react-i18next'
import { connect } from 'react-redux' import { connect } from 'react-redux'
@ -15,8 +15,7 @@ import { developerModeSelector } from 'reducers/settings'
import installApp from 'commands/installApp' import installApp from 'commands/installApp'
import uninstallApp from 'commands/uninstallApp' import uninstallApp from 'commands/uninstallApp'
import Box from 'components/base/Box' import Box from 'components/base/Box'
import Space from 'components/base/Space' import Modal from 'components/base/Modal'
import Modal, { ModalBody, ModalFooter, ModalTitle, ModalContent } from 'components/base/Modal'
import Tooltip from 'components/base/Tooltip' import Tooltip from 'components/base/Tooltip'
import Text from 'components/base/Text' import Text from 'components/base/Text'
import ProgressBar from 'components/ProgressBar' import ProgressBar from 'components/ProgressBar'
@ -32,6 +31,7 @@ import CheckCircle from 'icons/CheckCircle'
import { FreezeDeviceChangeEvents } from './HookDeviceChange' import { FreezeDeviceChangeEvents } from './HookDeviceChange'
import ManagerApp, { Container as FakeManagerAppContainer } from './ManagerApp' import ManagerApp, { Container as FakeManagerAppContainer } from './ManagerApp'
import AppSearchBar from './AppSearchBar' import AppSearchBar from './AppSearchBar'
import ModalBody from '../base/Modal/ModalBody'
const mapStateToProps = state => ({ const mapStateToProps = state => ({
isDevMode: developerModeSelector(state), isDevMode: developerModeSelector(state),
@ -39,15 +39,35 @@ const mapStateToProps = state => ({
const List = styled(Box).attrs({ const List = styled(Box).attrs({
horizontal: true, horizontal: true,
m: -3,
})` })`
flex-wrap: wrap; flex-wrap: wrap;
> * {
width: calc(50% - 10px);
margin-bottom: 20px;
&:nth-child(even) {
margin-left: 20px;
}
@media (max-width: 1000px) {
width: 100%;
&:nth-child(even) {
margin-left: 0;
}
}
}
` `
const ICONS_FALLBACK = { const ICONS_FALLBACK = {
bitcoin_testnet: 'bitcoin', bitcoin_testnet: 'bitcoin',
} }
const CATALOG_INFO_ICON = (
<Box color="grey">
<IconInfoCircle size={12} />
</Box>
)
type Status = 'loading' | 'idle' | 'busy' | 'success' | 'error' type Status = 'loading' | 'idle' | 'busy' | 'success' | 'error'
type Mode = 'home' | 'installing' | 'uninstalling' type Mode = 'home' | 'installing' | 'uninstalling'
@ -79,6 +99,20 @@ const LoadingApp = () => (
const loadingApp = <LoadingApp /> const loadingApp = <LoadingApp />
const FAKE_LIST = (
<List>
{loadingApp}
{loadingApp}
{loadingApp}
{loadingApp}
{loadingApp}
{loadingApp}
{loadingApp}
{loadingApp}
{loadingApp}
</List>
)
class AppsList extends PureComponent<Props, State> { class AppsList extends PureComponent<Props, State> {
state = { state = {
status: 'loading', status: 'loading',
@ -150,108 +184,103 @@ class AppsList extends PureComponent<Props, State> {
handleCloseModal = () => this.setState({ status: 'idle', mode: 'home' }) handleCloseModal = () => this.setState({ status: 'idle', mode: 'home' })
renderModal = () => { renderBody = () => {
const { t } = this.props const { t } = this.props
const { app, status, error, mode, progress } = this.state const { app, status, error, mode, progress } = this.state
return (
<Modal return ['busy', 'idle'].includes(status) ? (
isOpened={status !== 'idle' && status !== 'loading'} <Box grow align="center" justify="center">
render={() => ( {mode === 'installing' ? (
<ModalBody align="center" justify="center" style={{ height: 300 }}> <Box color="grey" grow align="center" mb={5}>
<FreezeDeviceChangeEvents /> <Update size={30} />
{status === 'busy' || status === 'idle' ? ( </Box>
<Fragment> ) : (
<ModalTitle> <Box color="grey" grow align="center" mb={5}>
{mode === 'installing' ? ( <Trash size={30} />
<Box color="grey"> </Box>
<Update size={30} />
</Box>
) : (
<Box color="grey">
<Trash size={30} />
</Box>
)}
</ModalTitle>
<ModalContent>
<Text ff="Museo Sans|Regular" fontSize={6} color="dark">
{t(`manager.apps.${mode}`, { app })}
</Text>
<Box mt={6}>
<ProgressBar width={150} progress={progress} />
</Box>
</ModalContent>
</Fragment>
) : status === 'error' ? (
<Fragment>
<TrackPage
category="Manager"
name="Error Modal"
error={error && error.name}
app={app}
/>
<ModalContent grow align="center" justify="center" mt={5}>
<Box color="alertRed">
<ExclamationCircleThin size={44} />
</Box>
<Box
color="dark"
mt={4}
fontSize={6}
ff="Museo Sans|Regular"
textAlign="center"
style={{ maxWidth: 350 }}
>
<TranslatedError error={error} field="title" />
</Box>
<Box
color="graphite"
mt={2}
fontSize={4}
ff="Open Sans"
textAlign="center"
style={{ maxWidth: 350 }}
>
<TranslatedError error={error} field="description" />
</Box>
</ModalContent>
<ModalFooter horizontal justifyContent="flex-end" style={{ width: '100%' }}>
<Button primary onClick={this.handleCloseModal}>
{t('common.close')}
</Button>
</ModalFooter>
</Fragment>
) : status === 'success' ? (
<Fragment>
<ModalContent grow align="center" justify="center" mt={5}>
<Box color="positiveGreen">
<CheckCircle size={44} />
</Box>
<Box
color="dark"
mt={4}
fontSize={6}
ff="Museo Sans|Regular"
textAlign="center"
style={{ maxWidth: 350 }}
>
{t(
`manager.apps.${
mode === 'installing' ? 'installSuccess' : 'uninstallSuccess'
}`,
{ app },
)}
</Box>
</ModalContent>
<ModalFooter horizontal justifyContent="flex-end" style={{ width: '100%' }}>
<Button primary onClick={this.handleCloseModal}>
{t('common.close')}
</Button>
</ModalFooter>
</Fragment>
) : null}
</ModalBody>
)} )}
/> <Text ff="Museo Sans|Regular" fontSize={6} color="dark">
{t(`manager.apps.${mode}`, { app })}
</Text>
<Box mt={6}>
<ProgressBar width={150} progress={progress} />
</Box>
</Box>
) : status === 'error' ? (
<Box>
<TrackPage category="Manager" name="Error Modal" error={error && error.name} app={app} />
<Box grow align="center" justify="center" mt={5}>
<Box color="alertRed">
<ExclamationCircleThin size={44} />
</Box>
<Box
color="dark"
mt={4}
fontSize={6}
ff="Museo Sans|Regular"
textAlign="center"
style={{ maxWidth: 350 }}
>
<TranslatedError error={error} field="title" />
</Box>
<Box
color="graphite"
mt={2}
fontSize={4}
ff="Open Sans"
textAlign="center"
style={{ maxWidth: 350 }}
>
<TranslatedError error={error} field="description" />
</Box>
</Box>
</Box>
) : status === 'success' ? (
<Box grow align="center" justify="center" mt={5}>
<Box color="positiveGreen">
<CheckCircle size={44} />
</Box>
<Box
color="dark"
mt={4}
fontSize={6}
ff="Museo Sans|Regular"
textAlign="center"
style={{ maxWidth: 350 }}
>
{t(`manager.apps.${mode === 'installing' ? 'installSuccess' : 'uninstallSuccess'}`, {
app,
})}
</Box>
</Box>
) : null
}
renderFooter = () => {
const { t } = this.props
return (
<Box horizontal justifyContent="flex-end" style={{ width: '100%' }}>
<Button primary onClick={this.handleCloseModal}>
{t('common.close')}
</Button>
</Box>
)
}
renderModal = () => {
const { status } = this.state
return (
<Modal isOpened={status !== 'idle' && status !== 'loading'} centered>
<ModalBody
align="center"
justify="center"
title={''}
render={this.renderBody}
renderFooter={['error', 'success'].includes(status) ? this.renderFooter : undefined}
>
<FreezeDeviceChangeEvents />
</ModalBody>
</Modal>
) )
} }
@ -276,47 +305,30 @@ class AppsList extends PureComponent<Props, State> {
)} )}
</AppSearchBar> </AppSearchBar>
{this.renderModal()} {this.renderModal()}
{!appsLoaded && ( {!appsLoaded && FAKE_LIST}
<Fragment> </Box>
<Space of={30} /> )
<List> }
{loadingApp}
{loadingApp} renderTooltip = () => {
{loadingApp} const { t } = this.props
{loadingApp} return (
{loadingApp} <Box ff="Open Sans|SemiBold" fontSize={2}>
{loadingApp} {t('manager.apps.help')}
{loadingApp}
{loadingApp}
{loadingApp}
</List>
</Fragment>
)}
</Box> </Box>
) )
} }
render() { render() {
const { t } = this.props const { t } = this.props
return ( return (
<Box flow={6}> <Box>
<Box> <Box mb={4} color="dark" ff="Museo Sans" fontSize={5} flow={2} horizontal align="center">
<Box mb={4} color="dark" ff="Museo Sans" fontSize={5} flow={2} horizontal align="center"> <span>{t('manager.apps.all')}</span>
<span style={{ lineHeight: 1 }}>{t('manager.apps.all')}</span> <Tooltip render={this.renderTooltip}>{CATALOG_INFO_ICON}</Tooltip>
<Tooltip
render={() => (
<Box ff="Open Sans|SemiBold" fontSize={2}>
{t('manager.apps.help')}
</Box>
)}
>
<Box color="grey">
<IconInfoCircle size={12} />
</Box>
</Tooltip>
</Box>
{this.renderList()}
</Box> </Box>
{this.renderList()}
</Box> </Box>
) )
} }

3
src/components/ManagerPage/ManagerApp.js

@ -14,15 +14,12 @@ import Button from 'components/base/Button'
export const Container = styled(Box).attrs({ export const Container = styled(Box).attrs({
horizontal: true, horizontal: true,
my: 2,
mx: 3,
p: 4, p: 4,
bg: 'white', bg: 'white',
boxShadow: p => (p.noShadow ? -1 : 0), boxShadow: p => (p.noShadow ? -1 : 0),
borderRadius: 4, borderRadius: 4,
flow: 2, flow: 2,
})` })`
width: calc(50% - 30px);
line-height: normal; line-height: normal;
` `

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

@ -31,6 +31,8 @@ export function GenuineCheckUnavailableFooter({
</Button> </Button>
<Box horizontal ml="auto"> <Box horizontal ml="auto">
<Button <Button
outline
outlineColor="alertRed"
disabled={false} disabled={false}
event="Onboarding Skip Genuine Check" event="Onboarding Skip Genuine Check"
onClick={() => nextStep()} onClick={() => nextStep()}
@ -38,9 +40,6 @@ export function GenuineCheckUnavailableFooter({
> >
{t('common.skipThisStep')} {t('common.skipThisStep')}
</Button> </Button>
<Button onClick={nextStep} disabled primary>
{t('common.continue')}
</Button>
</Box> </Box>
</OnboardingFooterWrapper> </OnboardingFooterWrapper>
) )

4
src/components/OperationsList/AccountCell.js

@ -3,7 +3,7 @@
import React, { PureComponent } from 'react' import React, { PureComponent } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { getCryptoCurrencyIcon } from '@ledgerhq/live-common/lib/react' import { getCryptoCurrencyIcon } from '@ledgerhq/live-common/lib/react'
import type { Currency } from '@ledgerhq/live-common/lib/types' import type { CryptoCurrency } from '@ledgerhq/live-common/lib/types'
import Box from 'components/base/Box' import Box from 'components/base/Box'
const Cell = styled(Box).attrs({ const Cell = styled(Box).attrs({
@ -17,7 +17,7 @@ const Cell = styled(Box).attrs({
` `
type Props = { type Props = {
currency: Currency, currency: CryptoCurrency,
accountName: string, accountName: string,
} }

4
src/components/OperationsList/AmountCell.js

@ -3,7 +3,7 @@
import React, { PureComponent } from 'react' import React, { PureComponent } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { getOperationAmountNumber } from '@ledgerhq/live-common/lib/operation' import { getOperationAmountNumber } from '@ledgerhq/live-common/lib/operation'
import type { Currency, Unit, Operation } from '@ledgerhq/live-common/lib/types' import type { CryptoCurrency, Unit, Operation } from '@ledgerhq/live-common/lib/types'
import Box from 'components/base/Box' import Box from 'components/base/Box'
import CounterValue from 'components/CounterValue' import CounterValue from 'components/CounterValue'
import FormattedVal from 'components/base/FormattedVal' import FormattedVal from 'components/base/FormattedVal'
@ -18,7 +18,7 @@ const Cell = styled(Box).attrs({
type Props = { type Props = {
operation: Operation, operation: Operation,
currency: Currency, currency: CryptoCurrency,
unit: Unit, unit: Unit,
} }

2
src/components/SelectExchange.js

@ -87,7 +87,7 @@ class SelectExchange extends Component<
this.setState({ exchanges, isLoading: false }) this.setState({ exchanges, isLoading: false })
} }
} catch (error) { } catch (error) {
logger.error(error) logger.critical(error)
if (!this._unmounted && this._loadId === _loadId) { if (!this._unmounted && this._loadId === _loadId) {
this.setState({ error, isLoading: false }) this.setState({ error, isLoading: false })
} }

3
src/components/SettingsPage/CleanButton.js

@ -7,7 +7,7 @@ import logger from 'logger'
import type { T } from 'types/common' import type { T } from 'types/common'
import { cleanAccountsCache } from 'actions/accounts' import { cleanAccountsCache } from 'actions/accounts'
import Button from 'components/base/Button' import Button from 'components/base/Button'
import { ConfirmModal } from 'components/base/Modal' import ConfirmModal from 'components/base/Modal/ConfirmModal'
import { softReset } from 'helpers/reset' import { softReset } from 'helpers/reset'
import ResetFallbackModal from './ResetFallbackModal' import ResetFallbackModal from './ResetFallbackModal'
@ -60,6 +60,7 @@ class CleanButton extends PureComponent<Props, State> {
<ConfirmModal <ConfirmModal
analyticsName="CleanCache" analyticsName="CleanCache"
centered
isOpened={opened} isOpened={opened}
onClose={this.close} onClose={this.close}
onReject={this.close} onReject={this.close}

2
src/components/SettingsPage/CounterValueSelect.js

@ -15,7 +15,7 @@ const fiats = listFiatCurrencies()
// For now we take first unit, in the future we'll need to figure out something else // For now we take first unit, in the future we'll need to figure out something else
.map(fiat => ({ .map(fiat => ({
value: fiat.code, value: fiat.code,
label: `${fiat.name} - ${fiat.code}${fiat.symbol ? ` (${fiat.symbol})` : ''}`, label: `${fiat.name} - ${fiat.code}`,
fiat, fiat,
})) }))

67
src/components/SettingsPage/DisablePasswordModal.js

@ -8,7 +8,8 @@ import Box from 'components/base/Box'
import Button from 'components/base/Button' import Button from 'components/base/Button'
import InputPassword from 'components/base/InputPassword' import InputPassword from 'components/base/InputPassword'
import Label from 'components/base/Label' import Label from 'components/base/Label'
import { Modal, ModalContent, ModalBody, ModalTitle, ModalFooter } from 'components/base/Modal' import Modal from 'components/base/Modal'
import ModalBody from 'components/base/Modal/ModalBody'
import type { T } from 'types/common' import type { T } from 'types/common'
@ -61,37 +62,33 @@ class DisablePasswordModal extends PureComponent<Props, State> {
const { t, onClose, ...props } = this.props const { t, onClose, ...props } = this.props
const { currentPassword, incorrectPassword } = this.state const { currentPassword, incorrectPassword } = this.state
return ( return (
<Modal <Modal {...props} centered onHide={this.handleReset} onClose={onClose}>
{...props} <form onSubmit={this.disablePassword}>
onHide={this.handleReset} <ModalBody
onClose={onClose} onClose={onClose}
render={({ onClose }) => ( title={t('password.disablePassword.title')}
<form onSubmit={this.disablePassword}> render={() => (
<ModalBody onClose={onClose}> <Box ff="Open Sans" color="smoke" fontSize={4} textAlign="center" px={4}>
<ModalTitle data-e2e="disablePassword_modalTitle"> {t('password.disablePassword.desc')}
{t('password.disablePassword.title')} <Box px={7} mt={4} flow={3}>
</ModalTitle> <Box flow={1}>
<ModalContent> <Label htmlFor="password">
<Box ff="Open Sans" color="smoke" fontSize={4} textAlign="center" px={4}> {t('password.inputFields.currentPassword.label')}
{t('password.disablePassword.desc')} </Label>
<Box px={7} mt={4} flow={3}> <InputPassword
<Box flow={1}> autoFocus
<Label htmlFor="password"> type="password"
{t('password.inputFields.currentPassword.label')} id="password"
</Label> onChange={this.handleInputChange('currentPassword')}
<InputPassword value={currentPassword}
autoFocus error={incorrectPassword}
type="password" />
id="password"
onChange={this.handleInputChange('currentPassword')}
value={currentPassword}
error={incorrectPassword}
/>
</Box>
</Box> </Box>
</Box> </Box>
</ModalContent> </Box>
<ModalFooter horizontal align="center" justify="flex-end" flow={2}> )}
renderFooter={() => (
<Box horizontal align="center" justify="flex-end" flow={2}>
<Button small type="button" onClick={onClose}> <Button small type="button" onClick={onClose}>
{t('common.cancel')} {t('common.cancel')}
</Button> </Button>
@ -103,11 +100,11 @@ class DisablePasswordModal extends PureComponent<Props, State> {
> >
{t('common.save')} {t('common.save')}
</Button> </Button>
</ModalFooter> </Box>
</ModalBody> )}
</form> />
)} </form>
/> </Modal>
) )
} }
} }

8
src/components/SettingsPage/PasswordAutoLockSelect.js

@ -30,10 +30,10 @@ class PasswordAutoLockSelect extends PureComponent<Props> {
} }
timeouts = [ timeouts = [
{ value: 1, label: `1 ${this.props.t('app:time.minute')}` }, { value: 1, label: `1 ${this.props.t('time.minute')}` },
{ value: 10, label: `10 ${this.props.t('app:time.minute')}s` }, { value: 10, label: `10 ${this.props.t('time.minute')}s` },
{ value: 30, label: `30 ${this.props.t('app:time.minute')}s` }, { value: 30, label: `30 ${this.props.t('time.minute')}s` },
{ value: 60, label: `1 ${this.props.t('app:time.hour')}` }, { value: 60, label: `1 ${this.props.t('time.hour')}` },
{ value: -1, label: this.props.t(`app:common.never`) }, { value: -1, label: this.props.t(`app:common.never`) },
] ]

1
src/components/SettingsPage/PasswordForm.js

@ -70,6 +70,7 @@ class PasswordForm extends PureComponent<Props> {
</Label> </Label>
<InputPassword <InputPassword
style={{ width: 240 }} style={{ width: 240 }}
onEnter={onSubmit}
id="confirmPassword" id="confirmPassword"
onChange={onChange('confirmPassword')} onChange={onChange('confirmPassword')}
value={confirmPassword} value={confirmPassword}

49
src/components/SettingsPage/PasswordModal.js

@ -1,6 +1,6 @@
// @flow // @flow
import React, { PureComponent } from 'react' import React, { Fragment, PureComponent } from 'react'
import type { T } from 'types/common' import type { T } from 'types/common'
@ -8,7 +8,7 @@ import db from 'helpers/db'
import { PasswordIncorrectError } from '@ledgerhq/errors' import { PasswordIncorrectError } from '@ledgerhq/errors'
import Box from 'components/base/Box' import Box from 'components/base/Box'
import Button from 'components/base/Button' import Button from 'components/base/Button'
import { Modal, ModalContent, ModalBody, ModalTitle, ModalFooter } from 'components/base/Modal' import Modal, { ModalBody } from 'components/base/Modal'
import PasswordForm from './PasswordForm' import PasswordForm from './PasswordForm'
@ -17,6 +17,7 @@ type Props = {
onClose: () => void, onClose: () => void,
onChangePassword: (?string) => void, onChangePassword: (?string) => void,
hasPassword: boolean, hasPassword: boolean,
isOpened: boolean,
} }
type State = { type State = {
@ -36,6 +37,12 @@ const INITIAL_STATE = {
class PasswordModal extends PureComponent<Props, State> { class PasswordModal extends PureComponent<Props, State> {
state = INITIAL_STATE state = INITIAL_STATE
componentWillReceiveProps(nextProps: Props) {
if (!nextProps.isOpened) {
this.setState(INITIAL_STATE)
}
}
handleSave = (e: SyntheticEvent<HTMLFormElement>) => { handleSave = (e: SyntheticEvent<HTMLFormElement>) => {
const { currentPassword, newPassword } = this.state const { currentPassword, newPassword } = this.state
@ -73,23 +80,17 @@ class PasswordModal extends PureComponent<Props, State> {
} }
render() { render() {
const { t, hasPassword, onClose, ...props } = this.props const { t, hasPassword, onClose, isOpened, ...props } = this.props
const { currentPassword, newPassword, incorrectPassword, confirmPassword } = this.state const { currentPassword, newPassword, incorrectPassword, confirmPassword } = this.state
return ( return (
<Modal <Modal isOpened={isOpened} centered>
{...props} <ModalBody
onHide={this.handleReset} {...props}
onClose={onClose} title={hasPassword ? t('password.changePassword.title') : t('password.setPassword.title')}
render={({ onClose }) => ( onHide={this.handleReset}
<ModalBody onClose={onClose}> onClose={onClose}
{hasPassword ? ( render={() => (
<ModalTitle>{t('password.changePassword.title')}</ModalTitle> <Fragment>
) : (
<ModalTitle data-e2e="enablePassword_modal">
{t('password.setPassword.title')}
</ModalTitle>
)}
<ModalContent>
<Box <Box
ff="Museo Sans|Regular" ff="Museo Sans|Regular"
color="dark" color="dark"
@ -116,8 +117,10 @@ class PasswordModal extends PureComponent<Props, State> {
onChange={this.handleInputChange} onChange={this.handleInputChange}
t={t} t={t}
/> />
</ModalContent> </Fragment>
<ModalFooter horizontal align="center" justify="flex-end" flow={2}> )}
renderFooter={() => (
<Box horizontal align="center" justify="flex-end" flow={2}>
<Button <Button
small small
type="button" type="button"
@ -134,10 +137,10 @@ class PasswordModal extends PureComponent<Props, State> {
> >
{t('common.save')} {t('common.save')}
</Button> </Button>
</ModalFooter> </Box>
</ModalBody> )}
)} />
/> </Modal>
) )
} }
} }

16
src/components/SettingsPage/RepairDeviceButton.js

@ -6,11 +6,12 @@ import { connect } from 'react-redux'
import { withRouter } from 'react-router' import { withRouter } from 'react-router'
import { translate } from 'react-i18next' import { translate } from 'react-i18next'
import { push } from 'react-router-redux' import { push } from 'react-router-redux'
import logger from 'logger'
import type { T } from 'types/common' import type { T } from 'types/common'
import firmwareRepair from 'commands/firmwareRepair' import firmwareRepair from 'commands/firmwareRepair'
import Button from 'components/base/Button' import Button from 'components/base/Button'
import { RepairModal } from 'components/base/Modal' import RepairModal from 'components/base/Modal/RepairModal'
type Props = { type Props = {
t: T, t: T,
@ -32,27 +33,38 @@ class RepairDeviceButton extends PureComponent<Props, State> {
progress: 0, progress: 0,
} }
componentWillUnmount() {
if (this.timeout) {
clearTimeout(this.timeout)
}
}
open = () => this.setState({ opened: true, error: null }) open = () => this.setState({ opened: true, error: null })
sub: * sub: *
timeout: *
close = () => { close = () => {
if (this.sub) this.sub.unsubscribe() if (this.sub) this.sub.unsubscribe()
if (this.timeout) clearTimeout(this.timeout)
this.setState({ opened: false, isLoading: false, error: null, progress: 0 }) this.setState({ opened: false, isLoading: false, error: null, progress: 0 })
} }
repair = (version = null) => { repair = (version = null) => {
if (this.state.isLoading) return if (this.state.isLoading) return
const { push } = this.props const { push } = this.props
this.setState({ isLoading: true }) this.timeout = setTimeout(() => this.setState({ isLoading: true }), 500)
this.sub = firmwareRepair.send({ version }).subscribe({ this.sub = firmwareRepair.send({ version }).subscribe({
next: patch => { next: patch => {
this.setState(patch) this.setState(patch)
}, },
error: error => { error: error => {
logger.critical(error)
if (this.timeout) clearTimeout(this.timeout)
this.setState({ error, isLoading: false, progress: 0 }) this.setState({ error, isLoading: false, progress: 0 })
}, },
complete: () => { complete: () => {
if (this.timeout) clearTimeout(this.timeout)
this.setState({ opened: false, isLoading: false, progress: 0 }, () => { this.setState({ opened: false, isLoading: false, progress: 0 }, () => {
push('/manager') push('/manager')
}) })

3
src/components/SettingsPage/ResetButton.js

@ -9,7 +9,7 @@ import type { T } from 'types/common'
import { hardReset } from 'helpers/reset' import { hardReset } from 'helpers/reset'
import Box from 'components/base/Box' import Box from 'components/base/Box'
import Button from 'components/base/Button' import Button from 'components/base/Button'
import { ConfirmModal } from 'components/base/Modal' import ConfirmModal from 'components/base/Modal/ConfirmModal'
import IconTriangleWarning from 'icons/TriangleWarning' import IconTriangleWarning from 'icons/TriangleWarning'
import ResetFallbackModal from './ResetFallbackModal' import ResetFallbackModal from './ResetFallbackModal'
@ -58,6 +58,7 @@ class ResetButton extends PureComponent<Props, State> {
<ConfirmModal <ConfirmModal
analyticsName="HardReset" analyticsName="HardReset"
isDanger isDanger
centered
isLoading={pending} isLoading={pending}
isOpened={opened} isOpened={opened}
onClose={this.close} onClose={this.close}

2
src/components/SettingsPage/ResetFallbackModal.js

@ -3,7 +3,7 @@
import React, { PureComponent } from 'react' import React, { PureComponent } from 'react'
import { translate } from 'react-i18next' import { translate } from 'react-i18next'
import { ConfirmModal } from 'components/base/Modal' import ConfirmModal from 'components/base/Modal/ConfirmModal'
import { openUserDataFolderAndQuit } from 'helpers/reset' import { openUserDataFolderAndQuit } from 'helpers/reset'
type Props = { type Props = {

94
src/components/SettingsPage/SocketExport.js

@ -0,0 +1,94 @@
// @flow
import React, { PureComponent } from 'react'
import WebSocket from 'ws'
import IP from 'ip'
import { createStructuredSelector } from 'reselect'
import { activeAccountsSelector } from 'reducers/accounts'
import { exportSettingsSelector } from 'reducers/settings'
import { encode } from '@ledgerhq/live-common/lib/cross'
import connect from 'react-redux/es/connect/connect'
import Button from '../base/Button'
import QRCode from '../base/QRCode'
type Props = {
accounts: *,
settings: *,
}
type State = {
active: boolean,
}
const mapStateToProps = createStructuredSelector({
accounts: activeAccountsSelector,
settings: exportSettingsSelector,
})
class SocketExport extends PureComponent<Props, State> {
state = {
active: false,
}
componentWillMount() {
this.resetServer()
}
componentDidUpdate() {
if (!this.state.active) return
if (!this.server) {
this.resetServer()
}
}
componentWillUnmount() {
if (this.server) this.server.close()
}
resetServer = () => {
this.server = new WebSocket.Server({ port: 1234 })
const { accounts, settings } = this.props
const data = encode({
accounts,
settings,
exporterName: 'desktop',
exporterVersion: __APP_VERSION__,
})
// Secret handshake to avoid intruders
this.secret = Math.random()
.toString(36)
.slice(2)
if (this.server) {
this.server.on('connection', ws => {
ws.on('message', message => {
if (message === this.secret) {
ws.send(data)
ws.close()
this.setState({ active: false })
this.server = undefined
}
})
})
}
}
secret: string
server: *
canvas = React.createRef()
render() {
return this.state.active ? (
<QRCode size={50} data={`${this.secret}~${IP.address()}`} />
) : (
<Button primary small onClick={() => this.setState({ active: true })}>
{'Generate Code'}
</Button>
)
}
}
export default connect(mapStateToProps)(SocketExport)

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

@ -91,8 +91,8 @@ class TabGeneral extends PureComponent<Props> {
</Row> </Row>
{hasPassword ? ( {hasPassword ? (
<Row <Row
title={t('app:settings.profile.passwordAutoLock')} title={t('settings.profile.passwordAutoLock')}
desc={t('app:settings.profile.passwordAutoLockDesc')} desc={t('settings.profile.passwordAutoLockDesc')}
> >
<PasswordAutoLockSelect /> <PasswordAutoLockSelect />
</Row> </Row>

71
src/components/SettingsPage/sections/Export.js

@ -8,13 +8,16 @@ import type { T } from 'types/common'
import TrackPage from 'analytics/TrackPage' import TrackPage from 'analytics/TrackPage'
import styled from 'styled-components' import styled from 'styled-components'
import { SettingsSection as Section, SettingsSectionHeader as Header } from '../SettingsSection' import { SettingsSection as Section, SettingsSectionHeader as Header } from '../SettingsSection'
import { EXPERIMENTAL_WS_EXPORT } from '../../../config/constants'
import IconShare from '../../../icons/Share' import IconShare from '../../../icons/Share'
import Button from '../../base/Button' import Button from '../../base/Button'
import Modal, { ModalBody, ModalContent, ModalFooter, ModalTitle } from '../../base/Modal' import Modal from '../../base/Modal'
import ModalBody from '../../base/Modal/ModalBody'
import Box from '../../base/Box' import Box from '../../base/Box'
import QRCodeExporter from '../../QRCodeExporter' import QRCodeExporter from '../../QRCodeExporter'
import { BulletRow } from '../../Onboarding/helperComponents' import { BulletRow } from '../../Onboarding/helperComponents'
import Text from '../../base/Text' import Text from '../../base/Text'
import SocketExport from '../SocketExport'
const BulletRowIcon = styled(Box).attrs({ const BulletRowIcon = styled(Box).attrs({
ff: 'Rubik|Regular', ff: 'Rubik|Regular',
@ -66,7 +69,10 @@ class SectionExport extends PureComponent<Props, State> {
<Text ff="Open Sans|SemiBold" color="dark"> <Text ff="Open Sans|SemiBold" color="dark">
{'+'} {'+'}
</Text> </Text>
{'button in Accounts'} {'button in'}
<Text ff="Open Sans|SemiBold" color="dark">
{'Accounts'}
</Text>
</Trans> </Trans>
</Box> </Box>
), ),
@ -90,34 +96,45 @@ class SectionExport extends PureComponent<Props, State> {
icon: <BulletRowIcon>{'3'}</BulletRowIcon>, icon: <BulletRowIcon>{'3'}</BulletRowIcon>,
desc: ( desc: (
<Box style={{ display: 'block' }}> <Box style={{ display: 'block' }}>
<Trans i18nKey="settings.export.modal.step3" /> <Trans i18nKey="settings.export.modal.step3">
{'Scan the'}
<Text ff="Open Sans|SemiBold" color="dark">
{'LiveQR Code'}
</Text>
{'until the loader hits 100%'}
</Trans>
</Box> </Box>
), ),
}, },
] ]
return ( return (
<ModalBody onClose={onClose}> <ModalBody
<ModalTitle>{t('settings.export.modal.title')}</ModalTitle> onClose={onClose}
<ModalContent flow={2} justify="center" align="center"> title={t('settings.export.modal.title')}
<Box flow={2}> render={() => (
<QRCodeExporter size={330} /> <Box justify="center" align="center">
</Box> <Box flow={2}>
<Box shrink style={{ width: 330, fontSize: 13, marginTop: 20 }}> <QRCodeExporter size={330} />
<Text ff="Open Sans|SemiBold" color="dark"> </Box>
{t('settings.export.modal.listTitle')} <Box shrink style={{ width: 330, fontSize: 13, marginTop: 20 }}>
</Text> <Text ff="Open Sans|SemiBold" color="dark">
{t('settings.export.modal.listTitle')}
</Text>
</Box>
<Box style={{ width: 330 }}>
{stepsImportMobile.map(step => <BulletRow key={step.key} step={step} />)}
</Box>
</Box> </Box>
<Box style={{ width: 330 }}> )}
{stepsImportMobile.map(step => <BulletRow key={step.key} step={step} />)} renderFooter={() => (
<Box>
<Button small onClick={onClose} primary>
{t('settings.export.modal.button')}
</Button>
</Box> </Box>
</ModalContent> )}
<ModalFooter horizontal align="center" justify="flex-end" flow={2}> />
<Button small onClick={onClose} primary>
{t('settings.export.modal.button')}
</Button>
</ModalFooter>
</ModalBody>
) )
} }
@ -131,7 +148,7 @@ class SectionExport extends PureComponent<Props, State> {
<Header <Header
icon={<IconShare size={16} />} icon={<IconShare size={16} />}
title={t('settings.tabs.export')} title={t('settings.export.title')}
desc={t('settings.export.desc')} desc={t('settings.export.desc')}
renderRight={ renderRight={
<Button small onClick={this.onModalOpen} primary> <Button small onClick={this.onModalOpen} primary>
@ -139,6 +156,14 @@ class SectionExport extends PureComponent<Props, State> {
</Button> </Button>
} }
/> />
{EXPERIMENTAL_WS_EXPORT && (
<Header
icon={<IconShare size={16} />}
title="Experimental websocket local export ⚡"
desc="Generate a pairing code and use it on Ledger Live Mobile"
renderRight={<SocketExport />}
/>
)}
<Modal isOpened={isModalOpened} onClose={this.onModalClose} render={this.renderModal} /> <Modal isOpened={isModalOpened} onClose={this.onModalClose} render={this.renderModal} />
</Section> </Section>
) )

130
src/components/TopBanner.js

@ -0,0 +1,130 @@
// @flow
import React, { PureComponent } from 'react'
import styled from 'styled-components'
import { connect } from 'react-redux'
import Box from 'components/base/Box'
import { radii } from 'styles/theme'
import IconCross from 'icons/Cross'
import { createStructuredSelector } from 'reselect'
import { dismissBanner } from '../actions/settings'
import { dismissedBannersSelector } from '../reducers/settings'
export type Content = {
Icon?: React$ComponentType<*>,
message: React$Node,
right?: React$Node,
}
type Props = {
content?: Content,
status: string,
dismissable: boolean,
bannerId?: string,
dismissedBanners: string[],
dismissBanner: string => void,
}
const mapStateToProps = createStructuredSelector({
dismissedBanners: dismissedBannersSelector,
})
const mapDispatchToProps = {
dismissBanner,
}
class TopBanner extends PureComponent<Props> {
static defaultProps = {
status: '',
dismissable: false,
}
onDismiss = () => {
const { bannerId, dismissBanner } = this.props
if (bannerId) {
dismissBanner(bannerId)
}
}
render() {
const { dismissedBanners, bannerId, dismissable, content, status } = this.props
if (!content || (bannerId && dismissedBanners.includes(bannerId))) return null
const { Icon, message, right } = content
return (
<Container status={status}>
{Icon && (
<IconContainer>
<Icon size={16} />
</IconContainer>
)}
{message}
<RightContainer>{right}</RightContainer>
{dismissable && (
<CloseContainer onClick={this.onDismiss}>
<IconCross size={14} />
</CloseContainer>
)}
</Container>
)
}
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(TopBanner)
const IconContainer = styled.div`
margin-right: 15px;
display: flex;
align-items: center;
`
const colorForStatus = {
error: 'alertRed',
dark: '#142533',
}
const Container = styled(Box).attrs({
horizontal: true,
align: 'center',
py: '8px',
px: 3,
bg: p => colorForStatus[p.status] || 'wallet',
color: 'white',
mt: -20,
mb: 20,
fontSize: 4,
ff: 'Open Sans|SemiBold',
})`
border-radius: ${radii[1]}px;
`
const RightContainer = styled.div`
margin-left: auto;
`
export const FakeLink = styled.span`
color: white;
text-decoration: underline;
cursor: pointer;
`
const CloseContainer = styled(Box).attrs({
color: 'white',
})`
z-index: 1;
margin-left: 10px;
cursor: pointer;
&:hover {
color: #eee;
}
&:active {
color: #eee;
}
`

4
src/components/TopBar/index.js

@ -42,9 +42,7 @@ const Inner = styled(Box).attrs({
grow: true, grow: true,
flow: 4, flow: 4,
align: 'center', align: 'center',
})` })``
border-bottom: 1px solid ${p => p.theme.colors.fog};
`
const Bar = styled.div` const Bar = styled.div`
margin-left: 5px; margin-left: 5px;

103
src/components/UpdateNotifier/UpdateDownloaded.js

@ -1,103 +0,0 @@
// @flow
import React, { PureComponent } from 'react'
import { translate } from 'react-i18next'
import { compose } from 'redux'
import { connect } from 'react-redux'
import styled from 'styled-components'
import { getUpdateStatus, getUpdateData } from 'reducers/update'
import { sendEvent } from 'renderer/events'
import type { State } from 'reducers'
import type { UpdateStatus } from 'reducers/update'
import { radii } from 'styles/theme'
import Box from 'components/base/Box'
import Text from 'components/base/Text'
import UpdateIcon from 'icons/Update'
import type { T } from 'types/common'
type Props = {
t: T,
updateStatus: UpdateStatus,
}
const mapStateToProps = (state: State) => ({
updateStatus: getUpdateStatus(state),
updateData: getUpdateData(state),
})
const Container = styled(Box).attrs({
py: '8px',
px: 3,
bg: 'wallet',
color: 'white',
mt: '-50px',
mb: '35px',
style: p => ({
transform: `translate3d(0, ${p.offset}%, 0)`,
}),
})`
border-radius: ${radii[1]}px;
`
const NotifText = styled(Text).attrs({
ff: 'Open Sans|SemiBold',
fontSize: 4,
})``
class UpdateDownloaded extends PureComponent<Props> {
renderStatus() {
const { updateStatus, t } = this.props
switch (updateStatus) {
case 'idle':
case 'checking':
case 'unavailable':
case 'error':
case 'available':
case 'progress':
return null
case 'downloaded':
return (
<Box horizontal flow={3}>
<UpdateIcon size={16} />
<Box grow>
<NotifText>{t('update.newVersionReady')}</NotifText>
</Box>
<Box>
<NotifText
style={{ cursor: 'pointer', textDecoration: 'underline' }}
onClick={() => sendEvent('updater', 'quitAndInstall')}
>
{t('update.relaunch')}
</NotifText>
</Box>
</Box>
)
default:
return null
}
}
render() {
const { updateStatus, ...props } = this.props
const isToggled = updateStatus === 'downloaded'
if (!isToggled) {
return null
}
return <Container {...props}>{this.renderStatus()}</Container>
}
}
export default compose(
connect(
mapStateToProps,
null,
),
translate(),
)(UpdateDownloaded)

48
src/components/UpdateNotifier/UpdateInstalled.js

@ -1,48 +0,0 @@
// @flow
import { PureComponent } from 'react'
import { connect } from 'react-redux'
import semver from 'semver'
import { openModal } from 'reducers/modals'
import { lastUsedVersionSelector } from 'reducers/settings'
import { saveSettings } from 'actions/settings'
import { MODAL_RELEASES_NOTES } from 'config/constants'
import type { State } from 'reducers'
type Props = {
openModal: Function,
saveSettings: Function,
lastUsedVersion: string,
}
const mapStateToProps = (state: State) => ({
lastUsedVersion: lastUsedVersionSelector(state),
})
const mapDispatchToProps = {
openModal,
saveSettings,
}
class UpdateInstalled extends PureComponent<Props> {
componentDidMount() {
const { lastUsedVersion, saveSettings, openModal } = this.props
const currentVersion = __APP_VERSION__
if (semver.gt(currentVersion, lastUsedVersion)) {
openModal(MODAL_RELEASES_NOTES, currentVersion)
saveSettings({ lastUsedVersion: currentVersion })
}
}
render() {
return null
}
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(UpdateInstalled)

17
src/components/UpdateNotifier/index.js

@ -1,17 +0,0 @@
// @flow
import React, { PureComponent, Fragment } from 'react'
import UpdateDownloaded from './UpdateDownloaded'
import UpdateInstalled from './UpdateInstalled'
export default class UpdateNotifier extends PureComponent<{}> {
render() {
return (
<Fragment>
<UpdateDownloaded />
<UpdateInstalled />
</Fragment>
)
}
}

74
src/components/Updater/Banner.js

@ -0,0 +1,74 @@
// @flow
import React, { PureComponent } from 'react'
import { Trans } from 'react-i18next'
import { urls } from 'config/urls'
import { openURL } from 'helpers/linking'
import Spinner from 'components/base/Spinner'
import IconUpdate from 'icons/Update'
import IconDonjon from 'icons/Donjon'
import IconWarning from 'icons/TriangleWarning'
import { withUpdaterContext } from './UpdaterContext'
import type { UpdaterContextType } from './UpdaterContext'
import TopBanner, { FakeLink } from '../TopBanner'
import type { Content } from '../TopBanner'
type Props = {
context: UpdaterContextType,
}
export const VISIBLE_STATUS = ['download-progress', 'checking', 'check-success', 'error']
const CONTENT_BY_STATUS = (quitAndInstall, reDownload, progress): { [string]: Content } => ({
'download-progress': {
Icon: Spinner,
message: <Trans i18nKey="update.downloadInProgress" />,
right: <Trans i18nKey="update.downloadProgress" values={{ progress }} />,
},
checking: {
Icon: IconDonjon,
message: <Trans i18nKey="update.checking" />,
},
'check-success': {
Icon: IconUpdate,
message: <Trans i18nKey="update.checkSuccess" />,
right: (
<FakeLink onClick={quitAndInstall}>
<Trans i18nKey="update.quitAndInstall" />
</FakeLink>
),
},
error: {
Icon: IconWarning,
message: <Trans i18nKey="update.error" />,
right: (
<FakeLink onClick={reDownload}>
<Trans i18nKey="update.reDownload" />
</FakeLink>
),
},
})
class UpdaterTopBanner extends PureComponent<Props> {
reDownload = () => {
openURL(urls.liveHome)
}
render() {
const { context } = this.props
const { status, quitAndInstall, downloadProgress } = context
if (!VISIBLE_STATUS.includes(status)) return null
const content: ?Content = CONTENT_BY_STATUS(quitAndInstall, this.reDownload, downloadProgress)[
status
]
if (!content) return null
return <TopBanner content={content} status={status} />
}
}
export default withUpdaterContext(UpdaterTopBanner)

70
src/components/Updater/DebugUpdater.js

@ -0,0 +1,70 @@
// @flow
/* eslint-disable react/jsx-no-literals */
import React, { Component } from 'react'
import { withUpdaterContext } from './UpdaterContext'
import type { UpdaterContextType } from './UpdaterContext'
const statusToDebug = ['idle', 'download-progress', 'checking', 'check-success', 'error']
type Props = {
context: UpdaterContextType,
}
class DebugUpdater extends Component<Props> {
render() {
const { context } = this.props
const { status, setStatus, quitAndInstall } = context
return (
<div style={styles.root}>
<h1>
DEBUG UPDATE<br />
------------<br />
</h1>
<b>status:</b> {status}
<div style={{ marginTop: 20 }}>
{statusToDebug.map(s => (
<button key={s} style={styles.btn} onClick={() => setStatus(s)}>
{status === s ? `[${s}]` : s}
</button>
))}
</div>
<div style={{ marginTop: 20 }}>
<b>simulate update</b>
</div>
<div style={{ marginTop: 20 }}>
<button style={styles.btn} onClick={quitAndInstall}>
{'quit and install'}
</button>
</div>
</div>
)
}
}
const styles = {
root: {
position: 'fixed',
bottom: 0,
right: 0,
padding: 10,
fontSize: 10,
background: 'black',
color: 'white',
fontFamily: 'monospace',
zIndex: 1000,
maxWidth: 250,
},
btn: {
cursor: 'pointer',
background: 'lightgreen',
color: 'black',
border: 'none',
marginRight: 10,
marginTop: 10,
padding: '0px 10px',
},
}
export default withUpdaterContext(DebugUpdater)

37
src/components/Updater/UpdateDot.js

@ -0,0 +1,37 @@
// @flow
import React from 'react'
import styled from 'styled-components'
import { colors } from 'styles/theme'
import { withUpdaterContext } from './UpdaterContext'
import { VISIBLE_STATUS } from './Banner'
import type { UpdaterContextType, UpdateStatus } from './UpdaterContext'
type Props = {
context: UpdaterContextType,
}
const getColor = ({ status }: { status: UpdateStatus }) =>
status === 'error' ? colors.alertRed : colors.wallet
const getOpacity = ({ status }: { status: UpdateStatus }) =>
status === 'download-progress' || status === 'checking' ? 0.5 : 1
const Dot = styled.div`
opacity: ${getOpacity};
width: 8px;
height: 8px;
background-color: ${getColor};
border-radius: 50%;
`
function UpdateDot(props: Props) {
const { context } = props
const { status } = context
if (!VISIBLE_STATUS.includes(status)) return null
return <Dot status={status} />
}
export default withUpdaterContext(UpdateDot)

95
src/components/Updater/UpdaterContext.js

@ -0,0 +1,95 @@
// @flow
import React, { Component } from 'react'
import autoUpdate from 'commands/autoUpdate'
import quitAndInstallElectronUpdate from 'commands/quitAndInstallElectronUpdate'
export type UpdateStatus =
| 'idle'
| 'checking-for-update'
| 'update-available'
| 'update-not-available'
| 'download-progress'
| 'update-downloaded'
| 'checking'
| 'check-success'
| 'error'
export type UpdaterContextType = {
status: UpdateStatus,
downloadProgress: number,
quitAndInstall: () => void,
setStatus: UpdateStatus => void,
error: ?Error,
}
type UpdaterProviderProps = {
children: *,
}
type UpdaterProviderState = {
status: UpdateStatus,
downloadProgress: number,
error: ?Error,
}
const UpdaterContext = React.createContext()
class Provider extends Component<UpdaterProviderProps, UpdaterProviderState> {
constructor() {
super()
if (__PROD__) {
this.sub = autoUpdate.send({}).subscribe({
next: e => {
if (e.status === 'download-progress') {
const downloadProgress = e.payload && e.payload.percent ? e.payload.percent : 0
this.setState({ status: e.status, downloadProgress })
} else {
this.setStatus(e.status)
}
},
error: error => this.setState({ status: 'error', error }),
})
}
this.state = {
status: 'idle',
downloadProgress: 0,
error: null,
}
}
componentWillUnmount() {
if (this.sub) {
this.sub.unsubscribe()
}
}
sub = null
setStatus = (status: UpdateStatus) => this.setState({ status })
setDownloadProgress = (downloadProgress: number) => this.setState({ downloadProgress })
quitAndInstall = () => quitAndInstallElectronUpdate.send().toPromise()
render() {
const { status, downloadProgress, error } = this.state
const value = {
status,
downloadProgress,
error,
setStatus: this.setStatus,
quitAndInstall: this.quitAndInstall,
}
return <UpdaterContext.Provider value={value}>{this.props.children}</UpdaterContext.Provider>
}
}
export const withUpdaterContext = (ComponentToDecorate: React$ComponentType<*>) => (props: *) => (
<UpdaterContext.Consumer>
{context => <ComponentToDecorate {...props} context={context} />}
</UpdaterContext.Consumer>
)
export const UpdaterProvider = Provider

8
src/components/WithFeesAPI.js

@ -1,13 +1,13 @@
// @flow // @flow
import { Component } from 'react' import { Component } from 'react'
import type { Currency } from '@ledgerhq/live-common/lib/types' import type { CryptoCurrency } from '@ledgerhq/live-common/lib/types'
import { getEstimatedFees } from 'api/Fees' import { getEstimatedFees } from '@ledgerhq/live-common/lib/api/Fees'
import type { Fees } from 'api/Fees' import type { Fees } from '@ledgerhq/live-common/lib/api/Fees'
// FIXME we need to abstract this out like we did for CounterValues // FIXME we need to abstract this out like we did for CounterValues
export default class WithFeesAPI extends Component< export default class WithFeesAPI extends Component<
{ {
currency: Currency, currency: CryptoCurrency,
render: Fees => *, render: Fees => *,
renderLoading: () => *, renderLoading: () => *,
renderError: Error => *, renderError: Error => *,

6
src/components/base/CurrencyBadge.js

@ -4,7 +4,7 @@ import React from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { getCryptoCurrencyIcon } from '@ledgerhq/live-common/lib/react' import { getCryptoCurrencyIcon } from '@ledgerhq/live-common/lib/react'
import type { Currency } from '@ledgerhq/live-common/lib/types' import type { CryptoCurrency } from '@ledgerhq/live-common/lib/types'
import { rgba } from 'styles/helpers' import { rgba } from 'styles/helpers'
@ -26,7 +26,7 @@ export function CurrencyCircleIcon({
size, size,
...props ...props
}: { }: {
currency: Currency, currency: CryptoCurrency,
size: number, size: number,
}) { }) {
const Icon = getCryptoCurrencyIcon(currency) const Icon = getCryptoCurrencyIcon(currency)
@ -37,7 +37,7 @@ export function CurrencyCircleIcon({
) )
} }
function CurrencyBadge({ currency, ...props }: { currency: Currency }) { function CurrencyBadge({ currency, ...props }: { currency: CryptoCurrency }) {
return ( return (
<Box horizontal flow={3} {...props}> <Box horizontal flow={3} {...props}>
<CurrencyCircleIcon size={40} currency={currency} /> <CurrencyCircleIcon size={40} currency={currency} />

59
src/components/base/Modal/ConfirmModal.js

@ -9,7 +9,8 @@ import TrackPage from 'analytics/TrackPage'
import Button from 'components/base/Button' import Button from 'components/base/Button'
import Box from 'components/base/Box' import Box from 'components/base/Box'
import { Modal, ModalContent, ModalBody, ModalTitle, ModalFooter } from './index' import Modal from './index'
import ModalBody from './ModalBody'
type Props = { type Props = {
isOpened: boolean, isOpened: boolean,
@ -21,11 +22,13 @@ type Props = {
confirmText?: string, confirmText?: string,
cancelText?: string, cancelText?: string,
onReject: Function, onReject: Function,
onClose?: Function,
onConfirm: Function, onConfirm: Function,
t: T, t: T,
isLoading?: boolean, isLoading?: boolean,
analyticsName: string, analyticsName: string,
cancellable?: boolean, cancellable?: boolean,
centered?: boolean,
} }
class ConfirmModal extends PureComponent<Props> { class ConfirmModal extends PureComponent<Props> {
@ -43,23 +46,38 @@ class ConfirmModal extends PureComponent<Props> {
onConfirm, onConfirm,
isLoading, isLoading,
renderIcon, renderIcon,
onClose,
t, t,
analyticsName, analyticsName,
centered,
...props ...props
} = this.props } = this.props
const realConfirmText = confirmText || t('common.confirm') const realConfirmText = confirmText || t('common.confirm')
const realCancelText = cancelText || t('common.cancel') const realCancelText = cancelText || t('common.cancel')
return ( return (
<Modal <Modal isOpened={isOpened} centered={centered}>
isOpened={isOpened} <ModalBody
preventBackdropClick={isLoading} preventBackdropClick={isLoading}
{...props} {...props}
render={({ onClose }) => ( onClose={!cancellable && isLoading ? undefined : onClose}
<ModalBody onClose={!cancellable && isLoading ? undefined : onClose}> title={title}
<TrackPage category="Modal" name={analyticsName} /> renderFooter={() => (
<ModalTitle>{title}</ModalTitle> <Box horizontal align="center" justify="flex-end" flow={2}>
<ModalContent> {!isLoading && <Button onClick={onReject}>{realCancelText}</Button>}
<Button
onClick={onConfirm}
primary={!isDanger}
danger={isDanger}
isLoading={isLoading}
disabled={isLoading}
>
{realConfirmText}
</Button>
</Box>
)}
render={() => (
<Box>
{subTitle && ( {subTitle && (
<Box ff="Museo Sans|Regular" color="dark" textAlign="center" mb={2} mt={3}> <Box ff="Museo Sans|Regular" color="dark" textAlign="center" mb={2} mt={3}>
{subTitle} {subTitle}
@ -73,22 +91,11 @@ class ConfirmModal extends PureComponent<Props> {
<Box ff="Open Sans" color="smoke" fontSize={4} textAlign="center"> <Box ff="Open Sans" color="smoke" fontSize={4} textAlign="center">
{desc} {desc}
</Box> </Box>
</ModalContent> </Box>
<ModalFooter horizontal align="center" justify="flex-end" flow={2}> )}
{!isLoading && <Button onClick={onReject}>{realCancelText}</Button>} />
<Button <TrackPage category="Modal" name={analyticsName} />
onClick={onConfirm} </Modal>
primary={!isDanger}
danger={isDanger}
isLoading={isLoading}
disabled={isLoading}
>
{realConfirmText}
</Button>
</ModalFooter>
</ModalBody>
)}
/>
) )
} }
} }

143
src/components/base/Modal/ModalBody.js

@ -1,95 +1,102 @@
// @flow // @flow
import React, { PureComponent } from 'react' import React, { PureComponent, Fragment } from 'react'
import styled, { keyframes } from 'styled-components' import Animated from 'animated/lib/targets/react-dom'
import { findDOMNode } from 'react-dom'
import Box from 'components/base/Box' import ModalContent from './ModalContent'
import IconCross from 'icons/Cross' import ModalHeader from './ModalHeader'
import ModalFooter from './ModalFooter'
export const Container = styled(Box).attrs({ import type { RenderProps } from './index'
px: 5,
pb: 5,
})``
type Props = { type Props = {
deferHeight?: number, title: string,
onClose?: Function, onBack?: void => void,
children: any, onClose?: void => void,
render?: (?RenderProps) => any,
renderFooter?: (?RenderProps) => any,
renderProps?: RenderProps,
noScroll?: boolean,
refocusWhenChange?: any,
} }
type State = { type State = {
isHidden: boolean, animGradient: Animated.Value,
} }
class ModalBody extends PureComponent<Props, State> { class ModalBody extends PureComponent<Props, State> {
static defaultProps = { state = {
onClose: undefined, animGradient: new Animated.Value(0),
} }
state = { componentDidUpdate(prevProps: Props) {
isHidden: true, const shouldFocus = prevProps.refocusWhenChange !== this.props.refocusWhenChange
if (shouldFocus) {
if (this._content) {
const node = findDOMNode(this._content) // eslint-disable-line react/no-find-dom-node
if (node) {
// $FlowFixMe
node.focus()
}
}
}
} }
componentDidMount() { _content = null
setTimeout(() => {
window.requestAnimationFrame(() => { animateGradient = (isScrollable: boolean) => {
this.setState({ isHidden: false }) const anim = {
}) duration: 150,
}, 150) toValue: isScrollable ? 1 : 0,
}
Animated.timing(this.state.animGradient, anim).start()
} }
render() { render() {
const { children, onClose, deferHeight, ...props } = this.props const { onBack, onClose, title, render, renderFooter, renderProps, noScroll } = this.props
const { isHidden } = this.state const { animGradient } = this.state
const gradientStyle = {
...GRADIENT_STYLE,
opacity: animGradient,
}
return ( return (
<Body <Fragment>
style={{ height: isHidden && deferHeight ? deferHeight : undefined }} <ModalHeader onBack={onBack} onClose={onClose}>
data-e2e="modalBody" {title}
> </ModalHeader>
{onClose && ( <ModalContent
<CloseContainer onClick={onClose}> tabIndex={0}
<IconCross size={16} /> ref={n => (this._content = n)}
</CloseContainer> onIsScrollableChange={this.animateGradient}
)} noScroll={noScroll}
{(!isHidden || !deferHeight) && <Inner {...props}>{children}</Inner>} >
</Body> {render && render(renderProps)}
</ModalContent>
<div style={GRADIENT_WRAPPER_STYLE}>
<Animated.div style={gradientStyle} />
</div>
{renderFooter && <ModalFooter>{renderFooter(renderProps)}</ModalFooter>}
</Fragment>
) )
} }
} }
const CloseContainer = styled(Box).attrs({ const GRADIENT_STYLE = {
p: 4, background: 'linear-gradient(rgba(255, 255, 255, 0), #ffffff)',
color: 'fog', height: 40,
})` position: 'absolute',
position: absolute; bottom: 0,
top: 0; left: 0,
right: 0; right: 20,
z-index: 1; }
&:hover {
color: ${p => p.theme.colors.grey};
}
&:active {
color: ${p => p.theme.colors.dark};
}
`
const Body = styled(Box).attrs({
bg: p => p.theme.colors.white,
relative: true,
borderRadius: 1,
})`
box-shadow: 0 10px 20px 0 rgba(0, 0, 0, 0.2);
`
const appear = keyframes`
from { opacity: 0; }
to { opacity: 1; }
`
const Inner = styled(Box)` const GRADIENT_WRAPPER_STYLE = {
animation: ${appear} 80ms linear; height: 0,
` position: 'relative',
pointerEvents: 'none',
}
export default ModalBody export default ModalBody

59
src/components/base/Modal/ModalContent.js

@ -0,0 +1,59 @@
// @flow
/* eslint-disable jsx-a11y/no-noninteractive-tabindex */
import React, { PureComponent } from 'react'
class ModalContent extends PureComponent<{
children: any,
onIsScrollableChange: boolean => void,
noScroll?: boolean,
}> {
componentDidMount() {
window.requestAnimationFrame(() => {
if (this._isUnmounted) return
this.showHideGradient()
if (this._outer) {
const ro = new ResizeObserver(this.showHideGradient)
ro.observe(this._outer)
}
})
}
componentWillUnmount() {
this._isUnmounted = true
}
_outer = null
_isUnmounted = false
showHideGradient = () => {
if (!this._outer) return
const { onIsScrollableChange } = this.props
const isScrollable = this._outer.scrollHeight > this._outer.clientHeight
onIsScrollableChange(isScrollable)
}
render() {
const { children, noScroll } = this.props
const contentStyle = {
...CONTENT_STYLE,
overflow: noScroll ? 'visible' : 'auto',
}
return (
<div style={contentStyle} ref={n => (this._outer = n)} tabIndex={0}>
{children}
</div>
)
}
}
const CONTENT_STYLE = {
flexShrink: 1,
padding: 20,
paddingBottom: 40,
}
export default ModalContent

18
src/components/base/Modal/ModalFooter.js

@ -0,0 +1,18 @@
// @flow
import React from 'react'
import { colors } from 'styles/theme'
const MODAL_FOOTER_STYLE = {
display: 'flex',
justifyContent: 'flex-end',
borderTop: `2px solid ${colors.lightGrey}`,
padding: 20,
}
const ModalFooter = ({ children }: { children: any }) => (
<div style={MODAL_FOOTER_STYLE}>{children}</div>
)
export default ModalFooter

93
src/components/base/Modal/ModalHeader.js

@ -0,0 +1,93 @@
// @flow
import React from 'react'
import styled from 'styled-components'
import { translate } from 'react-i18next'
import type { T } from 'types/common'
import Box from 'components/base/Box'
import IconAngleLeft from 'icons/AngleLeft'
import IconCross from 'icons/Cross'
const MODAL_HEADER_STYLE = {
position: 'relative',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: 20,
}
const ModalTitle = styled(Box).attrs({
color: 'dark',
ff: 'Museo Sans|Regular',
fontSize: 6,
grow: true,
shrink: true,
})`
text-align: center;
line-height: 1;
`
const iconAngleLeft = <IconAngleLeft size={16} />
const iconCross = <IconCross size={16} />
const ModalHeaderAction = styled(Box).attrs({
horizontal: true,
align: 'center',
fontSize: 3,
p: 4,
color: 'grey',
})`
position: absolute;
top: 0;
left: ${p => (p.right ? 'auto' : 0)};
right: ${p => (p.right ? 0 : 'auto')};
line-height: 0;
cursor: pointer;
&:hover {
color: ${p => p.theme.colors.graphite};
}
&:active {
color: ${p => p.theme.colors.dark};
}
span {
border-bottom: 1px dashed transparent;
}
&:focus span {
border-bottom-color: inherit;
}
`
const ModalHeader = ({
children,
onBack,
onClose,
t,
}: {
children: any,
onBack: void => void,
onClose: void => void,
t: T,
}) => (
<div style={MODAL_HEADER_STYLE}>
{onBack && (
<ModalHeaderAction onClick={onBack}>
{iconAngleLeft}
<span>{t('common.back')}</span>
</ModalHeaderAction>
)}
<ModalTitle>{children}</ModalTitle>
{onClose && (
<ModalHeaderAction right color="fog" onClick={onClose}>
{iconCross}
</ModalHeaderAction>
)}
</div>
)
export default translate()(ModalHeader)

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

@ -1,75 +0,0 @@
// @flow
import React from 'react'
import styled from 'styled-components'
import { translate } from 'react-i18next'
import type { T } from 'types/common'
import Box from 'components/base/Box'
import IconAngleLeft from 'icons/AngleLeft'
const Container = styled(Box).attrs({
alignItems: 'center',
color: 'dark',
ff: 'Museo Sans|Regular',
fontSize: 6,
justifyContent: 'center',
p: 5,
relative: true,
})``
const Back = styled(Box).attrs({
unstyled: true,
horizontal: true,
align: 'center',
color: 'grey',
ff: 'Open Sans',
fontSize: 3,
p: 4,
})`
position: absolute;
line-height: 1;
top: 0;
left: 0;
&:hover {
color: ${p => p.theme.colors.graphite};
}
&:active {
color: ${p => p.theme.colors.dark};
}
span {
border-bottom: 1px dashed transparent;
}
&:focus span {
border-bottom-color: inherit;
}
`
function ModalTitle({
t,
onBack,
children,
...props
}: {
t: T,
onBack: any => void,
children: any,
}) {
return (
<Container {...props} data-e2e="modal_title">
{onBack && (
<Back onClick={onBack}>
<IconAngleLeft size={16} />
<span>{t('common.back')}</span>
</Back>
)}
{children}
</Container>
)
}
export default translate()(ModalTitle)

95
src/components/base/Modal/RepairModal.js

@ -17,7 +17,8 @@ import ProgressCircle from 'components/ProgressCircle'
import TranslatedError from 'components/TranslatedError' import TranslatedError from 'components/TranslatedError'
import ExclamationCircleThin from 'icons/ExclamationCircleThin' import ExclamationCircleThin from 'icons/ExclamationCircleThin'
import { Modal, ModalContent, ModalBody, ModalTitle, ModalFooter } from './index' import Modal from './index'
import ModalBody from './ModalBody'
const Container = styled(Box).attrs({ const Container = styled(Box).attrs({
alignItems: 'center', alignItems: 'center',
@ -39,18 +40,18 @@ const Separator = styled(Box).attrs({
` `
const DisclaimerStep = ({ desc }: { desc?: string }) => ( const DisclaimerStep = ({ desc }: { desc?: string }) => (
<ModalContent> <Box>
{desc ? ( {desc ? (
<Box ff="Open Sans" color="smoke" fontSize={4} textAlign="center" mb={2}> <Box ff="Open Sans" color="smoke" fontSize={4} textAlign="center" mb={2}>
{desc} {desc}
</Box> </Box>
) : null} ) : null}
</ModalContent> </Box>
) )
const FlashStep = ({ progress, t }: { progress: number, t: * }) => const FlashStep = ({ progress, t }: { progress: number, t: * }) =>
progress === 0 ? ( progress === 0 ? (
<ModalContent> <Box>
<Box mx={7}> <Box mx={7}>
<Text ff="Open Sans|Regular" align="center" color="smoke"> <Text ff="Open Sans|Regular" align="center" color="smoke">
<Bullet>{'1.'}</Bullet> <Bullet>{'1.'}</Bullet>
@ -74,9 +75,9 @@ const FlashStep = ({ progress, t }: { progress: number, t: * }) =>
alt={t('manager.modal.mcuFirst')} alt={t('manager.modal.mcuFirst')}
/> />
</Box> </Box>
</ModalContent> </Box>
) : ( ) : (
<ModalContent> <Box>
<Box mx={7} align="center"> <Box mx={7} align="center">
<ProgressCircle size={64} progress={progress} /> <ProgressCircle size={64} progress={progress} />
</Box> </Box>
@ -88,11 +89,11 @@ const FlashStep = ({ progress, t }: { progress: number, t: * }) =>
{t('manager.modal.mcuPin')} {t('manager.modal.mcuPin')}
</Text> </Text>
</Box> </Box>
</ModalContent> </Box>
) )
const ErrorStep = ({ error }: { error: Error }) => ( const ErrorStep = ({ error }: { error: Error }) => (
<ModalContent> <Box>
<Container> <Container>
<Box color="alertRed"> <Box color="alertRed">
<ExclamationCircleThin size={44} /> <ExclamationCircleThin size={44} />
@ -118,7 +119,7 @@ const ErrorStep = ({ error }: { error: Error }) => (
<TranslatedError error={error} field="description" /> <TranslatedError error={error} field="description" />
</Box> </Box>
</Container> </Container>
</ModalContent> </Box>
) )
type Props = { type Props = {
@ -177,38 +178,44 @@ class RepairModal extends PureComponent<Props, *> {
return ( return (
<Modal <Modal
isOpened={isOpened} isOpened={isOpened}
centered
preventBackdropClick={isLoading} preventBackdropClick={isLoading}
onClose={!cancellable && isLoading ? undefined : onReject}
{...props} {...props}
render={({ onClose }) => ( >
<ModalBody onClose={!cancellable && isLoading ? undefined : onClose}> <TrackPage category="Modal" name={analyticsName} />
<TrackPage category="Modal" name={analyticsName} /> <ModalBody
<ModalTitle>{title}</ModalTitle> title={title}
{error ? ( render={() => (
<ErrorStep error={error} /> <Box>
) : isLoading ? ( {error ? (
<FlashStep t={t} progress={progress} /> <ErrorStep error={error} />
) : ( ) : isLoading ? (
<DisclaimerStep desc={desc} /> <FlashStep t={t} progress={progress} />
)} ) : (
<DisclaimerStep desc={desc} />
{!isLoading && !error ? ( )}
<Box py={2} px={5}>
<Select {!isLoading && !error ? (
isSearchable={false} <Box py={2} px={5}>
isClearable={false} <Select
value={selectedOption} isSearchable={false}
onChange={this.onChange} isClearable={false}
autoFocus value={selectedOption}
options={forceRepairChoices} onChange={this.onChange}
renderOption={this.renderOption} autoFocus
renderValue={this.renderValue} options={forceRepairChoices}
/> renderOption={this.renderOption}
</Box> renderValue={this.renderValue}
) : null} />
</Box>
{!isLoading ? ( ) : null}
<ModalFooter horizontal align="center" justify="flex-end" flow={2}> </Box>
{error ? <Button onClick={onReject}>{t(`common.close`)}</Button> : null} )}
renderFooter={() =>
!isLoading ? (
<Box horizontal align="center" justify="flex-end" flow={2}>
<Button onClick={onReject}>{t(`common.${error ? 'close' : 'cancel'}`)}</Button>
{error ? null : ( {error ? null : (
<> <>
<Button <Button
@ -222,11 +229,11 @@ class RepairModal extends PureComponent<Props, *> {
</Button> </Button>
</> </>
)} )}
</ModalFooter> </Box>
) : null} ) : null
</ModalBody> }
)} />
/> </Modal>
) )
} }
} }

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

@ -1,54 +1,28 @@
// @flow // @flow
/* eslint-disable jsx-a11y/click-events-have-key-events */
/* eslint-disable jsx-a11y/no-static-element-interactions */ /* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable react/no-multi-comp */ /* eslint-disable jsx-a11y/click-events-have-key-events */
import React, { Component } from 'react' import React, { PureComponent, Fragment } from 'react'
import { findDOMNode } from 'react-dom' import { createPortal } from 'react-dom'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import Mortal from 'react-mortal'
import styled from 'styled-components'
import noop from 'lodash/noop' import noop from 'lodash/noop'
import { EXPERIMENTAL_CENTER_MODAL } from 'config/constants' import Animated from 'animated/lib/targets/react-dom'
import Easing from 'animated/lib/Easing'
import { rgba } from 'styles/helpers'
import { radii } from 'styles/theme'
import { closeModal, isModalOpened, getModalData } from 'reducers/modals' import { closeModal, isModalOpened, getModalData } from 'reducers/modals'
import { colors } from 'styles/theme'
import Box from 'components/base/Box'
import GrowScroll from 'components/base/GrowScroll'
export { default as ModalBody } from './ModalBody' export { default as ModalBody } from './ModalBody'
export { default as ConfirmModal } from './ConfirmModal'
export { default as RepairModal } from './RepairModal'
export { default as ModalTitle } from './ModalTitle'
const springConfig = { const animShowHide = {
stiffness: 320, duration: 200,
easing: Easing.bezier(0.3, 1.0, 0.5, 0.8),
} }
type OwnProps = { const domNode = process.env.STORYBOOK_ENV ? document.body : document.getElementById('modals')
name?: string, // eslint-disable-line
isOpened?: boolean,
onBeforeOpen?: ({ data: * }) => *, // eslint-disable-line
onClose?: () => void,
onHide?: () => void,
preventBackdropClick?: boolean,
render: Function,
refocusWhenChange?: string,
width?: string,
}
type Props = OwnProps & { const mapStateToProps = (state, { name, isOpened, onBeforeOpen }: Props): * => {
isOpened?: boolean,
data?: any,
} & {
onClose?: () => void,
}
const mapStateToProps = (state, { name, isOpened, onBeforeOpen }: OwnProps): * => {
const data = getModalData(state, name || '') const data = getModalData(state, name || '')
const modalOpened = isOpened || (name && isModalOpened(state, name)) const modalOpened = isOpened || (name && isModalOpened(state, name))
@ -62,7 +36,7 @@ const mapStateToProps = (state, { name, isOpened, onBeforeOpen }: OwnProps): * =
} }
} }
const mapDispatchToProps = (dispatch: *, { name, onClose = noop }: OwnProps): * => ({ const mapDispatchToProps = (dispatch: *, { name, onClose = noop }: Props): * => ({
onClose: name onClose: name
? () => { ? () => {
dispatch(closeModal(name)) dispatch(closeModal(name))
@ -71,178 +45,187 @@ const mapDispatchToProps = (dispatch: *, { name, onClose = noop }: OwnProps): *
: onClose, : onClose,
}) })
const Container = styled(Box).attrs({ export type RenderProps = {
color: 'grey', onClose?: void => void,
sticky: true, data: any,
style: p => ({ }
pointerEvents: p.isVisible ? 'auto' : 'none',
}),
})`
position: fixed;
z-index: 30;
`
const Backdrop = styled(Box).attrs({
bg: p => rgba(p.theme.colors.black, 0.4),
sticky: true,
style: p => ({
opacity: p.op,
}),
})`
position: fixed;
`
const NonClickableHeadArea = styled.div`
position: fixed;
height: 48px;
width: 100%;
top: 0;
left: 0;
z-index: 1;
`
const Wrapper = styled(Box).attrs({
bg: 'transparent',
flow: 4,
style: p => ({
opacity: p.op,
transform: `scale3d(${p.scale}, ${p.scale}, ${p.scale})`,
}),
})`
outline: none;
width: ${p => (p.width ? p.width : '500px')};
z-index: 2;
`
class Pure extends Component<any> {
shouldComponentUpdate(nextProps) {
if (nextProps.isAnimated) {
return false
}
return true
}
render() { type Props = {
const { data, onClose, render } = this.props isOpened?: boolean,
children?: any,
centered?: boolean,
onClose?: void => void,
onHide?: void => void,
render?: RenderProps => any,
data?: any,
preventBackdropClick?: boolean,
return render({ data, onClose }) name?: string, // eslint-disable-line
} onBeforeOpen?: ({ data: * }) => *, // eslint-disable-line
} }
function stopPropagation(e) { type State = {
e.stopPropagation() animShowHide: Animated.Value,
isInDOM: boolean,
} }
const wrap = EXPERIMENTAL_CENTER_MODAL class Modal extends PureComponent<Props, State> {
? children => ( state = {
<Box alignItems="center" justifyContent="center" grow> animShowHide: new Animated.Value(0),
{children} isInDOM: this.props.isOpened === true,
</Box> }
)
: children => (
<GrowScroll alignItems="center" full pt={8}>
{children}
</GrowScroll>
)
export class Modal extends Component<Props> { static getDerivedStateFromProps(nextProps: Props) {
static defaultProps = { const patch = {}
isOpened: false, if (nextProps.isOpened) {
onHide: noop, patch.isInDOM = true
preventBackdropClick: false, }
return patch
} }
shouldComponentUpdate(nextProps: Props) { componentDidMount() {
if (this.props.isOpened || nextProps.isOpened) { if (this.props.isOpened) {
return true this.animateEnter()
} }
return false this.state.animShowHide.addListener(({ value }) => {
if (value === 0) {
const { onHide } = this.props
this.setState({ isInDOM: false })
if (onHide) {
onHide()
}
}
if (value === 1) this.setState({ isInDOM: true })
})
document.addEventListener('keyup', this.handleKeyup)
} }
componentDidUpdate(prevProps: Props) { componentDidUpdate(prevProps: Props) {
const didOpened = this.props.isOpened && !prevProps.isOpened const didOpened = !prevProps.isOpened && this.props.isOpened
const didClose = !this.props.isOpened && prevProps.isOpened const didClosed = prevProps.isOpened && !this.props.isOpened
const shouldFocus = didOpened || this.props.refocusWhenChange !== prevProps.refocusWhenChange
if (didOpened) { if (didOpened) {
// Store a reference to the last active element, to restore it after this.animateEnter()
// modal close
this._lastFocusedElement = document.activeElement
}
if (shouldFocus) {
this.focusWrapper()
} }
if (didClose) { if (didClosed) {
if (this._lastFocusedElement) { this.animateLeave()
this._lastFocusedElement.focus()
}
} }
} }
_wrapper = null componentWillUnmount() {
_lastFocusedElement = null document.removeEventListener('keyup', this.handleKeyup)
}
handleKeyup = (e: KeyboardEvent) => {
const { onClose, preventBackdropClick } = this.props
if (e.which === 27 && onClose && !preventBackdropClick) {
onClose()
}
}
focusWrapper = () => { handleClickOnBackdrop = () => {
// Forced to use findDOMNode here, because innerRef is giving a proxied component const { preventBackdropClick, onClose } = this.props
const domWrapper = findDOMNode(this._wrapper) // eslint-disable-line react/no-find-dom-node if (!preventBackdropClick && onClose) {
if (domWrapper instanceof HTMLDivElement && !domWrapper.contains(this._lastFocusedElement)) { onClose()
domWrapper.focus()
} }
} }
swallowClick = e => {
e.preventDefault()
e.stopPropagation()
}
animateEnter = () =>
Animated.timing(this.state.animShowHide, { ...animShowHide, toValue: 1 }).start()
animateLeave = () =>
Animated.timing(this.state.animShowHide, { ...animShowHide, toValue: 0 }).start()
render() { render() {
const { preventBackdropClick, isOpened, onHide, render, data, onClose, width } = this.props const { animShowHide, isInDOM } = this.state
const { children, render, centered, onClose, data, isOpened } = this.props
return (
<Mortal if (!isInDOM) {
isOpened={isOpened} return null
onClose={onClose} }
onHide={onHide}
closeOnEsc={!preventBackdropClick} const backdropStyle = {
motionStyle={(spring, isVisible) => ({ ...BACKDROP_STYLE,
opacity: spring(isVisible ? 1 : 0, springConfig), opacity: animShowHide,
scale: spring(isVisible ? 1 : 0.95, springConfig), }
})}
> const containerStyle = {
{(m, isVisible, isAnimated) => ( ...CONTAINER_STYLE,
<Container isVisible={isVisible} onClick={preventBackdropClick ? undefined : onClose}> justifyContent: centered ? 'center' : 'flex-start',
<Backdrop op={m.opacity} /> pointerEvents: isOpened ? 'auto' : 'none',
<NonClickableHeadArea onClick={stopPropagation} /> }
{wrap(
<Wrapper const scale = animShowHide.interpolate({
tabIndex={-1} inputRange: [0, 1],
op={m.opacity} outputRange: [1.1, 1],
scale={m.scale} clamp: true,
innerRef={n => (this._wrapper = n)} })
onClick={stopPropagation}
width={width} const bodyWrapperStyle = {
> ...BODY_WRAPPER_STYLE,
<Pure isAnimated={isAnimated} render={render} data={data} onClose={onClose} /> opacity: animShowHide,
</Wrapper>, transform: [{ scale }],
)} }
</Container>
)} const renderProps = {
</Mortal> onClose,
data,
}
const modal = (
<Fragment>
<Animated.div style={backdropStyle} />
<div style={containerStyle} onClick={this.handleClickOnBackdrop}>
<Animated.div style={bodyWrapperStyle} onClick={this.swallowClick}>
{render && render(renderProps)}
{children}
</Animated.div>
</div>
</Fragment>
) )
return domNode ? createPortal(modal, domNode) : null
} }
} }
export const ModalFooter = styled(Box).attrs({ const BACKDROP_STYLE = {
px: 5, pointerEvents: 'none',
py: 3, position: 'fixed',
})` top: 0,
border-top: 2px solid ${p => p.theme.colors.lightGrey}; left: 0,
border-bottom-left-radius: ${radii[1]}px; right: 0,
border-bottom-right-radius: ${radii[1]}px; bottom: 0,
` background: 'rgba(0, 0, 0, 0.4)',
zIndex: 100,
export const ModalContent = styled(Box).attrs({ }
px: 5,
pb: 5, const CONTAINER_STYLE = {
selectable: true, ...BACKDROP_STYLE,
})`` background: 'transparent',
padding: '60px 0 60px 0',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}
const BODY_WRAPPER_STYLE = {
background: 'white',
width: 500,
borderRadius: 3,
boxShadow: 'box-shadow: 0 10px 20px 0 rgba(0, 0, 0, 0.2)',
color: colors.smoke,
flexShrink: 1,
display: 'flex',
flexDirection: 'column',
}
export default connect( export default connect(
mapStateToProps, mapStateToProps,

75
src/components/base/Modal/stories.js

@ -2,50 +2,57 @@
import React from 'react' import React from 'react'
import { storiesOf } from '@storybook/react' import { storiesOf } from '@storybook/react'
import { action } from '@storybook/addon-actions'
import { boolean, text } from '@storybook/addon-knobs' import { boolean, text } from '@storybook/addon-knobs'
import { action } from '@storybook/addon-actions'
import { import Modal from 'components/base/Modal'
Modal, import ModalBody from 'components/base/Modal/ModalBody'
ModalBody, import Input from 'components/base/Input'
ModalTitle, import Label from 'components/base/Label'
ModalContent,
ModalFooter,
ConfirmModal,
} from 'components/base/Modal'
import Box from 'components/base/Box' import Box from 'components/base/Box'
import Button from 'components/base/Button'
const stories = storiesOf('Components/base', module) const stories = storiesOf('Components/base', module)
stories.add('Modal', () => ( stories.add('Modal', () => (
<Modal <Modal
isOpened={boolean('isOpened', true)} isOpened={boolean('isOpened', true)}
centered={boolean('centered', true)}
onClose={action('onClose')}
render={({ onClose }) => ( render={({ onClose }) => (
<ModalBody onClose={onClose}> <ModalBody
<ModalTitle>{'modal title'}</ModalTitle> onClose={onClose}
<ModalContent>{'this is the modal content'}</ModalContent> onBack={action('onBack')}
<ModalFooter horizontal align="center"> title={text('title', 'Send funds')}
<Box grow>{'modal footer'}</Box> render={() => (
<Button primary>{'Next'}</Button> <Box flow={4}>
</ModalFooter> <Box flow={2}>
</ModalBody> <Label>{'first field'}</Label>
)} <Input autoFocus />
/> </Box>
)) <Box horizontal flow={4}>
<Box flow={2} flex={1}>
stories.add('ConfirmModal', () => ( <Label>{'second field'}</Label>
<ConfirmModal <Input />
categoryName="" </Box>
isOpened <Box flow={2} flex={1}>
isDanger={boolean('isDanger', false)} <Label>{'third field'}</Label>
title={text('title', 'Hard reset')} <Input />
subTitle={text('subTitle', 'Are you sure houston?')} </Box>
desc={text( </Box>
'desc', <Box horizontal flow={4}>
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer non nibh diam. In eget ipsum arcu donec finibus', <Box flow={2} flex={1}>
<Label>{'second field'}</Label>
<Input />
</Box>
<Box flow={2} flex={1}>
<Label>{'third field'}</Label>
<Input />
</Box>
</Box>
</Box>
)}
renderFooter={() => 'footer'}
/>
)} )}
onConfirm={action('onConfirm')}
onReject={action('onReject')}
/> />
)) ))

13
src/components/base/SideBar/SideBarListItem.js

@ -11,7 +11,7 @@ export type Props = {
icon?: any, // TODO: type should be more precise, but, eh ¯\_(ツ)_/¯ icon?: any, // TODO: type should be more precise, but, eh ¯\_(ツ)_/¯
disabled?: boolean, disabled?: boolean,
iconActiveColor: ?string, iconActiveColor: ?string,
hasNotif?: boolean, NotifComponent?: React$ComponentType<*>,
isActive?: boolean, isActive?: boolean,
onClick?: void => void, onClick?: void => void,
isActive?: boolean, isActive?: boolean,
@ -24,7 +24,7 @@ class SideBarListItem extends PureComponent<Props> {
label, label,
desc, desc,
iconActiveColor, iconActiveColor,
hasNotif, NotifComponent,
onClick, onClick,
isActive, isActive,
disabled, disabled,
@ -47,7 +47,7 @@ class SideBarListItem extends PureComponent<Props> {
)} )}
{!!desc && desc(this.props)} {!!desc && desc(this.props)}
</Box> </Box>
{!!hasNotif && <Bullet />} {NotifComponent && <NotifComponent />}
</Container> </Container>
) )
} }
@ -85,11 +85,4 @@ const Container = styled(Tabbable).attrs({
}}; }};
` `
const Bullet = styled.div`
background: ${p => p.theme.colors.wallet};
width: 8px;
height: 8px;
border-radius: 100%;
`
export default SideBarListItem export default SideBarListItem

1
src/components/base/SideBar/stories.js

@ -33,7 +33,6 @@ const SIDEBAR_ITEMS = [
label: 'Third very very very very long text very long text very long', label: 'Third very very very very long text very long text very long',
icon: IconControls, icon: IconControls,
iconActiveColor: '#27a2db', iconActiveColor: '#27a2db',
hasNotif: true,
}, },
{ {
key: 'fourth', key: 'fourth',

44
src/components/base/Stepper/index.js

@ -1,12 +1,12 @@
// @flow // @flow
import React, { PureComponent } from 'react' import React, { PureComponent, Fragment } from 'react'
import invariant from 'invariant' import invariant from 'invariant'
import { translate } from 'react-i18next' import { translate } from 'react-i18next'
import type { T } from 'types/common' import type { T } from 'types/common'
import { ModalContent, ModalTitle, ModalFooter, ModalBody } from 'components/base/Modal' import { ModalBody } from 'components/base/Modal'
import Breadcrumb from 'components/Breadcrumb' import Breadcrumb from 'components/Breadcrumb'
type Props = { type Props = {
@ -29,6 +29,7 @@ export type Step = {
shouldRenderFooter?: StepProps => boolean, shouldRenderFooter?: StepProps => boolean,
shouldPreventClose?: boolean | (StepProps => boolean), shouldPreventClose?: boolean | (StepProps => boolean),
onBack?: StepProps => void, onBack?: StepProps => void,
noScroll?: boolean,
} }
type State = { type State = {
@ -72,6 +73,7 @@ class Stepper extends PureComponent<Props, State> {
onBack, onBack,
shouldPreventClose, shouldPreventClose,
shouldRenderFooter, shouldRenderFooter,
noScroll,
} = step } = step
const stepProps: StepProps = { const stepProps: StepProps = {
@ -89,25 +91,27 @@ class Stepper extends PureComponent<Props, State> {
: !!shouldPreventClose : !!shouldPreventClose
return ( return (
<ModalBody onClose={preventClose ? undefined : onClose}> <ModalBody
<ModalTitle onBack={onBack ? () => onBack(stepProps) : undefined}>{title}</ModalTitle> refocusWhenChange={stepId}
<ModalContent> onClose={preventClose ? undefined : onClose}
<Breadcrumb onBack={onBack ? () => onBack(stepProps) : undefined}
mb={6} title={title}
currentStep={stepIndex} noScroll={noScroll}
items={steps} render={() => (
stepsDisabled={disabledSteps} <Fragment>
stepsErrors={errorSteps} <Breadcrumb
/> mb={6}
<StepComponent {...stepProps} /> currentStep={stepIndex}
{children} items={steps}
</ModalContent> stepsDisabled={disabledSteps}
{renderFooter && ( stepsErrors={errorSteps}
<ModalFooter horizontal align="center" justify="flex-end"> />
<StepFooter {...stepProps} /> <StepComponent {...stepProps} />
</ModalFooter> {children}
</Fragment>
)} )}
</ModalBody> renderFooter={renderFooter ? () => <StepFooter {...stepProps} /> : undefined}
/>
) )
} }
} }

6
src/components/layout/Default.js

@ -34,6 +34,8 @@ import IsUnlocked from 'components/IsUnlocked'
import SideBar from 'components/MainSideBar' import SideBar from 'components/MainSideBar'
import TopBar from 'components/TopBar' import TopBar from 'components/TopBar'
import SyncBackground from 'components/SyncBackground' import SyncBackground from 'components/SyncBackground'
import DebugUpdater from 'components/Updater/DebugUpdater'
import SyncContinuouslyPendingOperations from '../SyncContinouslyPendingOperations' import SyncContinuouslyPendingOperations from '../SyncContinouslyPendingOperations'
import HSMStatusBanner from '../HSMStatusBanner' import HSMStatusBanner from '../HSMStatusBanner'
@ -41,7 +43,7 @@ const Main = styled(GrowScroll).attrs({
px: 6, px: 6,
})` })`
outline: none; outline: none;
padding-top: ${p => p.theme.sizes.topBarHeight + p.theme.space[7]}px; padding-top: ${p => p.theme.sizes.topBarHeight + p.theme.space[4]}px;
` `
type Props = { type Props = {
@ -98,6 +100,8 @@ class Default extends Component<Props> {
<ModalComponent key={name} /> <ModalComponent key={name} />
))} ))}
{process.env.DEBUG_UPDATE && <DebugUpdater />}
<SyncContinuouslyPendingOperations priority={20} interval={SYNC_PENDING_INTERVAL} /> <SyncContinuouslyPendingOperations priority={20} interval={SYNC_PENDING_INTERVAL} />
<SyncBackground /> <SyncBackground />

90
src/components/modals/AccountSettingRenderBody.js

@ -1,13 +1,13 @@
// @flow // @flow
import React, { PureComponent } from 'react' import React, { PureComponent, Fragment } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { compose } from 'redux' import { compose } from 'redux'
import get from 'lodash/get' import get from 'lodash/get'
import { translate } from 'react-i18next' import { translate } from 'react-i18next'
import type { Account, Unit, Currency } from '@ledgerhq/live-common/lib/types' import type { Account, Unit, CryptoCurrency } from '@ledgerhq/live-common/lib/types'
import type { T } from 'types/common' import type { T } from 'types/common'
import { MODAL_SETTINGS_ACCOUNT, MAX_ACCOUNT_NAME_SIZE } from 'config/constants' import { MODAL_SETTINGS_ACCOUNT, MAX_ACCOUNT_NAME_SIZE } from 'config/constants'
import { validateNameEdition } from '@ledgerhq/live-common/lib/account' import { validateNameEdition } from '@ledgerhq/live-common/lib/account'
@ -23,18 +23,14 @@ import TrackPage from 'analytics/TrackPage'
import Spoiler from 'components/base/Spoiler' import Spoiler from 'components/base/Spoiler'
import CryptoCurrencyIcon from 'components/CryptoCurrencyIcon' import CryptoCurrencyIcon from 'components/CryptoCurrencyIcon'
import Box from 'components/base/Box' import Box from 'components/base/Box'
import Space from 'components/base/Space'
import Button from 'components/base/Button' import Button from 'components/base/Button'
import Input from 'components/base/Input' import Input from 'components/base/Input'
import Select from 'components/base/Select' import Select from 'components/base/Select'
import SyncAgo from 'components/SyncAgo' import SyncAgo from 'components/SyncAgo'
import { import ConfirmModal from 'components/base/Modal/ConfirmModal'
ModalBody, import ModalBody from 'components/base/Modal/ModalBody'
ModalTitle,
ModalFooter,
ModalContent,
ConfirmModal,
} from 'components/base/Modal'
type State = { type State = {
accountName: ?string, accountName: ?string,
@ -74,7 +70,7 @@ const defaultState = {
isRemoveAccountModalOpen: false, isRemoveAccountModalOpen: false,
} }
class HelperComp extends PureComponent<Props, State> { class AccountSettingRenderBody extends PureComponent<Props, State> {
state = { state = {
...defaultState, ...defaultState,
} }
@ -84,7 +80,6 @@ class HelperComp extends PureComponent<Props, State> {
} }
getAccount(data: Object): Account { getAccount(data: Object): Account {
// FIXME this should be a selector
const { accountName } = this.state const { accountName } = this.state
const account = get(data, 'account', {}) const account = get(data, 'account', {})
@ -129,10 +124,9 @@ class HelperComp extends PureComponent<Props, State> {
}) })
handleSubmit = (account: Account, onClose: () => void) => ( handleSubmit = (account: Account, onClose: () => void) => (
e: SyntheticEvent<HTMLFormElement>, e: SyntheticEvent<HTMLFormElement | HTMLInputElement>,
) => { ) => {
e.preventDefault() e.preventDefault()
const { updateAccount, setDataModal } = this.props const { updateAccount, setDataModal } = this.props
const { accountName, accountUnit, endpointConfig, endpointConfigError } = this.state const { accountName, accountUnit, endpointConfig, endpointConfigError } = this.state
@ -194,10 +188,10 @@ class HelperComp extends PureComponent<Props, State> {
endpointConfigError, endpointConfigError,
} = this.state } = this.state
const { t, onClose, data } = this.props const { t, onClose, data } = this.props
if (!data) return null
const account = this.getAccount(data) const account = this.getAccount(data)
const bridge = getBridgeForCurrency(account.currency) const bridge = getBridgeForCurrency(account.currency)
const usefulData = { const usefulData = {
xpub: account.xpub || undefined, xpub: account.xpub || undefined,
index: account.index, index: account.index,
@ -206,12 +200,15 @@ class HelperComp extends PureComponent<Props, State> {
blockHeight: account.blockHeight, blockHeight: account.blockHeight,
} }
const onSubmit = this.handleSubmit(account, onClose)
return ( return (
<ModalBody onClose={onClose}> <ModalBody
<form onSubmit={this.handleSubmit(account, onClose)}> onClose={onClose}
<TrackPage category="Modal" name="AccountSettings" /> title={t('account.settings.title')}
<ModalTitle>{t('account.settings.title')}</ModalTitle> render={() => (
<ModalContent mb={3}> <Fragment>
<TrackPage category="Modal" name="AccountSettings" />
<Container> <Container>
<Box> <Box>
<OptionRowTitle>{t('account.settings.accountName.title')}</OptionRowTitle> <OptionRowTitle>{t('account.settings.accountName.title')}</OptionRowTitle>
@ -224,6 +221,7 @@ class HelperComp extends PureComponent<Props, State> {
value={account.name} value={account.name}
maxLength={MAX_ACCOUNT_NAME_SIZE} maxLength={MAX_ACCOUNT_NAME_SIZE}
onChange={this.handleChangeName} onChange={this.handleChangeName}
onEnter={onSubmit}
onFocus={e => this.handleFocus(e, 'accountName')} onFocus={e => this.handleFocus(e, 'accountName')}
error={accountNameError} error={accountNameError}
/> />
@ -268,8 +266,7 @@ class HelperComp extends PureComponent<Props, State> {
) : null} ) : null}
<Spoiler textTransform title={t('account.settings.advancedLogs')}> <Spoiler textTransform title={t('account.settings.advancedLogs')}>
<SyncAgo date={account.lastSyncDate} /> <SyncAgo date={account.lastSyncDate} />
<textarea <div
readOnly
style={{ style={{
userSelect: 'text', userSelect: 'text',
border: '1px dashed #f9f9f9', border: '1px dashed #f9f9f9',
@ -277,15 +274,33 @@ class HelperComp extends PureComponent<Props, State> {
color: '#000', color: '#000',
fontFamily: 'monospace', fontFamily: 'monospace',
fontSize: '10px', fontSize: '10px',
height: 200,
outline: 'none', outline: 'none',
padding: '20px', padding: '20px',
width: '100%',
whiteSpace: 'pre-wrap',
wordWrap: 'break-word',
overflow: 'auto',
}} }}
value={JSON.stringify(usefulData, null, 2)} >
/> {JSON.stringify(usefulData, null, 2)}
</div>
</Spoiler> </Spoiler>
</ModalContent> <ConfirmModal
<ModalFooter horizontal> analyticsName="RemoveAccount"
isDanger
isOpened={isRemoveAccountModalOpen}
onClose={this.handleCloseRemoveAccountModal}
onReject={this.handleCloseRemoveAccountModal}
onConfirm={() => this.handleRemoveAccount(account)}
title={t('settings.removeAccountModal.title')}
subTitle={t('common.areYouSure')}
desc={t('settings.removeAccountModal.desc')}
/>
<Space of={20} />
</Fragment>
)}
renderFooter={() => (
<Fragment>
<Button <Button
event="OpenAccountDelete" event="OpenAccountDelete"
danger danger
@ -294,23 +309,12 @@ class HelperComp extends PureComponent<Props, State> {
> >
{t('common.delete')} {t('common.delete')}
</Button> </Button>
<Button event="DoneEditingAccount" ml="auto" type="submit" primary> <Button event="DoneEditingAccount" ml="auto" onClick={onSubmit} primary>
{t('common.apply')} {t('common.apply')}
</Button> </Button>
</ModalFooter> </Fragment>
</form> )}
<ConfirmModal />
analyticsName="RemoveAccount"
isDanger
isOpened={isRemoveAccountModalOpen}
onClose={this.handleCloseRemoveAccountModal}
onReject={this.handleCloseRemoveAccountModal}
onConfirm={() => this.handleRemoveAccount(account)}
title={t('settings.removeAccountModal.title')}
subTitle={t('common.areYouSure')}
desc={t('settings.removeAccountModal.desc')}
/>
</ModalBody>
) )
} }
} }
@ -321,9 +325,9 @@ export default compose(
mapDispatchToProps, mapDispatchToProps,
), ),
translate(), translate(),
)(HelperComp) )(AccountSettingRenderBody)
export function InputLeft({ currency }: { currency: Currency }) { export function InputLeft({ currency }: { currency: CryptoCurrency }) {
return ( return (
<Box ml={2} style={{ justifyContent: 'center' }} color={currency.color}> <Box ml={2} style={{ justifyContent: 'center' }} color={currency.color}>
<CryptoCurrencyIcon currency={currency} size={16} /> <CryptoCurrencyIcon currency={currency} size={16} />

16
src/components/modals/AddAccounts/index.js

@ -9,7 +9,7 @@ import { createStructuredSelector } from 'reselect'
import Track from 'analytics/Track' import Track from 'analytics/Track'
import SyncSkipUnderPriority from 'components/SyncSkipUnderPriority' import SyncSkipUnderPriority from 'components/SyncSkipUnderPriority'
import type { Currency, Account } from '@ledgerhq/live-common/lib/types' import type { CryptoCurrency, Account } from '@ledgerhq/live-common/lib/types'
import { MODAL_ADD_ACCOUNTS } from 'config/constants' import { MODAL_ADD_ACCOUNTS } from 'config/constants'
import type { T, Device } from 'types/common' import type { T, Device } from 'types/common'
@ -24,6 +24,7 @@ import { closeModal } from 'reducers/modals'
import Modal from 'components/base/Modal' import Modal from 'components/base/Modal'
import Stepper from 'components/base/Stepper' import Stepper from 'components/base/Stepper'
import { validateNameEdition } from '@ledgerhq/live-common/lib/account' import { validateNameEdition } from '@ledgerhq/live-common/lib/account'
import logger from 'logger'
import StepChooseCurrency, { StepChooseCurrencyFooter } from './steps/01-step-choose-currency' import StepChooseCurrency, { StepChooseCurrencyFooter } from './steps/01-step-choose-currency'
import StepConnectDevice, { StepConnectDeviceFooter } from './steps/02-step-connect-device' import StepConnectDevice, { StepConnectDeviceFooter } from './steps/02-step-connect-device'
@ -43,6 +44,7 @@ const createSteps = () => {
footer: StepChooseCurrencyFooter, footer: StepChooseCurrencyFooter,
onBack: null, onBack: null,
hideFooter: false, hideFooter: false,
noScroll: true,
}, },
{ {
id: 'connectDevice', id: 'connectDevice',
@ -88,7 +90,7 @@ type State = {
scanStatus: ScanStatus | string, scanStatus: ScanStatus | string,
isAppOpened: boolean, isAppOpened: boolean,
currency: ?Currency, currency: ?CryptoCurrency,
scannedAccounts: Account[], scannedAccounts: Account[],
checkedAccountsIds: string[], checkedAccountsIds: string[],
editedNames: { [_: string]: string }, editedNames: { [_: string]: string },
@ -98,7 +100,7 @@ type State = {
export type StepProps = DefaultStepProps & { export type StepProps = DefaultStepProps & {
t: T, t: T,
currency: ?Currency, currency: ?CryptoCurrency,
device: ?Device, device: ?Device,
isAppOpened: boolean, isAppOpened: boolean,
scannedAccounts: Account[], scannedAccounts: Account[],
@ -110,7 +112,7 @@ export type StepProps = DefaultStepProps & {
onGoStep1: () => void, onGoStep1: () => void,
onCloseModal: () => void, onCloseModal: () => void,
resetScanState: () => void, resetScanState: () => void,
setCurrency: (?Currency) => void, setCurrency: (?CryptoCurrency) => void,
setAppOpened: boolean => void, setAppOpened: boolean => void,
setScanStatus: (ScanStatus, ?Error) => string, setScanStatus: (ScanStatus, ?Error) => string,
setAccountName: (Account, string) => void, setAccountName: (Account, string) => void,
@ -162,9 +164,12 @@ class AddAccounts extends PureComponent<Props, State> {
handleCloseModal = () => this.props.closeModal(MODAL_ADD_ACCOUNTS) handleCloseModal = () => this.props.closeModal(MODAL_ADD_ACCOUNTS)
handleStepChange = (step: Step) => this.setState({ stepId: step.id }) handleStepChange = (step: Step) => this.setState({ stepId: step.id })
handleSetCurrency = (currency: ?Currency) => this.setState({ currency }) handleSetCurrency = (currency: ?CryptoCurrency) => this.setState({ currency })
handleSetScanStatus = (scanStatus: string, err: ?Error = null) => { handleSetScanStatus = (scanStatus: string, err: ?Error = null) => {
if (err) {
logger.critical(err)
}
this.setState({ scanStatus, err }) this.setState({ scanStatus, err })
} }
@ -241,6 +246,7 @@ class AddAccounts extends PureComponent<Props, State> {
return ( return (
<Modal <Modal
centered
name={MODAL_ADD_ACCOUNTS} name={MODAL_ADD_ACCOUNTS}
refocusWhenChange={stepId} refocusWhenChange={stepId}
onHide={() => this.setState({ ...INITIAL_STATE })} onHide={() => this.setState({ ...INITIAL_STATE })}

32
src/components/modals/Debug.js

@ -1,11 +1,12 @@
// @flow // @flow
/* eslint-disable react/jsx-no-literals */ /* eslint-disable react/jsx-no-literals */
import React, { Component } from 'react' import React, { Component, Fragment } from 'react'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { createStructuredSelector } from 'reselect' import { createStructuredSelector } from 'reselect'
import { getCryptoCurrencyById } from '@ledgerhq/live-common/lib/currencies' import { getCryptoCurrencyById } from '@ledgerhq/live-common/lib/currencies'
import { getDerivationScheme, runDerivationScheme } from '@ledgerhq/live-common/lib/derivation' import { getDerivationScheme, runDerivationScheme } from '@ledgerhq/live-common/lib/derivation'
import Modal, { ModalBody, ModalTitle, ModalContent } from 'components/base/Modal' import Modal from 'components/base/Modal'
import ModalBody from 'components/base/Modal/ModalBody'
import { getCurrentDevice } from 'reducers/devices' import { getCurrentDevice } from 'reducers/devices'
import Button from 'components/base/Button' import Button from 'components/base/Button'
import Box from 'components/base/Box' import Box from 'components/base/Box'
@ -127,14 +128,12 @@ class Debug extends Component<*, *> {
const { device } = this.props const { device } = this.props
const { logs } = this.state const { logs } = this.state
return ( return (
<Modal <Modal name="MODAL_DEBUG" centered onHide={this.onHide}>
name="MODAL_DEBUG" <ModalBody
onHide={this.onHide} title="developer internal tools"
render={({ onClose }: *) => ( render={() => (
<ModalBody onClose={onClose}> <Box>
<SyncSkipUnderPriority priority={99999999} /> <SyncSkipUnderPriority priority={99999999} />
<ModalTitle>developer internal tools</ModalTitle>
<ModalContent>
<Box style={{ height: 60, overflow: 'auto' }}> <Box style={{ height: 60, overflow: 'auto' }}>
{device && ( {device && (
<Box horizontal style={{ padding: 10 }}> <Box horizontal style={{ padding: 10 }}>
@ -191,6 +190,7 @@ class Debug extends Component<*, *> {
> >
{logs.map(log => ( {logs.map(log => (
<Box <Box
key={log.txt}
style={{ style={{
userSelect: 'all', userSelect: 'all',
color: log.type === 'error' ? '#c22' : '#888', color: log.type === 'error' ? '#c22' : '#888',
@ -200,6 +200,10 @@ class Debug extends Component<*, *> {
</Box> </Box>
))} ))}
</Box> </Box>
</Box>
)}
renderFooter={() => (
<Fragment>
<Button <Button
style={{ position: 'absolute', right: 30, bottom: 28 }} style={{ position: 'absolute', right: 30, bottom: 28 }}
onClick={() => { onClick={() => {
@ -208,10 +212,10 @@ class Debug extends Component<*, *> {
> >
Clear Clear
</Button> </Button>
</ModalContent> </Fragment>
</ModalBody> )}
)} />
/> </Modal>
) )
} }
} }

47
src/components/modals/Disclaimer.js

@ -7,42 +7,59 @@ import type { T } from 'types/common'
import { MODAL_DISCLAIMER } from 'config/constants' import { MODAL_DISCLAIMER } from 'config/constants'
import Modal, { ModalBody, ModalTitle, ModalContent, ModalFooter } from 'components/base/Modal' import Modal from 'components/base/Modal'
import ModalBody from 'components/base/Modal/ModalBody'
import Button from 'components/base/Button' import Button from 'components/base/Button'
import Box from 'components/base/Box' import Box from 'components/base/Box'
import { HandShield } from 'components/WarnBox' import { HandShield } from 'components/WarnBox'
import { compose } from 'redux'
import connect from 'react-redux/es/connect/connect'
import { closeModal } from '../../reducers/modals'
type Props = { type Props = {
t: T, t: T,
closeModal: string => void,
}
const mapDispatchToProps = {
closeModal,
} }
class DisclaimerModal extends PureComponent<Props> { class DisclaimerModal extends PureComponent<Props> {
onClose = () => this.props.closeModal(MODAL_DISCLAIMER)
render() { render() {
const { t } = this.props const { t } = this.props
return ( return (
<Modal <Modal name={MODAL_DISCLAIMER} centered>
name={MODAL_DISCLAIMER} <ModalBody
render={({ onClose }) => ( onClose={this.onClose}
<ModalBody onClose={onClose}> title={t('disclaimerModal.title')}
<ModalTitle>{t('disclaimerModal.title')}</ModalTitle> render={() => (
<ModalContent flow={4} ff="Open Sans|Regular" fontSize={4} color="smoke"> <Box flow={4} ff="Open Sans|Regular" fontSize={4} color="smoke">
<Box align="center" mt={4} pb={4}> <Box align="center" mt={4} pb={4}>
<HandShield size={55} /> <HandShield size={55} />
</Box> </Box>
<p>{t('disclaimerModal.desc_1')}</p> <p>{t('disclaimerModal.desc_1')}</p>
<p>{t('disclaimerModal.desc_2')}</p> <p>{t('disclaimerModal.desc_2')}</p>
</ModalContent> </Box>
<ModalFooter horizontal justifyContent="flex-end"> )}
<Button data-e2e="continue_button" onClick={onClose} primary> renderFooter={() => (
<Box horizontal justifyContent="flex-end">
<Button data-e2e="continue_button" onClick={this.onClose} primary>
{t('disclaimerModal.cta')} {t('disclaimerModal.cta')}
</Button> </Button>
</ModalFooter> </Box>
</ModalBody> )}
)} />
/> </Modal>
) )
} }
} }
export default translate()(DisclaimerModal) export default compose(
connect(
null,
mapDispatchToProps,
),
translate(),
)(DisclaimerModal)

215
src/components/modals/OperationDetails.js

@ -18,12 +18,10 @@ import { MODAL_OPERATION_DETAILS } from 'config/constants'
import { getMarketColor } from 'styles/helpers' import { getMarketColor } from 'styles/helpers'
import Box from 'components/base/Box' import Box from 'components/base/Box'
import GradientBox from 'components/GradientBox'
import GrowScroll from 'components/base/GrowScroll'
import Button from 'components/base/Button' import Button from 'components/base/Button'
import Bar from 'components/base/Bar' import Bar from 'components/base/Bar'
import FormattedVal from 'components/base/FormattedVal' import FormattedVal from 'components/base/FormattedVal'
import Modal, { ModalBody, ModalTitle, ModalFooter, ModalContent } from 'components/base/Modal' import Modal, { ModalBody } from 'components/base/Modal'
import Text from 'components/base/Text' import Text from 'components/base/Text'
import CopyWithFeedback from 'components/base/CopyWithFeedback' import CopyWithFeedback from 'components/base/CopyWithFeedback'
@ -130,122 +128,120 @@ const OperationDetails = connect(mapStateToProps)((props: Props) => {
const uniqueSenders = uniq(senders) const uniqueSenders = uniq(senders)
return ( return (
<ModalBody onClose={onClose}> <ModalBody
<TrackPage category="Modal" name="OperationDetails" /> title={t('operationDetails.title')}
<ModalTitle>{t('operationDetails.title')}</ModalTitle> onClose={onClose}
<ModalContent relative style={{ height: 500 }} px={0} pb={0}> render={() => (
<GrowScroll px={5} pt={1} pb={8}> <Box flow={3}>
<Box flow={3}> <Box alignItems="center" mt={1}>
<Box alignItems="center" mt={1}> <ConfirmationCheck
<ConfirmationCheck marketColor={marketColor}
marketColor={marketColor} isConfirmed={isConfirmed}
isConfirmed={isConfirmed} style={{
style={{ transform: 'scale(1.5)',
transform: 'scale(1.5)', }}
}} t={t}
t={t} type={type}
type={type} withTooltip={false}
withTooltip={false} />
/> <Box my={4} alignItems="center">
<Box my={4} alignItems="center"> <Box>
<Box> <FormattedVal
<FormattedVal color={amount.isNegative() ? 'smoke' : undefined}
color={amount.isNegative() ? 'smoke' : undefined} unit={unit}
unit={unit} alwaysShowSign
alwaysShowSign showCode
showCode val={amount}
val={amount} fontSize={7}
fontSize={7} disableRounding
disableRounding />
/>
</Box>
<Box mt={1}>
<CounterValue
color="grey"
fontSize={5}
date={date}
currency={currency}
value={amount}
/>
</Box>
</Box>
</Box>
<Box horizontal flow={2}>
<Box flex={1}>
<OpDetailsTitle>{t('operationDetails.account')}</OpDetailsTitle>
<OpDetailsData>{name}</OpDetailsData>
</Box> </Box>
<Box flex={1}> <Box mt={1}>
<OpDetailsTitle>{t('operationDetails.date')}</OpDetailsTitle> <CounterValue
<OpDetailsData>{moment(date).format('LLL')}</OpDetailsData> color="grey"
fontSize={5}
date={date}
currency={currency}
value={amount}
/>
</Box> </Box>
</Box> </Box>
<B /> </Box>
<Box horizontal flow={2}> <Box horizontal flow={2}>
<Box flex={1}> <Box flex={1}>
<OpDetailsTitle>{t('operationDetails.fees')}</OpDetailsTitle> <OpDetailsTitle>{t('operationDetails.account')}</OpDetailsTitle>
{fee ? ( <OpDetailsData>{name}</OpDetailsData>
<Fragment>
<OpDetailsData>
<FormattedVal unit={unit} showCode val={fee} color="smoke" />
</OpDetailsData>
</Fragment>
) : (
<OpDetailsData>{t('operationDetails.noFees')}</OpDetailsData>
)}
</Box>
<Box flex={1}>
<OpDetailsTitle>{t('operationDetails.status')}</OpDetailsTitle>
<OpDetailsData color={isConfirmed ? 'positiveGreen' : null} horizontal flow={1}>
<Box>
{isConfirmed
? t('operationDetails.confirmed')
: t('operationDetails.notConfirmed')}
</Box>
<Box>{`(${confirmations})`}</Box>
</OpDetailsData>
</Box>
</Box> </Box>
<B /> <Box flex={1}>
<Box> <OpDetailsTitle>{t('operationDetails.date')}</OpDetailsTitle>
<OpDetailsTitle>{t('operationDetails.identifier')}</OpDetailsTitle> <OpDetailsData>{moment(date).format('LLL')}</OpDetailsData>
<OpDetailsData>
<Ellipsis canSelect>{hash}</Ellipsis>
<GradientHover>
<CopyWithFeedback text={hash} />
</GradientHover>
</OpDetailsData>
</Box> </Box>
<B /> </Box>
<Box> <B />
<OpDetailsTitle>{t('operationDetails.from')}</OpDetailsTitle> <Box horizontal flow={2}>
<DataList lines={uniqueSenders} t={t} /> <Box flex={1}>
<OpDetailsTitle>{t('operationDetails.fees')}</OpDetailsTitle>
{fee ? (
<Fragment>
<OpDetailsData>
<FormattedVal unit={unit} showCode val={fee} color="smoke" />
</OpDetailsData>
</Fragment>
) : (
<OpDetailsData>{t('operationDetails.noFees')}</OpDetailsData>
)}
</Box> </Box>
<B /> <Box flex={1}>
<Box> <OpDetailsTitle>{t('operationDetails.status')}</OpDetailsTitle>
<OpDetailsTitle>{t('operationDetails.to')}</OpDetailsTitle> <OpDetailsData color={isConfirmed ? 'positiveGreen' : null} horizontal flow={1}>
<DataList lines={recipients} t={t} /> <Box>
{isConfirmed
? t('operationDetails.confirmed')
: t('operationDetails.notConfirmed')}
</Box>
<Box>{`(${confirmations})`}</Box>
</OpDetailsData>
</Box> </Box>
{Object.entries(extra).map(([key, value]) => (
<Box key={key}>
<OpDetailsTitle>
<Trans i18nKey={`operationDetails.extra.${key}`} defaults={key} />
</OpDetailsTitle>
<OpDetailsData>{value}</OpDetailsData>
</Box>
))}
</Box> </Box>
</GrowScroll> <B />
<GradientBox /> <Box>
</ModalContent> <OpDetailsTitle>{t('operationDetails.identifier')}</OpDetailsTitle>
<OpDetailsData>
{url && ( <Ellipsis canSelect>{hash}</Ellipsis>
<ModalFooter horizontal justify="flex-end" flow={2}> <GradientHover>
<CopyWithFeedback text={hash} />
</GradientHover>
</OpDetailsData>
</Box>
<B />
<Box>
<OpDetailsTitle>{t('operationDetails.from')}</OpDetailsTitle>
<DataList lines={uniqueSenders} t={t} />
</Box>
<B />
<Box>
<OpDetailsTitle>{t('operationDetails.to')}</OpDetailsTitle>
<DataList lines={recipients} t={t} />
</Box>
{Object.entries(extra).map(([key, value]) => (
<Box key={key}>
<OpDetailsTitle>
<Trans i18nKey={`operationDetails.extra.${key}`} defaults={key} />
</OpDetailsTitle>
<OpDetailsData>{value}</OpDetailsData>
</Box>
))}
</Box>
)}
renderFooter={() =>
url && (
<Button primary onClick={() => openURL(url)}> <Button primary onClick={() => openURL(url)}>
{t('operationDetails.viewOperation')} {t('operationDetails.viewOperation')}
</Button> </Button>
</ModalFooter> )
)} }
>
<TrackPage category="Modal" name="OperationDetails" />
</ModalBody> </ModalBody>
) )
}) })
@ -255,12 +251,13 @@ type ModalRenderProps = {
account: string, account: string,
operation: string, operation: string,
}, },
onClose: Function, onClose?: Function,
} }
const OperationDetailsWrapper = ({ t }: { t: T }) => ( const OperationDetailsWrapper = ({ t }: { t: T }) => (
<Modal <Modal
name={MODAL_OPERATION_DETAILS} name={MODAL_OPERATION_DETAILS}
centered
render={(props: ModalRenderProps) => { render={(props: ModalRenderProps) => {
const { data, onClose } = props const { data, onClose } = props
return <OperationDetails t={t} {...data} onClose={onClose} /> return <OperationDetails t={t} {...data} onClose={onClose} />

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

@ -8,6 +8,7 @@ import { createStructuredSelector } from 'reselect'
import SyncSkipUnderPriority from 'components/SyncSkipUnderPriority' import SyncSkipUnderPriority from 'components/SyncSkipUnderPriority'
import logger from 'logger'
import Track from 'analytics/Track' import Track from 'analytics/Track'
import type { Account } from '@ledgerhq/live-common/lib/types' import type { Account } from '@ledgerhq/live-common/lib/types'
@ -141,6 +142,9 @@ class ReceiveModal extends PureComponent<Props, State> {
handleChangeAppOpened = (isAppOpened: boolean) => this.setState({ isAppOpened }) handleChangeAppOpened = (isAppOpened: boolean) => this.setState({ isAppOpened })
handleChangeAddressVerified = (isAddressVerified: boolean, err: ?Error) => { handleChangeAddressVerified = (isAddressVerified: boolean, err: ?Error) => {
if (err && err.name !== 'UserRefusedAddress') {
logger.critical(err)
}
this.setState({ isAddressVerified, verifyAddressError: err }) this.setState({ isAddressVerified, verifyAddressError: err })
} }
@ -193,16 +197,17 @@ class ReceiveModal extends PureComponent<Props, State> {
return ( return (
<Modal <Modal
name={MODAL_RECEIVE} name={MODAL_RECEIVE}
centered
refocusWhenChange={stepId} refocusWhenChange={stepId}
onHide={this.handleReset} onHide={this.handleReset}
preventBackdropClick={isModalLocked} preventBackdropClick={isModalLocked}
onBeforeOpen={this.handleBeforeOpenModal} onBeforeOpen={this.handleBeforeOpenModal}
render={({ onClose }) => ( render={() => (
<Stepper <Stepper
title={t('receive.title')} title={t('receive.title')}
initialStepId={stepId} initialStepId={stepId}
onStepChange={this.handleStepChange} onStepChange={this.handleStepChange}
onClose={onClose} onClose={addtionnalProps.closeModal}
steps={this.STEPS} steps={this.STEPS}
disabledSteps={disabledSteps} disabledSteps={disabledSteps}
errorSteps={errorSteps} errorSteps={errorSteps}

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

@ -7,14 +7,13 @@ import network from 'api/network'
import Button from 'components/base/Button' import Button from 'components/base/Button'
import Box from 'components/base/Box' import Box from 'components/base/Box'
import GrowScroll from 'components/base/GrowScroll'
import Text from 'components/base/Text' import Text from 'components/base/Text'
import Spinner from 'components/base/Spinner' import Spinner from 'components/base/Spinner'
import GradientBox from 'components/GradientBox' import GradientBox from 'components/GradientBox'
import TranslatedError from 'components/TranslatedError' import TranslatedError from 'components/TranslatedError'
import TrackPage from 'analytics/TrackPage' import TrackPage from 'analytics/TrackPage'
import Markdown, { Notes } from 'components/base/Markdown' import Markdown, { Notes } from 'components/base/Markdown'
import { ModalBody, ModalTitle, ModalContent, ModalFooter } from 'components/base/Modal' import ModalBody from 'components/base/Modal/ModalBody'
import type { T } from 'types/common' import type { T } from 'types/common'
@ -115,21 +114,26 @@ class ReleaseNotesBody extends PureComponent<Props, State> {
const { onClose, t } = this.props const { onClose, t } = this.props
return ( return (
<ModalBody onClose={onClose}> <ModalBody
<TrackPage category="Modal" name="ReleaseNotes" /> onClose={onClose}
<ModalTitle>{t('releaseNotes.title')}</ModalTitle> title={t('releaseNotes.title')}
<ModalContent relative style={{ height: 500 }} px={0} pb={0}> render={() => (
<GrowScroll px={5} pb={8}> <Box relative style={{ height: 500 }} px={0} pb={0}>
{this.renderContent()} <TrackPage category="Modal" name="ReleaseNotes" />
</GrowScroll> <Box px={5} pb={8}>
<GradientBox /> {this.renderContent()}
</ModalContent> </Box>
<ModalFooter horizontal justifyContent="flex-end"> <GradientBox />
<Button onClick={onClose} primary> </Box>
{t('common.continue')} )}
</Button> renderFooter={() => (
</ModalFooter> <Box horizontal justifyContent="flex-end">
</ModalBody> <Button onClick={onClose} primary>
{t('common.continue')}
</Button>
</Box>
)}
/>
) )
} }
} }

56
src/components/modals/ReleaseNotes/index.js

@ -1,16 +1,56 @@
// @flow // @flow
import React from 'react'
import React, { PureComponent } from 'react'
import { connect } from 'react-redux'
import semver from 'semver'
import type { State } from 'reducers'
import { openModal } from 'reducers/modals'
import { lastUsedVersionSelector } from 'reducers/settings'
import { saveSettings } from 'actions/settings'
import { MODAL_RELEASES_NOTES } from 'config/constants' import { MODAL_RELEASES_NOTES } from 'config/constants'
import Modal from 'components/base/Modal' import Modal from 'components/base/Modal'
import ReleaseNotesBody from './ReleaseNotesBody' import ReleaseNotesBody from './ReleaseNotesBody'
const ReleaseNotesModal = () => ( type Props = {
<Modal openModal: Function,
name={MODAL_RELEASES_NOTES} saveSettings: Function,
render={({ data, onClose }) => <ReleaseNotesBody version={data} onClose={onClose} />} lastUsedVersion: string,
/> }
)
const mapStateToProps = (state: State) => ({
lastUsedVersion: lastUsedVersionSelector(state),
})
const mapDispatchToProps = {
openModal,
saveSettings,
}
class ReleaseNotesModal extends PureComponent<Props> {
componentDidMount() {
const { lastUsedVersion, saveSettings, openModal } = this.props
const currentVersion = __APP_VERSION__
if (semver.gt(currentVersion, lastUsedVersion)) {
openModal(MODAL_RELEASES_NOTES, currentVersion)
saveSettings({ lastUsedVersion: currentVersion })
}
}
render() {
return (
<Modal
name={MODAL_RELEASES_NOTES}
centered
render={({ data, onClose }) => <ReleaseNotesBody version={data} onClose={onClose} />}
/>
)
}
}
export default ReleaseNotesModal export default connect(
mapStateToProps,
mapDispatchToProps,
)(ReleaseNotesModal)

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

@ -53,8 +53,8 @@ class RecipientField<Transaction> extends Component<
const { account, bridge, transaction } = this.props const { account, bridge, transaction } = this.props
const syncId = ++this.syncId const syncId = ++this.syncId
const recipient = bridge.getTransactionRecipient(account, transaction) const recipient = bridge.getTransactionRecipient(account, transaction)
const isValid = await bridge.isRecipientValid(account.currency, recipient) const isValid = await bridge.isRecipientValid(account, recipient)
const warning = await bridge.getRecipientWarning(account.currency, recipient) const warning = await bridge.getRecipientWarning(account, recipient)
if (syncId !== this.syncId) return if (syncId !== this.syncId) return
if (this.isUnmounted) return if (this.isUnmounted) return
this.setState({ isValid, warning }) this.setState({ isValid, warning })
@ -69,9 +69,7 @@ class RecipientField<Transaction> extends Component<
if (amount) { if (amount) {
t = bridge.editTransactionAmount(account, t, amount) t = bridge.editTransactionAmount(account, t, amount)
} }
const warning = fromQRCode const warning = fromQRCode ? await bridge.getRecipientWarning(account, recipient) : null
? await bridge.getRecipientWarning(account.currency, recipient)
: null
if (this.isUnmounted) return false if (this.isUnmounted) return false
if (warning) { if (warning) {
// clear the input if field has warning AND has a warning // clear the input if field has warning AND has a warning
@ -97,7 +95,7 @@ class RecipientField<Transaction> extends Component<
const error = const error =
!value || isValid !value || isValid
? QRCodeRefusedReason ? QRCodeRefusedReason
: new InvalidAddress(null, { currencyName: account.currency.name }) : warning || new InvalidAddress(null, { currencyName: account.currency.name })
return ( return (
<Box flow={1}> <Box flow={1}>

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

@ -12,6 +12,7 @@ import Track from 'analytics/Track'
import { updateAccountWithUpdater } from 'actions/accounts' import { updateAccountWithUpdater } from 'actions/accounts'
import { MODAL_SEND } from 'config/constants' import { MODAL_SEND } from 'config/constants'
import { getBridgeForCurrency } from 'bridge' import { getBridgeForCurrency } from 'bridge'
import logger from 'logger'
import type { WalletBridge } from 'bridge/types' import type { WalletBridge } from 'bridge/types'
import type { T, Device } from 'types/common' import type { T, Device } from 'types/common'
@ -180,6 +181,9 @@ class SendModal extends PureComponent<Props, State<*>> {
} }
handleTransactionError = (error: Error) => { handleTransactionError = (error: Error) => {
if (!(error instanceof UserRefusedOnDevice)) {
logger.critical(error)
}
const stepVerificationIndex = this.STEPS.findIndex(step => step.id === 'verification') const stepVerificationIndex = this.STEPS.findIndex(step => step.id === 'verification')
if (stepVerificationIndex === -1) return if (stepVerificationIndex === -1) return
this.setState({ error }) this.setState({ error })
@ -273,6 +277,7 @@ class SendModal extends PureComponent<Props, State<*>> {
return ( return (
<Modal <Modal
name={MODAL_SEND} name={MODAL_SEND}
centered
refocusWhenChange={stepId} refocusWhenChange={stepId}
onHide={this.handleReset} onHide={this.handleReset}
preventBackdropClick={isModalLocked} preventBackdropClick={isModalLocked}

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

@ -133,7 +133,7 @@ export class StepAmountFooter extends PureComponent<
const totalSpent = await bridge.getTotalSpent(account, transaction) const totalSpent = await bridge.getTotalSpent(account, transaction)
if (syncId !== this.syncId) return if (syncId !== this.syncId) return
const isRecipientValid = await bridge.isRecipientValid( const isRecipientValid = await bridge.isRecipientValid(
account.currency, account,
bridge.getTransactionRecipient(account, transaction), bridge.getTransactionRecipient(account, transaction),
) )
if (syncId !== this.syncId) return if (syncId !== this.syncId) return

5
src/components/modals/SettingsAccount.js

@ -11,7 +11,10 @@ export default class SettingsAccount extends PureComponent<*, *> {
return ( return (
<Modal <Modal
name={MODAL_SETTINGS_ACCOUNT} name={MODAL_SETTINGS_ACCOUNT}
render={({ data, onClose }) => <AccountSettingRenderBody data={data} onClose={onClose} />} centered
render={({ data, onClose }) => (
<AccountSettingRenderBody {...this.props} data={data} onClose={onClose} />
)}
/> />
) )
} }

62
src/components/modals/ShareAnalytics.js

@ -1,20 +1,30 @@
// @flow // @flow
import React, { PureComponent } from 'react' import React, { Fragment, PureComponent } from 'react'
import { translate } from 'react-i18next' import { translate } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
import { MODAL_SHARE_ANALYTICS } from 'config/constants' import { MODAL_SHARE_ANALYTICS } from 'config/constants'
import Modal, { ModalBody, ModalTitle, ModalContent, ModalFooter } from 'components/base/Modal' import Modal from 'components/base/Modal'
import Button from 'components/base/Button' import Button from 'components/base/Button'
import Box from 'components/base/Box' import Box from 'components/base/Box'
import { connect } from 'react-redux'
import { compose } from 'redux'
import type { T } from 'types/common' import type { T } from 'types/common'
import { closeModal } from '../../reducers/modals'
import ModalBody from '../base/Modal/ModalBody'
type Props = { type Props = {
t: T, t: T,
closeModal: string => void,
} }
const mapDispatchToProps = {
closeModal,
}
class ShareAnalytics extends PureComponent<Props, *> { class ShareAnalytics extends PureComponent<Props, *> {
onClose = () => this.props.closeModal(MODAL_SHARE_ANALYTICS)
render() { render() {
const { t } = this.props const { t } = this.props
const items = [ const items = [
@ -56,30 +66,38 @@ class ShareAnalytics extends PureComponent<Props, *> {
}, },
] ]
return ( return (
<Modal <Modal name={MODAL_SHARE_ANALYTICS} centered>
name={MODAL_SHARE_ANALYTICS} <ModalBody
render={({ onClose }) => ( onClose={this.onClose}
<ModalBody onClose={onClose}> title={t('onboarding.analytics.shareAnalytics.title')}
<ModalTitle data-e2e="modal_title_shareAnalytics"> render={() => (
{t('onboarding.analytics.shareAnalytics.title')} <Fragment>
</ModalTitle> <InlineDesc>{t('onboarding.analytics.shareAnalytics.desc')}</InlineDesc>
<InlineDesc>{t('onboarding.analytics.shareAnalytics.desc')}</InlineDesc> <Box mx={5}>
<ModalContent mx={5}> <Ul>{items.map(item => <li key={item.key}>{item.desc}</li>)}</Ul>
<Ul>{items.map(item => <li key={item.key}>{item.desc}</li>)}</Ul> </Box>
</ModalContent> </Fragment>
<ModalFooter horizontal justifyContent="flex-end"> )}
<Button onClick={onClose} primary data-e2e="modal_buttonClose_shareAnalytics"> renderFooter={() => (
<Fragment>
<Button onClick={this.onClose} primary data-e2e="modal_buttonClose_shareAnalytics">
{t('common.close')} {t('common.close')}
</Button> </Button>
</ModalFooter> </Fragment>
</ModalBody> )}
)} />
/> </Modal>
) )
} }
} }
export default translate()(ShareAnalytics) export default compose(
connect(
null,
mapDispatchToProps,
),
translate(),
)(ShareAnalytics)
export const Ul = styled.ul.attrs({ export const Ul = styled.ul.attrs({
ff: 'Open Sans|Regular', ff: 'Open Sans|Regular',
@ -93,5 +111,5 @@ export const InlineDesc = styled(Box).attrs({
ff: 'Open Sans|SemiBold', ff: 'Open Sans|SemiBold',
fontSize: 4, fontSize: 4,
color: 'dark', color: 'dark',
mx: '45px', mx: '15px',
})`` })``

59
src/components/modals/TechnicalData.js

@ -1,19 +1,30 @@
// @flow // @flow
import React, { PureComponent } from 'react' import React, { Fragment, PureComponent } from 'react'
import { translate } from 'react-i18next' import { translate } from 'react-i18next'
import { MODAL_TECHNICAL_DATA } from 'config/constants' import { MODAL_TECHNICAL_DATA } from 'config/constants'
import Modal, { ModalBody, ModalTitle, ModalContent, ModalFooter } from 'components/base/Modal' import Modal, { ModalBody } from 'components/base/Modal'
import Button from 'components/base/Button' import Button from 'components/base/Button'
import type { T } from 'types/common' import type { T } from 'types/common'
import { connect } from 'react-redux'
import { compose } from 'redux'
import Box from 'components/base/Box'
import { Ul, InlineDesc } from './ShareAnalytics' import { Ul, InlineDesc } from './ShareAnalytics'
import { closeModal } from '../../reducers/modals'
type Props = { type Props = {
t: T, t: T,
closeModal: string => void,
}
const mapDispatchToProps = {
closeModal,
} }
class TechnicalData extends PureComponent<Props, *> { class TechnicalData extends PureComponent<Props, *> {
onClose = () => this.props.closeModal(MODAL_TECHNICAL_DATA)
render() { render() {
const { t } = this.props const { t } = this.props
@ -33,27 +44,35 @@ class TechnicalData extends PureComponent<Props, *> {
] ]
return ( return (
<Modal <Modal name={MODAL_TECHNICAL_DATA} centered>
name={MODAL_TECHNICAL_DATA} <ModalBody
render={({ onClose }) => ( onClose={this.onClose}
<ModalBody onClose={onClose}> title={t('onboarding.analytics.technicalData.mandatoryContextual.title')}
<ModalTitle data-e2e="modal_title_TechData"> render={() => (
{t('onboarding.analytics.technicalData.mandatoryContextual.title')} <Fragment>
</ModalTitle> <InlineDesc>{t('onboarding.analytics.technicalData.desc')}</InlineDesc>
<InlineDesc>{t('onboarding.analytics.technicalData.desc')}</InlineDesc> <Box mx={5}>
<ModalContent mx={5}> <Ul>{items.map(item => <li key={item.key}>{item.desc}</li>)}</Ul>
<Ul>{items.map(item => <li key={item.key}>{item.desc}</li>)}</Ul> </Box>
</ModalContent> </Fragment>
<ModalFooter horizontal justifyContent="flex-end"> )}
<Button onClick={onClose} primary data-e2e="modal_buttonClose_techData"> renderFooter={() => (
<Fragment>
<Button onClick={this.onClose} primary data-e2e="modal_buttonClose_techData">
{t('common.close')} {t('common.close')}
</Button> </Button>
</ModalFooter> </Fragment>
</ModalBody> )}
)} />
/> </Modal>
) )
} }
} }
export default translate()(TechnicalData) export default compose(
connect(
null,
mapDispatchToProps,
),
translate(),
)(TechnicalData)

78
src/components/modals/UpdateFirmware/Disclaimer.js

@ -1,12 +1,13 @@
// @flow // @flow
/* eslint react/jsx-no-literals: 0 */ /* eslint react/jsx-no-literals: 0 */
import React, { PureComponent, Fragment } from 'react' import React, { PureComponent } from 'react'
import { translate, Trans } from 'react-i18next' import { translate, Trans } from 'react-i18next'
import type { OsuFirmware, FinalFirmware } from '@ledgerhq/live-common/lib/types/manager' import type { OsuFirmware, FinalFirmware } from '@ledgerhq/live-common/lib/types/manager'
import type { T } from 'types/common' import type { T } from 'types/common'
import Modal, { ModalBody, ModalFooter, ModalTitle, ModalContent } from 'components/base/Modal' import Modal from 'components/base/Modal'
import ModalBody from 'components/base/Modal/ModalBody'
import Text from 'components/base/Text' import Text from 'components/base/Text'
import Button from 'components/base/Button' import Button from 'components/base/Button'
import GrowScroll from 'components/base/GrowScroll' import GrowScroll from 'components/base/GrowScroll'
@ -17,6 +18,7 @@ import TrackPage from 'analytics/TrackPage'
import type { ModalStatus } from 'components/ManagerPage/FirmwareUpdate' import type { ModalStatus } from 'components/ManagerPage/FirmwareUpdate'
import { getCleanVersion } from 'components/ManagerPage/FirmwareUpdate' import { getCleanVersion } from 'components/ManagerPage/FirmwareUpdate'
import Box from '../../base/Box/Box'
type Props = { type Props = {
t: T, t: T,
@ -35,49 +37,51 @@ class DisclaimerModal extends PureComponent<Props, State> {
render(): React$Node { render(): React$Node {
const { status, firmware, onClose, t, goToNextStep } = this.props const { status, firmware, onClose, t, goToNextStep } = this.props
return ( return (
<Modal <Modal isOpened={status === 'disclaimer'} onClose={onClose}>
isOpened={status === 'disclaimer'} <TrackPage category="Manager" name="DisclaimerModal" />
onClose={onClose} <ModalBody
render={({ onClose }) => ( grow
<ModalBody onClose={onClose} grow align="center" justify="center" mt={3}> align="center"
<TrackPage category="Manager" name="DisclaimerModal" /> justify="center"
<Fragment> mt={3}
<ModalTitle>{t('manager.firmware.update')}</ModalTitle> title={t('manager.firmware.update')}
<ModalContent> render={() => (
<Text ff="Open Sans|Regular" fontSize={4} color="graphite" align="center"> <Box>
<Trans i18nKey="manager.firmware.disclaimerTitle"> <Text ff="Open Sans|Regular" fontSize={4} color="graphite" align="center">
You are about to install <Trans i18nKey="manager.firmware.disclaimerTitle">
<Text ff="Open Sans|SemiBold" color="dark"> You are about to install
{`firmware version ${ <Text ff="Open Sans|SemiBold" color="dark">
firmware && firmware.osu ? getCleanVersion(firmware.osu.name) : '' {`firmware version ${
}`} firmware && firmware.osu ? getCleanVersion(firmware.osu.name) : ''
</Text> }`}
</Trans> </Text>
</Text> </Trans>
<Text ff="Open Sans|Regular" fontSize={4} color="graphite" align="center"> </Text>
{t('manager.firmware.disclaimerAppDelete')} <Text ff="Open Sans|Regular" fontSize={4} color="graphite" align="center">
{t('manager.firmware.disclaimerAppReinstall')} {t('manager.firmware.disclaimerAppDelete')}
</Text> {t('manager.firmware.disclaimerAppReinstall')}
</ModalContent> </Text>
{firmware && firmware.osu ? ( {firmware && firmware.osu ? (
<ModalContent relative pb={0} style={{ height: 250, width: '100%' }}> <Box relative pb={0} style={{ height: 250, width: '100%' }}>
<GrowScroll pb={5}> <GrowScroll pb={5}>
<Notes> <Notes>
<Markdown>{firmware.osu.notes}</Markdown> <Markdown>{firmware.osu.notes}</Markdown>
</Notes> </Notes>
</GrowScroll> </GrowScroll>
<GradientBox /> <GradientBox />
</ModalContent> </Box>
) : null} ) : null}
<ModalFooter horizontal justifyContent="flex-end" style={{ width: '100%' }}> </Box>
<Button primary onClick={goToNextStep}> )}
{t('common.continue')} renderFooter={() => (
</Button> <Box horizontal justifyContent="flex-end">
</ModalFooter> <Button primary onClick={goToNextStep}>
</Fragment> {t('common.continue')}
</ModalBody> </Button>
)} </Box>
/> )}
/>
</Modal>
) )
} }
} }

7
src/components/modals/UpdateFirmware/index.js

@ -11,6 +11,7 @@ import type { FirmwareUpdateContext } from '@ledgerhq/live-common/lib/types/mana
import type { StepProps as DefaultStepProps, Step } from 'components/base/Stepper' import type { StepProps as DefaultStepProps, Step } from 'components/base/Stepper'
import type { ModalStatus } from 'components/ManagerPage/FirmwareUpdate' import type { ModalStatus } from 'components/ManagerPage/FirmwareUpdate'
import logger from 'logger'
import { FreezeDeviceChangeEvents } from '../../ManagerPage/HookDeviceChange' import { FreezeDeviceChangeEvents } from '../../ManagerPage/HookDeviceChange'
import StepFullFirmwareInstall from './steps/01-step-install-full-firmware' import StepFullFirmwareInstall from './steps/01-step-install-full-firmware'
@ -82,7 +83,10 @@ class UpdateModal extends PureComponent<Props, State> {
t: this.props.t, t: this.props.t,
}) })
setError = (e: Error) => this.setState({ error: e }) setError = (e: Error) => {
logger.critical(e)
this.setState({ error: e })
}
handleReset = () => this.setState({ stepId: 'idCheck', error: null, nonce: this.state.nonce++ }) handleReset = () => this.setState({ stepId: 'idCheck', error: null, nonce: this.state.nonce++ })
@ -103,6 +107,7 @@ class UpdateModal extends PureComponent<Props, State> {
return ( return (
<Modal <Modal
onClose={onClose} onClose={onClose}
centered
onHide={this.handleReset} onHide={this.handleReset}
isOpened={status === 'install'} isOpened={status === 'install'}
refocusWhenChange={stepId} refocusWhenChange={stepId}

6
src/config/constants.js

@ -81,6 +81,7 @@ export const SHOW_LEGACY_NEW_ACCOUNT = boolFromEnv('SHOW_LEGACY_NEW_ACCOUNT')
export const SHOW_MOCK_HSMWARNINGS = boolFromEnv('SHOW_MOCK_HSMWARNINGS') export const SHOW_MOCK_HSMWARNINGS = boolFromEnv('SHOW_MOCK_HSMWARNINGS')
export const HIGHLIGHT_I18N = boolFromEnv('HIGHLIGHT_I18N') export const HIGHLIGHT_I18N = boolFromEnv('HIGHLIGHT_I18N')
export const DISABLE_ACTIVITY_INDICATORS = boolFromEnv('DISABLE_ACTIVITY_INDICATORS') export const DISABLE_ACTIVITY_INDICATORS = boolFromEnv('DISABLE_ACTIVITY_INDICATORS')
export const EXPERIMENTAL_WS_EXPORT = boolFromEnv('EXPERIMENTAL_WS_EXPORT')
export const EXPERIMENTAL_CENTER_MODAL = boolFromEnv('EXPERIMENTAL_CENTER_MODAL') export const EXPERIMENTAL_CENTER_MODAL = boolFromEnv('EXPERIMENTAL_CENTER_MODAL')
export const EXPERIMENTAL_FIRMWARE_UPDATE = boolFromEnv('EXPERIMENTAL_FIRMWARE_UPDATE') export const EXPERIMENTAL_FIRMWARE_UPDATE = boolFromEnv('EXPERIMENTAL_FIRMWARE_UPDATE')
export const EXPERIMENTAL_HTTP_ON_RENDERER = boolFromEnv('EXPERIMENTAL_HTTP_ON_RENDERER') export const EXPERIMENTAL_HTTP_ON_RENDERER = boolFromEnv('EXPERIMENTAL_HTTP_ON_RENDERER')
@ -89,6 +90,11 @@ export const EXPERIMENTAL_MARKET_INDICATOR_SETTINGS = boolFromEnv(
) )
export const USE_MOCK_DATA = boolFromEnv('USE_MOCK_DATA') export const USE_MOCK_DATA = boolFromEnv('USE_MOCK_DATA')
// Auto update
export const UPDATE_CHECK_IGNORE = boolFromEnv('UPDATE_IGNORE_CHECK', true)
export const UPDATE_CHECK_FEED = stringFromEnv('UPDATE_CHECK_FEED', 'https://insert.feed.here')
// Other constants // Other constants
export const MAX_ACCOUNT_NAME_SIZE = 50 export const MAX_ACCOUNT_NAME_SIZE = 50

6
src/config/urls.js

@ -1,11 +1,17 @@
// @flow // @flow
export const urls = { export const urls = {
liveHome: 'https://www.ledger.com/pages/ledger-live',
// Social // Social
twitter: 'https://twitter.com/LedgerHQ', twitter: 'https://twitter.com/LedgerHQ',
github: 'https://github.com/LedgerHQ/ledger-live-desktop', github: 'https://github.com/LedgerHQ/ledger-live-desktop',
reddit: 'https://www.reddit.com/r/ledgerwallet/', reddit: 'https://www.reddit.com/r/ledgerwallet/',
// Campaigns
promoNanoX:
'https://www.ledger.com/pages/ledger-nano-x#utm_source=Ledger%20Live%20Desktop%20App&utm_medium=Ledger%20Live&utm_campaign=Ledger%20Live%20Desktop%20-%20Banner%20LNX',
// Ledger support // Ledger support
faq: 'https://support.ledgerwallet.com/hc/en-us', faq: 'https://support.ledgerwallet.com/hc/en-us',
terms: 'https://www.ledger.com/pages/terms-of-use-and-disclaimer', terms: 'https://www.ledger.com/pages/terms-of-use-and-disclaimer',

39
src/icons/Donjon.js

@ -0,0 +1,39 @@
// @flow
import React, { Fragment } from 'react'
const path = (
<Fragment>
<path
fill="currentColor"
d="m1085.1 267.76v-110.55c0-7.85 3.74-12.45 9-12.21l-144.56-80.23a33.93 33.93 0 0 1 10.11 23.23v110.55z"
/>
<path
fill="currentColor"
d="m1336 406.28v-110.55c0-7.18 3.14-11.6 7.67-12.14l-141.41-78.52a34.34 34.34 0 0 1 8.29 21.4l-0.07 110.53z"
/>
<polygon
fill="currentColor"
transform="translate(206.55 -4.4141e-6)"
points="1254.9 715.08 1167.2 814.98 627.52 515.62 627.52 368.78"
/>
<path
fill="currentColor"
d="m332.03 295.73v110.55l125.52-69.28v-110.55c0-10.18 6.24-21.87 13.94-26.12l97.6-53.87c7.7-4.25 13.94 0.55 13.94 10.73v110.55l125.52-69.24v-110.55c0-10.18 6.24-21.87 13.94-26.12l111.58-61.83-0.11 18.69 0.11 350.09-627.41 346.3-0.11-350.08c0-10.18 6.24-21.87 13.94-26.12l97.59-53.88c7.7-4.25 13.95 0.55 13.95 10.73z"
/>
<path
fill="currentColor"
d="m1175.8 1193.4 2.8-121.54c1.28-55.46-31.41-116.37-72.65-135.34-41.24-19-76 10.88-77.3 66.34l-2.8 121.54z"
/>
<path
fill="currentColor"
d="m294.46 814.91q1.86 205.53 76.34 387.56 67.47 166.41 186.66 290.1 107.94 114.69 236.09 166.43a98.88 98.88 0 0 0 40.48 9v-1152.4z"
/>
</Fragment>
)
export default ({ size, ...p }: { size: number }) => (
<svg viewBox="0 0 1668 1668" width={size} {...p}>
{path}
</svg>
)

13
src/icons/TriangleWarning.js

@ -9,8 +9,17 @@ const path = (
/> />
) )
export default ({ height, width, ...p }: { height: number, width: number }) => ( export default ({
<svg viewBox="0 0 17 17" height={height} width={width} {...p}> height,
width,
size,
...p
}: {
height?: number,
width?: number,
size?: number,
}) => (
<svg viewBox="0 0 17 17" height={height || size} width={width || size} {...p}>
{path} {path}
</svg> </svg>
) )

30
src/icons/device/NanoX.js

@ -0,0 +1,30 @@
// @flow
import React from 'react'
export default ({ size = 30, ...p }: { size: number }) => (
<svg viewBox="0 0 6 16" height={size} width={size} {...p}>
<defs>
<path
id="a"
d="M5.75 6.835a3.509 3.509 0 0 0-1.5-1.105V1.75h-2.5v3.98a3.509 3.509 0 0 0-1.5 1.105V1.666C.25.884.884.25 1.666.25h2.668c.782 0 1.416.634 1.416 1.416v5.169zm-1.5 7.415V9.5a1.25 1.25 0 1 0-2.5 0v4.75h2.5zM3 6.75A2.75 2.75 0 0 1 5.75 9.5v4.834c0 .782-.634 1.416-1.416 1.416H1.666A1.416 1.416 0 0 1 .25 14.334V9.5A2.75 2.75 0 0 1 3 6.75z"
/>
</defs>
<g fill="none" fillRule="evenodd">
<path
fill="#000"
fillRule="nonzero"
d="M5.75 6.835a3.509 3.509 0 0 0-1.5-1.105V1.75h-2.5v3.98a3.509 3.509 0 0 0-1.5 1.105V1.666C.25.884.884.25 1.666.25h2.668c.782 0 1.416.634 1.416 1.416v5.169zm-1.5 7.415V9.5a1.25 1.25 0 1 0-2.5 0v4.75h2.5zM3 6.75A2.75 2.75 0 0 1 5.75 9.5v4.834c0 .782-.634 1.416-1.416 1.416H1.666A1.416 1.416 0 0 1 .25 14.334V9.5A2.75 2.75 0 0 1 3 6.75z"
/>
<g>
<mask id="b" fill="#fff">
<use xlinkHref="#a" />
</mask>
<use fill="#FFF" xlinkHref="#a" />
<g fill="#FFF" mask="url(#b)">
<path d="M-5 0h16v16H-5z" />
</g>
</g>
</g>
</svg>
)

1
src/icons/device/index.js

@ -1,2 +1,3 @@
export Blue from './Blue' export Blue from './Blue'
export NanoS from './NanoS' export NanoS from './NanoS'
export NanoX from './NanoX'

1
src/index.ejs

@ -75,6 +75,7 @@
<img class="logo" src="<%= __DEV__ ? '.' : '../static' %>/images/ledgerlive-logo.svg" alt="" /> <img class="logo" src="<%= __DEV__ ? '.' : '../static' %>/images/ledgerlive-logo.svg" alt="" />
</div> </div>
<div id="app"></div> <div id="app"></div>
<div id="modals"></div>
<script> <script>
const { remote } = require('electron') const { remote } = require('electron')
const { name } = remote.getCurrentWindow() const { name } = remote.getCurrentWindow()

63
src/internals/index.js

@ -1,16 +1,18 @@
// @flow // @flow
import '@babel/polyfill' import '@babel/polyfill'
import { serializeError } from '@ledgerhq/errors/lib/helpers'
import 'helpers/live-common-setup' import 'helpers/live-common-setup'
import 'helpers/live-common-setup-internal-hw' import 'helpers/live-common-setup-internal-hw'
import commands from 'commands'
import logger from 'logger' import logger from 'logger'
import LoggerTransport from 'logger/logger-transport-internal' import LoggerTransport from 'logger/logger-transport-internal'
import uuid from 'uuid/v4' import uuid from 'uuid/v4'
import { setImplementation } from 'api/network' import { setImplementation } from 'api/network'
import sentry from 'sentry/node' import sentry from 'sentry/node'
import { EXPERIMENTAL_HTTP_ON_RENDERER } from 'config/constants' import { EXPERIMENTAL_HTTP_ON_RENDERER } from 'config/constants'
import { serializeError } from '@ledgerhq/errors/lib/helpers' import { executeCommand, unsubscribeCommand } from 'main/commandHandler'
require('../env') require('../env')
@ -58,64 +60,11 @@ if (EXPERIMENTAL_HTTP_ON_RENDERER) {
}) })
} }
const subscriptions = {}
process.on('message', m => { process.on('message', m => {
if (m.type === 'command') { if (m.type === 'command') {
const { data, requestId, id } = m.command executeCommand(m.command, process.send.bind(process))
const cmd = commands.find(cmd => cmd.id === id)
if (!cmd) {
logger.warn(`command ${id} not found`)
return
}
const startTime = Date.now()
logger.onCmd('cmd.START', id, 0, data)
try {
subscriptions[requestId] = cmd.impl(data).subscribe({
next: data => {
logger.onCmd('cmd.NEXT', id, Date.now() - startTime, data)
process.send({
type: 'cmd.NEXT',
requestId,
data,
})
},
complete: () => {
delete subscriptions[requestId]
logger.onCmd('cmd.COMPLETE', id, Date.now() - startTime)
process.send({
type: 'cmd.COMPLETE',
requestId,
})
},
error: error => {
logger.warn('Command error:', { error })
delete subscriptions[requestId]
logger.onCmd('cmd.ERROR', id, Date.now() - startTime, error)
process.send({
type: 'cmd.ERROR',
requestId,
data: serializeError(error),
})
},
})
} catch (error) {
logger.warn('Command impl error:', { error })
delete subscriptions[requestId]
logger.onCmd('cmd.ERROR', id, Date.now() - startTime, error)
process.send({
type: 'cmd.ERROR',
requestId,
data: serializeError(error),
})
}
} else if (m.type === 'command-unsubscribe') { } else if (m.type === 'command-unsubscribe') {
const { requestId } = m unsubscribeCommand(m.requestId)
const sub = subscriptions[requestId]
if (sub) {
sub.unsubscribe()
delete subscriptions[requestId]
}
} else if (m.type === 'executeHttpQueryPayload') { } else if (m.type === 'executeHttpQueryPayload') {
const { payload } = m const { payload } = m
const defer = defers[payload.id] const defer = defers[payload.id]

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

Loading…
Cancel
Save