import React, { useCallback, useContext, useEffect, useRef, useState } from 'react'; import { ActivityIndicator, Image, Keyboard, KeyboardAvoidingView, StyleSheet, Text, TextInput, Platform, TouchableOpacity, TouchableWithoutFeedback, View, I18nManager, } from 'react-native'; import ReactNativeHapticFeedback from 'react-native-haptic-feedback'; import { Icon } from 'react-native-elements'; import { useFocusEffect, useNavigation, useRoute } from '@react-navigation/native'; import { BlueAlertWalletExportReminder, BlueDismissKeyboardInputAccessory, BlueLoading } from '../../BlueComponents'; import navigationStyle from '../../components/navigationStyle'; import AmountInput from '../../components/AmountInput'; import * as NavigationService from '../../NavigationService'; import { BitcoinUnit, Chain } from '../../models/bitcoinUnits'; import loc, { formatBalance, formatBalanceWithoutSuffix, formatBalancePlain } from '../../loc'; import Lnurl from '../../class/lnurl'; import { BlueStorageContext } from '../../blue_modules/storage-context'; import Notifications from '../../blue_modules/notifications'; import alert from '../../components/Alert'; import { parse } from 'url'; // eslint-disable-line n/no-deprecated-api import { requestCameraAuthorization } from '../../helpers/scan-qr'; import { useTheme } from '../../components/themes'; import { isTorCapable } from '../../blue_modules/environment'; import Button from '../../components/Button'; const currency = require('../../blue_modules/currency'); const torrific = isTorCapable ? require('../../blue_modules/torrific') : require('../../scripts/maccatalystpatches/torrific.js'); const LNDCreateInvoice = () => { const { wallets, saveToDisk, setSelectedWalletID, isTorDisabled } = useContext(BlueStorageContext); const { walletID, uri } = useRoute().params; const wallet = useRef(wallets.find(item => item.getID() === walletID) || wallets.find(item => item.chain === Chain.OFFCHAIN)); const { name } = useRoute(); const { colors } = useTheme(); const { navigate, getParent, goBack, pop, setParams } = useNavigation(); const [unit, setUnit] = useState(wallet.current?.getPreferredBalanceUnit() || BitcoinUnit.BTC); const [amount, setAmount] = useState(); const [renderWalletSelectionButtonHidden, setRenderWalletSelectionButtonHidden] = useState(false); const [isLoading, setIsLoading] = useState(true); const [description, setDescription] = useState(''); const [lnurlParams, setLNURLParams] = useState(); const styleHooks = StyleSheet.create({ scanRoot: { backgroundColor: colors.scanLabel, }, scanClick: { color: colors.inverseForegroundColor, }, walletNameText: { color: colors.buttonAlternativeTextColor, }, walletNameBalance: { color: colors.buttonAlternativeTextColor, }, walletNameSats: { color: colors.buttonAlternativeTextColor, }, root: { backgroundColor: colors.elevated, }, amount: { backgroundColor: colors.elevated, }, fiat: { borderColor: colors.formBorder, borderBottomColor: colors.formBorder, backgroundColor: colors.inputBackgroundColor, }, }); useEffect(() => { // console.log(params) Keyboard.addListener('keyboardDidShow', _keyboardDidShow); Keyboard.addListener('keyboardDidHide', _keyboardDidHide); return () => { Keyboard.removeAllListeners('keyboardDidShow'); Keyboard.removeAllListeners('keyboardDidHide'); }; }, []); const renderReceiveDetails = async () => { try { wallet.current.setUserHasSavedExport(true); await saveToDisk(); if (uri) { processLnurl(uri); } } catch (e) { console.log(e); } setIsLoading(false); }; useEffect(() => { if (wallet.current && wallet.current.getID() !== walletID) { const newWallet = wallets.find(w => w.getID() === walletID); if (newWallet) { wallet.current = newWallet; setSelectedWalletID(newWallet.getID()); } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [walletID]); useFocusEffect( useCallback(() => { if (wallet.current) { setSelectedWalletID(walletID); if (wallet.current.getUserHasSavedExport()) { renderReceiveDetails(); } else { BlueAlertWalletExportReminder({ onSuccess: () => renderReceiveDetails(), onFailure: () => { getParent().pop(); NavigationService.navigate('WalletExportRoot', { screen: 'WalletExport', params: { walletID, }, }); }, }); } } else { ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false }); alert(loc.wallets.add_ln_wallet_first); goBack(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [wallet]), ); const _keyboardDidShow = () => { setRenderWalletSelectionButtonHidden(true); }; const _keyboardDidHide = () => { setRenderWalletSelectionButtonHidden(false); }; const createInvoice = async () => { setIsLoading(true); try { let invoiceAmount = amount; switch (unit) { case BitcoinUnit.SATS: invoiceAmount = parseInt(invoiceAmount, 10); // basically nop break; case BitcoinUnit.BTC: invoiceAmount = currency.btcToSatoshi(invoiceAmount); break; case BitcoinUnit.LOCAL_CURRENCY: // trying to fetch cached sat equivalent for this fiat amount invoiceAmount = AmountInput.getCachedSatoshis(invoiceAmount) || currency.btcToSatoshi(currency.fiatToBTC(invoiceAmount)); break; } if (lnurlParams) { const { min, max } = lnurlParams; if (invoiceAmount < min || invoiceAmount > max) { let text; if (invoiceAmount < min) { text = unit === BitcoinUnit.SATS ? loc.formatString(loc.receive.minSats, { min }) : loc.formatString(loc.receive.minSatsFull, { min, currency: formatBalance(min, unit) }); } else { text = unit === BitcoinUnit.SATS ? loc.formatString(loc.receive.maxSats, { max }) : loc.formatString(loc.receive.maxSatsFull, { max, currency: formatBalance(max, unit) }); } ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false }); alert(text); setIsLoading(false); return; } } const invoiceRequest = await wallet.current.addInvoice(invoiceAmount, description); ReactNativeHapticFeedback.trigger('notificationSuccess', { ignoreAndroidSystemSettings: false }); // lets decode payreq and subscribe groundcontrol so we can receive push notification when our invoice is paid /** @type LightningCustodianWallet */ const decoded = await wallet.current.decodeInvoice(invoiceRequest); await Notifications.tryToObtainPermissions(); Notifications.majorTomToGroundControl([], [decoded.payment_hash], []); // send to lnurl-withdraw callback url if that exists if (lnurlParams) { const { callback, k1 } = lnurlParams; const callbackUrl = callback + (callback.indexOf('?') !== -1 ? '&' : '?') + 'k1=' + k1 + '&pr=' + invoiceRequest; let reply; if (!isTorDisabled && callbackUrl.includes('.onion')) { const api = new torrific.Torsbee(); const torResponse = await api.get(callbackUrl); reply = torResponse.body; if (reply && typeof reply === 'string') reply = JSON.parse(reply); } else { const resp = await fetch(callbackUrl, { method: 'GET' }); if (resp.status >= 300) { const text = await resp.text(); throw new Error(text); } reply = await resp.json(); } if (reply.status === 'ERROR') { throw new Error('Reply from server: ' + reply.reason); } } setTimeout(async () => { // wallet object doesnt have this fresh invoice in its internals, so we refetch it and only then save await wallet.current.fetchUserInvoices(1); await saveToDisk(); }, 1000); navigate('LNDViewInvoice', { invoice: invoiceRequest, walletID: wallet.current.getID(), }); } catch (Err) { ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false }); setIsLoading(false); alert(Err.message); } }; const processLnurl = async data => { setIsLoading(true); if (!wallet.current) { ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false }); alert(loc.wallets.no_ln_wallet_error); return goBack(); } // decoding the lnurl const url = Lnurl.getUrlFromLnurl(data); const { query } = parse(url, true); if (query.tag === Lnurl.TAG_LOGIN_REQUEST) { navigate('LnurlAuth', { lnurl: data, walletID: walletID ?? wallet.current.getID(), }); return; } // calling the url let reply; try { if (!isTorDisabled && url.includes('.onion')) { const api = new torrific.Torsbee(); const torResponse = await api.get(url); reply = torResponse.body; if (reply && typeof reply === 'string') reply = JSON.parse(reply); } else { const resp = await fetch(url, { method: 'GET' }); if (resp.status >= 300) { throw new Error('Bad response from server'); } reply = await resp.json(); if (reply.status === 'ERROR') { throw new Error('Reply from server: ' + reply.reason); } } if (reply.tag === Lnurl.TAG_PAY_REQUEST) { // we are here by mistake. user wants to SEND to lnurl-pay, but he is on a screen that creates // invoices (including through lnurl-withdraw) navigate('ScanLndInvoiceRoot', { screen: 'LnurlPay', params: { lnurl: data, walletID: walletID ?? wallet.current.getID(), }, }); return; } if (reply.tag !== Lnurl.TAG_WITHDRAW_REQUEST) { throw new Error('Unsupported lnurl'); } // amount that comes from lnurl is always in sats let newAmount = (reply.maxWithdrawable / 1000).toString(); const sats = newAmount; switch (unit) { case BitcoinUnit.SATS: // nop break; case BitcoinUnit.BTC: newAmount = currency.satoshiToBTC(newAmount); break; case BitcoinUnit.LOCAL_CURRENCY: newAmount = formatBalancePlain(newAmount, BitcoinUnit.LOCAL_CURRENCY); AmountInput.setCachedSatoshis(newAmount, sats); break; } // setting the invoice creating screen with the parameters setLNURLParams({ k1: reply.k1, callback: reply.callback, fixed: reply.minWithdrawable === reply.maxWithdrawable, min: (reply.minWithdrawable || 0) / 1000, max: reply.maxWithdrawable / 1000, }); setAmount(newAmount); setDescription(reply.defaultDescription); setIsLoading(false); } catch (Err) { Keyboard.dismiss(); setIsLoading(false); ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false }); alert(Err.message); } }; const renderCreateButton = () => { return ( {isLoading ? :