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
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.

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)
- 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/).
<a href="https://github.com/LedgerHQ/ledger-live-desktop/releases">
@ -85,6 +87,7 @@ SKIP_GENUINE=1
SKIP_ONBOARDING=1
SHOW_LEGACY_NEW_ACCOUNT=1
HIGHLIGHT_I18N=1
EXPERIMENTAL_WS_EXPORT=0
## constants
GET_CALLS_TIMEOUT=30000

26
package.json

@ -20,7 +20,7 @@
"test-e2e": "jest test-e2e",
"test-sync": "bash test-e2e/sync/launch.sh",
"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",
"publish-storybook": "bash ./scripts/legacy/publish-storybook.sh",
"reset-files": "bash ./scripts/legacy/reset-files.sh"
@ -35,13 +35,14 @@
}
},
"dependencies": {
"@ledgerhq/hw-app-btc": "^4.34.0",
"@ledgerhq/hw-app-eth": "^4.32.0",
"@ledgerhq/hw-app-xrp": "^4.32.0",
"@ledgerhq/hw-transport": "^4.32.0",
"@ledgerhq/hw-transport-node-hid": "^4.32.0",
"@ledgerhq/errors": "^4.35.1",
"@ledgerhq/hw-app-btc": "^4.35.0",
"@ledgerhq/hw-app-eth": "^4.35.0",
"@ledgerhq/hw-app-xrp": "^4.35.0",
"@ledgerhq/hw-transport": "^4.35.0",
"@ledgerhq/hw-transport-node-hid": "^4.35.0",
"@ledgerhq/ledger-core": "2.0.0-rc.16",
"@ledgerhq/live-common": "4.14.1",
"@ledgerhq/live-common": "4.16.0",
"animated": "^0.2.2",
"async": "^2.6.1",
"axios": "^0.18.0",
@ -64,18 +65,20 @@
"i18next": "^11.2.2",
"i18next-node-fs-backend": "^1.0.0",
"invariant": "^2.2.4",
"ip": "^1.1.5",
"jsqr": "^1.1.1",
"lodash": "^4.17.5",
"lru-cache": "^4.1.3",
"measure-scrollbar": "^1.1.0",
"moment": "^2.22.2",
"openpgp": "^4.2.1",
"qrcode": "^1.2.0",
"qrloop": "0.8.1",
"qs": "^6.5.1",
"raven": "^2.5.0",
"raven-js": "^3.24.2",
"react": "^16.6.1",
"react-dom": "^16.4.1",
"react": "^16.6.3",
"react-dom": "^16.6.3",
"react-i18next": "^7.7.0",
"react-key-handler": "^1.0.1",
"react-markdown": "^3.3.2",
@ -111,7 +114,7 @@
"winston": "^3.0.0",
"winston-transport": "^4.2.0",
"write-file-atomic": "^2.3.0",
"ws": "^5.1.1",
"ws": "^6.1.3",
"zxcvbn": "^4.4.2"
},
"devDependencies": {
@ -182,7 +185,8 @@
"webpack": "^4.6.0",
"webpack-bundle-analyzer": "^2.11.1",
"webpack-cli": "^2.0.14",
"yaml-loader": "^0.5.0"
"yaml-loader": "^0.5.0",
"yarn-deduplicate": "^1.1.1"
},
"engines": {
"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"
fi
runJob "rm -rf ./node_modules/.cache" "Removing node modules cache..." "done" "fail"
runJob "yarn compile" "compiling..." "compiled" "failed to compile" "verbose"
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()
},
error: error => {
logger.critical(error)
this.props.setAccountSyncState(accountId, { pending: false, error })
next()
},

12
src/bridge/EthereumJSBridge.js

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

8
src/bridge/LibcoreBridge.js

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

35
src/bridge/RippleJSBridge.js

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

4
src/bridge/index.js

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

8
src/bridge/types.js

