Browse Source

Merge branch 'master' of github.com:ledgerhq/ledger-live-desktop into pixel-push

master
Gaëtan Renaudeau 7 years ago
parent
commit
781c113e13
  1. 11
      src/commands/listApps.js
  2. 2
      src/components/AccountPage/index.js
  3. 1
      src/components/DashboardPage/index.js
  4. 19
      src/components/MainSideBar.js
  5. 6
      src/components/ManagerPage/AppsList.js
  6. 24
      src/components/OperationsList/index.js
  7. 6
      src/components/OperationsList/stories.js
  8. 30
      src/components/PillsDaysCount.js
  9. 31
      src/components/modals/AccountSettingRenderBody.js
  10. 4
      src/components/modals/ImportAccounts/AccountRow.js
  11. 12
      src/components/modals/ImportAccounts/steps/01-step-choose-currency.js
  12. 209
      src/components/modals/ImportAccounts/steps/03-step-import.js
  13. 16
      src/helpers/apps/listApps.js
  14. 1
      src/helpers/constants.js
  15. 3
      src/helpers/derivations.js
  16. 2
      src/helpers/devices/getFirmwareInfo.js
  17. 3
      src/helpers/devices/getLatestFirmwareForDevice.js
  18. 1
      src/renderer/i18n/instanciate.js
  19. 12
      static/i18n/en/importAccounts.yml
  20. 4
      static/i18n/en/settings.yml

11
src/commands/listApps.js

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

2
src/components/AccountPage/index.js

