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

18
app/components/Wallet/Wallet.scss

@ -196,9 +196,14 @@
transition: all 0.25s; transition: all 0.25s;
} }
.spinnerContainer {
position: relative;
display: inline;
}
.spinner { .spinner {
height: 20px; height: 30px;
width: 20px; width: 30px;
border: 1px solid rgba(235, 184, 100, 0.1); border: 1px solid rgba(235, 184, 100, 0.1);
border-left-color: rgba(235, 184, 100, 0.4); border-left-color: rgba(235, 184, 100, 0.4);
-webkit-border-radius: 999px; -webkit-border-radius: 999px;
@ -210,6 +215,15 @@
animation: animation-rotate 1000ms linear infinite; animation: animation-rotate 1000ms linear infinite;
} }
.timeout {
position: absolute;
top: 20%;
left: 0;
font-size: 10px;
width: 32px;
text-align: center;
}
.icon { .icon {
margin-right: 5px; 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 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_SUCCESSFULL = 'PAYMENT_SUCCESSFULL'
export const PAYMENT_FAILED = 'PAYMENT_FAILED' 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) { export function paymentSuccessfull(payment) {
return { return {
type: PAYMENT_SUCCESSFULL, type: PAYMENT_SUCCESSFULL,
@ -96,21 +119,30 @@ export const paymentFailed = (event, { error }) => dispatch => {
dispatch(setError(error)) dispatch(setError(error))
} }
export const payInvoice = paymentRequest => dispatch => { export const payInvoice = paymentRequest => (dispatch, getState) => {
dispatch(sendPayment()) dispatch(sendPayment())
ipcRenderer.send('lnd', { msg: 'sendPayment', data: { paymentRequest } }) 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 // Close the form modal once the payment has been sent
dispatch(setFormType(null)) dispatch(setFormType(null))
}
// if LND hangs on sending the payment we'll cut it after 10 seconds and return an error // Tick checks if the payment is sending and checks the timeout every second. If the payment is still sending and the
// setTimeout(() => { // 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 { payment } = getState() const tick = (dispatch, getState) => {
const { payment } = getState()
// if (payment.sendingPayment) { if (payment.sendingPayment && payment.paymentTimeout <= 0) {
// dispatch(paymentFailed(null, { error: 'Shoot, we\'re having some trouble sending your payment.' })) dispatch(paymentFailed(null, { error: 'Shoot, there was some trouble sending your payment.' }))
// } } else {
// }, 10000) dispatch(tickTimeout())
}
} }
// ------------------------------------ // ------------------------------------
@ -121,9 +153,32 @@ const ACTION_HANDLERS = {
[RECEIVE_PAYMENTS]: (state, { payments }) => ({ ...state, paymentLoading: false, payments }), [RECEIVE_PAYMENTS]: (state, { payments }) => ({ ...state, paymentLoading: false, payments }),
[SET_PAYMENT]: (state, { payment }) => ({ ...state, payment }), [SET_PAYMENT]: (state, { payment }) => ({ ...state, payment }),
[SEND_PAYMENT]: state => ({ ...state, sendingPayment: true }), [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 }), [SHOW_SUCCESS_SCREEN]: state => ({ ...state, showSuccessPayScreen: true }),
[HIDE_SUCCESS_SCREEN]: state => ({ ...state, showSuccessPayScreen: false }) [HIDE_SUCCESS_SCREEN]: state => ({ ...state, showSuccessPayScreen: false })
@ -142,6 +197,8 @@ export { paymentSelectors }
const initialState = { const initialState = {
sendingPayment: false, sendingPayment: false,
paymentLoading: false, paymentLoading: false,
paymentTimeout: 60000,
paymentInterval: null,
payments: [], payments: [],
payment: null, payment: null,
showSuccessPayScreen: false showSuccessPayScreen: false

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

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

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

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

Loading…
Cancel
Save