/* global alert */ import React from 'react'; import { Text, ActivityIndicator, Platform, KeyboardAvoidingView, View, TouchableWithoutFeedback, TouchableOpacity, Keyboard, } from 'react-native'; import PropTypes from 'prop-types'; import { BlueButton, SafeBlueArea, BlueCard, BlueDismissKeyboardInputAccessory, BlueNavigationStyle, BlueAddressInput, BlueBitcoinAmount, BlueLoading, } from '../../BlueComponents'; 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'); 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); this.keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', this._keyboardDidShow); this.keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', this._keyboardDidHide); if (!BlueApp.getWallets().some(item => item.type === LightningCustodianWallet.type)) { ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false }); alert('Before paying a Lightning invoice, you must first add a Lightning wallet.'); props.navigation.dismiss(); } 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'); } } for (let w of BlueApp.getWallets()) { if (w.getSecret() === fromSecret) { fromWallet = w; break; } } this.state = { fromWallet, fromSecret, destination: '', }; } } static getDerivedStateFromProps(props, state) { if (props.navigation.state.params.uri) { let data = props.navigation.state.params.uri; // handling BIP21 w/BOLT11 support let ind = data.indexOf('lightning='); if (ind !== -1) { data = data.substring(ind + 10).split('&')[0]; } data = data.replace('LIGHTNING:', '').replace('lightning:', ''); console.log(data); /** * @type {LightningCustodianWallet} */ let w = state.fromWallet; let decoded; try { decoded = w.decodeInvoice(data); 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(); props.navigation.setParams({ uri: undefined }); return { invoice: data, decoded, expiresIn, destination: data, isAmountInitiallyEmpty: decoded.num_satoshis === '0', isLoading: false, }; } catch (Err) { ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false }); Keyboard.dismiss(); props.navigation.setParams({ uri: undefined }); setTimeout(() => alert(Err.message), 10); return { ...state, isLoading: false }; } } 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 }); }; 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 }); ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false }); return alert('Invoice expired'); } 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 }); ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false }); return alert(loc.lnd.sameWalletAsInvoiceError); } try { await fromWallet.payInvoice(this.state.invoice, this.state.decoded.num_satoshis); } catch (Err) { console.log(Err.message); this.setState({ isLoading: false }); ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false }); 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, }); }, ); } processTextForInvoice = text => { if (text.toLowerCase().startsWith('lnb') || text.toLowerCase().startsWith('lightning:lnb')) { this.processInvoice(text); } else { this.setState({ decoded: undefined, expiresIn: undefined, destination: text }); } }; shouldDisablePayButton = () => { if (typeof this.state.decoded !== 'object') { return true; } else { if (!this.state.decoded.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 ( {!this.state.isLoading && ( this.props.navigation.navigate('SelectWallet', { onWalletSelect: this.onWalletSelect, chainType: Chain.OFFCHAIN }) } > {loc.wallets.select_wallet.toLowerCase()} )} this.props.navigation.navigate('SelectWallet', { onWalletSelect: this.onWalletSelect, chainType: Chain.OFFCHAIN }) } > {this.state.fromWallet.getLabel()} {loc.formatBalanceWithoutSuffix(this.state.fromWallet.getBalance(), BitcoinUnit.SATS, false)} {BitcoinUnit.SATS} ); }; onWalletSelect = wallet => { this.setState({ fromSecret: wallet.getSecret(), fromWallet: wallet }, () => { this.props.navigation.pop(); }); }; render() { if (!this.state.fromWallet) { return ; } return ( { 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} /> { text = text.trim(); this.processTextForInvoice(text); }} onBarScanned={this.processInvoice} address={this.state.destination} isLoading={this.state.isLoading} placeholder={loc.lnd.placeholder} inputAccessoryViewID={BlueDismissKeyboardInputAccessory.InputAccessoryViewID} launchedBy={this.props.navigation.state.routeName} /> {this.state.hasOwnProperty('decoded') && this.state.decoded !== undefined ? this.state.decoded.description : ''} {this.state.expiresIn !== undefined && ( Expires in: {this.state.expiresIn} )} {this.state.isLoading ? ( ) : ( { this.pay(); }} disabled={this.shouldDisablePayButton()} /> )} {this.renderWalletSelectionButton()} ); } } ScanLndInvoice.propTypes = { navigation: PropTypes.shape({ goBack: PropTypes.func, navigate: PropTypes.func, pop: PropTypes.func, getParam: PropTypes.func, setParams: PropTypes.func, dismiss: PropTypes.func, state: PropTypes.shape({ routeName: PropTypes.string, params: PropTypes.shape({ uri: PropTypes.string, fromSecret: PropTypes.string, }), }), }), };