Browse Source

Merge pull request #378 from meriadec/design-import-accounts

Redesign import accounts
master
Gaëtan Renaudeau 7 years ago
committed by GitHub
parent
commit
0fb8a10692
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      src/bridge/makeMockBridge.js
  2. 6
      src/components/Breadcrumb/Step.js
  3. 5
      src/components/CryptoCurrencyIcon.js
  4. 7
      src/components/DeviceConnect/index.js
  5. 2
      src/components/SideBar/Item.js
  6. 73
      src/components/SideBar/index.js
  7. 7
      src/components/base/Box/index.js
  8. 56
      src/components/base/CurrencyBadge.js
  9. 10
      src/components/base/FormattedVal/__tests__/__snapshots__/FormattedVal.test.js.snap
  10. 27
      src/components/base/Input/index.js
  11. 4
      src/components/base/Modal/ModalBody.js
  12. 67
      src/components/base/Modal/ModalTitle.js
  13. 12
      src/components/base/Modal/index.js
  14. 9
      src/components/base/Radio/index.js
  15. 30
      src/components/base/Spinner.js
  16. 27
      src/components/modals/AddAccount/01-step-currency.js
  17. 51
      src/components/modals/AddAccount/03-step-import.js
  18. 297
      src/components/modals/AddAccount/index.js
  19. 167
      src/components/modals/ImportAccounts/AccountRow.js
  20. 224
      src/components/modals/ImportAccounts/index.js
  21. 26
      src/components/modals/ImportAccounts/steps/01-step-choose-currency.js
  22. 53
      src/components/modals/ImportAccounts/steps/02-step-connect-device.js
  23. 168
      src/components/modals/ImportAccounts/steps/03-step-import.js
  24. 25
      src/components/modals/ImportAccounts/steps/04-step-finish.js
  25. 2
      src/components/modals/StepConnectDevice.js
  26. 2
      src/components/modals/index.js
  27. 60
      src/helpers/libcore.js
  28. 2
      src/icons/Home.js
  29. 5
      src/icons/Loader.js
  30. 2
      static/i18n/en/common.yml
  31. 6
      static/i18n/en/importAccounts.yml

4
src/bridge/makeMockBridge.js

