Browse Source

Merge branch 'master' into auto-update

master
Thibaut 7 years ago
committed by GitHub
parent
commit
f3c97677de
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      package.json
  2. 5
      src/bridge/EthereumJSBridge.js
  3. 145
      src/bridge/LibcoreBridge.js
  4. 6
      src/bridge/RippleJSBridge.js
  5. 2
      src/bridge/types.js
  6. 2
      src/commands/installOsuFirmware.js
  7. 44
      src/commands/libcoreScanAccounts.js
  8. 94
      src/commands/libcoreSignAndBroadcast.js
  9. 9
      src/commands/listApps.js
  10. 2
      src/components/App.js
  11. 2
      src/components/FeesField/BitcoinKind.js
  12. 14
      src/components/ManagerPage/AppsList.js
  13. 74
      src/components/ManagerPage/index.js
  14. 4
      src/components/Onboarding/OnboardingBreadcrumb.js
  15. 5
      src/components/Onboarding/OnboardingFooter.js
  16. 14
      src/components/Onboarding/index.js
  17. 107
      src/components/Onboarding/steps/Analytics.js
  18. 72
      src/components/Onboarding/steps/GenuineCheck.js
  19. 15
      src/components/Onboarding/steps/Init.js
  20. 2
      src/components/RecipientAddress/index.js
  21. 2
      src/components/base/FlipTicker/index.js
  22. 6
      src/components/modals/Send/01-step-amount.js
  23. 13
      src/helpers/apps/listApps.js
  24. 1
      src/helpers/ipc.js
  25. 6
      src/helpers/libcore.js
  26. 16
      src/icons/Recover.js
  27. 48
      src/internals/accounts/index.js
  28. 71
      src/internals/accounts/signAndBroadcastTransaction/btc.js
  29. 7
      src/internals/accounts/sync.js
  30. 6
      src/internals/devices/index.js
  31. 3
      src/internals/manager/index.js
  32. 27
      src/reducers/onboarding.js
  33. 2
      src/reducers/settings.js
  34. 8
      static/i18n/en/onboarding.yml
  35. 12
      yarn.lock

6
package.json

