BlueWallet/screen/lnd/lnurlPay.tsx

298 lines
9.9 KiB
TypeScript
Raw Normal View History

2020-12-15 22:15:57 -05:00
import AsyncStorage from '@react-native-async-storage/async-storage';
2024-09-08 13:30:00 +01:00
import { RouteProp, useRoute } from '@react-navigation/native';
2024-05-20 10:54:13 +01:00
import React, { useEffect, useState } from 'react';
import { I18nManager, Image, ScrollView, StyleSheet, Text, TouchableOpacity, View } 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 { btcToSatoshi, fiatToBTC, satoshiToBTC, satoshiToLocalCurrency } from '../../blue_modules/currency';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback';
import { BlueCard, BlueDismissKeyboardInputAccessory, BlueLoading, BlueSpacing20, BlueText } 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';
import SafeArea from '../../components/SafeArea';
2024-05-20 10:54:13 +01:00
import { useTheme } from '../../components/themes';
import prompt from '../../helpers/prompt';
2024-09-08 13:30:00 +01:00
import { unlockWithBiometrics, useBiometrics } from '../../hooks/useBiometrics';
2024-05-20 10:54:13 +01:00
import loc, { formatBalance, formatBalanceWithoutSuffix } from '../../loc';
import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
2024-05-31 13:22:22 -04:00
import { useStorage } from '../../hooks/context/useStorage';
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
2024-09-08 13:30:00 +01:00
import { LightningCustodianWallet } from '../../class/wallets/lightning-custodian-wallet';
import { TWallet } from '../../class/wallets/types';
import { pop } from '../../NavigationService';
2020-07-23 19:06:13 +01:00
2024-09-08 13:30:00 +01:00
type RouteParams = {
walletID: string;
lnurl: string;
};
const _cacheFiatToSat: Record<string, string> = {};
2024-09-08 13:30:00 +01:00
const LnurlPay: React.FC = () => {
2024-05-17 18:34:39 -04:00
const { wallets } = useStorage();
2024-06-03 21:54:32 -04:00
const { isBiometricUseCapableAndEnabled } = useBiometrics();
2024-09-08 13:30:00 +01:00
const route = useRoute<RouteProp<Record<string, RouteParams>, string>>();
const { walletID, lnurl } = route.params;
const wallet = wallets.find(w => w.getID() === walletID) as LightningCustodianWallet;
const [unit, setUnit] = useState<BitcoinUnit>(wallet.getPreferredBalanceUnit());
const [isLoading, setIsLoading] = useState<boolean>(true);
const [_LN, setLN] = useState<Lnurl | undefined>();
const [payButtonDisabled, setPayButtonDisabled] = useState<boolean>(true);
const [payload, setPayload] = useState<any>();
const { setParams, navigate } = useExtendedNavigation();
const [amount, setAmount] = useState<string | undefined>();
2021-08-25 01:55:22 -04:00
const { colors } = useTheme();
const stylesHook = StyleSheet.create({
root: {
backgroundColor: colors.background,
},
walletWrapLabel: {
color: colors.buttonAlternativeTextColor,
},
walletWrapBalance: {
color: colors.buttonAlternativeTextColor,
},
walletWrapSats: {
color: colors.buttonAlternativeTextColor,
},
});
2020-07-23 19:06:13 +01:00
2021-08-25 01:55:22 -04:00
useEffect(() => {
if (lnurl) {
const ln = new Lnurl(lnurl, AsyncStorage);
ln.callLnurlPayService()
.then(setPayload)
.catch(error => {
presentAlert({ message: error.message });
pop();
});
2021-08-25 01:55:22 -04:00
setLN(ln);
setIsLoading(false);
}
2024-09-08 13:30:00 +01:00
}, [lnurl]);
2020-07-23 19:06:13 +01:00
2021-08-25 01:55:22 -04:00
useEffect(() => {
setPayButtonDisabled(isLoading);
}, [isLoading]);
2020-07-23 19:06:13 +01:00
2021-08-25 01:55:22 -04:00
useEffect(() => {
2024-09-08 13:30:00 +01:00
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:
2024-01-28 11:11:08 -04:00
newAmount = satoshiToBTC(newAmount);
break;
case BitcoinUnit.LOCAL_CURRENCY:
2024-01-28 11:11:08 -04:00
newAmount = satoshiToLocalCurrency(newAmount, false);
2024-09-08 13:30:00 +01:00
_cacheFiatToSat[newAmount] = String(originalSatAmount);
break;
}
2024-09-08 13:30:00 +01:00
setAmount(newAmount.toString());
2021-08-25 01:55:22 -04:00
}
2024-09-08 13:30:00 +01:00
}, [payload, _LN, unit]);
2020-07-23 19:06:13 +01:00
2024-09-08 13:30:00 +01:00
const onWalletSelect = (w: TWallet) => {
setParams({ walletID: w.getID() });
2021-08-25 01:55:22 -04:00
pop();
2020-07-23 19:06:13 +01:00
};
2021-08-25 01:55:22 -04:00
const pay = async () => {
setPayButtonDisabled(true);
2024-09-08 13:30:00 +01:00
if (!_LN || !amount) return;
2020-07-23 19:06:13 +01:00
2024-05-17 18:34:39 -04:00
const isBiometricsEnabled = await isBiometricUseCapableAndEnabled();
2020-07-23 19:06:13 +01:00
if (isBiometricsEnabled) {
2024-05-17 18:34:39 -04:00
if (!(await unlockWithBiometrics())) {
2020-07-23 19:06:13 +01:00
return;
}
}
2024-09-08 13:30:00 +01:00
let amountSats: number | false;
2021-08-25 01:55:22 -04:00
switch (unit) {
2020-07-23 19:06:13 +01:00
case BitcoinUnit.SATS:
2024-09-08 13:30:00 +01:00
amountSats = parseInt(amount, 10);
2020-07-23 19:06:13 +01:00
break;
case BitcoinUnit.BTC:
2024-09-08 13:30:00 +01:00
amountSats = btcToSatoshi(amount);
2020-07-23 19:06:13 +01:00
break;
case BitcoinUnit.LOCAL_CURRENCY:
2024-09-08 13:30:00 +01:00
if (_cacheFiatToSat[String(amount)]) {
amountSats = parseInt(_cacheFiatToSat[amount], 10);
} else {
2024-09-08 13:30:00 +01:00
amountSats = btcToSatoshi(fiatToBTC(parseFloat(amount)));
}
2020-07-23 19:06:13 +01:00
break;
2024-09-08 13:30:00 +01:00
default:
throw new Error('Unknown unit type');
2020-07-23 19:06:13 +01:00
}
try {
2024-09-08 13:30:00 +01:00
let comment: string | undefined;
if (_LN.getCommentAllowed()) {
2021-08-23 14:40:39 +01:00
comment = await prompt('Comment', '', false, 'plain-text');
}
2024-09-08 13:30:00 +01:00
const bolt11payload = await _LN.requestBolt11FromLnurlPayService(amountSats, comment);
// @ts-ignore fixme after lnurl.js converted to ts
2021-08-25 01:55:22 -04:00
await wallet.payInvoice(bolt11payload.pr);
2024-09-08 13:30:00 +01:00
// @ts-ignore fixme after lnurl.js converted to ts
2021-08-25 01:55:22 -04:00
const decoded = wallet.decodeInvoice(bolt11payload.pr);
setPayButtonDisabled(false);
2020-07-23 19:06:13 +01:00
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
2021-08-25 01:55:22 -04:00
if (wallet.last_paid_invoice_result && wallet.last_paid_invoice_result.payment_preimage) {
2024-09-08 13:30:00 +01:00
await _LN.storeSuccess(decoded.payment_hash, wallet.last_paid_invoice_result.payment_preimage);
2020-07-23 19:06:13 +01:00
}
2021-08-25 01:55:22 -04:00
navigate('ScanLndInvoiceRoot', {
2020-07-23 19:06:13 +01:00
screen: 'LnurlPaySuccess',
params: {
paymentHash: decoded.payment_hash,
justPaid: true,
2021-08-25 01:55:22 -04:00
fromWalletID: walletID,
2020-07-23 19:06:13 +01:00
},
});
2022-04-09 13:30:31 -04:00
setIsLoading(false);
2024-09-08 13:30:00 +01:00
} catch (err) {
console.log((err as Error).message);
2021-08-25 01:55:22 -04:00
setIsLoading(false);
setPayButtonDisabled(false);
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
2024-09-08 13:30:00 +01:00
return presentAlert({ message: (err as Error).message });
2020-07-23 19:06:13 +01:00
}
};
2021-08-25 01:55:22 -04:00
const renderWalletSelectionButton = (
<View style={styles.walletSelectRoot}>
{!isLoading && (
<TouchableOpacity
accessibilityRole="button"
style={styles.walletSelectTouch}
onPress={() => navigate('SelectWallet', { onWalletSelect, chainType: Chain.OFFCHAIN })}
>
<Text style={styles.walletSelectText}>{loc.wallets.select_wallet.toLowerCase()}</Text>
<Icon name={I18nManager.isRTL ? 'angle-left' : 'angle-right'} size={18} type="font-awesome" color="#9aa0aa" />
</TouchableOpacity>
)}
<View style={styles.walletWrap}>
<TouchableOpacity
accessibilityRole="button"
style={styles.walletWrapTouch}
onPress={() => navigate('SelectWallet', { onWalletSelect, chainType: Chain.OFFCHAIN })}
>
<Text style={[styles.walletWrapLabel, stylesHook.walletWrapLabel]}>{wallet.getLabel()}</Text>
<Text style={[styles.walletWrapBalance, stylesHook.walletWrapBalance]}>
{formatBalanceWithoutSuffix(wallet.getBalance(), BitcoinUnit.SATS, false)}
</Text>
<Text style={[styles.walletWrapSats, stylesHook.walletWrapSats]}>{BitcoinUnit.SATS}</Text>
</TouchableOpacity>
2020-07-23 19:06:13 +01:00
</View>
2021-08-25 01:55:22 -04:00
</View>
);
2020-07-23 19:06:13 +01:00
2021-08-25 01:55:22 -04:00
const renderGotPayload = () => {
2020-07-23 19:06:13 +01:00
return (
<SafeArea>
2024-09-08 13:30:00 +01:00
<ScrollView contentContainerStyle={styles.scrollviewContainer}>
2020-07-23 19:06:13 +01:00
<BlueCard>
<AmountInput
2021-08-25 01:55:22 -04:00
isLoading={isLoading}
2024-09-08 13:30:00 +01:00
amount={amount}
2021-08-25 01:55:22 -04:00
onAmountUnitChange={setUnit}
onChangeText={setAmount}
disabled={payload && payload.fixed}
unit={unit}
2024-09-08 13:30:00 +01:00
// @ts-ignore idk
2020-07-23 19:06:13 +01:00
inputAccessoryViewID={BlueDismissKeyboardInputAccessory.InputAccessoryViewID}
/>
<BlueText style={styles.alignSelfCenter}>
2021-08-25 01:55:22 -04:00
{loc.formatString(loc.lndViewInvoice.please_pay_between_and, {
min: formatBalance(payload?.min, unit),
max: formatBalance(payload?.max, unit),
2021-08-25 01:55:22 -04:00
})}
2020-07-23 19:06:13 +01:00
</BlueText>
<BlueSpacing20 />
{payload?.image && (
<>
2021-09-04 14:55:23 -04:00
<Image style={styles.img} source={{ uri: payload?.image }} />
<BlueSpacing20 />
</>
)}
2021-08-25 01:55:22 -04:00
<BlueText style={styles.alignSelfCenter}>{payload?.description}</BlueText>
<BlueText style={styles.alignSelfCenter}>{payload?.domain}</BlueText>
2020-07-23 19:06:13 +01:00
<BlueSpacing20 />
2023-11-15 04:40:22 -04:00
{payButtonDisabled ? <BlueLoading /> : <Button title={loc.lnd.payButton} onPress={pay} />}
2020-07-23 19:06:13 +01:00
<BlueSpacing20 />
</BlueCard>
</ScrollView>
2021-08-25 01:55:22 -04:00
{renderWalletSelectionButton}
</SafeArea>
2020-07-23 19:06:13 +01:00
);
2021-08-25 01:55:22 -04:00
};
2020-07-23 19:06:13 +01:00
2024-09-08 13:30:00 +01:00
return isLoading || !wallet || amount === undefined ? (
2021-08-25 01:55:22 -04:00
<View style={[styles.root, stylesHook.root]}>
<BlueLoading />
</View>
) : (
renderGotPayload()
);
2020-07-23 19:06:13 +01:00
};
2021-08-25 01:55:22 -04:00
export default LnurlPay;
2020-07-23 19:06:13 +01:00
const styles = StyleSheet.create({
2024-09-08 13:30:00 +01:00
scrollviewContainer: { justifyContent: 'space-around' },
2020-07-23 19:06:13 +01:00
img: { width: 200, height: 200, alignSelf: 'center' },
alignSelfCenter: {
alignSelf: 'center',
},
2020-12-05 19:37:51 -05:00
root: {
flex: 1,
2021-08-25 01:55:22 -04:00
justifyContent: 'center',
2020-12-05 19:37:51 -05:00
},
2020-07-23 19:06:13 +01:00
walletSelectRoot: {
alignItems: 'center',
justifyContent: 'flex-end',
},
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,
},
});