From fbb054f8f63842502ac731f35944bd30308dbfea Mon Sep 17 00:00:00 2001 From: sneurlax Date: Sun, 1 Mar 2026 21:55:29 -0600 Subject: [PATCH 1/7] feat: show sat/vB fee rate inline next to fee amount Replaces the separate sats/vByte row on the send confirmation screen with a wrapping amount + rate line that survives long amounts at large text scales. The rate is omitted when the virtual size is unknown or non positive, and uses the same locale decimal separator as the amount it sits beside. closes #413 --- .../send_view/confirm_transaction_view.dart | 92 +++++++------------ .../sub_widgets/fee_amount_with_rate.dart | 70 ++++++++++++++ .../send_view/fee_amount_with_rate_test.dart | 83 +++++++++++++++++ 3 files changed, 185 insertions(+), 60 deletions(-) create mode 100644 lib/pages/send_view/sub_widgets/fee_amount_with_rate.dart create mode 100644 test/pages/send_view/fee_amount_with_rate_test.dart diff --git a/lib/pages/send_view/confirm_transaction_view.dart b/lib/pages/send_view/confirm_transaction_view.dart index b54b3b071e..dd4f00cb84 100644 --- a/lib/pages/send_view/confirm_transaction_view.dart +++ b/lib/pages/send_view/confirm_transaction_view.dart @@ -67,6 +67,7 @@ import '../../wl_gen/interfaces/libepiccash_interface.dart'; import '../pinpad_views/lock_screen_view.dart'; import '../wallet_view/wallet_view.dart'; import 'sub_widgets/epic_slatepack_dialog.dart'; +import 'sub_widgets/fee_amount_with_rate.dart'; import 'sub_widgets/mwc_slatepack_dialog.dart'; import 'sub_widgets/sending_transaction_dialog.dart'; @@ -668,6 +669,10 @@ class _ConfirmTransactionViewState fee = widget.txData.fee; amountWithoutChange = widget.txData.amountWithoutChange!; } + final feeRateVSize = widget.txData.fee == null ? null : widget.txData.vSize; + final feeRateLocale = ref.watch( + localeServiceChangeNotifierProvider.select((value) => value.locale), + ); return ConditionalParent( condition: !isDesktop, @@ -809,14 +814,25 @@ class _ConfirmTransactionViewState child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - "Transaction fee", - style: STextStyles.smallMed12(context), + Expanded( + child: Text( + "Transaction fee", + style: STextStyles.smallMed12(context), + ), ), - SelectableText( - ref.watch(pAmountFormatter(coin)).format(fee!), - style: STextStyles.itemSubtitle12(context), - textAlign: TextAlign.right, + const SizedBox(width: 12), + Expanded( + child: FeeAmountWithRate( + formattedAmount: ref + .watch(pAmountFormatter(coin)) + .format(fee!), + feeSats: fee.raw.toInt(), + vSize: feeRateVSize, + locale: feeRateLocale, + amountStyle: STextStyles.itemSubtitle12(context), + rateStyle: STextStyles.smallMed12(context), + alignment: WrapAlignment.end, + ), ), ], ), @@ -836,25 +852,6 @@ class _ConfirmTransactionViewState ], ), ), - if (widget.txData.fee != null && widget.txData.vSize != null) - const SizedBox(height: 12), - if (widget.txData.fee != null && widget.txData.vSize != null) - RoundedWhiteContainer( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "sats/vByte", - style: STextStyles.smallMed12(context), - ), - const SizedBox(height: 4), - SelectableText( - "~${fee!.raw.toInt() ~/ widget.txData.vSize!}", - style: STextStyles.itemSubtitle12(context), - ), - ], - ), - ), if ((coin is Epiccash || coin is Mimblewimblecoin) && widget.txData.noteOnChain!.isNotEmpty) const SizedBox(height: 12), @@ -1388,40 +1385,15 @@ class _ConfirmTransactionViewState color: Theme.of( context, ).extension()!.textFieldDefaultBG, - child: SelectableText( - ref.watch(pAmountFormatter(coin)).format(fee!), - style: STextStyles.itemSubtitle(context), - ), - ), - ), - if (isDesktop && - !widget.isPaynymTransaction && - widget.txData.fee != null && - widget.txData.vSize != null) - Padding( - padding: const EdgeInsets.only(left: 32), - child: Text( - "sats/vByte", - style: STextStyles.desktopTextExtraExtraSmall(context), - ), - ), - if (isDesktop && - !widget.isPaynymTransaction && - widget.txData.fee != null && - widget.txData.vSize != null) - Padding( - padding: const EdgeInsets.only(top: 10, left: 32, right: 32), - child: RoundedContainer( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 18, - ), - color: Theme.of( - context, - ).extension()!.textFieldDefaultBG, - child: SelectableText( - "~${fee!.raw.toInt() ~/ widget.txData.vSize!}", - style: STextStyles.itemSubtitle(context), + child: FeeAmountWithRate( + formattedAmount: ref + .watch(pAmountFormatter(coin)) + .format(fee!), + feeSats: fee.raw.toInt(), + vSize: feeRateVSize, + locale: feeRateLocale, + amountStyle: STextStyles.itemSubtitle(context), + rateStyle: STextStyles.desktopTextExtraExtraSmall(context), ), ), ), diff --git a/lib/pages/send_view/sub_widgets/fee_amount_with_rate.dart b/lib/pages/send_view/sub_widgets/fee_amount_with_rate.dart new file mode 100644 index 0000000000..861dcd54a3 --- /dev/null +++ b/lib/pages/send_view/sub_widgets/fee_amount_with_rate.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; + +import '../../../utilities/util.dart'; + +/// Fee rate in sat/vB to one decimal, or null when [vSize] is unknown or non +/// positive (fixed fee coins, spark mints, account based coins). +/// +/// [locale] picks the decimal separator so the rate matches the amount +/// formatted next to it. +String? formatFeeRate({ + required int feeSats, + required int? vSize, + String? locale, +}) { + if (vSize == null || vSize <= 0) { + return null; + } + + final rate = (feeSats / vSize).toStringAsFixed(1); + if (locale == null) { + return rate; + } + + final separator = Util.getSymbolsFor(locale: locale)?.DECIMAL_SEP ?? '.'; + return separator == '.' ? rate : rate.replaceFirst('.', separator); +} + +class FeeAmountWithRate extends StatelessWidget { + const FeeAmountWithRate({ + super.key, + required this.formattedAmount, + required this.feeSats, + required this.vSize, + required this.amountStyle, + required this.rateStyle, + this.alignment = WrapAlignment.start, + this.locale, + }); + + final String formattedAmount; + final int feeSats; + final int? vSize; + final TextStyle amountStyle; + final TextStyle rateStyle; + final WrapAlignment alignment; + final String? locale; + + @override + Widget build(BuildContext context) { + final rate = formatFeeRate(feeSats: feeSats, vSize: vSize, locale: locale); + + return Wrap( + alignment: alignment, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + SelectableText( + formattedAmount, + style: amountStyle, + textAlign: TextAlign.right, + ), + if (rate != null) + Text( + ' (~$rate sat/vB)', + style: rateStyle, + textAlign: TextAlign.right, + ), + ], + ); + } +} diff --git a/test/pages/send_view/fee_amount_with_rate_test.dart b/test/pages/send_view/fee_amount_with_rate_test.dart new file mode 100644 index 0000000000..41b7d67e94 --- /dev/null +++ b/test/pages/send_view/fee_amount_with_rate_test.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stackwallet/pages/send_view/sub_widgets/fee_amount_with_rate.dart'; + +void main() { + test('formats positive fee rates to one decimal place', () { + expect(formatFeeRate(feeSats: 126, vSize: 100), '1.3'); + expect(formatFeeRate(feeSats: 1000, vSize: 3), '333.3'); + }); + + test('omits fee rates without a positive virtual size', () { + expect(formatFeeRate(feeSats: 126, vSize: null), isNull); + expect(formatFeeRate(feeSats: 126, vSize: 0), isNull); + expect(formatFeeRate(feeSats: 126, vSize: -1), isNull); + }); + + test('uses the locale decimal separator', () { + expect(formatFeeRate(feeSats: 126, vSize: 100), '1.3'); + expect(formatFeeRate(feeSats: 126, vSize: 100, locale: 'en_US'), '1.3'); + expect(formatFeeRate(feeSats: 126, vSize: 100, locale: 'de_DE'), '1,3'); + expect(formatFeeRate(feeSats: 126, vSize: 100, locale: 'fr'), '1,3'); + }); + + testWidgets('renders the rate in the same locale as the amount', ( + tester, + ) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: FeeAmountWithRate( + formattedAmount: '0,00000126 BTC', + feeSats: 126, + vSize: 100, + locale: 'de_DE', + amountStyle: TextStyle(fontSize: 12), + rateStyle: TextStyle(fontSize: 12), + ), + ), + ), + ); + + expect(find.text(' (~1,3 sat/vB)'), findsOneWidget); + }); + + testWidgets('wraps a long amount and rate at large text scale', ( + tester, + ) async { + await tester.pumpWidget( + const MaterialApp( + home: MediaQuery( + data: MediaQueryData( + size: Size(240, 600), + textScaler: TextScaler.linear(2), + ), + child: Scaffold( + body: SizedBox( + width: 240, + child: Row( + children: [ + Expanded(child: Text('Transaction fee')), + SizedBox(width: 12), + Expanded( + child: FeeAmountWithRate( + formattedAmount: '1234567890.12345678 BTC', + feeSats: 999999999, + vSize: 1, + amountStyle: TextStyle(fontSize: 12), + rateStyle: TextStyle(fontSize: 12), + alignment: WrapAlignment.end, + ), + ), + ], + ), + ), + ), + ), + ), + ); + + expect(find.text(' (~999999999.0 sat/vB)'), findsOneWidget); + expect(tester.takeException(), isNull); + }); +} From b7832848f776e7046200e664bea7edb709485fbc Mon Sep 17 00:00:00 2001 From: sneurlax Date: Sun, 1 Mar 2026 21:55:29 -0600 Subject: [PATCH 2/7] feat: show sat/vB fee rate inline next to fee amount Replaces the separate sats/vByte row on the send confirmation screen with a wrapping amount + rate line that survives long amounts at large text scales. The rate is omitted when the virtual size is unknown or non positive, and uses the same locale decimal separator as the amount it sits beside. closes #413 --- .../send_view/confirm_transaction_view.dart | 92 +++++++------------ .../sub_widgets/fee_amount_with_rate.dart | 70 ++++++++++++++ .../send_view/fee_amount_with_rate_test.dart | 83 +++++++++++++++++ 3 files changed, 185 insertions(+), 60 deletions(-) create mode 100644 lib/pages/send_view/sub_widgets/fee_amount_with_rate.dart create mode 100644 test/pages/send_view/fee_amount_with_rate_test.dart diff --git a/lib/pages/send_view/confirm_transaction_view.dart b/lib/pages/send_view/confirm_transaction_view.dart index a4886bae84..703d0c0645 100644 --- a/lib/pages/send_view/confirm_transaction_view.dart +++ b/lib/pages/send_view/confirm_transaction_view.dart @@ -67,6 +67,7 @@ import '../../wl_gen/interfaces/libepiccash_interface.dart'; import '../pinpad_views/lock_screen_view.dart'; import '../wallet_view/wallet_view.dart'; import 'sub_widgets/epic_slatepack_dialog.dart'; +import 'sub_widgets/fee_amount_with_rate.dart'; import 'sub_widgets/mwc_slatepack_dialog.dart'; import 'sub_widgets/sending_transaction_dialog.dart'; @@ -677,6 +678,10 @@ class _ConfirmTransactionViewState fee = widget.txData.fee; amountWithoutChange = widget.txData.amountWithoutChange!; } + final feeRateVSize = widget.txData.fee == null ? null : widget.txData.vSize; + final feeRateLocale = ref.watch( + localeServiceChangeNotifierProvider.select((value) => value.locale), + ); return ConditionalParent( condition: !isDesktop, @@ -822,14 +827,25 @@ class _ConfirmTransactionViewState child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - "Transaction fee", - style: STextStyles.smallMed12(context), + Expanded( + child: Text( + "Transaction fee", + style: STextStyles.smallMed12(context), + ), ), - SelectableText( - ref.watch(pAmountFormatter(coin)).format(fee!), - style: STextStyles.itemSubtitle12(context), - textAlign: TextAlign.right, + const SizedBox(width: 12), + Expanded( + child: FeeAmountWithRate( + formattedAmount: ref + .watch(pAmountFormatter(coin)) + .format(fee!), + feeSats: fee.raw.toInt(), + vSize: feeRateVSize, + locale: feeRateLocale, + amountStyle: STextStyles.itemSubtitle12(context), + rateStyle: STextStyles.smallMed12(context), + alignment: WrapAlignment.end, + ), ), ], ), @@ -849,25 +865,6 @@ class _ConfirmTransactionViewState ], ), ), - if (widget.txData.fee != null && widget.txData.vSize != null) - const SizedBox(height: 12), - if (widget.txData.fee != null && widget.txData.vSize != null) - RoundedWhiteContainer( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "sats/vByte", - style: STextStyles.smallMed12(context), - ), - const SizedBox(height: 4), - SelectableText( - "~${fee!.raw.toInt() ~/ widget.txData.vSize!}", - style: STextStyles.itemSubtitle12(context), - ), - ], - ), - ), if ((coin is Epiccash || coin is Mimblewimblecoin) && widget.txData.noteOnChain!.isNotEmpty) const SizedBox(height: 12), @@ -1405,40 +1402,15 @@ class _ConfirmTransactionViewState color: Theme.of(context) .extension()! .textFieldDefaultBG, - child: SelectableText( - ref.watch(pAmountFormatter(coin)).format(fee!), - style: STextStyles.itemSubtitle(context), - ), - ), - ), - if (isDesktop && - !widget.isPaynymTransaction && - widget.txData.fee != null && - widget.txData.vSize != null) - Padding( - padding: const EdgeInsets.only(left: 32), - child: Text( - "sats/vByte", - style: STextStyles.desktopTextExtraExtraSmall(context), - ), - ), - if (isDesktop && - !widget.isPaynymTransaction && - widget.txData.fee != null && - widget.txData.vSize != null) - Padding( - padding: const EdgeInsets.only(top: 10, left: 32, right: 32), - child: RoundedContainer( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 18, - ), - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, - child: SelectableText( - "~${fee!.raw.toInt() ~/ widget.txData.vSize!}", - style: STextStyles.itemSubtitle(context), + child: FeeAmountWithRate( + formattedAmount: ref + .watch(pAmountFormatter(coin)) + .format(fee!), + feeSats: fee.raw.toInt(), + vSize: feeRateVSize, + locale: feeRateLocale, + amountStyle: STextStyles.itemSubtitle(context), + rateStyle: STextStyles.desktopTextExtraExtraSmall(context), ), ), ), diff --git a/lib/pages/send_view/sub_widgets/fee_amount_with_rate.dart b/lib/pages/send_view/sub_widgets/fee_amount_with_rate.dart new file mode 100644 index 0000000000..861dcd54a3 --- /dev/null +++ b/lib/pages/send_view/sub_widgets/fee_amount_with_rate.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; + +import '../../../utilities/util.dart'; + +/// Fee rate in sat/vB to one decimal, or null when [vSize] is unknown or non +/// positive (fixed fee coins, spark mints, account based coins). +/// +/// [locale] picks the decimal separator so the rate matches the amount +/// formatted next to it. +String? formatFeeRate({ + required int feeSats, + required int? vSize, + String? locale, +}) { + if (vSize == null || vSize <= 0) { + return null; + } + + final rate = (feeSats / vSize).toStringAsFixed(1); + if (locale == null) { + return rate; + } + + final separator = Util.getSymbolsFor(locale: locale)?.DECIMAL_SEP ?? '.'; + return separator == '.' ? rate : rate.replaceFirst('.', separator); +} + +class FeeAmountWithRate extends StatelessWidget { + const FeeAmountWithRate({ + super.key, + required this.formattedAmount, + required this.feeSats, + required this.vSize, + required this.amountStyle, + required this.rateStyle, + this.alignment = WrapAlignment.start, + this.locale, + }); + + final String formattedAmount; + final int feeSats; + final int? vSize; + final TextStyle amountStyle; + final TextStyle rateStyle; + final WrapAlignment alignment; + final String? locale; + + @override + Widget build(BuildContext context) { + final rate = formatFeeRate(feeSats: feeSats, vSize: vSize, locale: locale); + + return Wrap( + alignment: alignment, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + SelectableText( + formattedAmount, + style: amountStyle, + textAlign: TextAlign.right, + ), + if (rate != null) + Text( + ' (~$rate sat/vB)', + style: rateStyle, + textAlign: TextAlign.right, + ), + ], + ); + } +} diff --git a/test/pages/send_view/fee_amount_with_rate_test.dart b/test/pages/send_view/fee_amount_with_rate_test.dart new file mode 100644 index 0000000000..41b7d67e94 --- /dev/null +++ b/test/pages/send_view/fee_amount_with_rate_test.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stackwallet/pages/send_view/sub_widgets/fee_amount_with_rate.dart'; + +void main() { + test('formats positive fee rates to one decimal place', () { + expect(formatFeeRate(feeSats: 126, vSize: 100), '1.3'); + expect(formatFeeRate(feeSats: 1000, vSize: 3), '333.3'); + }); + + test('omits fee rates without a positive virtual size', () { + expect(formatFeeRate(feeSats: 126, vSize: null), isNull); + expect(formatFeeRate(feeSats: 126, vSize: 0), isNull); + expect(formatFeeRate(feeSats: 126, vSize: -1), isNull); + }); + + test('uses the locale decimal separator', () { + expect(formatFeeRate(feeSats: 126, vSize: 100), '1.3'); + expect(formatFeeRate(feeSats: 126, vSize: 100, locale: 'en_US'), '1.3'); + expect(formatFeeRate(feeSats: 126, vSize: 100, locale: 'de_DE'), '1,3'); + expect(formatFeeRate(feeSats: 126, vSize: 100, locale: 'fr'), '1,3'); + }); + + testWidgets('renders the rate in the same locale as the amount', ( + tester, + ) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: FeeAmountWithRate( + formattedAmount: '0,00000126 BTC', + feeSats: 126, + vSize: 100, + locale: 'de_DE', + amountStyle: TextStyle(fontSize: 12), + rateStyle: TextStyle(fontSize: 12), + ), + ), + ), + ); + + expect(find.text(' (~1,3 sat/vB)'), findsOneWidget); + }); + + testWidgets('wraps a long amount and rate at large text scale', ( + tester, + ) async { + await tester.pumpWidget( + const MaterialApp( + home: MediaQuery( + data: MediaQueryData( + size: Size(240, 600), + textScaler: TextScaler.linear(2), + ), + child: Scaffold( + body: SizedBox( + width: 240, + child: Row( + children: [ + Expanded(child: Text('Transaction fee')), + SizedBox(width: 12), + Expanded( + child: FeeAmountWithRate( + formattedAmount: '1234567890.12345678 BTC', + feeSats: 999999999, + vSize: 1, + amountStyle: TextStyle(fontSize: 12), + rateStyle: TextStyle(fontSize: 12), + alignment: WrapAlignment.end, + ), + ), + ], + ), + ), + ), + ), + ), + ); + + expect(find.text(' (~999999999.0 sat/vB)'), findsOneWidget); + expect(tester.takeException(), isNull); + }); +} From 8e75c3e00c82ee0e336f33ffc399c65998f9a10d Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 6 Oct 2026 21:55:01 -0500 Subject: [PATCH 3/7] style(names): dart format name confirm transaction views --- .../confirm_name_transaction_view.dart | 471 ++++++++---------- .../confirm_spark_name_transaction_view.dart | 110 ++-- 2 files changed, 271 insertions(+), 310 deletions(-) diff --git a/lib/pages/namecoin_names/confirm_name_transaction_view.dart b/lib/pages/namecoin_names/confirm_name_transaction_view.dart index ff2f0b74a4..91dce9e608 100644 --- a/lib/pages/namecoin_names/confirm_name_transaction_view.dart +++ b/lib/pages/namecoin_names/confirm_name_transaction_view.dart @@ -225,10 +225,9 @@ class _ConfirmNameTransactionViewState child: Text( "Ok", style: STextStyles.button(context).copyWith( - color: - Theme.of( - context, - ).extension()!.accentColorDark, + color: Theme.of(context) + .extension()! + .accentColorDark, ), ), onPressed: () { @@ -273,81 +272,76 @@ class _ConfirmNameTransactionViewState return ConditionalParent( condition: !isDesktop, - builder: - (child) => Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - backgroundColor: - Theme.of(context).extension()!.background, - leading: AppBarBackButton( - onPressed: () async { - // if (FocusScope.of(context).hasFocus) { - // FocusScope.of(context).unfocus(); - // await Future.delayed(Duration(milliseconds: 50)); - // } - Navigator.of(context).pop(); - }, - ), - title: Text( - "Confirm transaction", - style: STextStyles.navBarTitle(context), - ), - ), - body: SafeArea( - child: LayoutBuilder( - builder: (builderContext, constraints) { - return Padding( - padding: const EdgeInsets.only( - left: 12, - top: 12, - right: 12, + builder: (child) => Background( + child: Scaffold( + backgroundColor: Theme.of(context) + .extension()! + .background, + appBar: AppBar( + backgroundColor: Theme.of(context) + .extension()! + .background, + leading: AppBarBackButton( + onPressed: () async { + // if (FocusScope.of(context).hasFocus) { + // FocusScope.of(context).unfocus(); + // await Future.delayed(Duration(milliseconds: 50)); + // } + Navigator.of(context).pop(); + }, + ), + title: Text( + "Confirm transaction", + style: STextStyles.navBarTitle(context), + ), + ), + body: SafeArea( + child: LayoutBuilder( + builder: (builderContext, constraints) { + return Padding( + padding: const EdgeInsets.only(left: 12, top: 12, right: 12), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 24, ), - child: SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight - 24, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(4), - child: child, - ), - ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: child, ), ), - ); - }, - ), - ), + ), + ), + ); + }, ), ), + ), + ), child: ConditionalParent( condition: isDesktop, - builder: - (child) => Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - mainAxisSize: MainAxisSize.min, + builder: (child) => Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + Row( children: [ - Row( - children: [ - AppBarBackButton( - size: 40, - iconSize: 24, - onPressed: - () => - Navigator.of(context, rootNavigator: true).pop(), - ), - Text( - "Confirm transaction", - style: STextStyles.desktopH3(context), - ), - ], + AppBarBackButton( + size: 40, + iconSize: 24, + onPressed: () => + Navigator.of(context, rootNavigator: true).pop(), + ), + Text( + "Confirm transaction", + style: STextStyles.desktopH3(context), ), - Flexible(child: SingleChildScrollView(child: child)), ], ), + Flexible(child: SingleChildScrollView(child: child)), + ], + ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: isDesktop ? MainAxisSize.min : MainAxisSize.max, @@ -487,18 +481,18 @@ class _ConfirmNameTransactionViewState ), child: RoundedWhiteContainer( padding: const EdgeInsets.all(0), - borderColor: - Theme.of(context).extension()!.background, + borderColor: Theme.of(context) + .extension()! + .background, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Container( decoration: BoxDecoration( - color: - Theme.of( - context, - ).extension()!.background, + color: Theme.of(context) + .extension()! + .background, borderRadius: BorderRadius.only( topLeft: Radius.circular( Constants.size.circularBorderRadius, @@ -550,24 +544,23 @@ class _ConfirmNameTransactionViewState const SizedBox(height: 2), SelectableText( widget.txData.opNameState!.name, - style: STextStyles.desktopTextExtraExtraSmall( - context, - ).copyWith( - color: - Theme.of( - context, - ).extension()!.textDark, - ), + style: + STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: Theme.of(context) + .extension()! + .textDark, + ), ), ], ), ), Container( height: 1, - color: - Theme.of( - context, - ).extension()!.background, + color: Theme.of(context) + .extension()! + .background, ), Padding( padding: const EdgeInsets.all(12), @@ -584,14 +577,14 @@ class _ConfirmNameTransactionViewState const SizedBox(height: 2), SelectableText( widget.txData.opNameState!.value, - style: STextStyles.desktopTextExtraExtraSmall( - context, - ).copyWith( - color: - Theme.of( - context, - ).extension()!.textDark, - ), + style: + STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: Theme.of(context) + .extension()! + .textDark, + ), ), ], ), @@ -609,14 +602,12 @@ class _ConfirmNameTransactionViewState children: [ SelectableText( "Note (optional)", - style: STextStyles.desktopTextExtraSmall( - context, - ).copyWith( - color: - Theme.of(context) + style: STextStyles.desktopTextExtraSmall(context) + .copyWith( + color: Theme.of(context) .extension()! .textFieldActiveSearchIconRight, - ), + ), textAlign: TextAlign.left, ), const SizedBox(height: 10), @@ -631,49 +622,48 @@ class _ConfirmNameTransactionViewState enableSuggestions: isDesktop ? false : true, controller: noteController, focusNode: _noteFocusNode, - style: STextStyles.desktopTextExtraSmall( - context, - ).copyWith( - color: - Theme.of( - context, - ).extension()!.textFieldActiveText, - height: 1.8, - ), + style: STextStyles.desktopTextExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension()! + .textFieldActiveText, + height: 1.8, + ), onChanged: (_) => setState(() {}), - decoration: standardInputDecoration( - "Type something...", - _noteFocusNode, - context, - desktopMed: true, - ).copyWith( - contentPadding: const EdgeInsets.only( - left: 16, - top: 11, - bottom: 12, - right: 5, - ), - suffixIcon: - noteController.text.isNotEmpty + decoration: + standardInputDecoration( + "Type something...", + _noteFocusNode, + context, + desktopMed: true, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 11, + bottom: 12, + right: 5, + ), + suffixIcon: noteController.text.isNotEmpty ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState( - () => noteController.text = "", - ); - }, - ), - ], + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState( + () => + noteController.text = "", + ); + }, + ), + ], + ), ), - ), - ) + ) : null, - ), + ), ), ), const SizedBox(height: 20), @@ -697,10 +687,9 @@ class _ConfirmNameTransactionViewState horizontal: 16, vertical: 18, ), - color: - Theme.of( - context, - ).extension()!.textFieldDefaultBG, + color: Theme.of(context) + .extension()! + .textFieldDefaultBG, child: Builder( builder: (context) { final externalCalls = ref.watch( @@ -711,21 +700,17 @@ class _ConfirmNameTransactionViewState String fiatAmount = "N/A"; if (externalCalls) { - final price = - ref - .read(priceAnd24hChangeNotifierProvider) - .getPrice(coin) - ?.value; + final price = ref + .read(priceAnd24hChangeNotifierProvider) + .getPrice(coin) + ?.value; if (price != null && price > Decimal.zero) { fiatAmount = (amountWithoutChange.decimal * price) .toAmount(fractionDigits: 2) .fiatString( - locale: - ref - .read( - localeServiceChangeNotifierProvider, - ) - .locale, + locale: ref + .read(localeServiceChangeNotifierProvider) + .locale, ); } } @@ -770,10 +755,9 @@ class _ConfirmNameTransactionViewState horizontal: 16, vertical: 18, ), - color: - Theme.of( - context, - ).extension()!.textFieldDefaultBG, + color: Theme.of(context) + .extension()! + .textFieldDefaultBG, child: SelectableText( widget.txData.recipients!.first.address, style: STextStyles.itemSubtitle(context), @@ -797,10 +781,9 @@ class _ConfirmNameTransactionViewState horizontal: 16, vertical: 18, ), - color: - Theme.of( - context, - ).extension()!.textFieldDefaultBG, + color: Theme.of(context) + .extension()! + .textFieldDefaultBG, child: SelectableText( ref.watch(pAmountFormatter(coin)).format(fee!), style: STextStyles.itemSubtitle(context), @@ -827,10 +810,9 @@ class _ConfirmNameTransactionViewState horizontal: 16, vertical: 18, ), - color: - Theme.of( - context, - ).extension()!.textFieldDefaultBG, + color: Theme.of(context) + .extension()! + .textFieldDefaultBG, child: SelectableText( "~${fee!.raw.toInt() ~/ widget.txData.vSize!}", style: STextStyles.itemSubtitle(context), @@ -840,64 +822,50 @@ class _ConfirmNameTransactionViewState if (!isDesktop) const Spacer(), SizedBox(height: isDesktop ? 23 : 12), Padding( - padding: - isDesktop - ? const EdgeInsets.symmetric(horizontal: 32) - : const EdgeInsets.all(0), + padding: isDesktop + ? const EdgeInsets.symmetric(horizontal: 32) + : const EdgeInsets.all(0), child: RoundedContainer( - padding: - isDesktop - ? const EdgeInsets.symmetric( - horizontal: 16, - vertical: 18, - ) - : const EdgeInsets.all(12), - color: - Theme.of( - context, - ).extension()!.snackBarBackSuccess, + padding: isDesktop + ? const EdgeInsets.symmetric(horizontal: 16, vertical: 18) + : const EdgeInsets.all(12), + color: Theme.of(context) + .extension()! + .snackBarBackSuccess, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( isDesktop ? "Total amount to send" : "Total amount", - style: - isDesktop - ? STextStyles.desktopTextExtraExtraSmall( - context, - ).copyWith( - color: - Theme.of(context) - .extension()! - .textConfirmTotalAmount, - ) - : STextStyles.titleBold12(context).copyWith( - color: - Theme.of(context) - .extension()! - .textConfirmTotalAmount, - ), + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension()! + .textConfirmTotalAmount, + ) + : STextStyles.titleBold12(context).copyWith( + color: Theme.of(context) + .extension()! + .textConfirmTotalAmount, + ), ), SelectableText( ref .watch(pAmountFormatter(coin)) .format(amountWithoutChange + fee!), - style: - isDesktop - ? STextStyles.desktopTextExtraExtraSmall( - context, - ).copyWith( - color: - Theme.of(context) - .extension()! - .textConfirmTotalAmount, - ) - : STextStyles.itemSubtitle12(context).copyWith( - color: - Theme.of(context) - .extension()! - .textConfirmTotalAmount, - ), + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension()! + .textConfirmTotalAmount, + ) + : STextStyles.itemSubtitle12(context).copyWith( + color: Theme.of(context) + .extension()! + .textConfirmTotalAmount, + ), textAlign: TextAlign.right, ), ], @@ -906,10 +874,9 @@ class _ConfirmNameTransactionViewState ), SizedBox(height: isDesktop ? 28 : 16), Padding( - padding: - isDesktop - ? const EdgeInsets.symmetric(horizontal: 32) - : const EdgeInsets.all(0), + padding: isDesktop + ? const EdgeInsets.symmetric(horizontal: 32) + : const EdgeInsets.all(0), child: PrimaryButton( label: "Send", buttonHeight: isDesktop ? ButtonHeight.l : null, @@ -919,28 +886,27 @@ class _ConfirmNameTransactionViewState if (isDesktop) { unlocked = await showDialog( context: context, - builder: - (context) => DesktopDialog( - maxWidth: 580, - maxHeight: double.infinity, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [DesktopDialogCloseButton()], - ), - Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - bottom: 32, - ), - child: DesktopAuthSend(coin: coin), - ), - ], + builder: (context) => DesktopDialog( + maxWidth: 580, + maxHeight: double.infinity, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [DesktopDialogCloseButton()], ), - ), + Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: DesktopAuthSend(coin: coin), + ), + ], + ), + ), ); } else { unlocked = await Navigator.push( @@ -948,18 +914,16 @@ class _ConfirmNameTransactionViewState RouteGenerator.getRoute( shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, - builder: - (_) => const LockscreenView( - showBackButton: true, - popOnSuccess: true, - routeOnSuccessArguments: true, - routeOnSuccess: "", - biometricsCancelButtonString: "CANCEL", - biometricsLocalizedReason: - "Authenticate to send transaction", - biometricsAuthenticationTitle: - "Confirm Transaction", - ), + builder: (_) => const LockscreenView( + showBackButton: true, + popOnSuccess: true, + routeOnSuccessArguments: true, + routeOnSuccess: "", + biometricsCancelButtonString: "CANCEL", + biometricsLocalizedReason: + "Authenticate to send transaction", + biometricsAuthenticationTitle: "Confirm Transaction", + ), settings: const RouteSettings( name: "/confirmsendlockscreen", ), @@ -975,10 +939,9 @@ class _ConfirmNameTransactionViewState unawaited( showFloatingFlushBar( type: FlushBarType.warning, - message: - Util.isDesktop - ? "Invalid passphrase" - : "Invalid PIN", + message: Util.isDesktop + ? "Invalid passphrase" + : "Invalid PIN", context: context, ), ); diff --git a/lib/pages/spark_names/confirm_spark_name_transaction_view.dart b/lib/pages/spark_names/confirm_spark_name_transaction_view.dart index 1c466c7ea3..48bd2e6484 100644 --- a/lib/pages/spark_names/confirm_spark_name_transaction_view.dart +++ b/lib/pages/spark_names/confirm_spark_name_transaction_view.dart @@ -215,9 +215,9 @@ class _ConfirmSparkNameTransactionViewState child: Text( "Ok", style: STextStyles.button(context).copyWith( - color: Theme.of( - context, - ).extension()!.accentColorDark, + color: Theme.of(context) + .extension()! + .accentColorDark, ), ), onPressed: () { @@ -264,13 +264,13 @@ class _ConfirmSparkNameTransactionViewState condition: !isDesktop, builder: (child) => Background( child: Scaffold( - backgroundColor: Theme.of( - context, - ).extension()!.background, + backgroundColor: Theme.of(context) + .extension()! + .background, appBar: AppBar( - backgroundColor: Theme.of( - context, - ).extension()!.background, + backgroundColor: Theme.of(context) + .extension()! + .background, leading: AppBarBackButton( onPressed: () async { // if (FocusScope.of(context).hasFocus) { @@ -477,18 +477,18 @@ class _ConfirmSparkNameTransactionViewState ), child: RoundedWhiteContainer( padding: const EdgeInsets.all(0), - borderColor: Theme.of( - context, - ).extension()!.background, + borderColor: Theme.of(context) + .extension()! + .background, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Container( decoration: BoxDecoration( - color: Theme.of( - context, - ).extension()!.background, + color: Theme.of(context) + .extension()! + .background, borderRadius: BorderRadius.only( topLeft: Radius.circular( Constants.size.circularBorderRadius, @@ -544,9 +544,9 @@ class _ConfirmSparkNameTransactionViewState STextStyles.desktopTextExtraExtraSmall( context, ).copyWith( - color: Theme.of( - context, - ).extension()!.textDark, + color: Theme.of(context) + .extension()! + .textDark, ), ), ], @@ -554,9 +554,9 @@ class _ConfirmSparkNameTransactionViewState ), Container( height: 1, - color: Theme.of( - context, - ).extension()!.background, + color: Theme.of(context) + .extension()! + .background, ), Padding( padding: const EdgeInsets.all(12), @@ -577,9 +577,9 @@ class _ConfirmSparkNameTransactionViewState STextStyles.desktopTextExtraExtraSmall( context, ).copyWith( - color: Theme.of( - context, - ).extension()!.textDark, + color: Theme.of(context) + .extension()! + .textDark, ), ), ], @@ -620,9 +620,9 @@ class _ConfirmSparkNameTransactionViewState focusNode: _noteFocusNode, style: STextStyles.desktopTextExtraSmall(context) .copyWith( - color: Theme.of( - context, - ).extension()!.textFieldActiveText, + color: Theme.of(context) + .extension()! + .textFieldActiveText, height: 1.8, ), onChanged: (_) => setState(() {}), @@ -683,9 +683,9 @@ class _ConfirmSparkNameTransactionViewState horizontal: 16, vertical: 18, ), - color: Theme.of( - context, - ).extension()!.textFieldDefaultBG, + color: Theme.of(context) + .extension()! + .textFieldDefaultBG, child: Builder( builder: (context) { final externalCalls = ref.watch( @@ -751,9 +751,9 @@ class _ConfirmSparkNameTransactionViewState horizontal: 16, vertical: 18, ), - color: Theme.of( - context, - ).extension()!.textFieldDefaultBG, + color: Theme.of(context) + .extension()! + .textFieldDefaultBG, child: SelectableText( widget.txData.recipients!.first.address, style: STextStyles.itemSubtitle(context), @@ -777,9 +777,9 @@ class _ConfirmSparkNameTransactionViewState horizontal: 16, vertical: 18, ), - color: Theme.of( - context, - ).extension()!.textFieldDefaultBG, + color: Theme.of(context) + .extension()! + .textFieldDefaultBG, child: SelectableText( ref.watch(pAmountFormatter(coin)).format(fee!), style: STextStyles.itemSubtitle(context), @@ -806,9 +806,9 @@ class _ConfirmSparkNameTransactionViewState horizontal: 16, vertical: 18, ), - color: Theme.of( - context, - ).extension()!.textFieldDefaultBG, + color: Theme.of(context) + .extension()! + .textFieldDefaultBG, child: SelectableText( "~${fee!.raw.toInt() ~/ widget.txData.vSize!}", style: STextStyles.itemSubtitle(context), @@ -825,22 +825,21 @@ class _ConfirmSparkNameTransactionViewState padding: isDesktop ? const EdgeInsets.symmetric(horizontal: 16, vertical: 18) : const EdgeInsets.all(12), - color: Theme.of( - context, - ).extension()!.snackBarBackSuccess, + color: Theme.of(context) + .extension()! + .snackBarBackSuccess, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( isDesktop ? "Total amount to send" : "Total amount", style: isDesktop - ? STextStyles.desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension()! - .textConfirmTotalAmount, - ) + ? STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension()! + .textConfirmTotalAmount, + ) : STextStyles.titleBold12(context).copyWith( color: Theme.of(context) .extension()! @@ -852,13 +851,12 @@ class _ConfirmSparkNameTransactionViewState .watch(pAmountFormatter(coin)) .format(amountWithoutChange + fee!), style: isDesktop - ? STextStyles.desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension()! - .textConfirmTotalAmount, - ) + ? STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension()! + .textConfirmTotalAmount, + ) : STextStyles.itemSubtitle12(context).copyWith( color: Theme.of(context) .extension()! From 31d8cbc7f946b6f8806495caac0df5f21813cf40 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 6 Oct 2026 21:55:29 -0500 Subject: [PATCH 4/7] feat(names): show sat/vB fee rate inline on name confirm screens Use FeeAmountWithRate in the namecoin and spark name confirm views in place of the separate integer sats/vByte rows. --- .../confirm_name_transaction_view.dart | 90 +++++++------------ .../confirm_spark_name_transaction_view.dart | 90 +++++++------------ 2 files changed, 64 insertions(+), 116 deletions(-) diff --git a/lib/pages/namecoin_names/confirm_name_transaction_view.dart b/lib/pages/namecoin_names/confirm_name_transaction_view.dart index 91dce9e608..44821c7dde 100644 --- a/lib/pages/namecoin_names/confirm_name_transaction_view.dart +++ b/lib/pages/namecoin_names/confirm_name_transaction_view.dart @@ -49,6 +49,7 @@ import '../../widgets/stack_dialog.dart'; import '../../widgets/stack_text_field.dart'; import '../../widgets/textfield_icon_button.dart'; import '../pinpad_views/lock_screen_view.dart'; +import '../send_view/sub_widgets/fee_amount_with_rate.dart'; import '../send_view/sub_widgets/sending_transaction_dialog.dart'; class ConfirmNameTransactionView extends ConsumerStatefulWidget { @@ -269,6 +270,10 @@ class _ConfirmNameTransactionViewState final fee = widget.txData.fee; final amountWithoutChange = widget.txData.amountWithoutChange!; + final feeRateVSize = widget.txData.fee == null ? null : widget.txData.vSize; + final feeRateLocale = ref.watch( + localeServiceChangeNotifierProvider.select((value) => value.locale), + ); return ConditionalParent( condition: !isDesktop, @@ -420,37 +425,29 @@ class _ConfirmNameTransactionViewState child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - "Transaction fee", - style: STextStyles.smallMed12(context), + Expanded( + child: Text( + "Transaction fee", + style: STextStyles.smallMed12(context), + ), ), - SelectableText( - ref.watch(pAmountFormatter(coin)).format(fee!), - style: STextStyles.itemSubtitle12(context), - textAlign: TextAlign.right, + const SizedBox(width: 12), + Expanded( + child: FeeAmountWithRate( + formattedAmount: ref + .watch(pAmountFormatter(coin)) + .format(fee!), + feeSats: fee.raw.toInt(), + vSize: feeRateVSize, + locale: feeRateLocale, + amountStyle: STextStyles.itemSubtitle12(context), + rateStyle: STextStyles.smallMed12(context), + alignment: WrapAlignment.end, + ), ), ], ), ), - if (widget.txData.fee != null && widget.txData.vSize != null) - const SizedBox(height: 12), - if (widget.txData.fee != null && widget.txData.vSize != null) - RoundedWhiteContainer( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "sats/vByte", - style: STextStyles.smallMed12(context), - ), - const SizedBox(height: 4), - SelectableText( - "~${fee.raw.toInt() ~/ widget.txData.vSize!}", - style: STextStyles.itemSubtitle12(context), - ), - ], - ), - ), if (widget.txData.note != null && widget.txData.note!.isNotEmpty) const SizedBox(height: 12), @@ -784,38 +781,15 @@ class _ConfirmNameTransactionViewState color: Theme.of(context) .extension()! .textFieldDefaultBG, - child: SelectableText( - ref.watch(pAmountFormatter(coin)).format(fee!), - style: STextStyles.itemSubtitle(context), - ), - ), - ), - if (isDesktop && - widget.txData.fee != null && - widget.txData.vSize != null) - Padding( - padding: const EdgeInsets.only(top: 16, left: 32), - child: Text( - "sats/vByte", - style: STextStyles.desktopTextExtraExtraSmall(context), - ), - ), - if (isDesktop && - widget.txData.fee != null && - widget.txData.vSize != null) - Padding( - padding: const EdgeInsets.only(top: 10, left: 32, right: 32), - child: RoundedContainer( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 18, - ), - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, - child: SelectableText( - "~${fee!.raw.toInt() ~/ widget.txData.vSize!}", - style: STextStyles.itemSubtitle(context), + child: FeeAmountWithRate( + formattedAmount: ref + .watch(pAmountFormatter(coin)) + .format(fee!), + feeSats: fee.raw.toInt(), + vSize: feeRateVSize, + locale: feeRateLocale, + amountStyle: STextStyles.itemSubtitle(context), + rateStyle: STextStyles.desktopTextExtraExtraSmall(context), ), ), ), diff --git a/lib/pages/spark_names/confirm_spark_name_transaction_view.dart b/lib/pages/spark_names/confirm_spark_name_transaction_view.dart index 48bd2e6484..4c792147e4 100644 --- a/lib/pages/spark_names/confirm_spark_name_transaction_view.dart +++ b/lib/pages/spark_names/confirm_spark_name_transaction_view.dart @@ -46,6 +46,7 @@ import '../../widgets/stack_dialog.dart'; import '../../widgets/stack_text_field.dart'; import '../../widgets/textfield_icon_button.dart'; import '../pinpad_views/lock_screen_view.dart'; +import '../send_view/sub_widgets/fee_amount_with_rate.dart'; import '../send_view/sub_widgets/sending_transaction_dialog.dart'; class ConfirmSparkNameTransactionView extends ConsumerStatefulWidget { @@ -259,6 +260,10 @@ class _ConfirmSparkNameTransactionViewState final fee = widget.txData.fee; final amountWithoutChange = widget.txData.amountWithoutChange!; + final feeRateVSize = widget.txData.fee == null ? null : widget.txData.vSize; + final feeRateLocale = ref.watch( + localeServiceChangeNotifierProvider.select((value) => value.locale), + ); return ConditionalParent( condition: !isDesktop, @@ -416,37 +421,29 @@ class _ConfirmSparkNameTransactionViewState child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - "Transaction fee", - style: STextStyles.smallMed12(context), + Expanded( + child: Text( + "Transaction fee", + style: STextStyles.smallMed12(context), + ), ), - SelectableText( - ref.watch(pAmountFormatter(coin)).format(fee!), - style: STextStyles.itemSubtitle12(context), - textAlign: TextAlign.right, + const SizedBox(width: 12), + Expanded( + child: FeeAmountWithRate( + formattedAmount: ref + .watch(pAmountFormatter(coin)) + .format(fee!), + feeSats: fee.raw.toInt(), + vSize: feeRateVSize, + locale: feeRateLocale, + amountStyle: STextStyles.itemSubtitle12(context), + rateStyle: STextStyles.smallMed12(context), + alignment: WrapAlignment.end, + ), ), ], ), ), - if (widget.txData.fee != null && widget.txData.vSize != null) - const SizedBox(height: 12), - if (widget.txData.fee != null && widget.txData.vSize != null) - RoundedWhiteContainer( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "sats/vByte", - style: STextStyles.smallMed12(context), - ), - const SizedBox(height: 4), - SelectableText( - "~${fee.raw.toInt() ~/ widget.txData.vSize!}", - style: STextStyles.itemSubtitle12(context), - ), - ], - ), - ), if (widget.txData.note != null && widget.txData.note!.isNotEmpty) const SizedBox(height: 12), @@ -780,38 +777,15 @@ class _ConfirmSparkNameTransactionViewState color: Theme.of(context) .extension()! .textFieldDefaultBG, - child: SelectableText( - ref.watch(pAmountFormatter(coin)).format(fee!), - style: STextStyles.itemSubtitle(context), - ), - ), - ), - if (isDesktop && - widget.txData.fee != null && - widget.txData.vSize != null) - Padding( - padding: const EdgeInsets.only(top: 16, left: 32), - child: Text( - "sats/vByte", - style: STextStyles.desktopTextExtraExtraSmall(context), - ), - ), - if (isDesktop && - widget.txData.fee != null && - widget.txData.vSize != null) - Padding( - padding: const EdgeInsets.only(top: 10, left: 32, right: 32), - child: RoundedContainer( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 18, - ), - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, - child: SelectableText( - "~${fee!.raw.toInt() ~/ widget.txData.vSize!}", - style: STextStyles.itemSubtitle(context), + child: FeeAmountWithRate( + formattedAmount: ref + .watch(pAmountFormatter(coin)) + .format(fee!), + feeSats: fee.raw.toInt(), + vSize: feeRateVSize, + locale: feeRateLocale, + amountStyle: STextStyles.itemSubtitle(context), + rateStyle: STextStyles.desktopTextExtraExtraSmall(context), ), ), ), From 0d1efce0efb745021766a322511d9bad27bfd84f Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 10 Nov 2026 13:57:34 -0600 Subject: [PATCH 5/7] fix(fees): preserve exact fee rates inline --- .../send_view/confirm_transaction_view.dart | 1 - .../sub_widgets/fee_amount_with_rate.dart | 6 +---- lib/utilities/amount/fee_rate_formatter.dart | 3 +-- .../send_view/fee_amount_with_rate_test.dart | 24 ++++++++++++++----- 4 files changed, 20 insertions(+), 14 deletions(-) diff --git a/lib/pages/send_view/confirm_transaction_view.dart b/lib/pages/send_view/confirm_transaction_view.dart index ccffdd2295..c75e9eda14 100644 --- a/lib/pages/send_view/confirm_transaction_view.dart +++ b/lib/pages/send_view/confirm_transaction_view.dart @@ -1507,7 +1507,6 @@ class _ConfirmTransactionViewState amountStyle: STextStyles.itemSubtitle(context), rateStyle: STextStyles.desktopTextExtraExtraSmall(context), ), - ), ), ), if (!isDesktop) const Spacer(), diff --git a/lib/pages/send_view/sub_widgets/fee_amount_with_rate.dart b/lib/pages/send_view/sub_widgets/fee_amount_with_rate.dart index 3960b83623..cdb1fcb9bc 100644 --- a/lib/pages/send_view/sub_widgets/fee_amount_with_rate.dart +++ b/lib/pages/send_view/sub_widgets/fee_amount_with_rate.dart @@ -49,11 +49,7 @@ class FeeAmountWithRate extends StatelessWidget { textAlign: TextAlign.right, ), if (rate != null) - Text( - ' ($rate sat/vB)', - style: rateStyle, - textAlign: TextAlign.right, - ), + Text(' ($rate sat/vB)', style: rateStyle, textAlign: TextAlign.right), ], ); } diff --git a/lib/utilities/amount/fee_rate_formatter.dart b/lib/utilities/amount/fee_rate_formatter.dart index ee8c7dde40..a7daf41082 100644 --- a/lib/utilities/amount/fee_rate_formatter.dart +++ b/lib/utilities/amount/fee_rate_formatter.dart @@ -17,8 +17,7 @@ String? formatApproximateFeeRate( final scaled = rawFee * BigInt.from(10); var tenths = scaled ~/ size; if ((scaled % size) * BigInt.two >= size) tenths += BigInt.one; - final rate = - '~${tenths ~/ BigInt.from(10)}.${tenths % BigInt.from(10)}'; + final rate = '~${tenths ~/ BigInt.from(10)}.${tenths % BigInt.from(10)}'; if (locale == null) { return rate; } diff --git a/test/pages/send_view/fee_amount_with_rate_test.dart b/test/pages/send_view/fee_amount_with_rate_test.dart index bd5d073172..5c5283568f 100644 --- a/test/pages/send_view/fee_amount_with_rate_test.dart +++ b/test/pages/send_view/fee_amount_with_rate_test.dart @@ -10,7 +10,10 @@ void main() { test('preserves precision for values beyond double range', () { final fee = BigInt.parse('900719925474099312345678901234567890'); - expect(formatFeeRate(feeRaw: fee, vSize: 3), '~300239975158033104115226300411522630.0'); + expect( + formatFeeRate(feeRaw: fee, vSize: 3), + '~300239975158033104115226300411522630.0', + ); }); test('omits fee rates without a positive virtual size', () { @@ -22,16 +25,25 @@ void main() { test('uses the locale decimal separator', () { expect(formatFeeRate(feeRaw: BigInt.from(126), vSize: 100), '~1.3'); - expect(formatFeeRate(feeRaw: BigInt.from(126), vSize: 100, locale: 'en_US'), '~1.3'); - expect(formatFeeRate(feeRaw: BigInt.from(126), vSize: 100, locale: 'de_DE'), '~1,3'); - expect(formatFeeRate(feeRaw: BigInt.from(126), vSize: 100, locale: 'fr'), '~1,3'); + expect( + formatFeeRate(feeRaw: BigInt.from(126), vSize: 100, locale: 'en_US'), + '~1.3', + ); + expect( + formatFeeRate(feeRaw: BigInt.from(126), vSize: 100, locale: 'de_DE'), + '~1,3', + ); + expect( + formatFeeRate(feeRaw: BigInt.from(126), vSize: 100, locale: 'fr'), + '~1,3', + ); }); testWidgets('renders the rate in the same locale as the amount', ( tester, ) async { await tester.pumpWidget( - const MaterialApp( + MaterialApp( home: Scaffold( body: FeeAmountWithRate( formattedAmount: '0,00000126 BTC', @@ -52,7 +64,7 @@ void main() { tester, ) async { await tester.pumpWidget( - const MaterialApp( + MaterialApp( home: MediaQuery( data: MediaQueryData( size: Size(240, 600), From 018749339208ee29807fe042f0c70fa989a4f1a4 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 10 Nov 2026 14:13:30 -0600 Subject: [PATCH 6/7] fix(fees): use exact fee values in name flows --- lib/pages/send_view/confirm_transaction_view.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/pages/send_view/confirm_transaction_view.dart b/lib/pages/send_view/confirm_transaction_view.dart index c75e9eda14..4bed0b0651 100644 --- a/lib/pages/send_view/confirm_transaction_view.dart +++ b/lib/pages/send_view/confirm_transaction_view.dart @@ -33,7 +33,6 @@ import '../../themes/stack_colors.dart'; import '../../themes/theme_providers.dart'; import '../../utilities/amount/amount.dart'; import '../../utilities/amount/amount_formatter.dart'; -import '../../utilities/amount/fee_rate_formatter.dart'; import '../../utilities/constants.dart'; import '../../utilities/logger.dart'; import '../../utilities/text_styles.dart'; From 8f9403b22eead1826c1c2f2b1771926977ebd59e Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 10 Nov 2026 14:14:11 -0600 Subject: [PATCH 7/7] fix(fees): type fee screen authentication routes --- lib/pages/namecoin_names/confirm_name_transaction_view.dart | 4 ++-- .../spark_names/confirm_spark_name_transaction_view.dart | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/pages/namecoin_names/confirm_name_transaction_view.dart b/lib/pages/namecoin_names/confirm_name_transaction_view.dart index 5cde38524e..548b5aeb2a 100644 --- a/lib/pages/namecoin_names/confirm_name_transaction_view.dart +++ b/lib/pages/namecoin_names/confirm_name_transaction_view.dart @@ -883,9 +883,9 @@ class _ConfirmNameTransactionViewState ), ); } else { - unlocked = await Navigator.push( + unlocked = await Navigator.push( context, - RouteGenerator.getRoute( + RouteGenerator.getRoute( shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, builder: (_) => const LockscreenView( diff --git a/lib/pages/spark_names/confirm_spark_name_transaction_view.dart b/lib/pages/spark_names/confirm_spark_name_transaction_view.dart index 32e60b7dcd..169e709d1a 100644 --- a/lib/pages/spark_names/confirm_spark_name_transaction_view.dart +++ b/lib/pages/spark_names/confirm_spark_name_transaction_view.dart @@ -879,9 +879,9 @@ class _ConfirmSparkNameTransactionViewState ), ); } else { - unlocked = await Navigator.push( + unlocked = await Navigator.push( context, - RouteGenerator.getRoute( + RouteGenerator.getRoute( shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, builder: (_) => const LockscreenView(