Skip to content
This repository was archived by the owner on Feb 23, 2021. It is now read-only.

Commit 874b40c

Browse files
authored
Merge pull request #1035 from lightninglabs/payscreen-max-toggle
Payscreen max toggle
2 parents c60bb5c + 3b07843 commit 874b40c

File tree

7 files changed

+179
-13
lines changed

7 files changed

+179
-13
lines changed

src/action/payment.js

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ class PaymentAction {
103103
this._store.payment.fee = '';
104104
this._store.payment.note = '';
105105
this._store.payment.useScanner = false;
106+
this._store.payment.sendAll = false;
106107
this._nav.goPay();
107108
}
108109

@@ -122,6 +123,25 @@ class PaymentAction {
122123
*/
123124
setAmount({ amount }) {
124125
this._store.payment.amount = amount;
126+
this._store.payment.sendAll = false;
127+
}
128+
129+
/**
130+
* Set the payment amount to the max amount that can be sent. This
131+
* is useful for people to move their coins off of the app.
132+
* @return {Promise<undefined>}
133+
*/
134+
async toggleMax() {
135+
const { payment, balanceSatoshis, settings } = this._store;
136+
if (payment.sendAll) {
137+
return this.setAmount({ amount: '0' });
138+
}
139+
let amtSat = Math.floor(0.8 * balanceSatoshis);
140+
payment.amount = toAmount(amtSat, settings);
141+
await this.estimateFee();
142+
amtSat = balanceSatoshis - toSatoshis(payment.fee, settings);
143+
payment.amount = toAmount(amtSat, settings);
144+
payment.sendAll = true;
125145
}
126146

127147
/**
@@ -214,7 +234,10 @@ class PaymentAction {
214234
*/
215235
async initPayBitcoinConfirm() {
216236
try {
217-
await this.estimateFee();
237+
const { payment } = this._store;
238+
if (!payment.fee || !payment.sendAll) {
239+
await this.estimateFee();
240+
}
218241
this._nav.goPayBitcoinConfirm();
219242
} catch (err) {
220243
this._notification.display({ msg: 'Fee estimation failed!', err });
@@ -238,11 +261,7 @@ class PaymentAction {
238261
}, PAYMENT_TIMEOUT);
239262
this._nav.goWait();
240263
try {
241-
const { payment, settings } = this._store;
242-
await this._grpc.sendCommand('sendCoins', {
243-
addr: payment.address,
244-
amount: toSatoshis(payment.amount, settings),
245-
});
264+
await this._sendPayment();
246265
this._nav.goPayBitcoinDone();
247266
} catch (err) {
248267
this._nav.goPayBitcoinConfirm();
@@ -252,6 +271,16 @@ class PaymentAction {
252271
}
253272
}
254273

274+
async _sendPayment() {
275+
const { payment, settings } = this._store;
276+
let amount = payment.sendAll ? 0 : toSatoshis(payment.amount, settings);
277+
await this._grpc.sendCommand('sendCoins', {
278+
addr: payment.address,
279+
amount,
280+
sendAll: payment.sendAll,
281+
});
282+
}
283+
255284
/**
256285
* Send the amount specified in the invoice as a lightning transaction and
257286
* display the wait screen while the payment confirms.

src/component/button.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -523,4 +523,39 @@ RadioButton.propTypes = {
523523
selected: PropTypes.bool.isRequired,
524524
};
525525

526+
//
527+
// Max Button
528+
//
529+
530+
const maxStyles = StyleSheet.create({
531+
touchable: {
532+
borderStyle: 'solid',
533+
borderWidth: 1,
534+
minHeight: 0,
535+
minWidth: 0,
536+
height: 30,
537+
width: 50,
538+
},
539+
});
540+
541+
export const MaxButton = ({ active, style, ...props }) => (
542+
<Button
543+
style={[
544+
{ borderColor: active ? color.blackText : color.greyPlaceholder },
545+
maxStyles.touchable,
546+
style,
547+
]}
548+
{...props}
549+
>
550+
<Text style={{ color: active ? color.blackText : color.greyPlaceholder }}>
551+
Max
552+
</Text>
553+
</Button>
554+
);
555+
556+
MaxButton.propTypes = {
557+
active: PropTypes.bool.isRequired,
558+
style: ViewPropTypes.style,
559+
};
560+
526561
export default Button;

src/store.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ export class Store {
6868
amount: '',
6969
fee: '',
7070
note: '',
71+
sendAll: false,
7172
useScanner: false,
7273
},
7374
peers: [],

src/view/pay-bitcoin.js

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@ import Background from '../component/background';
66
import MainContent from '../component/main-content';
77
import { InputField, AmountInputField } from '../component/field';
88
import { Header, Title } from '../component/header';
9-
import { CancelButton, BackButton, PillButton } from '../component/button';
9+
import {
10+
MaxButton,
11+
CancelButton,
12+
BackButton,
13+
PillButton,
14+
} from '../component/button';
1015
import Card from '../component/card';
1116
import BitcoinIcon from '../asset/icon/bitcoin';
1217
import { FormStretcher, FormText } from '../component/form';
@@ -18,12 +23,13 @@ const styles = StyleSheet.create({
1823
paddingLeft: 20,
1924
paddingRight: 20,
2025
},
21-
balance: {
22-
marginBottom: 10,
23-
},
2426
unit: {
2527
color: color.blackText,
2628
},
29+
maxBtn: {
30+
marginTop: 10,
31+
marginBottom: 20,
32+
},
2733
nextBtn: {
2834
marginTop: 20,
2935
backgroundColor: color.orange,
@@ -57,6 +63,11 @@ const PayBitcoinView = ({ store, nav, payment }) => (
5763
{store.unitFiatLabel}
5864
</BalanceLabelUnit>
5965
</BalanceLabel>
66+
<MaxButton
67+
style={styles.maxBtn}
68+
active={store.payment.sendAll}
69+
onPress={() => payment.toggleMax()}
70+
/>
6071
<InputField
6172
placeholder="Bitcoin Address"
6273
value={store.payment.address}

stories/component/button-story.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
AddButton,
1919
QrButton,
2020
DownButton,
21+
MaxButton,
2122
} from '../../src/component/button';
2223
import CancelIcon from '../../src/asset/icon/cancel';
2324
import CopyPurpleIcon from '../../src/asset/icon/copy-purple';
@@ -39,6 +40,12 @@ storiesOf('Button', module)
3940
style={{ backgroundColor: color.purple }}
4041
onPress={action('clicked')}
4142
/>
43+
))
44+
.add('Max Button (active)', () => (
45+
<MaxButton active={true} onPress={action('clicked')} />
46+
))
47+
.add('Max Button (inactive)', () => (
48+
<MaxButton active={false} onPress={action('clicked')} />
4249
));
4350

4451
storiesOf('Button', module)

stories/screen-story.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ sinon.stub(invoice, 'generateUri');
107107
const payment = new PaymentAction(store, grpc, nav, notify);
108108
sinon.stub(payment, 'checkType');
109109
sinon.stub(payment, 'payBitcoin');
110+
sinon.stub(payment, 'toggleMax');
110111
sinon.stub(payment, 'payLightning');
111112
sinon.stub(payment, 'initPayBitcoinConfirm');
112113
const channel = new ChannelAction(store, grpc, nav, notify);

test/unit/action/payment.spec.js

Lines changed: 85 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -160,32 +160,69 @@ describe('Action Payments Unit Tests', () => {
160160
store.payment.address = 'foo';
161161
store.payment.amount = 'bar';
162162
store.payment.note = 'baz';
163+
store.payment.fee = 'blub';
164+
store.payment.useScanner = true;
165+
store.payment.sendAll = true;
163166
payment.init();
164167
expect(store.payment.address, 'to equal', '');
165168
expect(store.payment.amount, 'to equal', '');
166169
expect(store.payment.note, 'to equal', '');
170+
expect(store.payment.fee, 'to equal', '');
171+
expect(store.payment.useScanner, 'to equal', false);
172+
expect(store.payment.sendAll, 'to equal', false);
167173
expect(nav.goPay, 'was called once');
168174
});
169175
});
170176

171177
describe('initPayBitcoinConfirm()', () => {
172-
it('should get estimate and navigate to confirm view', async () => {
178+
beforeEach(() => {
173179
store.payment.address = 'foo';
174180
store.payment.amount = '2000';
175181
grpc.sendCommand.withArgs('estimateFee').resolves({
176182
feeSat: 10000,
177183
});
184+
});
185+
186+
it('should get estimate and navigate to confirm view', async () => {
187+
await payment.initPayBitcoinConfirm();
188+
expect(grpc.sendCommand, 'was called once');
189+
expect(nav.goPayBitcoinConfirm, 'was called once');
190+
expect(notification.display, 'was not called');
191+
expect(store.payment.fee, 'to be', '0.0001');
192+
});
193+
194+
it('should not get estimate and navigate if fee and sendAll are set', async () => {
195+
store.payment.fee = '0.0002';
196+
store.payment.sendAll = true;
178197
await payment.initPayBitcoinConfirm();
198+
expect(grpc.sendCommand, 'was not called');
199+
expect(nav.goPayBitcoinConfirm, 'was called once');
200+
expect(notification.display, 'was not called');
201+
expect(store.payment.fee, 'to be', '0.0002');
202+
});
203+
204+
it('should get estimate and navigate if fee is set', async () => {
205+
store.payment.fee = '0.0002';
206+
await payment.initPayBitcoinConfirm();
207+
expect(grpc.sendCommand, 'was called once');
208+
expect(nav.goPayBitcoinConfirm, 'was called once');
209+
expect(notification.display, 'was not called');
210+
expect(store.payment.fee, 'to be', '0.0001');
211+
});
212+
213+
it('should get estimate and navigate if sendAll is set', async () => {
214+
store.payment.sendAll = true;
215+
await payment.initPayBitcoinConfirm();
216+
expect(grpc.sendCommand, 'was called once');
179217
expect(nav.goPayBitcoinConfirm, 'was called once');
180218
expect(notification.display, 'was not called');
181219
expect(store.payment.fee, 'to be', '0.0001');
182220
});
183221

184222
it('should display notification on error', async () => {
185-
store.payment.address = 'foo';
186-
store.payment.amount = '2000';
187223
grpc.sendCommand.withArgs('estimateFee').rejects();
188224
await payment.initPayBitcoinConfirm();
225+
expect(grpc.sendCommand, 'was called once');
189226
expect(nav.goPayBitcoinConfirm, 'was not called');
190227
expect(notification.display, 'was called once');
191228
expect(store.payment.fee, 'to be', '');
@@ -209,6 +246,13 @@ describe('Action Payments Unit Tests', () => {
209246
payment.setAmount({ amount: 'some-amount' });
210247
expect(store.payment.amount, 'to equal', 'some-amount');
211248
});
249+
250+
it('should reset sendAll if set', () => {
251+
store.payment.sendAll = true;
252+
payment.setAmount({ amount: 'some-amount' });
253+
expect(store.payment.amount, 'to equal', 'some-amount');
254+
expect(store.payment.sendAll, 'to be false');
255+
});
212256
});
213257

214258
describe('checkType()', () => {
@@ -295,6 +339,27 @@ describe('Action Payments Unit Tests', () => {
295339
});
296340
});
297341

342+
describe('toggleMax()', () => {
343+
it('should set the payment amount to the wallet balance minus fee', async () => {
344+
store.payment.address = 'some-address';
345+
store.balanceSatoshis = 100000;
346+
grpc.sendCommand.resolves({ feeSat: 100 });
347+
await payment.toggleMax();
348+
expect(store.payment.amount, 'to match', /^0[,.]0{3}9{3}$/);
349+
expect(store.payment.sendAll, 'to be true');
350+
});
351+
352+
it('should disable sendAll and reset amount', async () => {
353+
store.payment.sendAll = true;
354+
store.payment.amount = 1000;
355+
store.payment.address = 'some-address';
356+
store.balanceSatoshis = 100000;
357+
await payment.toggleMax();
358+
expect(store.payment.amount, 'to be', '0');
359+
expect(store.payment.sendAll, 'to be false');
360+
});
361+
});
362+
298363
describe('payBitcoin()', () => {
299364
it('should send on-chain transaction', async () => {
300365
store.payment.amount = '0.00001';
@@ -305,6 +370,23 @@ describe('Action Payments Unit Tests', () => {
305370
expect(grpc.sendCommand, 'was called with', 'sendCoins', {
306371
addr: 'some-address',
307372
amount: 1000,
373+
sendAll: false,
374+
});
375+
expect(nav.goPayBitcoinDone, 'was called once');
376+
expect(notification.display, 'was not called');
377+
});
378+
379+
it('should set amount to 0 on sendAll', async () => {
380+
store.payment.sendAll = true;
381+
store.payment.amount = '0.00001';
382+
store.payment.address = 'some-address';
383+
grpc.sendCommand.withArgs('sendCoins').resolves();
384+
await payment.payBitcoin();
385+
expect(nav.goWait, 'was called once');
386+
expect(grpc.sendCommand, 'was called with', 'sendCoins', {
387+
addr: 'some-address',
388+
amount: 0,
389+
sendAll: true,
308390
});
309391
expect(nav.goPayBitcoinDone, 'was called once');
310392
expect(notification.display, 'was not called');

0 commit comments

Comments
 (0)