Browse Source

Merge pull request #752 from meriadec/refactor-send-modal

Refactor send modal to use Stepper
master
Gaëtan Renaudeau 7 years ago
committed by GitHub
parent
commit
50b8a9530c
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      src/components/base/ChildSwitch.js
  2. 12
      src/components/base/Input/index.js
  3. 28
      src/components/modals/Receive/index.js
  4. 102
      src/components/modals/Send/01-step-amount.js
  5. 16
      src/components/modals/Send/02-step-connect-device.js
  6. 41
      src/components/modals/Send/03-step-verification.js
  7. 14
      src/components/modals/Send/AccountField.js
  8. 50
      src/components/modals/Send/ConfirmationFooter.js
  9. 106
      src/components/modals/Send/Footer.js
  10. 0
      src/components/modals/Send/fields/AmountField.js
  11. 0
      src/components/modals/Send/fields/RecipientField.js
  12. 436
      src/components/modals/Send/index.js
  13. 187
      src/components/modals/Send/steps/01-step-amount.js
  14. 30
      src/components/modals/Send/steps/02-step-connect-device.js
  15. 85
      src/components/modals/Send/steps/03-step-verification.js
  16. 73
      src/components/modals/Send/steps/04-step-confirmation.js

5
src/components/base/ChildSwitch.js

@ -1,5 +0,0 @@
// @flow
import { Children } from 'react'
export default ({ children, index }: { children: React$Node, index: number }): ?React$Node =>
Children.toArray(children)[index] || null

12
src/components/base/Input/index.js

