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