Skip to content

Commit

Permalink
feat(coinify checkout validations)
Browse files Browse the repository at this point in the history
  • Loading branch information
sixtedemaupeou committed May 25, 2018
1 parent 19c854f commit 1cfaecd
Show file tree
Hide file tree
Showing 17 changed files with 99 additions and 148 deletions.
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React from 'react'
import styled from 'styled-components'

import { equals } from 'ramda'
import { Text, TextInput } from 'blockchain-info-components'
import _debounce from 'lodash.debounce'

const Container = styled.div`
position: relative;
Expand All @@ -25,54 +25,60 @@ const getErrorState = (meta) => {
}

class TextBoxDebounced extends React.Component {
static getDerivedStateFromProps (nextProps, prevState) {
if (!equals(nextProps.input.value, prevState)) {
return { value: nextProps.input.value }
}
return null
}

constructor (props) {
super(props)

this.state = { value: props.input.value }
this.lastPropValue = props.input.value

this.debouncedOnChange = _debounce(event => {
props.input.onChange(event.target.value)
}, 500)

this.handleChange = event => {
event.persist()
this.setState({ value: event.target.value })
this.debouncedOnChange(event)
}
this.timeout = undefined
this.handleChange = this.handleChange.bind(this)
this.handleBlur = this.handleBlur.bind(this)
this.handleFocus = this.handleFocus.bind(this)
}

getValue () {
const value = this.props.input.value !== this.lastPropValue
? this.props.input.value
: this.state.value
componentWillUnmount () {
clearTimeout(this.timeout)
}

this.lastPropValue = this.props.input.value
handleChange (e) {
e.preventDefault()
const value = e.target.value
this.setState({ value })
if (this.timeout) clearTimeout(this.timeout)
this.timeout = setTimeout(() => {
this.props.input.onChange(value)
}, 500)
}

return value
handleBlur () {
this.props.input.onBlur(this.state.value)
}

handleFocus () {}
handleFocus () {
this.props.input.onFocus(this.state.value)
}

render () {
const { input, meta, borderRightNone, disabled, placeholder, center, errorBottom, autoFocus } = this.props
const { meta, disabled, placeholder } = this.props
const errorState = getErrorState(meta)

return (
<Container>
<TextInput {...input}
onChange={this.handleChange}
value={this.getValue()}
borderRightNone={borderRightNone}
autoFocus={autoFocus}
<TextInput
value={this.state.value}
errorState={errorState}
disabled={disabled}
initial={meta.initial}
placeholder={placeholder}
center={center}
onFocus={this.handleFocus()}
onChange={this.handleChange}
onFocus={this.handleFocus}
onBlur={this.handleBlur}
/>
{meta.touched && meta.error && <Error size='12px' weight={300} color='error' errorBottom={errorBottom}>{meta.error}</Error>}
{meta.touched && meta.error && <Error size='12px' weight={300} color='error'>{meta.error}</Error>}
</Container>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,6 @@ export const COINIFY_INITIALIZED = '@COMPONENT.COINIFY_INITIALIZED'
export const COINIFY_CHECKOUT_BUSY_ON = '@COMPONENT.COINIFY_CHECKOUT_BUSY_ON'
export const COINIFY_CHECKOUT_BUSY_OFF = '@COMPONENT.COINIFY_CHECKOUT_BUSY_OFF'

export const COINIFY_SET_CHECKOUT_MAX = '@COMPONENT.COINIFY_SET_CHECKOUT_MAX'
export const COINIFY_SET_CHECKOUT_MIN = '@COMPONENT.COINIFY_SET_CHECKOUT_MIN'

export const COINIFY_SET_CHECKOUT_ERROR = '@COMPONENT.COINIFY_SET_CHECKOUT_ERROR'
export const COINIFY_CLEAR_CHECKOUT_ERROR = '@COMPONENT.COINIFY_CLEAR_CHECKOUT_ERROR'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,6 @@ export const initializeCheckoutForm = (type) => ({ type: AT.COINIFY_INITIALIZED,
export const coinifyCheckoutBusyOn = () => ({ type: AT.COINIFY_CHECKOUT_BUSY_ON })
export const coinifyCheckoutBusyOff = () => ({ type: AT.COINIFY_CHECKOUT_BUSY_OFF })

export const setCheckoutMax = (amount, type) => ({ type: AT.COINIFY_SET_CHECKOUT_MAX, payload: { amount, type } })
export const setCheckoutMin = (amount, type) => ({ type: AT.COINIFY_SET_CHECKOUT_MIN, payload: { amount, type } })

export const setCoinifyCheckoutError = (error) => ({ type: AT.COINIFY_SET_CHECKOUT_ERROR, payload: error })
export const clearCoinifyCheckoutError = () => ({ type: AT.COINIFY_CLEAR_CHECKOUT_ERROR })

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ export default ({ coreSagas }) => {
yield takeLatest(AT.COINIFY_SELL, coinifySagas.sell)
yield takeLatest(actionTypes.CHANGE, coinifySagas.handleChange)
yield takeLatest(AT.COINIFY_INITIALIZED, coinifySagas.initialized)
yield takeLatest(AT.COINIFY_SET_CHECKOUT_MAX, coinifySagas.setMinMax)
yield takeLatest(AT.COINIFY_SET_CHECKOUT_MIN, coinifySagas.setMinMax)
yield takeLatest(AT.COINIFY_FROM_ISX, coinifySagas.fromISX)
yield takeLatest(AT.COINIFY_TRIGGER_KYC, coinifySagas.triggerKYC)
yield takeLatest(AT.OPEN_KYC, coinifySagas.openKYC)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,6 @@ export default ({ coreSagas }) => {
} else {
payment = yield payment.to(trade.receiveAddress)
}
payment = yield payment.to(trade.receiveAddress)
payment = yield payment.description(`Exchange Trade COINIFY=${trade.id}`)

try {
Expand All @@ -104,7 +103,6 @@ export default ({ coreSagas }) => {
yield put(A.coinifySuccess())
yield put(actions.form.change('buySellTabStatus', 'status', 'order_history'))
} catch (e) {
console.log('Error in coinifysell', e)
yield put(A.coinifyFailure(e))
yield put(actions.logs.logErrorMessage(logLocation, 'sell', e))
}
Expand Down Expand Up @@ -160,6 +158,15 @@ export default ({ coreSagas }) => {
const leftResult = yield call(coreSagas.data.coinify.fetchQuote,
{ quote: { amount: payload * 100, baseCurrency: values.currency, quoteCurrency: 'BTC', type } })
const amount = Math.abs(leftResult.quoteAmount)

if (type === 'sell') {
const payment = yield select(sendBtcSelectors.getPayment)
const effectiveBalance = prop('effectiveBalance', payment.getOrElse(undefined))
if (service.isOverEffectiveMax(amount, effectiveBalance)) {
yield put(A.setCoinifyCheckoutError('over_effective_max'))
}
}

yield put(actions.form.initialize(form, merge(values, { 'rightVal': amount / 1e8 })))
yield put(A.coinifyCheckoutBusyOff())
break
Expand Down Expand Up @@ -188,23 +195,6 @@ export default ({ coreSagas }) => {
}
}

const setMinMax = function * (action) {
try {
yield put(A.coinifyCheckoutBusyOn())
const { amount, type } = action.payload
const form = type === 'buy' ? 'coinifyCheckoutBuy' : 'coinifyCheckoutSell'
const values = yield select(selectors.form.getFormValues(form))
const leftResult = yield call(coreSagas.data.coinify.fetchQuote,
{ quote: { amount: amount * 100, baseCurrency: values.currency, quoteCurrency: 'BTC', type } })
yield put(actions.form.initialize(form,
merge(values, { 'leftVal': amount, 'rightVal': Math.abs(leftResult.quoteAmount) / 1e8 })))
yield put(A.coinifyCheckoutBusyOff())
yield put(A.clearCoinifyCheckoutError())
} catch (e) {
yield put(actions.logs.logErrorMessage(logLocation, 'setCheckoutMax', e))
}
}

const fromISX = function * (action) {
const status = action.payload
try {
Expand Down Expand Up @@ -315,7 +305,6 @@ export default ({ coreSagas }) => {
openKYC,
sell,
triggerKYC,
setMinMax,
cancelISX,
finishTrade,
cancelTrade
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ class CoinifyBuyContainer extends React.Component {
fetchBuyQuote={quote => fetchQuote({ quote, nextAddress: value.nextAddress })}
currency={currency}
checkoutBusy={checkoutBusy}
setMax={(amt) => this.props.coinifyActions.setCheckoutMax(amt, 'buy')}
setMin={(amt) => this.props.coinifyActions.setCheckoutMin(amt, 'buy')}
setMax={(amt) => formActions.change('coinifyCheckoutBuy', 'leftVal', amt)}
setMin={(amt) => formActions.change('coinifyCheckoutBuy', 'leftVal', amt)}
paymentMedium={paymentMedium}
initiateBuy={this.startBuy}
step={step}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { StepTransition } from 'components/Utilities/Stepper'
import QuoteInput from './QuoteInput'
import { MethodContainer } from 'components/BuySell/styled.js'

const OrderCheckout = ({ quoteR, rateQuoteR, account, onFetchQuote, reason, limits,
const OrderCheckout = ({ quoteR, rateQuoteR, account, onFetchQuote, reason, limits, checkoutError,
type, defaultCurrency, symbol, checkoutBusy, busy, setMax, setMin, increaseLimit, onOrderCheckoutSubmit }) => {
const quoteInputSpec = {
method: type, // buy or sell
Expand All @@ -20,22 +20,10 @@ const OrderCheckout = ({ quoteR, rateQuoteR, account, onFetchQuote, reason, limi
? <FormattedMessage id='buy.output_method.title.buy' defaultMessage='I want to buy' />
: <FormattedMessage id='buy.output_method.title.sell' defaultMessage='I want to sell' />

const limitsHelper = (quoteR, limits) => {
if (quoteR.error) return true
return quoteR.map(q => {
if (q.baseCurrency === 'USD') {
return +q.baseAmount > limits.max || +q.baseAmount < limits.min || +q.quoteAmount > limits.effectiveMax
}
if (q.baseCurrency === 'BTC') {
return Math.abs(q.quoteAmount) > limits.max || Math.abs(q.quoteAmount) < limits.min || +q.baseAmount > limits.effectiveMax
}
}).data
}

const submitButtonHelper = () => (
reason.indexOf('has_remaining') > -1
? <StepTransition next Component={Button} onClick={onOrderCheckoutSubmit} style={spacing('mt-45')}
nature='primary' fullwidth disabled={checkoutBusy || Remote.Loading.is(quoteR) || limitsHelper(quoteR, limits)}>
nature='primary' fullwidth disabled={checkoutBusy || Remote.Loading.is(quoteR) || checkoutError}>
{
Remote.Loading.is(quoteR)
? <HeartbeatLoader height='20px' width='20px' color='white' />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { QuoteInputTemplateBuy, QuoteInputTemplateSell } from './QuoteInputTemplate'
import { actions, selectors } from 'data'
import { actions } from 'data'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import { path } from 'ramda'
Expand All @@ -26,7 +26,6 @@ class QuoteInput extends Component {
setMin={setMin}
checkoutError={checkoutError}
increaseLimit={increaseLimit}
type={type}
/>
},
Failure: (msg) => <div>Failure: {msg.error}</div>,
Expand All @@ -44,7 +43,7 @@ QuoteInput.propTypes = {
checkoutError: PropTypes.oneOfType([PropTypes.string, PropTypes.bool])
}

const mapStateToProps = state => ({
const mapStateToProps = (state, ownProps) => ({
checkoutError: path(['coinify', 'checkoutError'], state),
data: getQuoteInputData(state)
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { FormattedMessage } from 'react-intl'
import { Icon, Text } from 'blockchain-info-components'
import { SelectBoxCoinifyCurrency, TextBoxDebounced } from 'components/Form'
import { Field, reduxForm } from 'redux-form'
import { head } from 'ramda'
import { has, head, prop } from 'ramda'
import { getReasonExplanation } from 'services/CoinifyService'

const Wrapper = styled.div`
Expand Down Expand Up @@ -77,13 +77,16 @@ const LimitsHelper = styled.div`
`

