diff --git a/src/bridge/makeMockBridge.js b/src/bridge/makeMockBridge.js index 80a3de90..6cd38b72 100644 --- a/src/bridge/makeMockBridge.js +++ b/src/bridge/makeMockBridge.js @@ -79,13 +79,13 @@ function makeMockBridge(opts?: Opts): WalletBridge<*> { async function job() { if (Math.random() > scanAccountDeviceSuccessRate) { - await delay(5000) + await delay(1000) if (!unsubscribed) error(new Error('scan failed')) return } const nbAccountToGen = 3 for (let i = 0; i < nbAccountToGen && !unsubscribed; i++) { - await delay(2000) + await delay(500) const account = genAccount(String(Math.random()), { operationsSize: 0, currency, diff --git a/src/components/Breadcrumb/Step.js b/src/components/Breadcrumb/Step.js index 1492365a..9460b122 100644 --- a/src/components/Breadcrumb/Step.js +++ b/src/components/Breadcrumb/Step.js @@ -3,6 +3,8 @@ import React from 'react' import styled from 'styled-components' +import { colors } from 'styles/theme' + import Box from 'components/base/Box' import IconCheck from 'icons/Check' @@ -27,13 +29,13 @@ const Wrapper = styled(Box).attrs({ const StepNumber = styled(Box).attrs({ alignItems: 'center', justifyContent: 'center', - color: 'fog', + color: p => (['active', 'valid'].includes(p.status) ? 'white' : 'fog'), bg: p => ['active', 'valid'].includes(p.status) ? 'wallet' : p.status === 'error' ? 'alertRed' : 'white', ff: 'Rubik|Regular', })` border-radius: 50%; - border: 1px solid #d8d8d8; + border: 1px solid ${p => (['active', 'valid'].includes(p.status) ? colors.wallet : colors.fog)}; font-size: 10px; height: ${RADIUS}px; line-height: 10px; diff --git a/src/components/CryptoCurrencyIcon.js b/src/components/CryptoCurrencyIcon.js index 843c223a..8264a0b4 100644 --- a/src/components/CryptoCurrencyIcon.js +++ b/src/components/CryptoCurrencyIcon.js @@ -6,13 +6,14 @@ import type { CryptoCurrency } from '@ledgerhq/live-common/lib/types' type Props = { currency: CryptoCurrency, size: number, + color?: string, } class CryptoCurrencyIcon extends PureComponent { render() { - const { currency, size } = this.props + const { currency, size, color } = this.props const IconCurrency = getCryptoCurrencyIcon(currency) - return IconCurrency ? : null + return IconCurrency ? : null } } diff --git a/src/components/DeviceConnect/index.js b/src/components/DeviceConnect/index.js index c693625f..b353d607 100644 --- a/src/components/DeviceConnect/index.js +++ b/src/components/DeviceConnect/index.js @@ -111,7 +111,6 @@ const Info = styled(Box).attrs({ fontSize: 3, horizontal: true, ml: 1, - pt: 1, })` strong { font-weight: 600; @@ -310,10 +309,8 @@ class DeviceConnect extends PureComponent { {appState.fail ? ( - - - - + + {accountName ? ( {'You must use the device associated to the account '} diff --git a/src/components/SideBar/Item.js b/src/components/SideBar/Item.js index ee334406..71e48124 100644 --- a/src/components/SideBar/Item.js +++ b/src/components/SideBar/Item.js @@ -34,7 +34,7 @@ const Container = styled(Tabbable).attrs({ horizontal: true, pl: 3, })` - cursor: pointer; + cursor: ${p => (p.isActive ? 'default' : 'pointer')}; color: ${p => p.theme.colors.dark}; background: ${p => (p.isActive ? p.theme.colors.lightGrey : '')}; height: ${p => (p.big ? 50 : 36)}px; diff --git a/src/components/SideBar/index.js b/src/components/SideBar/index.js index 17f7e8ea..53b16d26 100644 --- a/src/components/SideBar/index.js +++ b/src/components/SideBar/index.js @@ -1,6 +1,6 @@ // @flow -import React, { PureComponent } from 'react' +import React, { PureComponent, Fragment } from 'react' import { compose } from 'redux' import { translate } from 'react-i18next' import styled from 'styled-components' @@ -8,7 +8,7 @@ import { connect } from 'react-redux' import { getCryptoCurrencyIcon } from '@ledgerhq/live-common/lib/react' 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' @@ -56,7 +56,6 @@ const PlusBtn = styled(Tabbable).attrs({ type Props = { t: T, - accounts: Account[], openModal: Function, updateStatus: UpdateStatus, } @@ -72,7 +71,7 @@ const mapDispatchToProps: Object = { class SideBar extends PureComponent { render() { - const { t, accounts, openModal, updateStatus } = this.props + const { t, openModal, updateStatus } = this.props return ( @@ -80,7 +79,11 @@ class SideBar extends PureComponent { {t('sidebar:menu')} - } linkTo="/" highlight={updateStatus === 'downloaded'}> + } + linkTo="/" + highlight={updateStatus === 'downloaded'} + > {t('dashboard:title')} } modal={MODAL_SEND}> @@ -101,35 +104,13 @@ class SideBar extends PureComponent { {t('sidebar:accounts')} t('addAccount:title')}> - openModal(MODAL_ADD_ACCOUNT)}> + openModal('importAccounts')}> - {accounts.map(account => { - const Icon = getCryptoCurrencyIcon(account.currency) - return ( - - } - iconActiveColor={account.currency.color} - icon={Icon ? : null} - key={account.id} - linkTo={`/account/${account.id}`} - > - {account.name} - - ) - })} + @@ -138,9 +119,37 @@ class SideBar extends PureComponent { } } +const AccountsList = connect(state => ({ + accounts: getVisibleAccounts(state), +}))(({ accounts }: { accounts: Account[] }) => ( + + {accounts.map(account => { + const Icon = getCryptoCurrencyIcon(account.currency) + return ( + + } + iconActiveColor={account.currency.color} + icon={Icon ? : null} + key={account.id} + linkTo={`/account/${account.id}`} + > + {account.name} + + ) + })} + +)) + export default compose( - connect(mapStateToProps, mapDispatchToProps, null, { - pure: false, - }), + connect(mapStateToProps, mapDispatchToProps, null, { pure: false }), translate(), )(SideBar) diff --git a/src/components/base/Box/index.js b/src/components/base/Box/index.js index dc058b2f..f961388c 100644 --- a/src/components/base/Box/index.js +++ b/src/components/base/Box/index.js @@ -49,6 +49,13 @@ const Box = styled.div` overflow-y: ${p => (p.scroll === true ? 'auto' : '')}; position: ${p => (p.relative ? 'relative' : p.sticky ? 'absolute' : '')}; + ${p => + p.selectable && + ` + user-select: text; + cursor: text; + `}; + ${p => p.sticky && ` diff --git a/src/components/base/CurrencyBadge.js b/src/components/base/CurrencyBadge.js new file mode 100644 index 00000000..0e4de748 --- /dev/null +++ b/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 ( + + {Icon && } + + ) +} + +function CurrencyBadge({ currency, ...props }: { currency: Currency }) { + return ( + + + + + {currency.ticker} + + + {currency.name} + + + + ) +} + +export default CurrencyBadge diff --git a/src/components/base/FormattedVal/__tests__/__snapshots__/FormattedVal.test.js.snap b/src/components/base/FormattedVal/__tests__/__snapshots__/FormattedVal.test.js.snap index 45d28048..6d59e715 100644 --- a/src/components/base/FormattedVal/__tests__/__snapshots__/FormattedVal.test.js.snap +++ b/src/components/base/FormattedVal/__tests__/__snapshots__/FormattedVal.test.js.snap @@ -2,7 +2,7 @@ exports[`components FormattedVal renders a formatted val 1`] = `
4 @@ -11,7 +11,7 @@ exports[`components FormattedVal renders a formatted val 1`] = ` exports[`components FormattedVal renders a percent 1`] = `
30 % @@ -20,7 +20,7 @@ exports[`components FormattedVal renders a percent 1`] = ` exports[`components FormattedVal shows code 1`] = `
BTC 4 @@ -29,7 +29,7 @@ exports[`components FormattedVal shows code 1`] = ` exports[`components FormattedVal shows sign 1`] = `
+ 4 @@ -38,7 +38,7 @@ exports[`components FormattedVal shows sign 1`] = ` exports[`components FormattedVal shows sign 2`] = `
- 4 diff --git a/src/components/base/Input/index.js b/src/components/base/Input/index.js index 906fae6e..d95f4fb1 100644 --- a/src/components/base/Input/index.js +++ b/src/components/base/Input/index.js @@ -66,9 +66,11 @@ export const Textarea = styled.textarea.attrs({ type Props = { keepEvent?: boolean, - onBlur: Function, + onBlur: (SyntheticInputEvent) => void, onChange?: Function, - onFocus: Function, + onEnter?: (SyntheticKeyboardEvent) => void, + onEsc?: (SyntheticKeyboardEvent) => void, + onFocus: (SyntheticInputEvent) => void, renderLeft?: any, renderRight?: any, containerProps?: Object, @@ -100,6 +102,21 @@ class Input extends PureComponent { } } + handleKeyDown = (e: SyntheticKeyboardEvent) => { + // 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 ! handleClick = () => this._input && this._input.focus() @@ -119,6 +136,11 @@ class Input extends PureComponent { onBlur(e) } + handleSelectEverything = () => { + this._input && this._input.setSelectionRange(0, this._input.value.length) + this._input && this._input.focus() + } + _input = null render() { @@ -142,6 +164,7 @@ class Input extends PureComponent { onFocus={this.handleFocus} onBlur={this.handleBlur} onChange={this.handleChange} + onKeyDown={this.handleKeyDown} /> {renderRight} diff --git a/src/components/base/Modal/ModalBody.js b/src/components/base/Modal/ModalBody.js index 17d9fa8b..f6056f2a 100644 --- a/src/components/base/Modal/ModalBody.js +++ b/src/components/base/Modal/ModalBody.js @@ -67,6 +67,10 @@ const CloseContainer = styled(Box).attrs({ &:hover { color: ${p => p.theme.colors.grey}; } + + &:active { + color: ${p => p.theme.colors.dark}; + } ` const Body = styled(Box).attrs({ diff --git a/src/components/base/Modal/ModalTitle.js b/src/components/base/Modal/ModalTitle.js new file mode 100644 index 00000000..2ef88f09 --- /dev/null +++ b/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 ( + + {onBack && ( + + + {t('common:back')} + + )} + {children} + + ) +} + +export default translate()(ModalTitle) diff --git a/src/components/base/Modal/index.js b/src/components/base/Modal/index.js index 950970b4..26511ea1 100644 --- a/src/components/base/Modal/index.js +++ b/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 ConfirmModal } from './ConfirmModal' +export { default as ModalTitle } from './ModalTitle' const springConfig = { stiffness: 320, @@ -163,6 +164,7 @@ export class Modal extends Component { isOpened={isOpened} onClose={onClose} onHide={onHide} + closeOnEsc={!preventBackdropClick} motionStyle={(spring, isVisible) => ({ opacity: spring(isVisible ? 1 : 0, springConfig), scale: spring(isVisible ? 1 : 0.95, springConfig), @@ -188,16 +190,6 @@ export class Modal extends Component { } } -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({ px: 5, py: 3, diff --git a/src/components/base/Radio/index.js b/src/components/base/Radio/index.js index d6c9584b..4d52f722 100644 --- a/src/components/base/Radio/index.js +++ b/src/components/base/Radio/index.js @@ -7,15 +7,16 @@ import { Tabbable } from 'components/base/Box' const Base = styled(Tabbable).attrs({ relative: true })` 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%; height: 19px; width: 19px; transition: all ease-in-out 0.1s; + background-color: white; &:focus { 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, @@ -65,8 +66,4 @@ function Radio(props: Props) { return onChange && onChange(!isChecked)} /> } -Radio.defaultProps = { - onChange: null, -} - export default Radio diff --git a/src/components/base/Spinner.js b/src/components/base/Spinner.js new file mode 100644 index 00000000..6d3cec98 --- /dev/null +++ b/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 ( + + + + ) +} diff --git a/src/components/modals/AddAccount/01-step-currency.js b/src/components/modals/AddAccount/01-step-currency.js deleted file mode 100644 index e038db07..00000000 --- a/src/components/modals/AddAccount/01-step-currency.js +++ /dev/null @@ -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) => ( - - - - -) diff --git a/src/components/modals/AddAccount/03-step-import.js b/src/components/modals/AddAccount/03-step-import.js deleted file mode 100644 index 8821aaed..00000000 --- a/src/components/modals/AddAccount/03-step-import.js +++ /dev/null @@ -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 ( - - (design is not yet integrated!) - {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 ( - onToggleAccount(account) : undefined} - > - - - {account.name} - - - - ) - })} - - ) -} - -export default StepImport diff --git a/src/components/modals/AddAccount/index.js b/src/components/modals/AddAccount/index.js deleted file mode 100644 index 2799a0e9..00000000 --- a/src/components/modals/AddAccount/index.js +++ /dev/null @@ -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 { - 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 - } - - 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 + + ) +} + +export default StepChooseCurrency diff --git a/src/components/modals/ImportAccounts/steps/02-step-connect-device.js b/src/components/modals/ImportAccounts/steps/02-step-connect-device.js new file mode 100644 index 00000000..89ee79d2 --- /dev/null +++ b/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 ( + + + + + + {`You're about to import your `} + {`${currency.name} (${ + currency.ticker + })`} + {` account(s) from your Ledger device. Please follow the steps below:`} + + + + { + if (s === 'connected') { + setState({ isAppOpened: true }) + } + }} + /> + + ) +} + +export function StepConnectDeviceFooter({ t, transitionTo, isAppOpened }: StepProps) { + return ( + + ) +} + +export default StepConnectDevice diff --git a/src/components/modals/ImportAccounts/steps/03-step-import.js b/src/components/modals/ImportAccounts/steps/03-step-import.js new file mode 100644 index 00000000..a77e3dbe --- /dev/null +++ b/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 { + 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 ( + + {err && {err.message}} + + + {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 ( + + ) + })} + {scanStatus === 'scanning' && ( + + + + )} + + + + {['error', 'finished'].includes(scanStatus) && ( + + )} + + + ) + } +} + +export default StepImport + +export const StepImportFooter = ({ scanStatus, onClickImport, checkedAccountsIds }: StepProps) => ( + +) diff --git a/src/components/modals/ImportAccounts/steps/04-step-finish.js b/src/components/modals/ImportAccounts/steps/04-step-finish.js new file mode 100644 index 00000000..71eb7758 --- /dev/null +++ b/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 ( + + + + + {'Great success!'} + + + ) +} + +export default StepFinish diff --git a/src/components/modals/StepConnectDevice.js b/src/components/modals/StepConnectDevice.js index b36d6740..99a61b00 100644 --- a/src/components/modals/StepConnectDevice.js +++ b/src/components/modals/StepConnectDevice.js @@ -13,7 +13,7 @@ type Props = { account?: ?Account, currency?: ?CryptoCurrency, deviceSelected?: ?Device, - onChangeDevice: Device => void, + onChangeDevice?: Device => void, onStatusChange: string => void, } diff --git a/src/components/modals/index.js b/src/components/modals/index.js index 5f286eef..c44a197b 100644 --- a/src/components/modals/index.js +++ b/src/components/modals/index.js @@ -1,5 +1,5 @@ export Debug from './Debug' -export AddAccount from './AddAccount' +export ImportAccounts from './ImportAccounts' export OperationDetails from './OperationDetails' export Receive from './Receive' export Send from './Send' diff --git a/src/helpers/libcore.js b/src/helpers/libcore.js index 1db3b08f..ac42c1e0 100644 --- a/src/helpers/libcore.js +++ b/src/helpers/libcore.js @@ -136,7 +136,8 @@ async function scanNextAccount(props: { ? await wallet.getAccount(accountIndex) : 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) } @@ -214,16 +215,6 @@ async function buildAccountRaw({ // $FlowFixMe ops: NJSOperation[], }): Promise { - /* - const balanceByDay = ops.length - ? await getBalanceByDaySinceOperation({ - njsAccount, - njsOperation: ops[0], - core, - }) - : {} - */ - const njsBalance = await njsAccount.getBalance() const balance = njsBalance.toLong() @@ -269,7 +260,7 @@ async function buildAccountRaw({ freshAddressPath, balance, blockHeight, - archived: true, + archived: false, index: accountIndex, operations, pendingOperations: [], @@ -318,48 +309,3 @@ function buildOperationRaw({ 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() - ) -} -*/ diff --git a/src/icons/Home.js b/src/icons/Home.js index 72768838..4850aa6a 100644 --- a/src/icons/Home.js +++ b/src/icons/Home.js @@ -3,7 +3,7 @@ import React from 'react' const path = ( - + ) export default ({ size, ...p }: { size: number }) => ( diff --git a/src/icons/Loader.js b/src/icons/Loader.js index eb63088e..5c205762 100644 --- a/src/icons/Loader.js +++ b/src/icons/Loader.js @@ -3,7 +3,10 @@ import React from 'react' const path = ( - + ) export default ({ size, ...p }: { size: number }) => ( diff --git a/static/i18n/en/common.yml b/static/i18n/en/common.yml index 8a75b33d..d158d1fe 100644 --- a/static/i18n/en/common.yml +++ b/static/i18n/en/common.yml @@ -8,7 +8,7 @@ continue: Continue chooseWalletPlaceholder: Choose a wallet... currency: Currency selectAccount: Select an account -selectCurrency: Select an currency +selectCurrency: Select a currency sortBy: Sort by search: Search save: Save diff --git a/static/i18n/en/importAccounts.yml b/static/i18n/en/importAccounts.yml new file mode 100644 index 00000000..9e44b2a8 --- /dev/null +++ b/static/i18n/en/importAccounts.yml @@ -0,0 +1,6 @@ +title: Import accounts +breadcrumb: + informations: Informations + connectDevice: Connect device + import: Import + finish: End