@ -180,7 +180,7 @@ class AccountPage extends PureComponent<Props, State> {
)}
/>
</Box>
<OperationsList canShowMore account={account} title={t('account:lastOperations')} />
<OperationsList account={account} title={t('account:lastOperations')} />
</Fragment>
) : (
<EmptyStateAccount account={account} />

1
src/components/DashboardPage/index.js

@ -168,7 +168,6 @@ class DashboardPage extends PureComponent<Props, State> {
</Box>
{displayOperations && (
<OperationsList
canShowMore
onAccountClick={this.onAccountClick}
accounts={accounts}
title={t('dashboard:recentActivity')}

19
src/components/MainSideBar.js

@ -17,10 +17,13 @@ import type { UpdateStatus } from 'reducers/update'
import { MODAL_RECEIVE, MODAL_SEND } from 'config/constants'
import { rgba } from 'styles/helpers'
import { accountsSelector } from 'reducers/accounts'
import { openModal } from 'reducers/modals'
import { getUpdateStatus } from 'reducers/update'
import Tooltip from 'components/base/Tooltip'
import { SideBarList } from 'components/base/SideBar'
import Box, { Tabbable } from 'components/base/Box'
import Space from 'components/base/Space'
@ -140,9 +143,11 @@ class MainSideBar extends PureComponent<Props> {
scroll
title={t('sidebar:accounts')}
titleRight={
<PlusWrapper onClick={() => openModal('importAccounts')}>
<IconCirclePlus size={16} />
</PlusWrapper>
<Tooltip render={() => t('importAccounts:title')}>
<PlusWrapper onClick={() => openModal('importAccounts')}>
<IconCirclePlus size={16} />
</PlusWrapper>
</Tooltip>
}
items={accountsItems}
emptyText={t('emptyState:sidebar.text')}
@ -157,15 +162,15 @@ const PlusWrapper = styled(Tabbable).attrs({
cursor: 'pointer',
borderRadius: 1,
})`
opacity: 0.4;
color: ${p => p.theme.colors.smoke};
&:hover {
opacity: 1;
color: ${p => p.theme.colors.dark};
}
border: 1px dashed rgba(0, 0, 0, 0);
border: 1px solid transparent;
&:focus {
border: 1px dashed rgba(0, 0, 0, 0.2);
outline: none;
border-color: ${p => rgba(p.theme.colors.wallet, 0.3)};
}
`

6
src/components/ManagerPage/AppsList.js

@ -46,7 +46,7 @@ type LedgerApp = {
type Props = {
device: Device,
// targetId: string | number,
targetId: string | number,
t: T,
}
@ -75,8 +75,8 @@ class AppsList extends PureComponent<Props, State> {
async fetchAppList() {
try {
// const { targetId } = this.props // TODO: REUSE THIS WHEN SERVER IS UP
const appsList = CACHED_APPS || (await listApps.send().toPromise())
const { targetId } = this.props
const appsList = CACHED_APPS || (await listApps.send({ targetId }).toPromise())
CACHED_APPS = appsList
if (!this._unmounted) {
this.setState({ appsList, status: 'idle' })

24
src/components/OperationsList/index.js

@ -52,8 +52,7 @@ const mapDispatchToProps = {
type Props = {
account: Account,
accounts: Account[],
canShowMore: boolean,
openModal: Function,
openModal: (string, Object) => *,
t: T,
withAccount?: boolean,
title?: string,
@ -66,10 +65,12 @@ type State = {
const initialState = {
nbToShow: 20,
}
const footerPlaceholder = null // TODO figure out with design what we want here
export class OperationsList extends PureComponent<Props, State> {
static defaultProps = {
withAccount: false,
canShowMore: false,
}
state = initialState
@ -86,7 +87,7 @@ export class OperationsList extends PureComponent<Props, State> {
}
render() {
const { account, accounts, canShowMore, t, title, withAccount } = this.props
const { account, accounts, t, title, withAccount } = this.props
const { nbToShow } = this.state
if (!account && !accounts) {
@ -130,13 +131,14 @@ export class OperationsList extends PureComponent<Props, State> {
</Card>
</Box>
))}
{canShowMore &&
!groupedOperations.completed && (
<ShowMore onClick={this.fetchMoreOperations}>
<span>{t('operationsList:showMore')}</span>
<IconAngleDown size={12} />
</ShowMore>
)}
{!groupedOperations.completed ? (
<ShowMore onClick={this.fetchMoreOperations}>
<span>{t('operationsList:showMore')}</span>
<IconAngleDown size={12} />
</ShowMore>
) : (
footerPlaceholder
)}
</Box>
</Defer>
)

6
src/components/OperationsList/stories.js

@ -15,10 +15,6 @@ const account2 = genAccount('account2')
stories.add('OperationsList', () => (
<Box bg="lightGrey" p={6} m={-4}>
<OperationsList
accounts={[account1, account2]}
canShowMore={boolean('canShowMore')}
withAccount={boolean('withAccount')}
/>
<OperationsList accounts={[account1, account2]} withAccount={boolean('withAccount')} />
</Box>
))

30
src/components/PillsDaysCount.js

@ -1,6 +1,6 @@
// @flow
import React from 'react'
import React, { PureComponent } from 'react'
import { translate } from 'react-i18next'
import type { T } from 'types/common'
@ -9,7 +9,7 @@ import Pills from 'components/base/Pills'
type Props = {
selectedTime: string,
onChange: Function,
onChange: ({ key: string, value: *, label: string }) => void,
t: T,
}
@ -19,18 +19,20 @@ const itemsTimes = [
{ key: 'year', value: 365 },
]
function PillsDaysCount(props: Props) {
const { selectedTime, onChange, t } = props
return (
<Pills
items={itemsTimes.map(item => ({
...item,
label: t(`time:${item.key}`),
}))}
activeKey={selectedTime}
onChange={onChange}
/>
)
class PillsDaysCount extends PureComponent<Props> {
render() {
const { selectedTime, onChange, t } = this.props
return (
<Pills
items={itemsTimes.map(item => ({
...item,
label: t(`time:${item.key}`),
}))}
activeKey={selectedTime}
onChange={onChange}
/>
)
}
}
export default translate()(PillsDaysCount)

31
src/components/modals/AccountSettingRenderBody.js

@ -20,12 +20,19 @@ import Box from 'components/base/Box'
import Button from 'components/base/Button'
import Input from 'components/base/Input'
import Select from 'components/base/LegacySelect'
import { ModalBody, ModalTitle, ModalFooter, ModalContent } from 'components/base/Modal'
import {
ModalBody,
ModalTitle,
ModalFooter,
ModalContent,
ConfirmModal,
} from 'components/base/Modal'
type State = {
accountName: string | null,
accountUnit: Unit | null,
accountNameError: boolean,
isRemoveAccountModalOpen: boolean,
}
type Props = {
@ -47,6 +54,7 @@ const defaultState = {
accountName: null,
accountUnit: null,
accountNameError: false,
isRemoveAccountModalOpen: false,
}
class HelperComp extends PureComponent<Props, State> {
@ -106,15 +114,18 @@ class HelperComp extends PureComponent<Props, State> {
handleChangeUnit = (value: Unit) => {
this.setState({ accountUnit: value })
}
handleOpenRemoveAccountModal = () => this.setState({ isRemoveAccountModalOpen: true })
handleCloseRemoveAccountModal = () => this.setState({ isRemoveAccountModalOpen: false })
handleRemoveAccount = (account: Account) => {
const { removeAccount } = this.props
const { removeAccount, onClose } = this.props
removeAccount(account)
this.props.onClose()
this.setState({ isRemoveAccountModalOpen: false })
onClose()
}
render() {
const { accountUnit, accountNameError } = this.state
const { accountUnit, accountNameError, isRemoveAccountModalOpen } = this.state
const { t, onClose, data } = this.props
const account = this.getAccount(data)
@ -183,7 +194,7 @@ class HelperComp extends PureComponent<Props, State> {
</Spoiler>
</ModalContent>
<ModalFooter horizontal>
<Button small danger type="button" onClick={() => this.handleRemoveAccount(account)}>
<Button small danger type="button" onClick={this.handleOpenRemoveAccountModal}>
{t('common:delete')}
</Button>
<Button small ml="auto" type="submit" primary>
@ -191,6 +202,16 @@ class HelperComp extends PureComponent<Props, State> {
</Button>
</ModalFooter>
</form>
<ConfirmModal
isDanger
isOpened={isRemoveAccountModalOpen}
onClose={this.handleCloseRemoveAccountModal}
onReject={this.handleCloseRemoveAccountModal}
onConfirm={() => this.handleRemoveAccount(account)}
title={t('settings:removeAccountModal.title')}
subTitle={t('settings:removeAccountModal.subTitle')}
desc={t('settings:removeAccountModal.desc')}
/>
</ModalBody>
)
}

4
src/components/modals/ImportAccounts/AccountRow.js

@ -17,7 +17,7 @@ import IconCheck from 'icons/Check'
type Props = {
account: Account,
isChecked: boolean,
isDisabled: boolean,
isDisabled?: boolean,
onClick: Account => void,
onAccountUpdate: Account => void,
}
@ -110,7 +110,7 @@ export default class AccountRow extends PureComponent<Props, State> {
fontSize={4}
color="grey"
/>
<Radio isChecked={isChecked || isDisabled} />
<Radio isChecked={isChecked || !!isDisabled} />
</AccountRowContainer>
)
}

12
src/components/modals/ImportAccounts/steps/01-step-choose-currency.js

@ -1,6 +1,7 @@
// @flow
import React, { Fragment } from 'react'
import isArray from 'lodash/isArray'
import SelectCurrency from 'components/SelectCurrency'
import Button from 'components/base/Button'
@ -9,7 +10,16 @@ import CurrencyBadge from 'components/base/CurrencyBadge'
import type { StepProps } from '../index'
function StepChooseCurrency({ currency, setState }: StepProps) {
return <SelectCurrency onChange={currency => setState({ currency })} value={currency} />
return (
<SelectCurrency
onChange={currency => {
setState({
currency: isArray(currency) && currency.length === 0 ? null : currency,
})
}}
value={currency}
/>
)
}
export function StepChooseCurrencyFooter({ transitionTo, currency, t }: StepProps) {

209
src/components/modals/ImportAccounts/steps/03-step-import.js

@ -1,7 +1,9 @@
// @flow
import React, { PureComponent } from 'react'
import styled from 'styled-components'
import type { Account } from '@ledgerhq/live-common/lib/types'
import uniq from 'lodash/uniq'
import { getBridgeForCurrency } from 'bridge'
@ -48,10 +50,18 @@ class StepImport extends PureComponent<StepProps> {
this.scanSubscription = bridge.scanAccountsOnDevice(currency, devicePath, {
next: account => {
const { scannedAccounts } = this.props
const { scannedAccounts, checkedAccountsIds, existingAccounts } = this.props
const hasAlreadyBeenScanned = !!scannedAccounts.find(a => account.id === a.id)
const hasAlreadyBeenImported = !!existingAccounts.find(a => account.id === a.id)
const isNewAccount = account.operations.length === 0
if (!hasAlreadyBeenScanned) {
setState({ scannedAccounts: [...scannedAccounts, account] })
setState({
scannedAccounts: [...scannedAccounts, account],
checkedAccountsIds:
!hasAlreadyBeenImported && !isNewAccount
? uniq([...checkedAccountsIds, account.id])
: checkedAccountsIds,
})
}
},
complete: () => setState({ scanStatus: 'finished' }),
@ -103,63 +113,125 @@ class StepImport extends PureComponent<StepProps> {
})
}
handleToggleSelectAll = () => {
handleSelectAll = () => {
const { scannedAccounts, setState } = this.props
setState({ checkedAccountsIds: scannedAccounts.map(a => a.id) })
setState({
checkedAccountsIds: scannedAccounts.filter(a => a.operations.length > 0).map(a => a.id),
})
}
handleUnselectAll = () => this.props.setState({ checkedAccountsIds: [] })
render() {
const { scanStatus, err, scannedAccounts, checkedAccountsIds, existingAccounts } = this.props
const { scanStatus, err, scannedAccounts, checkedAccountsIds, existingAccounts, t } = this.props
const importableAccounts = scannedAccounts.filter(acc => {
if (acc.operations.length <= 0) {
return false
}
return existingAccounts.find(a => a.id === acc.id) === undefined
})
const creatableAccounts = scannedAccounts.filter(acc => {
if (acc.operations.length > 0) {
return false
}
return existingAccounts.find(a => a.id === acc.id) === undefined
})
const isAllSelected = scannedAccounts.filter(acc => acc.operations.length > 0).every(acc => {
const isChecked = !!checkedAccountsIds.find(id => acc.id === id)
const isImported = !!existingAccounts.find(a => acc.id === a.id)
return isChecked || isImported
})
return (
<Box>
{err && <Box shrink>{err.message}</Box>}
{!!scannedAccounts.length && (
<Box horizontal justify="flex-end" mb={2}>
<FakeLink onClick={this.handleToggleSelectAll} fontSize={3}>
{'Select all'}
</FakeLink>
</Box>
)}
<Box flow={2}>
{scannedAccounts.map(account => {
const isChecked = checkedAccountsIds.find(id => id === account.id) !== undefined
const existingAccount = existingAccounts.find(a => a.id === account.id)
const isDisabled = existingAccount !== undefined
return (
<Box flow={5}>
{(!!importableAccounts.length || scanStatus === 'scanning') && (
<Box>
{!!importableAccounts.length && (
<Box horizontal mb={3} align="center">
<Box
ff="Open Sans|Bold"
color="dark"
fontSize={2}
style={{ textTransform: 'uppercase' }}
>
{t('importAccounts:accountToImportSubtitle', {
count: importableAccounts.length,
})}
</Box>
<FakeLink
ml="auto"
onClick={isAllSelected ? this.handleUnselectAll : this.handleSelectAll}
fontSize={3}
>
{isAllSelected
? t('importAccounts:unselectAll')
: t('importAccounts:selectAll')}
</FakeLink>
</Box>
)}
<Box flow={2}>
{importableAccounts.map(account => {
const isChecked = checkedAccountsIds.find(id => id === account.id) !== undefined
const existingAccount = existingAccounts.find(a => a.id === account.id)
const isDisabled = existingAccount !== undefined
return (
<AccountRow
key={account.id}
account={existingAccount || account}
isChecked={isChecked}
isDisabled={isDisabled}
onClick={this.handleToggleAccount}
onAccountUpdate={this.handleAccountUpdate}
/>
)
})}
{scanStatus === 'scanning' && (
<LoadingRow>
<Spinner color="grey" size={16} />
</LoadingRow>
)}
</Box>
</Box>
)}
{creatableAccounts.length > 0 && (
<Box>
<Box horizontal mb={3} align="center">
<Box
ff="Open Sans|Bold"
color="dark"
fontSize={2}
style={{ textTransform: 'uppercase' }}
>
{t('importAccounts:createNewAccount')}
</Box>
</Box>
<AccountRow
key={account.id}
account={existingAccount || account}
isChecked={isChecked}
isDisabled={isDisabled}
account={creatableAccounts[0]}
isChecked={
checkedAccountsIds.find(id => id === creatableAccounts[0].id) !== undefined
}
onClick={this.handleToggleAccount}
onAccountUpdate={this.handleAccountUpdate}
/>
)
})}
{scanStatus === 'scanning' && (
<Box
horizontal
bg="lightGrey"
borderRadius={3}
px={3}
align="center"
justify="center"
style={{ height: 48 }}
>
<Spinner color="grey" size={24} />
</Box>
)}
</Box>
<Box horizontal mt={2}>
{['error', 'finished'].includes(scanStatus) && (
{['error'].includes(scanStatus) && (
<Button small outline onClick={this.handleRetry}>
<Box horizontal flow={2} align="center">
<IconExchange size={13} />
<span>{'retry sync'}</span>
<span>{t('importAccounts:retrySync')}</span>
</Box>
</Button>
)}
@ -171,12 +243,55 @@ class StepImport extends PureComponent<StepProps> {
export default StepImport
export const StepImportFooter = ({ scanStatus, onClickImport, checkedAccountsIds }: StepProps) => (
<Button
primary
disabled={scanStatus !== 'finished' || checkedAccountsIds.length === 0}
onClick={() => onClickImport()}
>
{'Import accounts'}
</Button>
)
export const LoadingRow = styled(Box).attrs({
horizontal: true,
borderRadius: 1,
px: 3,
align: 'center',
justify: 'center',
})`
height: 48px;
border: 1px dashed ${p => p.theme.colors.fog};
`
export const StepImportFooter = ({
scanStatus,
onClickImport,
checkedAccountsIds,
scannedAccounts,
t,
}: StepProps) => {
const willCreateAccount = checkedAccountsIds.some(id => {
const account = scannedAccounts.find(a => a.id === id)
return account && account.operations.length === 0
})
const willImportAccounts = checkedAccountsIds.some(id => {
const account = scannedAccounts.find(a => a.id === id)
return account && account.operations.length > 0
})
const importedAccountsCount = checkedAccountsIds.filter(id => {
const account = scannedAccounts.find(acc => acc.id === id)
return account && account.operations.length > 0
}).length
const ctaWording =
willCreateAccount && willImportAccounts
? `${t('importAccounts:cta.create')} / ${t('importAccounts:cta.import', {
count: importedAccountsCount,
})}`
: willCreateAccount
? t('importAccounts:cta.create')
: t('importAccounts:cta.import', { count: importedAccountsCount })
return (
<Button
primary
disabled={scanStatus !== 'finished' || checkedAccountsIds.length === 0}
onClick={() => onClickImport()}
>
{ctaWording}
</Button>
)
}

16
src/helpers/apps/listApps.js

@ -1,18 +1,18 @@
// @flow
import axios from 'axios'
// const { API_BASE_URL } = process.env
import { API_BASE_URL } from 'helpers/constants'
export default async (/* targetId: string | number */) => {
export default async (targetId: string | number) => {
try {
// const { data: deviceData } = await axios.get(
// `${API_BASE_URL}/device_versions_target_id/${targetId}`,
// )
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]
// }
if (deviceData.name in data) {
return data[deviceData.name]
}
return data['nanos-1.4']
} catch (err) {

1
src/helpers/constants.js

@ -2,6 +2,7 @@
export const BASE_SOCKET_URL = 'ws://api.ledgerwallet.com'
export const BASE_SOCKET_URL_TEMP = 'ws://manager.ledger.fr:3500'
export const API_BASE_URL = process.env.API_BASE_URL || 'https://beta.manager.live.ledger.fr/api'
// If you want to test locally with https://github.com/LedgerHQ/ledger-update-python-api
// export const BASE_SOCKET_URL = 'ws://localhost:3001/update'

3
src/helpers/derivations.js

@ -11,9 +11,12 @@ const ethLegacyMEW: Derivation = ({ x }) => `44'/60'/0'/${x}`
const etcLegacyMEW: Derivation = ({ x }) => `44'/60'/160720'/${x}`
const rippleLegacy: Derivation = ({ x }) => `44'/144'/0'/${x}'`
const legacyDerivations = {
ethereum: [ethLegacyMEW],
ethereum_classic: [etcLegacyMEW],
ripple: [rippleLegacy],
}
export const standardDerivation: Derivation = ({ currency, segwit, x }) => {

2
src/helpers/devices/getFirmwareInfo.js

@ -2,7 +2,7 @@
import axios from 'axios'
import isEmpty from 'lodash/isEmpty'
const { API_BASE_URL } = process.env
import { API_BASE_URL } from 'helpers/constants'
type Input = {
version: string,

3
src/helpers/devices/getLatestFirmwareForDevice.js

@ -1,11 +1,10 @@
// @flow
import axios from 'axios'
import isEmpty from 'lodash/isEmpty'
import { API_BASE_URL } from 'helpers/constants'
import getFirmwareInfo from './getFirmwareInfo'
const { API_BASE_URL } = process.env
type Input = {
targetId: string | number,
version: string,

1
src/renderer/i18n/instanciate.js

@ -3,6 +3,7 @@ import i18n from 'i18next'
const commonConfig = {
fallbackLng: 'en',
debug: false,
compatibilityJSON: 'v2',
react: {
wait: process.env.NODE_ENV !== 'test',
},

12
static/i18n/en/importAccounts.yml

@ -1,6 +1,16 @@
title: Import accounts
title: Add accounts
breadcrumb:
informations: Informations
connectDevice: Connect device
import: Import
finish: End
accountToImportSubtitle: Account to import
accountToImportSubtitle_plural: 'Accounts to import ({{count}})'
selectAll: Select all
unselectAll: Unselect all
createNewAccount: Create new account
retrySync: Retry sync
cta:
create: 'Create account'
import: 'Import account'
import_plural: 'Import accounts'

4
static/i18n/en/settings.yml

@ -57,6 +57,10 @@ softResetModal:
title: Clean application cache
subTitle: Are you sure houston?
desc: Lorem ipsum dolor sit amet
removeAccountModal:
title: Delete this account
subTitle: Are you sure houston?
desc: Lorem ipsum dolor sit amet
exportLogs:
title: Export Logs
desc: Export Logs

Loading…
Cancel
Save