diff --git a/loc/en.json b/loc/en.json index ecc27b259..56c67a587 100644 --- a/loc/en.json +++ b/loc/en.json @@ -211,6 +211,7 @@ "details_adv_fee_bump": "Allow Fee Bump", "details_adv_full": "Use Full Balance", "details_adv_full_sure": "Are you sure you want to use your wallet’s full balance for this transaction?", + "details_adv_full_sure_frozen": "Are you sure you want to use your wallet’s full balance for this transaction? Please note that frozen coins are excluded.", "details_adv_import": "Import Transaction", "details_adv_import_qr": "Import Transaction (QR)", "details_amount_field_is_not_valid": "The amount is not valid.", @@ -218,12 +219,14 @@ "details_create": "Create Invoice", "details_error_decode": "Unable to decode Bitcoin address", "details_fee_field_is_not_valid": "The fee is not valid.", + "details_frozen": "{amount} BTC are frozen", "details_next": "Next", "details_no_signed_tx": "The selected file doesn’t contain a transaction that can be imported.", "details_note_placeholder": "Note to Self", "details_scan": "Scan", "details_scan_hint": "Double tap to scan or import a destination", "details_total_exceeds_balance": "The sending amount exceeds the available balance.", + "details_total_exceeds_balance_frozen": "The sending amount exceeds the available balance. Please note that the frozen coins are excluded.", "details_unrecognized_file_format": "Unrecognized file format", "details_wallet_before_tx": "Before creating a transaction, you must first add a Bitcoin wallet.", "dynamic_init": "Initializing", diff --git a/screen/send/details.js b/screen/send/details.js index 362312fba..a64318f2f 100644 --- a/screen/send/details.js +++ b/screen/send/details.js @@ -1,4 +1,4 @@ -import React, { useState, useRef, useEffect, useContext, useMemo } from 'react'; +import React, { useState, useRef, useEffect, useCallback, useContext, useMemo } from 'react'; import { ActivityIndicator, Alert, @@ -17,7 +17,7 @@ import { TouchableWithoutFeedback, View, } from 'react-native'; -import { useNavigation, useRoute, useTheme } from '@react-navigation/native'; +import { useNavigation, useRoute, useTheme, useFocusEffect } from '@react-navigation/native'; import { Icon } from 'react-native-elements'; import AsyncStorage from '@react-native-async-storage/async-storage'; import ReactNativeHapticFeedback from 'react-native-haptic-feedback'; @@ -25,7 +25,7 @@ import RNFS from 'react-native-fs'; import BigNumber from 'bignumber.js'; import * as bitcoin from 'bitcoinjs-lib'; -import { BlueButton, BlueDismissKeyboardInputAccessory, BlueListItem, BlueLoading } from '../../BlueComponents'; +import { BlueButton, BlueDismissKeyboardInputAccessory, BlueListItem, BlueLoading, BlueText } from '../../BlueComponents'; import { navigationStyleTx } from '../../components/navigationStyle'; import NetworkTransactionFees, { NetworkTransactionFee } from '../../models/networkTransactionFees'; import { BitcoinUnit, Chain } from '../../models/bitcoinUnits'; @@ -74,6 +74,7 @@ const SendDetails = () => { const [feeUnit, setFeeUnit] = useState(); const [amountUnit, setAmountUnit] = useState(); const [utxo, setUtxo] = useState(null); + const [frozenBalance, setFrozenBlance] = useState(false); const [payjoinUrl, setPayjoinUrl] = useState(null); const [changeAddress, setChangeAddress] = useState(); const [dumb, setDumb] = useState(false); @@ -252,6 +253,14 @@ const SendDetails = () => { const changeAddress = getChangeAddressFast(); const requestedSatPerByte = Number(feeRate); const lutxo = utxo || wallet.getUtxo(); + let frozen = 0; + if (!utxo) { + // if utxo is not limited search for frozen outputs and calc it's balance + frozen = wallet + .getUtxo(true) + .filter(o => !lutxo.some(i => i.txid === o.txid && i.vout === o.vout)) + .reduce((prev, curr) => prev + curr.value, 0); + } const options = [ { key: 'current', fee: requestedSatPerByte }, @@ -316,8 +325,17 @@ const SendDetails = () => { } setFeePrecalc(newFeePrecalc); + setFrozenBlance(frozen); }, [wallet, networkTransactionFees, utxo, addresses, feeRate, dumb]); // eslint-disable-line react-hooks/exhaustive-deps + // we need to re-calculate fees if user opens-closes coin control + useFocusEffect( + useCallback(() => { + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); + setDumb(v => !v); + }, []), + ); + const getChangeAddressFast = () => { if (changeAddress) return changeAddress; // cache @@ -445,7 +463,7 @@ const SendDetails = () => { console.log('validation error'); } else if (balance - transaction.amountSats < 0) { // first sanity check is that sending amount is not bigger than available balance - error = loc.send.details_total_exceeds_balance; + error = frozenBalance > 0 ? loc.send.details_total_exceeds_balance_frozen : loc.send.details_total_exceeds_balance; console.log('validation error'); } else if (transaction.address) { const address = transaction.address.trim().toLowerCase(); @@ -977,9 +995,10 @@ const SendDetails = () => { const onUseAllPressed = () => { ReactNativeHapticFeedback.trigger('notificationWarning'); + const message = frozenBalance > 0 ? loc.send.details_adv_full_sure_frozen : loc.send.details_adv_full_sure; Alert.alert( loc.send.details_adv_full, - loc.send.details_adv_full_sure, + message, [ { text: loc._.ok, @@ -1368,6 +1387,15 @@ const SendDetails = () => { disabled={!isEditable} inputAccessoryViewID={InputAccessoryAllFunds.InputAccessoryViewID} /> + + {frozenBalance > 0 && ( + + + {loc.formatString(loc.send.details_frozen, { amount: formatBalanceWithoutSuffix(frozenBalance, BitcoinUnit.BTC, true) })} + + + )} + { text = text.trim(); @@ -1637,6 +1665,12 @@ const styles = StyleSheet.create({ height: 40, justifyContent: 'center', }, + frozenContainer: { + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + marginVertical: 8, + }, }); SendDetails.navigationOptions = navigationStyleTx({}, options => ({