Browse Source

Refactor and clean ReceiveModal to use Stepper

master
meriadec 7 years ago
parent
commit
1ddbeccce6
No known key found for this signature in database GPG Key ID: 1D2FC2305E2CB399
  1. 23
      src/components/modals/Receive/01-step-account.js
  2. 57
      src/components/modals/Receive/03-step-confirm-address.js
  3. 46
      src/components/modals/Receive/04-step-receive-funds.js
  4. 449
      src/components/modals/Receive/index.js
  5. 27
      src/components/modals/Receive/steps/01-step-account.js
  6. 42
      src/components/modals/Receive/steps/02-step-connect-device.js
  7. 96
      src/components/modals/Receive/steps/03-step-confirm-address.js
  8. 66
      src/components/modals/Receive/steps/04-step-receive-funds.js

23
src/components/modals/Receive/01-step-account.js

@ -1,23 +0,0 @@
// @flow
import React from 'react'
import type { Account } 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 SelectAccount from 'components/SelectAccount'
type Props = {
account: ?Account,
onChangeAccount: Function,
t: T,
}
export default (props: Props) => (
<Box flow={1}>
<Label>{props.t('app:receive.steps.chooseAccount.label')}</Label>
<SelectAccount autoFocus onChange={props.onChangeAccount} value={props.account} />
</Box>
)

57
src/components/modals/Receive/03-step-confirm-address.js

@ -1,57 +0,0 @@
// @flow
import React, { Fragment } from 'react'
import styled from 'styled-components'
import type { Account } from '@ledgerhq/live-common/lib/types'
import type { Device, T } from 'types/common'
import Box from 'components/base/Box'
import CurrentAddressForAccount from 'components/CurrentAddressForAccount'
import DeviceConfirm from 'components/DeviceConfirm'
const Container = styled(Box).attrs({
alignItems: 'center',
fontSize: 4,
color: 'dark',
px: 7,
})``
const Title = styled(Box).attrs({
ff: 'Museo Sans|Regular',
fontSize: 6,
mb: 1,
})``
const Text = styled(Box).attrs({
color: 'smoke',
})`
text-align: center;
`
type Props = {
account: ?Account,
addressVerified: ?boolean,
device: ?Device,
t: T,
}
export default (props: Props) => (
<Container>
{props.addressVerified === false ? (
<Fragment>
<Title>{props.t('app:receive.steps.confirmAddress.error.title')}</Title>
<Text mb={5}>{props.t('app:receive.steps.confirmAddress.error.text')}</Text>
<DeviceConfirm error />
</Fragment>
) : (
<Fragment>
<Title>{props.t('app:receive.steps.confirmAddress.action')}</Title>
<Text>{props.t('app:receive.steps.confirmAddress.text')}</Text>
{props.account && <CurrentAddressForAccount account={props.account} />}
{props.device &&
props.account && <DeviceConfirm mb={2} mt={-1} error={props.addressVerified === false} />}
</Fragment>
)}
</Container>
)

46
src/components/modals/Receive/04-step-receive-funds.js

@ -1,46 +0,0 @@
// @flow
import React from 'react'
import type { Account } from '@ledgerhq/live-common/lib/types'
import type { T } from 'types/common'
import Box from 'components/base/Box'
import CurrentAddressForAccount from 'components/CurrentAddressForAccount'
import Label from 'components/base/Label'
import RequestAmount from 'components/RequestAmount'
type Props = {
account: ?Account,
addressVerified: ?boolean,
amount: string | number,
onChangeAmount: Function,
onVerify: Function,
t: T,
}
export default (props: Props) => (
<Box flow={5}>
<Box flow={1}>
<Label>{props.t('app:receive.steps.receiveFunds.label')}</Label>
<RequestAmount
account={props.account}
onChange={props.onChangeAmount}
value={props.amount}
withMax={false}
/>
</Box>
{props.account && (
<CurrentAddressForAccount
account={props.account}
addressVerified={props.addressVerified}
amount={props.amount}
onVerify={props.onVerify}
withBadge
withFooter
withQRCode
withVerify={props.addressVerified === false}
/>
)}
</Box>
)

