diff --git a/src/api/Ethereum.js b/src/api/Ethereum.js index 716accad..9b3d4c9b 100644 --- a/src/api/Ethereum.js +++ b/src/api/Ethereum.js @@ -1,5 +1,6 @@ // @flow import axios from 'axios' +import { retry } from 'helpers/promise' import type { CryptoCurrency } from '@ledgerhq/live-common/lib/types' import { blockchainBaseURL, userFriendlyError } from './Ledger' @@ -52,19 +53,25 @@ export const apiForCurrency = (currency: CryptoCurrency): API => { return data }, async getCurrentBlock() { - const { data } = await userFriendlyError(axios.get(`${baseURL}/blocks/current`)) + const { data } = await userFriendlyError(retry(() => axios.get(`${baseURL}/blocks/current`))) return data }, 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 }, 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 }, 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 }, } diff --git a/src/components/CounterValue/index.js b/src/components/CounterValue/index.js index 16f4b08b..5a1e726a 100644 --- a/src/components/CounterValue/index.js +++ b/src/components/CounterValue/index.js @@ -20,6 +20,8 @@ type OwnProps = { date?: Date, value: number, + + alwaysShowSign?: boolean, } type Props = OwnProps & { @@ -47,8 +49,11 @@ const mapStateToProps = (state: State, props: OwnProps) => { } class CounterValue extends PureComponent { + static defaultProps = { + alwaysShowSign: true, // FIXME this shouldn't be true by default + } render() { - const { value, counterValueCurrency, date, ...props } = this.props + const { value, counterValueCurrency, date, alwaysShowSign, ...props } = this.props if (!value && value !== 0) { return null } @@ -57,7 +62,7 @@ class CounterValue extends PureComponent { val={value} unit={counterValueCurrency.units[0]} showCode - alwaysShowSign + alwaysShowSign={alwaysShowSign} {...props} /> ) diff --git a/src/components/DeviceConfirm/index.js b/src/components/DeviceConfirm/index.js index 1c6ecb17..a9c88790 100644 --- a/src/components/DeviceConfirm/index.js +++ b/src/components/DeviceConfirm/index.js @@ -23,17 +23,17 @@ const pulseAnimation = p => keyframes` ` const Wrapper = styled(Box).attrs({ - color: p => (p.notValid ? 'alertRed' : 'wallet'), + color: p => (p.error ? 'alertRed' : 'wallet'), 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; ` 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; - left: ${p => (p.notValid ? 152 : 193)}px; + left: ${p => (p.error ? 152 : 193)}px; bottom: 16px; svg { @@ -41,9 +41,9 @@ const WrapperIcon = styled(Box)` } ` -const Check = ({ notValid }: { notValid: boolean }) => ( - - {notValid ? : } +const Check = ({ error }: { error: * }) => ( + + {error ? : } ) @@ -74,7 +74,7 @@ const PushButton = styled(Box)` ` type Props = { - notValid: boolean, + error: *, } const SVG = ( @@ -165,8 +165,8 @@ const SVG = ( const DeviceConfirm = (props: Props) => ( - {props.notValid ? : null} - + {!props.error ? : null} + {SVG} ) diff --git a/src/components/DeviceConfirm/stories.js b/src/components/DeviceConfirm/stories.js index 60d53d65..9e9b6d93 100644 --- a/src/components/DeviceConfirm/stories.js +++ b/src/components/DeviceConfirm/stories.js @@ -8,4 +8,4 @@ import DeviceConfirm from 'components/DeviceConfirm' const stories = storiesOf('Components', module) -stories.add('DeviceConfirm', () => ) +stories.add('DeviceConfirm', () => ) diff --git a/src/components/DeviceSignTransaction.js b/src/components/DeviceSignTransaction.js index 91ccaa31..fe1f35e0 100644 --- a/src/components/DeviceSignTransaction.js +++ b/src/components/DeviceSignTransaction.js @@ -5,23 +5,16 @@ import type { Device } from 'types/common' import type { WalletBridge } from 'bridge/types' type Props = { + children: *, onOperationBroadcasted: (op: Operation) => void, - render: ({ error: ?Error }) => React$Node, + onError: Error => void, device: Device, account: Account, bridge: WalletBridge<*>, transaction: *, } -type State = { - error: ?Error, -} - -class DeviceSignTransaction extends PureComponent { - state = { - error: null, - } - +class DeviceSignTransaction extends PureComponent { componentDidMount() { this.sign() } @@ -32,20 +25,17 @@ class DeviceSignTransaction extends PureComponent { unmount = false sign = async () => { - const { device, account, transaction, bridge, onOperationBroadcasted } = this.props + const { device, account, transaction, bridge, onOperationBroadcasted, onError } = this.props try { const optimisticOperation = await bridge.signAndBroadcast(account, transaction, device.path) onOperationBroadcasted(optimisticOperation) } catch (error) { - console.warn(error) - this.setState({ error }) + onError(error) } } render() { - const { render } = this.props - const { error } = this.state - return render({ error }) + return this.props.children } } diff --git a/src/components/RecipientAddress/index.js b/src/components/RecipientAddress/index.js index 745ca3b0..f96205c3 100644 --- a/src/components/RecipientAddress/index.js +++ b/src/components/RecipientAddress/index.js @@ -77,12 +77,13 @@ class RecipientAddress extends PureComponent { } render() { - const { onChange, withQrCode, value } = this.props + const { onChange, withQrCode, value, ...rest } = this.props const { qrReaderOpened } = this.state return ( { static defaultProps = { max: Infinity, + canBeSpent: true, withMax: true, } @@ -97,12 +100,13 @@ export class RequestAmount extends PureComponent { } 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 rightUnit = rightCurrency.units[0] return ( p.theme.colors.white}; 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')}; 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({ @@ -74,6 +85,7 @@ type Props = { renderLeft?: any, renderRight?: any, containerProps?: Object, + error?: string | boolean, small?: boolean, } @@ -145,7 +157,7 @@ class Input extends PureComponent { render() { const { isFocus } = this.state - const { renderLeft, renderRight, containerProps, small } = this.props + const { renderLeft, renderRight, containerProps, small, error } = this.props return ( { shrink {...containerProps} small={small} + error={error} > {renderLeft} @@ -166,6 +179,7 @@ class Input extends PureComponent { onChange={this.handleChange} onKeyDown={this.handleKeyDown} /> + {error && typeof error === 'string' ? {error} : null} {renderRight} diff --git a/src/components/modals/Receive/03-step-confirm-address.js b/src/components/modals/Receive/03-step-confirm-address.js index 46cf076f..e5bbe89a 100644 --- a/src/components/modals/Receive/03-step-confirm-address.js +++ b/src/components/modals/Receive/03-step-confirm-address.js @@ -44,7 +44,7 @@ export default (props: Props) => ( {props.t('receive:steps.confirmAddress.error.title')} {props.t('receive:steps.confirmAddress.error.text')} - + ) : ( @@ -58,7 +58,7 @@ export default (props: Props) => ( account={props.account} device={props.device} onCheck={props.onCheck} - render={({ isVerified }) => } + render={({ isVerified }) => } /> )} diff --git a/src/components/modals/Send/01-step-amount.js b/src/components/modals/Send/01-step-amount.js index b0efaec3..80a3a0bb 100644 --- a/src/components/modals/Send/01-step-amount.js +++ b/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 { 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' -import RequestAmount from 'components/RequestAmount' -import SelectAccount from 'components/SelectAccount' - -const AccountField = ({ onChange, value, t }: *) => ( - - - - -) - -// TODO we should use isRecipientValid & provide a feedback to user -const RecipientField = ({ bridge, account, transaction, onChangeTransaction, t }: *) => ( - - - { - 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 - }} - /> - -) - -const AmountField = ({ bridge, account, transaction, onChangeTransaction, t }: *) => ( - - - - onChangeTransaction(bridge.editTransactionAmount(account, transaction, amount)) - } - value={bridge.getTransactionAmount(account, transaction)} - /> - -) +import AccountField from './AccountField' +import RecipientField from './RecipientField' +import AmountField from './AmountField' type PropsStepAmount = { t: T, diff --git a/src/components/modals/Send/03-step-verification.js b/src/components/modals/Send/03-step-verification.js index 93a14fa9..d94677d4 100644 --- a/src/components/modals/Send/03-step-verification.js +++ b/src/components/modals/Send/03-step-verification.js @@ -35,10 +35,21 @@ type Props = { bridge: ?WalletBridge<*>, transaction: *, onOperationBroadcasted: (op: Operation) => void, + onError: (e: Error) => void, + hasError: boolean, t: T, } -export default ({ account, device, bridge, transaction, onOperationBroadcasted, t }: Props) => ( +export default ({ + account, + device, + bridge, + transaction, + onOperationBroadcasted, + t, + onError, + hasError, +}: Props) => ( {multiline(t('send:steps.verification.warning'))} {t('send:steps.verification.body')} @@ -52,11 +63,10 @@ export default ({ account, device, bridge, transaction, onOperationBroadcasted, transaction={transaction} bridge={bridge} onOperationBroadcasted={onOperationBroadcasted} - render={({ error }) => ( - // FIXME we really really REALLY should use error for the display. otherwise we are completely blind on error cases.. - - )} - /> + onError={onError} + > + + )} ) diff --git a/src/components/modals/Send/04-step-confirmation.js b/src/components/modals/Send/04-step-confirmation.js index c897ad35..50301761 100644 --- a/src/components/modals/Send/04-step-confirmation.js +++ b/src/components/modals/Send/04-step-confirmation.js @@ -8,6 +8,7 @@ import IconExclamationCircleThin from 'icons/ExclamationCircleThin' import Box from 'components/base/Box' import { multiline } from 'styles/helpers' import { colors } from 'styles/theme' +import { formatError } from 'helpers/errors' import type { T } from 'types/common' @@ -39,10 +40,11 @@ const Text = styled(Box).attrs({ type Props = { optimisticOperation: ?Operation, t: T, + error: ?Error, } function StepConfirmation(props: Props) { - const { t, optimisticOperation } = props + const { t, optimisticOperation, error } = props const Icon = optimisticOperation ? IconCheckCircle : IconExclamationCircleThin const iconColor = optimisticOperation ? colors.positiveGreen : colors.alertRed const tPrefix = optimisticOperation @@ -55,7 +57,9 @@ function StepConfirmation(props: Props) { {t(`${tPrefix}.title`)} - {multiline(t(`${tPrefix}.text`))} + + {optimisticOperation ? multiline(t(`${tPrefix}.text`)) : error ? formatError(error) : null} + {optimisticOperation ? optimisticOperation.hash : ''} diff --git a/src/components/modals/Send/AccountField.js b/src/components/modals/Send/AccountField.js new file mode 100644 index 00000000..9e6aec1b --- /dev/null +++ b/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 }: *) => ( + + + + +) + +export default AccountField diff --git a/src/components/modals/Send/AmountField.js b/src/components/modals/Send/AmountField.js new file mode 100644 index 00000000..896cb25c --- /dev/null +++ b/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 ( + + + + onChangeTransaction(bridge.editTransactionAmount(account, transaction, amount)) + } + value={bridge.getTransactionAmount(account, transaction)} + /> + + ) + } +} + +export default AmountField diff --git a/src/components/modals/Send/Footer.js b/src/components/modals/Send/Footer.js index 83d50081..1dee1bc1 100644 --- a/src/components/modals/Send/Footer.js +++ b/src/components/modals/Send/Footer.js @@ -54,7 +54,7 @@ class Footer extends PureComponent< this.resync() } } - async componentWillUnmount() { + componentWillUnmount() { this.unmount = true } unmount = false @@ -77,7 +77,7 @@ class Footer extends PureComponent< {')'} @@ -102,7 +103,7 @@ class Footer extends PureComponent< )} - diff --git a/src/components/modals/Send/RecipientField.js b/src/components/modals/Send/RecipientField.js new file mode 100644 index 00000000..505f8032 --- /dev/null +++ b/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 = { + t: T, + account: Account, + bridge: WalletBridge, + transaction: Transaction, + onChangeTransaction: Transaction => void, +} + +class RecipientField extends Component, { isValid: boolean }> { + state = { + isValid: 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 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 ( + + + { + 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 + }} + /> + + ) + } +} + +export default RecipientField diff --git a/src/components/modals/Send/SendModalBody.js b/src/components/modals/Send/SendModalBody.js index af26f6de..00f64486 100644 --- a/src/components/modals/Send/SendModalBody.js +++ b/src/components/modals/Send/SendModalBody.js @@ -43,6 +43,7 @@ type State = { appStatus: ?string, deviceSelected: ?Device, optimisticOperation: ?Operation, + error: ?Error, } type Step = { @@ -74,6 +75,7 @@ class SendModalBody extends PureComponent> { account, bridge, transaction, + error: null, } this.steps = [ @@ -97,7 +99,7 @@ class SendModalBody extends PureComponent> { }, { label: t('send:steps.confirmation.title'), - prevStep: 1, + prevStep: 0, }, ] } @@ -142,9 +144,20 @@ class SendModalBody extends PureComponent> { this.setState({ optimisticOperation, 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 => { const bridge = getBridgeForCurrency(account.currency) this.setState({ @@ -159,7 +172,7 @@ class SendModalBody extends PureComponent> { } onGoToFirstStep = () => { - this.setState({ stepIndex: 0 }) + this.setState({ stepIndex: 0, error: null }) } steps: Step[] @@ -173,6 +186,7 @@ class SendModalBody extends PureComponent> { bridge, optimisticOperation, deviceSelected, + error, } = this.state const step = this.steps[stepIndex] @@ -216,9 +230,11 @@ class SendModalBody extends PureComponent> { transaction={transaction} device={deviceSelected} onOperationBroadcasted={this.onOperationBroadcasted} + onError={this.onOperationError} + hasError={!!error} /> - + diff --git a/src/helpers/errors.js b/src/helpers/errors.js new file mode 100644 index 00000000..9e7bd079 --- /dev/null +++ b/src/helpers/errors.js @@ -0,0 +1,3 @@ +// @flow + +export const formatError = (e: Error) => e.message diff --git a/src/helpers/promise.js b/src/helpers/promise.js index 210c94ae..6cebf415 100644 --- a/src/helpers/promise.js +++ b/src/helpers/promise.js @@ -20,8 +20,9 @@ export function retry(f: () => Promise, options?: $Shape) return result } // In case of failure, wait the interval, retry the action - return result.catch(() => - delay(interval).then(() => rec(remainingTry - 1, interval * intervalMultiplicator)), - ) + return result.catch(e => { + console.warn('Promise#retry', e) + return delay(interval).then(() => rec(remainingTry - 1, interval * intervalMultiplicator)) + }) } } diff --git a/src/internals/index.js b/src/internals/index.js index 2e5fe74d..a2d77efe 100644 --- a/src/internals/index.js +++ b/src/internals/index.js @@ -39,6 +39,7 @@ process.on('message', m => { type: 'ERROR', requestId, data: { + ...error, name: error && error.name, message: error && error.message, }, diff --git a/static/i18n/en/send.yml b/static/i18n/en/send.yml index 825618a7..dd9047e8 100644 --- a/static/i18n/en/send.yml +++ b/static/i18n/en/send.yml @@ -23,12 +23,10 @@ steps: confirmation: title: Confirmation success: - title: Transaction successfully completed - text: You may have to wait few confirmations unitl the transaction appear + title: Transaction successfully broadcasted + text: | + with the following transaction id: cta: View operation details error: - title: Transaction aborted - text: | - The transaction have been aborted on your device. - You can try again the operation. + title: Transaction error cta: Retry operation