BlueWallet/screen/lnd/scanLndInvoice.js

444 lines
14 KiB
JavaScript
Raw Normal View History

2024-05-20 11:54:13 +02:00
import { useFocusEffect, useNavigation, useRoute } from '@react-navigation/native';
2024-05-18 00:34:39 +02:00
import React, { useCallback, useEffect, useState } from 'react';
import {
ActivityIndicator,
2024-05-20 11:54:13 +02:00
I18nManager,
Keyboard,
2024-05-20 11:54:13 +02:00
KeyboardAvoidingView,
Platform,
ScrollView,
StyleSheet,
2024-05-20 11:54:13 +02:00
Text,
TouchableOpacity,
View,
} from 'react-native';
2024-06-12 18:46:44 +02:00
import { Icon } from '@rneui/themed';
2024-05-20 11:54:13 +02:00
import { btcToSatoshi, fiatToBTC } from '../../blue_modules/currency';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback';
import { BlueCard, BlueDismissKeyboardInputAccessory, BlueLoading } from '../../BlueComponents';
2020-07-23 20:06:13 +02:00
import Lnurl from '../../class/lnurl';
2024-05-20 11:54:13 +02:00
import AddressInput from '../../components/AddressInput';
import presentAlert from '../../components/Alert';
2024-05-20 11:54:13 +02:00
import AmountInput from '../../components/AmountInput';
2023-11-15 09:40:22 +01:00
import Button from '../../components/Button';
import SafeArea from '../../components/SafeArea';
2024-05-20 11:54:13 +02:00
import { useTheme } from '../../components/themes';
2024-06-04 03:54:32 +02:00
import { useBiometrics, unlockWithBiometrics } from '../../hooks/useBiometrics';
2024-05-20 11:54:13 +02:00
import loc, { formatBalanceWithoutSuffix } from '../../loc';
import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
2024-05-31 19:22:22 +02:00
import { useStorage } from '../../hooks/context/useStorage';
2020-12-05 06:17:51 +01:00
const ScanLndInvoice = () => {
2024-05-18 00:34:39 +02:00
const { wallets, fetchAndSaveWalletTransactions } = useStorage();
2024-06-04 03:54:32 +02:00
const { isBiometricUseCapableAndEnabled } = useBiometrics();
2020-12-05 06:17:51 +01:00
const { colors } = useTheme();
const { walletID, uri, invoice } = useRoute().params;
const name = useRoute().name;
/** @type {LightningCustodianWallet} */
2020-12-07 16:58:28 +01:00
const [wallet, setWallet] = useState(
2021-09-09 13:00:11 +02:00
wallets.find(item => item.getID() === walletID) || wallets.find(item => item.chain === Chain.OFFCHAIN),
2020-12-05 06:28:36 +01:00
);
2020-12-11 04:56:01 +01:00
const { navigate, setParams, goBack, pop } = useNavigation();
2020-12-05 06:17:51 +01:00
const [isLoading, setIsLoading] = useState(false);
const [renderWalletSelectionButtonHidden, setRenderWalletSelectionButtonHidden] = useState(false);
const [destination, setDestination] = useState('');
2020-12-18 17:08:02 +01:00
const [unit, setUnit] = useState(BitcoinUnit.SATS);
2020-12-05 06:17:51 +01:00
const [decoded, setDecoded] = useState();
const [amount, setAmount] = useState();
const [isAmountInitiallyEmpty, setIsAmountInitiallyEmpty] = useState();
const [expiresIn, setExpiresIn] = useState();
const stylesHook = StyleSheet.create({
walletWrapLabel: {
color: colors.buttonAlternativeTextColor,
},
walletWrapBalance: {
color: colors.buttonAlternativeTextColor,
},
walletWrapSats: {
color: colors.buttonAlternativeTextColor,
},
root: {
backgroundColor: colors.elevated,
},
});
useEffect(() => {
2024-07-07 02:21:29 +02:00
const showSubscription = Keyboard.addListener(Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow', _keyboardDidShow);
const hideSubscription = Keyboard.addListener(Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide', _keyboardDidHide);
2020-12-05 06:17:51 +01:00
return () => {
showSubscription.remove();
hideSubscription.remove();
2020-12-05 06:17:51 +01:00
};
}, []);
2020-12-05 06:28:36 +01:00
useEffect(() => {
2020-12-11 04:56:01 +01:00
if (walletID && wallet?.getID() !== walletID) {
2020-12-07 16:58:28 +01:00
setWallet(wallets.find(w => w.getID() === walletID));
2020-12-05 06:28:36 +01:00
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [walletID]);
2020-12-11 04:56:01 +01:00
useFocusEffect(
useCallback(() => {
if (!wallet) {
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
2020-12-11 04:56:01 +01:00
goBack();
setTimeout(() => presentAlert({ message: loc.wallets.no_ln_wallet_error }), 500);
2020-12-11 04:56:01 +01:00
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [wallet]),
);
2020-12-05 06:17:51 +01:00
useEffect(() => {
2020-12-11 04:56:01 +01:00
if (wallet && uri) {
if (Lnurl.isLnurl(uri)) return processLnurlPay(uri);
if (Lnurl.isLightningAddress(uri)) return processLnurlPay(uri);
2020-12-05 06:17:51 +01:00
let data = uri;
// handling BIP21 w/BOLT11 support
const ind = data.indexOf('lightning=');
if (ind !== -1) {
data = data.substring(ind + 10).split('&')[0];
}
2019-01-06 21:21:04 +01:00
data = data.replace('LIGHTNING:', '').replace('lightning:', '');
console.log(data);
let newDecoded;
2019-01-06 21:21:04 +01:00
try {
newDecoded = wallet.decodeInvoice(data);
let newExpiresIn = (newDecoded.timestamp * 1 + newDecoded.expiry * 1) * 1000; // ms
if (+new Date() > newExpiresIn) {
newExpiresIn = loc.lnd.expired;
2019-01-06 21:21:04 +01:00
} else {
const time = Math.round((newExpiresIn - +new Date()) / (60 * 1000));
newExpiresIn = loc.formatString(loc.lnd.expiresIn, { time });
2019-01-06 21:21:04 +01:00
}
Keyboard.dismiss();
2020-12-05 06:17:51 +01:00
setParams({ uri: undefined, invoice: data });
setIsAmountInitiallyEmpty(newDecoded.num_satoshis === '0');
2020-12-05 06:17:51 +01:00
setDestination(data);
setIsLoading(false);
setAmount(newDecoded.num_satoshis);
setExpiresIn(newExpiresIn);
setDecoded(newDecoded);
2019-01-06 21:21:04 +01:00
} catch (Err) {
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
2020-01-20 04:33:17 +01:00
Keyboard.dismiss();
2020-12-05 06:17:51 +01:00
setParams({ uri: undefined });
setTimeout(() => presentAlert({ message: Err.message }), 10);
2020-12-05 06:17:51 +01:00
setIsLoading(false);
2021-09-04 21:04:58 +02:00
setAmount();
setDestination();
setExpiresIn();
setDecoded();
}
2020-01-20 04:33:17 +01:00
}
2020-12-05 06:17:51 +01:00
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [uri]);
2020-01-20 04:33:17 +01:00
2020-12-05 06:17:51 +01:00
const _keyboardDidShow = () => {
setRenderWalletSelectionButtonHidden(true);
2020-01-20 04:33:17 +01:00
};
2020-12-05 06:17:51 +01:00
const _keyboardDidHide = () => {
setRenderWalletSelectionButtonHidden(false);
2020-01-20 04:33:17 +01:00
};
2020-12-05 06:17:51 +01:00
const processInvoice = data => {
if (Lnurl.isLnurl(data)) return processLnurlPay(data);
if (Lnurl.isLightningAddress(data)) return processLnurlPay(data);
2020-12-05 06:17:51 +01:00
setParams({ uri: data });
2019-01-24 08:36:01 +01:00
};
2020-12-05 06:17:51 +01:00
const processLnurlPay = data => {
navigate('ScanLndInvoiceRoot', {
2020-07-23 20:06:13 +02:00
screen: 'LnurlPay',
params: {
lnurl: data,
2021-08-25 07:55:22 +02:00
walletID: walletID || wallet.getID(),
2020-07-23 20:06:13 +02:00
},
});
};
2020-12-05 06:17:51 +01:00
const pay = async () => {
if (!decoded) {
return null;
}
2024-05-18 00:34:39 +02:00
const isBiometricsEnabled = await isBiometricUseCapableAndEnabled();
if (isBiometricsEnabled) {
2024-05-18 00:34:39 +02:00
if (!(await unlockWithBiometrics())) {
return;
}
}
2020-12-05 06:17:51 +01:00
let amountSats = amount;
switch (unit) {
2020-06-09 16:08:18 +02:00
case BitcoinUnit.SATS:
amountSats = parseInt(amountSats, 10); // nop
2020-06-09 16:08:18 +02:00
break;
case BitcoinUnit.BTC:
2024-01-28 16:11:08 +01:00
amountSats = btcToSatoshi(amountSats);
2020-06-09 16:08:18 +02:00
break;
case BitcoinUnit.LOCAL_CURRENCY:
2024-01-28 16:11:08 +01:00
amountSats = btcToSatoshi(fiatToBTC(amountSats));
2020-06-09 16:08:18 +02:00
break;
}
2020-12-05 06:17:51 +01:00
setIsLoading(true);
2020-06-09 16:08:18 +02:00
const newExpiresIn = (decoded.timestamp * 1 + decoded.expiry * 1) * 1000; // ms
if (+new Date() > newExpiresIn) {
2020-12-05 06:17:51 +01:00
setIsLoading(false);
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
return presentAlert({ message: loc.lnd.errorInvoiceExpired });
2020-12-05 06:17:51 +01:00
}
const currentUserInvoices = wallet.user_invoices_raw; // not fetching invoices, as we assume they were loaded previously
if (currentUserInvoices.some(i => i.payment_hash === decoded.payment_hash)) {
2020-12-05 06:17:51 +01:00
setIsLoading(false);
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
return presentAlert({ message: loc.lnd.sameWalletAsInvoiceError });
2020-12-05 06:17:51 +01:00
}
2020-12-05 06:17:51 +01:00
try {
2020-12-07 16:58:28 +01:00
await wallet.payInvoice(invoice, amountSats);
2020-12-05 06:17:51 +01:00
} catch (Err) {
console.log(Err.message);
setIsLoading(false);
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
return presentAlert({ message: Err.message });
2020-12-05 06:17:51 +01:00
}
2020-12-05 06:17:51 +01:00
navigate('Success', {
2020-12-18 17:08:02 +01:00
amount: amountSats,
2020-12-05 06:17:51 +01:00
amountUnit: BitcoinUnit.SATS,
invoiceDescription: decoded.description,
});
fetchAndSaveWalletTransactions(wallet.getID());
2020-12-05 06:17:51 +01:00
};
2020-12-05 06:17:51 +01:00
const processTextForInvoice = text => {
if (
text.toLowerCase().startsWith('lnb') ||
text.toLowerCase().startsWith('lightning:lnb') ||
Lnurl.isLnurl(text) ||
Lnurl.isLightningAddress(text)
) {
2020-12-05 06:17:51 +01:00
processInvoice(text);
2018-12-24 16:29:33 +01:00
} else {
2020-12-05 06:17:51 +01:00
setDecoded(undefined);
setExpiresIn(undefined);
setDestination(text);
2018-12-24 16:29:33 +01:00
}
};
2020-12-05 06:17:51 +01:00
const shouldDisablePayButton = () => {
if (!decoded) {
return true;
} else {
2020-12-05 06:17:51 +01:00
if (!amount) {
return true;
}
}
2020-12-05 06:17:51 +01:00
return !(amount > 0);
// return decoded.num_satoshis <= 0 || isLoading || isNaN(decoded.num_satoshis);
};
2020-12-07 16:12:49 +01:00
const naviageToSelectWallet = () => {
navigate('SelectWallet', { onWalletSelect, chainType: Chain.OFFCHAIN });
};
2020-12-05 06:17:51 +01:00
const renderWalletSelectionButton = () => {
if (renderWalletSelectionButtonHidden) return;
2020-12-07 16:58:28 +01:00
const walletLabel = wallet.getLabel();
return (
<View style={styles.walletSelectRoot}>
2020-12-05 06:17:51 +01:00
{!isLoading && (
<TouchableOpacity accessibilityRole="button" style={styles.walletSelectTouch} onPress={naviageToSelectWallet}>
<Text style={styles.walletSelectText}>{loc.wallets.select_wallet.toLowerCase()}</Text>
2021-03-19 16:12:17 +01:00
<Icon name={I18nManager.isRTL ? 'angle-left' : 'angle-right'} size={18} type="font-awesome" color="#9aa0aa" />
</TouchableOpacity>
)}
<View style={styles.walletWrap}>
<TouchableOpacity accessibilityRole="button" disabled={isLoading} style={styles.walletWrapTouch} onPress={naviageToSelectWallet}>
2020-12-07 16:58:28 +01:00
<Text style={[styles.walletWrapLabel, stylesHook.walletWrapLabel]}>{walletLabel}</Text>
2020-12-05 06:17:51 +01:00
<Text style={[styles.walletWrapBalance, stylesHook.walletWrapBalance]}>
2020-12-07 16:58:28 +01:00
{formatBalanceWithoutSuffix(wallet.getBalance(), BitcoinUnit.SATS, false)}
</Text>
2020-12-05 06:17:51 +01:00
<Text style={[styles.walletWrapSats, stylesHook.walletWrapSats]}>{BitcoinUnit.SATS}</Text>
</TouchableOpacity>
</View>
</View>
);
};
2020-12-05 06:17:51 +01:00
const getFees = () => {
const min = Math.floor(decoded.num_satoshis * 0.003);
const max = Math.floor(decoded.num_satoshis * 0.01) + 1;
2021-09-02 22:22:43 +02:00
return `${min} ${BitcoinUnit.SATS} - ${max} ${BitcoinUnit.SATS}`;
2020-12-05 06:17:51 +01:00
};
2020-06-04 15:07:33 +02:00
const onBlur = () => {
processTextForInvoice(destination);
};
2020-12-05 06:17:51 +01:00
const onWalletSelect = selectedWallet => {
setParams({ walletID: selectedWallet.getID() });
pop();
};
2020-12-07 17:06:45 +01:00
if (wallet === undefined || !wallet) {
2020-12-11 04:56:01 +01:00
return (
<View style={[styles.loadingIndicator, stylesHook.root]}>
<BlueLoading />
</View>
);
2020-06-09 16:08:18 +02:00
}
2020-12-05 06:17:51 +01:00
return (
<SafeArea style={stylesHook.root}>
2020-12-05 06:17:51 +01:00
<View style={[styles.root, stylesHook.root]}>
2021-05-08 01:09:24 +02:00
<ScrollView contentContainerStyle={styles.scroll} keyboardShouldPersistTaps="handled">
2020-12-05 06:17:51 +01:00
<KeyboardAvoidingView enabled behavior="position" keyboardVerticalOffset={20}>
<View style={styles.scrollMargin}>
<AmountInput
2020-12-05 06:17:51 +01:00
pointerEvents={isAmountInitiallyEmpty ? 'auto' : 'none'}
isLoading={isLoading}
amount={amount}
onAmountUnitChange={setUnit}
onChangeText={setAmount}
2020-12-07 16:15:33 +01:00
disabled={!decoded || isLoading || decoded.num_satoshis > 0}
2021-05-08 01:09:24 +02:00
unit={unit}
2020-12-05 06:17:51 +01:00
inputAccessoryViewID={BlueDismissKeyboardInputAccessory.InputAccessoryViewID}
/>
</View>
<BlueCard>
<AddressInput
2020-12-05 06:17:51 +01:00
onChangeText={text => {
text = text.trim();
setDestination(text);
2020-12-05 06:17:51 +01:00
}}
2024-07-05 01:52:00 +02:00
onBarScanned={data => processTextForInvoice(data.data)}
2020-12-05 06:17:51 +01:00
address={destination}
isLoading={isLoading}
placeholder={loc.lnd.placeholder}
inputAccessoryViewID={BlueDismissKeyboardInputAccessory.InputAccessoryViewID}
launchedBy={name}
onBlur={onBlur}
2021-10-15 16:52:25 +02:00
keyboardType="email-address"
2020-12-05 06:17:51 +01:00
/>
<View style={styles.description}>
<Text numberOfLines={0} style={styles.descriptionText}>
{decoded !== undefined ? decoded.description : ''}
</Text>
</View>
2020-12-05 06:17:51 +01:00
{expiresIn !== undefined && (
<View>
2021-09-06 10:28:16 +02:00
<Text style={styles.expiresIn}>{expiresIn}</Text>
2020-12-05 06:17:51 +01:00
{decoded && decoded.num_satoshis > 0 && (
<Text style={styles.expiresIn}>{loc.formatString(loc.lnd.potentialFee, { fee: getFees() })}</Text>
)}
</View>
2020-12-05 06:17:51 +01:00
)}
<BlueCard>
{isLoading ? (
<View>
<ActivityIndicator />
</View>
) : (
2020-06-03 22:40:16 +02:00
<View>
2023-11-15 09:40:22 +01:00
<Button title={loc.lnd.payButton} onPress={pay} disabled={shouldDisablePayButton()} />
2020-06-04 15:07:33 +02:00
</View>
)}
</BlueCard>
2020-12-05 06:17:51 +01:00
</BlueCard>
</KeyboardAvoidingView>
{renderWalletSelectionButton()}
</ScrollView>
</View>
<BlueDismissKeyboardInputAccessory />
</SafeArea>
2020-12-05 06:17:51 +01:00
);
2020-04-17 17:23:18 +02:00
};
2020-07-15 19:32:59 +02:00
2020-12-05 06:17:51 +01:00
export default ScanLndInvoice;
2024-01-12 14:10:48 +01:00
2020-12-05 06:17:51 +01:00
const styles = StyleSheet.create({
walletSelectRoot: {
marginBottom: 16,
alignItems: 'center',
justifyContent: 'flex-end',
},
2020-12-11 04:56:01 +01:00
loadingIndicator: {
flex: 1,
justifyContent: 'center',
},
2020-12-05 06:17:51 +01:00
walletSelectTouch: {
flexDirection: 'row',
alignItems: 'center',
},
walletSelectText: {
color: '#9aa0aa',
fontSize: 14,
marginRight: 8,
},
walletWrap: {
flexDirection: 'row',
alignItems: 'center',
marginVertical: 4,
},
walletWrapTouch: {
flexDirection: 'row',
alignItems: 'center',
},
walletWrapLabel: {
fontSize: 14,
},
walletWrapBalance: {
fontSize: 14,
fontWeight: '600',
marginLeft: 4,
marginRight: 4,
},
walletWrapSats: {
fontSize: 11,
fontWeight: '600',
textAlignVertical: 'bottom',
marginTop: 2,
},
root: {
flex: 1,
},
scroll: {
flex: 1,
justifyContent: 'space-between',
},
scrollMargin: {
marginTop: 60,
},
description: {
flexDirection: 'row',
marginHorizontal: 20,
alignItems: 'center',
marginVertical: 0,
borderRadius: 4,
},
descriptionText: {
color: '#81868e',
fontWeight: '500',
fontSize: 14,
},
expiresIn: {
2021-08-26 20:00:23 +02:00
writingDirection: I18nManager.isRTL ? 'rtl' : 'ltr',
2020-12-05 06:17:51 +01:00
color: '#81868e',
fontSize: 12,
left: 20,
top: 10,
},
});