import React, { useState, useEffect, useContext } from 'react'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { I18nManager, Image, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import { useNavigation, useRoute } from '@react-navigation/native'; import { Icon } from 'react-native-elements'; import { BlueCard, BlueDismissKeyboardInputAccessory, BlueLoading, BlueSpacing20, BlueText } from '../../BlueComponents'; import navigationStyle from '../../components/navigationStyle'; import AmountInput from '../../components/AmountInput'; import Lnurl from '../../class/lnurl'; import { BitcoinUnit, Chain } from '../../models/bitcoinUnits'; import loc, { formatBalanceWithoutSuffix, formatBalance } from '../../loc'; import Biometric from '../../class/biometrics'; import { BlueStorageContext } from '../../blue_modules/storage-context'; import presentAlert from '../../components/Alert'; import { useTheme } from '../../components/themes'; import Button from '../../components/Button'; import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback'; import SafeArea from '../../components/SafeArea'; import { btcToSatoshi, fiatToBTC, satoshiToBTC, satoshiToLocalCurrency } from '../../blue_modules/currency'; const prompt = require('../../helpers/prompt'); /** * if user has default currency - fiat, attempting to pay will trigger conversion from entered in input field fiat value * to satoshi, and attempt to pay this satoshi value, which might be a little bit off from `min` & `max` values * provided by LnUrl. thats why we cache initial precise conversion rate so the reverse conversion wont be off. */ const _cacheFiatToSat = {}; const LnurlPay = () => { const { wallets } = useContext(BlueStorageContext); const { walletID, lnurl } = useRoute().params; /** @type {LightningCustodianWallet} */ const wallet = wallets.find(w => w.getID() === walletID); const [unit, setUnit] = useState(wallet.getPreferredBalanceUnit()); const [isLoading, setIsLoading] = useState(true); const [_LN, setLN] = useState(); const [payButtonDisabled, setPayButtonDisabled] = useState(true); const [payload, setPayload] = useState(); const { setParams, pop, navigate } = useNavigation(); const [amount, setAmount] = useState(); const { colors } = useTheme(); const stylesHook = StyleSheet.create({ root: { backgroundColor: colors.background, }, walletWrapLabel: { color: colors.buttonAlternativeTextColor, }, walletWrapBalance: { color: colors.buttonAlternativeTextColor, }, walletWrapSats: { color: colors.buttonAlternativeTextColor, }, }); useEffect(() => { if (lnurl) { const ln = new Lnurl(lnurl, AsyncStorage); ln.callLnurlPayService() .then(setPayload) .catch(error => { presentAlert({ message: error.message }); pop(); }); setLN(ln); setIsLoading(false); } }, [lnurl, pop]); useEffect(() => { setPayButtonDisabled(isLoading); }, [isLoading]); useEffect(() => { if (payload) { /** @type {Lnurl} */ const LN = _LN; let originalSatAmount; let newAmount = (originalSatAmount = LN.getMin()); if (!newAmount) { presentAlert({ message: 'Internal error: incorrect LNURL amount' }); return; } switch (unit) { case BitcoinUnit.BTC: newAmount = satoshiToBTC(newAmount); break; case BitcoinUnit.LOCAL_CURRENCY: newAmount = satoshiToLocalCurrency(newAmount, false); _cacheFiatToSat[newAmount] = originalSatAmount; break; } setAmount(newAmount); } }, [payload]); // eslint-disable-line react-hooks/exhaustive-deps const onWalletSelect = w => { setParams({ walletID: w.getID() }); pop(); }; const pay = async () => { setPayButtonDisabled(true); /** @type {Lnurl} */ const LN = _LN; const isBiometricsEnabled = await Biometric.isBiometricUseCapableAndEnabled(); if (isBiometricsEnabled) { if (!(await Biometric.unlockWithBiometrics())) { return; } } let amountSats = amount; switch (unit) { case BitcoinUnit.SATS: amountSats = parseInt(amountSats, 10); // nop break; case BitcoinUnit.BTC: amountSats = btcToSatoshi(amountSats); break; case BitcoinUnit.LOCAL_CURRENCY: if (_cacheFiatToSat[amount]) { amountSats = _cacheFiatToSat[amount]; } else { amountSats = btcToSatoshi(fiatToBTC(amountSats)); } break; } let bolt11payload; try { let comment; if (LN.getCommentAllowed()) { comment = await prompt('Comment', '', false, 'plain-text'); } bolt11payload = await LN.requestBolt11FromLnurlPayService(amountSats, comment); await wallet.payInvoice(bolt11payload.pr); const decoded = wallet.decodeInvoice(bolt11payload.pr); setPayButtonDisabled(false); // success, probably triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess); if (wallet.last_paid_invoice_result && wallet.last_paid_invoice_result.payment_preimage) { await LN.storeSuccess(decoded.payment_hash, wallet.last_paid_invoice_result.payment_preimage); } navigate('ScanLndInvoiceRoot', { screen: 'LnurlPaySuccess', params: { paymentHash: decoded.payment_hash, justPaid: true, fromWalletID: walletID, }, }); setIsLoading(false); } catch (Err) { console.log(Err.message); setIsLoading(false); setPayButtonDisabled(false); triggerHapticFeedback(HapticFeedbackTypes.NotificationError); return presentAlert({ message: Err.message }); } }; const renderWalletSelectionButton = ( {!isLoading && ( navigate('SelectWallet', { onWalletSelect, chainType: Chain.OFFCHAIN })} > {loc.wallets.select_wallet.toLowerCase()} )} navigate('SelectWallet', { onWalletSelect, chainType: Chain.OFFCHAIN })} > {wallet.getLabel()} {formatBalanceWithoutSuffix(wallet.getBalance(), BitcoinUnit.SATS, false)} {BitcoinUnit.SATS} ); const renderGotPayload = () => { return ( {loc.formatString(loc.lndViewInvoice.please_pay_between_and, { min: formatBalance(payload?.min, unit), max: formatBalance(payload?.max, unit), })} {payload?.image && ( <> )} {payload?.description} {payload?.domain} {payButtonDisabled ? :