Browse Source

feature(payform): add form validation to payform

renovate/lint-staged-8.x
Jack Mallers 7 years ago
parent
commit
c208db0d3e
  1. 27
      app/components/Form/PayForm.js
  2. 43
      app/components/Form/PayForm.scss
  3. 51
      app/reducers/payform.js
  4. 40
      app/routes/app/containers/AppContainer.js
  5. 1
      package.json

27
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 {
<div className={styles.container}>
{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'>
<CurrencyIcon currency={currency} crypto={crypto} />
</label>
@ -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 {
</i>
}
</aside>
<section className={styles.input}>
<section className={`${styles.input} ${showErrors.payInput && styles.error}`}>
<input
type='text'
placeholder='Payment request or bitcoin address'
value={payInput}
onChange={event => setPayInput(event.target.value)}
onBlur={onPayInputBlur}
id='paymentRequest'
/>
</section>
<section className={`${styles.payInputError} ${showErrors.payInput && styles.active}`}>
{showErrors.payInput &&
<span>{errors.payInput}</span>
}
</section>
</div>
<section className={styles.buttonGroup}>
<div className={styles.button} onClick={onPaySubmit}>Pay</div>
<div className={`${styles.button} ${isValid && styles.active}`} onClick={onPaySubmit}>Pay</div>
</section>
</div>
)
@ -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

43
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;
}
}
}

51
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 }
// ------------------------------------

40
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({

1
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",

Loading…
Cancel
Save