Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 32 additions & 60 deletions lib/pages/send_view/confirm_transaction_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
),
),
],
),
Expand All @@ -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),
Expand Down Expand Up @@ -1388,40 +1385,15 @@ class _ConfirmTransactionViewState
color: Theme.of(
context,
).extension<StackColors>()!.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<StackColors>()!.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),
),
),
),
Expand Down
70 changes: 70 additions & 0 deletions lib/pages/send_view/sub_widgets/fee_amount_with_rate.dart
Original file line number Diff line number Diff line change
@@ -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,
),
],
);
}
}
83 changes: 83 additions & 0 deletions test/pages/send_view/fee_amount_with_rate_test.dart
Original file line number Diff line number Diff line change
@@ -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);
});
}
Loading