BlueWallet/screen/lnd/scanLndInvoice.js

462 lines
14 KiB
JavaScript
Raw Normal View History

/* global alert */
import React from 'react';
import {
Text,
ActivityIndicator,
KeyboardAvoidingView,
View,
TouchableOpacity,
StatusBar,
Keyboard,
ScrollView,
StyleSheet,
} from 'react-native';
import PropTypes from 'prop-types';
2019-01-24 08:36:01 +01:00
import {
BlueButton,
SafeBlueArea,
BlueCard,
BlueDismissKeyboardInputAccessory,
2019-01-24 08:36:01 +01:00
BlueNavigationStyle,
BlueAddressInput,
BlueBitcoinAmount,
2019-12-28 01:53:34 +01:00
BlueLoading,
2019-01-24 08:36:01 +01:00
} from '../../BlueComponents';
import { LightningCustodianWallet } from '../../class/wallets/lightning-custodian-wallet';
import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
import { Icon } from 'react-native-elements';
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
import Biometric from '../../class/biometrics';
2020-07-20 15:38:46 +02:00
import loc, { formatBalanceWithoutSuffix } from '../../loc';
2020-07-15 19:32:59 +02:00
import { BlueCurrentTheme } from '../../components/themes';
const BlueApp = require('../../BlueApp');
const EV = require('../../blue_modules/events');
2020-06-09 16:08:18 +02:00
const currency = require('../../blue_modules/currency');
const styles = StyleSheet.create({
walletSelectRoot: {
marginBottom: 16,
alignItems: 'center',
justifyContent: 'flex-end',
},
walletSelectTouch: {
flexDirection: 'row',
alignItems: 'center',
},
walletSelectText: {
color: '#9aa0aa',
fontSize: 14,
marginRight: 8,
},
walletWrap: {
flexDirection: 'row',
alignItems: 'center',
marginVertical: 4,
},
walletWrapTouch: {
flexDirection: 'row',
alignItems: 'center',
},
walletWrapLabel: {
2020-07-15 19:32:59 +02:00
color: BlueCurrentTheme.colors.buttonAlternativeTextColor,
fontSize: 14,
},
walletWrapBalance: {
2020-07-15 19:32:59 +02:00
color: BlueCurrentTheme.colors.buttonAlternativeTextColor,
fontSize: 14,
fontWeight: '600',
marginLeft: 4,
marginRight: 4,
},
walletWrapSats: {
2020-07-15 19:32:59 +02:00
color: BlueCurrentTheme.colors.buttonAlternativeTextColor,
fontSize: 11,
fontWeight: '600',
textAlignVertical: 'bottom',
marginTop: 2,
},
root: {
flex: 1,
2020-07-15 19:32:59 +02:00
backgroundColor: BlueCurrentTheme.colors.elevated,
},
scroll: {
flex: 1,
justifyContent: 'space-between',
},
scrollMargin: {
marginTop: 60,
},
description: {
flexDirection: 'row',
marginHorizontal: 20,
alignItems: 'center',
marginVertical: 0,
borderRadius: 4,
},
descriptionText: {
color: '#81868e',
fontWeight: '500',
fontSize: 14,
},
expiresIn: {
color: '#81868e',
fontSize: 12,
left: 20,
top: 10,
},
});
export default class ScanLndInvoice extends React.Component {
state = {
isLoading: false,
isAmountInitiallyEmpty: false,
renderWalletSelectionButtonHidden: false,
};
constructor(props) {
super(props);
2019-12-28 01:53:34 +01:00
this.keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', this._keyboardDidShow);
this.keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', this._keyboardDidHide);
2018-12-29 19:24:51 +01:00
if (!BlueApp.getWallets().some(item => item.type === LightningCustodianWallet.type)) {
2019-05-03 14:36:11 +02:00
ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false });
2018-12-29 19:24:51 +01:00
alert('Before paying a Lightning invoice, you must first add a Lightning wallet.');
2020-05-27 13:12:17 +02:00
props.navigation.dangerouslyGetParent().pop();
2018-12-29 19:24:51 +01:00
} else {
let fromSecret;
2020-05-27 13:12:17 +02:00
if (props.route.params.fromSecret) fromSecret = props.route.params.fromSecret;
2018-12-29 19:24:51 +01:00
let fromWallet = {};
if (!fromSecret) {
const lightningWallets = BlueApp.getWallets().filter(item => item.type === LightningCustodianWallet.type);
if (lightningWallets.length > 0) {
fromSecret = lightningWallets[0].getSecret();
console.warn('warning: using ln wallet index 0');
2018-12-29 19:24:51 +01:00
}
2018-12-24 16:29:33 +01:00
}
for (const w of BlueApp.getWallets()) {
2018-12-29 19:24:51 +01:00
if (w.getSecret() === fromSecret) {
fromWallet = w;
break;
}
}
2018-12-29 19:24:51 +01:00
this.state = {
fromWallet,
fromSecret,
2020-06-09 16:08:18 +02:00
unit: BitcoinUnit.SATS,
2018-12-29 19:24:51 +01:00
destination: '',
};
}
}
2020-01-20 04:33:17 +01:00
static getDerivedStateFromProps(props, state) {
2020-05-27 13:12:17 +02:00
if (props.route.params.uri) {
let data = props.route.params.uri;
// handling BIP21 w/BOLT11 support
const ind = data.indexOf('lightning=');
if (ind !== -1) {
data = data.substring(ind + 10).split('&')[0];
}
2019-01-06 21:21:04 +01:00
data = data.replace('LIGHTNING:', '').replace('lightning:', '');
console.log(data);
/**
* @type {LightningCustodianWallet}
*/
const w = state.fromWallet;
2019-01-06 21:21:04 +01:00
let decoded;
try {
2019-12-26 20:34:35 +01:00
decoded = w.decodeInvoice(data);
2019-01-06 21:21:04 +01:00
let expiresIn = (decoded.timestamp * 1 + decoded.expiry * 1) * 1000; // ms
if (+new Date() > expiresIn) {
2020-07-20 15:38:46 +02:00
expiresIn = loc.lnd.expiredLow;
2019-01-06 21:21:04 +01:00
} else {
expiresIn = Math.round((expiresIn - +new Date()) / (60 * 1000)) + ' min';
}
Keyboard.dismiss();
2020-01-20 04:33:17 +01:00
props.navigation.setParams({ uri: undefined });
return {
2019-01-06 21:21:04 +01:00
invoice: data,
decoded,
2020-06-09 16:08:18 +02:00
unit: state.unit,
amount: decoded.num_satoshis,
2019-01-06 21:21:04 +01:00
expiresIn,
destination: data,
isAmountInitiallyEmpty: decoded.num_satoshis === '0',
isLoading: false,
2020-01-20 04:33:17 +01:00
};
2019-01-06 21:21:04 +01:00
} catch (Err) {
2019-05-03 14:36:11 +02:00
ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false });
2020-01-20 04:33:17 +01:00
Keyboard.dismiss();
props.navigation.setParams({ uri: undefined });
setTimeout(() => alert(Err.message), 10);
return { ...state, isLoading: false };
}
2020-01-20 04:33:17 +01:00
}
return state;
}
componentWillUnmount() {
this.keyboardDidShowListener.remove();
this.keyboardDidHideListener.remove();
}
_keyboardDidShow = () => {
this.setState({ renderWalletSelectionButtonHidden: true });
};
_keyboardDidHide = () => {
this.setState({ renderWalletSelectionButtonHidden: false });
};
processInvoice = data => {
this.props.navigation.setParams({ uri: data });
2019-01-24 08:36:01 +01:00
};
async pay() {
if (!('decoded' in this.state)) {
return null;
}
const isBiometricsEnabled = await Biometric.isBiometricUseCapableAndEnabled();
if (isBiometricsEnabled) {
if (!(await Biometric.unlockWithBiometrics())) {
return;
}
}
2020-06-09 16:08:18 +02:00
let amountSats = this.state.amount;
switch (this.state.unit) {
case BitcoinUnit.SATS:
amountSats = parseInt(amountSats); // nop
break;
case BitcoinUnit.BTC:
amountSats = currency.btcToSatoshi(amountSats);
break;
case BitcoinUnit.LOCAL_CURRENCY:
amountSats = currency.btcToSatoshi(currency.fiatToBTC(amountSats));
break;
}
this.setState(
{
isLoading: true,
},
async () => {
const decoded = this.state.decoded;
/** @type {LightningCustodianWallet} */
const fromWallet = this.state.fromWallet;
const expiresIn = (decoded.timestamp * 1 + decoded.expiry * 1) * 1000; // ms
if (+new Date() > expiresIn) {
this.setState({ isLoading: false });
2019-05-03 14:36:11 +02:00
ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false });
2020-07-20 15:38:46 +02:00
return alert(loc.lnd.errorInvoiceExpired);
}
2019-01-10 17:16:44 +01:00
const currentUserInvoices = fromWallet.user_invoices_raw; // not fetching invoices, as we assume they were loaded previously
if (currentUserInvoices.some(invoice => invoice.payment_hash === decoded.payment_hash)) {
this.setState({ isLoading: false });
2019-05-03 14:36:11 +02:00
ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false });
return alert(loc.lnd.sameWalletAsInvoiceError);
}
try {
2020-06-09 16:08:18 +02:00
await fromWallet.payInvoice(this.state.invoice, amountSats);
} catch (Err) {
console.log(Err.message);
this.setState({ isLoading: false });
2019-05-03 14:36:11 +02:00
ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false });
2019-02-04 22:09:51 +01:00
return alert(Err.message);
}
EV(EV.enum.REMOTE_TRANSACTIONS_COUNT_CHANGED); // someone should fetch txs
this.props.navigation.navigate('Success', {
2020-06-09 16:08:18 +02:00
amount: amountSats,
amountUnit: BitcoinUnit.SATS,
invoiceDescription: this.state.decoded.description,
});
},
);
}
2018-12-24 16:29:33 +01:00
processTextForInvoice = text => {
if (text.toLowerCase().startsWith('lnb') || text.toLowerCase().startsWith('lightning:lnb')) {
this.processInvoice(text);
} else {
2018-12-25 17:34:51 +01:00
this.setState({ decoded: undefined, expiresIn: undefined, destination: text });
2018-12-24 16:29:33 +01:00
}
};
shouldDisablePayButton = () => {
if (typeof this.state.decoded !== 'object') {
return true;
} else {
2020-06-09 16:08:18 +02:00
if (!this.state.amount) {
return true;
}
}
2020-06-09 16:08:18 +02:00
return !(this.state.amount > 0);
// return this.state.decoded.num_satoshis <= 0 || this.state.isLoading || isNaN(this.state.decoded.num_satoshis);
};
renderWalletSelectionButton = () => {
if (this.state.renderWalletSelectionButtonHidden) return;
return (
<View style={styles.walletSelectRoot}>
{!this.state.isLoading && (
<TouchableOpacity
style={styles.walletSelectTouch}
onPress={() =>
this.props.navigation.navigate('SelectWallet', { onWalletSelect: this.onWalletSelect, chainType: Chain.OFFCHAIN })
}
>
<Text style={styles.walletSelectText}>{loc.wallets.select_wallet.toLowerCase()}</Text>
<Icon name="angle-right" size={18} type="font-awesome" color="#9aa0aa" />
</TouchableOpacity>
)}
<View style={styles.walletWrap}>
<TouchableOpacity
style={styles.walletWrapTouch}
onPress={() =>
this.props.navigation.navigate('SelectWallet', { onWalletSelect: this.onWalletSelect, chainType: Chain.OFFCHAIN })
}
>
<Text style={styles.walletWrapLabel}>{this.state.fromWallet.getLabel()}</Text>
<Text style={styles.walletWrapBalance}>
2020-07-20 15:38:46 +02:00
{formatBalanceWithoutSuffix(this.state.fromWallet.getBalance(), BitcoinUnit.SATS, false)}
</Text>
<Text style={styles.walletWrapSats}>{BitcoinUnit.SATS}</Text>
</TouchableOpacity>
</View>
</View>
);
};
2020-06-04 15:07:33 +02:00
getFees() {
const min = Math.floor(this.state.decoded.num_satoshis * 0.003);
const max = Math.floor(this.state.decoded.num_satoshis * 0.01) + 1;
return `${min} sat - ${max} sat`;
}
onWalletSelect = wallet => {
this.setState({ fromSecret: wallet.getSecret(), fromWallet: wallet }, () => {
this.props.navigation.pop();
});
};
2020-06-09 16:08:18 +02:00
async componentDidMount() {
console.log('scanLndInvoice did mount');
}
render() {
2019-12-28 01:53:34 +01:00
if (!this.state.fromWallet) {
return <BlueLoading />;
}
2020-07-20 15:38:46 +02:00
return (
<SafeBlueArea forceInset={{ horizontal: 'always' }} style={styles.root}>
<StatusBar barStyle="light-content" />
<View style={styles.root}>
<ScrollView contentContainerStyle={styles.scroll}>
2020-04-07 21:17:43 +02:00
<KeyboardAvoidingView enabled behavior="position" keyboardVerticalOffset={20}>
<View style={styles.scrollMargin}>
<BlueBitcoinAmount
pointerEvents={this.state.isAmountInitiallyEmpty ? 'auto' : 'none'}
isLoading={this.state.isLoading}
2020-06-09 16:08:18 +02:00
amount={this.state.amount}
2020-07-20 15:38:46 +02:00
onAmountUnitChange={unit => this.setState({ unit })}
onChangeText={text => {
2020-06-09 16:08:18 +02:00
this.setState({ amount: text });
/* if (typeof this.state.decoded === 'object') {
text = parseInt(text || 0);
const decoded = this.state.decoded;
decoded.num_satoshis = text;
this.setState({ decoded: decoded });
2020-06-09 16:08:18 +02:00
} */
}}
2020-06-09 16:08:18 +02:00
disabled={
typeof this.state.decoded !== 'object' ||
this.state.isLoading ||
(this.state.decoded && this.state.decoded.num_satoshis > 0)
}
unit={BitcoinUnit.SATS}
inputAccessoryViewID={BlueDismissKeyboardInputAccessory.InputAccessoryViewID}
/>
</View>
2019-10-06 13:40:21 +02:00
<BlueCard>
<BlueAddressInput
onChangeText={text => {
2020-01-20 04:33:17 +01:00
text = text.trim();
this.processTextForInvoice(text);
}}
onBarScanned={this.processInvoice}
address={this.state.destination}
isLoading={this.state.isLoading}
placeholder={loc.lnd.placeholder}
inputAccessoryViewID={BlueDismissKeyboardInputAccessory.InputAccessoryViewID}
2020-05-27 13:12:17 +02:00
launchedBy={this.props.route.name}
/>
<View style={styles.description}>
<Text numberOfLines={0} style={styles.descriptionText}>
{'decoded' in this.state && this.state.decoded !== undefined ? this.state.decoded.description : ''}
</Text>
</View>
2020-06-04 15:07:33 +02:00
{this.state.expiresIn !== undefined && (
2020-06-03 22:40:16 +02:00
<View>
2020-07-20 15:38:46 +02:00
<Text style={styles.expiresIn}>{loc.formatString(loc.lnd.expiresIn, { time: this.state.expiresIn })}</Text>
2020-06-04 15:07:33 +02:00
{this.state.decoded && this.state.decoded.num_satoshis > 0 && (
2020-07-20 15:38:46 +02:00
<Text style={styles.expiresIn}>{loc.formatString(loc.lnd.potentialFee, { fee: this.getFees() })}</Text>
2020-06-04 15:07:33 +02:00
)}
</View>
)}
<BlueCard>
{this.state.isLoading ? (
<View>
<ActivityIndicator />
</View>
) : (
2020-07-20 15:38:46 +02:00
<BlueButton title={loc.lnd.payButton} onPress={() => this.pay()} disabled={this.shouldDisablePayButton()} />
)}
</BlueCard>
</BlueCard>
</KeyboardAvoidingView>
{this.renderWalletSelectionButton()}
2020-04-07 21:17:43 +02:00
</ScrollView>
</View>
<BlueDismissKeyboardInputAccessory />
</SafeBlueArea>
);
}
}
ScanLndInvoice.propTypes = {
navigation: PropTypes.shape({
goBack: PropTypes.func,
navigate: PropTypes.func,
pop: PropTypes.func,
2020-01-20 04:33:17 +01:00
setParams: PropTypes.func,
2020-05-27 13:12:17 +02:00
dangerouslyGetParent: PropTypes.func,
}),
route: PropTypes.shape({
name: PropTypes.string,
params: PropTypes.shape({
uri: PropTypes.string,
fromSecret: PropTypes.string,
}),
}),
2020-04-17 17:23:18 +02:00
};
2020-07-15 19:32:59 +02:00
ScanLndInvoice.navigationOptions = ({ navigation }) => ({
...BlueNavigationStyle(navigation, true),
title: loc.send.header,
headerLeft: null,
});