Browse Source

Merge pull request #426 from gre/improve-errors-send-flow

Improve errors in send flow
master
Gaëtan Renaudeau 7 years ago
committed by GitHub
parent
commit
46ba5b884c
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 15
      src/api/Ethereum.js
  2. 9
      src/components/CounterValue/index.js
  3. 20
      src/components/DeviceConfirm/index.js
  4. 2
      src/components/DeviceConfirm/stories.js
  5. 22
      src/components/DeviceSignTransaction.js
  6. 3
      src/components/RecipientAddress/index.js
  7. 6
      src/components/RequestAmount/index.js
  8. 18
      src/components/base/Input/index.js
  9. 4
      src/components/modals/Receive/03-step-confirm-address.js
  10. 54
      src/components/modals/Send/01-step-amount.js
  11. 22
      src/components/modals/Send/03-step-verification.js
  12. 8
      src/components/modals/Send/04-step-confirmation.js
  13. 14
      src/components/modals/Send/AccountField.js
  14. 53
      src/components/modals/Send/AmountField.js
  15. 7
      src/components/modals/Send/Footer.js
  16. 79
      src/components/modals/Send/RecipientField.js
  17. 22
      src/components/modals/Send/SendModalBody.js
  18. 3
      src/helpers/errors.js
  19. 7
      src/helpers/promise.js
  20. 1
      src/internals/index.js
  21. 10
      static/i18n/en/send.yml

15
src/api/Ethereum.js

@ -1,5 +1,6 @@
// @flow // @flow
import axios from 'axios' import axios from 'axios'
import { retry } from 'helpers/promise'
import type { CryptoCurrency } from '@ledgerhq/live-common/lib/types' import type { CryptoCurrency } from '@ledgerhq/live-common/lib/types'
import { blockchainBaseURL, userFriendlyError } from './Ledger' import { blockchainBaseURL, userFriendlyError } from './Ledger'
@ -52,19 +53,25 @@ export const apiForCurrency = (currency: CryptoCurrency): API => {
return data return data
}, },
async getCurrentBlock() { async getCurrentBlock() {
const { data } = await userFriendlyError(axios.get(`${baseURL}/blocks/current`)) const { data } = await userFriendlyError(retry(() => axios.get(`${baseURL}/blocks/current`)))
return data return data
}, },
async getAccountNonce(address) { async getAccountNonce(address) {
const { data } = await userFriendlyError(axios.get(`${baseURL}/addresses/${address}/nonce`)) const { data } = await userFriendlyError(
retry(() => axios.get(`${baseURL}/addresses/${address}/nonce`)),
)
return data[0].nonce return data[0].nonce
}, },
async broadcastTransaction(tx) { async broadcastTransaction(tx) {
const { data } = await userFriendlyError(axios.post(`${baseURL}/transactions/send`, { tx })) const { data } = await userFriendlyError(
retry(() => axios.post(`${baseURL}/transactions/send`, { tx })),
)
return data.result return data.result
}, },
async getAccountBalance(address) { async getAccountBalance(address) {
const { data } = await userFriendlyError(axios.get(`${baseURL}/addresses/${address}/balance`)) const { data } = await userFriendlyError(
retry(() => axios.get(`${baseURL}/addresses/${address}/balance`)),
)
return data[0].balance return data[0].balance
}, },
} }

9
src/components/CounterValue/index.js

