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 {
}
-
+
+
+ {showErrors.payInput &&
+ {errors.payInput}
+ }
+
)
@@ -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..b031b8ff 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,30 @@ payFormSelectors.showPayLoadingScreen = createSelector(
(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 }
// ------------------------------------
diff --git a/app/routes/app/containers/AppContainer.js b/app/routes/app/containers/AppContainer.js
index 17782a5a..7da0a72b 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 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: () => {
- 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",
diff --git a/test/components/Form.spec.js b/test/components/Form.spec.js
index 3024a3b4..d90556ff 100644
--- a/test/components/Form.spec.js
+++ b/test/components/Form.spec.js
@@ -15,9 +15,12 @@ const payFormProps = {
currentAmount: '0',
inputCaption: '',
showPayLoadingScreen: false,
+ payFormIsValid: {},
setPayAmount: () => {},
+ onPayAmountBlur: () => {},
setPayInput: () => {},
+ onPayInputBlur: () => {},
fetchInvoice: () => {},