@ -79,13 +79,13 @@ function makeMockBridge(opts?: Opts): WalletBridge<*> {
async function job() { async function job() {
if (Math.random() > scanAccountDeviceSuccessRate) { if (Math.random() > scanAccountDeviceSuccessRate) {
await delay(5000) await delay(1000)
if (!unsubscribed) error(new Error('scan failed')) if (!unsubscribed) error(new Error('scan failed'))
return return
} }
const nbAccountToGen = 3 const nbAccountToGen = 3
for (let i = 0; i < nbAccountToGen && !unsubscribed; i++) { for (let i = 0; i < nbAccountToGen && !unsubscribed; i++) {
await delay(2000) await delay(500)
const account = genAccount(String(Math.random()), { const account = genAccount(String(Math.random()), {
operationsSize: 0, operationsSize: 0,
currency, currency,

6
src/components/Breadcrumb/Step.js

@ -3,6 +3,8 @@
import React from 'react' import React from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { colors } from 'styles/theme'
import Box from 'components/base/Box' import Box from 'components/base/Box'
import IconCheck from 'icons/Check' import IconCheck from 'icons/Check'
@ -27,13 +29,13 @@ const Wrapper = styled(Box).attrs({
const StepNumber = styled(Box).attrs({ const StepNumber = styled(Box).attrs({
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
color: 'fog', color: p => (['active', 'valid'].includes(p.status) ? 'white' : 'fog'),
bg: p => bg: p =>
['active', 'valid'].includes(p.status) ? 'wallet' : p.status === 'error' ? 'alertRed' : 'white', ['active', 'valid'].includes(p.status) ? 'wallet' : p.status === 'error' ? 'alertRed' : 'white',
ff: 'Rubik|Regular', ff: 'Rubik|Regular',
})` })`
border-radius: 50%; border-radius: 50%;
border: 1px solid #d8d8d8; border: 1px solid ${p => (['active', 'valid'].includes(p.status) ? colors.wallet : colors.fog)};
font-size: 10px; font-size: 10px;
height: ${RADIUS}px; height: ${RADIUS}px;
line-height: 10px; line-height: 10px;

5
src/components/CryptoCurrencyIcon.js

@ -6,13 +6,14 @@ import type { CryptoCurrency } from '@ledgerhq/live-common/lib/types'
type Props = { type Props = {
currency: CryptoCurrency, currency: CryptoCurrency,
size: number, size: number,
color?: string,
} }
class CryptoCurrencyIcon extends PureComponent<Props> { class CryptoCurrencyIcon extends PureComponent<Props> {
render() { render() {
const { currency, size } = this.props const { currency, size, color } = this.props
const IconCurrency = getCryptoCurrencyIcon(currency) const IconCurrency = getCryptoCurrencyIcon(currency)
return IconCurrency ? <IconCurrency size={size} /> : null return IconCurrency ? <IconCurrency size={size} color={color} /> : null
} }
} }

7
src/components/DeviceConnect/index.js

@ -111,7 +111,6 @@ const Info = styled(Box).attrs({
fontSize: 3, fontSize: 3,
horizontal: true, horizontal: true,
ml: 1, ml: 1,
pt: 1,
})` })`
strong { strong {
font-weight: 600; font-weight: 600;
@ -310,10 +309,8 @@ class DeviceConnect extends PureComponent<Props> {
{appState.fail ? ( {appState.fail ? (
<Info hasErrors> <Info hasErrors>
<Box> <IconInfoCircle size={12} />
<IconInfoCircle size={12} /> <Box shrink selectable>
</Box>
<Box style={{ userSelect: 'text' }}>
{accountName ? ( {accountName ? (
<Trans i18nKey="deviceConnect:info" parent="div"> <Trans i18nKey="deviceConnect:info" parent="div">
{'You must use the device associated to the account '} {'You must use the device associated to the account '}

2
src/components/SideBar/Item.js

@ -34,7 +34,7 @@ const Container = styled(Tabbable).attrs({
horizontal: true, horizontal: true,
pl: 3, pl: 3,
})` })`
cursor: pointer; cursor: ${p => (p.isActive ? 'default' : 'pointer')};
color: ${p => p.theme.colors.dark}; color: ${p => p.theme.colors.dark};
background: ${p => (p.isActive ? p.theme.colors.lightGrey : '')}; background: ${p => (p.isActive ? p.theme.colors.lightGrey : '')};
height: ${p => (p.big ? 50 : 36)}px; height: ${p => (p.big ? 50 : 36)}px;

73
src/components/SideBar/index.js

@ -1,6 +1,6 @@
// @flow // @flow
import React, { PureComponent } from 'react' import React, { PureComponent, Fragment } from 'react'
import { compose } from 'redux' import { compose } from 'redux'
import { translate } from 'react-i18next' import { translate } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
@ -8,7 +8,7 @@ import { connect } from 'react-redux'
import { getCryptoCurrencyIcon } from '@ledgerhq/live-common/lib/react' import { getCryptoCurrencyIcon } from '@ledgerhq/live-common/lib/react'
import type { Account } from '@ledgerhq/live-common/lib/types' import type { Account } from '@ledgerhq/live-common/lib/types'
import { MODAL_SEND, MODAL_RECEIVE, MODAL_ADD_ACCOUNT } from 'config/constants' import { MODAL_SEND, MODAL_RECEIVE } from 'config/constants'
import type { T } from 'types/common' import type { T } from 'types/common'
@ -56,7 +56,6 @@ const PlusBtn = styled(Tabbable).attrs({
type Props = { type Props = {
t: T, t: T,
accounts: Account[],
openModal: Function, openModal: Function,
updateStatus: UpdateStatus, updateStatus: UpdateStatus,
} }
@ -72,7 +71,7 @@ const mapDispatchToProps: Object = {
class SideBar extends PureComponent<Props> { class SideBar extends PureComponent<Props> {
render() { render() {
const { t, accounts, openModal, updateStatus } = this.props const { t, openModal, updateStatus } = this.props
return ( return (
<Container bg="white"> <Container bg="white">
@ -80,7 +79,11 @@ class SideBar extends PureComponent<Props> {
<Box flow={4}> <Box flow={4}>
<CapsSubtitle>{t('sidebar:menu')}</CapsSubtitle> <CapsSubtitle>{t('sidebar:menu')}</CapsSubtitle>
<Box px={4} flow={2}> <Box px={4} flow={2}>
<Item icon={<IconPieChart size={16} />} linkTo="/" highlight={updateStatus === 'downloaded'}> <Item
icon={<IconPieChart size={16} />}
linkTo="/"
highlight={updateStatus === 'downloaded'}
>
{t('dashboard:title')} {t('dashboard:title')}
</Item> </Item>
<Item icon={<IconSend size={16} />} modal={MODAL_SEND}> <Item icon={<IconSend size={16} />} modal={MODAL_SEND}>
@ -101,35 +104,13 @@ class SideBar extends PureComponent<Props> {
<CapsSubtitle horizontal alignItems="center"> <CapsSubtitle horizontal alignItems="center">
<Box grow>{t('sidebar:accounts')}</Box> <Box grow>{t('sidebar:accounts')}</Box>
<Tooltip render={() => t('addAccount:title')}> <Tooltip render={() => t('addAccount:title')}>
<PlusBtn onClick={() => openModal(MODAL_ADD_ACCOUNT)}> <PlusBtn onClick={() => openModal('importAccounts')}>
<IconPlus size={16} /> <IconPlus size={16} />
</PlusBtn> </PlusBtn>
</Tooltip> </Tooltip>
</CapsSubtitle> </CapsSubtitle>
<GrowScroll pb={4} px={4} flow={2}> <GrowScroll pb={4} px={4} flow={2}>
{accounts.map(account => { <AccountsList />
const Icon = getCryptoCurrencyIcon(account.currency)
return (
<Item
big
desc={
<FormattedVal
alwaysShowSign={false}
color="graphite"
unit={account.unit}
showCode
val={account.balance || 0}
/>
}
iconActiveColor={account.currency.color}
icon={Icon ? <Icon size={16} /> : null}
key={account.id}
linkTo={`/account/${account.id}`}
>
{account.name}
</Item>
)
})}
</GrowScroll> </GrowScroll>
</Box> </Box>
</Box> </Box>
@ -138,9 +119,37 @@ class SideBar extends PureComponent<Props> {
} }
} }
const AccountsList = connect(state => ({
accounts: getVisibleAccounts(state),
}))(({ accounts }: { accounts: Account[] }) => (
<Fragment>
{accounts.map(account => {
const Icon = getCryptoCurrencyIcon(account.currency)
return (
<Item
big
desc={
<FormattedVal
alwaysShowSign={false}
color="graphite"
unit={account.unit}
showCode
val={account.balance || 0}
/>
}
iconActiveColor={account.currency.color}
icon={Icon ? <Icon size={16} /> : null}
key={account.id}
linkTo={`/account/${account.id}`}
>
{account.name}
</Item>
)
})}
</Fragment>
))
export default compose( export default compose(
connect(mapStateToProps, mapDispatchToProps, null, { connect(mapStateToProps, mapDispatchToProps, null, { pure: false }),
pure: false,
}),
translate(), translate(),
)(SideBar) )(SideBar)

7
src/components/base/Box/index.js

@ -49,6 +49,13 @@ const Box = styled.div`
overflow-y: ${p => (p.scroll === true ? 'auto' : '')}; overflow-y: ${p => (p.scroll === true ? 'auto' : '')};
position: ${p => (p.relative ? 'relative' : p.sticky ? 'absolute' : '')}; position: ${p => (p.relative ? 'relative' : p.sticky ? 'absolute' : '')};
${p =>
p.selectable &&
`
user-select: text;
cursor: text;
`};
${p => ${p =>
p.sticky && p.sticky &&
` `

56
src/components/base/CurrencyBadge.js

@ -0,0 +1,56 @@
// @flow
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 { rgba } from 'styles/helpers'
import Box from 'components/base/Box'
const CryptoIconWrapper = styled(Box).attrs({
align: 'center',
justify: 'center',
bg: p => rgba(p.cryptoColor, 0.1),
color: p => p.cryptoColor,
})`
border-radius: 50%;
width: ${p => p.size || 40}px;
height: ${p => p.size || 40}px;
`
export function CurrencyCircleIcon({
currency,
size,
...props
}: {
currency: Currency,
size: number,
}) {
const Icon = getCryptoCurrencyIcon(currency)
return (
<CryptoIconWrapper size={size} cryptoColor={currency.color} {...props}>
{Icon && <Icon size={size / 2} />}
</CryptoIconWrapper>
)
}
function CurrencyBadge({ currency, ...props }: { currency: Currency }) {
return (
<Box horizontal flow={3} {...props}>
<CurrencyCircleIcon size={40} currency={currency} />
<Box>
<Box ff="Museo Sans|ExtraBold" color="dark" fontSize={2} style={{ letterSpacing: 2 }}>
{currency.ticker}
</Box>
<Box ff="Open Sans" color="dark" fontSize={5}>
{currency.name}
</Box>
</Box>
</Box>
)
}
export default CurrencyBadge

10
src/components/base/FormattedVal/__tests__/__snapshots__/FormattedVal.test.js.snap

@ -2,7 +2,7 @@
exports[`components FormattedVal renders a formatted val 1`] = ` exports[`components FormattedVal renders a formatted val 1`] = `
<div <div
className="k45ou1-0 iqaJGf e345n3-0 eoitYS" className="k45ou1-0 iqaJGf e345n3-0 kZMOmW"
color="#66be54" color="#66be54"
> >
4 4
@ -11,7 +11,7 @@ exports[`components FormattedVal renders a formatted val 1`] = `
exports[`components FormattedVal renders a percent 1`] = ` exports[`components FormattedVal renders a percent 1`] = `
<div <div
className="k45ou1-0 iqaJGf e345n3-0 eoitYS" className="k45ou1-0 iqaJGf e345n3-0 kZMOmW"
color="#66be54" color="#66be54"
> >
30 % 30 %
@ -20,7 +20,7 @@ exports[`components FormattedVal renders a percent 1`] = `
exports[`components FormattedVal shows code 1`] = ` exports[`components FormattedVal shows code 1`] = `
<div <div
className="k45ou1-0 iqaJGf e345n3-0 eoitYS" className="k45ou1-0 iqaJGf e345n3-0 kZMOmW"
color="#66be54" color="#66be54"
> >
BTC 4 BTC 4
@ -29,7 +29,7 @@ exports[`components FormattedVal shows code 1`] = `
exports[`components FormattedVal shows sign 1`] = ` exports[`components FormattedVal shows sign 1`] = `
<div <div
className="k45ou1-0 iqaJGf e345n3-0 eoitYS" className="k45ou1-0 iqaJGf e345n3-0 kZMOmW"
color="#66be54" color="#66be54"
> >
+ 4 + 4
@ -38,7 +38,7 @@ exports[`components FormattedVal shows sign 1`] = `
exports[`components FormattedVal shows sign 2`] = ` exports[`components FormattedVal shows sign 2`] = `
<div <div
className="k45ou1-0 iqaJGf e345n3-0 lkKRAD" className="k45ou1-0 iqaJGf e345n3-0 fMRVKr"
color="#ea2e49" color="#ea2e49"
> >
- 4 - 4

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

@ -66,9 +66,11 @@ export const Textarea = styled.textarea.attrs({
type Props = { type Props = {
keepEvent?: boolean, keepEvent?: boolean,
onBlur: Function, onBlur: (SyntheticInputEvent<HTMLInputElement>) => void,
onChange?: Function, onChange?: Function,
onFocus: Function, onEnter?: (SyntheticKeyboardEvent<HTMLInputElement>) => void,
onEsc?: (SyntheticKeyboardEvent<HTMLInputElement>) => void,
onFocus: (SyntheticInputEvent<HTMLInputElement>) => void,
renderLeft?: any, renderLeft?: any,
renderRight?: any, renderRight?: any,
containerProps?: Object, containerProps?: Object,
@ -100,6 +102,21 @@ class Input extends PureComponent<Props, State> {
} }
} }
handleKeyDown = (e: SyntheticKeyboardEvent<HTMLInputElement>) => {
// handle enter key
if (e.which === 13) {
const { onEnter } = this.props
if (onEnter) {
onEnter(e)
}
} else if (e.which === 27) {
const { onEsc } = this.props
if (onEsc) {
onEsc(e)
}
}
}
// FIXME this is a bad idea! this is the behavior of an input. instead renderLeft/renderRight should be pointer-event:none ! // FIXME this is a bad idea! this is the behavior of an input. instead renderLeft/renderRight should be pointer-event:none !
handleClick = () => this._input && this._input.focus() handleClick = () => this._input && this._input.focus()
@ -119,6 +136,11 @@ class Input extends PureComponent<Props, State> {
onBlur(e) onBlur(e)
} }
handleSelectEverything = () => {
this._input && this._input.setSelectionRange(0, this._input.value.length)
this._input && this._input.focus()
}
_input = null _input = null
render() { render() {
@ -142,6 +164,7 @@ class Input extends PureComponent<Props, State> {
onFocus={this.handleFocus} onFocus={this.handleFocus}
onBlur={this.handleBlur} onBlur={this.handleBlur}
onChange={this.handleChange} onChange={this.handleChange}
onKeyDown={this.handleKeyDown}
/> />
</Box> </Box>
{renderRight} {renderRight}

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

@ -67,6 +67,10 @@ const CloseContainer = styled(Box).attrs({
&:hover { &:hover {
color: ${p => p.theme.colors.grey}; color: ${p => p.theme.colors.grey};
} }
&:active {
color: ${p => p.theme.colors.dark};
}
` `
const Body = styled(Box).attrs({ const Body = styled(Box).attrs({

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

@ -0,0 +1,67 @@
// @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({
horizontal: true,
align: 'center',
color: 'grey',
ff: 'Open Sans',
fontSize: 3,
p: 4,
})`
cursor: pointer;
position: absolute;
top: 0;
left: 0;
&:hover {
color: ${p => p.theme.colors.graphite};
}
&:active {
color: ${p => p.theme.colors.dark};
}
`
function ModalTitle({
t,
onBack,
children,
...props
}: {
t: T,
onBack: any => void,
children: any,
}) {
return (
<Container {...props}>
{onBack && (
<Back onClick={onBack}>
<IconAngleLeft size={16} />
{t('common:back')}
</Back>
)}
{children}
</Container>
)
}
export default translate()(ModalTitle)

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

@ -22,6 +22,7 @@ import Defer from 'components/base/Defer'
export { default as ModalBody } from './ModalBody' export { default as ModalBody } from './ModalBody'
export { default as ConfirmModal } from './ConfirmModal' export { default as ConfirmModal } from './ConfirmModal'
export { default as ModalTitle } from './ModalTitle'
const springConfig = { const springConfig = {
stiffness: 320, stiffness: 320,
@ -163,6 +164,7 @@ export class Modal extends Component<Props> {
isOpened={isOpened} isOpened={isOpened}
onClose={onClose} onClose={onClose}
onHide={onHide} onHide={onHide}
closeOnEsc={!preventBackdropClick}
motionStyle={(spring, isVisible) => ({ motionStyle={(spring, isVisible) => ({
opacity: spring(isVisible ? 1 : 0, springConfig), opacity: spring(isVisible ? 1 : 0, springConfig),
scale: spring(isVisible ? 1 : 0.95, springConfig), scale: spring(isVisible ? 1 : 0.95, springConfig),
@ -188,16 +190,6 @@ export class Modal extends Component<Props> {
} }
} }
export const ModalTitle = styled(Box).attrs({
alignItems: 'center',
color: 'dark',
ff: 'Museo Sans|Regular',
fontSize: 6,
justifyContent: 'center',
p: 5,
relative: true,
})``
export const ModalFooter = styled(Box).attrs({ export const ModalFooter = styled(Box).attrs({
px: 5, px: 5,
py: 3, py: 3,

9
src/components/base/Radio/index.js

@ -7,15 +7,16 @@ import { Tabbable } from 'components/base/Box'
const Base = styled(Tabbable).attrs({ relative: true })` const Base = styled(Tabbable).attrs({ relative: true })`
outline: none; outline: none;
box-shadow: 0 0 0 1px ${p => (p.isChecked ? p.theme.colors.lightGrey : p.theme.colors.graphite)}; box-shadow: 0 0 0 1px ${p => (p.isChecked ? p.theme.colors.lightGrey : p.theme.colors.fog)};
border-radius: 50%; border-radius: 50%;
height: 19px; height: 19px;
width: 19px; width: 19px;
transition: all ease-in-out 0.1s; transition: all ease-in-out 0.1s;
background-color: white;
&:focus { &:focus {
box-shadow: 0 0 0 ${p => (p.isChecked ? 4 : 2)}px box-shadow: 0 0 0 ${p => (p.isChecked ? 4 : 2)}px
${p => (p.isChecked ? p.theme.colors.lightGrey : p.theme.colors.graphite)}; ${p => (p.isChecked ? p.theme.colors.lightGrey : p.theme.colors.fog)};
} }
&:before, &:before,
@ -65,8 +66,4 @@ function Radio(props: Props) {
return <Base {...props} isChecked={isChecked} onClick={() => onChange && onChange(!isChecked)} /> return <Base {...props} isChecked={isChecked} onClick={() => onChange && onChange(!isChecked)} />
} }
Radio.defaultProps = {
onChange: null,
}
export default Radio export default Radio

30
src/components/base/Spinner.js

@ -0,0 +1,30 @@
// @flow
import React from 'react'
import styled, { keyframes } from 'styled-components'
import Box from 'components/base/Box'
import IconLoader from 'icons/Loader'
const rotate = keyframes`
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
`
const Container = styled(Box)`
width: ${p => p.size}px;
height: ${p => p.size}px;
animation: ${rotate} 1.5s linear infinite;
`
export default function Spinner({ size, ...props }: { size: number }) {
return (
<Container size={size} {...props}>
<IconLoader size={size} />
</Container>
)
}

27
src/components/modals/AddAccount/01-step-currency.js

@ -1,27 +0,0 @@
// @flow
import React from 'react'
import type { CryptoCurrency } from '@ledgerhq/live-common/lib/types'
import type { T } from 'types/common'
import Box from 'components/base/Box'
import Label from 'components/base/Label'
import SelectCurrency from 'components/SelectCurrency'
type Props = {
onChangeCurrency: Function,
currency?: ?CryptoCurrency,
t: T,
}
export default (props: Props) => (
<Box flow={1}>
<Label>{props.t('common:currency')}</Label>
<SelectCurrency
placeholder={props.t('common:chooseWalletPlaceholder')}
onChange={props.onChangeCurrency}
value={props.currency}
/>
</Box>
)

51
src/components/modals/AddAccount/03-step-import.js

@ -1,51 +0,0 @@
// @flow
import React from 'react'
import type { Account } from '@ledgerhq/live-common/lib/types'
import Box from 'components/base/Box'
import CheckBox from 'components/base/CheckBox'
import FormattedVal from 'components/base/FormattedVal'
type Props = {
scannedAccounts: Account[],
selectedAccounts: Account[],
existingAccounts: Account[],
onToggleAccount?: Account => void,
}
function StepImport(props: Props) {
const { scannedAccounts, selectedAccounts, existingAccounts, onToggleAccount } = props
return (
<Box flow={4}>
<strong>(design is not yet integrated!)</strong>
{scannedAccounts.map(account => {
const isSelected = selectedAccounts.find(a => a.id === account.id)
const isExisting = existingAccounts.find(a => a.id === account.id && a.archived === false)
return (
<Box
horizontal
key={account.id}
onClick={onToggleAccount && !isExisting ? () => onToggleAccount(account) : undefined}
>
<CheckBox isChecked={!!isSelected} onChange={onToggleAccount} />
<Box grow fontSize={6} style={{ paddingLeft: 10 }}>
{account.name}
</Box>
<FormattedVal
alwaysShowSign={false}
color="warmGrey"
fontSize={6}
showCode
unit={account.currency.units[0]}
val={account.balance}
/>
</Box>
)
})}
</Box>
)
}
export default StepImport

297
src/components/modals/AddAccount/index.js

@ -1,297 +0,0 @@
// @flow
import React, { PureComponent } from 'react'
import { connect } from 'react-redux'
import { compose } from 'redux'
import { translate } from 'react-i18next'
import { createStructuredSelector } from 'reselect'
import type { Account, CryptoCurrency } from '@ledgerhq/live-common/lib/types'
import type { Device, T } from 'types/common'
import { MODAL_ADD_ACCOUNT } from 'config/constants'
import { closeModal } from 'reducers/modals'
import {
canCreateAccount,
getAccounts,
getVisibleAccounts,
getArchivedAccounts,
} from 'reducers/accounts'
import { addAccount, updateAccount } from 'actions/accounts'
import Box from 'components/base/Box'
import Breadcrumb from 'components/Breadcrumb'
import Button from 'components/base/Button'
import Modal, { ModalContent, ModalTitle, ModalFooter, ModalBody } from 'components/base/Modal'
import StepConnectDevice from 'components/modals/StepConnectDevice'
import { getBridgeForCurrency } from 'bridge'
import StepCurrency from './01-step-currency'
import StepImport from './03-step-import'
const GET_STEPS = t => [
{ label: t('addAccount:steps.currency.title'), Comp: StepCurrency },
{ label: t('addAccount:steps.connectDevice.title'), Comp: StepConnectDevice },
{ label: t('addAccount:steps.importProgress.title'), Comp: StepImport },
{ label: t('addAccount:steps.importAccounts.title'), Comp: StepImport },
]
const mapStateToProps = createStructuredSelector({
existingAccounts: getAccounts,
visibleAccounts: getVisibleAccounts,
archivedAccounts: getArchivedAccounts,
canCreateAccount,
})
const mapDispatchToProps = {
addAccount,
closeModal,
updateAccount,
}
type Props = {
existingAccounts: Account[],
addAccount: Function,
visibleAccounts: Account[],
archivedAccounts: Account[],
canCreateAccount: boolean,
closeModal: Function,
t: T,
updateAccount: Function,
}
type State = {
stepIndex: number,
currency: ?CryptoCurrency,
deviceSelected: ?Device,
selectedAccounts: Account[],
scannedAccounts: Account[],
// TODO: what's that.
fetchingCounterValues: boolean,
appStatus: ?string,
}
const INITIAL_STATE = {
stepIndex: 0,
currency: null,
deviceSelected: null,
selectedAccounts: [],
scannedAccounts: [],
fetchingCounterValues: false,
appStatus: null,
}
class AddAccountModal extends PureComponent<Props, State> {
state = INITIAL_STATE
componentWillUnmount() {
this.handleReset()
}
scanSubscription: *
startScanAccountsDevice() {
const { visibleAccounts } = this.props
const { deviceSelected, currency } = this.state
if (!deviceSelected || !currency) {
return
}
const bridge = getBridgeForCurrency(currency)
this.scanSubscription = bridge.scanAccountsOnDevice(currency, deviceSelected.path, {
next: account => {
if (!visibleAccounts.some(a => a.id === account.id)) {
this.setState(state => ({
scannedAccounts: [...state.scannedAccounts, account],
}))
}
},
complete: () => {
// we should be able to interrupt the scan too if you want to select early etc..
// like imagine there are way too more accounts to scan, so you are not stuck here.
this.setState({ stepIndex: 3 })
},
error: error => {
// TODO what to do ?
console.error(error)
},
})
}
canNext = () => {
const { stepIndex } = this.state
if (stepIndex === 0) {
const { currency } = this.state
return currency !== null
}
if (stepIndex === 1) {
const { deviceSelected, appStatus } = this.state
return deviceSelected !== null && appStatus === 'success'
}
if (stepIndex === 3) {
const { selectedAccounts } = this.state
return selectedAccounts.length > 0
}
return false
}
_steps = GET_STEPS(this.props.t)
handleChangeDevice = d => this.setState({ deviceSelected: d })
handleToggleAccount = account => {
const { selectedAccounts } = this.state
const isSelected = selectedAccounts.find(a => a === account)
this.setState({
selectedAccounts: isSelected
? selectedAccounts.filter(a => a !== account)
: [...selectedAccounts, account],
})
}
handleChangeCurrency = (currency: CryptoCurrency) => this.setState({ currency })
handleChangeStatus = (deviceStatus, appStatus) => this.setState({ appStatus })
handleImportAccount = () => {
const { addAccount } = this.props
const { selectedAccounts } = this.state
selectedAccounts.forEach(a => addAccount({ ...a, archived: false }))
this.setState({ selectedAccounts: [] })
closeModal(MODAL_ADD_ACCOUNT)
}
handleNextStep = () => {
const { stepIndex } = this.state
if (stepIndex >= this._steps.length - 1) {
return
}
this.setState({ stepIndex: stepIndex + 1 })
}
handleReset = () => {
this.setState(INITIAL_STATE)
if (this.scanSubscription) this.scanSubscription.unsubscribe()
}
renderStep() {
const { t, existingAccounts } = this.props
const { stepIndex, scannedAccounts, currency, deviceSelected, selectedAccounts } = this.state
const step = this._steps[stepIndex]
if (!step) {
return null
}
const { Comp } = step
const props = (predicate, props) => (predicate ? props : {})
const stepProps = {
t,
currency,
// STEP CURRENCY
...props(stepIndex === 0, {
onChangeCurrency: this.handleChangeCurrency,
}),
// STEP CONNECT DEVICE
...props(stepIndex === 1, {
deviceSelected,
onStatusChange: this.handleChangeStatus,
onChangeDevice: this.handleChangeDevice,
}),
// STEP ACCOUNT IMPORT PROGRESS
...props(stepIndex === 2, {
selectedAccounts,
scannedAccounts,
existingAccounts,
}),
// STEP FINISH AND SELECT ACCOUNTS
...props(stepIndex === 3, {
onToggleAccount: this.handleToggleAccount,
selectedAccounts,
scannedAccounts,
existingAccounts,
}),
}
return <Comp {...stepProps} />
}
renderButton() {
const { t } = this.props
const { fetchingCounterValues, stepIndex, selectedAccounts } = this.state
let onClick
switch (stepIndex) {
case 1:
onClick = () => {
this.handleNextStep()
this.startScanAccountsDevice()
}
break
case 3:
onClick = this.handleImportAccount
break
default:
onClick = this.handleNextStep
}
const props = {
primary: true,
disabled: fetchingCounterValues || !this.canNext(),
onClick,
children: fetchingCounterValues
? 'Fetching counterValues...'
: stepIndex === 3
? t('addAccount:steps.importAccounts.cta', {
count: selectedAccounts.length,
})
: t('common:next'),
}
return <Button {...props} />
}
render() {
const { t } = this.props
const { stepIndex } = this.state
return (
<Modal
name={MODAL_ADD_ACCOUNT}
onHide={this.handleReset}
render={({ onClose }) => (
<ModalBody onClose={onClose}>
<ModalTitle>{t('addAccount:title')}</ModalTitle>
<ModalContent>
<Breadcrumb mb={6} currentStep={stepIndex} items={this._steps} />
{this.renderStep()}
</ModalContent>
{stepIndex !== 2 && (
<ModalFooter>
<Box horizontal alignItems="center" justifyContent="flex-end">
{this.renderButton()}
</Box>
</ModalFooter>
)}
</ModalBody>
)}
/>
)
}
}
export default compose(connect(mapStateToProps, mapDispatchToProps), translate())(AddAccountModal)

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

@ -0,0 +1,167 @@
// @flow
import React, { PureComponent } from 'react'
import styled from 'styled-components'
import type { Account } from '@ledgerhq/live-common/lib/types'
import { darken } from 'styles/helpers'
import Box from 'components/base/Box'
import Radio from 'components/base/Radio'
import CryptoCurrencyIcon from 'components/CryptoCurrencyIcon'
import FormattedVal from 'components/base/FormattedVal'
import Input from 'components/base/Input'
import IconEdit from 'icons/Edit'
import IconCheck from 'icons/Check'
type Props = {
account: Account,
isChecked: boolean,
isDisabled: boolean,
onClick: Account => void,
onAccountUpdate: Account => void,
}
type State = {
isEditing: boolean,
accountNameCopy: string,
}
export default class AccountRow extends PureComponent<Props, State> {
state = {
isEditing: false,
accountNameCopy: '',
}
componentDidUpdate(prevProps: Props, prevState: State) {
const startedEditing = !prevState.isEditing && this.state.isEditing
if (startedEditing) {
this._input && this._input.handleSelectEverything()
}
}
handleEditClick = (e: SyntheticEvent<any>) => {
this.handlePreventSubmit(e)
const { account } = this.props
this.setState({ isEditing: true, accountNameCopy: account.name })
}
handleSubmitName = (e: SyntheticEvent<any>) => {
this.handlePreventSubmit(e)
const { account, onAccountUpdate, isChecked, onClick } = this.props
const { accountNameCopy } = this.state
const updatedAccount = { ...account, name: accountNameCopy }
this.setState({ isEditing: false, accountNameCopy: '' })
onAccountUpdate(updatedAccount)
if (!isChecked) {
onClick(updatedAccount)
}
}
handlePreventSubmit = (e: SyntheticEvent<any>) => {
// prevent account row to be submitted
e.preventDefault()
e.stopPropagation()
}
handleChangeName = (accountNameCopy: string) => this.setState({ accountNameCopy })
handleReset = () => this.setState({ isEditing: false, accountNameCopy: '' })
_input = null
render() {
const { account, isChecked, onClick, isDisabled } = this.props
const { isEditing, accountNameCopy } = this.state
return (
<AccountRowContainer onClick={() => onClick(account)} isDisabled={isDisabled}>
<CryptoCurrencyIcon currency={account.currency} size={16} color={account.currency.color} />
<Box shrink grow ff="Open Sans|SemiBold" color="dark" fontSize={4}>
{isEditing ? (
<Input
containerProps={{ style: { width: 260 } }}
value={accountNameCopy}
onChange={this.handleChangeName}
onClick={this.handlePreventSubmit}
onEnter={this.handleSubmitName}
onEsc={this.handleReset}
renderRight={
<InputRight onClick={this.handleSubmitName}>
<IconCheck size={16} />
</InputRight>
}
ref={input => (this._input = input)}
/>
) : (
<div style={{ textOverflow: 'ellipsis', overflow: 'hidden' }}>{account.name}</div>
)}
</Box>
{!isEditing && (
<Edit onClick={this.handleEditClick}>
<IconEdit size={13} />
<span>{'edit name'}</span>
</Edit>
)}
<FormattedVal
val={account.balance}
unit={account.unit}
showCode
fontSize={4}
color="grey"
/>
<Radio isChecked={isChecked || isDisabled} />
</AccountRowContainer>
)
}
}
const AccountRowContainer = styled(Box).attrs({
horizontal: true,
align: 'center',
bg: 'lightGrey',
px: 3,
flow: 3,
})`
height: 48px;
border-radius: 4px;
opacity: ${p => (p.isDisabled ? 0.5 : 1)};
pointer-events: ${p => (p.isDisabled ? 'none' : 'auto')};
&:hover {
cursor: pointer;
background-color: ${p => darken(p.theme.colors.lightGrey, 0.015)};
}
&:active {
background-color: ${p => darken(p.theme.colors.lightGrey, 0.03)};
}
`
const Edit = styled(Box).attrs({
color: 'wallet',
fontSize: 3,
horizontal: true,
align: 'center',
flow: 1,
py: 1,
})`
display: none;
${AccountRowContainer}:hover & {
display: flex;
}
&:hover {
text-decoration: underline;
}
`
const InputRight = styled(Box).attrs({
bg: 'wallet',
color: 'white',
align: 'center',
justify: 'center',
shrink: 0,
})`
width: 40px;
`

224
src/components/modals/ImportAccounts/index.js

@ -0,0 +1,224 @@
// @flow
import React, { PureComponent } from 'react'
import { compose } from 'redux'
import { connect } from 'react-redux'
import { translate } from 'react-i18next'
import type { Currency, Account } from '@ledgerhq/live-common/lib/types'
import type { T, Device } from 'types/common'
import { getCurrentDevice } from 'reducers/devices'
import { getAccounts } from 'reducers/accounts'
import { addAccount } from 'actions/accounts'
import { closeModal } from 'reducers/modals'
import Modal, { ModalContent, ModalTitle, ModalFooter, ModalBody } from 'components/base/Modal'
import Box from 'components/base/Box'
import Breadcrumb from 'components/Breadcrumb'
import StepChooseCurrency, { StepChooseCurrencyFooter } from './steps/01-step-choose-currency'
import StepConnectDevice, { StepConnectDeviceFooter } from './steps/02-step-connect-device'
import StepImport, { StepImportFooter } from './steps/03-step-import'
import StepFinish from './steps/04-step-finish'
const createSteps = ({ t }: { t: T }) => [
{
id: 'chooseCurrency',
label: t('importAccounts:breadcrumb.informations'),
component: StepChooseCurrency,
footer: StepChooseCurrencyFooter,
onBack: null,
hideFooter: false,
},
{
id: 'connectDevice',
label: t('importAccounts:breadcrumb.connectDevice'),
component: StepConnectDevice,
footer: StepConnectDeviceFooter,
onBack: ({ transitionTo }: StepProps) => transitionTo('chooseCurrency'),
hideFooter: false,
},
{
id: 'import',
label: t('importAccounts:breadcrumb.import'),
component: StepImport,
footer: StepImportFooter,
onBack: ({ transitionTo }: StepProps) => transitionTo('chooseCurrency'),
hideFooter: false,
},
{
id: 'finish',
label: t('importAccounts:breadcrumb.finish'),
component: StepFinish,
footer: null,
onBack: null,
hideFooter: true,
},
]
type Props = {
t: T,
currentDevice: ?Device,
existingAccounts: Account[],
closeModal: string => void,
addAccount: Account => void,
}
type StepId = 'chooseCurrency' | 'connectDevice' | 'import' | 'finish'
type ScanStatus = 'idle' | 'scanning' | 'error' | 'finished'
type State = {
stepId: StepId,
isAppOpened: boolean,
currency: ?Currency,
// scan process
scannedAccounts: Account[],
checkedAccountsIds: string[],
scanStatus: ScanStatus,
err: ?Error,
}
export type StepProps = {
t: T,
currency: ?Currency,
currentDevice: ?Device,
isAppOpened: boolean,
transitionTo: StepId => void,
setState: any => void,
onClickImport: void => Promise<void>,
onCloseModal: void => void,
// scan process
scannedAccounts: Account[],
existingAccounts: Account[],
checkedAccountsIds: string[],
scanStatus: ScanStatus,
err: ?Error,
}
const mapStateToProps = state => ({
currentDevice: getCurrentDevice(state),
existingAccounts: getAccounts(state),
})
const mapDispatchToProps = {
addAccount,
closeModal,
}
const INITIAL_STATE = {
stepId: 'chooseCurrency',
isAppOpened: false,
currency: null,
scannedAccounts: [],
checkedAccountsIds: [],
err: null,
scanStatus: 'idle',
}
class ImportAccounts extends PureComponent<Props, State> {
state = INITIAL_STATE
STEPS = createSteps({
t: this.props.t,
})
transitionTo = stepId => {
let nextState = { stepId }
if (stepId === 'chooseCurrency') {
nextState = { ...INITIAL_STATE }
}
this.setState(nextState)
}
handleClickImport = async () => {
const { addAccount } = this.props
const { scannedAccounts, checkedAccountsIds } = this.state
const accountsIdsMap = checkedAccountsIds.reduce((acc, cur) => {
acc[cur] = true
return acc
}, {})
const accountsToImport = scannedAccounts.filter(account => accountsIdsMap[account.id] === true)
for (let i = 0; i < accountsToImport.length; i++) {
await idleCallback()
addAccount(accountsToImport[i])
}
this.transitionTo('finish')
}
handleCloseModal = () => {
const { closeModal } = this.props
closeModal('importAccounts')
}
render() {
const { t, currentDevice, existingAccounts } = this.props
const {
stepId,
currency,
isAppOpened,
scannedAccounts,
checkedAccountsIds,
scanStatus,
err,
} = this.state
const stepIndex = this.STEPS.findIndex(s => s.id === stepId)
const step = this.STEPS[stepIndex]
if (!step) {
throw new Error(`ImportAccountsModal: step ${stepId} doesn't exists`)
}
const { component: StepComponent, footer: StepFooter, hideFooter, onBack } = step
const stepProps: StepProps = {
t,
currency,
currentDevice,
existingAccounts,
scannedAccounts,
checkedAccountsIds,
scanStatus,
err,
isAppOpened,
onClickImport: this.handleClickImport,
onCloseModal: this.handleCloseModal,
transitionTo: this.transitionTo,
setState: (...args) => this.setState(...args),
}
return (
<Modal
name="importAccounts"
preventBackdropClick
onHide={() => this.setState({ ...INITIAL_STATE })}
render={({ onClose }) => (
<ModalBody onClose={onClose}>
<ModalTitle onBack={onBack ? () => onBack(stepProps) : void 0}>
{t('importAccounts:title')}
</ModalTitle>
<ModalContent>
<Breadcrumb mb={6} currentStep={stepIndex} items={this.STEPS} />
<StepComponent {...stepProps} />
</ModalContent>
{!hideFooter && (
<ModalFooter horizontal align="center" justify="flex-end" style={{ height: 80 }}>
{StepFooter ? <StepFooter {...stepProps} /> : <Box>footer</Box>}
</ModalFooter>
)}
</ModalBody>
)}
/>
)
}
}
export default compose(connect(mapStateToProps, mapDispatchToProps), translate())(ImportAccounts)
function idleCallback() {
return new Promise(resolve => window.requestIdleCallback(resolve))
}

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

@ -0,0 +1,26 @@
// @flow
import React, { Fragment } from 'react'
import SelectCurrency from 'components/SelectCurrency'
import Button from 'components/base/Button'
import CurrencyBadge from 'components/base/CurrencyBadge'
import type { StepProps } from '../index'
function StepChooseCurrency({ currency, setState }: StepProps) {
return <SelectCurrency onChange={currency => setState({ currency })} value={currency} />
}
export function StepChooseCurrencyFooter({ transitionTo, currency, t }: StepProps) {
return (
<Fragment>
{currency && <CurrencyBadge mr="auto" currency={currency} />}
<Button primary disabled={!currency} onClick={() => transitionTo('connectDevice')}>
{t('common:next')}
</Button>
</Fragment>
)
}
export default StepChooseCurrency

53
src/components/modals/ImportAccounts/steps/02-step-connect-device.js

@ -0,0 +1,53 @@
// @flow
import React, { Fragment } from 'react'
import { Trans } from 'react-i18next'
import Button from 'components/base/Button'
import Box from 'components/base/Box'
import ConnectDevice from 'components/modals/StepConnectDevice'
import { CurrencyCircleIcon } from 'components/base/CurrencyBadge'
import type { StepProps } from '../index'
function StepConnectDevice({ t, currency, currentDevice, setState }: StepProps) {
if (!currency) {
throw new Error('No currency given')
}
return (
<Fragment>
<Box align="center" mb={6}>
<CurrencyCircleIcon mb={3} size={40} currency={currency} />
<Box ff="Open Sans" fontSize={4} color="dark" textAlign="center" style={{ width: 370 }}>
<Trans i18nKey="importAccounts:connectDevice.desc" parent="div">
{`You're about to import your `}
<strong style={{ fontWeight: 'bold' }}>{`${currency.name} (${
currency.ticker
})`}</strong>
{` account(s) from your Ledger device. Please follow the steps below:`}
</Trans>
</Box>
</Box>
<ConnectDevice
t={t}
deviceSelected={currentDevice}
currency={currency}
onStatusChange={s => {
if (s === 'connected') {
setState({ isAppOpened: true })
}
}}
/>
</Fragment>
)
}
export function StepConnectDeviceFooter({ t, transitionTo, isAppOpened }: StepProps) {
return (
<Button primary disabled={!isAppOpened} onClick={() => transitionTo('import')}>
{t('common:next')}
</Button>
)
}
export default StepConnectDevice

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

@ -0,0 +1,168 @@
// @flow
import React, { PureComponent } from 'react'
import type { Account } from '@ledgerhq/live-common/lib/types'
import { getBridgeForCurrency } from 'bridge'
import Box from 'components/base/Box'
import Button from 'components/base/Button'
import Spinner from 'components/base/Spinner'
import IconExchange from 'icons/Exchange'
import AccountRow from '../AccountRow'
import type { StepProps } from '../index'
class StepImport extends PureComponent<StepProps> {
componentDidMount() {
this.startScanAccountsDevice()
}
componentWillUnmount() {
if (this.scanSubscription) {
this.scanSubscription.unsubscribe()
}
}
scanSubscription = null
startScanAccountsDevice() {
const { currency, currentDevice, setState } = this.props
try {
if (!currency) {
throw new Error('No currency to scan')
}
if (!currentDevice) {
throw new Error('No device')
}
const bridge = getBridgeForCurrency(currency)
// TODO: use the real device
const devicePath = currentDevice.path
setState({ scanStatus: 'scanning' })
this.scanSubscription = bridge.scanAccountsOnDevice(currency, devicePath, {
next: account => {
const { scannedAccounts } = this.props
const hasAlreadyBeenScanned = !!scannedAccounts.find(a => account.id === a.id)
if (!hasAlreadyBeenScanned) {
setState({ scannedAccounts: [...scannedAccounts, account] })
}
},
complete: () => setState({ scanStatus: 'finished' }),
error: err => setState({ scanStatus: 'error', err }),
})
} catch (err) {
setState({ scanStatus: 'error', err })
}
}
handleRetry = () => {
if (this.scanSubscription) {
this.scanSubscription.unsubscribe()
this.scanSubscription = null
}
this.handleResetState()
this.startScanAccountsDevice()
}
handleResetState = () => {
const { setState } = this.props
setState({
scanStatus: 'idle',
err: null,
scannedAccounts: [],
checkedAccountsIds: [],
})
}
handleToggleAccount = (account: Account) => {
const { checkedAccountsIds, setState } = this.props
const isChecked = checkedAccountsIds.find(id => id === account.id) !== undefined
if (isChecked) {
setState({ checkedAccountsIds: checkedAccountsIds.filter(id => id !== account.id) })
} else {
setState({ checkedAccountsIds: [...checkedAccountsIds, account.id] })
}
}
handleAccountUpdate = (updatedAccount: Account) => {
const { scannedAccounts, setState } = this.props
setState({
scannedAccounts: scannedAccounts.map(account => {
if (account.id !== updatedAccount.id) {
return account
}
return updatedAccount
}),
})
}
render() {
const { scanStatus, err, scannedAccounts, checkedAccountsIds, existingAccounts } = this.props
return (
<Box>
{err && <Box shrink>{err.message}</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 (
<AccountRow
key={account.id}
account={existingAccount || account}
isChecked={isChecked}
isDisabled={isDisabled}
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) && (
<Button ml="auto" small outline onClick={this.handleRetry}>
<Box horizontal flow={2} align="center">
<IconExchange size={13} />
<span>{'retry'}</span>
</Box>
</Button>
)}
</Box>
</Box>
)
}
}
export default StepImport
export const StepImportFooter = ({ scanStatus, onClickImport, checkedAccountsIds }: StepProps) => (
<Button
primary
disabled={scanStatus !== 'finished' || checkedAccountsIds.length === 0}
onClick={() => onClickImport()}
>
{'Import accounts'}
</Button>
)

25
src/components/modals/ImportAccounts/steps/04-step-finish.js

@ -0,0 +1,25 @@
// @flow
import React from 'react'
import Box from 'components/base/Box'
import Button from 'components/base/Button'
import IconCheckCircle from 'icons/CheckCircle'
import type { StepProps } from '../index'
function StepFinish({ onCloseModal }: StepProps) {
return (
<Box align="center" py={6}>
<Box color="positiveGreen" mb={4}>
<IconCheckCircle size={40} />
</Box>
<Box mb={4}>{'Great success!'}</Box>
<Button primary onClick={onCloseModal}>
{'Close'}
</Button>
</Box>
)
}
export default StepFinish

2
src/components/modals/StepConnectDevice.js

@ -13,7 +13,7 @@ type Props = {
account?: ?Account, account?: ?Account,
currency?: ?CryptoCurrency, currency?: ?CryptoCurrency,
deviceSelected?: ?Device, deviceSelected?: ?Device,
onChangeDevice: Device => void, onChangeDevice?: Device => void,
onStatusChange: string => void, onStatusChange: string => void,
} }

2
src/components/modals/index.js

@ -1,5 +1,5 @@
export Debug from './Debug' export Debug from './Debug'
export AddAccount from './AddAccount' export ImportAccounts from './ImportAccounts'
export OperationDetails from './OperationDetails' export OperationDetails from './OperationDetails'
export Receive from './Receive' export Receive from './Receive'
export Send from './Send' export Send from './Send'

60
src/helpers/libcore.js

@ -136,7 +136,8 @@ async function scanNextAccount(props: {
? await wallet.getAccount(accountIndex) ? await wallet.getAccount(accountIndex)
: await core.createAccount(wallet, hwApp) : await core.createAccount(wallet, hwApp)
if (!hasBeenScanned) { const shouldSyncAccount = true // TODO: let's sync everytime. maybe in the future we can optimize.
if (shouldSyncAccount) {
await core.syncAccount(njsAccount) await core.syncAccount(njsAccount)
} }
@ -214,16 +215,6 @@ async function buildAccountRaw({
// $FlowFixMe // $FlowFixMe
ops: NJSOperation[], ops: NJSOperation[],
}): Promise<AccountRaw> { }): Promise<AccountRaw> {
/*
const balanceByDay = ops.length
? await getBalanceByDaySinceOperation({
njsAccount,
njsOperation: ops[0],
core,
})
: {}
*/
const njsBalance = await njsAccount.getBalance() const njsBalance = await njsAccount.getBalance()
const balance = njsBalance.toLong() const balance = njsBalance.toLong()
@ -269,7 +260,7 @@ async function buildAccountRaw({
freshAddressPath, freshAddressPath,
balance, balance,
blockHeight, blockHeight,
archived: true, archived: false,
index: accountIndex, index: accountIndex,
operations, operations,
pendingOperations: [], pendingOperations: [],
@ -318,48 +309,3 @@ function buildOperationRaw({
date: op.getDate().toISOString(), date: op.getDate().toISOString(),
} }
} }
/*
async function getBalanceByDaySinceOperation({
njsAccount,
njsOperation,
core,
}: {
njsAccount: NJSAccount,
njsOperation: NJSOperation,
core: Object,
}) {
const startDate = njsOperation.getDate()
// set end date to tomorrow
const endDate = new Date()
endDate.setDate(endDate.getDate() + 1)
const njsBalanceHistory = await njsAccount.getBalanceHistory(
startDate.toISOString(),
endDate.toISOString(),
core.TIME_PERIODS.DAY,
)
let i = 0
const res = {}
while (!areSameDay(startDate, endDate)) {
const dateSQLFormatted = startDate.toISOString().substr(0, 10)
const balanceDay = njsBalanceHistory[i]
if (balanceDay) {
res[dateSQLFormatted] = njsBalanceHistory[i].toLong()
} else {
console.warn(`No balance for day ${dateSQLFormatted}. This is a bug.`) // eslint-disable-line no-console
}
startDate.setDate(startDate.getDate() + 1)
i++
}
return res
}
function areSameDay(date1: Date, date2: Date): boolean {
return (
date1.getFullYear() === date2.getFullYear() &&
date1.getMonth() === date2.getMonth() &&
date1.getDate() === date2.getDate()
)
}
*/

2
src/icons/Home.js

@ -3,7 +3,7 @@
import React from 'react' import React from 'react'
const path = ( const path = (
<path d="m8.261 2.252a1.11 1.11 0 0 0-1.408 0l-6.772 5.545a0.224 0.224 0 0 0-0.03 0.313l0.563 0.69a0.224 0.224 0 0 0 0.314 0.03l0.408-0.333v5.502c0 0.245 0.2 0.445 0.445 0.445h4.667c0.122 0 0.222-0.1 0.222-0.222v-3.556h1.778v3.556c0 0.122 0.1 0.222 0.222 0.222h4.666c0.245 0 0.445-0.2 0.445-0.445v-5.505l0.408 0.333a0.224 0.224 0 0 0 0.314-0.03l0.564-0.69a0.227 0.227 0 0 0-0.036-0.31l-6.771-5.545zm4.184 10.858h-2.667v-3.555c0-0.122-0.1-0.222-0.222-0.222h-4c-0.122 0-0.222 0.1-0.222 0.222v3.555h-2.667v-5.708l4.747-3.889c0.08-0.066 0.2-0.066 0.28 0l4.748 3.89v5.707z"/> <path d="m8.261 2.252a1.11 1.11 0 0 0-1.408 0l-6.772 5.545a0.224 0.224 0 0 0-0.03 0.313l0.563 0.69a0.224 0.224 0 0 0 0.314 0.03l0.408-0.333v5.502c0 0.245 0.2 0.445 0.445 0.445h4.667c0.122 0 0.222-0.1 0.222-0.222v-3.556h1.778v3.556c0 0.122 0.1 0.222 0.222 0.222h4.666c0.245 0 0.445-0.2 0.445-0.445v-5.505l0.408 0.333a0.224 0.224 0 0 0 0.314-0.03l0.564-0.69a0.227 0.227 0 0 0-0.036-0.31l-6.771-5.545zm4.184 10.858h-2.667v-3.555c0-0.122-0.1-0.222-0.222-0.222h-4c-0.122 0-0.222 0.1-0.222 0.222v3.555h-2.667v-5.708l4.747-3.889c0.08-0.066 0.2-0.066 0.28 0l4.748 3.89v5.707z" />
) )
export default ({ size, ...p }: { size: number }) => ( export default ({ size, ...p }: { size: number }) => (

5
src/icons/Loader.js

@ -3,7 +3,10 @@
import React from 'react' import React from 'react'
const path = ( const path = (
<path d="M16.735 2.8c0-.69864021.5663598-1.265 1.265-1.265.6986402 0 1.265.56635979 1.265 1.265v6.4c0 .69864021-.5663598 1.265-1.265 1.265-.6986402 0-1.265-.56635979-1.265-1.265V2.8zm11.118533 3.55748685c.4940132-.49401323 1.2949669-.49401323 1.7889802 0 .4940132.49401323.4940132 1.29496693 0 1.78898016l-4.5254834 4.52548339c-.4940133.4940132-1.294967.4940132-1.7889802 0-.4940132-.4940132-.4940132-1.2949669 0-1.7889802l4.5254834-4.52548335zM33.2 16.735c.6986402 0 1.265.5663598 1.265 1.265 0 .6986402-.5663598 1.265-1.265 1.265h-6.4c-.6986402 0-1.265-.5663598-1.265-1.265 0-.6986402.5663598-1.265 1.265-1.265h6.4zm-3.5574868 11.118533c.4940132.4940132.4940132 1.2949669 0 1.7889802-.4940133.4940132-1.294967.4940132-1.7889802 0l-4.5254834-4.5254834c-.4940132-.4940133-.4940132-1.294967 0-1.7889802.4940132-.4940132 1.2949669-.4940132 1.7889802 0l4.5254834 4.5254834zM19.265 33.2c0 .6986402-.5663598 1.265-1.265 1.265-.6986402 0-1.265-.5663598-1.265-1.265v-6.4c0-.6986402.5663598-1.265 1.265-1.265.6986402 0 1.265.5663598 1.265 1.265v6.4zM8.14646701 29.6425132c-.49401323.4940132-1.29496693.4940132-1.78898016 0-.49401323-.4940133-.49401323-1.294967 0-1.7889802l4.52548335-4.5254834c.4940133-.4940132 1.294967-.4940132 1.7889802 0 .4940132.4940132.4940132 1.2949669 0 1.7889802l-4.52548339 4.5254834zM2.8 19.265c-.69864021 0-1.265-.5663598-1.265-1.265 0-.6986402.56635979-1.265 1.265-1.265h6.4c.69864021 0 1.265.5663598 1.265 1.265 0 .6986402-.56635979 1.265-1.265 1.265H2.8zM6.35748685 8.14646701c-.49401323-.49401323-.49401323-1.29496693 0-1.78898016.49401323-.49401323 1.29496693-.49401323 1.78898016 0l4.52548339 4.52548335c.4940132.4940133.4940132 1.294967 0 1.7889802-.4940132.4940132-1.2949669.4940132-1.7889802 0L6.35748685 8.14646701z" /> <path
fill="currentColor"
d="M16.735 2.8c0-.69864021.5663598-1.265 1.265-1.265.6986402 0 1.265.56635979 1.265 1.265v6.4c0 .69864021-.5663598 1.265-1.265 1.265-.6986402 0-1.265-.56635979-1.265-1.265V2.8zm11.118533 3.55748685c.4940132-.49401323 1.2949669-.49401323 1.7889802 0 .4940132.49401323.4940132 1.29496693 0 1.78898016l-4.5254834 4.52548339c-.4940133.4940132-1.294967.4940132-1.7889802 0-.4940132-.4940132-.4940132-1.2949669 0-1.7889802l4.5254834-4.52548335zM33.2 16.735c.6986402 0 1.265.5663598 1.265 1.265 0 .6986402-.5663598 1.265-1.265 1.265h-6.4c-.6986402 0-1.265-.5663598-1.265-1.265 0-.6986402.5663598-1.265 1.265-1.265h6.4zm-3.5574868 11.118533c.4940132.4940132.4940132 1.2949669 0 1.7889802-.4940133.4940132-1.294967.4940132-1.7889802 0l-4.5254834-4.5254834c-.4940132-.4940133-.4940132-1.294967 0-1.7889802.4940132-.4940132 1.2949669-.4940132 1.7889802 0l4.5254834 4.5254834zM19.265 33.2c0 .6986402-.5663598 1.265-1.265 1.265-.6986402 0-1.265-.5663598-1.265-1.265v-6.4c0-.6986402.5663598-1.265 1.265-1.265.6986402 0 1.265.5663598 1.265 1.265v6.4zM8.14646701 29.6425132c-.49401323.4940132-1.29496693.4940132-1.78898016 0-.49401323-.4940133-.49401323-1.294967 0-1.7889802l4.52548335-4.5254834c.4940133-.4940132 1.294967-.4940132 1.7889802 0 .4940132.4940132.4940132 1.2949669 0 1.7889802l-4.52548339 4.5254834zM2.8 19.265c-.69864021 0-1.265-.5663598-1.265-1.265 0-.6986402.56635979-1.265 1.265-1.265h6.4c.69864021 0 1.265.5663598 1.265 1.265 0 .6986402-.56635979 1.265-1.265 1.265H2.8zM6.35748685 8.14646701c-.49401323-.49401323-.49401323-1.29496693 0-1.78898016.49401323-.49401323 1.29496693-.49401323 1.78898016 0l4.52548339 4.52548335c.4940132.4940133.4940132 1.294967 0 1.7889802-.4940132.4940132-1.2949669.4940132-1.7889802 0L6.35748685 8.14646701z"
/>
) )
export default ({ size, ...p }: { size: number }) => ( export default ({ size, ...p }: { size: number }) => (

2
static/i18n/en/common.yml

@ -8,7 +8,7 @@ continue: Continue
chooseWalletPlaceholder: Choose a wallet... chooseWalletPlaceholder: Choose a wallet...
currency: Currency currency: Currency
selectAccount: Select an account selectAccount: Select an account
selectCurrency: Select an currency selectCurrency: Select a currency
sortBy: Sort by sortBy: Sort by
search: Search search: Search
save: Save save: Save

6
static/i18n/en/importAccounts.yml

@ -0,0 +1,6 @@
title: Import accounts
breadcrumb:
informations: Informations
connectDevice: Connect device
import: Import
finish: End
Loading…
Cancel
Save