CW-1017: Monero minor enhancements (#2347)

* feat: add max decimal handling for crypto amounts and test coverage * fix: correct decimal point handling in `withMaxDecimals` and update related test * fix: ensure amount formatting aligns with selected currency decimals in ReceivePage * fix: ensure amount controller syncs with ViewModel in AddressPage and ReceivePage * feat: add support for currency decimals and streamline amount handling logic * fix: ensure crypto and fiat amounts sync correctly in Send ViewModel

Konstantin Ullrich committed Jul 4, 2025 at 03:54 UTC 3647e3043ab9bacee6ec2661bffd0b252ebe44f2
12 files changed +78 -9
cw_core/lib/crypto_amount_format.dart
+17 -1
@@ -1 +1,17 @@
1 -double cryptoAmountToDouble({required num amount, required num divider}) => amount / divider;
\ No newline at end of file
1 +double cryptoAmountToDouble({required num amount, required num divider}) => amount / divider;
2 +
3 +extension MaxDecimals on String {
4 + String withMaxDecimals(int maxDecimals) {
5 + var parts = split(".");
6 +
7 + if (parts.length > 2) {
8 + parts = [parts.first, parts.sublist(1, parts.length).join("")];
9 + }
10 +
11 + if (parts.length == 2 && parts[1].length > maxDecimals) {
12 + parts[1] = parts[1].substring(0, maxDecimals);
13 + }
14 +
15 + return parts.join(".");
16 + }
17 +}
cw_core/lib/currency.dart
+2 -1
@@ -3,4 +3,5 @@ abstract class Currency {
3 String? get tag;
4 String? get fullName;
5 String? get iconPath;
6 -}
\ No newline at end of file
6 + int get decimals;
7 +}
cw_core/test/crypto_amount_format.dart new
+34
@@ -0,0 +1,34 @@
1 +import 'package:flutter_test/flutter_test.dart';
2 +import 'package:cw_core/crypto_amount_format.dart';
3 +
4 +void main() {
5 + group('String.withMaxDecimals', () {
6 + test('should return the original string when it has fewer decimal places than the max', () {
7 + final input = '123.45';
8 + final result = input.withMaxDecimals(3);
9 +
10 + expect(result, equals(input));
11 + });
12 +
13 + test('should truncate decimal places when the string has more than the max', () {
14 + final input = '123.4567';
15 + final result = input.withMaxDecimals(2);
16 +
17 + expect(result, equals('123.45'));
18 + });
19 +
20 + test('should handle strings with no decimal places', () {
21 + final input = '123';
22 + final result = input.withMaxDecimals(2);
23 +
24 + expect(result, equals(input));
25 + });
26 +
27 + test('should handle strings with multiple decimal points', () {
28 + final input = '123.45.67';
29 + final result = input.withMaxDecimals(4);
30 +
31 + expect(result, equals('123.4567'));
32 + });
33 + });
34 +}
lib/entities/fiat_currency.dart
+7 -1
@@ -2,10 +2,16 @@ import 'package:cw_core/currency.dart';
2 import 'package:cw_core/enumerable_item.dart';
3
4 class FiatCurrency extends EnumerableItem<String> with Serializable<String> implements Currency {
5 - const FiatCurrency({required String symbol, required this.countryCode, required this.fullName}) : super(title: symbol, raw: symbol);
5 + const FiatCurrency({
6 + required String symbol,
7 + required this.countryCode,
8 + required this.fullName,
9 + this.decimals = 2,
10 + }) : super(title: symbol, raw: symbol);
11
12 final String countryCode;
13 final String fullName;
14 + final int decimals;
15
16 static List<FiatCurrency> get all => _all.values.toList();
17
lib/src/screens/dashboard/pages/address_page.dart
+2 -5
@@ -8,7 +8,6 @@ import 'package:cw_core/receive_page_option.dart';
8 import 'package:cake_wallet/src/screens/dashboard/widgets/present_receive_option_picker.dart';
9 import 'package:cake_wallet/src/widgets/gradient_background.dart';
10 import 'package:cake_wallet/src/widgets/keyboard_done_button.dart';
11 -import 'package:cake_wallet/themes/core/material_base_theme.dart';
11 import 'package:cake_wallet/utils/responsive_layout_util.dart';
12 import 'package:cake_wallet/utils/share_util.dart';
13 import 'package:cake_wallet/view_model/dashboard/receive_option_view_model.dart';
@@ -34,9 +33,7 @@ class AddressPage extends BasePage {
33 _amountController = TextEditingController() {
34 _amountController.addListener(() {
35 if (_formKey.currentState!.validate()) {
37 - addressListViewModel.changeAmount(
38 - _amountController.text,
39 - );
36 + addressListViewModel.changeAmount(_amountController.text);
37 }
38 });
39 }
@@ -79,7 +76,7 @@ class AddressPage extends BasePage {
76 label: !isMobileView ? S.of(context).close : S.of(context).seed_alert_back,
77 child: TextButton(
78 style: ButtonStyle(
82 - overlayColor: MaterialStateColor.resolveWith((states) => Colors.transparent),
79 + overlayColor: WidgetStateColor.resolveWith((states) => Colors.transparent),
80 ),
81 onPressed: () => onClose(context),
82 child: !isMobileView ? _closeButton : _backButton,
lib/src/screens/exchange/widgets/exchange_card.dart
+1
@@ -225,6 +225,7 @@ class ExchangeCardState<T extends Currency> extends State<ExchangeCard<T>> {
225 ValueKey('${_cardInstanceName}_currency_amount_textfield_widget_key'),
226 imageArrow: widget.imageArrow,
227 selectedCurrency: _selectedCurrency.toString(),
228 + selectedCurrencyDecimals: _selectedCurrency.decimals,
229 amountFocusNode: widget.amountFocusNode,
230 amountController: amountController,
231 onTapPicker: () => _presentPicker(context),
lib/src/screens/receive/widgets/currency_input_field.dart
+5
@@ -1,12 +1,14 @@
1 import 'package:cake_wallet/generated/i18n.dart';
2 import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
3 import 'package:cake_wallet/themes/core/material_base_theme.dart';
4 +import 'package:cw_core/crypto_amount_format.dart';
5 import 'package:flutter/material.dart';
6 import 'package:flutter/services.dart';
7
8 class CurrencyAmountTextField extends StatelessWidget {
9 const CurrencyAmountTextField({
10 required this.selectedCurrency,
11 + required this.selectedCurrencyDecimals,
12 required this.amountFocusNode,
13 required this.amountController,
14 required this.isAmountEditable,
@@ -42,6 +44,7 @@ class CurrencyAmountTextField extends StatelessWidget {
44 final Key? currencyAmountTextFieldWidgetKey;
45 final Widget? imageArrow;
46 final String selectedCurrency;
47 + final int selectedCurrencyDecimals;
48 final String? tag;
49 final String? hintText;
50 final Color? tagBackgroundColor;
@@ -197,6 +200,8 @@ class CurrencyAmountTextField extends StatelessWidget {
200 color: Theme.of(context).colorScheme.onSurfaceVariant,
201 ),
202 validator: isAmountEditable ? currencyValueValidator : null,
203 + onChanged: (value) => amountController.text =
204 + value.replaceAll(',', '.').withMaxDecimals(selectedCurrencyDecimals),
205 ),
206 ),
207 ),
lib/src/screens/receive/widgets/qr_widget.dart
+2
@@ -211,6 +211,8 @@ class QRWidget extends StatelessWidget {
211 hasUnderlineBorder: true,
212 borderWidth: 0.0,
213 selectedCurrency: _currencyName,
214 + selectedCurrencyDecimals:
215 + addressListViewModel.selectedCurrency.decimals,
216 amountFocusNode: amountTextFieldFocusNode,
217 amountController: amountController,
218 padding: EdgeInsets.only(top: 20, left: _width / 4),
lib/src/screens/send/widgets/send_card.dart
+2
@@ -247,6 +247,7 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
247 currencyAmountTextFieldWidgetKey:
248 ValueKey('send_page_crypto_currency_amount_textfield_widget_key'),
249 selectedCurrency: sendViewModel.selectedCryptoCurrency.title,
250 + selectedCurrencyDecimals: sendViewModel.selectedCryptoCurrency.decimals,
251 amountFocusNode: widget.cryptoAmountFocus,
252 amountController: cryptoAmountController,
253 isAmountEditable: true,
@@ -308,6 +309,7 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
309 currencyAmountTextFieldWidgetKey:
310 ValueKey('send_page_fiat_currency_amount_textfield_widget_key'),
311 selectedCurrency: sendViewModel.fiat.title,
312 + selectedCurrencyDecimals: sendViewModel.fiat.decimals,
313 amountFocusNode: widget.fiatAmountFocus,
314 amountController: fiatAmountController,
315 hintText: '0.00',
lib/src/screens/send/widgets/send_template_card.dart
+2
@@ -122,6 +122,7 @@ class SendTemplateCard extends StatelessWidget {
122 hasUnderlineBorder: true,
123 borderWidth: 0.0,
124 selectedCurrency: template.selectedCurrency.title,
125 + selectedCurrencyDecimals: template.selectedCurrency.decimals,
126 amountFocusNode: _cryptoAmountFocus,
127 amountController: _cryptoAmountController,
128 isSelected: template.isCryptoSelected,
@@ -148,6 +149,7 @@ class SendTemplateCard extends StatelessWidget {
149 hasUnderlineBorder: true,
150 borderWidth: 0.0,
151 selectedCurrency: sendTemplateViewModel.fiatCurrency,
152 + selectedCurrencyDecimals: sendTemplateViewModel.fiatCurrencyDecimals,
153 amountFocusNode: _fiatAmountFocus,
154 amountController: _fiatAmountController,
155 isSelected: !template.isCryptoSelected,
lib/view_model/send/output.dart
+1 -1
@@ -250,7 +250,7 @@ abstract class OutputBase with Store {
250 sendAll = false;
251 }
252
253 - cryptoAmount = amount.replaceAll(',', '.');
253 + cryptoAmount = amount;
254 _updateFiatAmount();
255 }
256
lib/view_model/send/send_template_view_model.dart
+3
@@ -63,6 +63,9 @@ abstract class SendTemplateViewModelBase with Store {
63 @computed
64 String get fiatCurrency => _settingsStore.fiatCurrency.title;
65
66 + @computed
67 + int get fiatCurrencyDecimals => _settingsStore.fiatCurrency.decimals;
68 +
69 @computed
70 ObservableList<Template> get templates => _sendTemplateStore.templates;
71