BlueWallet/screen/lnd/lndCreateInvoice.js

502 lines
16 KiB
JavaScript
Raw Normal View History

/* global alert */
2020-12-03 02:36:48 +01:00
import React, { useCallback, useContext, useEffect, useRef, useState } from 'react';
2019-08-04 21:34:17 +02:00
import {
ActivityIndicator,
2020-12-25 17:09:53 +01:00
Image,
2020-12-04 14:39:47 +01:00
Keyboard,
2020-12-25 17:09:53 +01:00
KeyboardAvoidingView,
StatusBar,
2020-12-04 14:39:47 +01:00
StyleSheet,
2020-12-25 17:09:53 +01:00
Text,
TextInput,
2021-02-25 02:56:06 +01:00
Platform,
2020-12-25 17:09:53 +01:00
TouchableOpacity,
TouchableWithoutFeedback,
View,
2019-08-04 21:34:17 +02:00
} from 'react-native';
2020-12-25 17:09:53 +01:00
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
import { Icon } from 'react-native-elements';
import { useFocusEffect, useNavigation, useRoute, useTheme } from '@react-navigation/native';
import { BlueAlertWalletExportReminder, BlueButton, BlueDismissKeyboardInputAccessory, BlueLoading } from '../../BlueComponents';
2020-12-25 17:09:53 +01:00
import navigationStyle from '../../components/navigationStyle';
import AmountInput from '../../components/AmountInput';
2020-12-03 02:36:48 +01:00
import * as NavigationService from '../../NavigationService';
import { LightningCustodianWallet } from '../../class/wallets/lightning-custodian-wallet';
import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
2020-07-20 15:38:46 +02:00
import loc, { formatBalanceWithoutSuffix, formatBalancePlain } from '../../loc';
2020-07-23 20:06:13 +02:00
import Lnurl from '../../class/lnurl';
import { BlueStorageContext } from '../../blue_modules/storage-context';
import Notifications from '../../blue_modules/notifications';
2020-06-09 16:08:18 +02:00
const currency = require('../../blue_modules/currency');
2018-12-25 17:34:51 +01:00
2020-12-03 02:36:48 +01:00
const LNDCreateInvoice = () => {
const { wallets, saveToDisk, setSelectedWallet } = useContext(BlueStorageContext);
const { walletID, uri } = useRoute().params;
const wallet = useRef(
wallets.find(item => item.getID() === walletID) || wallets.find(item => item.type === LightningCustodianWallet.type),
);
const { name } = useRoute();
const { colors } = useTheme();
const { navigate, dangerouslyGetParent, goBack, pop, setParams } = useNavigation();
2020-12-11 21:27:59 +01:00
const [unit, setUnit] = useState(wallet.current?.getPreferredBalanceUnit() || BitcoinUnit.BTC);
2020-12-03 02:36:48 +01:00
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.removeListener('keyboardDidShow', _keyboardDidShow);
Keyboard.removeListener('keyboardDidHide', _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) {
2020-12-04 01:23:10 +01:00
const newWallet = wallets.find(w => w.getID() === walletID);
if (newWallet) {
wallet.current = newWallet;
setSelectedWallet(newWallet.getID());
}
2020-12-03 02:36:48 +01:00
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [walletID]);
useFocusEffect(
useCallback(() => {
if (wallet.current) {
setSelectedWallet(walletID);
if (wallet.current.getUserHasSavedExport()) {
renderReceiveDetails();
} else {
BlueAlertWalletExportReminder({
onSuccess: () => renderReceiveDetails(),
onFailure: () => {
dangerouslyGetParent().pop();
NavigationService.navigate('WalletExportRoot', {
screen: 'WalletExport',
params: {
walletID,
},
});
},
});
}
2020-12-11 21:27:59 +01:00
} else {
ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false });
alert(loc.wallets.add_ln_wallet_first);
goBack();
2020-12-03 02:36:48 +01: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); // basically nop
break;
case BitcoinUnit.BTC:
invoiceAmount = currency.btcToSatoshi(invoiceAmount);
break;
case BitcoinUnit.LOCAL_CURRENCY:
// trying to fetch cached sat equivalent for this fiat amount
invoiceAmount = AmountInput.getCachedSatoshis(invoiceAmount) || currency.btcToSatoshi(currency.fiatToBTC(invoiceAmount));
2020-12-03 02:36:48 +01:00
break;
}
const invoiceRequest = await wallet.current.addInvoice(invoiceAmount, description);
ReactNativeHapticFeedback.trigger('notificationSuccess', { ignoreAndroidSystemSettings: false });
// 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,
2020-12-11 21:27:59 +01:00
walletID: wallet.current.getID(),
2020-12-03 02:36:48 +01:00
isModal: true,
});
} catch (Err) {
ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false });
setIsLoading(false);
alert(Err.message);
}
};
const processLnurl = async data => {
setIsLoading(true);
2020-12-11 22:18:10 +01:00
if (!wallet.current) {
2020-12-03 02:36:48 +01:00
ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false });
2020-12-05 06:17:51 +01:00
alert(loc.wallets.no_ln_wallet_error);
2020-12-03 02:36:48 +01:00
return goBack();
}
// decoding the lnurl
const url = Lnurl.getUrlFromLnurl(data);
// 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,
2020-12-11 21:27:59 +01:00
fromWalletID: wallet.current.getID(),
2020-12-03 02:36:48 +01:00
},
});
return;
}
if (reply.tag !== Lnurl.TAG_WITHDRAW_REQUEST) {
throw new Error('Unsupported lnurl');
}
// amount that comes from lnurl is always in sats
let amount = (reply.maxWithdrawable / 1000).toString();
const sats = amount;
switch (unit) {
case BitcoinUnit.SATS:
// nop
break;
case BitcoinUnit.BTC:
amount = currency.satoshiToBTC(amount);
break;
case BitcoinUnit.LOCAL_CURRENCY:
amount = formatBalancePlain(amount, BitcoinUnit.LOCAL_CURRENCY);
AmountInput.setCachedSatoshis(amount, sats);
2020-12-03 02:36:48 +01: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(amount);
setDescription(reply.defaultDescription);
setIsLoading(false);
} catch (Err) {
Keyboard.dismiss();
setIsLoading(false);
ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false });
alert(Err.message);
}
};
const renderCreateButton = () => {
return (
<View style={styles.createButton}>
{isLoading ? (
<ActivityIndicator />
) : (
<BlueButton disabled={!(amount > 0)} onPress={createInvoice} title={loc.send.details_create} />
)}
</View>
);
};
const navigateToScanQRCode = () => {
NavigationService.navigate('ScanQRCodeRoot', {
screen: 'ScanQRCode',
params: {
onBarScanned: processLnurl,
launchedBy: name,
},
});
Keyboard.dismiss();
};
const renderScanClickable = () => {
return (
<TouchableOpacity disabled={isLoading} onPress={navigateToScanQRCode} style={[styles.scanRoot, styleHooks.scanRoot]}>
<Image style={{}} source={require('../../img/scan-white.png')} />
2020-12-04 01:23:10 +01:00
<Text style={[styles.scanClick, styleHooks.scanClick]}>{loc.send.details_scan}</Text>
2020-12-03 02:36:48 +01:00
</TouchableOpacity>
);
};
const navigateToSelectWallet = () => {
navigate('SelectWallet', { onWalletSelect: onWalletSelect, chainType: Chain.OFFCHAIN });
};
const renderWalletSelectionButton = () => {
if (renderWalletSelectionButtonHidden) return;
return (
<View style={styles.walletRoot}>
{!isLoading && (
<TouchableOpacity style={styles.walletChooseWrap} onPress={navigateToSelectWallet}>
<Text style={styles.walletChooseText}>{loc.wallets.select_wallet.toLowerCase()}</Text>
<Icon name="angle-right" size={18} type="font-awesome" color="#9aa0aa" />
</TouchableOpacity>
)}
<View style={styles.walletNameWrap}>
<TouchableOpacity style={styles.walletNameTouch} onPress={navigateToSelectWallet}>
<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 21:27:59 +01:00
if (!wallet.current) {
2020-12-03 02:36:48 +01:00
return (
2020-12-06 01:37:51 +01:00
<View style={[styles.root, styleHooks.root]}>
<StatusBar barStyle="light-content" />
<BlueLoading />
2020-12-03 02:36:48 +01:00
</View>
);
}
return (
<TouchableWithoutFeedback onPress={Keyboard.dismiss} accessible={false}>
<View style={[styles.root, styleHooks.root]}>
<StatusBar barStyle="light-content" />
<View style={[styles.amount, styleHooks.amount]}>
2021-02-25 02:56:06 +01:00
<KeyboardAvoidingView enabled={!Platform.isPad} behavior="position">
<AmountInput
2020-12-03 02:36:48 +01:00
isLoading={isLoading}
amount={amount}
onAmountUnitChange={setUnit}
onChangeText={text => {
if (lnurlParams) {
// in this case we prevent the user from changing the amount to < min or > max
const { min, max } = lnurlParams;
const nextAmount = parseInt(text);
if (nextAmount < min) {
text = min.toString();
} else if (nextAmount > max) {
text = max.toString();
}
}
setAmount(text);
}}
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 09:48:16 +01: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,
},
error: {
flex: 1,
paddingTop: 20,
},
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-03 02:36:48 +01:00
export default LNDCreateInvoice;
2018-12-25 17:34:51 +01:00
2021-02-15 09:03:54 +01:00
LNDCreateInvoice.navigationOptions = navigationStyle(
{
closeButton: true,
headerLeft: null,
},
opts => ({ ...opts, title: loc.receive.header }),
);