Address-formatting-enhancements-MWEB (#2189)

* apply formatting to address book and MWEB * fix wallet type exception * Update cw_core/lib/wallet_type.dart [skip ci] * Update lib/src/screens/contact/contact_list_page.dart [skip ci] * Update lib/src/screens/contact/contact_list_page.dart [skip ci] * Update lib/utils/address_formatter.dart [skip ci] * Update lib/utils/address_formatter.dart [skip ci] * Update lib/utils/address_formatter.dart --------- Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

Serhii committed Apr 11, 2025 at 20:51 UTC a7376c3225fb03ca7209840c177e8bc6d90ef88f
5 files changed +171 -82
cw_core/lib/wallet_type.dart
+35
@@ -247,3 +247,38 @@ CryptoCurrency walletTypeToCryptoCurrency(WalletType type, {bool isTestnet = fal
247 'Unexpected wallet type: ${type.toString()} for CryptoCurrency walletTypeToCryptoCurrency');
248 }
249 }
250 +
251 +WalletType? cryptoCurrencyToWalletType(CryptoCurrency type) {
252 + switch (type) {
253 + case CryptoCurrency.xmr:
254 + return WalletType.monero;
255 + case CryptoCurrency.btc:
256 + return WalletType.bitcoin;
257 + case CryptoCurrency.ltc:
258 + return WalletType.litecoin;
259 + case CryptoCurrency.xhv:
260 + return WalletType.haven;
261 + case CryptoCurrency.eth:
262 + return WalletType.ethereum;
263 + case CryptoCurrency.bch:
264 + return WalletType.bitcoinCash;
265 + case CryptoCurrency.nano:
266 + return WalletType.nano;
267 + case CryptoCurrency.banano:
268 + return WalletType.banano;
269 + case CryptoCurrency.maticpoly:
270 + return WalletType.polygon;
271 + case CryptoCurrency.sol:
272 + return WalletType.solana;
273 + case CryptoCurrency.trx:
274 + return WalletType.tron;
275 + case CryptoCurrency.wow:
276 + return WalletType.wownero;
277 + case CryptoCurrency.zano:
278 + return WalletType.zano;
279 + case CryptoCurrency.dcr:
280 + return WalletType.decred;
281 + default:
282 + return null;
283 + }
284 +}
lib/src/screens/contact/contact_list_page.dart
+43 -40
@@ -12,9 +12,11 @@ import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
12 import 'package:cake_wallet/src/widgets/standard_list.dart';
13 import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
14 import 'package:cake_wallet/themes/extensions/exchange_page_theme.dart';
15 +import 'package:cake_wallet/utils/address_formatter.dart';
16 import 'package:cake_wallet/utils/show_bar.dart';
17 import 'package:cake_wallet/utils/show_pop_up.dart';
18 import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart';
19 +import 'package:cw_core/wallet_type.dart';
20 import 'package:flutter/cupertino.dart';
21 import 'package:flutter/material.dart';
22 import 'package:flutter/services.dart';
@@ -235,7 +237,7 @@ class _ContactPageBodyState extends State<ContactPageBody> with SingleTickerProv
237 return;
238 }
239
238 - final isCopied = await showNameAndAddressDialog(context, contact.name, contact.address);
240 + final isCopied = await DialogService.showNameAndAddressDialog(context, contact);
241
242 if (isCopied) {
243 await Clipboard.setData(ClipboardData(text: contact.address));
@@ -280,21 +282,6 @@ class _ContactPageBodyState extends State<ContactPageBody> with SingleTickerProv
282 ? Image.asset(image, height: 24, width: 24)
283 : const SizedBox(height: 24, width: 24);
284 }
283 -
284 - Future<bool> showNameAndAddressDialog(BuildContext context, String name, String address) async {
285 - return await showPopUp<bool>(
286 - context: context,
287 - builder: (BuildContext context) {
288 - return AlertWithTwoActions(
289 - alertTitle: name,
290 - alertContent: address,
291 - rightButtonText: S.of(context).copy,
292 - leftButtonText: S.of(context).cancel,
293 - actionRightButton: () => Navigator.of(context).pop(true),
294 - actionLeftButton: () => Navigator.of(context).pop(false));
295 - }) ??
296 - false;
297 - }
285 }
286
287 class ContactListBody extends StatefulWidget {
@@ -357,7 +344,7 @@ class _ContactListBodyState extends State<ContactListBody> {
344 }
345
346 final isCopied =
360 - await showNameAndAddressDialog(context, contact.name, contact.address);
347 + await DialogService.showNameAndAddressDialog(context, contact);
348
349 if (isCopied) {
350 await Clipboard.setData(ClipboardData(text: contact.address));
@@ -434,7 +421,7 @@ class _ContactListBodyState extends State<ContactListBody> {
421 ),
422 SlidableAction(
423 onPressed: (_) async {
437 - final isDelete = await showAlertDialog(context);
424 + final isDelete = await DialogService.showAlertDialog(context);
425
426 if (isDelete) {
427 await widget.contactListViewModel.delete(contact);
@@ -494,33 +481,49 @@ class _ContactListBodyState extends State<ContactListBody> {
481 );
482 }
483
497 - Future<bool> showAlertDialog(BuildContext context) async {
484 +}
485 +
486 +class DialogService {
487 + static Future<bool> showAlertDialog(BuildContext context) async {
488 return await showPopUp<bool>(
499 - context: context,
500 - builder: (BuildContext context) {
501 - return AlertWithTwoActions(
502 - alertTitle: S.of(context).address_remove_contact,
503 - alertContent: S.of(context).address_remove_content,
504 - rightButtonText: S.of(context).remove,
505 - leftButtonText: S.of(context).cancel,
506 - actionRightButton: () => Navigator.of(context).pop(true),
507 - actionLeftButton: () => Navigator.of(context).pop(false));
508 - }) ??
489 + context: context,
490 + builder: (BuildContext context) {
491 + return AlertWithTwoActions(
492 + alertTitle: S.of(context).address_remove_contact,
493 + alertContent: S.of(context).address_remove_content,
494 + rightButtonText: S.of(context).remove,
495 + leftButtonText: S.of(context).cancel,
496 + actionRightButton: () => Navigator.of(context).pop(true),
497 + actionLeftButton: () => Navigator.of(context).pop(false));
498 + }) ??
499 false;
500 }
501
512 - Future<bool> showNameAndAddressDialog(BuildContext context, String name, String address) async {
502 + static Future<bool> showNameAndAddressDialog(BuildContext context,ContactBase contact) async {
503 return await showPopUp<bool>(
514 - context: context,
515 - builder: (BuildContext context) {
516 - return AlertWithTwoActions(
517 - alertTitle: name,
518 - alertContent: address,
519 - rightButtonText: S.of(context).copy,
520 - leftButtonText: S.of(context).cancel,
521 - actionRightButton: () => Navigator.of(context).pop(true),
522 - actionLeftButton: () => Navigator.of(context).pop(false));
523 - }) ??
504 + context: context,
505 + builder: (BuildContext context) {
506 + return AlertWithTwoActions(
507 + alertTitle: contact.name,
508 + alertContent: contact.address,
509 + alertContentTextWidget: AddressFormatter.buildSegmentedAddress(
510 + address: contact.address,
511 + textAlign: TextAlign.center,
512 + walletType: cryptoCurrencyToWalletType(contact.type),
513 + evenTextStyle: TextStyle(
514 + fontSize: 16,
515 + fontWeight: FontWeight.normal,
516 + fontFamily: 'Lato',
517 + color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
518 + decoration: TextDecoration.none,
519 + ),
520 + ),
521 + rightButtonText: S.of(context).copy,
522 + leftButtonText: S.of(context).cancel,
523 + actionRightButton: () => Navigator.of(context).pop(true),
524 + actionLeftButton: () => Navigator.of(context).pop(false));
525 + }) ??
526 false;
527 }
528 }
529 +
lib/src/widgets/alert_with_two_actions.dart
+4
@@ -6,6 +6,7 @@ class AlertWithTwoActions extends BaseAlertDialog {
6 AlertWithTwoActions({
7 required this.alertTitle,
8 required this.alertContent,
9 + this.alertContentTextWidget,
10 required this.leftButtonText,
11 required this.rightButtonText,
12 required this.actionLeftButton,
@@ -21,6 +22,7 @@ class AlertWithTwoActions extends BaseAlertDialog {
22
23 final String alertTitle;
24 final String alertContent;
25 + final Widget? alertContentTextWidget;
26 final String leftButtonText;
27 final String rightButtonText;
28 final VoidCallback actionLeftButton;
@@ -38,6 +40,8 @@ class AlertWithTwoActions extends BaseAlertDialog {
40 @override
41 String get contentText => alertContent;
42 @override
43 + Widget? get contentTextWidget => alertContentTextWidget;
44 + @override
45 String get leftActionButtonText => leftButtonText;
46 @override
47 String get rightActionButtonText => rightButtonText;
lib/src/widgets/base_alert_dialog.dart
+4 -1
@@ -13,6 +13,8 @@ class BaseAlertDialog extends StatelessWidget {
13
14 String get contentText => '';
15
16 + Widget? get contentTextWidget => null;
17 +
18 String get leftActionButtonText => '';
19
20 String get rightActionButtonText => '';
@@ -79,7 +81,8 @@ class BaseAlertDialog extends StatelessWidget {
81 child: Column(
82 mainAxisSize: MainAxisSize.min,
83 children: [
82 - Text(
84 + contentTextWidget ??
85 + Text(
86 contentText,
87 textAlign: TextAlign.center,
88 style: TextStyle(
lib/utils/address_formatter.dart
+85 -41
@@ -5,24 +5,31 @@ import 'dart:math' as math;
5 class AddressFormatter {
6 static Widget buildSegmentedAddress({
7 required String address,
8 - required WalletType walletType,
8 + WalletType? walletType,
9 required TextStyle evenTextStyle,
10 TextStyle? oddTextStyle,
11 TextAlign? textAlign,
12 bool shouldTruncate = false,
13 }) {
14 +
15 + final cleanAddress = address.replaceAll('bitcoincash:', '');
16 + final isMWEB = address.startsWith('ltcmweb');
17 + final chunkSize = walletType != null ? _getChunkSize(walletType) : 4;
18 +
19 if (shouldTruncate) {
20 return _buildTruncatedAddress(
16 - address: address,
17 - walletType: walletType,
21 + address: cleanAddress,
22 + isMWEB: isMWEB,
23 + chunkSize: chunkSize,
24 evenTextStyle: evenTextStyle,
25 oddTextStyle: oddTextStyle ?? evenTextStyle.copyWith(color: evenTextStyle.color!.withAlpha(150)),
26 textAlign: textAlign,
27 );
28 } else {
29 return _buildFullSegmentedAddress(
24 - address: address,
25 - walletType: walletType,
30 + address: cleanAddress,
31 + isMWEB: isMWEB,
32 + chunkSize: chunkSize,
33 evenTextStyle: evenTextStyle,
34 oddTextStyle: oddTextStyle ?? evenTextStyle.copyWith(color: evenTextStyle.color!.withAlpha(128)),
35 textAlign: textAlign,
@@ -32,19 +39,34 @@ class AddressFormatter {
39
40 static Widget _buildFullSegmentedAddress({
41 required String address,
35 - required WalletType walletType,
42 + required bool isMWEB,
43 + required int chunkSize,
44 required TextStyle evenTextStyle,
45 required TextStyle oddTextStyle,
46 TextAlign? textAlign,
47 }) {
48
41 - final cleanAddress = address.replaceAll('bitcoincash:', '');
42 - final chunkSize = _getChunkSize(walletType);
49 final chunks = <String>[];
50
45 - for (int i = 0; i < cleanAddress.length; i += chunkSize) {
46 - final chunk = cleanAddress.substring(i, math.min(i + chunkSize, cleanAddress.length));
47 - chunks.add(chunk);
51 + if (isMWEB) {
52 + const mwebDisplayPrefix = 'ltcmweb';
53 + chunks.add(mwebDisplayPrefix);
54 + final startIndex = mwebDisplayPrefix.length;
55 + for (int i = startIndex; i < address.length; i += chunkSize) {
56 + final chunk = address.substring(
57 + i,
58 + math.min(i + chunkSize, address.length),
59 + );
60 + chunks.add(chunk);
61 + }
62 + } else {
63 + for (int i = 0; i < address.length; i += chunkSize) {
64 + final chunk = address.substring(
65 + i,
66 + math.min(i + chunkSize, address.length),
67 + );
68 + chunks.add(chunk);
69 + }
70 }
71
72 final spans = <TextSpan>[];
@@ -62,46 +84,68 @@ class AddressFormatter {
84
85 static Widget _buildTruncatedAddress({
86 required String address,
65 - required WalletType walletType,
87 + required bool isMWEB,
88 + required int chunkSize,
89 required TextStyle evenTextStyle,
90 required TextStyle oddTextStyle,
91 TextAlign? textAlign,
92 }) {
93
71 - final cleanAddress = address.replaceAll('bitcoincash:', '');
94 + if (isMWEB) {
95 + const fixedPrefix = 'ltcmweb';
96 + final secondChunkStart = fixedPrefix.length;
97 + const chunkSize = 4;
98 + final secondChunk = address.substring(
99 + secondChunkStart,
100 + math.min(secondChunkStart + chunkSize, address.length),
101 + );
102 + final lastChunk = address.substring(address.length - chunkSize);
103
73 - final int digitCount = (walletType == WalletType.monero ||
74 - walletType == WalletType.wownero ||
75 - walletType == WalletType.zano)
76 - ? 6
77 - : 4;
104 + final spans = <TextSpan>[
105 + TextSpan(text: '$fixedPrefix ', style: evenTextStyle),
106 + TextSpan(text: '$secondChunk ', style: oddTextStyle),
107 + TextSpan(text: '... ', style: oddTextStyle),
108 + TextSpan(text: lastChunk, style: evenTextStyle),
109 + ];
110
79 - if (cleanAddress.length <= 2 * digitCount) {
80 - return _buildFullSegmentedAddress(
81 - address: cleanAddress,
82 - walletType: walletType,
83 - evenTextStyle: evenTextStyle,
84 - oddTextStyle: oddTextStyle,
85 - textAlign: textAlign,
111 + return RichText(
112 + text: TextSpan(children: spans),
113 + textAlign: textAlign ?? TextAlign.start,
114 + overflow: TextOverflow.visible,
115 );
87 - }
116 + } else {
117 + final int digitCount = chunkSize;
118
89 - final String firstPart = cleanAddress.substring(0, digitCount);
90 - final String secondPart = cleanAddress.substring(digitCount, digitCount * 2);
91 - final String lastPart = cleanAddress.substring(cleanAddress.length - digitCount);
119 + if (address.length <= 2 * digitCount) {
120 + return _buildFullSegmentedAddress(
121 + address: address,
122 + isMWEB: isMWEB,
123 + chunkSize: chunkSize,
124 + evenTextStyle: evenTextStyle,
125 + oddTextStyle: oddTextStyle,
126 + textAlign: textAlign,
127 + );
128 + }
129
93 - final spans = <TextSpan>[
94 - TextSpan(text: '$firstPart ', style: evenTextStyle),
95 - TextSpan(text: '$secondPart ', style: oddTextStyle),
96 - TextSpan(text: '... ', style: oddTextStyle),
97 - TextSpan(text: lastPart, style: evenTextStyle),
98 - ];
130 + final String firstPart = address.substring(0, digitCount);
131 + final String secondPart =
132 + address.substring(digitCount, digitCount * 2);
133 + final String lastPart =
134 + address.substring(address.length - digitCount);
135
100 - return RichText(
101 - text: TextSpan(children: spans),
102 - textAlign: textAlign ?? TextAlign.start,
103 - overflow: TextOverflow.visible,
104 - );
136 + final spans = <TextSpan>[
137 + TextSpan(text: '$firstPart ', style: evenTextStyle),
138 + TextSpan(text: '$secondPart ', style: oddTextStyle),
139 + TextSpan(text: '... ', style: oddTextStyle),
140 + TextSpan(text: lastPart, style: evenTextStyle),
141 + ];
142 +
143 + return RichText(
144 + text: TextSpan(children: spans),
145 + textAlign: textAlign ?? TextAlign.start,
146 + overflow: TextOverflow.visible,
147 + );
148 + }
149 }
150
151 static int _getChunkSize(WalletType walletType) {
@@ -114,4 +158,4 @@ class AddressFormatter {
158 return 4;
159 }
160 }
117 -}
161 +}
\ No newline at end of file