Browse Source

feat(payment): add 60 second payment timeout

In this commit we introduce a 60 second timeout for LN payments. This addresses the issue of infinitely hanging LN payments and improves the UX. The UI now has a 60 second countdown where Zap will clear the payment if it is not successfully routed
renovate/lint-staged-8.x
Jack Mallers 7 years ago
parent
commit
8548b744ec
  1. 11
      app/components/Wallet/Wallet.js
  2. 18
      app/components/Wallet/Wallet.scss
  3. 77
      app/reducers/payment.js
  4. 1
      app/routes/activity/containers/ActivityContainer.js
  5. 10
      test/unit/reducers/__snapshots__/payment.spec.js.snap

11
app/components/Wallet/Wallet.js

@ -30,7 +30,8 @@ const Wallet = ({
setCurrency,
setWalletCurrencyFilters,
network,
settingsProps
settingsProps,
paymentTimeout
}) => {
const fiatAmount = btc.satoshisToFiat(
parseInt(balance.walletBalance, 10) + parseInt(balance.channelBalance, 10),
@ -112,8 +113,11 @@ const Wallet = ({
<div className={styles.notificationBox}>
{showPayLoadingScreen && (
<span>
<section className={`${styles.spinner} ${styles.icon}`} />
<section>Sending your transaction...</section>
<div className={styles.spinnerContainer}>
<section className={`${styles.spinner} ${styles.icon}`} />
<span className={styles.timeout}>{paymentTimeout / 1000}</span>
</div>
<section>Sending your transaction</section>
</span>
)}
{showSuccessPayScreen && (
@ -165,6 +169,7 @@ Wallet.propTypes = {
settingsProps: PropTypes.object.isRequired,
currentCurrencyFilters: PropTypes.array.isRequired,
currencyName: PropTypes.string.isRequired,
paymentTimeout: PropTypes.number.isRequired,
setCurrency: PropTypes.func.isRequired,
setWalletCurrencyFilters: PropTypes.func.isRequired
}

18
app/components/Wallet/Wallet.scss

@ -196,9 +196,14 @@
transition: all 0.25s;
}
.spinnerContainer {
position: relative;
display: inline;
}
.spinner {
height: 20px;
width: 20px;
height: 30px;
width: 30px;
border: 1px solid rgba(235, 184, 100, 0.1);
border-left-color: rgba(235, 184, 100, 0.4);
-webkit-border-radius: 999px;
@ -210,6 +215,15 @@
animation: animation-rotate 1000ms linear infinite;
}
.timeout {
position: absolute;
top: 20%;
left: 0;
font-size: 10px;
width: 32px;
text-align: center;
}
.icon {
margin-right: 5px;
}

77
app/reducers/payment.js

@ -15,6 +15,10 @@ export const RECEIVE_PAYMENTS = 'RECEIVE_PAYMENTS'
export const SEND_PAYMENT = 'SEND_PAYMENT'
export const TICK_TIMEOUT = 'TICK_TIMEOUT'
export const SET_INTERVAL = 'SET_INTERVAL'
export const RESET_TIMEOUT = 'RESET_TIMEOUT'
export const PAYMENT_SUCCESSFULL = 'PAYMENT_SUCCESSFULL'
export const PAYMENT_FAILED = 'PAYMENT_FAILED'
@ -43,6 +47,25 @@ export function sendPayment() {
}
}
export function tickTimeout() {
return {
type: TICK_TIMEOUT
}
}
export function setPaymentInterval(paymentInterval) {
return {
type: SET_INTERVAL,
paymentInterval
}
}
export function resetTimeout() {
return {
type: RESET_TIMEOUT
}
}
export function paymentSuccessfull(payment) {
return {
type: PAYMENT_SUCCESSFULL,
@ -96,21 +119,30 @@ export const paymentFailed = (event, { error }) => dispatch => {
dispatch(setError(error))
}
export const payInvoice = paymentRequest => dispatch => {
export const payInvoice = paymentRequest => (dispatch, getState) => {
dispatch(sendPayment())
ipcRenderer.send('lnd', { msg: 'sendPayment', data: { paymentRequest } })
// Set an interval to call tick which will continuously tick down the ticker until the payment goes through or it hits
// 0 and throws an error. We also call setPaymentInterval so we are storing the interval. This allows us to clear the
// interval in flexible ways whenever we need to
const paymentInterval = setInterval(() => tick(dispatch, getState), 1000)
dispatch(setPaymentInterval(paymentInterval))
// Close the form modal once the payment has been sent
dispatch(setFormType(null))
}
// if LND hangs on sending the payment we'll cut it after 10 seconds and return an error
// setTimeout(() => {
// const { payment } = getState()
// Tick checks if the payment is sending and checks the timeout every second. If the payment is still sending and the
// timeout is above 0 it will continue to tick it down, once we hit 0 we fire an error to the user and reset the reducer
const tick = (dispatch, getState) => {
const { payment } = getState()
// if (payment.sendingPayment) {
// dispatch(paymentFailed(null, { error: 'Shoot, we\'re having some trouble sending your payment.' }))
// }
// }, 10000)
if (payment.sendingPayment && payment.paymentTimeout <= 0) {
dispatch(paymentFailed(null, { error: 'Shoot, there was some trouble sending your payment.' }))
} else {
dispatch(tickTimeout())
}
}
// ------------------------------------
@ -121,9 +153,32 @@ const ACTION_HANDLERS = {
[RECEIVE_PAYMENTS]: (state, { payments }) => ({ ...state, paymentLoading: false, payments }),
[SET_PAYMENT]: (state, { payment }) => ({ ...state, payment }),
[SEND_PAYMENT]: state => ({ ...state, sendingPayment: true }),
[PAYMENT_SUCCESSFULL]: state => ({ ...state, sendingPayment: false }),
[PAYMENT_FAILED]: state => ({ ...state, sendingPayment: false }),
[TICK_TIMEOUT]: state => ({ ...state, paymentTimeout: state.paymentTimeout - 1000 }),
[SET_INTERVAL]: (state, { paymentInterval }) => ({ ...state, paymentInterval }),
[PAYMENT_SUCCESSFULL]: state => {
clearInterval(state.paymentInterval)
return {
...state,
sendingPayment: false,
paymentInterval: null,
paymentTimeout: 60000
}
},
[PAYMENT_FAILED]: state => {
clearInterval(state.paymentInterval)
return {
...state,
sendingPayment: false,
paymentInterval: null,
paymentTimeout: 60000
}
},
[SHOW_SUCCESS_SCREEN]: state => ({ ...state, showSuccessPayScreen: true }),
[HIDE_SUCCESS_SCREEN]: state => ({ ...state, showSuccessPayScreen: false })
@ -142,6 +197,8 @@ export { paymentSelectors }
const initialState = {
sendingPayment: false,
paymentLoading: false,
paymentTimeout: 60000,
paymentInterval: null,
payments: [],
payment: null,
showSuccessPayScreen: false

1
app/routes/activity/containers/ActivityContainer.js

@ -98,6 +98,7 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => ({
currentCurrencyFilters: stateProps.currentCurrencyFilters,
currencyName: stateProps.currencyName,
network: stateProps.info.network,
paymentTimeout: stateProps.payment.paymentTimeout,
setCurrency: dispatchProps.setCurrency,
setWalletCurrencyFilters: dispatchProps.setWalletCurrencyFilters,

10
test/unit/reducers/__snapshots__/payment.spec.js.snap

@ -3,7 +3,9 @@
exports[`reducers paymentReducer should correctly getPayments 1`] = `
Object {
"payment": null,
"paymentInterval": null,
"paymentLoading": true,
"paymentTimeout": 60000,
"payments": Array [],
"sendingPayment": false,
"showSuccessPayScreen": false,
@ -13,7 +15,9 @@ Object {
exports[`reducers paymentReducer should correctly paymentSuccessful 1`] = `
Object {
"payment": null,
"paymentInterval": null,
"paymentLoading": false,
"paymentTimeout": 60000,
"payments": Array [],
"sendingPayment": false,
"showSuccessPayScreen": false,
@ -23,7 +27,9 @@ Object {
exports[`reducers paymentReducer should correctly receivePayments 1`] = `
Object {
"payment": null,
"paymentInterval": null,
"paymentLoading": false,
"paymentTimeout": 60000,
"payments": Array [
1,
2,
@ -36,7 +42,9 @@ Object {
exports[`reducers paymentReducer should correctly sendPayment 1`] = `
Object {
"payment": "foo",
"paymentInterval": null,
"paymentLoading": false,
"paymentTimeout": 60000,
"payments": Array [],
"sendingPayment": false,
"showSuccessPayScreen": false,
@ -46,7 +54,9 @@ Object {
exports[`reducers paymentReducer should handle initial state 1`] = `
Object {
"payment": null,
"paymentInterval": null,
"paymentLoading": false,
"paymentTimeout": 60000,
"payments": Array [],
"sendingPayment": false,
"showSuccessPayScreen": false,

Loading…
Cancel
Save