const getLimitsError = (errorType, limits, symbol, setMin) => {
if (errorType === 'below_min') return `Your limit of ${symbol}${limits.max} is below the minimum allowed amount.`
if (errorType === 'over_max') return `Enter an amount under your ${symbol}${limits.max.toLocaleString()} limit`
if (errorType === 'under_min') return <FormattedMessage id='buy.quote_input.under_min' defaultMessage='Enter an amount above the {setMin} minimum' values={{ setMin: <a onClick={() => setMin(limits.min)}>{symbol}{limits.min.toLocaleString()}</a> }} />
switch (errorType) {
case 'below_min': return `Your limit of ${symbol}${limits.max} is below the minimum allowed amount.`
case 'over_max': return `Enter an amount under your ${symbol}${limits.max.toLocaleString()} limit`
case 'under_min': return <FormattedMessage id='buy.quote_input.under_min' defaultMessage='Enter an amount above the {setMin} minimum' values={{ setMin: <a onClick={() => setMin(limits.min)}>{symbol}{limits.min.toLocaleString()}</a> }} />
case 'over_effective_max': return `Enter an amount less than your balance minus the priority fee (${limits.effectiveMax / 1e8} BTC)`
}
}

const FiatConvertor = (props) => {
const { val, disabled, setMax, setMin, limits, checkoutError, defaultCurrency, symbol, increaseLimit, type } = props
const { val, disabled, setMax, setMin, limits, checkoutError, defaultCurrency, symbol, increaseLimit } = props
const currency = 'BTC'
const level = val.level || { name: 1 }
const kyc = val.kycs.length && head(val.kycs)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import React from 'react'
import { connect } from 'react-redux'

import AddBankDetails from './template.js'

Expand All @@ -9,10 +8,4 @@ class AddBankDetailsContainer extends React.PureComponent {
}
}

