Browse Source

Merge pull request #55 from LN-Zap/feature/payform-error-handling

feature(payform): add form validation to payform
renovate/lint-staged-8.x
jackmallers 7 years ago
committed by GitHub
parent
commit
1e333f2262
  1. 27
      app/components/Form/PayForm.js
  2. 43
      app/components/Form/PayForm.scss
  3. 50
      app/reducers/payform.js
  4. 40
      app/routes/app/containers/AppContainer.js
  5. 1
      package.json
  6. 3
      test/components/Form.spec.js

27
app/components/Form/PayForm.js

@ -21,7 +21,7 @@ class PayForm extends Component {
render() { render() {
const { const {
payform: { amount, payInput }, payform: { amount, payInput, showErrors },
currency, currency,
crypto, crypto,
@ -30,9 +30,13 @@ class PayForm extends Component {
currentAmount, currentAmount,
inputCaption, inputCaption,
showPayLoadingScreen, showPayLoadingScreen,
payFormIsValid: { errors, isValid },
setPayAmount, setPayAmount,
onPayAmountBlur,
setPayInput, setPayInput,
onPayInputBlur,
onPaySubmit onPaySubmit
} = this.props } = this.props
@ -41,7 +45,12 @@ class PayForm extends Component {
<div className={styles.container}> <div className={styles.container}>
{showPayLoadingScreen && <LoadingBolt />} {showPayLoadingScreen && <LoadingBolt />}
<section className={`${styles.amountContainer} ${isLn ? styles.ln : ''}`}> <section className={`${styles.amountContainer} ${isLn ? styles.ln : ''} ${showErrors.amount && styles.error}`}>
<section className={`${styles.amountError} ${showErrors.amount && styles.active}`}>
{showErrors.amount &&
<span>{errors.amount}</span>
}
</section>
<label htmlFor='amount'> <label htmlFor='amount'>
<CurrencyIcon currency={currency} crypto={crypto} /> <CurrencyIcon currency={currency} crypto={crypto} />
</label> </label>
@ -58,6 +67,7 @@ class PayForm extends Component {
} }
value={currentAmount} value={currentAmount}
onChange={event => setPayAmount(event.target.value)} onChange={event => setPayAmount(event.target.value)}
onBlur={onPayAmountBlur}
id='amount' id='amount'
readOnly={isLn} readOnly={isLn}
/> />
@ -80,18 +90,24 @@ class PayForm extends Component {
</i> </i>
} }
</aside> </aside>
<section className={styles.input}> <section className={`${styles.input} ${showErrors.payInput && styles.error}`}>
<input <input
type='text' type='text'
placeholder='Payment request or bitcoin address' placeholder='Payment request or bitcoin address'
value={payInput} value={payInput}
onChange={event => setPayInput(event.target.value)} onChange={event => setPayInput(event.target.value)}
onBlur={onPayInputBlur}
id='paymentRequest' id='paymentRequest'
/> />
</section> </section>
<section className={`${styles.payInputError} ${showErrors.payInput && styles.active}`}>
{showErrors.payInput &&
<span>{errors.payInput}</span>
}
</section>
</div> </div>
<section className={styles.buttonGroup}> <section className={styles.buttonGroup}>
<div className={styles.button} onClick={onPaySubmit}>Pay</div> <div className={`${styles.button} ${isValid && styles.active}`} onClick={onPaySubmit}>Pay</div>
</section> </section>
</div> </div>
) )
@ -112,9 +128,12 @@ PayForm.propTypes = {
]).isRequired, ]).isRequired,
inputCaption: PropTypes.string.isRequired, inputCaption: PropTypes.string.isRequired,
showPayLoadingScreen: PropTypes.bool.isRequired, showPayLoadingScreen: PropTypes.bool.isRequired,
payFormIsValid: PropTypes.object.isRequired,
setPayAmount: PropTypes.func.isRequired, setPayAmount: PropTypes.func.isRequired,
onPayAmountBlur: PropTypes.func.isRequired,
setPayInput: PropTypes.func.isRequired, setPayInput: PropTypes.func.isRequired,
onPayInputBlur: PropTypes.func.isRequired,
fetchInvoice: PropTypes.func.isRequired, fetchInvoice: PropTypes.func.isRequired,
onPaySubmit: PropTypes.func.isRequired onPaySubmit: PropTypes.func.isRequired

43
app/components/Form/PayForm.scss

