From a7376c3225fb03ca7209840c177e8bc6d90ef88f Mon Sep 17 00:00:00 2001 From: Serhii Date: Fri, 11 Apr 2025 20:51:30 +0300 Subject: [PATCH] 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 --- cw_core/lib/wallet_type.dart | 35 +++++ .../screens/contact/contact_list_page.dart | 83 +++++------ lib/src/widgets/alert_with_two_actions.dart | 4 + lib/src/widgets/base_alert_dialog.dart | 5 +- lib/utils/address_formatter.dart | 130 ++++++++++++------ 5 files changed, 173 insertions(+), 84 deletions(-) diff --git a/cw_core/lib/wallet_type.dart b/cw_core/lib/wallet_type.dart index fed998ed0..40a2b31d5 100644 --- a/cw_core/lib/wallet_type.dart +++ b/cw_core/lib/wallet_type.dart @@ -247,3 +247,38 @@ CryptoCurrency walletTypeToCryptoCurrency(WalletType type, {bool isTestnet = fal 'Unexpected wallet type: ${type.toString()} for CryptoCurrency walletTypeToCryptoCurrency'); } } + +WalletType? cryptoCurrencyToWalletType(CryptoCurrency type) { + switch (type) { + case CryptoCurrency.xmr: + return WalletType.monero; + case CryptoCurrency.btc: + return WalletType.bitcoin; + case CryptoCurrency.ltc: + return WalletType.litecoin; + case CryptoCurrency.xhv: + return WalletType.haven; + case CryptoCurrency.eth: + return WalletType.ethereum; + case CryptoCurrency.bch: + return WalletType.bitcoinCash; + case CryptoCurrency.nano: + return WalletType.nano; + case CryptoCurrency.banano: + return WalletType.banano; + case CryptoCurrency.maticpoly: + return WalletType.polygon; + case CryptoCurrency.sol: + return WalletType.solana; + case CryptoCurrency.trx: + return WalletType.tron; + case CryptoCurrency.wow: + return WalletType.wownero; + case CryptoCurrency.zano: + return WalletType.zano; + case CryptoCurrency.dcr: + return WalletType.decred; + default: + return null; + } +} diff --git a/lib/src/screens/contact/contact_list_page.dart b/lib/src/screens/contact/contact_list_page.dart index ee1dcd112..4dfb2a1eb 100644 --- a/lib/src/screens/contact/contact_list_page.dart +++ b/lib/src/screens/contact/contact_list_page.dart @@ -12,9 +12,11 @@ import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; import 'package:cake_wallet/src/widgets/standard_list.dart'; import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cake_wallet/themes/extensions/exchange_page_theme.dart'; +import 'package:cake_wallet/utils/address_formatter.dart'; import 'package:cake_wallet/utils/show_bar.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -235,7 +237,7 @@ class _ContactPageBodyState extends State with SingleTickerProv return; } - final isCopied = await showNameAndAddressDialog(context, contact.name, contact.address); + final isCopied = await DialogService.showNameAndAddressDialog(context, contact); if (isCopied) { await Clipboard.setData(ClipboardData(text: contact.address)); @@ -280,21 +282,6 @@ class _ContactPageBodyState extends State with SingleTickerProv ? Image.asset(image, height: 24, width: 24) : const SizedBox(height: 24, width: 24); } - - Future showNameAndAddressDialog(BuildContext context, String name, String address) async { - return await showPopUp( - context: context, - builder: (BuildContext context) { - return AlertWithTwoActions( - alertTitle: name, - alertContent: address, - rightButtonText: S.of(context).copy, - leftButtonText: S.of(context).cancel, - actionRightButton: () => Navigator.of(context).pop(true), - actionLeftButton: () => Navigator.of(context).pop(false)); - }) ?? - false; - } } class ContactListBody extends StatefulWidget { @@ -357,7 +344,7 @@ class _ContactListBodyState extends State { } final isCopied = - await showNameAndAddressDialog(context, contact.name, contact.address); + await DialogService.showNameAndAddressDialog(context, contact); if (isCopied) { await Clipboard.setData(ClipboardData(text: contact.address)); @@ -434,7 +421,7 @@ class _ContactListBodyState extends State { ), SlidableAction( onPressed: (_) async { - final isDelete = await showAlertDialog(context); + final isDelete = await DialogService.showAlertDialog(context); if (isDelete) { await widget.contactListViewModel.delete(contact); @@ -494,33 +481,49 @@ class _ContactListBodyState extends State { ); } - Future showAlertDialog(BuildContext context) async { +} + +class DialogService { + static Future showAlertDialog(BuildContext context) async { return await showPopUp( - context: context, - builder: (BuildContext context) { - return AlertWithTwoActions( - alertTitle: S.of(context).address_remove_contact, - alertContent: S.of(context).address_remove_content, - rightButtonText: S.of(context).remove, - leftButtonText: S.of(context).cancel, - actionRightButton: () => Navigator.of(context).pop(true), - actionLeftButton: () => Navigator.of(context).pop(false)); - }) ?? + context: context, + builder: (BuildContext context) { + return AlertWithTwoActions( + alertTitle: S.of(context).address_remove_contact, + alertContent: S.of(context).address_remove_content, + rightButtonText: S.of(context).remove, + leftButtonText: S.of(context).cancel, + actionRightButton: () => Navigator.of(context).pop(true), + actionLeftButton: () => Navigator.of(context).pop(false)); + }) ?? false; } - Future showNameAndAddressDialog(BuildContext context, String name, String address) async { + static Future showNameAndAddressDialog(BuildContext context,ContactBase contact) async { return await showPopUp( - context: context, - builder: (BuildContext context) { - return AlertWithTwoActions( - alertTitle: name, - alertContent: address, - rightButtonText: S.of(context).copy, - leftButtonText: S.of(context).cancel, - actionRightButton: () => Navigator.of(context).pop(true), - actionLeftButton: () => Navigator.of(context).pop(false)); - }) ?? + context: context, + builder: (BuildContext context) { + return AlertWithTwoActions( + alertTitle: contact.name, + alertContent: contact.address, + alertContentTextWidget: AddressFormatter.buildSegmentedAddress( + address: contact.address, + textAlign: TextAlign.center, + walletType: cryptoCurrencyToWalletType(contact.type), + evenTextStyle: TextStyle( + fontSize: 16, + fontWeight: FontWeight.normal, + fontFamily: 'Lato', + color: Theme.of(context).extension()!.titleColor, + decoration: TextDecoration.none, + ), + ), + rightButtonText: S.of(context).copy, + leftButtonText: S.of(context).cancel, + actionRightButton: () => Navigator.of(context).pop(true), + actionLeftButton: () => Navigator.of(context).pop(false)); + }) ?? false; } } + diff --git a/lib/src/widgets/alert_with_two_actions.dart b/lib/src/widgets/alert_with_two_actions.dart index e3d4408a6..c2ab872af 100644 --- a/lib/src/widgets/alert_with_two_actions.dart +++ b/lib/src/widgets/alert_with_two_actions.dart @@ -6,6 +6,7 @@ class AlertWithTwoActions extends BaseAlertDialog { AlertWithTwoActions({ required this.alertTitle, required this.alertContent, + this.alertContentTextWidget, required this.leftButtonText, required this.rightButtonText, required this.actionLeftButton, @@ -21,6 +22,7 @@ class AlertWithTwoActions extends BaseAlertDialog { final String alertTitle; final String alertContent; + final Widget? alertContentTextWidget; final String leftButtonText; final String rightButtonText; final VoidCallback actionLeftButton; @@ -38,6 +40,8 @@ class AlertWithTwoActions extends BaseAlertDialog { @override String get contentText => alertContent; @override + Widget? get contentTextWidget => alertContentTextWidget; + @override String get leftActionButtonText => leftButtonText; @override String get rightActionButtonText => rightButtonText; diff --git a/lib/src/widgets/base_alert_dialog.dart b/lib/src/widgets/base_alert_dialog.dart index 02b8f85ab..2ea067a76 100644 --- a/lib/src/widgets/base_alert_dialog.dart +++ b/lib/src/widgets/base_alert_dialog.dart @@ -13,6 +13,8 @@ class BaseAlertDialog extends StatelessWidget { String get contentText => ''; + Widget? get contentTextWidget => null; + String get leftActionButtonText => ''; String get rightActionButtonText => ''; @@ -79,7 +81,8 @@ class BaseAlertDialog extends StatelessWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ - Text( + contentTextWidget ?? + Text( contentText, textAlign: TextAlign.center, style: TextStyle( diff --git a/lib/utils/address_formatter.dart b/lib/utils/address_formatter.dart index 0bcc713d0..f2083c772 100644 --- a/lib/utils/address_formatter.dart +++ b/lib/utils/address_formatter.dart @@ -5,24 +5,31 @@ import 'dart:math' as math; class AddressFormatter { static Widget buildSegmentedAddress({ required String address, - required WalletType walletType, + WalletType? walletType, required TextStyle evenTextStyle, TextStyle? oddTextStyle, TextAlign? textAlign, bool shouldTruncate = false, }) { + + final cleanAddress = address.replaceAll('bitcoincash:', ''); + final isMWEB = address.startsWith('ltcmweb'); + final chunkSize = walletType != null ? _getChunkSize(walletType) : 4; + if (shouldTruncate) { return _buildTruncatedAddress( - address: address, - walletType: walletType, + address: cleanAddress, + isMWEB: isMWEB, + chunkSize: chunkSize, evenTextStyle: evenTextStyle, oddTextStyle: oddTextStyle ?? evenTextStyle.copyWith(color: evenTextStyle.color!.withAlpha(150)), textAlign: textAlign, ); } else { return _buildFullSegmentedAddress( - address: address, - walletType: walletType, + address: cleanAddress, + isMWEB: isMWEB, + chunkSize: chunkSize, evenTextStyle: evenTextStyle, oddTextStyle: oddTextStyle ?? evenTextStyle.copyWith(color: evenTextStyle.color!.withAlpha(128)), textAlign: textAlign, @@ -32,19 +39,34 @@ class AddressFormatter { static Widget _buildFullSegmentedAddress({ required String address, - required WalletType walletType, + required bool isMWEB, + required int chunkSize, required TextStyle evenTextStyle, required TextStyle oddTextStyle, TextAlign? textAlign, }) { - final cleanAddress = address.replaceAll('bitcoincash:', ''); - final chunkSize = _getChunkSize(walletType); final chunks = []; - for (int i = 0; i < cleanAddress.length; i += chunkSize) { - final chunk = cleanAddress.substring(i, math.min(i + chunkSize, cleanAddress.length)); - chunks.add(chunk); + if (isMWEB) { + const mwebDisplayPrefix = 'ltcmweb'; + chunks.add(mwebDisplayPrefix); + final startIndex = mwebDisplayPrefix.length; + for (int i = startIndex; i < address.length; i += chunkSize) { + final chunk = address.substring( + i, + math.min(i + chunkSize, address.length), + ); + chunks.add(chunk); + } + } else { + for (int i = 0; i < address.length; i += chunkSize) { + final chunk = address.substring( + i, + math.min(i + chunkSize, address.length), + ); + chunks.add(chunk); + } } final spans = []; @@ -62,46 +84,68 @@ class AddressFormatter { static Widget _buildTruncatedAddress({ required String address, - required WalletType walletType, + required bool isMWEB, + required int chunkSize, required TextStyle evenTextStyle, required TextStyle oddTextStyle, TextAlign? textAlign, }) { - final cleanAddress = address.replaceAll('bitcoincash:', ''); + if (isMWEB) { + const fixedPrefix = 'ltcmweb'; + final secondChunkStart = fixedPrefix.length; + const chunkSize = 4; + final secondChunk = address.substring( + secondChunkStart, + math.min(secondChunkStart + chunkSize, address.length), + ); + final lastChunk = address.substring(address.length - chunkSize); - final int digitCount = (walletType == WalletType.monero || - walletType == WalletType.wownero || - walletType == WalletType.zano) - ? 6 - : 4; + final spans = [ + TextSpan(text: '$fixedPrefix ', style: evenTextStyle), + TextSpan(text: '$secondChunk ', style: oddTextStyle), + TextSpan(text: '... ', style: oddTextStyle), + TextSpan(text: lastChunk, style: evenTextStyle), + ]; - if (cleanAddress.length <= 2 * digitCount) { - return _buildFullSegmentedAddress( - address: cleanAddress, - walletType: walletType, - evenTextStyle: evenTextStyle, - oddTextStyle: oddTextStyle, - textAlign: textAlign, + return RichText( + text: TextSpan(children: spans), + textAlign: textAlign ?? TextAlign.start, + overflow: TextOverflow.visible, + ); + } else { + final int digitCount = chunkSize; + + if (address.length <= 2 * digitCount) { + return _buildFullSegmentedAddress( + address: address, + isMWEB: isMWEB, + chunkSize: chunkSize, + evenTextStyle: evenTextStyle, + oddTextStyle: oddTextStyle, + textAlign: textAlign, + ); + } + + final String firstPart = address.substring(0, digitCount); + final String secondPart = + address.substring(digitCount, digitCount * 2); + final String lastPart = + address.substring(address.length - digitCount); + + final spans = [ + TextSpan(text: '$firstPart ', style: evenTextStyle), + TextSpan(text: '$secondPart ', style: oddTextStyle), + TextSpan(text: '... ', style: oddTextStyle), + TextSpan(text: lastPart, style: evenTextStyle), + ]; + + return RichText( + text: TextSpan(children: spans), + textAlign: textAlign ?? TextAlign.start, + overflow: TextOverflow.visible, ); } - - final String firstPart = cleanAddress.substring(0, digitCount); - final String secondPart = cleanAddress.substring(digitCount, digitCount * 2); - final String lastPart = cleanAddress.substring(cleanAddress.length - digitCount); - - final spans = [ - TextSpan(text: '$firstPart ', style: evenTextStyle), - TextSpan(text: '$secondPart ', style: oddTextStyle), - TextSpan(text: '... ', style: oddTextStyle), - TextSpan(text: lastPart, style: evenTextStyle), - ]; - - return RichText( - text: TextSpan(children: spans), - textAlign: textAlign ?? TextAlign.start, - overflow: TextOverflow.visible, - ); } static int _getChunkSize(WalletType walletType) { @@ -114,4 +158,4 @@ class AddressFormatter { return 4; } } -} +} \ No newline at end of file