From 24cceb5795b52469dc7e811d894e528ae17d7c5e Mon Sep 17 00:00:00 2001 From: malik1004x Date: Wed, 18 Mar 2026 22:34:02 +0100 Subject: [PATCH] fix ui edge cases (#3109) * wrap navbar in observer to account for disabling pages * always display fiat amount with 2 decimals on confirm sheet * don't show limit popup if max limit is 0 (this signifies the providers didn't load yet) * fix deposit amount getting set to "ALL" in race condition * disable showing hardware wallets on l2 wallet selector * fix reset in account customizer if designs weren't loaded properly * add withDecimals string extension * add fallback on failed display amount parse --- cw_core/lib/crypto_amount_format.dart | 20 ++ lib/core/amount_parsing_proxy.dart | 15 +- lib/new-ui/pages/account_customizer.dart | 5 +- lib/new-ui/pages/swap_page.dart | 5 +- .../send_page/l2_action_wallet_selector.dart | 2 +- .../widgets/send_page/send_confirm_sheet.dart | 8 +- .../widgets/swap_page/swap_limit_popup.dart | 2 +- .../widgets/new_main_navbar_widget.dart | 203 +++++++++--------- 8 files changed, 151 insertions(+), 109 deletions(-) diff --git a/cw_core/lib/crypto_amount_format.dart b/cw_core/lib/crypto_amount_format.dart index 1fb0d5ecf..d6ddf111a 100644 --- a/cw_core/lib/crypto_amount_format.dart +++ b/cw_core/lib/crypto_amount_format.dart @@ -17,6 +17,26 @@ extension MaxDecimals on String { return parts.join("."); } + String withDecimals(int decimals) { + var parts = split("."); + + if (parts.length > 2) { + parts = [parts.first, parts.sublist(1).join("")]; + } + + if (parts.length == 1) { + parts.add(""); + } + + if (parts[1].length > decimals) { + parts[1] = parts[1].substring(0, decimals); + } else { + parts[1] = parts[1].padRight(decimals, '0'); + } + + return parts.join("."); + } + /// Format a stringified number to a localized representation /// 1.000.000,00 in de_DE diff --git a/lib/core/amount_parsing_proxy.dart b/lib/core/amount_parsing_proxy.dart index 58fb1056c..4b941129e 100644 --- a/lib/core/amount_parsing_proxy.dart +++ b/lib/core/amount_parsing_proxy.dart @@ -2,6 +2,7 @@ import 'package:cake_wallet/entities/bitcoin_amount_display_mode.dart'; import 'package:cake_wallet/src/screens/wallet_connect/utils/string_parsing.dart'; import 'package:cw_core/crypto_amount_format.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/utils/print_verbose.dart'; class AmountParsingProxy { final BitcoinAmountDisplayMode displayMode; @@ -19,11 +20,19 @@ class AmountParsingProxy { /// [getCryptoOutputAmount] turns the input [amount] into the preferred representation of [cryptoCurrency] String getDisplayCryptoAmount(String amount, CryptoCurrency cryptoCurrency) { - if (useSatoshi(cryptoCurrency) && amount.isNotEmpty) { - return cryptoCurrency.parseAmount(amount.withMaxDecimals(cryptoCurrency.decimals)).toString(); + + try { + if (useSatoshi(cryptoCurrency) && amount.isNotEmpty) { + return cryptoCurrency.parseAmount(amount.withMaxDecimals(cryptoCurrency.decimals)).toString(); + } + + return amount.withMaxDecimals(cryptoCurrency.decimals); + + } catch(_) { + printV("failed to parse amount $amount for currency ${cryptoCurrency.title}, falling back to showing unparsed"); + return amount; } - return amount.withMaxDecimals(cryptoCurrency.decimals); } /// [getCryptoStringRepresentation] turns the input [amount] into the preferred representation of [cryptoCurrency] diff --git a/lib/new-ui/pages/account_customizer.dart b/lib/new-ui/pages/account_customizer.dart index 20f3b7b7b..fd93e45d6 100644 --- a/lib/new-ui/pages/account_customizer.dart +++ b/lib/new-ui/pages/account_customizer.dart @@ -17,6 +17,7 @@ import 'package:cake_wallet/view_model/monero_account_list/account_list_item.dar import 'package:cake_wallet/view_model/monero_account_list/monero_account_edit_or_create_view_model.dart'; import 'package:cake_wallet/view_model/monero_account_list/monero_account_list_view_model.dart'; import 'package:cw_core/balance_card_style_settings.dart'; +import 'package:cw_core/card_design.dart'; import 'package:cw_core/generate_name.dart'; import 'package:cw_core/sync_status.dart'; import 'package:cw_core/utils/print_verbose.dart'; @@ -381,7 +382,9 @@ class _AccountCustomizerState extends State { selected: true, designSwitchDuration: Duration(milliseconds: 200), width: cardWidth, - design: widget.dashboardViewModel.cardDesigns[i], + design: i >= widget.dashboardViewModel.cardDesigns.length + ? CardDesign.genericDefault + : widget.dashboardViewModel.cardDesigns[i], ), order: i, accountListItem: accounts[i])); diff --git a/lib/new-ui/pages/swap_page.dart b/lib/new-ui/pages/swap_page.dart index 35ad8b31c..3899a2af4 100644 --- a/lib/new-ui/pages/swap_page.dart +++ b/lib/new-ui/pages/swap_page.dart @@ -292,7 +292,10 @@ class _NewSwapPageState extends State { _depositAmountDebounce.run(() { widget.exchangeViewModel.calculateBestRate(); - widget.exchangeViewModel.changeDepositAmount(amount: depositAmountController.text); + if (depositAmountController.text != widget.exchangeViewModel.depositAmount && + depositAmountController.text != S.of(context).all) { + widget.exchangeViewModel.changeDepositAmount(amount: depositAmountController.text); + } widget.exchangeViewModel.isReceiveAmountEntered = false; widget.exchangeViewModel.isFixedRateMode = false; if (!receiveKey.currentState!.amountFocusNode.hasFocus) { diff --git a/lib/new-ui/widgets/send_page/l2_action_wallet_selector.dart b/lib/new-ui/widgets/send_page/l2_action_wallet_selector.dart index 40686a055..2ee57ef9e 100644 --- a/lib/new-ui/widgets/send_page/l2_action_wallet_selector.dart +++ b/lib/new-ui/widgets/send_page/l2_action_wallet_selector.dart @@ -57,7 +57,7 @@ class _L2ActionWalletSelectorState extends State { if (widget.showOtherWallets) { () async { items.addAll((await WalletInfo.getAll()) - .where((item) => item.type == widget.sendViewModel.walletType)); + .where((item) => item.type == widget.sendViewModel.walletType && item.hardwareWalletType == null)); items.sort((a, b) { if (a.name == widget.sendViewModel.wallet.name) return -1; diff --git a/lib/new-ui/widgets/send_page/send_confirm_sheet.dart b/lib/new-ui/widgets/send_page/send_confirm_sheet.dart index 8516605c7..ac0f53204 100644 --- a/lib/new-ui/widgets/send_page/send_confirm_sheet.dart +++ b/lib/new-ui/widgets/send_page/send_confirm_sheet.dart @@ -154,8 +154,10 @@ class SendTransactionDetails extends StatelessWidget { String sumStr(List list, double Function(T) picker) => sumBy(list, picker).toString(); - String sumWithUnit(List list, double Function(T) picker, String unit) => - "${sumStr(list, picker)} $unit"; + String sumWithUnit(List list, double Function(T) picker, String unit, {int? decimals}) { + final str = sumStr(list, picker); + return "${decimals == null ? str : str.withDecimals(decimals)} $unit"; + } Widget _buildMainContent(BuildContext context) { @@ -198,6 +200,7 @@ class SendTransactionDetails extends StatelessWidget { sendViewModel.outputs, (o) => double.tryParse(o.fiatAmount.replaceAll(",", "")) ?? 0, sendViewModel.fiatCurrency.title, + decimals:2 ) : sendViewModel.pendingTransactionFiatAmountFormatted; @@ -206,6 +209,7 @@ class SendTransactionDetails extends StatelessWidget { sendViewModel.outputs, (o) => double.tryParse(o.estimatedFeeFiatAmount.replaceAll(",", "")) ?? 0, sendViewModel.fiatCurrency.title, + decimals:2 ) : sendViewModel.pendingTransactionFeeFiatAmountFormatted; diff --git a/lib/new-ui/widgets/swap_page/swap_limit_popup.dart b/lib/new-ui/widgets/swap_page/swap_limit_popup.dart index a6520ddf6..3dea8a5b9 100644 --- a/lib/new-ui/widgets/swap_page/swap_limit_popup.dart +++ b/lib/new-ui/widgets/swap_page/swap_limit_popup.dart @@ -23,7 +23,7 @@ class SwapLimitPopup extends StatelessWidget { final double? amount = double.tryParse(exchangeViewModel.depositAmountCanonical); final max = exchangeViewModel.limits.max ?? double.infinity; final min = exchangeViewModel.limits.min ?? 0; - final tooLarge = amount != null && amount > max; + final tooLarge = amount != null && max != 0 && amount > max; final tooSmall = amount != null && min != 0 && amount < min; final show = amount != null && (tooLarge || tooSmall); diff --git a/lib/src/screens/dashboard/widgets/new_main_navbar_widget.dart b/lib/src/screens/dashboard/widgets/new_main_navbar_widget.dart index 86ecf3abb..f16cb2f6e 100644 --- a/lib/src/screens/dashboard/widgets/new_main_navbar_widget.dart +++ b/lib/src/screens/dashboard/widgets/new_main_navbar_widget.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'dart:ui'; import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:cake_wallet/entities/new_main_actions.dart'; import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; @@ -123,124 +124,126 @@ class _NEWNewMainNavBarState extends State { final activeColor = theme.colorScheme.onSurface; final inactiveColor = theme.colorScheme.primary; - final visibleActions = NewMainActions.all - .where( - (action) => action.canShow?.call(widget.dashboardViewModel) ?? true) - .toList(); + return Observer( + builder: (_) { + final visibleActions = NewMainActions.all + .where((action) => action.canShow?.call(widget.dashboardViewModel) ?? true) + .toList(); - final pillWidth = _estimatePillWidthForAction( - context, visibleActions[widget.selectedIndex], - color: activeColor); + final pillWidth = _estimatePillWidthForAction(context, visibleActions[widget.selectedIndex], + color: activeColor); - final barWidth = calcBarWidth(pillWidth, visibleActions.length); + final barWidth = calcBarWidth(pillWidth, visibleActions.length); - final currentAction = visibleActions[widget.selectedIndex]; + final currentAction = visibleActions[widget.selectedIndex]; - return Align( - alignment: Alignment.bottomCenter, - child: SafeArea( - bottom: !(Platform.isIOS), - top: false, - child: Padding( - // tux PLEASE consult me (malik) before removing this padding. - padding: EdgeInsets.only(bottom: NewMainNavBar.barBottomPadding), - child: AnimatedContainer( - duration: barResizeDuration, - curve: Curves.easeOutCubic, - width: barWidth, - child: ClipRSuperellipse( - borderRadius: BorderRadius.circular(barBorderRadius), - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 3, sigmaY: 3), - child: Container( - height: NewMainNavBar.barHeight, - decoration: ShapeDecoration( - color: backgroundColor, - shape: RoundedSuperellipseBorder(borderRadius: BorderRadius.circular(barBorderRadius), - side: const BorderSide(color: Color(0x14FFFFFF), width: 1), + return Align( + alignment: Alignment.bottomCenter, + child: SafeArea( + bottom: !(Platform.isIOS), + top: false, + child: Padding( + // tux PLEASE consult me (malik) before removing this padding. + padding: EdgeInsets.only(bottom: NewMainNavBar.barBottomPadding), + child: AnimatedContainer( + duration: barResizeDuration, + curve: Curves.easeOutCubic, + width: barWidth, + child: ClipRSuperellipse( + borderRadius: BorderRadius.circular(barBorderRadius), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 3, sigmaY: 3), + child: Container( + height: NewMainNavBar.barHeight, + decoration: ShapeDecoration( + color: backgroundColor, + shape: RoundedSuperellipseBorder(borderRadius: BorderRadius.circular(barBorderRadius), + side: const BorderSide(color: Color(0x14FFFFFF), width: 1), + ), ), - ), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: barHorizontalPadding), - child: Stack( - alignment: Alignment.center, - children: [ - AnimatedPill( - left: calcLeft(widget.selectedIndex, pillWidth), - pillColor: pillColor, - currentAction: currentAction, - pillIconHeight: pillIconHeight, - pillIconWidth: pillIconWidth, - pillIconSpacing: pillIconSpacing, - pillBorderRadius: pillBorderRadius, - contentColor: activeColor, - estimateWidthForAction: pillWidth, - pillTextStyle: pillTextStyle, - pillMoveDuration: pillMoveDuration, - pillResizeDuration: pillResizeDuration, - ), - for (int i = 0; i < visibleActions.length; i++) - AnimatedPositioned( - duration: pillResizeDuration, - width: iconBoxWidth, - left: calcLeft(i, pillWidth)+((i == widget.selectedIndex) ? iconHorizontalPadding/100 : 0), - curve: Curves.easeOutCubic, - child: InkWell( - splashFactory: NoSplash.splashFactory, - splashColor: Colors.transparent, - borderRadius: BorderRadius.circular(pillBorderRadius), - onTap: () => _onItemTap(i), - child: AnimatedContainer( - duration: _firstFrame - ? Duration.zero - : inactiveIconMoveDuration, - curve: Curves.easeOutCubic, - width: - i == widget.selectedIndex ? pillWidth : iconBoxWidth, - alignment: Alignment.center, - child: AnimatedAlign( - duration: inactiveIconFadeDuration, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: barHorizontalPadding), + child: Stack( + alignment: Alignment.center, + children: [ + AnimatedPill( + left: calcLeft(widget.selectedIndex, pillWidth), + pillColor: pillColor, + currentAction: currentAction, + pillIconHeight: pillIconHeight, + pillIconWidth: pillIconWidth, + pillIconSpacing: pillIconSpacing, + pillBorderRadius: pillBorderRadius, + contentColor: activeColor, + estimateWidthForAction: pillWidth, + pillTextStyle: pillTextStyle, + pillMoveDuration: pillMoveDuration, + pillResizeDuration: pillResizeDuration, + ), + for (int i = 0; i < visibleActions.length; i++) + AnimatedPositioned( + duration: pillResizeDuration, + width: iconBoxWidth, + left: calcLeft(i, pillWidth)+((i == widget.selectedIndex) ? iconHorizontalPadding/100 : 0), + curve: Curves.easeOutCubic, + child: InkWell( + splashFactory: NoSplash.splashFactory, + splashColor: Colors.transparent, + borderRadius: BorderRadius.circular(pillBorderRadius), + onTap: () => _onItemTap(i), + child: AnimatedContainer( + duration: _firstFrame + ? Duration.zero + : inactiveIconMoveDuration, curve: Curves.easeOutCubic, + width: + i == widget.selectedIndex ? pillWidth : iconBoxWidth, alignment: Alignment.center, - child: AnimatedScale( - duration: inactiveIconAppearDuration, + child: AnimatedAlign( + duration: inactiveIconFadeDuration, curve: Curves.easeOutCubic, - scale: (i == widget.selectedIndex) ? 0.857 : 1.0, - child: TweenAnimationBuilder( - tween: ColorTween( - begin: (i == widget.selectedIndex) ? inactiveColor : activeColor, - end: (i==widget.selectedIndex) ? activeColor : inactiveColor, - ), - duration: iconColorChangeDuration, - builder: (context, value, child) { - return Container( - height: NewMainNavBar.barHeight, - child: CakeImageWidget(imageUrl: - visibleActions[i].image, - width: iconWidth, - height: iconHeight, - //fit: BoxFit.scaleDown, - colorFilter: ColorFilter.mode( - value ?? inactiveColor, - BlendMode.srcIn, + alignment: Alignment.center, + child: AnimatedScale( + duration: inactiveIconAppearDuration, + curve: Curves.easeOutCubic, + scale: (i == widget.selectedIndex) ? 0.857 : 1.0, + child: TweenAnimationBuilder( + tween: ColorTween( + begin: (i == widget.selectedIndex) ? inactiveColor : activeColor, + end: (i==widget.selectedIndex) ? activeColor : inactiveColor, + ), + duration: iconColorChangeDuration, + builder: (context, value, child) { + return Container( + height: NewMainNavBar.barHeight, + child: CakeImageWidget(imageUrl: + visibleActions[i].image, + width: iconWidth, + height: iconHeight, + //fit: BoxFit.scaleDown, + colorFilter: ColorFilter.mode( + value ?? inactiveColor, + BlendMode.srcIn, + ), ), - ), - ); - } + ); + } + ), ), ), ), ), ), - ), - ], - ), - )), + ], + ), + )), + ), ), ), ), ), - ), + ); + }, ); } }