BlueWallet/screen/lnd/scanLndInvoice.js

357 lines
12 KiB
JavaScript
Raw Normal View History

/* global alert */
import React from 'react';
import { Text, ActivityIndicator, View, TouchableWithoutFeedback, TouchableOpacity, Keyboard } 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,
} from '../../BlueComponents';
2018-12-24 16:29:33 +01:00
import { LightningCustodianWallet } from '../../class/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';
/** @type {AppStorage} */
let BlueApp = require('../../BlueApp');
let EV = require('../../events');
2019-05-02 22:33:03 +02:00
const loc = require('../../loc');
export default class ScanLndInvoice extends React.Component {
static navigationOptions = ({ navigation }) => ({
...BlueNavigationStyle(navigation, true),
title: loc.send.header,
headerLeft: null,
});
state = {
isLoading: false,
isAmountInitiallyEmpty: false,
renderWalletSelectionButtonHidden: false,
};
constructor(props) {
super(props);
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.');
2019-01-25 21:22:51 +01:00
props.navigation.dismiss();
2018-12-29 19:24:51 +01:00
} else {
let fromSecret;
if (props.navigation.state.params.fromSecret) fromSecret = props.navigation.state.params.fromSecret;
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
}
2018-12-29 19:24:51 +01:00
for (let w of BlueApp.getWallets()) {
if (w.getSecret() === fromSecret) {
fromWallet = w;
break;
}
}
2018-12-29 19:24:51 +01:00
this.state = {
fromWallet,
fromSecret,
destination: '',
};
}
}
async componentDidMount() {
this.keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', this._keyboardDidShow);
this.keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', this._keyboardDidHide);
2018-12-24 16:29:33 +01:00
if (this.props.navigation.state.params.uri) {
this.processTextForInvoice(this.props.navigation.getParam('uri'));
}
}
componentWillUnmount() {
this.keyboardDidShowListener.remove();
this.keyboardDidHideListener.remove();
}
_keyboardDidShow = () => {
this.setState({ renderWalletSelectionButtonHidden: true });
};
_keyboardDidHide = () => {
this.setState({ renderWalletSelectionButtonHidden: false });
};
2019-01-24 08:36:01 +01:00
processInvoice = data => {
2019-01-06 21:21:04 +01:00
this.setState({ isLoading: true }, async () => {
if (!this.state.fromWallet) {
2019-05-03 14:36:11 +02:00
ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false });
2019-01-06 21:21:04 +01:00
alert('Before paying a Lightning invoice, you must first add a Lightning wallet.');
return this.props.navigation.goBack();
}
// handling BIP21 w/BOLT11 support
let 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}
*/
let w = this.state.fromWallet;
let decoded;
try {
decoded = await 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) {
expiresIn = 'expired';
} else {
expiresIn = Math.round((expiresIn - +new Date()) / (60 * 1000)) + ' min';
}
Keyboard.dismiss();
this.setState({
invoice: data,
decoded,
expiresIn,
destination: data,
isAmountInitiallyEmpty: decoded.num_satoshis === '0',
isLoading: false,
});
} catch (Err) {
Keyboard.dismiss();
2019-01-06 21:21:04 +01:00
this.setState({ isLoading: false });
2019-05-03 14:36:11 +02:00
ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false });
2019-01-06 21:21:04 +01:00
alert(Err.message);
}
2019-01-06 21:21:04 +01:00
});
2019-01-24 08:36:01 +01:00
};
async pay() {
if (!this.state.hasOwnProperty('decoded')) {
return null;
}
const isBiometricsEnabled = await Biometric.isBiometricUseCapableAndEnabled();
if (isBiometricsEnabled) {
if (!(await Biometric.unlockWithBiometrics())) {
return;
}
}
this.setState(
{
isLoading: true,
},
async () => {
let decoded = this.state.decoded;
/** @type {LightningCustodianWallet} */
let fromWallet = this.state.fromWallet;
let 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 });
return alert('Invoice expired');
}
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 {
2019-01-06 21:55:50 +01:00
await fromWallet.payInvoice(this.state.invoice, this.state.decoded.num_satoshis);
} 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', {
amount: this.state.decoded.num_satoshis,
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 {
if (!this.state.decoded.hasOwnProperty('num_satoshis')) {
return true;
}
}
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={{ marginBottom: 16, alignItems: 'center' }}>
{!this.state.isLoading && (
<TouchableOpacity
style={{ flexDirection: 'row', alignItems: 'center' }}
onPress={() =>
this.props.navigation.navigate('SelectWallet', { onWalletSelect: this.onWalletSelect, chainType: Chain.OFFCHAIN })
}
>
<Text style={{ color: '#9aa0aa', fontSize: 14, marginRight: 8 }}>
{loc.wallets.select_wallet.toLowerCase()}
</Text>
<Icon name="angle-right" size={18} type="font-awesome" color="#9aa0aa" />
</TouchableOpacity>
)}
<View style={{ flexDirection: 'row', alignItems: 'center', marginVertical: 4 }}>
<TouchableOpacity
style={{ flexDirection: 'row', alignItems: 'center' }}
onPress={() =>
this.props.navigation.navigate('SelectWallet', { onWalletSelect: this.onWalletSelect, chainType: Chain.OFFCHAIN })
}
>
<Text style={{ color: '#0c2550', fontSize: 14 }}>{this.state.fromWallet.getLabel()}</Text>
<Text style={{ color: '#0c2550', fontSize: 14, fontWeight: '600', marginLeft: 8, marginRight: 4 }}>
{loc.formatBalanceWithoutSuffix(this.state.fromWallet.getBalance(), BitcoinUnit.SATS, false)}
</Text>
<Text style={{ color: '#0c2550', fontSize: 11, fontWeight: '600', textAlignVertical: 'bottom', marginTop: 2 }}>
{BitcoinUnit.SATS}
</Text>
</TouchableOpacity>
</View>
</View>
);
};
onWalletSelect = wallet => {
this.setState({ fromSecret: wallet.getSecret(), fromWallet: wallet }, () => {
this.props.navigation.pop();
});
};
render() {
return (
<TouchableWithoutFeedback onPress={Keyboard.dismiss} accessible={false}>
<SafeBlueArea forceInset={{ horizontal: 'always' }} style={{ flex: 1 }}>
<View style={{ flex: 1 }}>
<View style={{ flex: 1 }}>
<View style={{ marginTop: 60 }}>
<BlueBitcoinAmount
pointerEvents={this.state.isAmountInitiallyEmpty ? 'auto' : 'none'}
isLoading={this.state.isLoading}
amount={typeof this.state.decoded === 'object' ? this.state.decoded.num_satoshis : 0}
onChangeText={text => {
if (typeof this.state.decoded === 'object') {
text = parseInt(text || 0);
let decoded = this.state.decoded;
decoded.num_satoshis = text;
this.setState({ decoded: decoded });
}
}}
disabled={typeof this.state.decoded !== 'object' || this.state.isLoading}
unit={BitcoinUnit.SATS}
inputAccessoryViewID={BlueDismissKeyboardInputAccessory.InputAccessoryViewID}
/>
</View>
<BlueCard>
<BlueAddressInput
onChangeText={text => {
this.setState({ destination: text });
this.processTextForInvoice(text);
}}
onBarScanned={this.processInvoice}
address={this.state.destination}
isLoading={this.state.isLoading}
placeholder={loc.lnd.placeholder}
inputAccessoryViewID={BlueDismissKeyboardInputAccessory.InputAccessoryViewID}
/>
<View
style={{
flexDirection: 'row',
marginHorizontal: 20,
alignItems: 'center',
marginVertical: 0,
borderRadius: 4,
}}
>
<Text numberOfLines={0} style={{ color: '#81868e', fontWeight: '500', fontSize: 14 }}>
{this.state.hasOwnProperty('decoded') && this.state.decoded !== undefined ? this.state.decoded.description : ''}
</Text>
</View>
{this.state.expiresIn !== undefined && (
<Text style={{ color: '#81868e', fontSize: 12, left: 20, top: 10 }}>Expires in: {this.state.expiresIn}</Text>
)}
<BlueCard>
{this.state.isLoading ? (
<View>
<ActivityIndicator />
</View>
) : (
<BlueButton
title={'Pay'}
onPress={() => {
this.pay();
}}
disabled={this.shouldDisablePayButton()}
/>
)}
</BlueCard>
</BlueCard>
</View>
{this.renderWalletSelectionButton()}
</View>
<BlueDismissKeyboardInputAccessory />
</SafeBlueArea>
</TouchableWithoutFeedback>
);
}
}
ScanLndInvoice.propTypes = {
navigation: PropTypes.shape({
goBack: PropTypes.func,
navigate: PropTypes.func,
pop: PropTypes.func,
getParam: PropTypes.func,
dismiss: PropTypes.func,
state: PropTypes.shape({
params: PropTypes.shape({
2018-12-24 16:29:33 +01:00
uri: PropTypes.string,
fromSecret: PropTypes.string,
}),
}),
}),
};