449
src/components/modals/Receive/index.js

@ -1,192 +1,111 @@
// @flow
import React, { Fragment, PureComponent } from 'react'
import React, { PureComponent } from 'react'
import { compose } from 'redux'
import { connect } from 'react-redux'
import { translate } from 'react-i18next'
import { createStructuredSelector } from 'reselect'
import { accountsSelector } from 'reducers/accounts'
import SyncSkipUnderPriority from 'components/SyncSkipUnderPriority'
import type { Account } from '@ledgerhq/live-common/lib/types'
import type { T, Device } from 'types/common'
import { MODAL_RECEIVE } from 'config/constants'
import { isSegwitAccount } from 'helpers/bip32'
import type { T, Device } from 'types/common'
import type { StepProps as DefaultStepProps } from 'components/base/Stepper'
import getAddress from 'commands/getAddress'
import SyncSkipUnderPriority from 'components/SyncSkipUnderPriority'
import SyncOneAccountOnMount from 'components/SyncOneAccountOnMount'
import { getCurrentDevice } from 'reducers/devices'
import { accountsSelector } from 'reducers/accounts'
import { closeModal } from 'reducers/modals'
import Box from 'components/base/Box'
import Breadcrumb from 'components/Breadcrumb'
import Button from 'components/base/Button'
import Modal, { ModalBody, ModalTitle, ModalContent, ModalFooter } from 'components/base/Modal'
import StepConnectDevice from 'components/modals/StepConnectDevice'
import { WrongDeviceForAccount } from 'components/EnsureDeviceApp'
import Modal from 'components/base/Modal'
import Stepper from 'components/base/Stepper'
import StepAccount from './01-step-account'
import StepConfirmAddress from './03-step-confirm-address'
import StepReceiveFunds from './04-step-receive-funds'
import StepAccount, { StepAccountFooter } from './steps/01-step-account'
import StepConnectDevice, { StepConnectDeviceFooter } from './steps/02-step-connect-device'
import StepConfirmAddress, { StepConfirmAddressFooter } from './steps/03-step-confirm-address'
import StepReceiveFunds, { StepReceiveFundsFooter } from './steps/04-step-receive-funds'
type Props = {
t: T,
device: ?Device,
accounts: Account[],
closeModal: string => void,
}
type State = {
account: Account | null,
addressVerified: null | boolean,
amount: string | number,
appStatus: null | string,
deviceSelected: Device | null,
stepIndex: number,
stepsDisabled: Array<number>,
stepsErrors: Array<number>,
stepId: string,
account: ?Account,
isAppOpened: boolean,
isAddressVerified: ?boolean,
disabledSteps: number[],
errorSteps: number[],
}
const GET_STEPS = t => [
{ label: t('app:receive.steps.chooseAccount.title'), Comp: StepAccount },
{ label: t('app:receive.steps.connectDevice.title'), Comp: StepConnectDevice },
{ label: t('app:receive.steps.confirmAddress.title'), Comp: StepConfirmAddress },
{ label: t('app:receive.steps.receiveFunds.title'), Comp: StepReceiveFunds },
]
const INITIAL_STATE = {
account: null,
addressVerified: null,
amount: '',
appStatus: null,
deviceSelected: null,
stepIndex: 0,
stepsDisabled: [],
stepsErrors: [],
// FIXME the two above can be derivated from other info (if we keep error etc)
// we can get rid of it after a big refactoring (see how done in Send)
export type StepProps = DefaultStepProps & {
device: ?Device,
account: ?Account,
closeModal: void => void,
isAppOpened: boolean,
isAddressVerified: ?boolean,
onSkipConfirm: void => void,
onResetSkip: void => void,
onChangeAccount: (?Account) => void,
onChangeAppOpened: boolean => void,
onChangeAddressVerified: boolean => void,
}
const createSteps = ({ t }: { t: T }) => [
{
id: 'account',
label: t('app:receive.steps.chooseAccount.title'),
component: StepAccount,
footer: StepAccountFooter,
},
{
id: 'device',
label: t('app:receive.steps.connectDevice.title'),
component: StepConnectDevice,
footer: StepConnectDeviceFooter,
onBack: ({ transitionTo }: StepProps) => transitionTo('account'),
},
{
id: 'confirm',
label: t('app:receive.steps.confirmAddress.title'),
component: StepConfirmAddress,
footer: StepConfirmAddressFooter,
shouldRenderFooter: ({ isAddressVerified }: StepProps) => isAddressVerified === false,
shouldPreventClose: ({ isAddressVerified }: StepProps) => isAddressVerified === null,
},
{
id: 'receive',
label: t('app:receive.steps.receiveFunds.title'),
component: StepReceiveFunds,
footer: StepReceiveFundsFooter,
},
]
const mapStateToProps = createStructuredSelector({
device: getCurrentDevice,
accounts: accountsSelector,
})
class ReceiveModal extends PureComponent<Props, State> {
state = INITIAL_STATE
_steps = GET_STEPS(this.props.t)
canNext = () => {
const { account, stepIndex } = this.state
if (stepIndex === 0) {
return account !== null
}
if (stepIndex === 1) {
const { deviceSelected, appStatus } = this.state
return deviceSelected !== null && appStatus === 'success'
}
return false
}
canClose = () => {
const { stepIndex, addressVerified } = this.state
if (stepIndex === 2) {
return addressVerified === false
}
return true
}
canPrev = () => {
const { addressVerified, stepIndex } = this.state
if (stepIndex === 1) {
return true
}
if (stepIndex === 2) {
return addressVerified === false
}
if (stepIndex === 3) {
return true
}
return false
}
handleReset = () => this.setState(INITIAL_STATE)
handleNextStep = () => {
const { stepIndex } = this.state
if (stepIndex >= this._steps.length - 1) {
return
}
this.setState({ stepIndex: stepIndex + 1 })
// TODO: do that better
if (stepIndex === 1) {
this.verifyAddress()
}
}
handlePrevStep = () => {
const { stepIndex } = this.state
let newStepIndex
switch (stepIndex) {
default:
case 1:
newStepIndex = 0
break
case 2:
case 3:
newStepIndex = 1
break
}
this.setState({
addressVerified: null,
appStatus: null,
deviceSelected: null,
stepIndex: newStepIndex,
stepsDisabled: [],
stepsErrors: [],
})
}
handleChangeDevice = d => this.setState({ deviceSelected: d })
handleChangeAccount = account => this.setState({ account })
handleChangeStatus = (deviceStatus, appStatus) => this.setState({ appStatus })
handleCheckAddress = isVerified => {
this.setState({
addressVerified: isVerified,
stepsErrors: isVerified === false ? [2] : [],
})
if (isVerified === true) {
this.handleNextStep()
}
}
handleRetryCheckAddress = () => {
this.setState({
addressVerified: null,
stepsErrors: [],
})
const mapDispatchToProps = {
closeModal,
}
// TODO: do that better
this.verifyAddress()
}
const INITIAL_STATE = {
stepId: 'account',
account: null,
isAppOpened: false,
isAddressVerified: null,
disabledSteps: [],
errorSteps: [],
}
handleChangeAmount = amount => this.setState({ amount })
class ReceiveModal extends PureComponent<Props, State> {
state = INITIAL_STATE
STEPS = createSteps({ t: this.props.t })
handleBeforeOpenModal = ({ data }) => {
const { account } = this.state
@ -194,174 +113,87 @@ class ReceiveModal extends PureComponent<Props, State> {
if (!account) {
if (data && data.account) {
this.setState({
account: data.account,
stepIndex: 1,
})
this.setState({ account: data.account })
} else {
this.setState({
account: accounts[0],
})
this.setState({ account: accounts[0] })
}
}
}
handleSkipStep = () =>
this.setState({
addressVerified: false,
stepsErrors: [],
stepsDisabled: [1, 2],
stepIndex: this._steps.length - 1, // last step
})
verifyAddress = async () => {
const { account, deviceSelected: device } = this.state
try {
if (account && device) {
const { address } = await getAddress
.send({
currencyId: account.currency.id,
devicePath: device.path,
path: account.freshAddressPath,
segwit: isSegwitAccount(account),
verify: true,
})
.toPromise()
if (address !== account.freshAddress) {
throw new WrongDeviceForAccount(`WrongDeviceForAccount ${account.name}`, {
accountName: account.name,
})
}
this.handleCheckAddress(true)
} else {
this.handleCheckAddress(false)
handleReset = () => this.setState({ ...INITIAL_STATE })
handleCloseModal = () => this.props.closeModal(MODAL_RECEIVE)
handleStepChange = step => this.setState({ stepId: step.id })
handleChangeAccount = (account: ?Account) => this.setState({ account })
handleChangeAppOpened = (isAppOpened: boolean) => this.setState({ isAppOpened })
handleChangeAddressVerified = (isAddressVerified: boolean) => {
if (isAddressVerified) {
this.setState({ isAddressVerified })
} else {
const confirmStepIndex = this.STEPS.findIndex(step => step.id === 'confirm')
if (confirmStepIndex > -1) {
this.setState({
isAddressVerified,
errorSteps: [confirmStepIndex],
})
}
} catch (err) {
this.handleCheckAddress(false)
}
}
renderStep = () => {
const { account, amount, addressVerified, deviceSelected, stepIndex } = this.state
const { t } = this.props
const step = this._steps[stepIndex]
if (!step) {
return null
}
const { Comp } = step
const props = (predicate, props) => (predicate ? props : {})
const stepProps = {
t,
account,
...props(stepIndex === 0, {
onChangeAccount: this.handleChangeAccount,
}),
...props(stepIndex === 1, {
accountName: account ? account.name : undefined,
deviceSelected,
onChangeDevice: this.handleChangeDevice,
onStatusChange: this.handleChangeStatus,
}),
...props(stepIndex === 2, {
addressVerified,
onCheck: this.handleCheckAddress,
device: deviceSelected,
}),
...props(stepIndex === 3, {
addressVerified,
amount,
onChangeAmount: this.handleChangeAmount,
onVerify: this.handlePrevStep,
}),
handleResetSkip = () => this.setState({ disabledSteps: [] })
handleSkipConfirm = () => {
const connectStepIndex = this.STEPS.findIndex(step => step.id === 'device')
const confirmStepIndex = this.STEPS.findIndex(step => step.id === 'confirm')
if (confirmStepIndex > -1 && connectStepIndex > -1) {
this.setState({ disabledSteps: [connectStepIndex, confirmStepIndex] })
}
return <Comp {...stepProps} />
}
renderButton = () => {
const { t } = this.props
const { stepIndex, addressVerified } = this.state
let onClick
let props
switch (stepIndex) {
case 2:
props = {
primary: true,
onClick: this.handleRetryCheckAddress,
children: t('app:common.retry'),
}
break
default:
onClick = this.handleNextStep
props = {
primary: true,
disabled: !this.canNext(),
onClick,
children: t('app:common.next'),
}
}
return (
<Fragment>
{stepIndex === 1 && (
<Button onClick={this.handleSkipStep} fontSize={4}>
{t('app:receive.steps.connectDevice.withoutDevice')}
</Button>
)}
{stepIndex === 2 &&
addressVerified === false && (
<Button fontSize={4}>{t('app:receive.steps.confirmAddress.support')}</Button>
)}
<Button {...props} />
</Fragment>
)
}
render() {
const { t } = this.props
const { stepsErrors, stepsDisabled, stepIndex, account } = this.state
const { t, device } = this.props
const {
stepId,
account,
isAppOpened,
isAddressVerified,
disabledSteps,
errorSteps,
} = this.state
const addtionnalProps = {
device,
account,
isAppOpened,
isAddressVerified,
closeModal: this.handleCloseModal,
onSkipConfirm: this.handleSkipConfirm,
onResetSkip: this.handleResetSkip,
onChangeAccount: this.handleChangeAccount,
onChangeAppOpened: this.handleChangeAppOpened,
onChangeAddressVerified: this.handleChangeAddressVerified,
}
const canClose = this.canClose()
const canPrev = this.canPrev()
const isModalLocked = stepId === 'confirm' && isAddressVerified === null
return (
<Modal
name={MODAL_RECEIVE}
onBeforeOpen={this.handleBeforeOpenModal}
refocusWhenChange={stepId}
onHide={this.handleReset}
preventBackdropClick={!canClose}
preventBackdropClick={isModalLocked}
onBeforeOpen={this.handleBeforeOpenModal}
render={({ onClose }) => (
<ModalBody onClose={canClose ? onClose : undefined}>
<SyncSkipUnderPriority priority={9} />
{account && <SyncOneAccountOnMount priority={10} accountId={account.id} />}
<ModalTitle onBack={canPrev ? this.handlePrevStep : undefined}>
{t('app:receive.title')}
</ModalTitle>
<ModalContent>
<Breadcrumb
mb={6}
currentStep={stepIndex}
stepsErrors={stepsErrors}
stepsDisabled={stepsDisabled}
items={this._steps}
/>
{this.renderStep()}
</ModalContent>
{stepIndex !== 3 &&
canClose && (
<ModalFooter>
<Box horizontal alignItems="center" justifyContent="flex-end" flow={2}>
{this.renderButton()}
</Box>
</ModalFooter>
)}
</ModalBody>
<Stepper
title={t('app:receive.title')}
initialStepId={stepId}
onStepChange={this.handleStepChange}
onClose={onClose}
steps={this.STEPS}
disabledSteps={disabledSteps}
errorSteps={errorSteps}
{...addtionnalProps}
>
<SyncSkipUnderPriority priority={100} />
</Stepper>
)}
/>
)
@ -369,6 +201,9 @@ class ReceiveModal extends PureComponent<Props, State> {
}
export default compose(
connect(mapStateToProps),
connect(
mapStateToProps,
mapDispatchToProps,
),
translate(),
)(ReceiveModal)

27
src/components/modals/Receive/steps/01-step-account.js

@ -0,0 +1,27 @@
// @flow
import React from 'react'
import Box from 'components/base/Box'
import Label from 'components/base/Label'
import Button from 'components/base/Button'
import SelectAccount from 'components/SelectAccount'
import type { StepProps } from '../index'
export default function StepAccount({ t, account, onChangeAccount }: StepProps) {
return (
<Box flow={1}>
<Label>{t('app:receive.steps.chooseAccount.label')}</Label>
<SelectAccount autoFocus onChange={onChangeAccount} value={account} />
</Box>
)
}
export function StepAccountFooter({ t, transitionTo, account }: StepProps) {
return (
<Button disabled={!account} primary onClick={() => transitionTo('device')}>
{t('app:common.next')}
</Button>
)
}

42
src/components/modals/Receive/steps/02-step-connect-device.js

@ -0,0 +1,42 @@
// @flow
import React from 'react'
import Box from 'components/base/Box'
import Button from 'components/base/Button'
import EnsureDeviceAppInteraction from 'components/EnsureDeviceAppInteraction'
import type { StepProps } from '../index'
export default function StepConnectDevice({ account, onChangeAppOpened }: StepProps) {
return (
<EnsureDeviceAppInteraction
account={account}
waitBeforeSuccess={200}
onSuccess={() => onChangeAppOpened(true)}
/>
)
}
export function StepConnectDeviceFooter({
t,
transitionTo,
isAppOpened,
onSkipConfirm,
}: StepProps) {
return (
<Box horizontal flow={2}>
<Button
onClick={() => {
onSkipConfirm()
transitionTo('receive')
}}
>
{t('app:receive.steps.connectDevice.withoutDevice')}
</Button>
<Button disabled={!isAppOpened} primary onClick={() => transitionTo('confirm')}>
{t('app:common.next')}
</Button>
</Box>
)
}

96
src/components/modals/Receive/steps/03-step-confirm-address.js

@ -0,0 +1,96 @@
// @flow
import invariant from 'invariant'
import styled from 'styled-components'
import React, { Fragment, PureComponent } from 'react'
import getAddress from 'commands/getAddress'
import { isSegwitAccount } from 'helpers/bip32'
import Box from 'components/base/Box'
import Button from 'components/base/Button'
import DeviceConfirm from 'components/DeviceConfirm'
import CurrentAddressForAccount from 'components/CurrentAddressForAccount'
import { WrongDeviceForAccount } from 'components/EnsureDeviceApp'
import type { StepProps } from '../index'
export default class StepConfirmAddress extends PureComponent<StepProps> {
componentDidMount() {
this.confirmAddress()
}
confirmAddress = async () => {
const { account, device, onChangeAddressVerified, transitionTo } = this.props
invariant(account, 'No account given')
invariant(device, 'No device given')
try {
const params = {
currencyId: account.currency.id,
devicePath: device.path,
path: account.freshAddressPath,
segwit: isSegwitAccount(account),
verify: true,
}
const { address } = await getAddress.send(params).toPromise()
if (address !== account.freshAddress) {
throw new WrongDeviceForAccount(`WrongDeviceForAccount ${account.name}`, {
accountName: account.name,
})
}
onChangeAddressVerified(true)
transitionTo('receive')
} catch (err) {
onChangeAddressVerified(false)
}
}
render() {
const { t, device, account, isAddressVerified } = this.props
invariant(account, 'No account given')
invariant(device, 'No device given')
return (
<Container>
{isAddressVerified === false ? (
<Fragment>
<Title>{t('app:receive.steps.confirmAddress.error.title')}</Title>
<Text mb={5}>{t('app:receive.steps.confirmAddress.error.text')}</Text>
<DeviceConfirm error />
</Fragment>
) : (
<Fragment>
<Title>{t('app:receive.steps.confirmAddress.action')}</Title>
<Text>{t('app:receive.steps.confirmAddress.text')}</Text>
<CurrentAddressForAccount account={account} />
<DeviceConfirm mb={2} mt={-1} error={isAddressVerified === false} />
</Fragment>
)}
</Container>
)
}
}
export function StepConfirmAddressFooter({ t }: StepProps) {
// This will be displayed only if user rejected address
return <Button>{t('app:receive.steps.confirmAddress.support')}</Button>
}
const Container = styled(Box).attrs({
alignItems: 'center',
fontSize: 4,
color: 'dark',
px: 7,
})``
const Title = styled(Box).attrs({
ff: 'Museo Sans|Regular',
fontSize: 6,
mb: 1,
})``
const Text = styled(Box).attrs({
color: 'smoke',
})`
text-align: center;
`

66
src/components/modals/Receive/steps/04-step-receive-funds.js

@ -0,0 +1,66 @@
// @flow
import invariant from 'invariant'
import React, { PureComponent } from 'react'
import Button from 'components/base/Button'
import Box from 'components/base/Box'
import Label from 'components/base/Label'
import CurrentAddressForAccount from 'components/CurrentAddressForAccount'
import RequestAmount from 'components/RequestAmount'
import type { StepProps } from '../index'
type State = {
amount: number,
}
export default class StepReceiveFunds extends PureComponent<StepProps, State> {
state = {
amount: 0,
}
handleChangeAmount = (amount: number) => this.setState({ amount })
handleGoPrev = () => {
this.props.onChangeAppOpened(false)
this.props.onResetSkip()
this.props.transitionTo('device')
}
render() {
const { t, account, isAddressVerified } = this.props
const { amount } = this.state
invariant(account, 'No account given')
return (
<Box flow={5}>
<Box flow={1}>
<Label>{t('app:receive.steps.receiveFunds.label')}</Label>
<RequestAmount
account={account}
onChange={this.handleChangeAmount}
value={amount}
withMax={false}
/>
</Box>
<CurrentAddressForAccount
account={account}
addressVerified={isAddressVerified === true}
amount={amount}
onVerify={this.handleGoPrev}
withBadge
withFooter
withQRCode
withVerify={isAddressVerified !== true}
/>
</Box>
)
}
}
export function StepReceiveFundsFooter({ t, closeModal }: StepProps) {
return (
<Button primary onClick={closeModal}>
{t('app:common.close')}
</Button>
)
}
Loading…
Cancel
Save