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 { 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 presentAlert 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 Button from '../../components/Button'; import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback'; import { btcToSatoshi, fiatToBTC, satoshiToBTC } from '../../blue_modules/currency'; const LNDCreateInvoice = () => { const { wallets, saveToDisk, setSelectedWalletID } = 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 { triggerHapticFeedback(HapticFeedbackTypes.NotificationError); presentAlert({ message: 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 = btcToSatoshi(invoiceAmount); break; case BitcoinUnit.LOCAL_CURRENCY: // trying to fetch cached sat equivalent for this fiat amount invoiceAmount = AmountInput.getCachedSatoshis(invoiceAmount) || btcToSatoshi(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) }); } triggerHapticFeedback(HapticFeedbackTypes.NotificationError); presentAlert({ message: text }); setIsLoading(false); return; } } const invoiceRequest = await wallet.current.addInvoice(invoiceAmount, description); triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess); // 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; const resp = await fetch(callbackUrl, { method: 'GET' }); if (resp.status >= 300) { const text = await resp.text(); throw new Error(text); } const 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) { triggerHapticFeedback(HapticFeedbackTypes.NotificationError); setIsLoading(false); presentAlert({ message: Err.message }); } }; const processLnurl = async data => { setIsLoading(true); if (!wallet.current) { triggerHapticFeedback(HapticFeedbackTypes.NotificationError); presentAlert({ message: 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 try { const resp = await fetch(url, { method: 'GET' }); if (resp.status >= 300) { throw new Error('Bad response from server'); } const 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 = 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); triggerHapticFeedback(HapticFeedbackTypes.NotificationError); presentAlert({ message: Err.message }); } }; const renderCreateButton = () => { return ( {isLoading ? :