BlueWallet/screen/lnd/lndCreateInvoice.js

524 lines
16 KiB
JavaScript
Raw Normal View History

2024-05-31 13:18:01 -04:00
import React, { useCallback, useEffect, useRef, useState } from 'react';
2024-05-20 10:54:13 +01:00
import { useFocusEffect, useNavigation, useRoute } from '@react-navigation/native';
2019-08-04 20:34:17 +01:00
import {
ActivityIndicator,
2024-05-20 10:54:13 +01:00
I18nManager,
2020-12-25 19:09:53 +03:00
Image,
2020-12-04 13:39:47 +00:00
Keyboard,
2020-12-25 19:09:53 +03:00
KeyboardAvoidingView,
2024-05-20 10:54:13 +01:00
Platform,
2020-12-04 13:39:47 +00:00
StyleSheet,
2020-12-25 19:09:53 +03:00
Text,
TextInput,
TouchableOpacity,
TouchableWithoutFeedback,
View,
2019-08-04 20:34:17 +01:00
} from 'react-native';
2024-06-12 12:46:44 -04:00
import { Icon } from '@rneui/themed';
2024-05-20 10:54:13 +01:00
import { parse } from 'url'; // eslint-disable-line n/no-deprecated-api
import { btcToSatoshi, fiatToBTC, satoshiToBTC } from '../../blue_modules/currency';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback';
import Notifications from '../../blue_modules/notifications';
2024-03-24 16:46:56 -04:00
import { BlueDismissKeyboardInputAccessory, BlueLoading } from '../../BlueComponents';
2020-07-23 19:06:13 +01:00
import Lnurl from '../../class/lnurl';
import presentAlert from '../../components/Alert';
2024-05-20 10:54:13 +01:00
import AmountInput from '../../components/AmountInput';
2023-11-15 04:40:22 -04:00
import Button from '../../components/Button';
2024-05-20 10:54:13 +01:00
import { useTheme } from '../../components/themes';
2024-03-24 16:46:56 -04:00
import { presentWalletExportReminder } from '../../helpers/presentWalletExportReminder';
2024-05-20 10:54:13 +01:00
import { requestCameraAuthorization } from '../../helpers/scan-qr';
import loc, { formatBalance, formatBalancePlain, formatBalanceWithoutSuffix } from '../../loc';
import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
import * as NavigationService from '../../NavigationService';
2024-05-31 13:18:01 -04:00
import { useStorage } from '../../hooks/context/useStorage';
2018-12-25 11:34:51 -05:00
2020-12-02 20:36:48 -05:00
const LNDCreateInvoice = () => {
const { wallets, saveToDisk, setSelectedWalletID } = useStorage();
2020-12-02 20:36:48 -05:00
const { walletID, uri } = useRoute().params;
2021-09-09 12:00:11 +01:00
const wallet = useRef(wallets.find(item => item.getID() === walletID) || wallets.find(item => item.chain === Chain.OFFCHAIN));
2024-03-27 00:15:28 -04:00
const createInvoiceRef = useRef();
2020-12-02 20:36:48 -05:00
const { name } = useRoute();
const { colors } = useTheme();
2023-11-11 07:33:50 -04:00
const { navigate, getParent, goBack, pop, setParams } = useNavigation();
2020-12-11 20:27:59 +00:00
const [unit, setUnit] = useState(wallet.current?.getPreferredBalanceUnit() || BitcoinUnit.BTC);
2020-12-02 20:36:48 -05:00
const [amount, setAmount] = useState();
const [renderWalletSelectionButtonHidden, setRenderWalletSelectionButtonHidden] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [description, setDescription] = useState('');
const [lnurlParams, setLNURLParams] = useState();
2022-02-14 20:51:38 +03:00
2020-12-02 20:36:48 -05:00
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,
},
});
2020-12-02 20:36:48 -05:00
useEffect(() => {
const showSubscription = Keyboard.addListener(Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow', _keyboardDidShow,
);
const hideSubscription = Keyboard.addListener(Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide', _keyboardDidHide,
);
2020-12-02 20:36:48 -05:00
return () => {
showSubscription.remove();
hideSubscription.remove();
2020-12-02 20:36:48 -05:00
};
}, []);
2020-12-02 20:36:48 -05:00
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) {
2020-12-03 19:23:10 -05:00
const newWallet = wallets.find(w => w.getID() === walletID);
if (newWallet) {
wallet.current = newWallet;
setSelectedWalletID(newWallet.getID());
2020-12-03 19:23:10 -05:00
}
2020-12-02 20:36:48 -05:00
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [walletID]);
useFocusEffect(
useCallback(() => {
if (wallet.current) {
setSelectedWalletID(walletID);
2020-12-02 20:36:48 -05:00
if (wallet.current.getUserHasSavedExport()) {
renderReceiveDetails();
} else {
2024-03-24 16:46:56 -04:00
presentWalletExportReminder()
.then(() => {
renderReceiveDetails();
})
.catch(() => {
2023-11-11 07:33:50 -04:00
getParent().pop();
2020-12-02 20:36:48 -05:00
NavigationService.navigate('WalletExportRoot', {
screen: 'WalletExport',
params: {
walletID,
},
});
2024-03-24 16:46:56 -04:00
});
2020-12-02 20:36:48 -05:00
}
2020-12-11 20:27:59 +00:00
} else {
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
presentAlert({ message: loc.wallets.add_ln_wallet_first });
2020-12-11 20:27:59 +00:00
goBack();
2020-12-02 20:36:48 -05:00
}
// 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
2020-12-02 20:36:48 -05:00
break;
case BitcoinUnit.BTC:
2024-01-28 11:11:08 -04:00
invoiceAmount = btcToSatoshi(invoiceAmount);
2020-12-02 20:36:48 -05:00
break;
case BitcoinUnit.LOCAL_CURRENCY:
// trying to fetch cached sat equivalent for this fiat amount
2024-01-28 11:11:08 -04:00
invoiceAmount = AmountInput.getCachedSatoshis(invoiceAmount) || btcToSatoshi(fiatToBTC(invoiceAmount));
2020-12-02 20:36:48 -05:00
break;
}
2022-02-14 20:51:38 +03:00
if (lnurlParams) {
const { min, max } = lnurlParams;
if (invoiceAmount < min || invoiceAmount > max) {
2022-02-14 20:51:38 +03:00
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 });
2022-02-14 20:51:38 +03:00
setIsLoading(false);
return;
}
}
2020-12-02 20:36:48 -05:00
const invoiceRequest = await wallet.current.addInvoice(invoiceAmount, description);
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
2020-12-02 20:36:48 -05:00
// 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(createInvoiceRef);
2020-12-02 20:36:48 -05:00
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;
2023-12-15 11:56:41 -04:00
const resp = await fetch(callbackUrl, { method: 'GET' });
if (resp.status >= 300) {
const text = await resp.text();
throw new Error(text);
2020-12-02 20:36:48 -05:00
}
2023-12-15 11:56:41 -04:00
const reply = await resp.json();
2020-12-02 20:36:48 -05:00
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,
2020-12-11 20:27:59 +00:00
walletID: wallet.current.getID(),
2020-12-02 20:36:48 -05:00
});
} catch (Err) {
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
2020-12-02 20:36:48 -05:00
setIsLoading(false);
presentAlert({ message: Err.message });
2020-12-02 20:36:48 -05:00
}
};
const processLnurl = async data => {
setIsLoading(true);
2020-12-11 16:18:10 -05:00
if (!wallet.current) {
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
presentAlert({ message: loc.wallets.no_ln_wallet_error });
2020-12-02 20:36:48 -05:00
return goBack();
}
// decoding the lnurl
const url = Lnurl.getUrlFromLnurl(data);
2022-10-31 12:25:26 +00:00
const { query } = parse(url, true);
2022-02-11 14:18:56 +00:00
if (query.tag === Lnurl.TAG_LOGIN_REQUEST) {
navigate('LnurlAuth', {
lnurl: data,
walletID: walletID ?? wallet.current.getID(),
});
return;
}
2020-12-02 20:36:48 -05:00
// calling the url
try {
2023-12-15 11:56:41 -04:00
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);
2020-12-02 20:36:48 -05:00
}
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,
2021-08-25 01:55:22 -04:00
walletID: walletID ?? wallet.current.getID(),
2020-12-02 20:36:48 -05:00
},
});
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;
2020-12-02 20:36:48 -05:00
switch (unit) {
case BitcoinUnit.SATS:
// nop
break;
case BitcoinUnit.BTC:
2024-01-28 11:11:08 -04:00
newAmount = satoshiToBTC(newAmount);
2020-12-02 20:36:48 -05:00
break;
case BitcoinUnit.LOCAL_CURRENCY:
newAmount = formatBalancePlain(newAmount, BitcoinUnit.LOCAL_CURRENCY);
AmountInput.setCachedSatoshis(newAmount, sats);
2020-12-02 20:36:48 -05:00
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);
2020-12-02 20:36:48 -05:00
setDescription(reply.defaultDescription);
setIsLoading(false);
} catch (Err) {
Keyboard.dismiss();
setIsLoading(false);
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
presentAlert({ message: Err.message });
2020-12-02 20:36:48 -05:00
}
};
const renderCreateButton = () => {
return (
<View style={styles.createButton}>
2024-03-27 00:15:28 -04:00
{isLoading ? (
<ActivityIndicator />
) : (
<Button disabled={!(amount > 0)} ref={createInvoiceRef} onPress={createInvoice} title={loc.send.details_create} />
)}
2020-12-02 20:36:48 -05:00
</View>
);
};
const navigateToScanQRCode = () => {
2023-10-17 09:35:10 -04:00
requestCameraAuthorization().then(() => {
2023-10-17 09:39:32 -04:00
NavigationService.navigate('ScanQRCodeRoot', {
screen: 'ScanQRCode',
params: {
onBarScanned: processLnurl,
launchedBy: name,
},
});
Keyboard.dismiss();
2023-10-17 09:35:10 -04:00
});
2023-10-17 09:39:32 -04:00
};
2020-12-02 20:36:48 -05:00
const renderScanClickable = () => {
return (
<TouchableOpacity
disabled={isLoading}
onPress={navigateToScanQRCode}
style={[styles.scanRoot, styleHooks.scanRoot]}
accessibilityRole="button"
accessibilityLabel={loc.send.details_scan}
accessibilityHint={loc.send.details_scan_hint}
>
2020-12-02 20:36:48 -05:00
<Image style={{}} source={require('../../img/scan-white.png')} />
2020-12-03 19:23:10 -05:00
<Text style={[styles.scanClick, styleHooks.scanClick]}>{loc.send.details_scan}</Text>
2020-12-02 20:36:48 -05:00
</TouchableOpacity>
);
};
const navigateToSelectWallet = () => {
2022-10-31 12:25:26 +00:00
navigate('SelectWallet', { onWalletSelect, chainType: Chain.OFFCHAIN });
2020-12-02 20:36:48 -05:00
};
const renderWalletSelectionButton = () => {
if (renderWalletSelectionButtonHidden) return;
return (
<View style={styles.walletRoot}>
{!isLoading && (
<TouchableOpacity accessibilityRole="button" style={styles.walletChooseWrap} onPress={navigateToSelectWallet}>
2020-12-02 20:36:48 -05:00
<Text style={styles.walletChooseText}>{loc.wallets.select_wallet.toLowerCase()}</Text>
2021-03-18 22:30:01 -04:00
<Icon name={I18nManager.isRTL ? 'angle-left' : 'angle-right'} size={18} type="font-awesome" color="#9aa0aa" />
2020-12-02 20:36:48 -05:00
</TouchableOpacity>
)}
<View style={styles.walletNameWrap}>
<TouchableOpacity accessibilityRole="button" style={styles.walletNameTouch} onPress={navigateToSelectWallet}>
2020-12-02 20:36:48 -05:00
<Text style={[styles.walletNameText, styleHooks.walletNameText]}>{wallet.current.getLabel()}</Text>
<Text style={[styles.walletNameBalance, styleHooks.walletNameBalance]}>
{formatBalanceWithoutSuffix(wallet.current.getBalance(), BitcoinUnit.SATS, false)}
</Text>
<Text style={[styles.walletNameSats, styleHooks.walletNameSats]}>{BitcoinUnit.SATS}</Text>
</TouchableOpacity>
</View>
</View>
);
};
const onWalletSelect = selectedWallet => {
setParams({ walletID: selectedWallet.getID() });
pop();
};
2020-12-11 20:27:59 +00:00
if (!wallet.current) {
2020-12-02 20:36:48 -05:00
return (
2020-12-05 19:37:51 -05:00
<View style={[styles.root, styleHooks.root]}>
<BlueLoading />
2020-12-02 20:36:48 -05:00
</View>
);
}
return (
<TouchableWithoutFeedback onPress={Keyboard.dismiss} accessible={false}>
<View style={[styles.root, styleHooks.root]}>
<View style={[styles.amount, styleHooks.amount]}>
2021-02-24 20:56:06 -05:00
<KeyboardAvoidingView enabled={!Platform.isPad} behavior="position">
<AmountInput
2020-12-02 20:36:48 -05:00
isLoading={isLoading}
amount={amount}
onAmountUnitChange={setUnit}
2022-02-14 20:51:38 +03:00
onChangeText={setAmount}
2020-12-02 20:36:48 -05:00
disabled={isLoading || (lnurlParams && lnurlParams.fixed)}
unit={unit}
inputAccessoryViewID={BlueDismissKeyboardInputAccessory.InputAccessoryViewID}
/>
<View style={[styles.fiat, styleHooks.fiat]}>
<TextInput
onChangeText={setDescription}
placeholder={loc.receive.details_label}
value={description}
numberOfLines={1}
placeholderTextColor="#81868e"
style={styles.fiat2}
editable={!isLoading}
onSubmitEditing={Keyboard.dismiss}
inputAccessoryViewID={BlueDismissKeyboardInputAccessory.InputAccessoryViewID}
/>
{lnurlParams ? null : renderScanClickable()}
</View>
<BlueDismissKeyboardInputAccessory />
{renderCreateButton()}
</KeyboardAvoidingView>
</View>
{renderWalletSelectionButton()}
</View>
</TouchableWithoutFeedback>
);
};
const styles = StyleSheet.create({
createButton: {
2020-11-22 03:48:16 -05:00
marginHorizontal: 16,
marginVertical: 16,
minHeight: 45,
},
scanRoot: {
height: 36,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
borderRadius: 4,
paddingVertical: 4,
paddingHorizontal: 8,
marginHorizontal: 4,
},
scanClick: {
marginLeft: 4,
},
walletRoot: {
marginBottom: 16,
alignItems: 'center',
justifyContent: 'center',
},
walletChooseWrap: {
flexDirection: 'row',
alignItems: 'center',
},
walletChooseText: {
color: '#9aa0aa',
fontSize: 14,
marginRight: 8,
},
walletNameWrap: {
flexDirection: 'row',
alignItems: 'center',
marginVertical: 4,
},
walletNameTouch: {
flexDirection: 'row',
alignItems: 'center',
},
walletNameText: {
fontSize: 14,
},
walletNameBalance: {
fontSize: 14,
fontWeight: '600',
marginLeft: 8,
marginRight: 4,
},
walletNameSats: {
fontSize: 11,
fontWeight: '600',
textAlignVertical: 'bottom',
marginTop: 2,
},
root: {
flex: 1,
justifyContent: 'space-between',
},
amount: {
flex: 1,
},
fiat: {
flexDirection: 'row',
borderWidth: 1.0,
borderBottomWidth: 0.5,
minHeight: 44,
height: 44,
marginHorizontal: 20,
alignItems: 'center',
marginVertical: 8,
borderRadius: 4,
},
fiat2: {
flex: 1,
marginHorizontal: 8,
minHeight: 33,
color: '#81868e',
},
});
2020-12-02 20:36:48 -05:00
export default LNDCreateInvoice;
LNDCreateInvoice.routeName = 'LNDCreateInvoice';