@ -20,6 +20,8 @@ type OwnProps = {
date?: Date, date?: Date,
value: number, value: number,
alwaysShowSign?: boolean,
} }
type Props = OwnProps & { type Props = OwnProps & {
@ -47,8 +49,11 @@ const mapStateToProps = (state: State, props: OwnProps) => {
} }
class CounterValue extends PureComponent<Props> { class CounterValue extends PureComponent<Props> {
static defaultProps = {
alwaysShowSign: true, // FIXME this shouldn't be true by default
}
render() { render() {
const { value, counterValueCurrency, date, ...props } = this.props const { value, counterValueCurrency, date, alwaysShowSign, ...props } = this.props
if (!value && value !== 0) { if (!value && value !== 0) {
return null return null
} }
@ -57,7 +62,7 @@ class CounterValue extends PureComponent<Props> {
val={value} val={value}
unit={counterValueCurrency.units[0]} unit={counterValueCurrency.units[0]}
showCode showCode
alwaysShowSign alwaysShowSign={alwaysShowSign}
{...props} {...props}
/> />
) )

20
src/components/DeviceConfirm/index.js

@ -23,17 +23,17 @@ const pulseAnimation = p => keyframes`
` `
const Wrapper = styled(Box).attrs({ const Wrapper = styled(Box).attrs({
color: p => (p.notValid ? 'alertRed' : 'wallet'), color: p => (p.error ? 'alertRed' : 'wallet'),
relative: true, relative: true,
})` })`
padding-top: ${p => (p.notValid ? 0 : 30)}px; padding-top: ${p => (p.error ? 0 : 30)}px;
transition: color ease-in-out 0.1s; transition: color ease-in-out 0.1s;
` `
const WrapperIcon = styled(Box)` const WrapperIcon = styled(Box)`
color: ${p => (p.notValid ? p.theme.colors.alertRed : p.theme.colors.positiveGreen)}; color: ${p => (p.error ? p.theme.colors.alertRed : p.theme.colors.positiveGreen)};
position: absolute; position: absolute;
left: ${p => (p.notValid ? 152 : 193)}px; left: ${p => (p.error ? 152 : 193)}px;
bottom: 16px; bottom: 16px;
svg { svg {
@ -41,9 +41,9 @@ const WrapperIcon = styled(Box)`
} }
` `
const Check = ({ notValid }: { notValid: boolean }) => ( const Check = ({ error }: { error: * }) => (
<WrapperIcon notValid={notValid}> <WrapperIcon error={error}>
{notValid ? <IconCross size={10} /> : <IconCheck size={10} />} {error ? <IconCross size={10} /> : <IconCheck size={10} />}
</WrapperIcon> </WrapperIcon>
) )
@ -74,7 +74,7 @@ const PushButton = styled(Box)`
` `
type Props = { type Props = {
notValid: boolean, error: *,
} }
const SVG = ( const SVG = (
@ -165,8 +165,8 @@ const SVG = (
const DeviceConfirm = (props: Props) => ( const DeviceConfirm = (props: Props) => (
<Wrapper {...props}> <Wrapper {...props}>
{props.notValid ? <PushButton /> : null} {!props.error ? <PushButton /> : null}
<Check notValid={props.notValid} /> <Check error={props.error} />
{SVG} {SVG}
</Wrapper> </Wrapper>
) )

2
src/components/DeviceConfirm/stories.js

@ -8,4 +8,4 @@ import DeviceConfirm from 'components/DeviceConfirm'
const stories = storiesOf('Components', module) const stories = storiesOf('Components', module)
stories.add('DeviceConfirm', () => <DeviceConfirm notValid={boolean('notValid', false)} />) stories.add('DeviceConfirm', () => <DeviceConfirm error={boolean('notValid', false)} />)

22
src/components/DeviceSignTransaction.js

@ -5,23 +5,16 @@ import type { Device } from 'types/common'
import type { WalletBridge } from 'bridge/types' import type { WalletBridge } from 'bridge/types'
type Props = { type Props = {
children: *,
onOperationBroadcasted: (op: Operation) => void, onOperationBroadcasted: (op: Operation) => void,
render: ({ error: ?Error }) => React$Node, onError: Error => void,
device: Device, device: Device,
account: Account, account: Account,
bridge: WalletBridge<*>, bridge: WalletBridge<*>,
transaction: *, transaction: *,
} }
type State = { class DeviceSignTransaction extends PureComponent<Props> {
error: ?Error,
}
class DeviceSignTransaction extends PureComponent<Props, State> {
state = {
error: null,
}
componentDidMount() { componentDidMount() {
this.sign() this.sign()
} }
@ -32,20 +25,17 @@ class DeviceSignTransaction extends PureComponent<Props, State> {
unmount = false unmount = false
sign = async () => { sign = async () => {
const { device, account, transaction, bridge, onOperationBroadcasted } = this.props const { device, account, transaction, bridge, onOperationBroadcasted, onError } = this.props
try { try {
const optimisticOperation = await bridge.signAndBroadcast(account, transaction, device.path) const optimisticOperation = await bridge.signAndBroadcast(account, transaction, device.path)
onOperationBroadcasted(optimisticOperation) onOperationBroadcasted(optimisticOperation)
} catch (error) { } catch (error) {
console.warn(error) onError(error)
this.setState({ error })
} }
} }
render() { render() {
const { render } = this.props return this.props.children
const { error } = this.state
return render({ error })
} }
} }

3
src/components/RecipientAddress/index.js

@ -77,12 +77,13 @@ class RecipientAddress extends PureComponent<Props, State> {
} }
render() { render() {
const { onChange, withQrCode, value } = this.props const { onChange, withQrCode, value, ...rest } = this.props
const { qrReaderOpened } = this.state const { qrReaderOpened } = this.state
return ( return (
<Box relative justifyContent="center"> <Box relative justifyContent="center">
<Input <Input
{...rest}
value={value} value={value}
withQrCode={withQrCode} withQrCode={withQrCode}
onChange={onChange} onChange={onChange}

6
src/components/RequestAmount/index.js

@ -50,6 +50,8 @@ type Props = {
// left value (always the one which is returned) // left value (always the one which is returned)
value: number, value: number,
canBeSpent: boolean,
// max left value // max left value
max: number, max: number,
@ -76,6 +78,7 @@ type Props = {
export class RequestAmount extends PureComponent<Props> { export class RequestAmount extends PureComponent<Props> {
static defaultProps = { static defaultProps = {
max: Infinity, max: Infinity,
canBeSpent: true,
withMax: true, withMax: true,
} }
@ -97,12 +100,13 @@ export class RequestAmount extends PureComponent<Props> {
} }
renderInputs(containerProps: Object) { renderInputs(containerProps: Object) {
const { value, account, rightCurrency, getCounterValue, exchange } = this.props const { value, account, rightCurrency, getCounterValue, exchange, canBeSpent } = this.props
const right = getCounterValue(account.currency, rightCurrency, exchange)(value) || 0 const right = getCounterValue(account.currency, rightCurrency, exchange)(value) || 0
const rightUnit = rightCurrency.units[0] const rightUnit = rightCurrency.units[0]
return ( return (
<Box horizontal grow shrink> <Box horizontal grow shrink>
<InputCurrency <InputCurrency
error={canBeSpent ? null : 'Not enough balance'}
containerProps={containerProps} containerProps={containerProps}
defaultUnit={account.unit} defaultUnit={account.unit}
value={value} value={value}

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

@ -14,9 +14,20 @@ const Container = styled(Box).attrs({
})` })`
background: ${p => p.theme.colors.white}; background: ${p => p.theme.colors.white};
border-radius: ${p => p.theme.radii[1]}px; border-radius: ${p => p.theme.radii[1]}px;
border: 1px solid ${p => (p.isFocus ? p.theme.colors.wallet : p.theme.colors.fog)}; border: 1px solid
${p =>
p.error ? p.theme.colors.pearl : p.isFocus ? p.theme.colors.wallet : p.theme.colors.fog};
box-shadow: ${p => (p.isFocus ? `rgba(0, 0, 0, 0.05) 0 2px 2px` : 'none')}; box-shadow: ${p => (p.isFocus ? `rgba(0, 0, 0, 0.05) 0 2px 2px` : 'none')};
height: ${p => (p.small ? '34' : '40')}px; height: ${p => (p.small ? '34' : '40')}px;
position: relative;
`
const ErrorDisplay = styled(Box)`
position: absolute;
bottom: -20px;
left: 0px;
font-size: 12px;
color: ${p => p.theme.colors.pearl};
` `
const Base = styled.input.attrs({ const Base = styled.input.attrs({
@ -74,6 +85,7 @@ type Props = {
renderLeft?: any, renderLeft?: any,
renderRight?: any, renderRight?: any,
containerProps?: Object, containerProps?: Object,
error?: string | boolean,
small?: boolean, small?: boolean,
} }
@ -145,7 +157,7 @@ class Input extends PureComponent<Props, State> {
render() { render() {
const { isFocus } = this.state const { isFocus } = this.state
const { renderLeft, renderRight, containerProps, small } = this.props const { renderLeft, renderRight, containerProps, small, error } = this.props
return ( return (
<Container <Container
@ -154,6 +166,7 @@ class Input extends PureComponent<Props, State> {
shrink shrink
{...containerProps} {...containerProps}
small={small} small={small}
error={error}
> >
{renderLeft} {renderLeft}
<Box px={3} grow shrink> <Box px={3} grow shrink>
@ -166,6 +179,7 @@ class Input extends PureComponent<Props, State> {
onChange={this.handleChange} onChange={this.handleChange}
onKeyDown={this.handleKeyDown} onKeyDown={this.handleKeyDown}
/> />
{error && typeof error === 'string' ? <ErrorDisplay>{error}</ErrorDisplay> : null}
</Box> </Box>
{renderRight} {renderRight}
</Container> </Container>

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

@ -44,7 +44,7 @@ export default (props: Props) => (
<Fragment> <Fragment>
<Title>{props.t('receive:steps.confirmAddress.error.title')}</Title> <Title>{props.t('receive:steps.confirmAddress.error.title')}</Title>
<Text mb={5}>{props.t('receive:steps.confirmAddress.error.text')}</Text> <Text mb={5}>{props.t('receive:steps.confirmAddress.error.text')}</Text>
<DeviceConfirm notValid /> <DeviceConfirm error />
</Fragment> </Fragment>
) : ( ) : (
<Fragment> <Fragment>
@ -58,7 +58,7 @@ export default (props: Props) => (
account={props.account} account={props.account}
device={props.device} device={props.device}
onCheck={props.onCheck} onCheck={props.onCheck}
render={({ isVerified }) => <DeviceConfirm notValid={isVerified === false} />} render={({ isVerified }) => <DeviceConfirm error={isVerified === false} />}
/> />
</Box> </Box>
)} )}

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

@ -4,57 +4,9 @@ import type { Account } from '@ledgerhq/live-common/lib/types'
import type { T } from 'types/common' import type { T } from 'types/common'
import type { WalletBridge } from 'bridge/types' import type { WalletBridge } from 'bridge/types'
import Box from 'components/base/Box' import Box from 'components/base/Box'
import Label from 'components/base/Label' import AccountField from './AccountField'
import LabelInfoTooltip from 'components/base/LabelInfoTooltip' import RecipientField from './RecipientField'
import RecipientAddress from 'components/RecipientAddress' import AmountField from './AmountField'
import RequestAmount from 'components/RequestAmount'
import SelectAccount from 'components/SelectAccount'
const AccountField = ({ onChange, value, t }: *) => (
<Box flow={1}>
<Label>{t('send:steps.amount.selectAccountDebit')}</Label>
<SelectAccount onChange={onChange} value={value} />
</Box>
)
// TODO we should use isRecipientValid & provide a feedback to user
const RecipientField = ({ bridge, account, transaction, onChangeTransaction, t }: *) => (
<Box flow={1}>
<Label>
<span>{t('send:steps.amount.recipientAddress')}</span>
<LabelInfoTooltip ml={1} text={t('send:steps.amount.recipientAddress')} />
</Label>
<RecipientAddress
withQrCode
value={bridge.getTransactionRecipient(account, transaction)}
onChange={(recipient, maybeExtra) => {
const { amount, currency } = maybeExtra || {}
if (currency && currency.scheme !== account.currency.scheme) return false
let t = transaction
if (amount) {
t = bridge.editTransactionAmount(account, t, amount)
}
t = bridge.editTransactionRecipient(account, t, recipient)
onChangeTransaction(t)
return true
}}
/>
</Box>
)
const AmountField = ({ bridge, account, transaction, onChangeTransaction, t }: *) => (
<Box flow={1}>
<Label>{t('send:steps.amount.amount')}</Label>
<RequestAmount
withMax={false}
account={account}
onChange={amount =>
onChangeTransaction(bridge.editTransactionAmount(account, transaction, amount))
}
value={bridge.getTransactionAmount(account, transaction)}
/>
</Box>
)
type PropsStepAmount<Transaction> = { type PropsStepAmount<Transaction> = {
t: T, t: T,

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

@ -35,10 +35,21 @@ type Props = {
bridge: ?WalletBridge<*>, bridge: ?WalletBridge<*>,
transaction: *, transaction: *,
onOperationBroadcasted: (op: Operation) => void, onOperationBroadcasted: (op: Operation) => void,
onError: (e: Error) => void,
hasError: boolean,
t: T, t: T,
} }
export default ({ account, device, bridge, transaction, onOperationBroadcasted, t }: Props) => ( export default ({
account,
device,
bridge,
transaction,
onOperationBroadcasted,
t,
onError,
hasError,
}: Props) => (
<Container> <Container>
<WarnBox>{multiline(t('send:steps.verification.warning'))}</WarnBox> <WarnBox>{multiline(t('send:steps.verification.warning'))}</WarnBox>
<Info>{t('send:steps.verification.body')}</Info> <Info>{t('send:steps.verification.body')}</Info>
@ -52,11 +63,10 @@ export default ({ account, device, bridge, transaction, onOperationBroadcasted,
transaction={transaction} transaction={transaction}
bridge={bridge} bridge={bridge}
onOperationBroadcasted={onOperationBroadcasted} onOperationBroadcasted={onOperationBroadcasted}
render={({ error }) => ( onError={onError}
// FIXME we really really REALLY should use error for the display. otherwise we are completely blind on error cases.. >
<DeviceConfirm notValid={!!error} /> <DeviceConfirm error={hasError} />
)} </DeviceSignTransaction>
/>
)} )}
</Container> </Container>
) )

8
src/components/modals/Send/04-step-confirmation.js

@ -8,6 +8,7 @@ import IconExclamationCircleThin from 'icons/ExclamationCircleThin'
import Box from 'components/base/Box' import Box from 'components/base/Box'
import { multiline } from 'styles/helpers' import { multiline } from 'styles/helpers'
import { colors } from 'styles/theme' import { colors } from 'styles/theme'
import { formatError } from 'helpers/errors'
import type { T } from 'types/common' import type { T } from 'types/common'
@ -39,10 +40,11 @@ const Text = styled(Box).attrs({
type Props = { type Props = {
optimisticOperation: ?Operation, optimisticOperation: ?Operation,
t: T, t: T,
error: ?Error,
} }
function StepConfirmation(props: Props) { function StepConfirmation(props: Props) {
const { t, optimisticOperation } = props const { t, optimisticOperation, error } = props
const Icon = optimisticOperation ? IconCheckCircle : IconExclamationCircleThin const Icon = optimisticOperation ? IconCheckCircle : IconExclamationCircleThin
const iconColor = optimisticOperation ? colors.positiveGreen : colors.alertRed const iconColor = optimisticOperation ? colors.positiveGreen : colors.alertRed
const tPrefix = optimisticOperation const tPrefix = optimisticOperation
@ -55,7 +57,9 @@ function StepConfirmation(props: Props) {
<Icon size={43} /> <Icon size={43} />
</span> </span>
<Title>{t(`${tPrefix}.title`)}</Title> <Title>{t(`${tPrefix}.title`)}</Title>
<Text>{multiline(t(`${tPrefix}.text`))}</Text> <Text style={{ userSelect: 'text' }}>
{optimisticOperation ? multiline(t(`${tPrefix}.text`)) : error ? formatError(error) : null}
</Text>
<Text style={{ userSelect: 'text' }}> <Text style={{ userSelect: 'text' }}>
{optimisticOperation ? optimisticOperation.hash : ''} {optimisticOperation ? optimisticOperation.hash : ''}
</Text> </Text>

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

@ -0,0 +1,14 @@
// @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('send:steps.amount.selectAccountDebit')}</Label>
<SelectAccount onChange={onChange} value={value} />
</Box>
)
export default AccountField

53
src/components/modals/Send/AmountField.js

@ -0,0 +1,53 @@
// @flow
import React, { Component } from 'react'
import Box from 'components/base/Box'
import Label from 'components/base/Label'
import RequestAmount from 'components/RequestAmount'
class AmountField extends Component<*, { canBeSpent: boolean }> {
state = {
canBeSpent: true,
}
componentDidMount() {
this.resync()
}
componentDidUpdate(nextProps: *) {
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 canBeSpent = await bridge.canBeSpent(account, transaction)
if (this.unmount) return
this.setState({ canBeSpent })
}
render() {
const { bridge, account, transaction, onChangeTransaction, t } = this.props
const { canBeSpent } = this.state
return (
<Box flow={1}>
<Label>{t('send:steps.amount.amount')}</Label>
<RequestAmount
withMax={false}
account={account}
canBeSpent={canBeSpent}
onChange={amount =>
onChangeTransaction(bridge.editTransactionAmount(account, transaction, amount))
}
value={bridge.getTransactionAmount(account, transaction)}
/>
</Box>
)
}
}
export default AmountField

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

@ -54,7 +54,7 @@ class Footer extends PureComponent<
this.resync() this.resync()
} }
} }
async componentWillUnmount() { componentWillUnmount() {
this.unmount = true this.unmount = true
} }
unmount = false unmount = false
@ -77,7 +77,7 @@ class Footer extends PureComponent<
<Box horizontal flow={2} align="center"> <Box horizontal flow={2} align="center">
<FormattedVal <FormattedVal
disableRounding disableRounding
color={!canBeSpent ? 'pearl' : 'dark'} color="dark"
val={totalSpent} val={totalSpent}
unit={account.unit} unit={account.unit}
showCode showCode
@ -94,6 +94,7 @@ class Footer extends PureComponent<
color="grey" color="grey"
fontSize={3} fontSize={3}
showCode showCode
alwaysShowSign={false}
/> />
<Text ff="Rubik" fontSize={3}> <Text ff="Rubik" fontSize={3}>
{')'} {')'}
@ -102,7 +103,7 @@ class Footer extends PureComponent<
</Box> </Box>
</Box> </Box>
)} )}
<Button primary onClick={onNext} disabled={!canNext}> <Button primary onClick={onNext} disabled={!canNext || !canBeSpent}>
{'Next'} {'Next'}
</Button> </Button>
</Box> </Box>

79
src/components/modals/Send/RecipientField.js

@ -0,0 +1,79 @@
// @flow
import React, { Component } from 'react'
import type { Account } from '@ledgerhq/live-common/lib/types'
import type { T } from 'types/common'
import type { WalletBridge } from 'bridge/types'
import Box from 'components/base/Box'
import Label from 'components/base/Label'
import LabelInfoTooltip from 'components/base/LabelInfoTooltip'
import RecipientAddress from 'components/RecipientAddress'
type Props<Transaction> = {
t: T,
account: Account,
bridge: WalletBridge<Transaction>,
transaction: Transaction,
onChangeTransaction: Transaction => void,
}
class RecipientField<Transaction> extends Component<Props<Transaction>, { isValid: boolean }> {
state = {
isValid: true,
}
componentDidMount() {
this.resync()
}
componentDidUpdate(nextProps: Props<Transaction>) {
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 isValid = await bridge.isRecipientValid(
account.currency,
bridge.getTransactionRecipient(account, transaction),
)
if (this.unmount) return
this.setState({ isValid })
}
render() {
const { bridge, account, transaction, onChangeTransaction, t } = this.props
const { isValid } = this.state
const value = bridge.getTransactionRecipient(account, transaction)
return (
<Box flow={1}>
<Label>
<span>{t('send:steps.amount.recipientAddress')}</span>
<LabelInfoTooltip ml={1} text={t('send:steps.amount.recipientAddress')} />
</Label>
<RecipientAddress
withQrCode
error={!value || isValid ? null : `This is not a valid ${account.currency.name} address`}
value={value}
onChange={(recipient, maybeExtra) => {
const { amount, currency } = maybeExtra || {}
if (currency && currency.scheme !== account.currency.scheme) return false
let t = transaction
if (amount) {
t = bridge.editTransactionAmount(account, t, amount)
}
t = bridge.editTransactionRecipient(account, t, recipient)
onChangeTransaction(t)
return true
}}
/>
</Box>
)
}
}
export default RecipientField

22
src/components/modals/Send/SendModalBody.js

@ -43,6 +43,7 @@ type State<T> = {
appStatus: ?string, appStatus: ?string,
deviceSelected: ?Device, deviceSelected: ?Device,
optimisticOperation: ?Operation, optimisticOperation: ?Operation,
error: ?Error,
} }
type Step = { type Step = {
@ -74,6 +75,7 @@ class SendModalBody extends PureComponent<Props, State<*>> {
account, account,
bridge, bridge,
transaction, transaction,
error: null,
} }
this.steps = [ this.steps = [
@ -97,7 +99,7 @@ class SendModalBody extends PureComponent<Props, State<*>> {
}, },
{ {
label: t('send:steps.confirmation.title'), label: t('send:steps.confirmation.title'),
prevStep: 1, prevStep: 0,
}, },
] ]
} }
@ -142,9 +144,20 @@ class SendModalBody extends PureComponent<Props, State<*>> {
this.setState({ this.setState({
optimisticOperation, optimisticOperation,
stepIndex: stepIndex + 1, stepIndex: stepIndex + 1,
error: null,
}) })
} }
onOperationError = (error: Error) => {
// $FlowFixMe
if (error.statusCode === 0x6985) {
// User denied on device
this.setState({ error })
} else {
this.setState({ error, stepIndex: 3 })
}
}
onChangeAccount = account => { onChangeAccount = account => {
const bridge = getBridgeForCurrency(account.currency) const bridge = getBridgeForCurrency(account.currency)
this.setState({ this.setState({
@ -159,7 +172,7 @@ class SendModalBody extends PureComponent<Props, State<*>> {
} }
onGoToFirstStep = () => { onGoToFirstStep = () => {
this.setState({ stepIndex: 0 }) this.setState({ stepIndex: 0, error: null })
} }
steps: Step[] steps: Step[]
@ -173,6 +186,7 @@ class SendModalBody extends PureComponent<Props, State<*>> {
bridge, bridge,
optimisticOperation, optimisticOperation,
deviceSelected, deviceSelected,
error,
} = this.state } = this.state
const step = this.steps[stepIndex] const step = this.steps[stepIndex]
@ -216,9 +230,11 @@ class SendModalBody extends PureComponent<Props, State<*>> {
transaction={transaction} transaction={transaction}
device={deviceSelected} device={deviceSelected}
onOperationBroadcasted={this.onOperationBroadcasted} onOperationBroadcasted={this.onOperationBroadcasted}
onError={this.onOperationError}
hasError={!!error}
/> />
<StepConfirmation t={t} optimisticOperation={optimisticOperation} /> <StepConfirmation t={t} optimisticOperation={optimisticOperation} error={error} />
</ChildSwitch> </ChildSwitch>
</ModalContent> </ModalContent>

3
src/helpers/errors.js

@ -0,0 +1,3 @@
// @flow
export const formatError = (e: Error) => e.message

7
src/helpers/promise.js

@ -20,8 +20,9 @@ export function retry<A>(f: () => Promise<A>, options?: $Shape<typeof defaults>)
return result return result
} }
// In case of failure, wait the interval, retry the action // In case of failure, wait the interval, retry the action
return result.catch(() => return result.catch(e => {
delay(interval).then(() => rec(remainingTry - 1, interval * intervalMultiplicator)), console.warn('Promise#retry', e)
) return delay(interval).then(() => rec(remainingTry - 1, interval * intervalMultiplicator))
})
} }
} }

1
src/internals/index.js

@ -39,6 +39,7 @@ process.on('message', m => {
type: 'ERROR', type: 'ERROR',
requestId, requestId,
data: { data: {
...error,
name: error && error.name, name: error && error.name,
message: error && error.message, message: error && error.message,
}, },

10
static/i18n/en/send.yml

@ -23,12 +23,10 @@ steps:
confirmation: confirmation:
title: Confirmation title: Confirmation
success: success:
title: Transaction successfully completed title: Transaction successfully broadcasted
text: You may have to wait few confirmations unitl the transaction appear text: |
with the following transaction id:
cta: View operation details cta: View operation details
error: error:
title: Transaction aborted title: Transaction error
text: |
The transaction have been aborted on your device.
You can try again the operation.
cta: Retry operation cta: Retry operation

Loading…
Cancel
Save