fix custom fee ui state drift on mobile
What changed, and why it matters
This commit fixes a UI bug in Stack Wallet where the custom/preset fee selection could get out of sync between the fee picker and the actual transaction being built. Previously, whether a custom fee was active was tracked in a separate local variable that could drift from the shared state provider. The patch makes the send flow read the fee mode directly from the shared state provider at the moment the transaction is prepared, and adds a helper that only supplies a custom fee value when custom mode is actually selected. The risk is that a user might have a preset fee selected while the app still uses an old custom fee value, or vice versa, leading to an unexpected transaction fee.
Review the updated send flows to confirm the provider state is always refreshed before transaction preparation and that no other send paths still rely on the removed local custom-fee flags. Consider adding integration tests that switch between preset and custom fees and verify the prepared TxData.feeRateType and satsPerVByte values match the UI selection.
Security signals we found
UI state drift between fee picker and transaction preparation
Inconsistent custom/preset fee selection could lead to unintended transaction fees
Removal of parallel local state in favor of single source of truth (Riverpod provider)
New helper enforces that custom fee value is only passed when custom mode is active
Evidence from the diff
The change centralizes fee-rate state in Riverpod providers (feeRateTypeMobileStateProvider / feeRateTypeDesktopStateProvider) instead of local ValueNotifier
Changed components
lib/pages/send_view/send_view.dartlib/pages/send_view/sol_token_send_view.dartlib/pages/send_view/sub_widgets/transaction_fee_selection_sheet.dartlib/pages/send_view/token_send_view.dartlib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dartlib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send_fee_form.dartlib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dartlib/utilities/enums/fee_rate_type_enum.darttest/utilities/fee_rate_type_enum_test.dartInspect captured patch +121 / −202
diff --git a/lib/pages/send_view/send_view.dart b/lib/pages/send_view/send_view.dart
index e763eb9..ffc8fbb 100644
--- a/lib/pages/send_view/send_view.dart
+++ b/lib/pages/send_view/send_view.dart
@@ -967,9 +967,10 @@ class _SendViewState extends ConsumerState<SendView> {
final time = Future<dynamic>.delayed(const Duration(milliseconds: 2500));
Future<TxData> txDataFuture;
+ final feeRateType = ref.read(feeRateTypeMobileStateProvider);
+ final satsPerVByte = feeRateType.customSatsPerVByte(customFeeRate);
if (isPaynymSend) {
- final feeRate = ref.read(feeRateTypeMobileStateProvider);
txDataFuture = (wallet as PaynymInterface).preparePaymentCodeSend(
txData: TxData(
paynymAccountLite: widget.accountLite!,
@@ -981,8 +982,8 @@ class _SendViewState extends ConsumerState<SendView> {
addressType: AddressType.unknown,
),
],
- satsPerVByte: isCustomFee.value ? customFeeRate : null,
- feeRateType: feeRate,
+ satsPerVByte: satsPerVByte,
+ feeRateType: feeRateType,
utxos:
(wallet is CoinControlInterface &&
wallet is! SalviumWallet &&
@@ -1007,8 +1008,8 @@ class _SendViewState extends ConsumerState<SendView> {
isChange: false,
),
],
- feeRateType: ref.read(feeRateTypeMobileStateProvider),
- satsPerVByte: isCustomFee.value ? customFeeRate : null,
+ feeRateType: feeRateType,
+ satsPerVByte: satsPerVByte,
utxos: (coinControlEnabled && selectedUTXOs.isNotEmpty)
? selectedUTXOs
: null,
@@ -1027,8 +1028,8 @@ class _SendViewState extends ConsumerState<SendView> {
)!,
),
],
- feeRateType: ref.read(feeRateTypeMobileStateProvider),
- satsPerVByte: isCustomFee.value ? customFeeRate : null,
+ feeRateType: feeRateType,
+ satsPerVByte: satsPerVByte,
utxos: (coinControlEnabled && selectedUTXOs.isNotEmpty)
? selectedUTXOs
: null,
@@ -1080,8 +1081,8 @@ class _SendViewState extends ConsumerState<SendView> {
addressType: wallet.cryptoCurrency.getAddressType(_address!)!,
),
],
- feeRateType: ref.read(feeRateTypeMobileStateProvider),
- satsPerVByte: isCustomFee.value ? customFeeRate : null,
+ feeRateType: feeRateType,
+ satsPerVByte: satsPerVByte,
// these will need to be mweb utxos
// utxos:
@@ -1105,8 +1106,8 @@ class _SendViewState extends ConsumerState<SendView> {
),
],
memo: memo,
- feeRateType: ref.read(feeRateTypeMobileStateProvider),
- satsPerVByte: isCustomFee.value ? customFeeRate : null,
+ feeRateType: feeRateType,
+ satsPerVByte: satsPerVByte,
ethEIP1559Fee: ethFee,
utxos:
(wallet is CoinControlInterface &&
@@ -1261,7 +1262,6 @@ class _SendViewState extends ConsumerState<SendView> {
bool get isPaynymSend => widget.accountLite != null;
- final isCustomFee = ValueNotifier(false);
int customFeeRate = 1;
EthEIP1559Fee? ethFee;
@@ -1281,22 +1281,20 @@ class _SendViewState extends ConsumerState<SendView> {
ref.watch(pSendAmount)?.decimal ??
Decimal.zero)
.toAmount(fractionDigits: coin.fractionDigits),
- updateChosen: (String fee) {
- if (fee == "custom") {
- if (!isCustomFee.value) {
- setState(() {
- isCustomFee.value = true;
- });
- }
+ updateChosen: (feeRateType, fee) {
+ if (feeRateType.isCustom) {
return;
}
- _setCurrentFee(fee, true);
+ if (fee != null) {
+ _setCurrentFee(fee, true);
+ }
setState(() {
- _calculateFeesFuture = Future(() => fee);
- if (isCustomFee.value) {
- isCustomFee.value = false;
+ if (fee != null) {
+ _calculateFeesFuture = Future(() => fee);
}
+ customFeeRate = 1;
+ ethFee = null;
});
},
),
@@ -1318,12 +1316,6 @@ class _SendViewState extends ConsumerState<SendView> {
ref.refresh(feeSheetSessionCacheProvider);
ref.refresh(pIsExchangeAddress);
});
- isCustomFee.addListener(() {
- if (!isCustomFee.value) {
- customFeeRate = 1;
- ethFee = null;
- }
- });
hasFees = coin is! Epiccash && coin is! NanoCurrency && coin is! Tezos;
_currentFee = 0.toAmountAsRaw(fractionDigits: coin.fractionDigits);
@@ -1433,13 +1425,13 @@ class _SendViewState extends ConsumerState<SendView> {
_cryptoFocus.dispose();
_baseFocus.dispose();
_memoFocus.dispose();
- isCustomFee.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
debugPrint("BUILD: $runtimeType");
+ final isCustomFee = ref.watch(feeRateTypeMobileStateProvider).isCustom;
final String locale = ref.watch(
localeServiceChangeNotifierProvider.select((value) => value.locale),
);
@@ -2683,7 +2675,7 @@ class _SendViewState extends ConsumerState<SendView> {
false,
);
return Text(
- isCustomFee.value
+ isCustomFee
? ""
: "~${snapshot.data!}",
style:
@@ -2722,7 +2714,7 @@ class _SendViewState extends ConsumerState<SendView> {
),
],
),
- if (isCustomFee.value && !isEth)
+ if (isCustomFee && !isEth)
Padding(
padding: const EdgeInsets.only(
bottom: 12,
@@ -2735,9 +2727,9 @@ class _SendViewState extends ConsumerState<SendView> {
},
),
),
- if (isCustomFee.value && isEth)
+ if (isCustomFee && isEth)
const SizedBox(height: 12),
- if (isCustomFee.value && isEth)
+ if (isCustomFee && isEth)
EthFeeForm(
minGasLimit: kEthereumMinGasLimit,
stateChanged: (fee) => ethFee = fee,
diff --git a/lib/pages/send_view/sol_token_send_view.dart b/lib/pages/send_view/sol_token_send_view.dart
index f81015b..36b027f 100644
--- a/lib/pages/send_view/sol_token_send_view.dart
+++ b/lib/pages/send_view/sol_token_send_view.dart
@@ -1287,12 +1287,13 @@ class _SolTokenSendViewState extends ConsumerState<SolTokenSendView> {
tokenWallet
.tokenDecimals,
),
- updateChosen: (String fee) {
- setState(() {
- _calculateFeesFuture = Future(
- () => fee,
- );
- });
+ updateChosen: (_, fee) {
+ if (fee != null) {
+ setState(() {
+ _calculateFeesFuture =
+ Future(() => fee);
+ });
+ }
},
),
);
diff --git a/lib/pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart b/lib/pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart
index 387138d..333606f 100644
--- a/lib/pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart
+++ b/lib/pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart
@@ -58,7 +58,7 @@ class TransactionFeeSelectionSheet extends ConsumerStatefulWidget {
final String walletId;
final Amount amount;
- final Function updateChosen;
+ final void Function(FeeRateType feeRateType, String? fee) updateChosen;
final bool isToken;
@override
@@ -80,6 +80,16 @@ class _TransactionFeeSelectionSheetState
"Calculating...",
];
+ void _selectFeeRate(FeeRateType feeRateType, CryptoCurrency coin) {
+ ref.read(feeRateTypeMobileStateProvider.state).state = feeRateType;
+ widget.updateChosen(
+ feeRateType,
+ feeRateType.isCustom ? null : getAmount(feeRateType, coin),
+ );
+
+ Navigator.of(context).pop();
+ }
+
Amount _addFiroOpReturnFee({
required Amount fee,
required BigInt feeRate,
@@ -349,23 +359,7 @@ class _TransactionFeeSelectionSheetState
),
const SizedBox(height: 16),
GestureDetector(
- onTap: () {
- final state = ref
- .read(feeRateTypeMobileStateProvider.state)
- .state;
- if (state != FeeRateType.fast) {
- ref.read(feeRateTypeMobileStateProvider.state).state =
- FeeRateType.fast;
- }
- final String? fee = getAmount(
- FeeRateType.fast,
- wallet.info.coin,
- );
- if (fee != null) {
- widget.updateChosen(fee);
- }
- Navigator.of(context).pop();
- },
+ onTap: () => _selectFeeRate(FeeRateType.fast, coin),
child: Container(
color: Colors.transparent,
child: Row(
@@ -387,17 +381,8 @@ class _TransactionFeeSelectionSheetState
feeRateTypeMobileStateProvider.state,
)
.state,
- onChanged: (x) {
- ref
- .read(
- feeRateTypeMobileStateProvider
- .state,
- )
- .state =
- FeeRateType.fast;
-
- Navigator.of(context).pop();
- },
+ onChanged: (_) =>
+ _selectFeeRate(FeeRateType.fast, coin),
),
),
],
@@ -486,23 +471,7 @@ class _TransactionFeeSelectionSheetState
),
const SizedBox(height: 16),
GestureDetector(
- onTap: () {
- final state = ref
- .read(feeRateTypeMobileStateProvider.state)
- .state;
- if (state != FeeRateType.average) {
- ref.read(feeRateTypeMobileStateProvider.state).state =
- FeeRateType.average;
- }
- final String? fee = getAmount(
- FeeRateType.average,
- coin,
- );
- if (fee != null) {
- widget.updateChosen(fee);
- }
- Navigator.of(context).pop();
- },
+ onTap: () => _selectFeeRate(FeeRateType.average, coin),
child: Container(
color: Colors.transparent,
child: Row(
@@ -523,16 +492,10 @@ class _TransactionFeeSelectionSheetState
feeRateTypeMobileStateProvider.state,
)
.state,
- onChanged: (x) {
- ref
- .read(
- feeRateTypeMobileStateProvider
- .state,
- )
- .state =
- FeeRateType.average;
- Navigator.of(context).pop();
- },
+ onChanged: (_) => _selectFeeRate(
+ FeeRateType.average,
+ coin,
+ ),
),
),
],
@@ -621,20 +584,7 @@ class _TransactionFeeSelectionSheetState
),
const SizedBox(height: 16),
GestureDetector(
- onTap: () {
- final state = ref
- .read(feeRateTypeMobileStateProvider.state)
- .state;
- if (state != FeeRateType.slow) {
- ref.read(feeRateTypeMobileStateProvider.state).state =
- FeeRateType.slow;
- }
- final String? fee = getAmount(FeeRateType.slow, coin);
- if (fee != null) {
- widget.updateChosen(fee);
- }
- Navigator.of(context).pop();
- },
+ onTap: () => _selectFeeRate(FeeRateType.slow, coin),
child: Container(
color: Colors.transparent,
child: Row(
@@ -655,16 +605,8 @@ class _TransactionFeeSelectionSheetState
feeRateTypeMobileStateProvider.state,
)
.state,
- onChanged: (x) {
- ref
- .read(
- feeRateTypeMobileStateProvider
- .state,
- )
- .state =
- FeeRateType.slow;
- Navigator.of(context).pop();
- },
+ onChanged: (_) =>
+ _selectFeeRate(FeeRateType.slow, coin),
),
),
],
@@ -754,20 +696,7 @@ class _TransactionFeeSelectionSheetState
const SizedBox(height: 24),
if (wallet is ElectrumXInterface || coin is Ethereum)
GestureDetector(
- onTap: () {
- final state = ref
- .read(feeRateTypeMobileStateProvider.state)
- .state;
- if (state != FeeRateType.custom) {
- ref
- .read(feeRateTypeMobileStateProvider.state)
- .state =
- FeeRateType.custom;
- }
- widget.updateChosen("custom");
-
- Navigator.of(context).pop();
- },
+ onTap: () => _selectFeeRate(FeeRateType.custom, coin),
child: Container(
color: Colors.transparent,
child: Row(
@@ -789,16 +718,10 @@ class _TransactionFeeSelectionSheetState
.state,
)
.state,
- onChanged: (x) {
- ref
- .read(
- feeRateTypeMobileStateProvider
- .state,
- )
- .state =
- FeeRateType.custom;
- Navigator.of(context).pop();
- },
+ onChanged: (_) => _selectFeeRate(
+ FeeRateType.custom,
+ coin,
+ ),
),
),
],
diff --git a/lib/pages/send_view/token_send_view.dart b/lib/pages/send_view/token_send_view.dart
index 3d30fc5..9277117 100644
--- a/lib/pages/send_view/token_send_view.dart
+++ b/lib/pages/send_view/token_send_view.dart
@@ -118,8 +118,6 @@ class _TokenSendViewState extends ConsumerState<TokenSendView> {
late Future<String> _calculateFeesFuture;
String cachedFees = "";
- final isCustomFee = ValueNotifier(false);
-
EthEIP1559Fee? ethFee;
void _onTokenSendViewPasteAddressFieldButtonPressed() async {
@@ -586,9 +584,6 @@ class _TokenSendViewState extends ConsumerState<TokenSendView> {
@override
void initState() {
ref.refresh(feeSheetSessionCacheProvider);
- isCustomFee.addListener(() {
- if (!isCustomFee.value) ethFee = null;
- });
_calculateFeesFuture = calculateFees();
_data = widget.autoFillData;
@@ -637,13 +632,13 @@ class _TokenSendViewState extends ConsumerState<TokenSendView> {
_addressFocusNode.dispose();
_cryptoFocus.dispose();
_baseFocus.dispose();
- isCustomFee.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
debugPrint("BUILD: $runtimeType");
+ final isCustomFee = ref.watch(feeRateTypeMobileStateProvider).isCustom;
final String locale = ref.watch(
localeServiceChangeNotifierProvider.select((value) => value.locale),
);
@@ -1157,7 +1152,7 @@ class _TokenSendViewState extends ConsumerState<TokenSendView> {
),
const SizedBox(height: 12),
Text(
- "Transaction fee ${isCustomFee.value ? "" : "(max)"}",
+ "Transaction fee ${isCustomFee ? "" : "(max)"}",
style: STextStyles.smallMed12(context),
textAlign: TextAlign.left,
),
@@ -1210,23 +1205,17 @@ class _TokenSendViewState extends ConsumerState<TokenSendView> {
tokenContract
.decimals,
),
- updateChosen: (String fee) {
- if (fee == "custom") {
- if (!isCustomFee.value) {
- setState(() {
- isCustomFee.value = true;
- });
- }
+ updateChosen: (feeRateType, fee) {
+ if (feeRateType.isCustom) {
return;
}
setState(() {
- _calculateFeesFuture = Future(
- () => fee,
- );
- if (isCustomFee.value) {
- isCustomFee.value = false;
+ if (fee != null) {
+ _calculateFeesFuture =
+ Future(() => fee);
}
+ ethFee = null;
});
},
),
@@ -1258,7 +1247,7 @@ class _TokenSendViewState extends ConsumerState<TokenSendView> {
ConnectionState.done &&
snapshot.hasData) {
return Text(
- isCustomFee.value
+ isCustomFee
? ""
: "~${snapshot.data!}",
style:
@@ -1302,8 +1291,8 @@ class _TokenSendViewState extends ConsumerState<TokenSendView> {
),
],
),
- if (isCustomFee.value) const SizedBox(height: 12),
- if (isCustomFee.value)
+ if (isCustomFee) const SizedBox(height: 12),
+ if (isCustomFee)
EthFeeForm(
minGasLimit: kEthereumTokenMinGasLimit,
stateChanged: (value) => ethFee = value,
diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart
index 5060d2b..58d6826 100644
--- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart
+++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart
@@ -44,6 +44,7 @@ import '../../../../utilities/amount/amount_unit.dart';
import '../../../../utilities/assets.dart';
import '../../../../utilities/clipboard_interface.dart';
import '../../../../utilities/constants.dart';
+import '../../../../utilities/enums/fee_rate_type_enum.dart';
import '../../../../utilities/logger.dart';
import '../../../../utilities/prefs.dart';
import '../../../../utilities/show_loading.dart';
@@ -140,7 +141,6 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
bool get isPaynymSend => widget.accountLite != null;
- bool isCustomFee = false;
int customFeeRate = 1;
EthEIP1559Fee? ethFee;
@@ -580,11 +580,11 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
TxData txData;
Future<TxData> txDataFuture;
+ final feeRateType = ref.read(feeRateTypeDesktopStateProvider);
+ final satsPerVByte = feeRateType.customSatsPerVByte(customFeeRate);
if (isPaynymSend) {
final paynymWallet = wallet as PaynymInterface;
-
- final feeRate = ref.read(feeRateTypeDesktopStateProvider);
txDataFuture = paynymWallet.preparePaymentCodeSend(
txData: TxData(
paynymAccountLite: widget.accountLite!,
@@ -596,8 +596,8 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
addressType: AddressType.unknown,
),
],
- satsPerVByte: isCustomFee ? customFeeRate : null,
- feeRateType: feeRate,
+ satsPerVByte: satsPerVByte,
+ feeRateType: feeRateType,
utxos:
(wallet is CoinControlInterface &&
wallet is! SalviumWallet &&
@@ -621,8 +621,8 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
isChange: false,
),
],
- feeRateType: ref.read(feeRateTypeDesktopStateProvider),
- satsPerVByte: isCustomFee ? customFeeRate : null,
+ feeRateType: feeRateType,
+ satsPerVByte: satsPerVByte,
utxos:
(coinControlEnabled &&
ref.read(pDesktopUseUTXOs).isNotEmpty)
@@ -643,8 +643,8 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
)!,
),
],
- feeRateType: ref.read(feeRateTypeDesktopStateProvider),
- satsPerVByte: isCustomFee ? customFeeRate : null,
+ feeRateType: feeRateType,
+ satsPerVByte: satsPerVByte,
utxos:
(coinControlEnabled &&
ref.read(pDesktopUseUTXOs).isNotEmpty)
@@ -698,8 +698,8 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
addressType: wallet.cryptoCurrency.getAddressType(_address!)!,
),
],
- feeRateType: ref.read(feeRateTypeDesktopStateProvider),
- satsPerVByte: isCustomFee ? customFeeRate : null,
+ feeRateType: feeRateType,
+ satsPerVByte: satsPerVByte,
// these will need to be mweb utxos
// utxos:
// (wallet is CoinControlInterface &&
@@ -722,8 +722,8 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
),
],
memo: memo,
- feeRateType: ref.read(feeRateTypeDesktopStateProvider),
- satsPerVByte: isCustomFee ? customFeeRate : null,
+ feeRateType: feeRateType,
+ satsPerVByte: satsPerVByte,
nonce: wallet.cryptoCurrency is Ethereum
? int.tryParse(nonceController.text)
: null,
@@ -2127,8 +2127,7 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
walletId: walletId,
isToken: false,
onCustomFeeSliderChanged: (value) => customFeeRate = value,
- onCustomFeeOptionChanged: (value) {
- isCustomFee = value;
+ onCustomFeeOptionChanged: () {
customFeeRate = 1;
ethFee = null;
},
diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send_fee_form.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send_fee_form.dart
index b1e2b46..eb40e66 100644
--- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send_fee_form.dart
+++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send_fee_form.dart
@@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart';
import '../../../../providers/providers.dart';
+import '../../../../providers/ui/fee_rate_type_state_provider.dart';
import '../../../../providers/ui/preview_tx_button_state_provider.dart';
import '../../../../providers/wallet/desktop_fee_providers.dart';
import '../../../../providers/wallet/public_private_balance_state_provider.dart';
@@ -39,7 +40,7 @@ class DesktopSendFeeForm extends ConsumerStatefulWidget {
final String walletId;
final bool isToken;
final void Function(int) onCustomFeeSliderChanged;
- final void Function(bool) onCustomFeeOptionChanged;
+ final VoidCallback onCustomFeeOptionChanged;
final void Function(EthEIP1559Fee)? onCustomEip1559FeeOptionChanged;
@override
@@ -58,15 +59,6 @@ class _DesktopSendFeeFormState extends ConsumerState<DesktopSendFeeForm> {
bool get isEth => cryptoCurrency is Ethereum;
- bool _isCustomFeeValue = false;
- bool get _isCustomFee => _isCustomFeeValue;
- set _isCustomFee(bool newValue) {
- if (_isCustomFeeValue != newValue) {
- _isCustomFeeValue = newValue;
- widget.onCustomFeeOptionChanged.call(_isCustomFeeValue);
- }
- }
-
(FeeRateType, String?, String?)? feeSelectionResult;
Amount _addFiroOpReturnFee({
@@ -104,6 +96,7 @@ class _DesktopSendFeeFormState extends ConsumerState<DesktopSendFeeForm> {
@override
Widget build(BuildContext context) {
+ final isCustomFee = ref.watch(feeRateTypeDesktopStateProvider).isCustom;
final canEditFees =
isEth ||
cryptoCurrency is Solana ||
@@ -124,6 +117,7 @@ class _DesktopSendFeeFormState extends ConsumerState<DesktopSendFeeForm> {
CustomTextButton(
text: "Edit",
onTap: () async {
+ final wasCustomFee = isCustomFee;
feeSelectionResult =
await showDialog<(FeeRateType, String?, String?)?>(
context: context,
@@ -134,12 +128,9 @@ class _DesktopSendFeeFormState extends ConsumerState<DesktopSendFeeForm> {
);
if (feeSelectionResult != null) {
- if (_isCustomFee &&
- feeSelectionResult!.$1 != FeeRateType.custom) {
- _isCustomFee = false;
- } else if (!_isCustomFee &&
- feeSelectionResult!.$1 == FeeRateType.custom) {
- _isCustomFee = true;
+ final selectedIsCustomFee = feeSelectionResult!.$1.isCustom;
+ if (wasCustomFee != selectedIsCustomFee) {
+ widget.onCustomFeeOptionChanged.call();
}
}
@@ -150,7 +141,7 @@ class _DesktopSendFeeFormState extends ConsumerState<DesktopSendFeeForm> {
),
child: Text(
"Transaction fee"
- "${_isCustomFee ? "" : " (${isEth ? "max" : "estimated"})"}",
+ "${isCustomFee ? "" : " (${isEth ? "max" : "estimated"})"}",
style: STextStyles.desktopTextExtraSmall(context).copyWith(
color: Theme.of(
context,
@@ -160,7 +151,7 @@ class _DesktopSendFeeFormState extends ConsumerState<DesktopSendFeeForm> {
),
),
const SizedBox(height: 10),
- if (!_isCustomFee)
+ if (!isCustomFee)
Padding(
padding: const EdgeInsets.all(10),
child: (feeSelectionResult?.$2 == null)
@@ -340,7 +331,7 @@ class _DesktopSendFeeFormState extends ConsumerState<DesktopSendFeeForm> {
],
),
),
- if (_isCustomFee && isEth)
+ if (isCustomFee && isEth)
EthFeeForm(
minGasLimit: widget.isToken
? kEthereumTokenMinGasLimit
@@ -348,7 +339,7 @@ class _DesktopSendFeeFormState extends ConsumerState<DesktopSendFeeForm> {
stateChanged: (value) =>
widget.onCustomEip1559FeeOptionChanged?.call(value),
),
- if (_isCustomFee && !isEth)
+ if (isCustomFee && !isEth)
Padding(
padding: const EdgeInsets.only(bottom: 12, top: 16),
child: FeeSlider(
diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart
index 014f8d8..bd77a9b 100644
--- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart
+++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart
@@ -1038,7 +1038,7 @@ class _DesktopTokenSendState extends ConsumerState<DesktopTokenSend> {
walletId: walletId,
isToken: true,
onCustomFeeSliderChanged: (value) => {},
- onCustomFeeOptionChanged: (value) {
+ onCustomFeeOptionChanged: () {
ethFee = null;
},
onCustomEip1559FeeOptionChanged: (value) => ethFee = value,
diff --git a/lib/utilities/enums/fee_rate_type_enum.dart b/lib/utilities/enums/fee_rate_type_enum.dart
index 0ad32f1..a434835 100644
--- a/lib/utilities/enums/fee_rate_type_enum.dart
+++ b/lib/utilities/enums/fee_rate_type_enum.dart
@@ -11,6 +11,10 @@
enum FeeRateType { fast, average, slow, custom }
extension FeeRateTypeExt on FeeRateType {
+ bool get isCustom => this == FeeRateType.custom;
+
+ int? customSatsPerVByte(int satsPerVByte) => isCustom ? satsPerVByte : null;
+
String get prettyName {
switch (this) {
case FeeRateType.fast:
diff --git a/test/utilities/fee_rate_type_enum_test.dart b/test/utilities/fee_rate_type_enum_test.dart
new file mode 100644
index 0000000..424736a
--- /dev/null
+++ b/test/utilities/fee_rate_type_enum_test.dart
@@ -0,0 +1,20 @@
+import 'package:flutter_test/flutter_test.dart';
+import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart';
+
+void main() {
+ group("FeeRateTypeExt.customSatsPerVByte", () {
+ test("returns the selected rate for a custom fee", () {
+ expect(FeeRateType.custom.customSatsPerVByte(7), 7);
+ });
+
+ test("returns null for preset fees", () {
+ for (final feeRateType in [
+ FeeRateType.fast,
+ FeeRateType.average,
+ FeeRateType.slow,
+ ]) {
+ expect(feeRateType.customSatsPerVByte(7), isNull);
+ }
+ });
+ });
+}
Why this scored 30/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.