@ -2,7 +2,7 @@
import type { Observable } from 'rxjs'
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.
// 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 function returns a Subscription and you MUST stop everything if it is unsubscribed.
// 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.
// 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)
pullMoreOperations(initialAccount: Account, count: number): Promise<(Account) => Account>;
isRecipientValid(currency: Currency, recipient: string): Promise<boolean>;
getRecipientWarning(currency: Currency, recipient: string): Promise<?Error>;
isRecipientValid(account: Account, recipient: string): Promise<boolean>;
getRecipientWarning(account: Account, recipient: string): Promise<?Error>;
// 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 debugAppInfosForCurrency from 'commands/debugAppInfosForCurrency'
import autoUpdate from 'commands/autoUpdate'
import firmwarePrepare from 'commands/firmwarePrepare'
import firmwareMain from 'commands/firmwareMain'
import firmwareRepair from 'commands/firmwareRepair'
@ -22,6 +23,7 @@ import libcoreSyncAccount from 'commands/libcoreSyncAccount'
import libcoreValidAddress from 'commands/libcoreValidAddress'
import listenDevices from 'commands/listenDevices'
import ping from 'commands/ping'
import quitAndInstallElectronUpdate from 'commands/quitAndInstallElectronUpdate'
import signTransaction from 'commands/signTransaction'
import testApdu from 'commands/testApdu'
import testCrash from 'commands/testCrash'
@ -29,6 +31,7 @@ import testInterval from 'commands/testInterval'
import uninstallApp from 'commands/uninstallApp'
const all: Array<Command<any, any>> = [
autoUpdate,
debugAppInfosForCurrency,
firmwarePrepare,
firmwareMain,
@ -48,6 +51,7 @@ const all: Array<Command<any, any>> = [
libcoreValidAddress,
listenDevices,
ping,
quitAndInstallElectronUpdate,
signTransaction,
testApdu,
testCrash,

2
src/commands/libcoreSignAndBroadcast.js

@ -129,7 +129,7 @@ async function signTransaction({
const hexPreviousTransaction = Buffer.from(rawPreviousTransaction).toString('hex')
const previousTransaction = hwApp.splitTransaction(
hexPreviousTransaction,
true, // set to true allow both segwit AND non-segwit
currency.supportsSegwit,
hasTimestamp,
hasExtraData,
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 CounterValues from 'helpers/countervalues'
import { BridgeSyncProvider } from 'bridge/BridgeSyncContext'
import { UpdaterProvider } from 'components/Updater/UpdaterContext'
const App = ({
store,
@ -31,13 +32,15 @@ const App = ({
<CounterValues.PollingProvider>
<I18nextProvider i18n={i18n} initialLanguage={language}>
<ThemeProvider theme={theme}>
<ThrowBlock>
<ConnectedRouter history={history}>
<Switch>
<Route component={Default} />
</Switch>
</ConnectedRouter>
</ThrowBlock>
<UpdaterProvider>
<ThrowBlock>
<ConnectedRouter history={history}>
<Switch>
<Route component={Default} />
</Switch>
</ConnectedRouter>
</ThrowBlock>
</UpdaterProvider>
</ThemeProvider>
</I18nextProvider>
</CounterValues.PollingProvider>

4
src/components/CounterValue/index.js

@ -3,7 +3,7 @@
import type { BigNumber } from 'bignumber.js'
import React, { PureComponent } from 'react'
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 {
counterValueCurrencySelector,
@ -19,7 +19,7 @@ import type { State } from 'reducers'
type OwnProps = {
// wich market to query
currency: Currency,
currency: CryptoCurrency,
// when? if not given: take latest
date?: Date,

4
src/components/CurrenciesStatusBanner.js

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

3
src/components/CurrentAddress/index.js

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

39
src/components/DashboardPage/index.js

@ -3,6 +3,7 @@
import React, { PureComponent, Fragment } from 'react'
import uniq from 'lodash/uniq'
import { compose } from 'redux'
import IconNanoX from 'icons/device/NanoX'
import { translate } from 'react-i18next'
import { connect } from 'react-redux'
import { push } from 'react-router-redux'
@ -24,17 +25,21 @@ import { saveSettings } from 'actions/settings'
import TrackPage from 'analytics/TrackPage'
import RefreshAccountsOrdering from 'components/RefreshAccountsOrdering'
import UpdateNotifier from 'components/UpdateNotifier'
import UpdateBanner from 'components/Updater/Banner'
import BalanceInfos from 'components/BalanceSummary/BalanceInfos'
import BalanceSummary from 'components/BalanceSummary'
import Box from 'components/base/Box'
import PillsDaysCount from 'components/PillsDaysCount'
import OperationsList from 'components/OperationsList'
import StickyBackToTop from 'components/StickyBackToTop'
import styled from 'styled-components'
import { openURL } from 'helpers/linking'
import EmptyState from './EmptyState'
import CurrentGreetings from './CurrentGreetings'
import SummaryDesc from './SummaryDesc'
import AccountCardList from './AccountCardList'
import TopBanner, { FakeLink } from '../TopBanner'
import { urls } from '../../config/urls'
const mapStateToProps = createStructuredSelector({
accounts: accountsSelector,
@ -84,7 +89,24 @@ class DashboardPage extends PureComponent<Props> {
return (
<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 />
<TrackPage
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(
connect(

9
src/components/DevToolsPage/AccountImporter.js

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

2
src/components/FeesField/BitcoinKind.js

@ -11,7 +11,7 @@ import type { T } from 'types/common'
import { FeeNotLoaded } from '@ledgerhq/errors'
import InputCurrency from 'components/base/InputCurrency'
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 GenericContainer from './GenericContainer'
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 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 GenericContainer from './GenericContainer'

5
src/components/FeesField/RippleKind.js

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

13
src/components/GenuineCheckModal.js

@ -5,7 +5,7 @@ import { translate } from 'react-i18next'
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'
type Props = {
@ -19,12 +19,13 @@ class GenuineCheckModal extends PureComponent<Props> {
renderBody = ({ onClose }) => {
const { t, onSuccess, onFail, onUnavailable } = this.props
return (
<ModalBody onClose={onClose}>
<ModalTitle>{t('genuinecheck.modal.title')}</ModalTitle>
<ModalContent>
<ModalBody
onClose={onClose}
title={t('genuinecheck.modal.title')}
render={() => (
<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 LedgerLiveLogo from 'components/base/LedgerLiveLogo'
import IconArrowRight from 'icons/ArrowRight'
import Button from './base/Button/index'
import ConfirmModal from './base/Modal/ConfirmModal'
import Button from 'components/base/Button/index'
import ConfirmModal from 'components/base/Modal/ConfirmModal'
type InputValue = {
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 { T } from 'types/common'
import type { UpdateStatus } from 'reducers/update'
import { MODAL_RECEIVE, MODAL_SEND, MODAL_ADD_ACCOUNTS } from 'config/constants'
import { i } from 'helpers/staticPath'
import { accountsSelector } from 'reducers/accounts'
import { openModal } from 'reducers/modals'
import { getUpdateStatus } from 'reducers/update'
import { developerModeSelector } from 'reducers/settings'
import { SideBarList, SideBarListItem } from 'components/base/SideBar'
import Box from 'components/base/Box'
import GrowScroll from 'components/base/GrowScroll'
import Space from 'components/base/Space'
import UpdateDot from 'components/Updater/UpdateDot'
import IconManager from 'icons/Manager'
import IconPieChart from 'icons/PieChart'
@ -39,7 +38,6 @@ import KeyboardContent from '../KeyboardContent'
const mapStateToProps = state => ({
accounts: accountsSelector(state),
updateStatus: getUpdateStatus(state),
developerMode: developerModeSelector(state),
})
@ -54,7 +52,6 @@ type Props = {
location: Location,
push: string => void,
openModal: string => void,
updateStatus: UpdateStatus,
developerMode: boolean,
}
@ -96,7 +93,7 @@ class MainSideBar extends PureComponent<Props> {
handleOpenImportModal = () => this.props.openModal(MODAL_ADD_ACCOUNTS)
render() {
const { t, accounts, location, updateStatus, developerMode } = this.props
const { t, accounts, location, developerMode } = this.props
const { pathname } = location
const addAccountButton = (
@ -122,7 +119,7 @@ class MainSideBar extends PureComponent<Props> {
iconActiveColor="wallet"
onClick={this.handleClickDashboard}
isActive={pathname === '/'}
hasNotif={updateStatus === 'downloaded'}
NotifComponent={UpdateDot}
/>
<SideBarListItem
label={t('send.title')}

284
src/components/ManagerPage/AppsList.js

@ -1,7 +1,7 @@
// @flow
/* eslint-disable react/jsx-no-literals */ // FIXME
import React, { PureComponent, Fragment } from 'react'
import React, { PureComponent } from 'react'
import styled from 'styled-components'
import { translate } from 'react-i18next'
import { connect } from 'react-redux'
@ -15,8 +15,7 @@ import { developerModeSelector } from 'reducers/settings'
import installApp from 'commands/installApp'
import uninstallApp from 'commands/uninstallApp'
import Box from 'components/base/Box'
import Space from 'components/base/Space'
import Modal, { ModalBody, ModalFooter, ModalTitle, ModalContent } from 'components/base/Modal'
import Modal from 'components/base/Modal'
import Tooltip from 'components/base/Tooltip'
import Text from 'components/base/Text'
import ProgressBar from 'components/ProgressBar'
@ -32,6 +31,7 @@ import CheckCircle from 'icons/CheckCircle'
import { FreezeDeviceChangeEvents } from './HookDeviceChange'
import ManagerApp, { Container as FakeManagerAppContainer } from './ManagerApp'
import AppSearchBar from './AppSearchBar'
import ModalBody from '../base/Modal/ModalBody'
const mapStateToProps = state => ({
isDevMode: developerModeSelector(state),
@ -39,15 +39,35 @@ const mapStateToProps = state => ({
const List = styled(Box).attrs({
horizontal: true,
m: -3,
})`
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 = {
bitcoin_testnet: 'bitcoin',
}
const CATALOG_INFO_ICON = (
<Box color="grey">
<IconInfoCircle size={12} />
</Box>
)
type Status = 'loading' | 'idle' | 'busy' | 'success' | 'error'
type Mode = 'home' | 'installing' | 'uninstalling'
@ -79,6 +99,20 @@ const LoadingApp = () => (
const loadingApp = <LoadingApp />
const FAKE_LIST = (
<List>
{loadingApp}
{loadingApp}
{loadingApp}
{loadingApp}
{loadingApp}
{loadingApp}
{loadingApp}
{loadingApp}
{loadingApp}
</List>
)
class AppsList extends PureComponent<Props, State> {
state = {
status: 'loading',
@ -150,108 +184,103 @@ class AppsList extends PureComponent<Props, State> {
handleCloseModal = () => this.setState({ status: 'idle', mode: 'home' })
renderModal = () => {
renderBody = () => {
const { t } = this.props
const { app, status, error, mode, progress } = this.state
return (
<Modal
isOpened={status !== 'idle' && status !== 'loading'}
render={() => (
<ModalBody align="center" justify="center" style={{ height: 300 }}>
<FreezeDeviceChangeEvents />
{status === 'busy' || status === 'idle' ? (
<Fragment>
<ModalTitle>
{mode === 'installing' ? (
<Box color="grey">
<Update size={30} />
</Box>
) : (
<Box color="grey">
<Trash size={30} />
</Box>
)}
</ModalTitle>
<ModalContent>
<Text ff="Museo Sans|Regular" fontSize={6} color="dark">
{t(`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>
return ['busy', 'idle'].includes(status) ? (
<Box grow align="center" justify="center">
{mode === 'installing' ? (
<Box color="grey" grow align="center" mb={5}>
<Update size={30} />
</Box>
) : (
<Box color="grey" grow align="center" mb={5}>
<Trash size={30} />
</Box>
)}
/>
<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>
{this.renderModal()}
{!appsLoaded && (
<Fragment>
<Space of={30} />
<List>
{loadingApp}
{loadingApp}
{loadingApp}
{loadingApp}
{loadingApp}
{loadingApp}
{loadingApp}
{loadingApp}
{loadingApp}
</List>
</Fragment>
)}
{!appsLoaded && FAKE_LIST}
</Box>
)
}
renderTooltip = () => {
const { t } = this.props
return (
<Box ff="Open Sans|SemiBold" fontSize={2}>
{t('manager.apps.help')}
</Box>
)
}
render() {
const { t } = this.props
return (
<Box flow={6}>
<Box>
<Box mb={4} color="dark" ff="Museo Sans" fontSize={5} flow={2} horizontal align="center">
<span style={{ lineHeight: 1 }}>{t('manager.apps.all')}</span>
<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 mb={4} color="dark" ff="Museo Sans" fontSize={5} flow={2} horizontal align="center">
<span>{t('manager.apps.all')}</span>
<Tooltip render={this.renderTooltip}>{CATALOG_INFO_ICON}</Tooltip>
</Box>
{this.renderList()}
</Box>
)
}

3
src/components/ManagerPage/ManagerApp.js

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

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

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

4
src/components/OperationsList/AccountCell.js

@ -3,7 +3,7 @@
import React, { PureComponent } from 'react'
import styled from 'styled-components'
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'
const Cell = styled(Box).attrs({
@ -17,7 +17,7 @@ const Cell = styled(Box).attrs({
`
type Props = {
currency: Currency,
currency: CryptoCurrency,
accountName: string,
}

4
src/components/OperationsList/AmountCell.js

@ -3,7 +3,7 @@
import React, { PureComponent } from 'react'
import styled from 'styled-components'
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 CounterValue from 'components/CounterValue'
import FormattedVal from 'components/base/FormattedVal'
@ -18,7 +18,7 @@ const Cell = styled(Box).attrs({
type Props = {
operation: Operation,
currency: Currency,
currency: CryptoCurrency,
unit: Unit,
}

2
src/components/SelectExchange.js

@ -87,7 +87,7 @@ class SelectExchange extends Component<
this.setState({ exchanges, isLoading: false })
}
} catch (error) {
logger.error(error)
logger.critical(error)
if (!this._unmounted && this._loadId === _loadId) {
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 { cleanAccountsCache } from 'actions/accounts'
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 ResetFallbackModal from './ResetFallbackModal'
@ -60,6 +60,7 @@ class CleanButton extends PureComponent<Props, State> {
<ConfirmModal
analyticsName="CleanCache"
centered
isOpened={opened}
onClose={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
.map(fiat => ({
value: fiat.code,
label: `${fiat.name} - ${fiat.code}${fiat.symbol ? ` (${fiat.symbol})` : ''}`,
label: `${fiat.name} - ${fiat.code}`,
fiat,
}))

67
src/components/SettingsPage/DisablePasswordModal.js

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

8
src/components/SettingsPage/PasswordAutoLockSelect.js

@ -30,10 +30,10 @@ class PasswordAutoLockSelect extends PureComponent<Props> {
}
timeouts = [
{ value: 1, label: `1 ${this.props.t('app:time.minute')}` },
{ value: 10, label: `10 ${this.props.t('app:time.minute')}s` },
{ value: 30, label: `30 ${this.props.t('app:time.minute')}s` },
{ value: 60, label: `1 ${this.props.t('app:time.hour')}` },
{ value: 1, label: `1 ${this.props.t('time.minute')}` },
{ value: 10, label: `10 ${this.props.t('time.minute')}s` },
{ value: 30, label: `30 ${this.props.t('time.minute')}s` },
{ value: 60, label: `1 ${this.props.t('time.hour')}` },
{ 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>
<InputPassword
style={{ width: 240 }}
onEnter={onSubmit}
id="confirmPassword"
onChange={onChange('confirmPassword')}
value={confirmPassword}

49
src/components/SettingsPage/PasswordModal.js

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

16
src/components/SettingsPage/RepairDeviceButton.js

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

3
src/components/SettingsPage/ResetButton.js

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

2
src/components/SettingsPage/ResetFallbackModal.js

@ -3,7 +3,7 @@
import React, { PureComponent } from 'react'
import { translate } from 'react-i18next'
import { ConfirmModal } from 'components/base/Modal'
import ConfirmModal from 'components/base/Modal/ConfirmModal'
import { openUserDataFolderAndQuit } from 'helpers/reset'
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>
{hasPassword ? (
<Row
title={t('app:settings.profile.passwordAutoLock')}
desc={t('app:settings.profile.passwordAutoLockDesc')}
title={t('settings.profile.passwordAutoLock')}
desc={t('settings.profile.passwordAutoLockDesc')}
>
<PasswordAutoLockSelect />
</Row>

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

@ -8,13 +8,16 @@ import type { T } from 'types/common'
import TrackPage from 'analytics/TrackPage'
import styled from 'styled-components'
import { SettingsSection as Section, SettingsSectionHeader as Header } from '../SettingsSection'
import { EXPERIMENTAL_WS_EXPORT } from '../../../config/constants'
import IconShare from '../../../icons/Share'
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 QRCodeExporter from '../../QRCodeExporter'
import { BulletRow } from '../../Onboarding/helperComponents'
import Text from '../../base/Text'
import SocketExport from '../SocketExport'
const BulletRowIcon = styled(Box).attrs({
ff: 'Rubik|Regular',
@ -66,7 +69,10 @@ class SectionExport extends PureComponent<Props, State> {
<Text ff="Open Sans|SemiBold" color="dark">
{'+'}
</Text>
{'button in Accounts'}
{'button in'}
<Text ff="Open Sans|SemiBold" color="dark">
{'Accounts'}
</Text>
</Trans>
</Box>
),
@ -90,34 +96,45 @@ class SectionExport extends PureComponent<Props, State> {
icon: <BulletRowIcon>{'3'}</BulletRowIcon>,
desc: (
<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>
),
},
]
return (
<ModalBody onClose={onClose}>
<ModalTitle>{t('settings.export.modal.title')}</ModalTitle>
<ModalContent flow={2} justify="center" align="center">
<Box flow={2}>
<QRCodeExporter size={330} />
</Box>
<Box shrink style={{ width: 330, fontSize: 13, marginTop: 20 }}>
<Text ff="Open Sans|SemiBold" color="dark">
{t('settings.export.modal.listTitle')}
</Text>
<ModalBody
onClose={onClose}
title={t('settings.export.modal.title')}
render={() => (
<Box justify="center" align="center">
<Box flow={2}>
<QRCodeExporter size={330} />
</Box>
<Box shrink style={{ width: 330, fontSize: 13, marginTop: 20 }}>
<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 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>
</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
icon={<IconShare size={16} />}
title={t('settings.tabs.export')}
title={t('settings.export.title')}
desc={t('settings.export.desc')}
renderRight={
<Button small onClick={this.onModalOpen} primary>
@ -139,6 +156,14 @@ class SectionExport extends PureComponent<Props, State> {
</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} />
</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,
flow: 4,
align: 'center',
})`
border-bottom: 1px solid ${p => p.theme.colors.fog};
`
})``
const Bar = styled.div`
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
import { Component } from 'react'
import type { Currency } from '@ledgerhq/live-common/lib/types'
import { getEstimatedFees } from 'api/Fees'
import type { Fees } from 'api/Fees'
import type { CryptoCurrency } from '@ledgerhq/live-common/lib/types'
import { getEstimatedFees } from '@ledgerhq/live-common/lib/api/Fees'
import type { Fees } from '@ledgerhq/live-common/lib/api/Fees'
// FIXME we need to abstract this out like we did for CounterValues
export default class WithFeesAPI extends Component<
{
currency: Currency,
currency: CryptoCurrency,
render: Fees => *,
renderLoading: () => *,
renderError: Error => *,

6
src/components/base/CurrencyBadge.js

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

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

@ -1,95 +1,102 @@
// @flow
import React, { PureComponent } from 'react'
import styled, { keyframes } from 'styled-components'
import React, { PureComponent, Fragment } from 'react'
import Animated from 'animated/lib/targets/react-dom'
import { findDOMNode } from 'react-dom'
import Box from 'components/base/Box'
import IconCross from 'icons/Cross'
import ModalContent from './ModalContent'
import ModalHeader from './ModalHeader'
import ModalFooter from './ModalFooter'
export const Container = styled(Box).attrs({
px: 5,
pb: 5,
})``
import type { RenderProps } from './index'
type Props = {
deferHeight?: number,
onClose?: Function,
children: any,
title: string,
onBack?: void => void,
onClose?: void => void,
render?: (?RenderProps) => any,
renderFooter?: (?RenderProps) => any,
renderProps?: RenderProps,
noScroll?: boolean,
refocusWhenChange?: any,
}
type State = {
isHidden: boolean,
animGradient: Animated.Value,
}
class ModalBody extends PureComponent<Props, State> {
static defaultProps = {
onClose: undefined,
state = {
animGradient: new Animated.Value(0),
}
state = {
isHidden: true,
componentDidUpdate(prevProps: Props) {
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() {
setTimeout(() => {
window.requestAnimationFrame(() => {
this.setState({ isHidden: false })
})
}, 150)
_content = null
animateGradient = (isScrollable: boolean) => {
const anim = {
duration: 150,
toValue: isScrollable ? 1 : 0,
}
Animated.timing(this.state.animGradient, anim).start()
}
render() {
const { children, onClose, deferHeight, ...props } = this.props
const { isHidden } = this.state
const { onBack, onClose, title, render, renderFooter, renderProps, noScroll } = this.props
const { animGradient } = this.state
const gradientStyle = {
...GRADIENT_STYLE,
opacity: animGradient,
}
return (
<Body
style={{ height: isHidden && deferHeight ? deferHeight : undefined }}
data-e2e="modalBody"
>
{onClose && (
<CloseContainer onClick={onClose}>
<IconCross size={16} />
</CloseContainer>
)}
{(!isHidden || !deferHeight) && <Inner {...props}>{children}</Inner>}
</Body>
<Fragment>
<ModalHeader onBack={onBack} onClose={onClose}>
{title}
</ModalHeader>
<ModalContent
tabIndex={0}
ref={n => (this._content = n)}
onIsScrollableChange={this.animateGradient}
noScroll={noScroll}
>
{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({
p: 4,
color: 'fog',
})`
position: absolute;
top: 0;
right: 0;
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 GRADIENT_STYLE = {
background: 'linear-gradient(rgba(255, 255, 255, 0), #ffffff)',
height: 40,
position: 'absolute',
bottom: 0,
left: 0,
right: 20,
}
const Inner = styled(Box)`
animation: ${appear} 80ms linear;
`
const GRADIENT_WRAPPER_STYLE = {
height: 0,
position: 'relative',
pointerEvents: 'none',
}
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 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({
alignItems: 'center',
@ -39,18 +40,18 @@ const Separator = styled(Box).attrs({
`
const DisclaimerStep = ({ desc }: { desc?: string }) => (
<ModalContent>
<Box>
{desc ? (
<Box ff="Open Sans" color="smoke" fontSize={4} textAlign="center" mb={2}>
{desc}
</Box>
) : null}
</ModalContent>
</Box>
)
const FlashStep = ({ progress, t }: { progress: number, t: * }) =>
progress === 0 ? (
<ModalContent>
<Box>
<Box mx={7}>
<Text ff="Open Sans|Regular" align="center" color="smoke">
<Bullet>{'1.'}</Bullet>
@ -74,9 +75,9 @@ const FlashStep = ({ progress, t }: { progress: number, t: * }) =>
alt={t('manager.modal.mcuFirst')}
/>
</Box>
</ModalContent>
</Box>
) : (
<ModalContent>
<Box>
<Box mx={7} align="center">
<ProgressCircle size={64} progress={progress} />
</Box>
@ -88,11 +89,11 @@ const FlashStep = ({ progress, t }: { progress: number, t: * }) =>
{t('manager.modal.mcuPin')}
</Text>
</Box>
</ModalContent>
</Box>
)
const ErrorStep = ({ error }: { error: Error }) => (
<ModalContent>
<Box>
<Container>
<Box color="alertRed">
<ExclamationCircleThin size={44} />
@ -118,7 +119,7 @@ const ErrorStep = ({ error }: { error: Error }) => (
<TranslatedError error={error} field="description" />
</Box>
</Container>
</ModalContent>
</Box>
)
type Props = {
@ -177,38 +178,44 @@ class RepairModal extends PureComponent<Props, *> {
return (
<Modal
isOpened={isOpened}
centered
preventBackdropClick={isLoading}
onClose={!cancellable && isLoading ? undefined : onReject}
{...props}
render={({ onClose }) => (
<ModalBody onClose={!cancellable && isLoading ? undefined : onClose}>
<TrackPage category="Modal" name={analyticsName} />
<ModalTitle>{title}</ModalTitle>
{error ? (
<ErrorStep error={error} />
) : isLoading ? (
<FlashStep t={t} progress={progress} />
) : (
<DisclaimerStep desc={desc} />
)}
{!isLoading && !error ? (
<Box py={2} px={5}>
<Select
isSearchable={false}
isClearable={false}
value={selectedOption}
onChange={this.onChange}
autoFocus
options={forceRepairChoices}
renderOption={this.renderOption}
renderValue={this.renderValue}
/>
</Box>
) : null}
{!isLoading ? (
<ModalFooter horizontal align="center" justify="flex-end" flow={2}>
{error ? <Button onClick={onReject}>{t(`common.close`)}</Button> : null}
>
<TrackPage category="Modal" name={analyticsName} />
<ModalBody
title={title}
render={() => (
<Box>
{error ? (
<ErrorStep error={error} />
) : isLoading ? (
<FlashStep t={t} progress={progress} />
) : (
<DisclaimerStep desc={desc} />
)}
{!isLoading && !error ? (
<Box py={2} px={5}>
<Select
isSearchable={false}
isClearable={false}
value={selectedOption}
onChange={this.onChange}
autoFocus
options={forceRepairChoices}
renderOption={this.renderOption}
renderValue={this.renderValue}
/>
</Box>
) : null}
</Box>
)}
renderFooter={() =>
!isLoading ? (
<Box horizontal align="center" justify="flex-end" flow={2}>
<Button onClick={onReject}>{t(`common.${error ? 'close' : 'cancel'}`)}</Button>
{error ? null : (
<>
<Button
@ -222,11 +229,11 @@ class RepairModal extends PureComponent<Props, *> {
</Button>
</>
)}
</ModalFooter>
) : null}
</ModalBody>
)}
/>
</Box>
) : null
}
/>
</Modal>
)
}
}

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

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

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

@ -2,50 +2,57 @@
import React from 'react'
import { storiesOf } from '@storybook/react'
import { action } from '@storybook/addon-actions'
import { boolean, text } from '@storybook/addon-knobs'
import { action } from '@storybook/addon-actions'
import {
Modal,
ModalBody,
ModalTitle,
ModalContent,
ModalFooter,
ConfirmModal,
} from 'components/base/Modal'
import Modal from 'components/base/Modal'
import ModalBody from 'components/base/Modal/ModalBody'
import Input from 'components/base/Input'
import Label from 'components/base/Label'
import Box from 'components/base/Box'
import Button from 'components/base/Button'
const stories = storiesOf('Components/base', module)
stories.add('Modal', () => (
<Modal
isOpened={boolean('isOpened', true)}
centered={boolean('centered', true)}
onClose={action('onClose')}
render={({ onClose }) => (
<ModalBody onClose={onClose}>
<ModalTitle>{'modal title'}</ModalTitle>
<ModalContent>{'this is the modal content'}</ModalContent>
<ModalFooter horizontal align="center">
<Box grow>{'modal footer'}</Box>
<Button primary>{'Next'}</Button>
</ModalFooter>
</ModalBody>
)}
/>
))
stories.add('ConfirmModal', () => (
<ConfirmModal
categoryName=""
isOpened
isDanger={boolean('isDanger', false)}
title={text('title', 'Hard reset')}
subTitle={text('subTitle', 'Are you sure houston?')}
desc={text(
'desc',
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer non nibh diam. In eget ipsum arcu donec finibus',
<ModalBody
onClose={onClose}
onBack={action('onBack')}
title={text('title', 'Send funds')}
render={() => (
<Box flow={4}>
<Box flow={2}>
<Label>{'first field'}</Label>
<Input autoFocus />
</Box>
<Box horizontal flow={4}>
<Box flow={2} flex={1}>
<Label>{'second field'}</Label>
<Input />
</Box>
<Box flow={2} flex={1}>
<Label>{'third field'}</Label>
<Input />
</Box>
</Box>
<Box horizontal flow={4}>
<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 ¯\_(ツ)_/¯
disabled?: boolean,
iconActiveColor: ?string,
hasNotif?: boolean,
NotifComponent?: React$ComponentType<*>,
isActive?: boolean,
onClick?: void => void,
isActive?: boolean,
@ -24,7 +24,7 @@ class SideBarListItem extends PureComponent<Props> {
label,
desc,
iconActiveColor,
hasNotif,
NotifComponent,
onClick,
isActive,
disabled,
@ -47,7 +47,7 @@ class SideBarListItem extends PureComponent<Props> {
)}
{!!desc && desc(this.props)}
</Box>
{!!hasNotif && <Bullet />}
{NotifComponent && <NotifComponent />}
</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

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',
icon: IconControls,
iconActiveColor: '#27a2db',
hasNotif: true,
},
{
key: 'fourth',

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

@ -1,12 +1,12 @@
// @flow
import React, { PureComponent } from 'react'
import React, { PureComponent, Fragment } from 'react'
import invariant from 'invariant'
import { translate } from 'react-i18next'
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'
type Props = {
@ -29,6 +29,7 @@ export type Step = {
shouldRenderFooter?: StepProps => boolean,
shouldPreventClose?: boolean | (StepProps => boolean),
onBack?: StepProps => void,
noScroll?: boolean,
}
type State = {
@ -72,6 +73,7 @@ class Stepper extends PureComponent<Props, State> {
onBack,
shouldPreventClose,
shouldRenderFooter,
noScroll,
} = step
const stepProps: StepProps = {
@ -89,25 +91,27 @@ class Stepper extends PureComponent<Props, State> {
: !!shouldPreventClose
return (
<ModalBody onClose={preventClose ? undefined : onClose}>
<ModalTitle onBack={onBack ? () => onBack(stepProps) : undefined}>{title}</ModalTitle>
<ModalContent>
<Breadcrumb
mb={6}
currentStep={stepIndex}
items={steps}
stepsDisabled={disabledSteps}
stepsErrors={errorSteps}
/>
<StepComponent {...stepProps} />
{children}
</ModalContent>
{renderFooter && (
<ModalFooter horizontal align="center" justify="flex-end">
<StepFooter {...stepProps} />
</ModalFooter>
<ModalBody
refocusWhenChange={stepId}
onClose={preventClose ? undefined : onClose}
onBack={onBack ? () => onBack(stepProps) : undefined}
title={title}
noScroll={noScroll}
render={() => (
<Fragment>
<Breadcrumb
mb={6}
currentStep={stepIndex}
items={steps}
stepsDisabled={disabledSteps}
stepsErrors={errorSteps}
/>
<StepComponent {...stepProps} />
{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 TopBar from 'components/TopBar'
import SyncBackground from 'components/SyncBackground'
import DebugUpdater from 'components/Updater/DebugUpdater'
import SyncContinuouslyPendingOperations from '../SyncContinouslyPendingOperations'
import HSMStatusBanner from '../HSMStatusBanner'
@ -41,7 +43,7 @@ const Main = styled(GrowScroll).attrs({
px: 6,
})`
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 = {
@ -98,6 +100,8 @@ class Default extends Component<Props> {
<ModalComponent key={name} />
))}
{process.env.DEBUG_UPDATE && <DebugUpdater />}
<SyncContinuouslyPendingOperations priority={20} interval={SYNC_PENDING_INTERVAL} />
<SyncBackground />

90
src/components/modals/AccountSettingRenderBody.js

@ -1,13 +1,13 @@
// @flow
import React, { PureComponent } from 'react'
import React, { PureComponent, Fragment } from 'react'
import styled from 'styled-components'
import { connect } from 'react-redux'
import { compose } from 'redux'
import get from 'lodash/get'
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 { MODAL_SETTINGS_ACCOUNT, MAX_ACCOUNT_NAME_SIZE } from 'config/constants'
import { validateNameEdition } from '@ledgerhq/live-common/lib/account'
@ -23,18 +23,14 @@ import TrackPage from 'analytics/TrackPage'
import Spoiler from 'components/base/Spoiler'
import CryptoCurrencyIcon from 'components/CryptoCurrencyIcon'
import Box from 'components/base/Box'
import Space from 'components/base/Space'
import Button from 'components/base/Button'
import Input from 'components/base/Input'
import Select from 'components/base/Select'
import SyncAgo from 'components/SyncAgo'
import {
ModalBody,
ModalTitle,
ModalFooter,
ModalContent,
ConfirmModal,
} from 'components/base/Modal'
import ConfirmModal from 'components/base/Modal/ConfirmModal'
import ModalBody from 'components/base/Modal/ModalBody'
type State = {
accountName: ?string,
@ -74,7 +70,7 @@ const defaultState = {
isRemoveAccountModalOpen: false,
}
class HelperComp extends PureComponent<Props, State> {
class AccountSettingRenderBody extends PureComponent<Props, State> {
state = {
...defaultState,
}
@ -84,7 +80,6 @@ class HelperComp extends PureComponent<Props, State> {
}
getAccount(data: Object): Account {
// FIXME this should be a selector
const { accountName } = this.state
const account = get(data, 'account', {})
@ -129,10 +124,9 @@ class HelperComp extends PureComponent<Props, State> {
})
handleSubmit = (account: Account, onClose: () => void) => (
e: SyntheticEvent<HTMLFormElement>,
e: SyntheticEvent<HTMLFormElement | HTMLInputElement>,
) => {
e.preventDefault()
const { updateAccount, setDataModal } = this.props
const { accountName, accountUnit, endpointConfig, endpointConfigError } = this.state
@ -194,10 +188,10 @@ class HelperComp extends PureComponent<Props, State> {
endpointConfigError,
} = this.state
const { t, onClose, data } = this.props
if (!data) return null
const account = this.getAccount(data)
const bridge = getBridgeForCurrency(account.currency)
const usefulData = {
xpub: account.xpub || undefined,
index: account.index,
@ -206,12 +200,15 @@ class HelperComp extends PureComponent<Props, State> {
blockHeight: account.blockHeight,
}
const onSubmit = this.handleSubmit(account, onClose)
return (
<ModalBody onClose={onClose}>
<form onSubmit={this.handleSubmit(account, onClose)}>
<TrackPage category="Modal" name="AccountSettings" />
<ModalTitle>{t('account.settings.title')}</ModalTitle>
<ModalContent mb={3}>
<ModalBody
onClose={onClose}
title={t('account.settings.title')}
render={() => (
<Fragment>
<TrackPage category="Modal" name="AccountSettings" />
<Container>
<Box>
<OptionRowTitle>{t('account.settings.accountName.title')}</OptionRowTitle>
@ -224,6 +221,7 @@ class HelperComp extends PureComponent<Props, State> {
value={account.name}
maxLength={MAX_ACCOUNT_NAME_SIZE}
onChange={this.handleChangeName}
onEnter={onSubmit}
onFocus={e => this.handleFocus(e, 'accountName')}
error={accountNameError}
/>
@ -268,8 +266,7 @@ class HelperComp extends PureComponent<Props, State> {
) : null}
<Spoiler textTransform title={t('account.settings.advancedLogs')}>
<SyncAgo date={account.lastSyncDate} />
<textarea
readOnly
<div
style={{
userSelect: 'text',
border: '1px dashed #f9f9f9',
@ -277,15 +274,33 @@ class HelperComp extends PureComponent<Props, State> {
color: '#000',
fontFamily: 'monospace',
fontSize: '10px',
height: 200,
outline: 'none',
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>
</ModalContent>
<ModalFooter horizontal>
<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')}
/>
<Space of={20} />
</Fragment>
)}
renderFooter={() => (
<Fragment>
<Button
event="OpenAccountDelete"
danger
@ -294,23 +309,12 @@ class HelperComp extends PureComponent<Props, State> {
>
{t('common.delete')}
</Button>
<Button event="DoneEditingAccount" ml="auto" type="submit" primary>
<Button event="DoneEditingAccount" ml="auto" onClick={onSubmit} primary>
{t('common.apply')}
</Button>
</ModalFooter>
</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>
</Fragment>
)}
/>
)
}
}
@ -321,9 +325,9 @@ export default compose(
mapDispatchToProps,
),
translate(),
)(HelperComp)
)(AccountSettingRenderBody)
export function InputLeft({ currency }: { currency: Currency }) {
export function InputLeft({ currency }: { currency: CryptoCurrency }) {
return (
<Box ml={2} style={{ justifyContent: 'center' }} color={currency.color}>
<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 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 type { T, Device } from 'types/common'
@ -24,6 +24,7 @@ import { closeModal } from 'reducers/modals'
import Modal from 'components/base/Modal'
import Stepper from 'components/base/Stepper'
import { validateNameEdition } from '@ledgerhq/live-common/lib/account'
import logger from 'logger'
import StepChooseCurrency, { StepChooseCurrencyFooter } from './steps/01-step-choose-currency'
import StepConnectDevice, { StepConnectDeviceFooter } from './steps/02-step-connect-device'
@ -43,6 +44,7 @@ const createSteps = () => {
footer: StepChooseCurrencyFooter,
onBack: null,
hideFooter: false,
noScroll: true,
},
{
id: 'connectDevice',
@ -88,7 +90,7 @@ type State = {
scanStatus: ScanStatus | string,
isAppOpened: boolean,
currency: ?Currency,
currency: ?CryptoCurrency,
scannedAccounts: Account[],
checkedAccountsIds: string[],
editedNames: { [_: string]: string },
@ -98,7 +100,7 @@ type State = {
export type StepProps = DefaultStepProps & {
t: T,
currency: ?Currency,
currency: ?CryptoCurrency,
device: ?Device,
isAppOpened: boolean,
scannedAccounts: Account[],
@ -110,7 +112,7 @@ export type StepProps = DefaultStepProps & {
onGoStep1: () => void,
onCloseModal: () => void,
resetScanState: () => void,
setCurrency: (?Currency) => void,
setCurrency: (?CryptoCurrency) => void,
setAppOpened: boolean => void,
setScanStatus: (ScanStatus, ?Error) => string,
setAccountName: (Account, string) => void,
@ -162,9 +164,12 @@ class AddAccounts extends PureComponent<Props, State> {
handleCloseModal = () => this.props.closeModal(MODAL_ADD_ACCOUNTS)
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) => {
if (err) {
logger.critical(err)
}
this.setState({ scanStatus, err })
}
@ -241,6 +246,7 @@ class AddAccounts extends PureComponent<Props, State> {
return (
<Modal
centered
name={MODAL_ADD_ACCOUNTS}
refocusWhenChange={stepId}
onHide={() => this.setState({ ...INITIAL_STATE })}

32
src/components/modals/Debug.js

@ -1,11 +1,12 @@
// @flow
/* eslint-disable react/jsx-no-literals */
import React, { Component } from 'react'
import React, { Component, Fragment } from 'react'
import { connect } from 'react-redux'
import { createStructuredSelector } from 'reselect'
import { getCryptoCurrencyById } from '@ledgerhq/live-common/lib/currencies'
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 Button from 'components/base/Button'
import Box from 'components/base/Box'
@ -127,14 +128,12 @@ class Debug extends Component<*, *> {
const { device } = this.props
const { logs } = this.state
return (
<Modal
name="MODAL_DEBUG"
onHide={this.onHide}
render={({ onClose }: *) => (
<ModalBody onClose={onClose}>
<SyncSkipUnderPriority priority={99999999} />
<ModalTitle>developer internal tools</ModalTitle>
<ModalContent>
<Modal name="MODAL_DEBUG" centered onHide={this.onHide}>
<ModalBody
title="developer internal tools"
render={() => (
<Box>
<SyncSkipUnderPriority priority={99999999} />
<Box style={{ height: 60, overflow: 'auto' }}>
{device && (
<Box horizontal style={{ padding: 10 }}>
@ -191,6 +190,7 @@ class Debug extends Component<*, *> {
>
{logs.map(log => (
<Box
key={log.txt}
style={{
userSelect: 'all',
color: log.type === 'error' ? '#c22' : '#888',
@ -200,6 +200,10 @@ class Debug extends Component<*, *> {
</Box>
))}
</Box>
</Box>
)}
renderFooter={() => (
<Fragment>
<Button
style={{ position: 'absolute', right: 30, bottom: 28 }}
onClick={() => {
@ -208,10 +212,10 @@ class Debug extends Component<*, *> {
>
Clear
</Button>
</ModalContent>
</ModalBody>
)}
/>
</Fragment>
)}
/>
</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, { 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 Box from 'components/base/Box'
import { HandShield } from 'components/WarnBox'
import { compose } from 'redux'
import connect from 'react-redux/es/connect/connect'
import { closeModal } from '../../reducers/modals'
type Props = {
t: T,
closeModal: string => void,
}
const mapDispatchToProps = {
closeModal,
}
class DisclaimerModal extends PureComponent<Props> {
onClose = () => this.props.closeModal(MODAL_DISCLAIMER)
render() {
const { t } = this.props
return (
<Modal
name={MODAL_DISCLAIMER}
render={({ onClose }) => (
<ModalBody onClose={onClose}>
<ModalTitle>{t('disclaimerModal.title')}</ModalTitle>
<ModalContent flow={4} ff="Open Sans|Regular" fontSize={4} color="smoke">
<Modal name={MODAL_DISCLAIMER} centered>
<ModalBody
onClose={this.onClose}
title={t('disclaimerModal.title')}
render={() => (
<Box flow={4} ff="Open Sans|Regular" fontSize={4} color="smoke">
<Box align="center" mt={4} pb={4}>
<HandShield size={55} />
</Box>
<p>{t('disclaimerModal.desc_1')}</p>
<p>{t('disclaimerModal.desc_2')}</p>
</ModalContent>
<ModalFooter horizontal justifyContent="flex-end">
<Button data-e2e="continue_button" onClick={onClose} primary>
</Box>
)}
renderFooter={() => (
<Box horizontal justifyContent="flex-end">
<Button data-e2e="continue_button" onClick={this.onClose} primary>
{t('disclaimerModal.cta')}
</Button>
</ModalFooter>
</ModalBody>
)}
/>
</Box>
)}
/>
</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 Box from 'components/base/Box'
import GradientBox from 'components/GradientBox'
import GrowScroll from 'components/base/GrowScroll'
import Button from 'components/base/Button'
import Bar from 'components/base/Bar'
import FormattedVal from 'components/base/FormattedVal'
import Modal, { ModalBody, ModalTitle, ModalFooter, ModalContent } from 'components/base/Modal'
import Modal, { ModalBody } from 'components/base/Modal'
import Text from 'components/base/Text'
import CopyWithFeedback from 'components/base/CopyWithFeedback'
@ -130,122 +128,120 @@ const OperationDetails = connect(mapStateToProps)((props: Props) => {
const uniqueSenders = uniq(senders)
return (
<ModalBody onClose={onClose}>
<TrackPage category="Modal" name="OperationDetails" />
<ModalTitle>{t('operationDetails.title')}</ModalTitle>
<ModalContent relative style={{ height: 500 }} px={0} pb={0}>
<GrowScroll px={5} pt={1} pb={8}>
<Box flow={3}>
<Box alignItems="center" mt={1}>
<ConfirmationCheck
marketColor={marketColor}
isConfirmed={isConfirmed}
style={{
transform: 'scale(1.5)',
}}
t={t}
type={type}
withTooltip={false}
/>
<Box my={4} alignItems="center">
<Box>
<FormattedVal
color={amount.isNegative() ? 'smoke' : undefined}
unit={unit}
alwaysShowSign
showCode
val={amount}
fontSize={7}
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>
<ModalBody
title={t('operationDetails.title')}
onClose={onClose}
render={() => (
<Box flow={3}>
<Box alignItems="center" mt={1}>
<ConfirmationCheck
marketColor={marketColor}
isConfirmed={isConfirmed}
style={{
transform: 'scale(1.5)',
}}
t={t}
type={type}
withTooltip={false}
/>
<Box my={4} alignItems="center">
<Box>
<FormattedVal
color={amount.isNegative() ? 'smoke' : undefined}
unit={unit}
alwaysShowSign
showCode
val={amount}
fontSize={7}
disableRounding
/>
</Box>
<Box flex={1}>
<OpDetailsTitle>{t('operationDetails.date')}</OpDetailsTitle>
<OpDetailsData>{moment(date).format('LLL')}</OpDetailsData>
<Box mt={1}>
<CounterValue
color="grey"
fontSize={5}
date={date}
currency={currency}
value={amount}
/>
</Box>
</Box>
<B />
<Box horizontal flow={2}>
<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 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 horizontal flow={2}>
<Box flex={1}>
<OpDetailsTitle>{t('operationDetails.account')}</OpDetailsTitle>
<OpDetailsData>{name}</OpDetailsData>
</Box>
<B />
<Box>
<OpDetailsTitle>{t('operationDetails.identifier')}</OpDetailsTitle>
<OpDetailsData>
<Ellipsis canSelect>{hash}</Ellipsis>
<GradientHover>
<CopyWithFeedback text={hash} />
</GradientHover>
</OpDetailsData>
<Box flex={1}>
<OpDetailsTitle>{t('operationDetails.date')}</OpDetailsTitle>
<OpDetailsData>{moment(date).format('LLL')}</OpDetailsData>
</Box>
<B />
<Box>
<OpDetailsTitle>{t('operationDetails.from')}</OpDetailsTitle>
<DataList lines={uniqueSenders} t={t} />
</Box>
<B />
<Box horizontal flow={2}>
<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>
<B />
<Box>
<OpDetailsTitle>{t('operationDetails.to')}</OpDetailsTitle>
<DataList lines={recipients} t={t} />
<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>
{Object.entries(extra).map(([key, value]) => (
<Box key={key}>
<OpDetailsTitle>
<Trans i18nKey={`operationDetails.extra.${key}`} defaults={key} />
</OpDetailsTitle>
<OpDetailsData>{value}</OpDetailsData>
</Box>
))}
</Box>
</GrowScroll>
<GradientBox />
</ModalContent>
{url && (
<ModalFooter horizontal justify="flex-end" flow={2}>
<B />
<Box>
<OpDetailsTitle>{t('operationDetails.identifier')}</OpDetailsTitle>
<OpDetailsData>
<Ellipsis canSelect>{hash}</Ellipsis>
<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)}>
{t('operationDetails.viewOperation')}
</Button>
</ModalFooter>
)}
)
}
>
<TrackPage category="Modal" name="OperationDetails" />
</ModalBody>
)
})
@ -255,12 +251,13 @@ type ModalRenderProps = {
account: string,
operation: string,
},
onClose: Function,
onClose?: Function,
}
const OperationDetailsWrapper = ({ t }: { t: T }) => (
<Modal
name={MODAL_OPERATION_DETAILS}
centered
render={(props: ModalRenderProps) => {
const { data, onClose } = props
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 logger from 'logger'
import Track from 'analytics/Track'
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 })
handleChangeAddressVerified = (isAddressVerified: boolean, err: ?Error) => {
if (err && err.name !== 'UserRefusedAddress') {
logger.critical(err)
}
this.setState({ isAddressVerified, verifyAddressError: err })
}
@ -193,16 +197,17 @@ class ReceiveModal extends PureComponent<Props, State> {
return (
<Modal
name={MODAL_RECEIVE}
centered
refocusWhenChange={stepId}
onHide={this.handleReset}
preventBackdropClick={isModalLocked}
onBeforeOpen={this.handleBeforeOpenModal}
render={({ onClose }) => (
render={() => (
<Stepper
title={t('receive.title')}
initialStepId={stepId}
onStepChange={this.handleStepChange}
onClose={onClose}
onClose={addtionnalProps.closeModal}
steps={this.STEPS}
disabledSteps={disabledSteps}
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 Box from 'components/base/Box'
import GrowScroll from 'components/base/GrowScroll'
import Text from 'components/base/Text'
import Spinner from 'components/base/Spinner'
import GradientBox from 'components/GradientBox'
import TranslatedError from 'components/TranslatedError'
import TrackPage from 'analytics/TrackPage'
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'
@ -115,21 +114,26 @@ class ReleaseNotesBody extends PureComponent<Props, State> {
const { onClose, t } = this.props
return (
<ModalBody onClose={onClose}>
<TrackPage category="Modal" name="ReleaseNotes" />
<ModalTitle>{t('releaseNotes.title')}</ModalTitle>
<ModalContent relative style={{ height: 500 }} px={0} pb={0}>
<GrowScroll px={5} pb={8}>
{this.renderContent()}
</GrowScroll>
<GradientBox />
</ModalContent>
<ModalFooter horizontal justifyContent="flex-end">
<Button onClick={onClose} primary>
{t('common.continue')}
</Button>
</ModalFooter>
</ModalBody>
<ModalBody
onClose={onClose}
title={t('releaseNotes.title')}
render={() => (
<Box relative style={{ height: 500 }} px={0} pb={0}>
<TrackPage category="Modal" name="ReleaseNotes" />
<Box px={5} pb={8}>
{this.renderContent()}
</Box>
<GradientBox />
</Box>
)}
renderFooter={() => (
<Box horizontal justifyContent="flex-end">
<Button onClick={onClose} primary>
{t('common.continue')}
</Button>
</Box>
)}
/>
)
}
}

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

@ -1,16 +1,56 @@
// @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 from 'components/base/Modal'
import ReleaseNotesBody from './ReleaseNotesBody'
const ReleaseNotesModal = () => (
<Modal
name={MODAL_RELEASES_NOTES}
render={({ data, onClose }) => <ReleaseNotesBody version={data} onClose={onClose} />}
/>
)
type Props = {
openModal: Function,
saveSettings: Function,
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 syncId = ++this.syncId
const recipient = bridge.getTransactionRecipient(account, transaction)
const isValid = await bridge.isRecipientValid(account.currency, recipient)
const warning = await bridge.getRecipientWarning(account.currency, recipient)
const isValid = await bridge.isRecipientValid(account, recipient)
const warning = await bridge.getRecipientWarning(account, recipient)
if (syncId !== this.syncId) return
if (this.isUnmounted) return
this.setState({ isValid, warning })
@ -69,9 +69,7 @@ class RecipientField<Transaction> extends Component<
if (amount) {
t = bridge.editTransactionAmount(account, t, amount)
}
const warning = fromQRCode
? await bridge.getRecipientWarning(account.currency, recipient)
: null
const warning = fromQRCode ? await bridge.getRecipientWarning(account, recipient) : null
if (this.isUnmounted) return false
if (warning) {
// clear the input if field has warning AND has a warning
@ -97,7 +95,7 @@ class RecipientField<Transaction> extends Component<
const error =
!value || isValid
? QRCodeRefusedReason
: new InvalidAddress(null, { currencyName: account.currency.name })
: warning || new InvalidAddress(null, { currencyName: account.currency.name })
return (
<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 { MODAL_SEND } from 'config/constants'
import { getBridgeForCurrency } from 'bridge'
import logger from 'logger'
import type { WalletBridge } from 'bridge/types'
import type { T, Device } from 'types/common'
@ -180,6 +181,9 @@ class SendModal extends PureComponent<Props, State<*>> {
}
handleTransactionError = (error: Error) => {
if (!(error instanceof UserRefusedOnDevice)) {
logger.critical(error)
}
const stepVerificationIndex = this.STEPS.findIndex(step => step.id === 'verification')
if (stepVerificationIndex === -1) return
this.setState({ error })
@ -273,6 +277,7 @@ class SendModal extends PureComponent<Props, State<*>> {
return (
<Modal
name={MODAL_SEND}
centered
refocusWhenChange={stepId}
onHide={this.handleReset}
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)
if (syncId !== this.syncId) return
const isRecipientValid = await bridge.isRecipientValid(
account.currency,
account,
bridge.getTransactionRecipient(account, transaction),
)
if (syncId !== this.syncId) return

5
src/components/modals/SettingsAccount.js

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

59
src/components/modals/TechnicalData.js

@ -1,19 +1,30 @@
// @flow
import React, { PureComponent } from 'react'
import React, { Fragment, PureComponent } from 'react'
import { translate } from 'react-i18next'
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 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 { closeModal } from '../../reducers/modals'
type Props = {
t: T,
closeModal: string => void,
}
const mapDispatchToProps = {
closeModal,
}
class TechnicalData extends PureComponent<Props, *> {
onClose = () => this.props.closeModal(MODAL_TECHNICAL_DATA)
render() {
const { t } = this.props
@ -33,27 +44,35 @@ class TechnicalData extends PureComponent<Props, *> {
]
return (
<Modal
name={MODAL_TECHNICAL_DATA}
render={({ onClose }) => (
<ModalBody onClose={onClose}>
<ModalTitle data-e2e="modal_title_TechData">
{t('onboarding.analytics.technicalData.mandatoryContextual.title')}
</ModalTitle>
<InlineDesc>{t('onboarding.analytics.technicalData.desc')}</InlineDesc>
<ModalContent mx={5}>
<Ul>{items.map(item => <li key={item.key}>{item.desc}</li>)}</Ul>
</ModalContent>
<ModalFooter horizontal justifyContent="flex-end">
<Button onClick={onClose} primary data-e2e="modal_buttonClose_techData">
<Modal name={MODAL_TECHNICAL_DATA} centered>
<ModalBody
onClose={this.onClose}
title={t('onboarding.analytics.technicalData.mandatoryContextual.title')}
render={() => (
<Fragment>
<InlineDesc>{t('onboarding.analytics.technicalData.desc')}</InlineDesc>
<Box mx={5}>
<Ul>{items.map(item => <li key={item.key}>{item.desc}</li>)}</Ul>
</Box>
</Fragment>
)}
renderFooter={() => (
<Fragment>
<Button onClick={this.onClose} primary data-e2e="modal_buttonClose_techData">
{t('common.close')}
</Button>
</ModalFooter>
</ModalBody>
)}
/>
</Fragment>
)}
/>
</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
/* eslint react/jsx-no-literals: 0 */
import React, { PureComponent, Fragment } from 'react'
import React, { PureComponent } from 'react'
import { translate, Trans } from 'react-i18next'
import type { OsuFirmware, FinalFirmware } from '@ledgerhq/live-common/lib/types/manager'
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 Button from 'components/base/Button'
import GrowScroll from 'components/base/GrowScroll'
@ -17,6 +18,7 @@ import TrackPage from 'analytics/TrackPage'
import type { ModalStatus } from 'components/ManagerPage/FirmwareUpdate'
import { getCleanVersion } from 'components/ManagerPage/FirmwareUpdate'
import Box from '../../base/Box/Box'
type Props = {
t: T,
@ -35,49 +37,51 @@ class DisclaimerModal extends PureComponent<Props, State> {
render(): React$Node {
const { status, firmware, onClose, t, goToNextStep } = this.props
return (
<Modal
isOpened={status === 'disclaimer'}
onClose={onClose}
render={({ onClose }) => (
<ModalBody onClose={onClose} grow align="center" justify="center" mt={3}>
<TrackPage category="Manager" name="DisclaimerModal" />
<Fragment>
<ModalTitle>{t('manager.firmware.update')}</ModalTitle>
<ModalContent>
<Text ff="Open Sans|Regular" fontSize={4} color="graphite" align="center">
<Trans i18nKey="manager.firmware.disclaimerTitle">
You are about to install
<Text ff="Open Sans|SemiBold" color="dark">
{`firmware version ${
firmware && firmware.osu ? getCleanVersion(firmware.osu.name) : ''
}`}
</Text>
</Trans>
</Text>
<Text ff="Open Sans|Regular" fontSize={4} color="graphite" align="center">
{t('manager.firmware.disclaimerAppDelete')}
{t('manager.firmware.disclaimerAppReinstall')}
</Text>
</ModalContent>
<Modal isOpened={status === 'disclaimer'} onClose={onClose}>
<TrackPage category="Manager" name="DisclaimerModal" />
<ModalBody
grow
align="center"
justify="center"
mt={3}
title={t('manager.firmware.update')}
render={() => (
<Box>
<Text ff="Open Sans|Regular" fontSize={4} color="graphite" align="center">
<Trans i18nKey="manager.firmware.disclaimerTitle">
You are about to install
<Text ff="Open Sans|SemiBold" color="dark">
{`firmware version ${
firmware && firmware.osu ? getCleanVersion(firmware.osu.name) : ''
}`}
</Text>
</Trans>
</Text>
<Text ff="Open Sans|Regular" fontSize={4} color="graphite" align="center">
{t('manager.firmware.disclaimerAppDelete')}
{t('manager.firmware.disclaimerAppReinstall')}
</Text>
{firmware && firmware.osu ? (
<ModalContent relative pb={0} style={{ height: 250, width: '100%' }}>
<Box relative pb={0} style={{ height: 250, width: '100%' }}>
<GrowScroll pb={5}>
<Notes>
<Markdown>{firmware.osu.notes}</Markdown>
</Notes>
</GrowScroll>
<GradientBox />
</ModalContent>
</Box>
) : null}
<ModalFooter horizontal justifyContent="flex-end" style={{ width: '100%' }}>
<Button primary onClick={goToNextStep}>
{t('common.continue')}
</Button>
</ModalFooter>
</Fragment>
</ModalBody>
)}
/>
</Box>
)}
renderFooter={() => (
<Box horizontal justifyContent="flex-end">
<Button primary onClick={goToNextStep}>
{t('common.continue')}
</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 { ModalStatus } from 'components/ManagerPage/FirmwareUpdate'
import logger from 'logger'
import { FreezeDeviceChangeEvents } from '../../ManagerPage/HookDeviceChange'
import StepFullFirmwareInstall from './steps/01-step-install-full-firmware'
@ -82,7 +83,10 @@ class UpdateModal extends PureComponent<Props, State> {
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++ })
@ -103,6 +107,7 @@ class UpdateModal extends PureComponent<Props, State> {
return (
<Modal
onClose={onClose}
centered
onHide={this.handleReset}
isOpened={status === 'install'}
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 HIGHLIGHT_I18N = boolFromEnv('HIGHLIGHT_I18N')
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_FIRMWARE_UPDATE = boolFromEnv('EXPERIMENTAL_FIRMWARE_UPDATE')
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')
// 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
export const MAX_ACCOUNT_NAME_SIZE = 50

6
src/config/urls.js

@ -1,11 +1,17 @@
// @flow
export const urls = {
liveHome: 'https://www.ledger.com/pages/ledger-live',
// Social
twitter: 'https://twitter.com/LedgerHQ',
github: 'https://github.com/LedgerHQ/ledger-live-desktop',
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
faq: 'https://support.ledgerwallet.com/hc/en-us',
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 }) => (
<svg viewBox="0 0 17 17" height={height} width={width} {...p}>
export default ({
height,
width,
size,
...p
}: {
height?: number,
width?: number,
size?: number,
}) => (
<svg viewBox="0 0 17 17" height={height || size} width={width || size} {...p}>
{path}
</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 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="" />
</div>
<div id="app"></div>
<div id="modals"></div>
<script>
const { remote } = require('electron')
const { name } = remote.getCurrentWindow()

63
src/internals/index.js

@ -1,16 +1,18 @@
// @flow
import '@babel/polyfill'
import { serializeError } from '@ledgerhq/errors/lib/helpers'
import 'helpers/live-common-setup'
import 'helpers/live-common-setup-internal-hw'
import commands from 'commands'
import logger from 'logger'
import LoggerTransport from 'logger/logger-transport-internal'
import uuid from 'uuid/v4'
import { setImplementation } from 'api/network'
import sentry from 'sentry/node'
import { EXPERIMENTAL_HTTP_ON_RENDERER } from 'config/constants'
import { serializeError } from '@ledgerhq/errors/lib/helpers'
import { executeCommand, unsubscribeCommand } from 'main/commandHandler'
require('../env')
@ -58,64 +60,11 @@ if (EXPERIMENTAL_HTTP_ON_RENDERER) {
})
}
const subscriptions = {}
process.on('message', m => {
if (m.type === 'command') {
const { data, requestId, id } = m.command
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),
})
}
executeCommand(m.command, process.send.bind(process))
} else if (m.type === 'command-unsubscribe') {
const { requestId } = m
const sub = subscriptions[requestId]
if (sub) {
sub.unsubscribe()
delete subscriptions[requestId]
}
unsubscribeCommand(m.requestId)
} else if (m.type === 'executeHttpQueryPayload') {
const { payload } = m
const defer = defers[payload.id]

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

Loading…
Cancel
Save