@ -160,7 +160,15 @@ class Input extends PureComponent<Props, State> {
render() {
const { isFocus } = this.state
const { renderLeft, renderRight, containerProps, editInPlace, small, error } = this.props
const {
renderLeft,
renderRight,
containerProps,
editInPlace,
small,
error,
...props
} = this.props
return (
<Container
@ -175,7 +183,7 @@ class Input extends PureComponent<Props, State> {
{renderLeft}
<Box px={3} grow shrink>
<Base
{...this.props}
{...props}
small={small}
innerRef={n => (this._input = n)}
onFocus={this.handleFocus}

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

@ -1,6 +1,6 @@
// @flow
import React, { PureComponent, Fragment } from 'react'
import React, { PureComponent } from 'react'
import { compose } from 'redux'
import { connect } from 'react-redux'
import { translate } from 'react-i18next'
@ -187,21 +187,19 @@ class ReceiveModal extends PureComponent<Props, State> {
preventBackdropClick={isModalLocked}
onBeforeOpen={this.handleBeforeOpenModal}
render={({ onClose }) => (
<Fragment>
<Stepper
title={t('app:receive.title')}
initialStepId={stepId}
onStepChange={this.handleStepChange}
onClose={onClose}
steps={this.STEPS}
disabledSteps={disabledSteps}
errorSteps={errorSteps}
{...addtionnalProps}
>
<Track onUnmount event="CloseModalReceive" />
<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>
</Fragment>
<SyncSkipUnderPriority priority={100} />
</Stepper>
)}
/>
)

102
src/components/modals/Send/01-step-amount.js

@ -1,102 +0,0 @@
// @flow
import React, { Fragment } from 'react'
import type { Account } from '@ledgerhq/live-common/lib/types'
import type { T } from 'types/common'
import type { WalletBridge } from 'bridge/types'
import TrackPage from 'analytics/TrackPage'
import Box from 'components/base/Box'
import AccountField from './AccountField'
import RecipientField from './RecipientField'
import AmountField from './AmountField'
type PropsStepAmount<Transaction> = {
t: T,
account: ?Account,
bridge: ?WalletBridge<Transaction>,
transaction: ?Transaction,
onChangeAccount: Account => void,
onChangeTransaction: Transaction => void,
}
function StepAmount({
t,
account,
bridge,
transaction,
onChangeAccount,
onChangeTransaction,
}: PropsStepAmount<*>) {
// TODO need to split each field into a component
const FeesField = bridge && bridge.EditFees
const AdvancedOptionsField = bridge && bridge.EditAdvancedOptions
return (
<Box flow={4}>
<TrackPage category="Send" name="Step1" />
<AccountField t={t} onChange={onChangeAccount} value={account} />
{/* HACK HACK HACK WTF */}
<div hidden>
{account &&
bridge &&
transaction && (
<RecipientField
account={account}
bridge={bridge}
transaction={transaction}
onChangeTransaction={onChangeTransaction}
t={t}
/>
)}
{account &&
bridge &&
transaction && (
<AmountField
account={account}
bridge={bridge}
transaction={transaction}
onChangeTransaction={onChangeTransaction}
t={t}
/>
)}
</div>
{/* HACK HACK HACK WTF */}
{account && bridge && transaction ? (
<Fragment key={account.id}>
<RecipientField
autoFocus
account={account}
bridge={bridge}
transaction={transaction}
onChangeTransaction={onChangeTransaction}
t={t}
/>
<AmountField
account={account}
bridge={bridge}
transaction={transaction}
onChangeTransaction={onChangeTransaction}
t={t}
/>
{FeesField && (
<FeesField account={account} value={transaction} onChange={onChangeTransaction} />
)}
{AdvancedOptionsField && (
<AdvancedOptionsField
account={account}
value={transaction}
onChange={onChangeTransaction}
/>
)}
</Fragment>
) : null}
</Box>
)
}
export default StepAmount

16
src/components/modals/Send/02-step-connect-device.js

@ -1,16 +0,0 @@
import React, { Component, Fragment } from 'react'
import TrackPage from 'analytics/TrackPage'
import StepConnectDevice from '../StepConnectDevice'
class SendStepConnectDevice extends Component<*> {
render() {
return (
<Fragment>
<TrackPage category="Send" name="Step2" />
<StepConnectDevice {...this.props} />
</Fragment>
)
}
}
export default SendStepConnectDevice

41
src/components/modals/Send/03-step-verification.js

@ -1,41 +0,0 @@
// @flow
import React from 'react'
import styled from 'styled-components'
import TrackPage from 'analytics/TrackPage'
import Box from 'components/base/Box'
import WarnBox from 'components/WarnBox'
import { multiline } from 'styles/helpers'
import DeviceConfirm from 'components/DeviceConfirm'
import type { T } from 'types/common'
const Container = styled(Box).attrs({
alignItems: 'center',
fontSize: 4,
pb: 4,
})``
const Info = styled(Box).attrs({
ff: 'Open Sans|SemiBold',
color: 'dark',
mt: 6,
mb: 4,
px: 5,
})`
text-align: center;
`
type Props = {
t: T,
}
export default ({ t }: Props) => (
<Container>
<TrackPage category="Send" name="Step3" />
<WarnBox>{multiline(t('app:send.steps.verification.warning'))}</WarnBox>
<Info>{t('app:send.steps.verification.body')}</Info>
<DeviceConfirm />
</Container>
)

14
src/components/modals/Send/AccountField.js

@ -1,14 +0,0 @@
// @flow
import React from 'react'
import Box from 'components/base/Box'
import Label from 'components/base/Label'
import SelectAccount from 'components/SelectAccount'
const AccountField = ({ onChange, value, t }: *) => (
<Box flow={1}>
<Label>{t('app:send.steps.amount.selectAccountDebit')}</Label>
<SelectAccount onChange={onChange} value={value} />
</Box>
)
export default AccountField

50
src/components/modals/Send/ConfirmationFooter.js

@ -1,50 +0,0 @@
// @flow
import React from 'react'
import type { Operation, Account } from '@ledgerhq/live-common/lib/types'
import { shell } from 'electron'
import Button from 'components/base/Button'
import { ModalFooter } from 'components/base/Modal'
import { getAccountOperationExplorer } from '@ledgerhq/live-common/lib/explorers'
import type { T } from 'types/common'
export default ({
t,
error,
account,
optimisticOperation,
onClose,
onGoToFirstStep,
}: {
t: T,
error: ?Error,
account: ?Account,
optimisticOperation: ?Operation,
onClose: () => void,
onGoToFirstStep: () => void,
}) => {
const url =
optimisticOperation && account && getAccountOperationExplorer(account, optimisticOperation)
return (
<ModalFooter horizontal alignItems="center" justifyContent="flex-end" flow={2}>
<Button onClick={onClose}>{t('app:common.close')}</Button>
{optimisticOperation ? (
// TODO: actually go to operations details
url ? (
<Button
onClick={() => {
shell.openExternal(url)
onClose()
}}
primary
>
{t('app:send.steps.confirmation.success.cta')}
</Button>
) : null
) : error ? (
<Button onClick={onGoToFirstStep} primary>
{t('app:send.steps.confirmation.error.cta')}
</Button>
) : null}
</ModalFooter>
)
}

106
src/components/modals/Send/Footer.js

@ -1,106 +0,0 @@
// @flow
import React, { PureComponent } from 'react'
import type { Account } from '@ledgerhq/live-common/lib/types'
import type { T } from 'types/common'
import { ModalFooter } from 'components/base/Modal'
import Box from 'components/base/Box'
import Button from 'components/base/Button'
import CounterValue from 'components/CounterValue'
import FormattedVal from 'components/base/FormattedVal'
import Label from 'components/base/Label'
import Text from 'components/base/Text'
import type { WalletBridge } from 'bridge/types'
type Props = {
t: T,
account: Account,
bridge: WalletBridge<*>,
transaction: *,
onNext: () => void,
canNext: ?boolean,
showTotal: boolean,
}
class Footer extends PureComponent<
Props,
{
totalSpent: number,
canBeSpent: boolean,
},
> {
state = {
totalSpent: 0,
canBeSpent: true,
}
componentDidMount() {
this.resync()
}
componentDidUpdate(nextProps: Props) {
if (
nextProps.account !== this.props.account ||
nextProps.transaction !== this.props.transaction
) {
this.resync()
}
}
componentWillUnmount() {
this.unmount = true
}
unmount = false
async resync() {
const { account, bridge, transaction } = this.props
const totalSpent = await bridge.getTotalSpent(account, transaction)
const canBeSpent = await bridge.canBeSpent(account, transaction)
if (this.unmount) return
this.setState({ totalSpent, canBeSpent })
}
render() {
const { account, t, onNext, canNext, showTotal } = this.props
const { totalSpent, canBeSpent } = this.state
return (
<ModalFooter>
<Box horizontal alignItems="center" justifyContent="flex-end" flow={2}>
{showTotal && (
<Box grow>
<Label>{t('app:send.totalSpent')}</Label>
<Box horizontal flow={2} align="center">
<FormattedVal
disableRounding
color="dark"
val={totalSpent}
unit={account.unit}
showCode
/>
<Box horizontal align="center">
<Text ff="Rubik" fontSize={3}>
{'(' /* eslint-disable-line react/jsx-no-literals */}
</Text>
<CounterValue
currency={account.currency}
value={totalSpent}
disableRounding
color="grey"
fontSize={3}
showCode
alwaysShowSign={false}
/>
<Text ff="Rubik" fontSize={3}>
{')' /* eslint-disable-line react/jsx-no-literals */}
</Text>
</Box>
</Box>
</Box>
)}
<Button primary onClick={onNext} disabled={!canNext || !canBeSpent}>
{t('app:common.next')}
</Button>
</Box>
</ModalFooter>
)
}
}
export default Footer

0
src/components/modals/Send/AmountField.js → src/components/modals/Send/fields/AmountField.js

0
src/components/modals/Send/RecipientField.js → src/components/modals/Send/fields/RecipientField.js

436
src/components/modals/Send/index.js

@ -1,130 +1,135 @@
// @flow
import logger from 'logger'
import invariant from 'invariant'
import React, { Component } from 'react'
import { translate } from 'react-i18next'
import { connect } from 'react-redux'
import React, { PureComponent } from 'react'
import { compose } from 'redux'
import { connect } from 'react-redux'
import { translate } from 'react-i18next'
import { createStructuredSelector } from 'reselect'
import type { Account, Operation } from '@ledgerhq/live-common/lib/types'
import Track from 'analytics/Track'
import type { Account, Operation } from '@ledgerhq/live-common/lib/types'
import type { T, Device } from 'types/common'
import type { WalletBridge } from 'bridge/types'
import { updateAccountWithUpdater } from 'actions/accounts'
import { MODAL_SEND } from 'config/constants'
import { getBridgeForCurrency } from 'bridge'
import type { WalletBridge } from 'bridge/types'
import type { T, Device } from 'types/common'
import type { StepProps as DefaultStepProps } from 'components/base/Stepper'
import { getCurrentDevice } from 'reducers/devices'
import { accountsSelector } from 'reducers/accounts'
import { updateAccountWithUpdater } from 'actions/accounts'
import { createCustomErrorClass } from 'helpers/errors'
import { closeModal } from 'reducers/modals'
import { MODAL_SEND } from 'config/constants'
import Modal, { ModalBody, ModalContent, ModalTitle } from 'components/base/Modal'
import PollCounterValuesOnMount from 'components/PollCounterValuesOnMount'
import Breadcrumb from 'components/Breadcrumb'
import ChildSwitch from 'components/base/ChildSwitch'
import Modal from 'components/base/Modal'
import Stepper from 'components/base/Stepper'
import SyncSkipUnderPriority from 'components/SyncSkipUnderPriority'
import SyncOneAccountOnMount from 'components/SyncOneAccountOnMount'
import Footer from './Footer'
import ConfirmationFooter from './ConfirmationFooter'
import StepAmount from './01-step-amount'
import StepConnectDevice from './02-step-connect-device'
import StepVerification from './03-step-verification'
import StepConfirmation from './04-step-confirmation'
export const UserRefusedOnDevice = createCustomErrorClass('UserRefusedOnDevice')
import StepAmount, { StepAmountFooter } from './steps/01-step-amount'
import StepConnectDevice, { StepConnectDeviceFooter } from './steps/02-step-connect-device'
import StepVerification from './steps/03-step-verification'
import StepConfirmation, { StepConfirmationFooter } from './steps/04-step-confirmation'
type Props = {
updateAccountWithUpdater: (string, (Account) => Account) => void,
accounts: Account[],
t: T,
device: ?Device,
accounts: Account[],
closeModal: string => void,
updateAccountWithUpdater: (string, (Account) => Account) => void,
}
type State<T> = {
type State<Transaction> = {
stepId: string,
account: ?Account,
transaction: ?T,
bridge: ?WalletBridge<T>,
stepIndex: number,
appStatus: ?string,
deviceSelected: ?Device,
bridge: ?WalletBridge<Transaction>,
transaction: ?Transaction,
optimisticOperation: ?Operation,
isAppOpened: boolean,
disabledSteps: number[],
errorSteps: number[],
amount: number,
error: ?Error,
}
type Step = {
label: string,
canNext: (State<*>) => boolean,
canPrev: (State<*>) => boolean,
canClose: (State<*>) => boolean,
hasError: (State<*>) => boolean,
prevStep?: number,
export type StepProps<Transaction> = DefaultStepProps & {
device: ?Device,
account: ?Account,
bridge: ?WalletBridge<Transaction>,
transaction: ?Transaction,
error: ?Error,
optimisticOperation: ?Operation,
closeModal: void => void,
isAppOpened: boolean,
onChangeAccount: (?Account) => void,
onChangeAppOpened: boolean => void,
onChangeTransaction: Transaction => void,
onTransactionError: Error => void,
onOperationBroadcasted: Operation => void,
onRetry: void => void,
}
const createSteps = ({ t }: { t: T }) => [
{
id: 'amount',
label: t('app:send.steps.amount.title'),
component: StepAmount,
footer: StepAmountFooter,
},
{
id: 'device',
label: t('app:send.steps.connectDevice.title'),
component: StepConnectDevice,
footer: StepConnectDeviceFooter,
onBack: ({ transitionTo }) => transitionTo('amount'),
},
{
id: 'verification',
label: t('app:send.steps.verification.title'),
component: StepVerification,
shouldPreventClose: true,
},
{
id: 'confirmation',
label: t('app:send.steps.confirmation.title'),
component: StepConfirmation,
footer: StepConfirmationFooter,
onBack: ({ transitionTo, onRetry }) => {
onRetry()
transitionTo('amount')
},
},
]
const mapStateToProps = createStructuredSelector({
device: getCurrentDevice,
accounts: accountsSelector,
})
const mapDispatchToProps = {
closeModal,
updateAccountWithUpdater,
}
const INITIAL_STATE = {
stepIndex: 0,
appStatus: null,
deviceSelected: null,
optimisticOperation: null,
stepId: 'amount',
amount: 0,
account: null,
bridge: null,
transaction: null,
error: null,
optimisticOperation: null,
isAppOpened: false,
disabledSteps: [],
errorSteps: [],
}
class SendModal extends Component<Props, State<*>> {
constructor({ t }: Props) {
super()
this.steps = [
{
label: t('app:send.steps.amount.title'),
canClose: () => true,
canPrev: () => false,
canNext: ({ bridge, account, transaction }) =>
bridge && account && transaction
? bridge.isValidTransaction(account, transaction)
: false,
hasError: () => false,
},
{
label: t('app:send.steps.connectDevice.title'),
canClose: () => true,
canNext: ({ deviceSelected, appStatus }) =>
deviceSelected !== null && appStatus === 'success',
prevStep: 0,
canPrev: () => true,
hasError: () => false,
},
{
label: t('app:send.steps.verification.title'),
canClose: ({ error }) => !!error,
canNext: () => true,
canPrev: ({ error }) => !!error,
prevStep: 0,
hasError: ({ error }) => (error && error.name === 'UserRefusedOnDevice') || false,
},
{
label: t('app:send.steps.confirmation.title'),
prevStep: 0,
canClose: () => true,
canPrev: () => true,
canNext: () => false,
hasError: ({ error }) => (error && error.name !== 'UserRefusedOnDevice') || false,
},
]
}
class SendModal extends PureComponent<Props, State<*>> {
state = INITIAL_STATE
signTransactionSub: *
STEPS = createSteps({ t: this.props.t })
handleReset = () => this.setState({ ...INITIAL_STATE })
handleCloseModal = () => this.props.closeModal(MODAL_SEND)
handleStepChange = step => this.setState({ stepId: step.id })
handleBeforeOpenModal = ({ data }) => {
const { account } = this.state
@ -137,240 +142,91 @@ class SendModal extends Component<Props, State<*>> {
}
}
handleReset = () => {
const { signTransactionSub } = this
if (signTransactionSub) {
signTransactionSub.unsubscribe()
handleChangeAccount = (account: Account) => {
if (account !== this.state.account) {
const bridge = getBridgeForCurrency(account.currency)
const transaction = bridge.createTransaction(account)
this.setState({ account, bridge, transaction })
}
this.setState(INITIAL_STATE)
}
signTransaction = async () => {
const { deviceSelected, account, transaction, bridge } = this.state
invariant(
deviceSelected && account && transaction && bridge,
'signTransaction invalid conditions',
)
this.signTransactionSub = bridge
.signAndBroadcast(account, transaction, deviceSelected.path)
.subscribe({
next: e => {
switch (e.type) {
case 'signed': {
this.onSigned()
break
}
case 'broadcasted': {
this.onOperationBroadcasted(e.operation)
break
}
default:
}
},
error: error => {
this.onOperationError(error)
},
})
}
onNextStep = () =>
this.setState(state => {
let { stepIndex, error } = state
if (stepIndex >= this.steps.length - 1) {
return null
}
if (!this.steps[stepIndex].canNext(state)) {
logger.warn('tried to next step without a valid state!', state, stepIndex)
return null
}
stepIndex++
if (stepIndex < 2) {
error = null
} else if (stepIndex === 2) {
this.signTransaction()
}
return { stepIndex, error }
})
handleChangeAppOpened = (isAppOpened: boolean) => this.setState({ isAppOpened })
handleChangeTransaction = transaction => this.setState({ transaction })
handleRetry = () => this.setState({ error: null, errorSteps: [] })
onChangeDevice = (deviceSelected: ?Device) => {
this.setState({ deviceSelected })
handleTransactionError = (error: Error) => {
const stepVerificationIndex = this.STEPS.findIndex(step => step.id === 'verification')
if (stepVerificationIndex === -1) return
this.setState({ error, errorSteps: [stepVerificationIndex] })
}
onChangeStatus = (deviceStatus: ?string, appStatus: ?string) => {
this.setState({ appStatus })
}
onPrevStep = () => {
const { stepIndex } = this.state
const step = this.steps[stepIndex]
if (step && 'prevStep' in step) {
this.setState({
appStatus: null,
deviceSelected: null,
error: null,
stepIndex: step.prevStep,
})
}
}
onSigned = () => {
this.setState({
stepIndex: 3,
})
}
onOperationBroadcasted = (optimisticOperation: Operation) => {
handleOperationBroadcasted = (optimisticOperation: Operation) => {
const { account, bridge } = this.state
const { updateAccountWithUpdater } = this.props
if (!account || !bridge) return
const { addPendingOperation } = bridge
if (addPendingOperation) {
this.props.updateAccountWithUpdater(account.id, account =>
updateAccountWithUpdater(account.id, account =>
addPendingOperation(account, optimisticOperation),
)
}
this.setState({
optimisticOperation,
stepIndex: 3,
error: null,
})
}
onOperationError = (error: *) => {
this.setState({
error: error.statusCode === 0x6985 ? new UserRefusedOnDevice() : error,
stepIndex: 3,
})
}
onChangeAccount = account => {
const bridge = getBridgeForCurrency(account.currency)
this.setState({
account,
bridge,
transaction: bridge.createTransaction(account),
})
}
onChangeTransaction = transaction => {
this.setState({ transaction })
}
onGoToFirstStep = () => {
this.setState({ stepIndex: 0, error: null })
this.setState({ optimisticOperation, error: null })
}
steps: Step[]
render() {
const { t } = this.props
const { t, device } = this.props
const {
stepIndex,
stepId,
account,
transaction,
isAppOpened,
disabledSteps,
errorSteps,
bridge,
transaction,
optimisticOperation,
deviceSelected,
error,
} = this.state
const step = this.steps[stepIndex]
if (!step) return null
const canClose = step.canClose(this.state)
const canNext = step.canNext(this.state)
const canPrev = step.canPrev(this.state)
const addtionnalProps = {
device,
account,
bridge,
transaction,
isAppOpened,
error,
optimisticOperation,
closeModal: this.handleCloseModal,
onChangeAccount: this.handleChangeAccount,
onChangeAppOpened: this.handleChangeAppOpened,
onChangeTransaction: this.handleChangeTransaction,
onTransactionError: this.handleTransactionError,
onRetry: this.handleRetry,
onOperationBroadcasted: this.handleOperationBroadcasted,
}
const stepsErrors = []
this.steps.forEach((s, i) => {
if (s.hasError(this.state)) {
stepsErrors.push(i)
}
})
const isModalLocked = stepId === 'verification'
return (
<Modal
name={MODAL_SEND}
refocusWhenChange={stepId}
onHide={this.handleReset}
preventBackdropClick={isModalLocked}
onBeforeOpen={this.handleBeforeOpenModal}
preventBackdropClick={!canClose}
onClose={canClose ? this.handleReset : undefined}
render={({ onClose }) => (
<ModalBody onClose={canClose ? onClose : undefined}>
<Stepper
title={t('app:send.title')}
initialStepId={stepId}
onStepChange={this.handleStepChange}
onClose={onClose}
steps={this.STEPS}
disabledSteps={disabledSteps}
errorSteps={errorSteps}
{...addtionnalProps}
>
<SyncSkipUnderPriority priority={100} />
<Track onUnmount event="CloseModalSend" />
<PollCounterValuesOnMount />
<SyncSkipUnderPriority priority={80} />
{account && <SyncOneAccountOnMount priority={81} accountId={account.id} />}
<ModalTitle onBack={canPrev ? this.onPrevStep : undefined}>
{t('app:send.title')}
</ModalTitle>
<ModalContent>
<Breadcrumb
t={t}
mb={6}
currentStep={stepIndex}
stepsErrors={stepsErrors}
items={this.steps}
/>
<ChildSwitch index={stepIndex}>
<StepAmount
t={t}
account={account}
bridge={bridge}
transaction={transaction}
onChangeAccount={this.onChangeAccount}
onChangeTransaction={this.onChangeTransaction}
/>
<StepConnectDevice
t={t}
account={account}
deviceSelected={deviceSelected}
onChangeDevice={this.onChangeDevice}
onStatusChange={this.onChangeStatus}
/>
<StepVerification
t={t}
account={account}
bridge={bridge}
transaction={transaction}
device={deviceSelected}
onOperationBroadcasted={this.onOperationBroadcasted}
onError={this.onOperationError}
hasError={!!error}
/>
<StepConfirmation t={t} optimisticOperation={optimisticOperation} error={error} />
</ChildSwitch>
</ModalContent>
{stepIndex === 3 ? (
<ConfirmationFooter
t={t}
error={error}
account={account}
optimisticOperation={optimisticOperation}
onClose={onClose}
onGoToFirstStep={this.onGoToFirstStep}
/>
) : (
account &&
bridge &&
transaction &&
stepIndex < 2 && (
<Footer
canNext={canNext}
onNext={this.onNextStep}
account={account}
bridge={bridge}
transaction={transaction}
showTotal={stepIndex === 0}
t={t}
/>
)
)}
</ModalBody>
</Stepper>
)}
/>
)

187
src/components/modals/Send/steps/01-step-amount.js

@ -0,0 +1,187 @@
// @flow
import React, { PureComponent, Fragment } from 'react'
import Box from 'components/base/Box'
import Button from 'components/base/Button'
import Label from 'components/base/Label'
import SelectAccount from 'components/SelectAccount'
import FormattedVal from 'components/base/FormattedVal'
import Text from 'components/base/Text'
import CounterValue from 'components/CounterValue'
import Spinner from 'components/base/Spinner'
import RecipientField from '../fields/RecipientField'
import AmountField from '../fields/AmountField'
import type { StepProps } from '../index'
export default ({
t,
account,
bridge,
transaction,
onChangeAccount,
onChangeTransaction,
}: StepProps<*>) => {
const FeesField = bridge && bridge.EditFees
const AdvancedOptionsField = bridge && bridge.EditAdvancedOptions
// TODO: figure out why flow can't understand when we put conditions in variables
// e.g:
// const hasTransaction = !!account && !!bridge && !!transaction
return (
<Box flow={4}>
<Box flow={1}>
<Label>{t('app:send.steps.amount.selectAccountDebit')}</Label>
<SelectAccount onChange={onChangeAccount} value={account} />
</Box>
{account &&
bridge &&
transaction && (
<RecipientField
autoFocus
account={account}
bridge={bridge}
transaction={transaction}
onChangeTransaction={onChangeTransaction}
t={t}
/>
)}
{account &&
bridge &&
transaction && (
<AmountField
account={account}
bridge={bridge}
transaction={transaction}
onChangeTransaction={onChangeTransaction}
t={t}
/>
)}
{account &&
bridge &&
transaction &&
FeesField && (
<FeesField account={account} value={transaction} onChange={onChangeTransaction} />
)}
{account &&
bridge &&
transaction &&
AdvancedOptionsField && (
<AdvancedOptionsField
account={account}
value={transaction}
onChange={onChangeTransaction}
/>
)}
</Box>
)
}
export class StepAmountFooter extends PureComponent<
StepProps<*>,
{
totalSpent: number,
canBeSpent: boolean,
isSyncing: boolean,
},
> {
state = {
isSyncing: false,
totalSpent: 0,
canBeSpent: true,
}
componentDidMount() {
this.resync()
}
componentDidUpdate(nextProps: StepProps<*>) {
if (
nextProps.account !== this.props.account ||
nextProps.transaction !== this.props.transaction
) {
this.resync()
}
}
componentWillUnmount() {
this._isUnmounted = true
}
_isUnmounted = false
async resync() {
const { account, bridge, transaction } = this.props
if (!account || !transaction || !bridge) {
return
}
this.setState({ isSyncing: true })
try {
const totalSpent = await bridge.getTotalSpent(account, transaction)
if (this._isUnmounted) return
const canBeSpent = await bridge.canBeSpent(account, transaction)
if (this._isUnmounted) return
this.setState({ totalSpent, canBeSpent, isSyncing: false })
} catch (err) {
this.setState({ isSyncing: false })
}
}
render() {
const { t, transitionTo, account, transaction, bridge } = this.props
const { totalSpent, canBeSpent, isSyncing } = this.state
const canNext =
account && transaction && bridge && bridge.isValidTransaction(account, transaction)
return (
<Fragment>
<Box grow>
<Label>{t('app:send.totalSpent')}</Label>
<Box horizontal flow={2} align="center">
{account && (
<FormattedVal
disableRounding
color="dark"
val={totalSpent}
unit={account.unit}
showCode
/>
)}
<Box horizontal align="center">
<Text ff="Rubik" fontSize={3}>
{'(' /* eslint-disable-line react/jsx-no-literals */}
</Text>
{account && (
<CounterValue
currency={account.currency}
value={totalSpent}
disableRounding
color="grey"
fontSize={3}
showCode
alwaysShowSign={false}
/>
)}
<Text ff="Rubik" fontSize={3}>
{')' /* eslint-disable-line react/jsx-no-literals */}
</Text>
</Box>
{isSyncing && <Spinner size={10} />}
</Box>
</Box>
<Button disabled={!canBeSpent || !canNext} primary onClick={() => transitionTo('device')}>
{t('app:common.next')}
</Button>
</Fragment>
)
}
}

30
src/components/modals/Send/steps/02-step-connect-device.js

@ -0,0 +1,30 @@
// @flow
import React, { Fragment } from 'react'
import TrackPage from 'analytics/TrackPage'
import Button from 'components/base/Button'
import EnsureDeviceApp from 'components/EnsureDeviceApp'
import type { StepProps } from '../index'
export default function StepConnectDevice({ account, onChangeAppOpened }: StepProps<*>) {
return (
<Fragment>
<TrackPage category="Send" name="Step2" />
<EnsureDeviceApp
account={account}
waitBeforeSuccess={200}
onSuccess={() => onChangeAppOpened(true)}
/>
</Fragment>
)
}
export function StepConnectDeviceFooter({ t, transitionTo, isAppOpened }: StepProps<*>) {
return (
<Button disabled={!isAppOpened} primary onClick={() => transitionTo('verification')}>
{t('app:common.next')}
</Button>
)
}

85
src/components/modals/Send/steps/03-step-verification.js

@ -0,0 +1,85 @@
// @flow
import invariant from 'invariant'
import React, { PureComponent } from 'react'
import styled from 'styled-components'
import { multiline } from 'styles/helpers'
import { createCustomErrorClass } from 'helpers/errors'
import TrackPage from 'analytics/TrackPage'
import Box from 'components/base/Box'
import WarnBox from 'components/WarnBox'
import DeviceConfirm from 'components/DeviceConfirm'
import type { StepProps } from '../index'
export const UserRefusedOnDevice = createCustomErrorClass('UserRefusedOnDevice')
const Container = styled(Box).attrs({ alignItems: 'center', fontSize: 4, pb: 4 })``
const Info = styled(Box).attrs({ ff: 'Open Sans|SemiBold', color: 'dark', mt: 6, mb: 4, px: 5 })`
text-align: center;
`
export default class StepVerification extends PureComponent<StepProps<*>> {
componentDidMount() {
this.signTransaction()
}
componentWillUnmount() {
this._isUnmounted = true
if (this._signTransactionSub) {
this._signTransactionSub.unsubscribe()
}
}
_isUnmounted = false
_signTransactionSub = null
signTransaction = async () => {
const {
device,
account,
transaction,
bridge,
onTransactionError,
transitionTo,
onOperationBroadcasted,
} = this.props
invariant(device && account && transaction && bridge, 'signTransaction invalid conditions')
this._signTransactionSub = bridge
.signAndBroadcast(account, transaction, device.path)
.subscribe({
next: e => {
switch (e.type) {
case 'signed': {
if (this._isUnmounted) return
transitionTo('confirmation')
break
}
case 'broadcasted': {
onOperationBroadcasted(e.operation)
break
}
default:
}
},
error: err => {
const error = err.statusCode === 0x6985 ? new UserRefusedOnDevice() : err
onTransactionError(error)
transitionTo('confirmation')
},
})
}
render() {
const { t } = this.props
return (
<Container>
<TrackPage category="Send" name="Step3" />
<WarnBox>{multiline(t('app:send.steps.verification.warning'))}</WarnBox>
<Info>{t('app:send.steps.verification.body')}</Info>
<DeviceConfirm />
</Container>
)
}
}

73
src/components/modals/Send/04-step-confirmation.js → src/components/modals/Send/steps/04-step-confirmation.js

@ -1,17 +1,22 @@
// @flow
import React from 'react'
import React, { Fragment } from 'react'
import { shell } from 'electron'
import styled from 'styled-components'
import type { Operation } from '@ledgerhq/live-common/lib/types'
import type { T } from 'types/common'
import { getAccountOperationExplorer } from '@ledgerhq/live-common/lib/explorers'
import { colors } from 'styles/theme'
import { multiline } from 'styles/helpers'
import TrackPage from 'analytics/TrackPage'
import Box from 'components/base/Box'
import Button from 'components/base/Button'
import Spinner from 'components/base/Spinner'
import TranslatedError from 'components/TranslatedError'
import IconCheckCircle from 'icons/CheckCircle'
import IconExclamationCircleThin from 'icons/ExclamationCircleThin'
import Box from 'components/base/Box'
import { multiline } from 'styles/helpers'
import { colors } from 'styles/theme'
import TranslatedError from '../../TranslatedError'
import type { StepProps } from '../index'
const Container = styled(Box).attrs({
alignItems: 'center',
@ -38,14 +43,7 @@ const Text = styled(Box).attrs({
text-align: center;
`
type Props = {
optimisticOperation: ?Operation,
t: T,
error: ?Error,
}
function StepConfirmation(props: Props) {
const { t, optimisticOperation, error } = props
export default function StepConfirmation({ t, optimisticOperation, error }: StepProps<*>) {
const Icon = optimisticOperation ? IconCheckCircle : error ? IconExclamationCircleThin : Spinner
const iconColor = optimisticOperation
? colors.positiveGreen
@ -57,7 +55,6 @@ function StepConfirmation(props: Props) {
: error
? 'app:send.steps.confirmation.error'
: 'app:send.steps.confirmation.pending'
return (
<Container>
<TrackPage category="Send" name="Step4" />
@ -79,4 +76,46 @@ function StepConfirmation(props: Props) {
)
}
export default StepConfirmation
export function StepConfirmationFooter({
t,
transitionTo,
account,
onRetry,
optimisticOperation,
error,
closeModal,
}: StepProps<*>) {
const url =
optimisticOperation && account && getAccountOperationExplorer(account, optimisticOperation)
return (
<Fragment>
<Button onClick={closeModal}>{t('app:common.close')}</Button>
{optimisticOperation ? (
// TODO: actually go to operations details
url ? (
<Button
ml={2}
onClick={() => {
shell.openExternal(url)
closeModal()
}}
primary
>
{t('app:send.steps.confirmation.success.cta')}
</Button>
) : null
) : error ? (
<Button
ml={2}
primary
onClick={() => {
onRetry()
transitionTo('amount')
}}
>
{t('app:send.steps.confirmation.error.cta')}
</Button>
) : null}
</Fragment>
)
}
Loading…
Cancel
Save