@ -3,7 +3,7 @@
"productName": "Ledger Live",
"description": "Ledger Live - Desktop",
"repository": "https://github.com/LedgerHQ/ledger-live-desktop",
"version": "0.1.0-alpha.3",
"version": "0.1.0-alpha.4",
"author": "Ledger",
"license": "MIT",
"scripts": {
@ -41,8 +41,8 @@
"@ledgerhq/hw-app-xrp": "^4.12.0",
"@ledgerhq/hw-transport": "^4.12.0",
"@ledgerhq/hw-transport-node-hid": "^4.12.0",
"@ledgerhq/ledger-core": "^1.2.1",
"@ledgerhq/live-common": "^2.9.1",
"@ledgerhq/ledger-core": "^1.3.0",
"@ledgerhq/live-common": "2.11.0",
"axios": "^0.18.0",
"babel-runtime": "^6.26.0",
"bcryptjs": "^2.4.3",

5
src/bridge/EthereumJSBridge.js

@ -115,7 +115,6 @@ const EthereumBridge: WalletBridge<Transaction> = {
let { txs } = await api.getTransactions(address)
if (finished) return { complete: true }
const path = freshAddressPath // FIXME
const freshAddress = address
if (txs.length === 0) {
@ -127,7 +126,6 @@ const EthereumBridge: WalletBridge<Transaction> = {
const account: $Exact<Account> = {
id: accountId,
xpub: '',
path, // FIXME we probably not want the address path in the account.path
freshAddress,
freshAddressPath,
name: 'New Account',
@ -153,7 +151,6 @@ const EthereumBridge: WalletBridge<Transaction> = {
const account: $Exact<Account> = {
id: accountId,
xpub: '',
path, // FIXME we probably not want the address path in the account.path
freshAddress,
freshAddressPath,
name: address.slice(32),
@ -308,7 +305,7 @@ const EthereumBridge: WalletBridge<Transaction> = {
.send({
currencyId: a.currency.id,
devicePath: deviceId,
path: a.path,
path: a.freshAddressPath,
transaction: { ...t, nonce },
})
.toPromise()

145
src/bridge/LibcoreBridge.js

@ -1,21 +1,20 @@
// @flow
import React from 'react'
import { ipcRenderer } from 'electron'
import { map } from 'rxjs/operators'
import { decodeAccount, encodeAccount } from 'reducers/accounts'
import runJob from 'renderer/runJob'
import FeesBitcoinKind from 'components/FeesField/BitcoinKind'
import AdvancedOptionsBitcoinKind from 'components/AdvancedOptions/BitcoinKind'
import libcoreScanAccounts from 'commands/libcoreScanAccounts'
import libcoreSignAndBroadcast from 'commands/libcoreSignAndBroadcast'
// import AdvancedOptionsBitcoinKind from 'components/AdvancedOptions/BitcoinKind'
import type { WalletBridge, EditProps } from './types'
const notImplemented = new Error('LibcoreBridge: not implemented')
// TODO for ipcRenderer listeners we should have a concept of requestId because
// to be able to listen to events that only concerns you
// IMPORTANT: please read ./types.js that specify & document everything
type Transaction = *
type Transaction = {
amount: number,
feePerByte: number,
recipient: string,
}
const EditFees = ({ account, onChange, value }: EditProps<Transaction>) => (
<FeesBitcoinKind
@ -27,6 +26,8 @@ const EditFees = ({ account, onChange, value }: EditProps<Transaction>) => (
/>
)
const EditAdvancedOptions = undefined // Not implemented yet
/*
const EditAdvancedOptions = ({ onChange, value }: EditProps<Transaction>) => (
<AdvancedOptionsBitcoinKind
isRBF={value.isRBF}
@ -35,93 +36,32 @@ const EditAdvancedOptions = ({ onChange, value }: EditProps<Transaction>) => (
}}
/>
)
*/
const LibcoreBridge: WalletBridge<Transaction> = {
synchronize(initialAccount, { next, complete, error }) {
const unbind = () => ipcRenderer.removeListener('msg', handleAccountSync)
function handleAccountSync(e, msg) {
switch (msg.type) {
case 'account.sync.progress': {
next(a => a)
// FIXME TODO: use next(), to actually emit account updates.....
// - need to sync the balance
// - need to sync block height & block hash
// - need to sync operations.
// - once all that, need to set lastSyncDate to new Date()
// - when you implement addPendingOperation you also here need to:
// - if there were pendingOperations that are now in operations, remove them as well.
// - if there are pendingOperations that is older than a threshold (that depends on blockchain speed typically)
// then we probably should trash them out? it's a complex question for UI
break
}
case 'account.sync.fail': {
unbind()
error(new Error('failed')) // TODO more error detail
break
}
case 'account.sync.success': {
unbind()
complete()
break
}
default:
}
}
ipcRenderer.on('msg', handleAccountSync)
// TODO how to start the sync ?!
return {
unsubscribe() {
unbind()
console.warn('LibcoreBridge: interrupting synchronization is not supported')
},
}
},
scanAccountsOnDevice(currency, deviceId, { next, complete, error }) {
const unbind = () => ipcRenderer.removeListener('msg', handleMsgEvent)
function handleMsgEvent(e, { data, type }) {
if (type === 'accounts.scanAccountsOnDevice.accountScanned') {
next({ ...decodeAccount(data), archived: true })
}
}
ipcRenderer.on('msg', handleMsgEvent)
let unsubscribed
runJob({
channel: 'accounts',
job: 'scan',
successResponse: 'accounts.scanAccountsOnDevice.success',
errorResponse: 'accounts.scanAccountsOnDevice.fail',
data: {
devicePath: deviceId,
scanAccountsOnDevice(currency, devicePath, observer) {
return libcoreScanAccounts
.send({
devicePath,
currencyId: currency.id,
},
}).then(
() => {
if (unsubscribed) return
unbind()
complete()
},
e => {
if (unsubscribed) return
unbind()
error(e)
},
)
})
.pipe(map(decodeAccount))
.subscribe(observer)
},
synchronize(_initialAccount, _observer) {
// FIXME TODO: use next(), to actually emit account updates.....
// - need to sync the balance
// - need to sync block height & block hash
// - need to sync operations.
// - once all that, need to set lastSyncDate to new Date()
// - when you implement addPendingOperation you also here need to:
// - if there were pendingOperations that are now in operations, remove them as well.
// - if there are pendingOperations that is older than a threshold (that depends on blockchain speed typically)
// then we probably should trash them out? it's a complex question for UI
return {
unsubscribe() {
unsubscribed = true
unbind()
console.warn('LibcoreBridge: interrupting scanAccounts is not implemented') // FIXME
console.warn('LibcoreBridge: sync not implemented')
},
}
},
@ -165,15 +105,20 @@ const LibcoreBridge: WalletBridge<Transaction> = {
getMaxAmount: (a, _t) => Promise.resolve(a.balance), // FIXME
signAndBroadcast: (account, transaction, deviceId) => {
const rawAccount = encodeAccount(account)
return runJob({
channel: 'accounts',
job: 'signAndBroadcastTransactionBTCLike',
successResponse: 'accounts.signAndBroadcastTransactionBTCLike.success',
errorResponse: 'accounts.signAndBroadcastTransactionBTCLike.fail',
data: { account: rawAccount, transaction, deviceId },
})
signAndBroadcast: async (account, transaction, deviceId) => {
const encodedAccount = encodeAccount(account)
const rawOp = await libcoreSignAndBroadcast
.send({
account: encodedAccount,
transaction,
deviceId,
})
.toPromise()
// quick HACK
const [op] = decodeAccount({ ...encodedAccount, operations: [rawOp] }).operations
return op
},
}

6
src/bridge/RippleJSBridge.js

@ -188,8 +188,6 @@ const RippleJSBridge: WalletBridge<Transaction> = {
for (const derivation of derivations) {
for (let index = 0; index < 255; index++) {
const freshAddressPath = derivation({ currency, x: index, segwit: false })
const path = freshAddressPath
// FIXME^ we need the account path, not the address path
const { address } = await await getAddress
.send({ currencyId: currency.id, devicePath: deviceId, path: freshAddressPath })
.toPromise()
@ -215,7 +213,6 @@ const RippleJSBridge: WalletBridge<Transaction> = {
next({
id: accountId,
xpub: '',
path,
name: 'New Account',
freshAddress,
freshAddressPath,
@ -247,7 +244,6 @@ const RippleJSBridge: WalletBridge<Transaction> = {
const account: $Exact<Account> = {
id: accountId,
xpub: '',
path,
name: address.slice(0, 8),
freshAddress,
freshAddressPath,
@ -426,7 +422,7 @@ const RippleJSBridge: WalletBridge<Transaction> = {
.send({
currencyId: a.currency.id,
devicePath: deviceId,
path: a.path,
path: a.freshAddressPath,
transaction: JSON.parse(prepared.txJSON),
})
.toPromise()

2
src/bridge/types.js

@ -15,7 +15,7 @@ export type Observer<T> = {
}
export type Subscription = {
unsubscribe: () => void,
+unsubscribe: () => void,
}
export type EditProps<Transaction> = {

2
src/commands/installOsuFirmware.js

@ -2,8 +2,8 @@
import { createCommand, Command } from 'helpers/ipc'
import { fromPromise } from 'rxjs/observable/fromPromise'
import { withDevice } from 'helpers/deviceAccess'
import { withDevice } from 'helpers/deviceAccess'
import installOsuFirmware from 'helpers/firmware/installOsuFirmware'
type Input = {

44
src/commands/libcoreScanAccounts.js

@ -0,0 +1,44 @@
// @flow
import type { AccountRaw } from '@ledgerhq/live-common/lib/types'
import { createCommand, Command } from 'helpers/ipc'
import { Observable } from 'rxjs'
import { scanAccountsOnDevice } from 'helpers/libcore'
type Input = {
devicePath: string,
currencyId: string,
}
type Result = AccountRaw
const cmd: Command<Input, Result> = createCommand(
'devices',
'libcoreScanAccounts',
({ devicePath, currencyId }) =>
Observable.create(o => {
// TODO scanAccountsOnDevice should directly return a Observable so we just have to pass-in
scanAccountsOnDevice({
devicePath,
currencyId,
onAccountScanned: account => {
o.next(account)
},
}).then(
() => {
o.complete()
},
e => {
o.error(e)
},
)
function unsubscribe() {
// FIXME not implemented
}
return unsubscribe
}),
)
export default cmd

94
src/commands/libcoreSignAndBroadcast.js

@ -0,0 +1,94 @@
// @flow
import type { AccountRaw, OperationRaw } from '@ledgerhq/live-common/lib/types'
import Btc from '@ledgerhq/hw-app-btc'
import { createCommand, Command } from 'helpers/ipc'
import { withDevice } from 'helpers/deviceAccess'
import { getWalletIdentifier } from 'helpers/libcore'
import { fromPromise } from 'rxjs/observable/fromPromise'
import { getCryptoCurrencyById } from '@ledgerhq/live-common/lib/helpers/currencies'
type BitcoinLikeTransaction = {
amount: number,
feePerByte: number,
recipient: string,
}
type Input = {
account: AccountRaw,
transaction: BitcoinLikeTransaction,
deviceId: string,
}
type Result = $Exact<OperationRaw>
const cmd: Command<Input, Result> = createCommand(
'devices',
'libcoreSignAndBroadcast',
({ account, transaction, deviceId }) => {
// TODO: investigate why importing it on file scope causes trouble
const core = require('init-ledger-core')()
return fromPromise(
withDevice(deviceId)(async transport => {
const hwApp = new Btc(transport)
const WALLET_IDENTIFIER = await getWalletIdentifier({
hwApp,
isSegwit: !!account.isSegwit,
currencyId: account.currencyId,
devicePath: deviceId,
})
const njsWallet = await core.getWallet(WALLET_IDENTIFIER)
const njsAccount = await njsWallet.getAccount(account.index)
const bitcoinLikeAccount = njsAccount.asBitcoinLikeAccount()
const njsWalletCurrency = njsWallet.getCurrency()
const amount = core.createAmount(njsWalletCurrency, transaction.amount)
const fees = core.createAmount(njsWalletCurrency, transaction.feePerByte)
const transactionBuilder = bitcoinLikeAccount.buildTransaction()
// TODO: check if is valid address. if not, it will fail silently on invalid
transactionBuilder.sendToAddress(amount, transaction.recipient)
// TODO: don't use hardcoded value for sequence (and first also maybe)
transactionBuilder.pickInputs(0, 0xffffff)
transactionBuilder.setFeesPerByte(fees)
const builded = await transactionBuilder.build()
const sigHashType = core.helpers.bytesToHex(
njsWalletCurrency.bitcoinLikeNetworkParameters.SigHash,
)
const currency = getCryptoCurrencyById(account.currencyId)
const signedTransaction = await core.signTransaction({
hwApp,
transaction: builded,
sigHashType,
supportsSegwit: currency.supportsSegwit,
isSegwit: account.isSegwit,
})
const txHash = await njsAccount
.asBitcoinLikeAccount()
.broadcastRawTransaction(signedTransaction)
// optimistic operation
return {
id: txHash,
hash: txHash,
type: 'OUT',
value: amount,
blockHash: null,
blockHeight: null,
senders: [account.freshAddress],
recipients: [transaction.recipient],
accountId: account.id,
date: new Date().toISOString(),
}
}),
)
},
)
export default cmd

9
src/commands/listApps.js

@ -5,11 +5,14 @@ import { fromPromise } from 'rxjs/observable/fromPromise'
import listApps from 'helpers/apps/listApps'
type Input = *
type Input = {
targetId: string | number,
}
type Result = *
const cmd: Command<Input, Result> = createCommand('manager', 'listApps', () =>
fromPromise(listApps()),
const cmd: Command<Input, Result> = createCommand('devices', 'listApps', ({ targetId }) =>
fromPromise(listApps(targetId)),
)
export default cmd

2
src/components/App.js

@ -20,8 +20,6 @@ import Print from 'components/layout/Print'
import CounterValues from 'helpers/countervalues'
import { BridgeSyncProvider } from 'bridge/BridgeSyncContext'
const { DEV_TOOLS } = process.env
const App = ({
store,
history,

2
src/components/FeesField/BitcoinKind.js

@ -53,7 +53,7 @@ class FeesField extends Component<Props & { fees?: Fees, error?: Error }, { item
let items: FeeItem[] = []
if (fees) {
for (const key of Object.keys(fees)) {
const feePerByte = Math.floor(fees[key] / 1000)
const feePerByte = Math.ceil(fees[key] / 1000)
const blockCount = parseInt(key, 10)
if (!isNaN(blockCount) && !isNaN(feePerByte)) {
items.push({ key, blockCount, feePerByte })

14
src/components/ManagerPage/AppsList.js

@ -42,6 +42,7 @@ type LedgerApp = {
type Props = {
device: Device,
targetId: string | number,
t: T,
}
@ -69,10 +70,15 @@ class AppsList extends PureComponent<Props, State> {
_unmounted = false
async fetchAppList() {
const appsList = CACHED_APPS || (await listApps.send().toPromise())
CACHED_APPS = appsList
if (!this._unmounted) {
this.setState({ appsList, status: 'idle' })
try {
const { targetId } = this.props
const appsList = CACHED_APPS || (await listApps.send({ targetId }).toPromise())
CACHED_APPS = appsList
if (!this._unmounted) {
this.setState({ appsList, status: 'idle' })
}
} catch (err) {
this.setState({ status: 'error', error: err.message })
}
}

74
src/components/ManagerPage/index.js

@ -1,13 +1,14 @@
// @flow
import React, { Component, Fragment } from 'react'
import React, { Fragment } from 'react'
import { translate } from 'react-i18next'
import type { Node } from 'react'
import type { T } from 'types/common'
import AppsList from './AppsList'
// import DeviceInfos from './DeviceInfos'
import FirmwareUpdate from './FirmwareUpdate'
// import FirmwareUpdate from './FirmwareUpdate'
import EnsureDevice from './EnsureDevice'
import EnsureDashboard from './EnsureDashboard'
import EnsureGenuine from './EnsureGenuine'
@ -16,44 +17,35 @@ type Props = {
t: T,
}
type State = {}
class ManagerPage extends Component<Props, State> {
render() {
const { t } = this.props
return (
<Fragment>
<EnsureDevice>
{device => (
<EnsureDashboard device={device}>
{deviceInfo => (
<Fragment>
{deviceInfo.mcu && <span>bootloader mode</span>}
{deviceInfo.final && <span>osu mode</span>}
{!deviceInfo.mcu &&
!deviceInfo.final && (
<EnsureGenuine device={device} t={t}>
<FirmwareUpdate
infos={{
targetId: deviceInfo.targetId,
version: deviceInfo.version,
}}
device={device}
t={t}
/>
<AppsList device={device} />
</EnsureGenuine>
)}
</Fragment>
)}
</EnsureDashboard>
const ManagerPage = ({ t }: Props): Node => (
<Fragment>
<EnsureDevice>
{device => (
<EnsureDashboard device={device}>
{deviceInfo => (
<Fragment>
{deviceInfo.mcu && <span>bootloader mode</span>}
{deviceInfo.final && <span>osu mode</span>}
{!deviceInfo.mcu &&
!deviceInfo.final && (
<EnsureGenuine device={device} t={t}>
{/* <FirmwareUpdate
infos={{
targetId: deviceInfo.targetId,
version: deviceInfo.version,
}}
device={device}
t={t}
/> */}
<AppsList device={device} targetId={deviceInfo.targetId} />
</EnsureGenuine>
)}
</Fragment>
)}
</EnsureDevice>
</Fragment>
)
}
}
</EnsureDashboard>
)}
</EnsureDevice>
</Fragment>
)
export default translate()(ManagerPage)

4
src/components/Onboarding/OnboardingBreadcrumb.js

@ -18,7 +18,7 @@ type Props = {
function OnboardingBreadcrumb(props: Props) {
const { onboarding } = props
const { stepName, isGenuineFail } = onboarding
const { stepName, genuine } = onboarding
const filteredSteps = onboarding.steps
.filter(step => !step.external)
@ -29,7 +29,7 @@ function OnboardingBreadcrumb(props: Props) {
return (
<Breadcrumb
stepsErrors={isGenuineFail ? [genuineStepIndex] : undefined}
stepsErrors={genuine.isGenuineFail ? [genuineStepIndex] : undefined}
currentStep={stepIndex}
items={filteredSteps}
/>

5
src/components/Onboarding/OnboardingFooter.js

@ -22,14 +22,15 @@ type Props = {
t: T,
nextStep: () => void,
prevStep: () => void,
isContinueDisabled?: boolean,
}
const OnboardingFooter = ({ t, nextStep, prevStep, ...props }: Props) => (
const OnboardingFooter = ({ t, nextStep, prevStep, isContinueDisabled, ...props }: Props) => (
<Wrapper {...props}>
<Button small outline onClick={() => prevStep()}>
{t('common:back')}
</Button>
<Button small primary onClick={() => nextStep()} ml="auto">
<Button disabled={isContinueDisabled} small primary onClick={() => nextStep()} ml="auto">
{t('common:continue')}
</Button>
</Wrapper>

14
src/components/Onboarding/index.js

@ -10,13 +10,7 @@ import type { T } from 'types/common'
import type { OnboardingState } from 'reducers/onboarding'
import { saveSettings } from 'actions/settings'
import {
nextStep,
prevStep,
jumpStep,
setGenuineCheckFail,
isLedgerNano,
} from 'reducers/onboarding'
import { nextStep, prevStep, jumpStep, updateGenuineCheck, isLedgerNano } from 'reducers/onboarding'
import { getCurrentDevice } from 'reducers/devices'
// import { unlock } from 'reducers/application'
@ -80,9 +74,10 @@ export type StepProps = {
nextStep: Function,
jumpStep: Function,
finish: Function,
saveSettings: Function,
// savePassword: Function,
getDeviceInfo: Function,
setGenuineCheckFail: Function,
updateGenuineCheck: Function,
isLedgerNano: Function,
}
@ -116,7 +111,7 @@ class Onboarding extends PureComponent<Props> {
const stepProps: StepProps = {
t,
onboarding,
setGenuineCheckFail,
updateGenuineCheck,
isLedgerNano,
prevStep,
nextStep,
@ -124,6 +119,7 @@ class Onboarding extends PureComponent<Props> {
finish: this.finish,
// savePassword: this.savePassword,
getDeviceInfo: this.getDeviceInfo,
saveSettings,
}
return (

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

@ -1,7 +1,9 @@
// @flow
import React from 'react'
import React, { PureComponent } from 'react'
import styled from 'styled-components'
import { connect } from 'react-redux'
import { saveSettings } from 'actions/settings'
import Box from 'components/base/Box'
import CheckBox from 'components/base/CheckBox'
@ -10,51 +12,78 @@ import OnboardingFooter from '../OnboardingFooter'
import type { StepProps } from '..'
export default (props: StepProps) => {
const { nextStep, prevStep, t } = props
return (
<Box sticky pt={150}>
<Box grow alignItems="center">
<Title>{t('onboarding:analytics.title')}</Title>
<Description>{t('onboarding:analytics.desc')}</Description>
const mapDispatchToProps = { saveSettings }
<Box mt={5}>
<Container>
<Box justify="center">
<Box horizontal>
<AnalyticsTitle>{t('onboarding:analytics.shareDiagnostics.title')}</AnalyticsTitle>
type State = {
analyticsToggle: boolean,
termsConditionsToggle: boolean,
}
class Analytics extends PureComponent<StepProps, State> {
state = {
analyticsToggle: false,
termsConditionsToggle: false,
}
handleAnalyticsToggle = (isChecked: boolean) => {
this.setState({ analyticsToggle: !this.state.analyticsToggle })
this.props.saveSettings({
shareAnalytics: isChecked,
})
}
handleTermsToggle = () => {
this.setState({ termsConditionsToggle: !this.state.termsConditionsToggle })
}
render() {
const { nextStep, prevStep, t } = this.props
const { analyticsToggle, termsConditionsToggle } = this.state
return (
<Box sticky pt={150}>
<Box grow alignItems="center">
<Title>{t('onboarding:analytics.title')}</Title>
<Description>{t('onboarding:analytics.desc')}</Description>
<Box mt={5}>
<Container>
<Box justify="center" style={{ width: 450 }}>
<Box horizontal>
<AnalyticsTitle>{t('onboarding:analytics.shareAnalytics.title')}</AnalyticsTitle>
</Box>
<AnalyticsText>{t('onboarding:analytics.shareAnalytics.desc')}</AnalyticsText>
</Box>
<AnalyticsText>{t('onboarding:analytics.shareDiagnostics.desc')}</AnalyticsText>
</Box>
<Box alignItems="center" horizontal mx={5}>
<CheckBox isChecked={false} />
</Box>
</Container>
<Container>
<Box justify="center">
<Box horizontal>
<AnalyticsTitle>{t('onboarding:analytics.shareDiagnostics.title')}</AnalyticsTitle>
<Box alignItems="center" horizontal mx={5}>
<CheckBox isChecked={analyticsToggle} onChange={this.handleAnalyticsToggle} />
</Box>
<AnalyticsText>{t('onboarding:analytics.shareDiagnostics.desc')}</AnalyticsText>
</Box>
<Box alignItems="center" horizontal mx={5}>
<CheckBox isChecked={false} />
</Box>
</Container>
</Container>
<Container>
<Box justify="center" style={{ width: 450 }}>
<Box horizontal>
<AnalyticsTitle>{t('onboarding:analytics.termsConditions.title')}</AnalyticsTitle>
</Box>
<AnalyticsText>{t('onboarding:analytics.termsConditions.desc')}</AnalyticsText>
</Box>
<Box alignItems="center" horizontal mx={5}>
<CheckBox isChecked={termsConditionsToggle} onChange={this.handleTermsToggle} />
</Box>
</Container>
</Box>
</Box>
<OnboardingFooter
horizontal
align="center"
flow={2}
t={t}
nextStep={nextStep}
prevStep={prevStep}
isContinueDisabled={!termsConditionsToggle}
/>
</Box>
<OnboardingFooter
horizontal
align="center"
flow={2}
t={t}
nextStep={nextStep}
prevStep={prevStep}
/>
</Box>
)
)
}
}
export default connect(null, mapDispatchToProps)(Analytics)
export const AnalyticsText = styled(Box).attrs({
ff: 'Open Sans|Regular',
fontSize: 3,

72
src/components/Onboarding/steps/GenuineCheck.js

@ -1,13 +1,14 @@
// @flow
import React, { PureComponent, Fragment } from 'react'
import { shell } from 'electron'
import { connect } from 'react-redux'
import styled from 'styled-components'
import { radii } from 'styles/theme'
import type { T } from 'types/common'
import { setGenuineCheckFail } from 'reducers/onboarding'
import { updateGenuineCheck } from 'reducers/onboarding'
import Box, { Card } from 'components/base/Box'
import Button from 'components/base/Button'
@ -23,25 +24,25 @@ import { Title, Description, IconOptionRow } from '../helperComponents'
import type { StepProps } from '..'
import OnboardingFooter from '../OnboardingFooter'
const mapDispatchToProps = { setGenuineCheckFail }
const mapDispatchToProps = { updateGenuineCheck }
type State = {
pinStepPass: boolean | null,
phraseStepPass: boolean | null,
cachedPinStepButton: string,
cachedPhraseStepButton: string,
cachedRecoveryStepButton: string,
isGenuineCheckModalOpened: boolean,
isDeviceGenuine: boolean,
}
const INITIAL_STATE = {
cachedPinStepButton: '',
cachedRecoveryStepButton: '',
isGenuineCheckModalOpened: false,
}
class GenuineCheck extends PureComponent<StepProps, State> {
state = {
pinStepPass: null,
phraseStepPass: null,
cachedPinStepButton: '',
cachedPhraseStepButton: '',
isGenuineCheckModalOpened: false,
isDeviceGenuine: false,
...INITIAL_STATE,
cachedPinStepButton: this.props.onboarding.genuine.pinStepPass ? 'yes' : '',
cachedRecoveryStepButton: this.props.onboarding.genuine.recoveryStepPass ? 'yes' : '',
}
getButtonLabel() {
@ -61,15 +62,21 @@ class GenuineCheck extends PureComponent<StepProps, State> {
}
handleButtonPass = (item: Object, step: string) => {
this.setState({ [`${step}`]: item.pass })
this.props.updateGenuineCheck({ [`${step}`]: item.pass })
if (step === 'pinStepPass') {
this.setState({ cachedPinStepButton: item.key })
} else {
this.setState({ cachedPhraseStepButton: item.key })
this.setState({ cachedRecoveryStepButton: item.key })
}
if (!item.pass) {
this.props.setGenuineCheckFail(true)
this.setState(INITIAL_STATE)
this.props.updateGenuineCheck({
isGenuineFail: true,
recoveryStepPass: false,
pinStepPass: false,
isDeviceGenuine: false,
})
}
}
@ -79,15 +86,19 @@ class GenuineCheck extends PureComponent<StepProps, State> {
handleGenuineCheck = async isGenuine => {
await new Promise(r => setTimeout(r, 1e3)) // let's wait a bit before closing modal
this.handleCloseGenuineCheckModal()
this.setState({ isDeviceGenuine: isGenuine })
this.props.updateGenuineCheck({
isDeviceGenuine: isGenuine,
})
}
redoGenuineCheck = () => {
this.props.setGenuineCheckFail(false)
this.props.updateGenuineCheck({ isGenuineFail: false })
}
contactSupport = () => {
console.log('contact support coming later')
const contactSupportUrl =
'https://support.ledgerwallet.com/hc/en-us/requests/new?ticket_form_id=248165'
shell.openExternal(contactSupportUrl)
}
renderGenuineFail = () => (
@ -101,16 +112,10 @@ class GenuineCheck extends PureComponent<StepProps, State> {
render() {
const { nextStep, prevStep, t, onboarding } = this.props
const {
pinStepPass,
phraseStepPass,
cachedPinStepButton,
cachedPhraseStepButton,
isGenuineCheckModalOpened,
isDeviceGenuine,
} = this.state
const { genuine } = onboarding
const { cachedPinStepButton, cachedRecoveryStepButton, isGenuineCheckModalOpened } = this.state
if (onboarding.isGenuineFail) {
if (genuine.isGenuineFail) {
return this.renderGenuineFail()
}
@ -137,7 +142,7 @@ class GenuineCheck extends PureComponent<StepProps, State> {
</CardWrapper>
</Box>
<Box mt={5}>
<CardWrapper isDisabled={!pinStepPass}>
<CardWrapper isDisabled={!genuine.pinStepPass}>
<Box justify="center">
<Box horizontal>
<IconOptionRow>2.</IconOptionRow>
@ -148,13 +153,13 @@ class GenuineCheck extends PureComponent<StepProps, State> {
<RadioGroup
style={{ margin: '0 30px' }}
items={this.getButtonLabel()}
activeKey={cachedPhraseStepButton}
onChange={item => this.handleButtonPass(item, 'phraseStepPass')}
activeKey={cachedRecoveryStepButton}
onChange={item => this.handleButtonPass(item, 'recoveryStepPass')}
/>
</CardWrapper>
</Box>
<Box mt={5}>
<CardWrapper isDisabled={!phraseStepPass}>
<CardWrapper isDisabled={!genuine.recoveryStepPass}>
<Box justify="center">
<Box horizontal>
<IconOptionRow>3.</IconOptionRow>
@ -166,10 +171,10 @@ class GenuineCheck extends PureComponent<StepProps, State> {
<Button
big
primary
disabled={!phraseStepPass}
disabled={!genuine.recoveryStepPass}
onClick={this.handleOpenGenuineCheckModal}
>
{isDeviceGenuine ? (
{genuine.isDeviceGenuine ? (
<Box horizontal align="center" flow={1}>
<IconCheck size={16} />
<span>{t('onboarding:genuineCheck.buttons.tryAgain')}</span>
@ -189,6 +194,7 @@ class GenuineCheck extends PureComponent<StepProps, State> {
t={t}
nextStep={nextStep}
prevStep={prevStep}
isContinueDisabled={!genuine.isDeviceGenuine}
/>
<GenuineCheckModal
isOpened={isGenuineCheckModalOpened}

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

@ -2,10 +2,15 @@
import React from 'react'
import { shell } from 'electron'
import { colors } from 'styles/theme'
import styled from 'styled-components'
import Box, { Card } from 'components/base/Box'
import IconUser from 'icons/User'
import IconAdd from 'icons/Plus'
import IconRecover from 'icons/Recover'
import IconCheck from 'icons/Check'
import IconExternalLink from 'icons/ExternalLink'
import IconChevronRight from 'icons/ChevronRight'
import { Title } from '../helperComponents'
@ -16,28 +21,28 @@ export default (props: StepProps) => {
const optionCards = [
{
key: 'newDevice',
icon: <IconUser size={22} />,
icon: <IconAdd size={16} />,
title: t('onboarding:init.newDevice.title'),
desc: t('onboarding:init.newDevice.desc'),
onClick: () => nextStep(),
},
{
key: 'restoreDevice',
icon: <IconUser size={22} />,
icon: <IconRecover size={16} />,
title: t('onboarding:init.restoreDevice.title'),
desc: t('onboarding:init.restoreDevice.desc'),
onClick: () => jumpStep('choosePIN'),
},
{
key: 'initializedDevice',
icon: <IconUser size={22} />,
icon: <IconCheck size={16} />,
title: t('onboarding:init.initializedDevice.title'),
desc: t('onboarding:init.initializedDevice.desc'),
onClick: () => jumpStep('choosePIN'),
},
{
key: 'noDevice',
icon: <IconUser size={22} />,
icon: <IconExternalLink size={16} />,
title: t('onboarding:init.noDevice.title'),
desc: t('onboarding:init.noDevice.desc'),
onClick: () => shell.openExternal('https://www.ledger.fr/'),
@ -82,7 +87,7 @@ export function OptionFlowCard({ card }: { card: CardType }) {
}}
onClick={onClick}
>
<Box justify="center" color="grey" style={{ width: 50 }}>
<Box justify="center" style={{ width: 50, color: colors.wallet }}>
{icon}
</Box>
<Box ff="Open Sans|Regular" justify="center" fontSize={4} grow>

2
src/components/RecipientAddress/index.js

@ -45,7 +45,7 @@ const BackgroundLayer = styled(Box)`
type Props = {
value: string,
// return false if it can't be changed (invalid info)
onChange: (string, { amount?: number, currency?: CryptoCurrency }) => ?boolean,
onChange: (string, ?{ amount?: number, currency?: CryptoCurrency }) => ?boolean,
withQrCode: boolean,
}

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

@ -51,7 +51,7 @@ class FlipTicker extends PureComponent<Props, State> {
const { height } = this.state
return (
<Container innerRef={n => (this._node = n)} {...p}>
{value.split('').map((l, i) => (
{[...value].map((l, i) => (
<Box key={i}>
{!/[0-9]/.test(l) ? (
l === ' ' ? (

6
src/components/modals/Send/01-step-amount.js

@ -17,6 +17,7 @@ const AccountField = ({ onChange, value, t }: *) => (
</Box>
)
// TODO we should use isRecipientValid & provide a feedback to user
const RecipientField = ({ bridge, account, transaction, onChangeTransaction, t }: *) => (
<Box flow={1}>
<Label>
@ -26,9 +27,8 @@ const RecipientField = ({ bridge, account, transaction, onChangeTransaction, t }
<RecipientAddress
withQrCode
value={bridge.getTransactionRecipient(account, transaction)}
onChange={(recipient, { amount, currency }) => {
console.log(recipient, amount, currency, account.currency)
// TODO we should use isRecipientValid & provide a feedback to user
onChange={(recipient, maybeExtra) => {
const { amount, currency } = maybeExtra || {}
if (currency && currency.scheme !== account.currency.scheme) return false
let t = transaction
if (amount) {

13
src/helpers/apps/listApps.js

@ -1,10 +1,19 @@
// @flow
import axios from 'axios'
export default async () => {
const { API_BASE_URL } = process.env
export default async (targetId: string | number) => {
try {
const { data: deviceData } = await axios.get(
`${API_BASE_URL}/device_versions_target_id/${targetId}`,
)
const { data } = await axios.get('https://api.ledgerwallet.com/update/applications')
if (deviceData.name in data) {
return data[deviceData.name]
}
return data['nanos-1.4']
} catch (err) {
const error = Error(err.message)

1
src/helpers/ipc.js

@ -41,6 +41,7 @@ export class Command<In, A> {
})
},
error: error => {
console.log('exec error:', error)
send({
type: `ERROR_${requestId}`,
data: {

6
src/internals/accounts/scanAccountsOnDevice.js → src/helpers/libcore.js

@ -18,10 +18,10 @@ import type { NJSAccount, NJSOperation } from '@ledgerhq/ledger-core/src/ledgerc
type Props = {
devicePath: string,
currencyId: string,
onAccountScanned: Function,
onAccountScanned: AccountRaw => void,
}
export default function scanAccountsOnDevice(props: Props): Promise<AccountRaw[]> {
export function scanAccountsOnDevice(props: Props): Promise<AccountRaw[]> {
const { devicePath, currencyId, onAccountScanned } = props
return withDevice(devicePath)(async transport => {
@ -269,7 +269,7 @@ async function buildAccountRaw({
freshAddressPath,
balance,
blockHeight,
archived: false,
archived: true,
index: accountIndex,
operations,
pendingOperations: [],

16
src/icons/Recover.js

@ -0,0 +1,16 @@
// @flow
import React from 'react'
const path = (
<path
fill="currentColor"
d="M15.65 7.985c.008 4.27-3.475 7.762-7.745 7.765a7.722 7.722 0 0 1-5.2-1.998.375.375 0 0 1-.013-.544l.53-.53a.376.376 0 0 1 .517-.012A6.228 6.228 0 0 0 7.9 14.25 6.247 6.247 0 0 0 14.15 8 6.247 6.247 0 0 0 7.9 1.75a6.23 6.23 0 0 0-4.434 1.845L5 5.108a.375.375 0 0 1-.264.642H.725a.375.375 0 0 1-.375-.375V1.42c0-.333.402-.5.638-.267l1.41 1.39A7.75 7.75 0 0 1 15.65 7.985zm-5.22 2.818l.44-.606a.375.375 0 0 0-.082-.524L8.65 8.118V3.625a.375.375 0 0 0-.375-.375h-.75a.375.375 0 0 0-.375.375v5.257l2.755 2.004a.375.375 0 0 0 .524-.083z"
/>
)
export default ({ size, ...p }: { size: number }) => (
<svg viewBox="0 0 16 16" height={size} width={size} {...p}>
{path}
</svg>
)

48
src/internals/accounts/index.js

@ -1,48 +0,0 @@
// @flow
import type { IPCSend } from 'types/electron'
import scanAccountsOnDevice from './scanAccountsOnDevice'
import signAndBroadcastTransactionBTCLike from './signAndBroadcastTransaction/btc'
import sync from './sync'
export default {
sync,
signAndBroadcastTransactionBTCLike,
scan: async (
send: IPCSend,
{
devicePath,
currencyId,
}: {
devicePath: string,
currencyId: string,
},
) => {
try {
send('accounts.scanAccountsOnDevice.start', { pid: process.pid }, { kill: false })
const accounts = await scanAccountsOnDevice({
devicePath,
currencyId,
onAccountScanned: account => {
send('accounts.scanAccountsOnDevice.accountScanned', account, { kill: false })
},
})
send('accounts.scanAccountsOnDevice.success', accounts)
} catch (err) {
send('accounts.scanAccountsOnDevice.fail', formatErr(err))
}
},
}
// TODO: move this to a helper
function formatErr(err) {
if (err instanceof Error) {
return err.message || err.code
}
if (typeof err === 'string') {
return err
}
return 'unknown error'
}

71
src/internals/accounts/signAndBroadcastTransaction/btc.js

@ -1,71 +0,0 @@
// @flow
import Btc from '@ledgerhq/hw-app-btc'
import { withDevice } from 'helpers/deviceAccess'
import type { AccountRaw } from '@ledgerhq/live-common/lib/types'
import type { IPCSend } from 'types/electron'
import { getWalletIdentifier } from '../scanAccountsOnDevice'
type BitcoinLikeTransaction = {
amount: number,
feePerByte: number,
recipient: string,
}
export default async function signAndBroadcastTransactionBTCLike(
send: IPCSend,
{
account,
transaction,
deviceId, // which is in fact `devicePath`
}: {
account: AccountRaw,
transaction: BitcoinLikeTransaction,
deviceId: string,
},
) {
try {
// TODO: investigate why importing it on file scope causes trouble
const core = require('init-ledger-core')()
const txHash = await withDevice(deviceId)(async transport => {
const hwApp = new Btc(transport)
const WALLET_IDENTIFIER = await getWalletIdentifier({
hwApp,
isSegwit: !!account.isSegwit,
currencyId: account.currencyId,
devicePath: deviceId,
})
const njsWallet = await core.getWallet(WALLET_IDENTIFIER)
const njsAccount = await njsWallet.getAccount(account.index)
const bitcoinLikeAccount = njsAccount.asBitcoinLikeAccount()
const njsWalletCurrency = njsWallet.getCurrency()
const amount = core.createAmount(njsWalletCurrency, transaction.amount)
const fees = core.createAmount(njsWalletCurrency, transaction.feePerByte)
const transactionBuilder = bitcoinLikeAccount.buildTransaction()
// TODO: check if is valid address. if not, it will fail silently on invalid
transactionBuilder.sendToAddress(amount, transaction.recipient)
// TODO: don't use hardcoded value for sequence (and first also maybe)
transactionBuilder.pickInputs(0, 0xffffff)
transactionBuilder.setFeesPerByte(fees)
const builded = await transactionBuilder.build()
const signedTransaction = await core.signTransaction(hwApp, builded)
const txHash = await njsAccount
.asBitcoinLikeAccount()
.broadcastRawTransaction(signedTransaction)
return txHash
})
send('accounts.signAndBroadcastTransactionBTCLike.success', txHash)
} catch (err) {
send('accounts.signAndBroadcastTransactionBTCLike.fail', err)
}
}

7
src/internals/accounts/sync.js

@ -1,7 +0,0 @@
// @flow
import type { IPCSend } from 'types/electron'
export default (send: IPCSend) => {
setTimeout(() => send('accounts.sync.success'), 5e3)
}

6
src/internals/devices/index.js

@ -1,6 +1,8 @@
// @flow
import type { Command } from 'helpers/ipc'
import libcoreScanAccounts from 'commands/libcoreScanAccounts'
import libcoreSignAndBroadcast from 'commands/libcoreSignAndBroadcast'
import getAddress from 'commands/getAddress'
import signTransaction from 'commands/signTransaction'
import getDeviceInfo from 'commands/getDeviceInfo'
@ -13,6 +15,7 @@ import uninstallApp from 'commands/uninstallApp'
import installOsuFirmware from 'commands/installOsuFirmware'
import installFinalFirmware from 'commands/installFinalFirmware'
import installMcu from 'commands/installMcu'
import listApps from 'commands/listApps'
export const commands: Array<Command<any, any>> = [
getAddress,
@ -22,9 +25,12 @@ export const commands: Array<Command<any, any>> = [
getIsGenuine,
getLatestFirmwareForDevice,
installApp,
libcoreScanAccounts,
libcoreSignAndBroadcast,
listenDevices,
uninstallApp,
installOsuFirmware,
installFinalFirmware,
installMcu,
listApps,
]

3
src/internals/manager/index.js

@ -1,7 +1,6 @@
// @flow
import type { Command } from 'helpers/ipc'
import listApps from 'commands/listApps'
import getMemInfo from 'commands/getMemInfo'
/**
@ -20,4 +19,4 @@ import getMemInfo from 'commands/getMemInfo'
*
*/
export const commands: Array<Command<any, any>> = [listApps, getMemInfo]
export const commands: Array<Command<any, any>> = [getMemInfo]

27
src/reducers/onboarding.js

@ -17,14 +17,24 @@ export type OnboardingState = {
stepIndex: number,
stepName: string, // TODO: specify that the string comes from Steps type
steps: Step[],
isGenuineFail: boolean,
genuine: {
pinStepPass: boolean,
recoveryStepPass: boolean,
isGenuineFail: boolean,
isDeviceGenuine: boolean,
},
isLedgerNano: boolean,
}
const state: OnboardingState = {
stepIndex: 0,
stepName: 'start',
isGenuineFail: false,
stepName: process.env.SKIP_ONBOARDING ? 'finish' : 'start',
genuine: {
pinStepPass: false,
recoveryStepPass: false,
isGenuineFail: false,
isDeviceGenuine: false,
},
isLedgerNano: true,
steps: [
{
@ -143,10 +153,15 @@ const handlers = {
const index = state.steps.indexOf(step)
return { ...state, stepName: step.name, stepIndex: index }
},
ONBOARDING_SET_GENUINE_CHECK_FAIL: (state, { payload: isGenuineFail }) => ({
UPDATE_GENUINE_CHECK: (state, { payload: obj }) => ({
...state,
isGenuineFail,
genuine: {
...state.genuine,
...obj,
},
}),
ONBOARDING_SET_DEVICE_TYPE: (state, { payload: isLedgerNano }) => ({
...state,
isLedgerNano,
@ -158,5 +173,5 @@ export default handleActions(handlers, state)
export const nextStep = createAction('ONBOARDING_NEXT_STEP')
export const prevStep = createAction('ONBOARDING_PREV_STEP')
export const jumpStep = createAction('ONBOARDING_JUMP_STEP')
export const setGenuineCheckFail = createAction('ONBOARDING_SET_GENUINE_CHECK_FAIL')
export const updateGenuineCheck = createAction('UPDATE_GENUINE_CHECK')
export const isLedgerNano = createAction('ONBOARDING_SET_DEVICE_TYPE')

2
src/reducers/settings.js

@ -30,6 +30,7 @@ export type SettingsState = {
},
region: string,
developerMode: boolean,
shareAnalytics: boolean,
}
/* have to check if available for all OS */
@ -69,6 +70,7 @@ const state: SettingsState = {
region,
developerMode: false,
loaded: false,
shareAnalytics: false,
}
function asCryptoCurrency(c: Currency): ?CryptoCurrency {

8
static/i18n/en/onboarding.yml

@ -82,12 +82,12 @@ setPassword:
analytics:
title: Help Ledger to improve its products and services
desc: This is a long text, please replace it with the final wording once it’s done.
Lorem ipsum dolor amet ledger lorem dolor ipsum amet
shareDiagnostics:
title: Share analytics
desc: Help Ledger improve its products and services by automatically sending diagnostics and usage data.
shareData:
shareAnalytics:
title: Share analytics
desc: Help Ledger improve its products and services by automatically sending diagnostics and usage data.
termsConditions:
title: Terms and Conditions
desc: Please accept terms and conditions to proceed
finish:
title: This is the title of the screen. 1 line is the maximum
desc: This is a long text, please replace it with the final wording once it’s done.
Lorem ipsum dolor amet ledger lorem dolor ipsum amet

12
yarn.lock

@ -1451,9 +1451,9 @@
dependencies:
events "^2.0.0"
"@ledgerhq/ledger-core@^1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@ledgerhq/ledger-core/-/ledger-core-1.2.1.tgz#8176a4fa9182d8e0fe4456cbdc4701e4e6e25145"
"@ledgerhq/ledger-core@^1.3.0":
version "1.3.0"
resolved "https://registry.yarnpkg.com/@ledgerhq/ledger-core/-/ledger-core-1.3.0.tgz#2b26d43c4a8973e00e0c6671a2da5183ef0ce229"
dependencies:
"@ledgerhq/hw-app-btc" "^4.7.3"
"@ledgerhq/hw-transport-node-hid" "^4.7.6"
@ -1464,9 +1464,9 @@
npm "^5.7.1"
prebuild-install "^2.2.2"
"@ledgerhq/live-common@^2.9.1":
version "2.9.2"
resolved "https://registry.yarnpkg.com/@ledgerhq/live-common/-/live-common-2.9.2.tgz#e77139368c690fc542cb95cdb87ea37890652377"
"@ledgerhq/live-common@2.11.0":
version "2.11.0"
resolved "https://registry.yarnpkg.com/@ledgerhq/live-common/-/live-common-2.11.0.tgz#9b8af8b76aab4094ec2189538f8330711a3b91a3"
dependencies:
axios "^0.18.0"
invariant "^2.2.2"

Loading…
Cancel
Save