committed by
GitHub
31 changed files with 932 additions and 504 deletions
@ -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 |
@ -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) |
@ -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> |
||||
|
) |
||||
|
} |
@ -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> |
|
||||
) |
|
@ -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 |
|
@ -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) |
|
@ -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; |
||||
|
` |
@ -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)) |
||||
|
} |
@ -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 |
@ -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 |
@ -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> |
||||
|
) |
@ -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 |
@ -0,0 +1,6 @@ |
|||||
|
title: Import accounts |
||||
|
breadcrumb: |
||||
|
informations: Informations |
||||
|
connectDevice: Connect device |
||||
|
import: Import |
||||
|
finish: End |
Loading…
Reference in new issue