mirror of
https://github.com/BlueWallet/BlueWallet.git
synced 2024-11-19 09:50:15 +01:00
299 lines
10 KiB
TypeScript
299 lines
10 KiB
TypeScript
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, 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 { DismissKeyboardInputAccessory, DismissKeyboardInputAccessoryViewID } from '../../components/DismissKeyboardInputAccessory';
|
|
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<string, string> = {};
|
|
|
|
const LnurlPay: React.FC = () => {
|
|
const { wallets } = useStorage();
|
|
const { isBiometricUseCapableAndEnabled } = useBiometrics();
|
|
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() ?? BitcoinUnit.BTC);
|
|
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>();
|
|
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 = (
|
|
<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>
|
|
</View>
|
|
</View>
|
|
);
|
|
|
|
const renderGotPayload = () => {
|
|
return (
|
|
<SafeArea>
|
|
<ScrollView contentContainerStyle={styles.scrollviewContainer}>
|
|
<BlueCard>
|
|
<AmountInput
|
|
isLoading={isLoading}
|
|
amount={amount}
|
|
onAmountUnitChange={setUnit}
|
|
onChangeText={setAmount}
|
|
disabled={payload && payload.fixed}
|
|
unit={unit}
|
|
inputAccessoryViewID={DismissKeyboardInputAccessoryViewID}
|
|
/>
|
|
<DismissKeyboardInputAccessory />
|
|
<BlueText style={styles.alignSelfCenter}>
|
|
{loc.formatString(loc.lndViewInvoice.please_pay_between_and, {
|
|
min: formatBalance(payload?.min, unit),
|
|
max: formatBalance(payload?.max, unit),
|
|
})}
|
|
</BlueText>
|
|
<BlueSpacing20 />
|
|
{payload?.image && (
|
|
<>
|
|
<Image style={styles.img} source={{ uri: payload?.image }} />
|
|
<BlueSpacing20 />
|
|
</>
|
|
)}
|
|
<BlueText style={styles.alignSelfCenter}>{payload?.description}</BlueText>
|
|
<BlueText style={styles.alignSelfCenter}>{payload?.domain}</BlueText>
|
|
<BlueSpacing20 />
|
|
{payButtonDisabled ? <BlueLoading /> : <Button title={loc.lnd.payButton} onPress={pay} />}
|
|
<BlueSpacing20 />
|
|
</BlueCard>
|
|
</ScrollView>
|
|
{renderWalletSelectionButton}
|
|
</SafeArea>
|
|
);
|
|
};
|
|
|
|
return isLoading || !wallet || amount === undefined ? (
|
|
<View style={[styles.root, stylesHook.root]}>
|
|
<BlueLoading />
|
|
</View>
|
|
) : (
|
|
renderGotPayload()
|
|
);
|
|
};
|
|
|
|
export default LnurlPay;
|
|
|
|
const styles = StyleSheet.create({
|
|
scrollviewContainer: { justifyContent: 'space-around' },
|
|
img: { width: 200, height: 200, alignSelf: 'center' },
|
|
alignSelfCenter: {
|
|
alignSelf: 'center',
|
|
},
|
|
root: {
|
|
flex: 1,
|
|
justifyContent: 'center',
|
|
},
|
|
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,
|
|
},
|
|
});
|