@ -10,16 +10,35 @@
} }
.amountContainer { .amountContainer {
position: relative;
color: $main; color: $main;
display: flex; display: flex;
justify-content: center; justify-content: center;
min-height: 120px; min-height: 120px;
margin-bottom: 20px; margin-bottom: 20px;
min-height: 175px; min-height: 175px;
border-bottom: 1px solid transparent;
&.ln { &.ln {
opacity: 0.75; 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] { label, input[type=number], input[type=text] {
color: inherit; color: inherit;
@ -92,6 +111,10 @@
position: relative; position: relative;
padding: 0 20px; padding: 0 20px;
&.error {
border-color: $red;
}
label, input[type=number], input[type=text] { label, input[type=number], input[type=text] {
font-size: inherit; 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 { .buttonGroup {
width: 100%; width: 100%;
display: flex; display: flex;
@ -120,7 +155,6 @@
overflow: hidden; overflow: hidden;
.button { .button {
cursor: pointer;
height: 55px; height: 55px;
min-height: 55px; min-height: 55px;
text-transform: none; text-transform: none;
@ -134,9 +168,16 @@
width: 100%; width: 100%;
text-align: center; text-align: center;
line-height: 55px; line-height: 55px;
opacity: 0.5;
cursor: default;
&:first-child { &:first-child {
border-right: 1px solid lighten($main, 20%); border-right: 1px solid lighten($main, 20%);
} }
&.active {
opacity: 1;
cursor: pointer;
}
} }
} }

50
app/reducers/payform.js

@ -1,5 +1,8 @@
import { createSelector } from 'reselect' import { createSelector } from 'reselect'
import bitcoin from 'bitcoinjs-lib' import bitcoin from 'bitcoinjs-lib'
import isEmpty from 'lodash/isEmpty'
import { tickerSelectors } from './ticker' import { tickerSelectors } from './ticker'
import { btc, bech32 } from '../utils' import { btc, bech32 } from '../utils'
@ -12,6 +15,11 @@ const initialState = {
payreq: '', payreq: '',
r_hash: '', r_hash: '',
amount: '0' 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_INPUT = 'SET_PAY_INPUT'
export const SET_PAY_INVOICE = 'SET_PAY_INVOICE' export const SET_PAY_INVOICE = 'SET_PAY_INVOICE'
export const UPDATE_PAY_ERRORS = 'UPDATE_PAY_ERRORS'
export const RESET_FORM = 'RESET_FORM' export const RESET_FORM = 'RESET_FORM'
// ------------------------------------ // ------------------------------------
@ -47,9 +57,10 @@ export function setPayInvoice(invoice) {
} }
} }
export function resetPayForm() { export function updatePayErrors(errorsObject) {
return { return {
type: RESET_FORM type: UPDATE_PAY_ERRORS,
errorsObject
} }
} }
@ -57,9 +68,11 @@ export function resetPayForm() {
// Action Handlers // Action Handlers
// ------------------------------------ // ------------------------------------
const ACTION_HANDLERS = { const ACTION_HANDLERS = {
[SET_PAY_AMOUNT]: (state, { amount }) => ({ ...state, amount }), [SET_PAY_AMOUNT]: (state, { amount }) => ({ ...state, amount, showErrors: Object.assign(state.showErrors, { amount: false }) }),
[SET_PAY_INPUT]: (state, { payInput }) => ({ ...state, payInput }), [SET_PAY_INPUT]: (state, { payInput }) => ({ ...state, payInput, showErrors: Object.assign(state.showErrors, { payInput: false }) }),
[SET_PAY_INVOICE]: (state, { invoice }) => ({ ...state, invoice }), [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) [RESET_FORM]: () => (initialState)
} }
@ -75,7 +88,7 @@ const payInvoiceSelector = state => state.payform.invoice
// transaction // transaction
const sendingTransactionSelector = state => state.transaction.sendingTransaction const sendingTransactionSelector = state => state.transaction.sendingTransaction
// transaction // payment
const sendingPaymentSelector = state => state.payment.sendingPayment const sendingPaymentSelector = state => state.payment.sendingPayment
// ticker // ticker
@ -84,7 +97,6 @@ const currencySelector = state => state.ticker.currency
payFormSelectors.isOnchain = createSelector( payFormSelectors.isOnchain = createSelector(
payInputSelector, payInputSelector,
(input) => { (input) => {
// TODO: work with bitcoin-js to fix p2wkh error and make testnet/mainnet dynamic
try { try {
bitcoin.address.toOutputScript(input, bitcoin.networks.testnet) bitcoin.address.toOutputScript(input, bitcoin.networks.testnet)
return true return true
@ -149,6 +161,30 @@ payFormSelectors.showPayLoadingScreen = createSelector(
(sendingTransaction, sendingPayment) => sendingTransaction || sendingPayment (sendingTransaction, sendingPayment) => sendingTransaction || sendingPayment
) )
payFormSelectors.payFormIsValid = createSelector(
payFormSelectors.isOnchain,
payFormSelectors.isLn,
payAmountSelector,
(isOnchain, isLn, amount) => {
const 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 } export { payFormSelectors }
// ------------------------------------ // ------------------------------------

40
app/routes/app/containers/AppContainer.js

@ -7,7 +7,7 @@ import { fetchInfo } from 'reducers/info'
import { showModal, hideModal } from 'reducers/modal' import { showModal, hideModal } from 'reducers/modal'
import { setFormType } from 'reducers/form' 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 { setRequestAmount, setRequestMemo } from 'reducers/requestform'
import { sendCoins } from 'reducers/transaction' import { sendCoins } from 'reducers/transaction'
@ -31,6 +31,8 @@ const mapDispatchToProps = {
setPayAmount, setPayAmount,
setPayInput, setPayInput,
updatePayErrors,
setRequestAmount, setRequestAmount,
setRequestMemo, setRequestMemo,
@ -59,7 +61,8 @@ const mapStateToProps = state => ({
isLn: payFormSelectors.isLn(state), isLn: payFormSelectors.isLn(state),
currentAmount: payFormSelectors.currentAmount(state), currentAmount: payFormSelectors.currentAmount(state),
inputCaption: payFormSelectors.inputCaption(state), inputCaption: payFormSelectors.inputCaption(state),
showPayLoadingScreen: payFormSelectors.showPayLoadingScreen(state) showPayLoadingScreen: payFormSelectors.showPayLoadingScreen(state),
payFormIsValid: payFormSelectors.payFormIsValid(state)
}) })
const mergeProps = (stateProps, dispatchProps, ownProps) => { const mergeProps = (stateProps, dispatchProps, ownProps) => {
@ -73,14 +76,45 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
currentAmount: stateProps.currentAmount, currentAmount: stateProps.currentAmount,
inputCaption: stateProps.inputCaption, inputCaption: stateProps.inputCaption,
showPayLoadingScreen: stateProps.showPayLoadingScreen, showPayLoadingScreen: stateProps.showPayLoadingScreen,
payFormIsValid: stateProps.payFormIsValid,
setPayAmount: dispatchProps.setPayAmount, setPayAmount: dispatchProps.setPayAmount,
setPayInput: dispatchProps.setPayInput, setPayInput: dispatchProps.setPayInput,
fetchInvoice: dispatchProps.fetchInvoice, fetchInvoice: dispatchProps.fetchInvoice,
onPayAmountBlur: () => {
// If the amount is now valid and showErrors was on, turn it off
if (stateProps.payFormIsValid.amountIsValid && stateProps.payform.showErrors.amount) {
dispatchProps.updatePayErrors({ amount: false })
}
// If the amount 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: () => { 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) { if (stateProps.isOnchain) {
dispatchProps.sendCoins({ dispatchProps.sendCoins({

1
package.json

@ -195,6 +195,7 @@
"font-awesome": "^4.7.0", "font-awesome": "^4.7.0",
"grpc": "^1.4.1", "grpc": "^1.4.1",
"history": "^4.6.3", "history": "^4.6.3",
"lodash": "^4.17.4",
"moment-timezone": "^0.5.13", "moment-timezone": "^0.5.13",
"prop-types": "^15.5.10", "prop-types": "^15.5.10",
"qrcode.react": "^0.7.1", "qrcode.react": "^0.7.1",

3
test/components/Form.spec.js

@ -15,9 +15,12 @@ const payFormProps = {
currentAmount: '0', currentAmount: '0',
inputCaption: '', inputCaption: '',
showPayLoadingScreen: false, showPayLoadingScreen: false,
payFormIsValid: {},
setPayAmount: () => {}, setPayAmount: () => {},
onPayAmountBlur: () => {},
setPayInput: () => {}, setPayInput: () => {},
onPayInputBlur: () => {},
fetchInvoice: () => {}, fetchInvoice: () => {},

Loading…
Cancel
Save