From c208db0d3ecb1204024751a5796636a51846ce4c Mon Sep 17 00:00:00 2001 From: Jack Mallers Date: Mon, 2 Oct 2017 14:08:56 -0500 Subject: [PATCH] feature(payform): add form validation to payform --- app/components/Form/PayForm.js | 27 ++++++++++-- app/components/Form/PayForm.scss | 43 ++++++++++++++++++- app/reducers/payform.js | 51 +++++++++++++++++++---- app/routes/app/containers/AppContainer.js | 40 ++++++++++++++++-- package.json | 1 + 5 files changed, 147 insertions(+), 15 deletions(-) diff --git a/app/components/Form/PayForm.js b/app/components/Form/PayForm.js index 4a1edc8f..3d93da5f 100644 --- a/app/components/Form/PayForm.js +++ b/app/components/Form/PayForm.js @@ -21,7 +21,7 @@ class PayForm extends Component { render() { const { - payform: { amount, payInput }, + payform: { amount, payInput, showErrors }, currency, crypto, @@ -30,9 +30,13 @@ class PayForm extends Component { currentAmount, inputCaption, showPayLoadingScreen, + payFormIsValid: { errors, isValid }, setPayAmount, + onPayAmountBlur, + setPayInput, + onPayInputBlur, onPaySubmit } = this.props @@ -41,7 +45,12 @@ class PayForm extends Component {
{showPayLoadingScreen && } -
+
+
+ {showErrors.amount && + {errors.amount} + } +
@@ -58,6 +67,7 @@ class PayForm extends Component { } value={currentAmount} onChange={event => setPayAmount(event.target.value)} + onBlur={onPayAmountBlur} id='amount' readOnly={isLn} /> @@ -80,18 +90,24 @@ class PayForm extends Component { } -
+
setPayInput(event.target.value)} + onBlur={onPayInputBlur} id='paymentRequest' />
+
+ {showErrors.payInput && + {errors.payInput} + } +
-
Pay
+
Pay
) @@ -112,9 +128,12 @@ PayForm.propTypes = { ]).isRequired, inputCaption: PropTypes.string.isRequired, showPayLoadingScreen: PropTypes.bool.isRequired, + payFormIsValid: PropTypes.object.isRequired, setPayAmount: PropTypes.func.isRequired, + onPayAmountBlur: PropTypes.func.isRequired, setPayInput: PropTypes.func.isRequired, + onPayInputBlur: PropTypes.func.isRequired, fetchInvoice: PropTypes.func.isRequired, onPaySubmit: PropTypes.func.isRequired diff --git a/app/components/Form/PayForm.scss b/app/components/Form/PayForm.scss index d50710ad..572434c8 100644 --- a/app/components/Form/PayForm.scss +++ b/app/components/Form/PayForm.scss @@ -10,16 +10,35 @@ } .amountContainer { + position: relative; color: $main; display: flex; justify-content: center; min-height: 120px; margin-bottom: 20px; min-height: 175px; + border-bottom: 1px solid transparent; &.ln { opacity: 0.75; } + + &.error { + border-color: $red; + } + + .amountError { + position: absolute; + top: 0; + right: 0; + opacity: 0; + color: $red; + transition: all 0.25s ease; + + &.active { + opacity: 1; + } + } label, input[type=number], input[type=text] { color: inherit; @@ -92,6 +111,10 @@ position: relative; padding: 0 20px; + &.error { + border-color: $red; + } + label, input[type=number], input[type=text] { font-size: inherit; } @@ -112,6 +135,18 @@ } } +.payInputError { + margin: 10px 0; + min-height: 20px; + color: $red; + opacity: 0; + transition: all 0.25s ease; + + &.active { + opacity: 1; + } +} + .buttonGroup { width: 100%; display: flex; @@ -120,7 +155,6 @@ overflow: hidden; .button { - cursor: pointer; height: 55px; min-height: 55px; text-transform: none; @@ -134,9 +168,16 @@ width: 100%; text-align: center; line-height: 55px; + opacity: 0.5; + cursor: default; &:first-child { border-right: 1px solid lighten($main, 20%); } + + &.active { + opacity: 1; + cursor: pointer; + } } } \ No newline at end of file diff --git a/app/reducers/payform.js b/app/reducers/payform.js index b7e8bcbe..e71b5aab 100644 --- a/app/reducers/payform.js +++ b/app/reducers/payform.js @@ -1,5 +1,8 @@ import { createSelector } from 'reselect' import bitcoin from 'bitcoinjs-lib' + +import isEmpty from 'lodash/isEmpty' + import { tickerSelectors } from './ticker' import { btc, bech32 } from '../utils' @@ -12,6 +15,11 @@ const initialState = { payreq: '', r_hash: '', amount: '0' + }, + + showErrors: { + amount: false, + payInput: false } } @@ -21,6 +29,8 @@ export const SET_PAY_AMOUNT = 'SET_PAY_AMOUNT' export const SET_PAY_INPUT = 'SET_PAY_INPUT' export const SET_PAY_INVOICE = 'SET_PAY_INVOICE' +export const UPDATE_PAY_ERRORS = 'UPDATE_PAY_ERRORS' + export const RESET_FORM = 'RESET_FORM' // ------------------------------------ @@ -47,9 +57,10 @@ export function setPayInvoice(invoice) { } } -export function resetPayForm() { +export function updatePayErrors(errorsObject) { return { - type: RESET_FORM + type: UPDATE_PAY_ERRORS, + errorsObject } } @@ -57,9 +68,11 @@ export function resetPayForm() { // Action Handlers // ------------------------------------ const ACTION_HANDLERS = { - [SET_PAY_AMOUNT]: (state, { amount }) => ({ ...state, amount }), - [SET_PAY_INPUT]: (state, { payInput }) => ({ ...state, payInput }), - [SET_PAY_INVOICE]: (state, { invoice }) => ({ ...state, invoice }), + [SET_PAY_AMOUNT]: (state, { amount }) => ({ ...state, amount, showErrors: Object.assign(state.showErrors, { amount: false }) }), + [SET_PAY_INPUT]: (state, { payInput }) => ({ ...state, payInput, showErrors: Object.assign(state.showErrors, { payInput: false }) }), + [SET_PAY_INVOICE]: (state, { invoice }) => ({ ...state, invoice, showErrors: Object.assign(state.showErrors, { amount: false }) }), + + [UPDATE_PAY_ERRORS]: (state, { errorsObject }) => ({ ...state, showErrors: Object.assign(state.showErrors, errorsObject) }), [RESET_FORM]: () => (initialState) } @@ -75,7 +88,7 @@ const payInvoiceSelector = state => state.payform.invoice // transaction const sendingTransactionSelector = state => state.transaction.sendingTransaction -// transaction +// payment const sendingPaymentSelector = state => state.payment.sendingPayment // ticker @@ -84,7 +97,6 @@ const currencySelector = state => state.ticker.currency payFormSelectors.isOnchain = createSelector( payInputSelector, (input) => { - // TODO: work with bitcoin-js to fix p2wkh error and make testnet/mainnet dynamic try { bitcoin.address.toOutputScript(input, bitcoin.networks.testnet) return true @@ -149,6 +161,31 @@ payFormSelectors.showPayLoadingScreen = createSelector( (sendingTransaction, sendingPayment) => sendingTransaction || sendingPayment ) +payFormSelectors.payFormIsValid = createSelector( + payFormSelectors.isOnchain, + payFormSelectors.isLn, + payAmountSelector, + payInputSelector, + (isOnchain, isLn, amount, invoice) => { + let errors = {} + + if (!isLn && amount <= 0) { + errors.amount = 'Amount must be more than 0' + } + + if (!isOnchain && !isLn) { + errors.payInput = 'Must be a valid BTC address or Lightning Network request' + } + + return { + errors, + amountIsValid: isEmpty(errors.amount), + payInputIsValid: isEmpty(errors.payInput), + isValid: isEmpty(errors) + } + } +) + export { payFormSelectors } // ------------------------------------ diff --git a/app/routes/app/containers/AppContainer.js b/app/routes/app/containers/AppContainer.js index 17782a5a..233f4313 100644 --- a/app/routes/app/containers/AppContainer.js +++ b/app/routes/app/containers/AppContainer.js @@ -7,7 +7,7 @@ import { fetchInfo } from 'reducers/info' import { showModal, hideModal } from 'reducers/modal' import { setFormType } from 'reducers/form' -import { setPayAmount, setPayInput, payFormSelectors } from 'reducers/payform' +import { setPayAmount, setPayInput, updatePayErrors, payFormSelectors } from 'reducers/payform' import { setRequestAmount, setRequestMemo } from 'reducers/requestform' import { sendCoins } from 'reducers/transaction' @@ -31,6 +31,8 @@ const mapDispatchToProps = { setPayAmount, setPayInput, + updatePayErrors, + setRequestAmount, setRequestMemo, @@ -59,7 +61,8 @@ const mapStateToProps = state => ({ isLn: payFormSelectors.isLn(state), currentAmount: payFormSelectors.currentAmount(state), inputCaption: payFormSelectors.inputCaption(state), - showPayLoadingScreen: payFormSelectors.showPayLoadingScreen(state) + showPayLoadingScreen: payFormSelectors.showPayLoadingScreen(state), + payFormIsValid: payFormSelectors.payFormIsValid(state) }) const mergeProps = (stateProps, dispatchProps, ownProps) => { @@ -73,14 +76,45 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => { currentAmount: stateProps.currentAmount, inputCaption: stateProps.inputCaption, showPayLoadingScreen: stateProps.showPayLoadingScreen, + payFormIsValid: stateProps.payFormIsValid, setPayAmount: dispatchProps.setPayAmount, setPayInput: dispatchProps.setPayInput, fetchInvoice: dispatchProps.fetchInvoice, + onPayAmountBlur: () => { + // If the input is now valid and showErrors was on, turn it off + if (stateProps.payFormIsValid.amountIsValid && stateProps.payform.showErrors.amount) { + dispatchProps.updatePayErrors({ amount: false }) + } + + // If the input is not valid and showErrors was off, turn it on + if (!stateProps.payFormIsValid.amountIsValid && !stateProps.payform.showErrors.amount) { + dispatchProps.updatePayErrors({ amount: true }) + } + }, + + onPayInputBlur: () => { + // If the input is now valid and showErrors was on, turn it off + if (stateProps.payFormIsValid.payInputIsValid && stateProps.payform.showErrors.payInput) { + dispatchProps.updatePayErrors({ payInput: false }) + } + + // If the input is not valid and showErrors was off, turn it on + if (!stateProps.payFormIsValid.payInputIsValid && !stateProps.payform.showErrors.payInput) { + dispatchProps.updatePayErrors({ payInput: true }) + } + }, onPaySubmit: () => { - if (!stateProps.isOnchain && !stateProps.isLn) { return } + if (!stateProps.payFormIsValid.isValid) { + dispatchProps.updatePayErrors({ + amount: Object.prototype.hasOwnProperty.call(stateProps.payFormIsValid.errors, 'amount'), + payInput: Object.prototype.hasOwnProperty.call(stateProps.payFormIsValid.errors, 'payInput') + }) + + return + } if (stateProps.isOnchain) { dispatchProps.sendCoins({ diff --git a/package.json b/package.json index fdf4f4c3..846a1e6a 100644 --- a/package.json +++ b/package.json @@ -195,6 +195,7 @@ "font-awesome": "^4.7.0", "grpc": "^1.4.1", "history": "^4.6.3", + "lodash": "^4.17.4", "moment-timezone": "^0.5.13", "prop-types": "^15.5.10", "qrcode.react": "^0.7.1",