const mapStateToProps = (state) => ({
})

const mapDispatchToProps = (dispatch) => ({
})

export default connect(mapStateToProps, mapDispatchToProps)(AddBankDetailsContainer)
export default AddBankDetailsContainer
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ class SellContainer extends React.Component {
}

render () {
const { data, modalActions, coinifyActions, coinifyDataActions,
const { data, modalActions, coinifyActions, coinifyDataActions, formActions,
rateQuoteR, sellQuoteR, currency, paymentMedium, trade, ...rest } = this.props
const { step, checkoutBusy, coinifyBusy } = rest
const { step, checkoutBusy, coinifyBusy, checkoutError } = rest
const { handleTrade, fetchQuote } = coinifyDataActions
const { showModal } = modalActions
const { coinifyNotAsked } = coinifyActions
Expand All @@ -45,7 +45,7 @@ class SellContainer extends React.Component {
})

return data.cata({
Success: (value) => <Success {...this.props}
Success: (value) => <Success
value={value}
handleTrade={handleTrade}
showModal={showModal}
Expand All @@ -54,15 +54,16 @@ class SellContainer extends React.Component {
fetchSellQuote={(quote) => fetchQuote({ quote, nextAddress: value.nextAddress })}
currency={currency}
checkoutBusy={checkoutBusy}
setMax={(amt) => coinifyActions.setCheckoutMax(amt, 'sell')}
setMin={(amt) => coinifyActions.setCheckoutMin(amt, 'sell')}
setMax={(amt) => formActions.change('coinifyCheckoutSell', 'leftVal', amt)}
setMin={(amt) => formActions.change('coinifyCheckoutSell', 'leftVal', amt)}
paymentMedium={paymentMedium}
initiateSell={this.startSell}
step={step}
busy={busy}
clearTradeError={() => coinifyNotAsked()}
trade={trade}
onOrderCheckoutSubmit={this.submitQuote}
checkoutError={checkoutError}
/>,
Failure: (msg) => <div>Failure: {msg.error}</div>,
Loading: () => <Loading />,
Expand Down

0 comments on commit 1cfaecd

Please sign in to comment.