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
This commit is contained in:
malik1004x
2026-03-18 22:34:02 +01:00
committed by GitHub
parent 410c496bfc
commit 24cceb5795
8 changed files with 151 additions and 109 deletions
+20
View File
@@ -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
+12 -3
View File
@@ -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]
+4 -1
View File
@@ -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<AccountCustomizer> {
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]));
+4 -1
View File
@@ -292,7 +292,10 @@ class _NewSwapPageState extends State<NewSwapPage> {
_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) {
@@ -57,7 +57,7 @@ class _L2ActionWalletSelectorState extends State<L2ActionWalletSelector> {
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;
@@ -154,8 +154,10 @@ class SendTransactionDetails extends StatelessWidget {
String sumStr<T>(List<T> list, double Function(T) picker) =>
sumBy(list, picker).toString();
String sumWithUnit<T>(List<T> list, double Function(T) picker, String unit) =>
"${sumStr(list, picker)} $unit";
String sumWithUnit<T>(List<T> 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;
@@ -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);
@@ -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<NewMainNavBar> {
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<Color?>(
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<Color?>(
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,
),
),
),
);
}
);
}
),
),
),
),
),
),
),
],
),
)),
],
),
)),
),
),
),
),
),
),
);
},
);
}
}