import AsyncStorage from '@react-native-async-storage/async-storage'; import { RouteProp, useRoute } from '@react-navigation/native'; import React, { useEffect, useState } from 'react'; import { I18nManager, Image, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import { Icon } from '@rneui/themed'; import { btcToSatoshi, fiatToBTC, satoshiToBTC, satoshiToLocalCurrency } from '../../blue_modules/currency'; import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback'; import { BlueCard, BlueDismissKeyboardInputAccessory, BlueLoading, BlueSpacing20, BlueText } from '../../BlueComponents'; import Lnurl from '../../class/lnurl'; import presentAlert from '../../components/Alert'; import AmountInput from '../../components/AmountInput'; import Button from '../../components/Button'; import SafeArea from '../../components/SafeArea'; import { useTheme } from '../../components/themes'; import prompt from '../../helpers/prompt'; import { unlockWithBiometrics, useBiometrics } from '../../hooks/useBiometrics'; import loc, { formatBalance, formatBalanceWithoutSuffix } from '../../loc'; import { BitcoinUnit, Chain } from '../../models/bitcoinUnits'; import { useStorage } from '../../hooks/context/useStorage'; import { useExtendedNavigation } from '../../hooks/useExtendedNavigation'; import { LightningCustodianWallet } from '../../class/wallets/lightning-custodian-wallet'; import { TWallet } from '../../class/wallets/types'; import { pop } from '../../NavigationService'; type RouteParams = { walletID: string; lnurl: string; }; const _cacheFiatToSat: Record = {}; const LnurlPay: React.FC = () => { const { wallets } = useStorage(); const { isBiometricUseCapableAndEnabled } = useBiometrics(); const route = useRoute, string>>(); const { walletID, lnurl } = route.params; const wallet = wallets.find(w => w.getID() === walletID) as LightningCustodianWallet; const [unit, setUnit] = useState(wallet?.getPreferredBalanceUnit() ?? BitcoinUnit.BTC); const [isLoading, setIsLoading] = useState(true); const [_LN, setLN] = useState(); const [payButtonDisabled, setPayButtonDisabled] = useState(true); const [payload, setPayload] = useState(); const { setParams, navigate } = useExtendedNavigation(); 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]); useEffect(() => { setPayButtonDisabled(isLoading); }, [isLoading]); useEffect(() => { if (payload && _LN) { let originalSatAmount: number | false; let newAmount: number | boolean | string = (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] = String(originalSatAmount); break; } setAmount(newAmount.toString()); } }, [payload, _LN, unit]); const onWalletSelect = (w: TWallet) => { setParams({ walletID: w.getID() }); pop(); }; const pay = async () => { setPayButtonDisabled(true); if (!_LN || !amount) return; const isBiometricsEnabled = await isBiometricUseCapableAndEnabled(); if (isBiometricsEnabled) { if (!(await unlockWithBiometrics())) { return; } } let amountSats: number | false; switch (unit) { case BitcoinUnit.SATS: amountSats = parseInt(amount, 10); break; case BitcoinUnit.BTC: amountSats = btcToSatoshi(amount); break; case BitcoinUnit.LOCAL_CURRENCY: if (_cacheFiatToSat[String(amount)]) { amountSats = parseInt(_cacheFiatToSat[amount], 10); } else { amountSats = btcToSatoshi(fiatToBTC(parseFloat(amount))); } break; default: throw new Error('Unknown unit type'); } try { let comment: string | undefined; if (_LN.getCommentAllowed()) { comment = await prompt('Comment', '', false, 'plain-text'); } const bolt11payload = await _LN.requestBolt11FromLnurlPayService(amountSats, comment); // @ts-ignore fixme after lnurl.js converted to ts await wallet.payInvoice(bolt11payload.pr); // @ts-ignore fixme after lnurl.js converted to ts const decoded = wallet.decodeInvoice(bolt11payload.pr); setPayButtonDisabled(false); 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 as Error).message); setIsLoading(false); setPayButtonDisabled(false); triggerHapticFeedback(HapticFeedbackTypes.NotificationError); return presentAlert({ message: (err as Error).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 ? :