From 2d07cda83803f365ad8b1e9a11dc797916fcd9d7 Mon Sep 17 00:00:00 2001 From: meriadec Date: Tue, 29 May 2018 11:44:58 +0200 Subject: [PATCH] Integrate the first three steps of import accounts modal --- src/components/Breadcrumb/Step.js | 2 +- src/components/CryptoCurrencyIcon.js | 5 +- src/components/base/Input/index.js | 17 ++ src/components/base/Radio/index.js | 5 +- src/components/base/Spinner.js | 30 ++++ .../modals/ImportAccounts/AccountRow.js | 159 ++++++++++++++++++ src/components/modals/ImportAccounts/index.js | 8 +- .../ImportAccounts/steps/03-step-import.js | 105 +++++++++--- src/icons/Loader.js | 5 +- 9 files changed, 303 insertions(+), 33 deletions(-) create mode 100644 src/components/base/Spinner.js create mode 100644 src/components/modals/ImportAccounts/AccountRow.js diff --git a/src/components/Breadcrumb/Step.js b/src/components/Breadcrumb/Step.js index ad0b7126..9460b122 100644 --- a/src/components/Breadcrumb/Step.js +++ b/src/components/Breadcrumb/Step.js @@ -29,7 +29,7 @@ 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', diff --git a/src/components/CryptoCurrencyIcon.js b/src/components/CryptoCurrencyIcon.js index 843c223a..192c8b64 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/base/Input/index.js b/src/components/base/Input/index.js index 906fae6e..e246ce10 100644 --- a/src/components/base/Input/index.js +++ b/src/components/base/Input/index.js @@ -68,6 +68,7 @@ type Props = { keepEvent?: boolean, onBlur: Function, onChange?: Function, + onEnter?: Function, onFocus: Function, renderLeft?: any, renderRight?: any, @@ -100,6 +101,16 @@ class Input extends PureComponent { } } + handleKeyDown = (e: SyntheticInputEvent) => { + // handle enter key + if (e.which === 13) { + const { onEnter } = this.props + if (onEnter) { + onEnter(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 +130,11 @@ class Input extends PureComponent { onBlur(e) } + handleSelectEverything = () => { + this._input.setSelectionRange(0, this._input.value.length) + this._input.focus() + } + _input = null render() { @@ -142,6 +158,7 @@ class Input extends PureComponent { onFocus={this.handleFocus} onBlur={this.handleBlur} onChange={this.handleChange} + onKeyDown={this.handleKeyDown} /> {renderRight} diff --git a/src/components/base/Radio/index.js b/src/components/base/Radio/index.js index d6c9584b..c63c8a09 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, 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/ImportAccounts/AccountRow.js b/src/components/modals/ImportAccounts/AccountRow.js new file mode 100644 index 00000000..b68dbd97 --- /dev/null +++ b/src/components/modals/ImportAccounts/AccountRow.js @@ -0,0 +1,159 @@ +// @flow + +import React, { PureComponent } from 'react' +import styled from 'styled-components' + +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, + onClick: Account => void, + onAccountUpdate: Account => void, +} + +type State = { + isEditing: boolean, + accountNameCopy: string, +} + +export default class AccountRow extends PureComponent { + state = { + isEditing: false, + accountNameCopy: '', + } + + componentDidUpdate(prevProps, prevState) { + const startedEditing = !prevState.isEditing && this.state.isEditing + if (startedEditing) { + this._input.handleSelectEverything() + } + } + + handleEditClick = e => { + this.handlePreventSubmit(e) + const { account } = this.props + this.setState({ isEditing: true, accountNameCopy: account.name }) + } + + handleSubmitName = e => { + 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 => { + // prevent account row to be submitted + e.preventDefault() + e.stopPropagation() + } + + handleChangeName = accountNameCopy => this.setState({ accountNameCopy }) + + _input = null + + render() { + const { account, isChecked, onClick } = this.props + const { isEditing, accountNameCopy } = this.state + + return ( + onClick(account)}> + + + {isEditing ? ( + + + + } + ref={input => (this._input = input)} + /> + ) : ( +
{account.name}
+ )} +
+ {!isEditing && ( + + + {'edit name'} + + )} + + +
+ ) + } +} + +const AccountRowContainer = styled(Box).attrs({ + horizontal: true, + align: 'center', + bg: 'lightGrey', + px: 3, + flow: 3, +})` + height: 48px; + border-radius: 4px; + cursor: pointer; + + &:hover { + 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; +` diff --git a/src/components/modals/ImportAccounts/index.js b/src/components/modals/ImportAccounts/index.js index ffd78eb8..f8267c2c 100644 --- a/src/components/modals/ImportAccounts/index.js +++ b/src/components/modals/ImportAccounts/index.js @@ -17,9 +17,11 @@ 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 from './steps/03-step-import' +import StepImport, { StepImportFooter } from './steps/03-step-import' import StepFinish from './steps/04-step-finish' +const { getCryptoCurrencyById } = require('@ledgerhq/live-common/lib/helpers/currencies') + const createSteps = ({ t }: { t: T }) => [ { id: 'chooseCurrency', @@ -41,9 +43,9 @@ const createSteps = ({ t }: { t: T }) => [ id: 'import', label: t('importAccounts:breadcrumb.import'), component: StepImport, - footer: null, + footer: StepImportFooter, onBack: ({ transitionTo }: StepProps) => transitionTo('chooseCurrency'), - hideFooter: true, + hideFooter: false, }, { id: 'finish', diff --git a/src/components/modals/ImportAccounts/steps/03-step-import.js b/src/components/modals/ImportAccounts/steps/03-step-import.js index 086bb5f5..5777b48c 100644 --- a/src/components/modals/ImportAccounts/steps/03-step-import.js +++ b/src/components/modals/ImportAccounts/steps/03-step-import.js @@ -1,7 +1,7 @@ // @flow import React, { PureComponent } from 'react' -import styled from 'styled-components' +import keyBy from 'lodash/keyBy' import type { Account } from '@ledgerhq/live-common/lib/types' @@ -9,6 +9,10 @@ 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' @@ -18,12 +22,14 @@ type State = { status: Status, err: ?Error, scannedAccounts: Account[], + checkedAccountsIds: string[], } const INITIAL_STATE = { status: 'scanning', err: null, scannedAccounts: [], + checkedAccountsIds: [], } class StepImport extends PureComponent { @@ -36,6 +42,9 @@ class StepImport extends PureComponent { componentWillUnmount() { console.log(`stopping import...`) + if (this.scanSubscription) { + this.scanSubscription.unsubscribe() + } } startScanAccountsDevice() { @@ -66,42 +75,90 @@ class StepImport extends PureComponent { } handleRetry = () => { + if (this.scanSubscription) { + this.scanSubscription.unsubscribe() + this.scanSubscription = null + } this.setState(INITIAL_STATE) this.startScanAccountsDevice() } + handleToggleAccount = account => { + const { checkedAccountsIds } = this.state + const isChecked = checkedAccountsIds.find(id => id === account.id) !== undefined + if (isChecked) { + this.setState({ checkedAccountsIds: checkedAccountsIds.filter(id => id !== account.id) }) + } else { + this.setState({ checkedAccountsIds: [...checkedAccountsIds, account.id] }) + } + } + + handleAccountUpdate = updatedAccount => { + const { scannedAccounts } = this.state + this.setState({ + scannedAccounts: scannedAccounts.map(account => { + if (account.id !== updatedAccount.id) { + return account + } + return updatedAccount + }), + }) + } + render() { - const { status, err, scannedAccounts } = this.state + const { status, err, scannedAccounts, checkedAccountsIds } = this.state return ( - {status === 'scanning' && {'Scanning in progress...'}} - {status === 'finished' && {'Finished'}} - {['error', 'finished'].includes(status) && ( - - )} {err && {err.toString()}} - - {scannedAccounts.map(account => )} - + + {scannedAccounts.map(account => { + const isChecked = checkedAccountsIds.find(id => id === account.id) !== undefined + return ( + + ) + })} + {status === 'scanning' && ( + + + + )} + + + + {['error', 'finished'].includes(status) && ( + + )} + ) } } -const AccountsList = styled(Box).attrs({ - flow: 2, -})`` - -const AccountRowContainer = styled(Box).attrs({ - horizontal: true, -})`` - -const AccountRow = ({ account }: { account: Account }) => ( - {account.name} -) - export default StepImport + +export const StepImportFooter = (props: StepProps) => { + return ( +
noetuhnoethunot
+ ) +} 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 }) => (