From 3f25d69244ff9d08ce96335b5b5e7e70de912ec2 Mon Sep 17 00:00:00 2001 From: Serhii Date: Thu, 10 Apr 2025 17:16:43 +0300 Subject: [PATCH 1/4] CW-1021-Address-formatting-enhancements (#2141) * Implement visual formatting for addresses * fix minor issues * Update transaction_details_page.dart * Update transaction_details_page.dart * fix multi recipient address formatting * Update address_cell.dart --- .../cake_pay_confirm_purchase_card_page.dart | 1 + .../exchange_trade/exchange_trade_page.dart | 1 + .../screens/receive/address_list_page.dart | 1 + lib/src/screens/receive/receive_page.dart | 2 +- .../screens/receive/widgets/address_cell.dart | 47 ++++--- .../screens/receive/widgets/address_list.dart | 8 +- .../screens/receive/widgets/qr_widget.dart | 12 +- lib/src/screens/send/send_page.dart | 1 + .../transaction_details/rbf_details_page.dart | 1 + .../transaction_details_page.dart | 96 +++++++++++++- .../confirm_sending_bottom_sheet_widget.dart | 108 +++++----------- lib/src/widgets/list_row.dart | 20 +-- lib/utils/address_formatter.dart | 117 ++++++++++++++++++ 13 files changed, 292 insertions(+), 123 deletions(-) create mode 100644 lib/utils/address_formatter.dart diff --git a/lib/src/screens/cake_pay/cards/cake_pay_confirm_purchase_card_page.dart b/lib/src/screens/cake_pay/cards/cake_pay_confirm_purchase_card_page.dart index 9ae2acf8e..51c17bc8b 100644 --- a/lib/src/screens/cake_pay/cards/cake_pay_confirm_purchase_card_page.dart +++ b/lib/src/screens/cake_pay/cards/cake_pay_confirm_purchase_card_page.dart @@ -381,6 +381,7 @@ class CakePayBuyCardDetailPage extends BasePage { return ConfirmSendingBottomSheet( key: ValueKey('send_page_confirm_sending_dialog_key'), currentTheme: currentTheme, + walletType: cakePayPurchaseViewModel.sendViewModel.walletType, paymentId: S.of(popupContext).payment_id, paymentIdValue: order?.orderId, expirationTime: cakePayPurchaseViewModel.formattedRemainingTime, diff --git a/lib/src/screens/exchange_trade/exchange_trade_page.dart b/lib/src/screens/exchange_trade/exchange_trade_page.dart index f328cc115..277d3c838 100644 --- a/lib/src/screens/exchange_trade/exchange_trade_page.dart +++ b/lib/src/screens/exchange_trade/exchange_trade_page.dart @@ -264,6 +264,7 @@ class ExchangeTradeState extends State { return ConfirmSendingBottomSheet( key: ValueKey('exchange_trade_page_confirm_sending_bottom_sheet_key'), currentTheme: widget.currentTheme, + walletType: widget.exchangeTradeViewModel.sendViewModel.walletType, titleText: S.of(bottomSheetContext).confirm_transaction, titleIconPath: widget.exchangeTradeViewModel.sendViewModel.selectedCryptoCurrency.iconPath, diff --git a/lib/src/screens/receive/address_list_page.dart b/lib/src/screens/receive/address_list_page.dart index 5f6794715..b2af4389c 100644 --- a/lib/src/screens/receive/address_list_page.dart +++ b/lib/src/screens/receive/address_list_page.dart @@ -20,6 +20,7 @@ class AddressListPage extends BasePage { children: [ AddressList( addressListViewModel: addressListViewModel, + currentTheme: currentTheme, onSelect: (String address) async { Navigator.of(context).pop(address); }, diff --git a/lib/src/screens/receive/receive_page.dart b/lib/src/screens/receive/receive_page.dart index 7e3c2b555..2a18f4d08 100644 --- a/lib/src/screens/receive/receive_page.dart +++ b/lib/src/screens/receive/receive_page.dart @@ -124,7 +124,7 @@ class ReceivePage extends BasePage { isLight: currentTheme.type == ThemeType.light, ), ), - AddressList(addressListViewModel: addressListViewModel), + AddressList(addressListViewModel: addressListViewModel, currentTheme: currentTheme), Padding( padding: EdgeInsets.fromLTRB(24, 24, 24, 32), child: Text( diff --git a/lib/src/screens/receive/widgets/address_cell.dart b/lib/src/screens/receive/widgets/address_cell.dart index beef7c762..354a4fcb5 100644 --- a/lib/src/screens/receive/widgets/address_cell.dart +++ b/lib/src/screens/receive/widgets/address_cell.dart @@ -1,7 +1,11 @@ -import 'package:auto_size_text/auto_size_text.dart'; import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/themes/extensions/qr_code_theme.dart'; +import 'package:cake_wallet/themes/extensions/receive_page_theme.dart'; +import 'package:cake_wallet/themes/theme_base.dart'; +import 'package:cake_wallet/utils/address_formatter.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; @@ -14,6 +18,8 @@ class AddressCell extends StatelessWidget { required this.isPrimary, required this.backgroundColor, required this.textColor, + required this.walletType, + required this.currentTheme, this.onTap, this.onEdit, this.onHide, @@ -30,6 +36,8 @@ class AddressCell extends StatelessWidget { required bool isCurrent, required Color backgroundColor, required Color textColor, + required WalletType walletType, + required ThemeBase currentTheme, Function(String)? onTap, bool hasBalance = false, bool hasReceived = false, @@ -45,6 +53,8 @@ class AddressCell extends StatelessWidget { isPrimary: item.isPrimary, backgroundColor: backgroundColor, textColor: textColor, + walletType: walletType, + currentTheme: currentTheme, onTap: onTap, onEdit: onEdit, onHide: onHide, @@ -62,6 +72,8 @@ class AddressCell extends StatelessWidget { final bool isPrimary; final Color backgroundColor; final Color textColor; + final WalletType walletType; + final ThemeBase currentTheme; final Function(String)? onTap; final Function()? onEdit; final Function()? onHide; @@ -73,21 +85,6 @@ class AddressCell extends StatelessWidget { final bool hasBalance; final bool hasReceived; - static const int addressPreviewLength = 8; - - String get formattedAddress { - final formatIfCashAddr = address.replaceAll('bitcoincash:', ''); - - if (formatIfCashAddr.length <= (name.isNotEmpty ? 16 : 43)) { - return formatIfCashAddr; - } else { - return formatIfCashAddr.substring(0, addressPreviewLength) + - '...' + - formatIfCashAddr.substring( - formatIfCashAddr.length - addressPreviewLength, formatIfCashAddr.length); - } - } - @override Widget build(BuildContext context) { final Widget cell = InkWell( @@ -139,16 +136,14 @@ class AddressCell extends StatelessWidget { ], ), Flexible( - child: AutoSizeText( - responsiveLayoutUtil.shouldRenderTabletUI ? address : formattedAddress, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: isChange ? 10 : 14, - color: textColor, - ), - ), - ), + child: AddressFormatter.buildSegmentedAddress( + address: address, + walletType: walletType, + shouldTruncate: name.isNotEmpty || address.length > 43 , + evenTextStyle: TextStyle( + fontSize: isChange ? 10 : 14, + color: textColor + ))), ], ), if (hasBalance || hasReceived) diff --git a/lib/src/screens/receive/widgets/address_list.dart b/lib/src/screens/receive/widgets/address_list.dart index 8d22d81ef..c5a1bc871 100644 --- a/lib/src/screens/receive/widgets/address_list.dart +++ b/lib/src/screens/receive/widgets/address_list.dart @@ -1,5 +1,3 @@ -import 'dart:math'; - import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/routes.dart'; @@ -9,6 +7,7 @@ import 'package:cake_wallet/src/screens/receive/widgets/address_cell.dart'; import 'package:cake_wallet/src/screens/receive/widgets/header_tile.dart'; import 'package:cake_wallet/src/widgets/section_divider.dart'; import 'package:cake_wallet/themes/extensions/receive_page_theme.dart'; +import 'package:cake_wallet/themes/theme_base.dart'; import 'package:cake_wallet/utils/list_item.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_account_list_header.dart'; @@ -20,16 +19,17 @@ import 'package:cw_core/wallet_type.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:mobx/mobx.dart'; class AddressList extends StatefulWidget { const AddressList({ super.key, required this.addressListViewModel, + required this.currentTheme, this.onSelect, }); final WalletAddressListViewModel addressListViewModel; + final ThemeBase currentTheme; final Function(String)? onSelect; @override @@ -161,6 +161,8 @@ class _AddressListState extends State { return AddressCell.fromItem( item, isCurrent: isCurrent, + currentTheme: widget.currentTheme, + walletType: widget.addressListViewModel.type, hasBalance: widget.addressListViewModel.isBalanceAvailable, hasReceived: widget.addressListViewModel.isReceivedAvailable, // hasReceived: diff --git a/lib/src/screens/receive/widgets/qr_widget.dart b/lib/src/screens/receive/widgets/qr_widget.dart index 9d09e57a1..b263ea7ef 100644 --- a/lib/src/screens/receive/widgets/qr_widget.dart +++ b/lib/src/screens/receive/widgets/qr_widget.dart @@ -5,6 +5,7 @@ import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart'; import 'package:cake_wallet/src/screens/receive/widgets/currency_input_field.dart'; import 'package:cake_wallet/themes/theme_base.dart'; +import 'package:cake_wallet/utils/address_formatter.dart'; import 'package:cake_wallet/utils/brightness_util.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/utils/show_bar.dart'; @@ -160,16 +161,15 @@ class QRWidget extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( - child: Text( - addressUri.address, + child: AddressFormatter.buildSegmentedAddress( + address: addressUri.address, + walletType: addressListViewModel.type, textAlign: TextAlign.center, - style: TextStyle( + evenTextStyle: TextStyle( fontSize: 15, fontWeight: FontWeight.w500, color: - Theme.of(context).extension()!.textColor), - ), - ), + Theme.of(context).extension()!.textColor))), Padding( padding: EdgeInsets.only(left: 12), child: copyImage, diff --git a/lib/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart index 368dac19e..8c42f1129 100644 --- a/lib/src/screens/send/send_page.dart +++ b/lib/src/screens/send/send_page.dart @@ -558,6 +558,7 @@ class SendPage extends BasePage { key: ValueKey('send_page_confirm_sending_dialog_key'), titleText: S.of(bottomSheetContext).confirm_transaction, currentTheme: currentTheme, + walletType: sendViewModel.walletType, titleIconPath: sendViewModel.selectedCryptoCurrency.iconPath, currency: sendViewModel.selectedCryptoCurrency, amount: S.of(bottomSheetContext).send_amount, diff --git a/lib/src/screens/transaction_details/rbf_details_page.dart b/lib/src/screens/transaction_details/rbf_details_page.dart index 10cd40940..ce6ee122b 100644 --- a/lib/src/screens/transaction_details/rbf_details_page.dart +++ b/lib/src/screens/transaction_details/rbf_details_page.dart @@ -190,6 +190,7 @@ class RBFDetailsPage extends BasePage { key: ValueKey('rbf_confirm_sending_bottom_sheet'), titleText: S.of(bottomSheetContext).confirm_transaction, currentTheme: currentTheme, + walletType: transactionDetailsViewModel.sendViewModel.walletType, titleIconPath: transactionDetailsViewModel.sendViewModel.selectedCryptoCurrency.iconPath, currency: transactionDetailsViewModel.sendViewModel.selectedCryptoCurrency, amount: S.of(bottomSheetContext).send_amount, diff --git a/lib/src/screens/transaction_details/transaction_details_page.dart b/lib/src/screens/transaction_details/transaction_details_page.dart index 9484bf4da..c1188d0c7 100644 --- a/lib/src/screens/transaction_details/transaction_details_page.dart +++ b/lib/src/screens/transaction_details/transaction_details_page.dart @@ -9,8 +9,11 @@ import 'package:cake_wallet/src/screens/transaction_details/textfield_list_item. import 'package:cake_wallet/src/screens/transaction_details/widgets/textfield_list_row.dart'; import 'package:cake_wallet/src/widgets/list_row.dart'; import 'package:cake_wallet/src/widgets/standard_list.dart'; +import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; +import 'package:cake_wallet/utils/address_formatter.dart'; import 'package:cake_wallet/utils/show_bar.dart'; import 'package:cake_wallet/view_model/transaction_details_view_model.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; @@ -39,13 +42,28 @@ class TransactionDetailsPage extends BasePage { final item = transactionDetailsViewModel.items[index]; if (item is StandartListItem) { + Widget? addressTextWidget; + + if (item.title.toLowerCase() == 'recipient addresses' || + item.title.toLowerCase() == 'source address') { + addressTextWidget = getFormattedAddress( + context: context, + value: item.value, + walletType: transactionDetailsViewModel.sendViewModel.walletType, + ); + } + return GestureDetector( key: item.key, onTap: () { Clipboard.setData(ClipboardData(text: item.value)); showBar(context, S.of(context).transaction_details_copied(item.title)); }, - child: ListRow(title: '${item.title}:', value: item.value), + child: ListRow( + title: '${item.title}:', + value: item.value, + textWidget: addressTextWidget, + ), ); } @@ -91,4 +109,80 @@ class TransactionDetailsPage extends BasePage { ], ); } + + Widget getFormattedAddress({ + required BuildContext context, + required String value, + required WalletType walletType, + }) { + final textStyle = TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context).extension()!.titleColor, + ); + final List children = []; + final bool hasDoubleNewline = value.contains('\n\n'); + + if (hasDoubleNewline) { + final blocks = value + .split('\n\n') + .map((b) => b.trim()) + .where((b) => b.isNotEmpty) + .toList(); + for (final block in blocks) { + final lines = block + .split('\n') + .map((l) => l.trim()) + .where((l) => l.isNotEmpty) + .toList(); + if (lines.length > 1) { + children.add(Text(lines.first, style: textStyle)); + for (int i = 1; i < lines.length; i++) { + children.add( + AddressFormatter.buildSegmentedAddress( + address: lines[i], + walletType: walletType, + evenTextStyle: textStyle, + ), + ); + } + } else { + children.add( + AddressFormatter.buildSegmentedAddress( + address: lines.first, + walletType: walletType, + evenTextStyle: textStyle, + ), + ); + } + children.add(SizedBox(height: 8)); + } + } else { + final lines = value + .split('\n') + .map((l) => l.trim()) + .where((l) => l.isNotEmpty) + .toList(); + bool firstLineIsContactName = (lines.length > 1 && lines.first.length < 20); + int startIndex = 0; + if (firstLineIsContactName) { + children.add(Text(lines.first, style: textStyle)); + startIndex = 1; + } + for (int i = startIndex; i < lines.length; i++) { + children.add( + AddressFormatter.buildSegmentedAddress( + address: lines[i], + walletType: walletType, + evenTextStyle: textStyle, + ), + ); + } + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: children, + ); + } } diff --git a/lib/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart b/lib/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart index 0732c87b2..f78717fb3 100644 --- a/lib/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart +++ b/lib/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart @@ -5,11 +5,12 @@ import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cake_wallet/themes/extensions/filter_theme.dart'; import 'package:cake_wallet/themes/extensions/sync_indicator_theme.dart'; import 'package:cake_wallet/themes/theme_base.dart'; +import 'package:cake_wallet/utils/address_formatter.dart'; import 'package:cake_wallet/view_model/send/output.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/pending_transaction.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; -import 'dart:math' as math; import 'base_bottom_sheet_widget.dart'; @@ -27,6 +28,7 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet { final String feeFiatAmount; final List outputs; final VoidCallback onSlideComplete; + final WalletType walletType; final PendingChange? change; final bool isOpenCryptoPay; @@ -46,6 +48,7 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet { required this.feeFiatAmount, required this.outputs, required this.onSlideComplete, + required this.walletType, this.change, this.isOpenCryptoPay = false, Key? key, @@ -91,6 +94,7 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet { itemTitle: paymentId!, currentTheme: currentTheme, itemTitleTextStyle: itemTitleTextStyle, + walletType: walletType, isBatchSending: false, amount: '', address: paymentIdValue!, @@ -139,6 +143,7 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet { name: isBatchSending ? batchContactTitle : contactName, address: _address, amount: _amount, + walletType: walletType, isBatchSending: isBatchSending, itemTitleTextStyle: itemTitleTextStyle, itemSubTitleTextStyle: itemSubTitleTextStyle, @@ -149,6 +154,7 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet { currentTheme: currentTheme, itemTitleTextStyle: itemTitleTextStyle, isBatchSending: isBatchSending, + walletType: walletType, amount: _amount, address: _address, itemSubTitleTextStyle: itemSubTitleTextStyle, @@ -166,6 +172,7 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet { address: change!.address, amount: change!.amount + ' ${currency.title}', isBatchSending: true, + walletType: walletType, itemTitleTextStyle: itemTitleTextStyle, itemSubTitleTextStyle: itemSubTitleTextStyle, tileBackgroundColor: tileBackgroundColor, @@ -275,6 +282,7 @@ class AddressTile extends StatelessWidget { required this.address, required this.itemSubTitleTextStyle, required this.tileBackgroundColor, + required this.walletType, }); final String itemTitle; @@ -285,18 +293,10 @@ class AddressTile extends StatelessWidget { final String address; final TextStyle itemSubTitleTextStyle; final Color tileBackgroundColor; + final WalletType walletType; @override Widget build(BuildContext context) { - final addressTextStyle = TextStyle( - fontSize: 12, - fontFamily: 'Lato', - fontWeight: FontWeight.w600, - color: currentTheme.type == ThemeType.bright - ? Theme.of(context).extension()!.titleColor.withOpacity(0.5) - : Theme.of(context).extension()!.titleColor, - decoration: TextDecoration.none, - ); return Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), decoration: BoxDecoration( @@ -313,41 +313,20 @@ class AddressTile extends StatelessWidget { if (isBatchSending) Text(amount, style: itemTitleTextStyle), ], ), - buildSegmentedAddress( + AddressFormatter.buildSegmentedAddress( address: address, - evenTextStyle: addressTextStyle, - oddTextStyle: itemSubTitleTextStyle, + walletType: walletType, + evenTextStyle: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w600, + color: Theme.of(context).extension()!.titleColor, + decoration: TextDecoration.none) ), ], ), ); } - - Widget buildSegmentedAddress({ - required String address, - int chunkSize = 6, - required TextStyle evenTextStyle, - required TextStyle oddTextStyle, - }) { - final spans = []; - - int index = 0; - for (int i = 0; i < address.length; i += chunkSize) { - final chunk = address.substring(i, math.min(i + chunkSize, address.length)); - final style = (index % 2 == 0) ? evenTextStyle : oddTextStyle; - - spans.add( - TextSpan(text: '$chunk ', style: style), - ); - - index++; - } - - return RichText( - text: TextSpan(children: spans, style: evenTextStyle), - overflow: TextOverflow.visible, - ); - } } class AddressExpansionTile extends StatelessWidget { @@ -362,6 +341,7 @@ class AddressExpansionTile extends StatelessWidget { required this.itemTitleTextStyle, required this.itemSubTitleTextStyle, required this.tileBackgroundColor, + required this.walletType, }); final String contactType; @@ -373,19 +353,10 @@ class AddressExpansionTile extends StatelessWidget { final TextStyle itemTitleTextStyle; final TextStyle itemSubTitleTextStyle; final Color tileBackgroundColor; + final WalletType walletType; @override Widget build(BuildContext context) { - final addressTextStyle = TextStyle( - fontSize: 12, - fontFamily: 'Lato', - fontWeight: FontWeight.w600, - color: currentTheme.type == ThemeType.bright - ? Theme.of(context).extension()!.titleColor.withOpacity(0.5) - : Theme.of(context).extension()!.titleColor, - decoration: TextDecoration.none, - ); - return Container( decoration: BoxDecoration( borderRadius: BorderRadius.all(Radius.circular(10)), @@ -420,10 +391,15 @@ class AddressExpansionTile extends StatelessWidget { Row( children: [ Expanded( - child: buildSegmentedAddress( - address: address, - evenTextStyle: addressTextStyle, - oddTextStyle: itemSubTitleTextStyle, + child: AddressFormatter.buildSegmentedAddress( + address: address, + walletType: walletType, + evenTextStyle: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w600, + color: Theme.of(context).extension()!.titleColor, + decoration: TextDecoration.none) ), ), ], @@ -434,30 +410,4 @@ class AddressExpansionTile extends StatelessWidget { ), ); } - - Widget buildSegmentedAddress({ - required String address, - int chunkSize = 6, - required TextStyle evenTextStyle, - required TextStyle oddTextStyle, - }) { - final spans = []; - - int index = 0; - for (int i = 0; i < address.length; i += chunkSize) { - final chunk = address.substring(i, math.min(i + chunkSize, address.length)); - final style = (index % 2 == 0) ? evenTextStyle : oddTextStyle; - - spans.add( - TextSpan(text: '$chunk ', style: style), - ); - - index++; - } - - return RichText( - text: TextSpan(children: spans, style: evenTextStyle), - overflow: TextOverflow.visible, - ); - } } diff --git a/lib/src/widgets/list_row.dart b/lib/src/widgets/list_row.dart index 417c7836b..11b27845b 100644 --- a/lib/src/widgets/list_row.dart +++ b/lib/src/widgets/list_row.dart @@ -12,7 +12,8 @@ class ListRow extends StatelessWidget { this.padding, this.color, this.hintTextColor, - this.mainTextColor + this.mainTextColor, + this.textWidget }); final String title; @@ -24,6 +25,16 @@ class ListRow extends StatelessWidget { final Color? color; final Color? hintTextColor; final Color? mainTextColor; + final Widget? textWidget; + + Widget _getTextWidget (BuildContext context) => textWidget ?? Text( + value, + style: TextStyle( + fontSize: valueFontSize, + fontWeight: FontWeight.w500, + color: mainTextColor ?? Theme.of(context).extension()!.titleColor + ), + ); @override Widget build(BuildContext context) { @@ -49,12 +60,7 @@ class ListRow extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( - child: Text(value, - style: TextStyle( - fontSize: valueFontSize, - fontWeight: FontWeight.w500, - color: mainTextColor ?? Theme.of(context).extension()!.titleColor)), - ), + child: _getTextWidget(context)), image != null ? Padding( padding: EdgeInsets.only(left: 24), diff --git a/lib/utils/address_formatter.dart b/lib/utils/address_formatter.dart new file mode 100644 index 000000000..0bcc713d0 --- /dev/null +++ b/lib/utils/address_formatter.dart @@ -0,0 +1,117 @@ +import 'package:cw_core/wallet_type.dart'; +import 'package:flutter/widgets.dart'; +import 'dart:math' as math; + +class AddressFormatter { + static Widget buildSegmentedAddress({ + required String address, + required WalletType walletType, + required TextStyle evenTextStyle, + TextStyle? oddTextStyle, + TextAlign? textAlign, + bool shouldTruncate = false, + }) { + if (shouldTruncate) { + return _buildTruncatedAddress( + address: address, + walletType: walletType, + evenTextStyle: evenTextStyle, + oddTextStyle: oddTextStyle ?? evenTextStyle.copyWith(color: evenTextStyle.color!.withAlpha(150)), + textAlign: textAlign, + ); + } else { + return _buildFullSegmentedAddress( + address: address, + walletType: walletType, + evenTextStyle: evenTextStyle, + oddTextStyle: oddTextStyle ?? evenTextStyle.copyWith(color: evenTextStyle.color!.withAlpha(128)), + textAlign: textAlign, + ); + } + } + + static Widget _buildFullSegmentedAddress({ + required String address, + required WalletType walletType, + 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); + } + + final spans = []; + for (int i = 0; i < chunks.length; i++) { + final style = (i % 2 == 0) ? evenTextStyle : oddTextStyle; + spans.add(TextSpan(text: '${chunks[i]} ', style: style)); + } + + return RichText( + text: TextSpan(children: spans), + textAlign: textAlign ?? TextAlign.start, + overflow: TextOverflow.visible, + ); + } + + static Widget _buildTruncatedAddress({ + required String address, + required WalletType walletType, + required TextStyle evenTextStyle, + required TextStyle oddTextStyle, + TextAlign? textAlign, + }) { + + final cleanAddress = address.replaceAll('bitcoincash:', ''); + + final int digitCount = (walletType == WalletType.monero || + walletType == WalletType.wownero || + walletType == WalletType.zano) + ? 6 + : 4; + + if (cleanAddress.length <= 2 * digitCount) { + return _buildFullSegmentedAddress( + address: cleanAddress, + walletType: walletType, + evenTextStyle: evenTextStyle, + oddTextStyle: oddTextStyle, + textAlign: textAlign, + ); + } + + 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) { + switch (walletType) { + case WalletType.monero: + case WalletType.wownero: + case WalletType.zano: + return 6; + default: + return 4; + } + } +} From ea9b87d480e2256683318fd010e07ffd3ce8a8a6 Mon Sep 17 00:00:00 2001 From: cyan Date: Thu, 10 Apr 2025 23:17:52 +0200 Subject: [PATCH 2/4] fix(cw_monero): wrong function in account rename call (#2183) --- cw_monero/lib/api/account_list.dart | 13 ++++++++----- cw_monero/lib/monero_account_list.dart | 12 ++++++------ cw_wownero/lib/api/account_list.dart | 3 +-- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/cw_monero/lib/api/account_list.dart b/cw_monero/lib/api/account_list.dart index 28b00c925..3ceef5815 100644 --- a/cw_monero/lib/api/account_list.dart +++ b/cw_monero/lib/api/account_list.dart @@ -1,4 +1,7 @@ +import 'dart:async'; + import 'package:cw_monero/api/wallet.dart'; +import 'package:cw_monero/monero_account_list.dart'; import 'package:monero/monero.dart' as monero; monero.wallet? wptr = null; @@ -15,7 +18,6 @@ monero.WalletListener? getWlptr() { return _wlptr!; } - monero.SubaddressAccount? subaddressAccount; bool isUpdating = false; @@ -51,8 +53,9 @@ void addAccountSync({required String label}) { } void setLabelForAccountSync({required int accountIndex, required String label}) { - // TODO(mrcyjanek): this may be wrong function? - monero.Wallet_setSubaddressLabel(wptr!, accountIndex: accountIndex, addressIndex: 0, label: label); + monero.SubaddressAccount_setLabel(subaddressAccount!, accountIndex: accountIndex, label: label); + MoneroAccountListBase.cachedAccounts[wptr!.address] = []; + refreshAccounts(); } void _addAccount(String label) => addAccountSync(label: label); @@ -66,10 +69,10 @@ void _setLabelForAccount(Map args) { Future addAccount({required String label}) async { _addAccount(label); - await store(); + unawaited(store()); } Future setLabelForAccount({required int accountIndex, required String label}) async { _setLabelForAccount({'accountIndex': accountIndex, 'label': label}); - await store(); + unawaited(store()); } \ No newline at end of file diff --git a/cw_monero/lib/monero_account_list.dart b/cw_monero/lib/monero_account_list.dart index 82a0efd32..c9a48a939 100644 --- a/cw_monero/lib/monero_account_list.dart +++ b/cw_monero/lib/monero_account_list.dart @@ -45,18 +45,18 @@ abstract class MoneroAccountListBase with Store { } } - Map> _cachedAccounts = {}; + static Map> cachedAccounts = {}; List getAll() { final allAccounts = account_list.getAllAccount(); final currentCount = allAccounts.length; - _cachedAccounts[account_list.wptr!.address] ??= []; + cachedAccounts[account_list.wptr!.address] ??= []; - if (_cachedAccounts[account_list.wptr!.address]!.length == currentCount) { - return _cachedAccounts[account_list.wptr!.address]!; + if (cachedAccounts[account_list.wptr!.address]!.length == currentCount) { + return cachedAccounts[account_list.wptr!.address]!; } - _cachedAccounts[account_list.wptr!.address] = allAccounts.map((accountRow) { + cachedAccounts[account_list.wptr!.address] = allAccounts.map((accountRow) { final balance = monero.SubaddressAccountRow_getUnlockedBalance(accountRow); return Account( @@ -66,7 +66,7 @@ abstract class MoneroAccountListBase with Store { ); }).toList(); - return _cachedAccounts[account_list.wptr!.address]!; + return cachedAccounts[account_list.wptr!.address]!; } Future addAccount({required String label}) async { diff --git a/cw_wownero/lib/api/account_list.dart b/cw_wownero/lib/api/account_list.dart index a73e4dcd2..5bd18d51e 100644 --- a/cw_wownero/lib/api/account_list.dart +++ b/cw_wownero/lib/api/account_list.dart @@ -48,8 +48,7 @@ void addAccountSync({required String label}) { } void setLabelForAccountSync({required int accountIndex, required String label}) { - // TODO(mrcyjanek): this may be wrong function? - wownero.Wallet_setSubaddressLabel(wptr!, accountIndex: accountIndex, addressIndex: 0, label: label); + wownero.SubaddressAccount_setLabel(subaddressAccount!, accountIndex: accountIndex, label: label); } void _addAccount(String label) => addAccountSync(label: label); From db051232cede5d831aed47ed93203f7de1a1c05b Mon Sep 17 00:00:00 2001 From: cyan Date: Fri, 11 Apr 2025 02:12:40 +0200 Subject: [PATCH 3/4] remove shrinkResources and minifyEnabled to reduce apk size (#2104) * remove shrinkResources and minifyEnabled to reduce apk size * remove shrinkResources and minifyEnabled to reduce apk size --- android/app/build.gradle | 3 --- android/app/proguard-rules.pro | 3 ++- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index c45ed9368..6c299c929 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -74,9 +74,6 @@ android { release { signingConfig signingConfigs.release - shrinkResources false - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } debug { diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro index d24d7f10a..921ee4d4c 100644 --- a/android/app/proguard-rules.pro +++ b/android/app/proguard-rules.pro @@ -5,4 +5,5 @@ -keep class io.flutter.view.** { *; } -keep class io.flutter.** { *; } -keep class io.flutter.plugins.** { *; } --dontwarn io.flutter.embedding.** \ No newline at end of file +-dontwarn io.flutter.embedding.** +-dontwarn com.google.android.play.core.splitcompat.SplitCompatApplication \ No newline at end of file From 6ed07a504e5e0010bf69135bbf51198789cbe5ad Mon Sep 17 00:00:00 2001 From: Serhii Date: Fri, 11 Apr 2025 04:23:31 +0300 Subject: [PATCH 4/4] add caching for supported assets (#2184) --- lib/buy/robinhood/robinhood_buy_provider.dart | 46 ++++++++++++++++++- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/lib/buy/robinhood/robinhood_buy_provider.dart b/lib/buy/robinhood/robinhood_buy_provider.dart index 90177ec61..93efd5642 100644 --- a/lib/buy/robinhood/robinhood_buy_provider.dart +++ b/lib/buy/robinhood/robinhood_buy_provider.dart @@ -35,9 +35,16 @@ class RobinhoodBuyProvider extends BuyProvider { static const _baseUrl = 'applink.robinhood.com'; static const _cIdBaseUrl = 'exchange-helper.cakewallet.com'; + static const _apiBaseUrl = 'api.robinhood.com'; + static const _assetsPath = '/catpay/v1/supported_currencies/'; - static const List _notSupportedCrypto = []; - static const List _notSupportedFiat = []; + static List _supportedCrypto = []; + static final List _supportedFiat = [FiatCurrency.usd]; + + static final List _notSupportedCrypto = []; + + static final List _notSupportedFiat = + FiatCurrency.all.where((fiat) => !_supportedFiat.contains(fiat)).toList(); @override String get title => 'Robinhood Connect'; @@ -58,6 +65,38 @@ class RobinhoodBuyProvider extends BuyProvider { String get _apiSecret => secrets.exchangeHelperApiKey; + Future> getSupportedAssets() async { + final uri = Uri.https(_apiBaseUrl, '$_assetsPath', {'applicationId': _applicationId}); + + try { + final response = await http.get(uri, headers: {'accept': 'application/json'}); + + if (response.statusCode == 200) { + final responseData = jsonDecode(response.body) as Map; + final pairs = responseData['cryptoCurrencyPairs'] as List; + + final supportedAssets = []; + + for (final item in pairs) { + String code = item['assetCurrency']['code'] as String; + if (code == 'AVAX') code = 'AVAXC'; + try { + final currency = CryptoCurrency.fromString(code); + supportedAssets.add(currency); + } catch (e) { + log('Robinhood: Unknown asset code "$code" - skipped'); + } + } + return supportedAssets; + } else { + return []; + } + } catch (e) { + log('Robinhood: Failed to fetch supported assets: $e'); + return []; + } + } + Future getSignature(String message) async { switch (wallet.type) { case WalletType.ethereum: @@ -156,6 +195,9 @@ class RobinhoodBuyProvider extends BuyProvider { String? countryCode}) async { String? paymentMethod; + if (_supportedCrypto.isEmpty) _supportedCrypto = await getSupportedAssets(); + if (_supportedCrypto.isNotEmpty && !(_supportedCrypto.contains(cryptoCurrency))) return null; + if (paymentType != null && paymentType != PaymentType.all) { paymentMethod = normalizePaymentMethod(paymentType); if (paymentMethod == null) return null;