Browse Source

Integrate the first three steps of import accounts modal

master
meriadec 7 years ago
parent
commit
2d07cda838
No known key found for this signature in database GPG Key ID: 1D2FC2305E2CB399
  1. 2
      src/components/Breadcrumb/Step.js
  2. 5
      src/components/CryptoCurrencyIcon.js
  3. 17
      src/components/base/Input/index.js
  4. 5
      src/components/base/Radio/index.js
  5. 30
      src/components/base/Spinner.js
  6. 159
      src/components/modals/ImportAccounts/AccountRow.js
  7. 8
      src/components/modals/ImportAccounts/index.js
  8. 105
      src/components/modals/ImportAccounts/steps/03-step-import.js
  9. 5
      src/icons/Loader.js

2
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',

5
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<Props> {
render() {
const { currency, size } = this.props
const { currency, size, color } = this.props
const IconCurrency = getCryptoCurrencyIcon(currency)
return IconCurrency ? <IconCurrency size={size} /> : null
return IconCurrency ? <IconCurrency size={size} color={color} /> : null
}
}

17
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<Props, State> {
}
}
handleKeyDown = (e: SyntheticInputEvent<HTMLInputElement>) => {
// 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<Props, State> {
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<Props, State> {
onFocus={this.handleFocus}
onBlur={this.handleBlur}
onChange={this.handleChange}
onKeyDown={this.handleKeyDown}
/>
</Box>
{renderRight}

5
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,

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>
)
}

159
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<Props, State> {
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 (
<AccountRowContainer onClick={() => onClick(account)}>
<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}
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} />
</AccountRowContainer>
)
}
}
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;
`

8
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',

105
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<StepProps, State> {
@ -36,6 +42,9 @@ class StepImport extends PureComponent<StepProps, State> {
componentWillUnmount() {
console.log(`stopping import...`)
if (this.scanSubscription) {
this.scanSubscription.unsubscribe()
}
}
startScanAccountsDevice() {
@ -66,42 +75,90 @@ class StepImport extends PureComponent<StepProps, State> {
}
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 (
<Box>
{status === 'scanning' && <Box>{'Scanning in progress...'}</Box>}
{status === 'finished' && <Box>{'Finished'}</Box>}
{['error', 'finished'].includes(status) && (
<Button outline onClick={this.handleRetry}>
{'retry'}
</Button>
)}
{err && <Box shrink>{err.toString()}</Box>}
<AccountsList>
{scannedAccounts.map(account => <AccountRow key={account.id} account={account} />)}
</AccountsList>
<Box flow={2}>
{scannedAccounts.map(account => {
const isChecked = checkedAccountsIds.find(id => id === account.id) !== undefined
return (
<AccountRow
key={account.id}
account={account}
isChecked={isChecked}
onClick={this.handleToggleAccount}
onAccountUpdate={this.handleAccountUpdate}
/>
)
})}
{status === '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(status) && (
<Button small outline onClick={this.handleRetry}>
<Box horizontal flow={2} align="center">
<IconExchange size={13} />
<span>{'retry'}</span>
</Box>
</Button>
)}
</Box>
</Box>
)
}
}
const AccountsList = styled(Box).attrs({
flow: 2,
})``
const AccountRowContainer = styled(Box).attrs({
horizontal: true,
})``
const AccountRow = ({ account }: { account: Account }) => (
<AccountRowContainer>{account.name}</AccountRowContainer>
)
export default StepImport
export const StepImportFooter = (props: StepProps) => {
return (
<div>noetuhnoethunot</div>
)
}

5
src/icons/Loader.js

@ -3,7 +3,10 @@
import React from 'react'
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 }) => (

Loading…
Cancel
Save