diff --git a/src/action/nav-mobile.js b/src/action/nav-mobile.js index 9312cffbb..6328ec3af 100644 --- a/src/action/nav-mobile.js +++ b/src/action/nav-mobile.js @@ -119,6 +119,10 @@ class NavAction { this._navigate('Pay'); } + goPayLightningSupplyAmount() { + this._navigate('PayLightningSupplyAmount'); + } + goPayLightningConfirm() { this._navigate('PayLightningConfirm'); } diff --git a/src/action/nav.js b/src/action/nav.js index 3fb5cc945..d16098e05 100644 --- a/src/action/nav.js +++ b/src/action/nav.js @@ -81,6 +81,10 @@ class NavAction { this._store.route = 'Pay'; } + goPayLightningSupplyAmount() { + this._store.route = 'PayLightningSupplyAmount'; + } + goPayLightningConfirm() { this._store.route = 'PayLightningConfirm'; } diff --git a/src/action/payment.js b/src/action/payment.js index ea779b066..99b0202f8 100644 --- a/src/action/payment.js +++ b/src/action/payment.js @@ -106,6 +106,7 @@ class PaymentAction { */ init() { this._store.payment.address = ''; + this._store.payment.destination = ''; this._store.payment.amount = ''; this._store.payment.targetConf = MED_TARGET_CONF; this._store.payment.fee = ''; @@ -165,7 +166,12 @@ class PaymentAction { return this._notification.display({ msg: 'Enter an invoice or address' }); } if (await this.decodeInvoice({ invoice: this._store.payment.address })) { - this._nav.goPayLightningConfirm(); + if (this._store.payment.amount === '0') { + this._store.payment.amount = ''; + this._nav.goPayLightningSupplyAmount(); + } else { + this._nav.goPayLightningConfirm(); + } } else if (isAddress(this._store.payment.address)) { this._nav.goPayBitcoin(); } else { @@ -173,6 +179,31 @@ class PaymentAction { } } + /** + * Check If payment amount was supplied and destination is set. Estimate + * the routing fee and go to the confirmation view for lightning payments. + * + * Note: This function is dependent on that an invoice has already been decoded. + * If payment amount is 0 the function will display a message and return. + */ + async checkAmountSuppliedAndGoPayLightningConfirm() { + if ( + this._store.payment.amount === '0' || + this._store.payment.amount === '' + ) { + this._notification.display({ msg: 'Enter amount to pay.' }); + } else if (this._store.payment.destination === '') { + this._notification.display({ msg: 'Internal Error, try again.' }); + this._nav.goHome(); + } else { + this.estimateLightningFee({ + destination: this._store.payment.destination, + satAmt: toSatoshis(this._store.payment.amount, this._store.settings), + }); + this._nav.goPayLightningConfirm(); + } + } + /** * Attempt to decode a lightning invoice using the lnd grpc api. If it is * an invoice the amount and note store values will be set and the lightning @@ -188,6 +219,7 @@ class PaymentAction { }); payment.amount = toAmount(request.numSatoshis, settings); payment.note = request.description; + payment.destination = request.destination; this.estimateLightningFee({ destination: request.destination, satAmt: request.numSatoshis, @@ -321,7 +353,7 @@ class PaymentAction { } /** - * Send the amount specified in the invoice as a lightning transaction and + * Send the amount specified in payment.amount as a lightning transaction and * display the wait screen while the payment confirms. * This action can be called from a view event handler as does all * the necessary error handling and notification display. @@ -336,6 +368,8 @@ class PaymentAction { try { this._nav.goWait(); const invoice = this._store.payment.address; + const { settings } = this._store; + const satAmt = toSatoshis(this._store.payment.amount, settings); const stream = this._grpc.sendStreamCommand('sendPayment'); await new Promise((resolve, reject) => { stream.on('data', data => { @@ -346,7 +380,13 @@ class PaymentAction { } }); stream.on('error', reject); - stream.write(JSON.stringify({ paymentRequest: invoice }), 'utf8'); + stream.write( + JSON.stringify({ + paymentRequest: invoice, + amt: satAmt, + }), + 'utf8' + ); }); if (failed) return; this._nav.goPayLightningDone(); diff --git a/src/view/main-mobile.js b/src/view/main-mobile.js index 7e9a2dee1..5e00347ea 100644 --- a/src/view/main-mobile.js +++ b/src/view/main-mobile.js @@ -33,6 +33,7 @@ import SettingUnitView from './setting-unit'; import SettingFiatView from './setting-fiat'; import CLIView from './cli'; import PaymentView from './payment-mobile'; +import PayLightningSupplyAmountView from './pay-lightning-supply-amount-mobile'; import PayLightningConfirmView from './pay-lightning-confirm-mobile'; import PayLightningDoneView from './pay-lightning-done-mobile'; import PaymentFailedView from './payment-failed-mobile'; @@ -171,6 +172,10 @@ const InvoiceQR = () => ( const Pay = () => ; +const PayLightningSupplyAmount = () => ( + +); + const PayLightningConfirm = () => ( ); @@ -236,6 +241,7 @@ const InvoiceStack = createStackNavigator( const PayStack = createStackNavigator( { Pay, + PayLightningSupplyAmount, PayLightningConfirm, Wait, PayLightningDone, diff --git a/src/view/main.js b/src/view/main.js index 8066ace05..4100609d6 100644 --- a/src/view/main.js +++ b/src/view/main.js @@ -21,6 +21,7 @@ import LoaderSyncing from './loader-syncing'; import Wait from './wait'; import Home from './home'; import Payment from './payment'; +import PayLightningSupplyAmount from './pay-lightning-supply-amount'; import PayLightningConfirm from './pay-lightning-confirm'; import PayLightningDone from './pay-lightning-done'; import PaymentFailed from './payment-failed'; @@ -130,6 +131,9 @@ class MainView extends Component { {route === 'Pay' && ( )} + {route === 'PayLightningSupplyAmount' && ( + + )} {route === 'PayLightningConfirm' && ( )} diff --git a/src/view/pay-lightning-supply-amount-mobile.js b/src/view/pay-lightning-supply-amount-mobile.js new file mode 100644 index 000000000..a92e520b3 --- /dev/null +++ b/src/view/pay-lightning-supply-amount-mobile.js @@ -0,0 +1,84 @@ +import React from 'react'; +import { StyleSheet } from 'react-native'; +import { observer } from 'mobx-react'; +import PropTypes from 'prop-types'; +import Background from '../component/background'; +import MainContent from '../component/main-content'; +import { NamedField, AmountInputField } from '../component/field'; +import { Header, Title } from '../component/header'; +import { CancelButton, Button, SmallGlasButton } from '../component/button'; +import Card from '../component/card'; +import LightningBoltIcon from '../asset/icon/lightning-bolt'; +import { FormStretcher, FormSubText } from '../component/form'; +import { BalanceLabel, BalanceLabelUnit } from '../component/label'; +import { color } from '../component/style'; + +const styles = StyleSheet.create({ + balance: { + marginTop: 15, + }, + unit: { + color: color.blackText, + }, + form: { + paddingTop: 10, + paddingBottom: 10, + }, + subText: { + paddingTop: 40, + paddingBottom: 40, + }, +}); + +const PayLightningSupplyAmountView = ({ store, nav, payment }) => ( + +
+
+ + + + payment.setAmount({ amount })} + onSubmitEditing={() => + payment.checkAmountSuppliedAndGoPayLightningConfirm() + } + /> + + {store.unitFiatLabel} + + + + {store.payment.note ? ( + + {store.payment.note} + + ) : null} + + + Payment Request did not specify an amount. This is often used for + tips/donations. + + + + payment.checkAmountSuppliedAndGoPayLightningConfirm()} + > + Pay + +
+); + +PayLightningSupplyAmountView.propTypes = { + store: PropTypes.object.isRequired, + nav: PropTypes.object.isRequired, + payment: PropTypes.object.isRequired, +}; + +export default observer(PayLightningSupplyAmountView); diff --git a/src/view/pay-lightning-supply-amount.js b/src/view/pay-lightning-supply-amount.js new file mode 100644 index 000000000..192de47ea --- /dev/null +++ b/src/view/pay-lightning-supply-amount.js @@ -0,0 +1,92 @@ +import React from 'react'; +import { StyleSheet } from 'react-native'; +import { observer } from 'mobx-react'; +import PropTypes from 'prop-types'; +import Background from '../component/background'; +import MainContent from '../component/main-content'; +import { NamedField, AmountInputField } from '../component/field'; +import { Header, Title } from '../component/header'; +import { CancelButton, BackButton, PillButton } from '../component/button'; +import Card from '../component/card'; +import LightningBoltIcon from '../asset/icon/lightning-bolt'; +import { FormStretcher, FormSubText } from '../component/form'; +import { BalanceLabel, BalanceLabelUnit } from '../component/label'; +import { color } from '../component/style'; + +const styles = StyleSheet.create({ + description: { + paddingLeft: 20, + paddingRight: 20, + }, + unit: { + color: color.blackText, + }, + maxBtn: { + marginTop: 10, + marginBottom: 20, + }, + nextBtn: { + marginTop: 20, + backgroundColor: color.purple, + }, + subText: { + paddingTop: 20, + paddingBottom: 40, + paddingLeft: 40, + paddingRight: 40, + }, +}); + +const PayLightningSupplyAmountView = ({ store, nav, payment }) => ( + +
+ nav.goHome()} /> + + <LightningBoltIcon height={12} width={6.1} /> + + nav.goHome()} /> +
+ + + + payment.setAmount({ amount })} + onSubmitEditing={() => + payment.checkAmountSuppliedAndGoPayLightningConfirm() + } + /> + + {store.unitFiatLabel} + + + + {store.payment.note ? ( + + {store.payment.note} + + ) : null} + + Payment Request did not specify an amount. This is often used for + tips/donations. + + + payment.checkAmountSuppliedAndGoPayLightningConfirm()} + > + Pay + + + +
+); + +PayLightningSupplyAmountView.propTypes = { + store: PropTypes.object.isRequired, + nav: PropTypes.object.isRequired, + payment: PropTypes.object.isRequired, +}; + +export default observer(PayLightningSupplyAmountView); diff --git a/test/unit/action/nav.spec.js b/test/unit/action/nav.spec.js index c65ffa2c5..f2bedb5ee 100644 --- a/test/unit/action/nav.spec.js +++ b/test/unit/action/nav.spec.js @@ -116,6 +116,13 @@ describe('Action Nav Unit Tests', () => { }); }); + describe('goPayLightningSupplyAmount()', () => { + it('should set correct route', () => { + nav.goPayLightningSupplyAmount(); + expect(store.route, 'to equal', 'PayLightningSupplyAmount'); + }); + }); + describe('goPayLightningConfirm()', () => { it('should set correct route', () => { nav.goPayLightningConfirm(); diff --git a/test/unit/action/payment.spec.js b/test/unit/action/payment.spec.js index 93931b26a..486443b3f 100644 --- a/test/unit/action/payment.spec.js +++ b/test/unit/action/payment.spec.js @@ -302,6 +302,14 @@ describe('Action Payments Unit Tests', () => { expect(payment.decodeInvoice, 'was not called'); }); + it('should detect zero amount payment', async () => { + store.payment.address = 'some-address'; + store.payment.amount = '0'; + payment.decodeInvoice.resolves(true); + await payment.checkType(); + expect(nav.goPayLightningSupplyAmount, 'was called once'); + }); + it('should decode successfully', async () => { store.payment.address = 'some-address'; payment.decodeInvoice.resolves(true); @@ -461,15 +469,33 @@ describe('Action Payments Unit Tests', () => { }); }); - it('should send lightning payment', async () => { + it('should send lightning payment with amount 0 if not specified.', async () => { + paymentsOnStub.withArgs('data').yields({ paymentError: '' }); + payment.setAddress({ address: 'lightning:some-invoice' }); + await payment.payLightning(); + expect(grpc.sendStreamCommand, 'was called with', 'sendPayment'); + expect( + paymentsWriteStub, + 'was called with', + JSON.stringify({ paymentRequest: 'some-invoice', amt: 0 }), + 'utf8' + ); + expect(nav.goWait, 'was called once'); + expect(nav.goPayLightningDone, 'was called once'); + expect(notification.display, 'was not called'); + }); + + it('should send lightning payment with amount.', async () => { paymentsOnStub.withArgs('data').yields({ paymentError: '' }); payment.setAddress({ address: 'lightning:some-invoice' }); + store.settings.unit = 'btc'; + payment.setAmount({ amount: '0.0001' }); await payment.payLightning(); expect(grpc.sendStreamCommand, 'was called with', 'sendPayment'); expect( paymentsWriteStub, 'was called with', - JSON.stringify({ paymentRequest: 'some-invoice' }), + JSON.stringify({ paymentRequest: 'some-invoice', amt: 10000 }), 'utf8' ); expect(nav.goWait, 'was called once');