ADD: show MAX sendable amount
When MAX is selected, display the calculated sendable amount below. - Works only for 1 recipient (Main goal of feature is this for atomic swap) - Shows ≈ prefix when recipient address is unknown (using P2TR estimate) - Shows exact amount when valid address is entered - Forces displayed amount into transaction (no recalculation on Next)
This commit is contained in:
parent
248182c1f6
commit
0f8b04d43a
@ -1,5 +1,6 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Badge, Icon, Text } from '@rneui/themed';
|
||||
import Clipboard from '@react-native-clipboard/clipboard';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import dayjs from 'dayjs';
|
||||
import {
|
||||
@ -24,6 +25,7 @@ import {
|
||||
satoshiToBTC,
|
||||
updateExchangeRate,
|
||||
} from '../blue_modules/currency';
|
||||
import triggerHapticFeedback, { HapticFeedbackTypes } from '../blue_modules/hapticFeedback';
|
||||
import { BlueText } from '../BlueComponents';
|
||||
import confirm from '../helpers/confirm';
|
||||
import loc, { formatBalancePlain, formatBalanceWithoutSuffix, removeTrailingZeros } from '../loc';
|
||||
@ -68,13 +70,31 @@ type AmountInputProps = Omit<TextInputProps, 'onChangeText' | 'value'> & {
|
||||
* Returns a BitcoinUnit value
|
||||
*/
|
||||
onAmountUnitChange: (unit: BitcoinUnit) => void;
|
||||
/**
|
||||
* Estimated sendable amount in satoshis when MAX is selected.
|
||||
* Displayed below the MAX label. Pass null to hide.
|
||||
*/
|
||||
maxSendableAmount?: number | null;
|
||||
/**
|
||||
* When true, shows ≈ prefix for maxSendableAmount (indicates estimate).
|
||||
*/
|
||||
isMaxAmountEstimate?: boolean;
|
||||
};
|
||||
|
||||
export const AmountInput: React.FC<AmountInputProps> = props => {
|
||||
const textInputRef = useRef<TextInput>(null);
|
||||
const { colors } = useTheme();
|
||||
const amount = props.amount || '0'; // internally amount is aways a string with a correct number
|
||||
const { onChangeText, unit, onAmountUnitChange, disabled = false, isLoading = false, ...otherProps } = props;
|
||||
const {
|
||||
onChangeText,
|
||||
unit,
|
||||
onAmountUnitChange,
|
||||
disabled = false,
|
||||
isLoading = false,
|
||||
maxSendableAmount,
|
||||
isMaxAmountEstimate,
|
||||
...otherProps
|
||||
} = props;
|
||||
const [isRateBeingUpdatedLocal, setIsRateBeingUpdatedLocal] = useState(false);
|
||||
const [outdatedRefreshRate, setOutdatedRefreshRate] = useState<CurrencyRate | undefined>();
|
||||
|
||||
@ -239,6 +259,13 @@ export const AmountInput: React.FC<AmountInputProps> = props => {
|
||||
}
|
||||
}, [onChangeText]);
|
||||
|
||||
const copyMaxEstimate = useCallback(() => {
|
||||
if (maxSendableAmount == null) return;
|
||||
const btcValue = removeTrailingZeros(new BigNumber(maxSendableAmount).dividedBy(100000000).toFixed(8));
|
||||
Clipboard.setString(btcValue);
|
||||
triggerHapticFeedback(HapticFeedbackTypes.Selection);
|
||||
}, [maxSendableAmount]);
|
||||
|
||||
const handleSelectionChange = useCallback(
|
||||
(event: NativeSyntheticEvent<TextInputSelectionChangeEventData>) => {
|
||||
const { selection } = event.nativeEvent;
|
||||
@ -283,6 +310,14 @@ export const AmountInput: React.FC<AmountInputProps> = props => {
|
||||
) : (
|
||||
<Pressable onPress={resetAmount}>
|
||||
<Text style={[styles.input, stylesHook.input]}>{BitcoinUnit.MAX}</Text>
|
||||
{maxSendableAmount != null && (
|
||||
<Text style={[styles.maxEstimate, stylesHook.localCurrency]} onLongPress={copyMaxEstimate}>
|
||||
{(isMaxAmountEstimate ? '≈ ' : '') +
|
||||
removeTrailingZeros(new BigNumber(maxSendableAmount).dividedBy(100000000).toFixed(8)) +
|
||||
' ' +
|
||||
loc.units[BitcoinUnit.BTC]}
|
||||
</Text>
|
||||
)}
|
||||
</Pressable>
|
||||
)}
|
||||
{unit !== BitcoinUnit.LOCAL_CURRENCY && amount !== BitcoinUnit.MAX && (
|
||||
@ -387,6 +422,11 @@ const styles = StyleSheet.create({
|
||||
color: '#9BA0A9',
|
||||
fontWeight: '600',
|
||||
},
|
||||
maxEstimate: {
|
||||
fontSize: 16,
|
||||
textAlign: 'center',
|
||||
marginTop: 4,
|
||||
},
|
||||
changeAmountUnit: {
|
||||
alignSelf: 'center',
|
||||
marginRight: 16,
|
||||
|
||||
@ -111,6 +111,15 @@ const SendDetails = () => {
|
||||
const balance: number = utxos ? utxos.reduce((prev, curr) => prev + curr.value, 0) : (wallet?.getBalance() ?? 0);
|
||||
const allBalance = formatBalanceWithoutSuffix(balance, BitcoinUnit.BTC, true);
|
||||
|
||||
// estimated sendable amount when MAX is selected (null if not applicable)
|
||||
const maxSendableAmount = useMemo(() => {
|
||||
if (addresses.length !== 1) return null;
|
||||
if (addresses[0].amount !== BitcoinUnit.MAX) return null;
|
||||
if (feePrecalc.current === null) return null;
|
||||
const sendable = balance - feePrecalc.current;
|
||||
return sendable > 0 ? sendable : null;
|
||||
}, [addresses, balance, feePrecalc]);
|
||||
|
||||
// if cutomFee is not set, we need to choose highest possible fee for wallet balance
|
||||
// if there are no funds for even Slow option, use 1 sat/vbyte fee
|
||||
const feeRate = useMemo(() => {
|
||||
@ -316,8 +325,12 @@ const SendDetails = () => {
|
||||
let targets = [];
|
||||
for (const transaction of addresses) {
|
||||
if (transaction.amount === BitcoinUnit.MAX) {
|
||||
// single output with MAX
|
||||
targets = [{ address: transaction.address }];
|
||||
// single output with MAX — use P2TR dummy (43-byte output) for conservative estimate
|
||||
const addr =
|
||||
transaction.address && wallet.isAddressValid(transaction.address)
|
||||
? transaction.address
|
||||
: 'bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqzk5jj0';
|
||||
targets = [{ address: addr }];
|
||||
break;
|
||||
}
|
||||
const value = transaction.amountSats;
|
||||
@ -586,7 +599,12 @@ const SendDetails = () => {
|
||||
for (const transaction of addresses) {
|
||||
if (transaction.amount === BitcoinUnit.MAX) {
|
||||
// output with MAX
|
||||
targets.push({ address: transaction.address });
|
||||
if (maxSendableAmount != null && addresses.length === 1) {
|
||||
// Force the exact displayed amount — remainder goes to fee
|
||||
targets.push({ address: transaction.address, value: maxSendableAmount });
|
||||
} else {
|
||||
targets.push({ address: transaction.address });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const value = parseInt(String(transaction.amountSats), 10);
|
||||
@ -1399,6 +1417,8 @@ const SendDetails = () => {
|
||||
editable={isEditable}
|
||||
disabled={!isEditable}
|
||||
inputAccessoryViewID={InputAccessoryAllFundsAccessoryViewID}
|
||||
maxSendableAmount={index === scrollIndex.current ? maxSendableAmount : null}
|
||||
isMaxAmountEstimate={!(item.address && wallet?.isAddressValid(item.address))}
|
||||
/>
|
||||
</View>
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user