CW-1468 Home Screen Top Bar Layout (#3576)
What changed, and why it matters
This commit is a routine user-interface redesign for the Cake Wallet home screen. It moves the wallet name and account customizer into a new top bar, adds an 'Accounts' entry in settings, and adds or updates widget tests. There is no indication in the commit that it fixes a security vulnerability or changes security-sensitive logic such as authentication, encryption, key handling, or transaction signing.
No security action required. Review as normal UI/UX code.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The diff refactors Flutter UI code: TopBar now conditionally swaps between a SyncBar and a WalletInfoBar; WalletInfoBar loses its account-customizer button and becomes a read-only label; account customization is reached via a new settings row or an existing cards tap; dependency injection and routing are updated accordingly. Extensive regression tests are added. No cryptographic, network, or permission changes are present.
Changed components
lib/new-ui/pages/home_page.dartlib/new-ui/pages/account_customizer.dartlib/new-ui/pages/settings_page.dartlib/new-ui/widgets/coins_page/top_bar_widget/top_bar.dartlib/new-ui/widgets/coins_page/top_bar_widget/sync_bar.dartlib/new-ui/widgets/coins_page/wallet_info.dartlib/di.dartlib/router.dartlib/routes.dartInspect captured patch +1260 / −336
diff --git a/lib/di.dart b/lib/di.dart
index 6f6dda7e..b12d1557 100644
--- a/lib/di.dart
+++ b/lib/di.dart
@@ -602,6 +602,13 @@ Future<void> setup({
getIt.registerFactory<AccountCreationModal>(() => AccountCreationModal(
accountEditOrCreateViewModel: getIt.get<MoneroAccountEditOrCreateViewModel>()));
+ getIt.registerFactoryParam<AccountCustomizer, DashboardViewModel, void>(
+ (dashboardViewModel, _) => AccountCustomizer(
+ accountListViewModel: getIt.get<MoneroAccountListViewModel>(),
+ dashboardViewModel: dashboardViewModel,
+ ),
+ );
+
getIt.registerFactory<LightningUsernameBloc>(
() => LightningUsernameBloc(getIt.get<AppStore>().wallet!));
diff --git a/lib/new-ui/pages/account_customizer.dart b/lib/new-ui/pages/account_customizer.dart
index ff9d6f59..8dd2ff26 100644
--- a/lib/new-ui/pages/account_customizer.dart
+++ b/lib/new-ui/pages/account_customizer.dart
@@ -1,7 +1,6 @@
import 'dart:ui';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/generated/i18n.dart';
-import 'package:cake_wallet/monero/monero.dart';
import 'package:cake_wallet/new-ui/pages/card_customizer.dart';
import 'package:cake_wallet/new-ui/viewmodels/card_customizer/card_customizer_bloc.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/cards/balance_card.dart';
@@ -24,7 +23,6 @@ import 'package:cw_core/utils/print_verbose.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
-import 'package:flutter_svg/flutter_svg.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
class AccountCustomizerListItem {
@@ -38,13 +36,9 @@ class AccountCustomizerListItem {
class AccountCustomizer extends StatefulWidget {
const AccountCustomizer(
- {super.key,
- required this.accountListViewModel,
- required this.accountEditOrCreateViewModel,
- required this.dashboardViewModel});
+ {super.key, required this.accountListViewModel, required this.dashboardViewModel});
final MoneroAccountListViewModel accountListViewModel;
- final MoneroAccountEditOrCreateViewModel accountEditOrCreateViewModel;
final DashboardViewModel dashboardViewModel;
@override
@@ -62,7 +56,7 @@ class _AccountCustomizerState extends State<AccountCustomizer> {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
loadCards();
- final activeId = monero!.getCurrentAccount(widget.dashboardViewModel.wallet).id;
+ final activeId = widget.accountListViewModel.selected.id;
for (int i = 0; i < _items.length - 1; i++) {
if (_items[i].accountListItem.id == activeId) {
final lastIndex = _items.length - 1;
diff --git a/lib/new-ui/pages/home_page.dart b/lib/new-ui/pages/home_page.dart
index a7ba2694..9fd87e20 100644
--- a/lib/new-ui/pages/home_page.dart
+++ b/lib/new-ui/pages/home_page.dart
@@ -14,11 +14,9 @@ import 'package:cake_wallet/new-ui/widgets/coins_page/cards/cards_view.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/mweb_ad.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/top_bar_widget/top_bar.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/unconfirmed_balance_widget.dart';
-import 'package:cake_wallet/new-ui/widgets/coins_page/wallet_info.dart';
import "package:cake_wallet/new-ui/widgets/coins_page/zcash_migration_modal.dart";
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/nft_view_model.dart';
-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:flutter/cupertino.dart';
import 'package:flutter/material.dart';
@@ -115,6 +113,7 @@ class _NewHomePageState extends State<NewHomePage> {
spacing: 24.0,
children: [
TopBar(
+ key: ValueKey(widget.dashboardViewModel.wallet.id),
dashboardViewModel: widget.dashboardViewModel,
lightningMode: _lightningMode,
onLightningSwitchPress: () {
@@ -122,8 +121,8 @@ class _NewHomePageState extends State<NewHomePage> {
_lightningMode = !_lightningMode;
});
},
- onSettingsButtonPress: () {
- CupertinoScaffold.showCupertinoModalBottomSheet(
+ onSettingsButtonPress: () async {
+ await CupertinoScaffold.showCupertinoModalBottomSheet(
context: context,
barrierColor: Colors.black.withAlpha(85),
builder: (context) => FractionallySizedBox(
@@ -133,70 +132,77 @@ class _NewHomePageState extends State<NewHomePage> {
authService: getIt.get<AuthService>(),
))),
);
+
+ if (!mounted) {
+ return;
+ }
+ _setAccountViewModel();
+ await widget.dashboardViewModel.loadCardDesigns();
+ if (mounted) {
+ setState(() {});
+ }
},
),
- Observer(
- builder: (_) => WalletInfoBar(
- lightningMode: _lightningMode,
- hardwareWalletType: widget.dashboardViewModel.wallet.hardwareWalletType,
- name: widget.dashboardViewModel.wallet.name,
- hasCustomize: accountListViewModel != null,
- onCustomizeButtonTap: openAccountCustomizer),
- ),
Column(
+ spacing: 20,
children: [
- Observer(
- builder: (_) => CardsView(
- key: ValueKey(widget.dashboardViewModel.wallet.name),
- onCustomizeTapped: openCardCustomizer,
- dashboardViewModel: widget.dashboardViewModel,
- accountListViewModel: accountListViewModel,
- onCompactModeBackgroundCardsTapped: openAccountCustomizer,
- lightningMode: _lightningMode,
- ),
- ),
- Observer(builder: (_) {
- return AnimatedSize(
- duration: Duration(milliseconds: 150),
- curve: Curves.easeInOutCubic,
- child: (widget.dashboardViewModel.shouldShowBalanceHiddenMessage)
- ? Column(
- children: [
- SizedBox(
- height: 12,
- width: double.infinity,
- ),
- Text(
- S.of(context).long_press_show_balance,
- style: TextStyle(
- color:
- Theme.of(context).colorScheme.onSurfaceVariant),
- )
- ],
- )
- : SizedBox(width: double.infinity),
- );
- }),
- UnconfirmedBalanceWidget(
- dashboardViewModel: widget.dashboardViewModel,
- ),
- ],
- ),
- Observer(
- builder: (_) {
- return Column(
+ Column(
children: [
- CoinActionRow(
- lightningMode: _lightningMode,
- showSwap: widget.dashboardViewModel.isEnabledSwapAction,
- walletType: widget.dashboardViewModel.wallet.type,
+ Observer(
+ builder: (_) => CardsView(
+ key: ValueKey(widget.dashboardViewModel.wallet.name),
+ onCustomizeTapped: openCardCustomizer,
+ dashboardViewModel: widget.dashboardViewModel,
+ accountListViewModel: accountListViewModel,
+ onCompactModeBackgroundCardsTapped: openAccountCustomizer,
+ lightningMode: _lightningMode,
+ ),
),
- MwebAd(
+ Observer(builder: (_) {
+ return AnimatedSize(
+ duration: Duration(milliseconds: 150),
+ curve: Curves.easeInOutCubic,
+ child: (widget.dashboardViewModel.shouldShowBalanceHiddenMessage)
+ ? Column(
+ children: [
+ SizedBox(
+ height: 12,
+ width: double.infinity,
+ ),
+ Text(
+ S.of(context).long_press_show_balance,
+ style: TextStyle(
+ color: Theme.of(context)
+ .colorScheme
+ .onSurfaceVariant),
+ )
+ ],
+ )
+ : SizedBox(width: double.infinity),
+ );
+ }),
+ UnconfirmedBalanceWidget(
dashboardViewModel: widget.dashboardViewModel,
),
],
- );
- },
+ ),
+ Observer(
+ builder: (_) {
+ return Column(
+ children: [
+ CoinActionRow(
+ lightningMode: _lightningMode,
+ showSwap: widget.dashboardViewModel.isEnabledSwapAction,
+ walletType: widget.dashboardViewModel.wallet.type,
+ ),
+ MwebAd(
+ dashboardViewModel: widget.dashboardViewModel,
+ ),
+ ],
+ );
+ },
+ ),
+ ],
),
],
),
@@ -234,7 +240,12 @@ class _NewHomePageState extends State<NewHomePage> {
);
}
- void openAccountCustomizer() async {
+ Future<void> openAccountCustomizer() async {
+ final accountList = accountListViewModel;
+ if (accountList == null) {
+ return;
+ }
+
await CupertinoScaffold.showCupertinoModalBottomSheet(
barrierColor: Colors.black.withAlpha(60),
context: context,
@@ -243,15 +254,15 @@ class _NewHomePageState extends State<NewHomePage> {
parentContext: context,
heightMode: ModalHeightModes.fullScreen,
rootPage: Material(
- child: AccountCustomizer(
- accountListViewModel: accountListViewModel!,
- accountEditOrCreateViewModel: getIt.get<MoneroAccountEditOrCreateViewModel>(),
- dashboardViewModel: widget.dashboardViewModel,
- )),
+ child: AccountCustomizer(
+ accountListViewModel: accountList,
+ dashboardViewModel: widget.dashboardViewModel,
+ ),
+ ),
);
},
);
- widget.dashboardViewModel.loadCardDesigns();
+ await widget.dashboardViewModel.loadCardDesigns();
}
void openCardCustomizer() async {
diff --git a/lib/new-ui/pages/settings_page.dart b/lib/new-ui/pages/settings_page.dart
index 8aaec520..ea216c43 100644
--- a/lib/new-ui/pages/settings_page.dart
+++ b/lib/new-ui/pages/settings_page.dart
@@ -25,6 +25,8 @@ bool _hasMweb(DashboardViewModel vm) => vm.hasMweb;
bool _hasWalletConnect(DashboardViewModel vm) => vm.hasWalletConnect;
+bool _hasAccounts(DashboardViewModel vm) => vm.balanceViewModel.hasAccounts;
+
bool _requiresKeyImageSync(DashboardViewModel vm) =>
vm.wallet.type == WalletType.monero &&
[HardwareWalletType.cupcake, HardwareWalletType.trezor].contains(vm.wallet.hardwareWalletType);
@@ -38,12 +40,14 @@ class SettingsListItem {
this.use2fa = _falseFunc,
this.condition = _trueFunc,
this.routeArgs,
+ this.routeArgsBuilder,
});
final String iconPath;
final String title;
final String route;
final Object? routeArgs;
+ final Object? Function(DashboardViewModel)? routeArgsBuilder;
final bool requireAuth;
final bool Function(DashboardViewModel) use2fa;
final bool Function(DashboardViewModel) condition;
@@ -58,6 +62,13 @@ class SettingsSectionData {
static SettingsSectionData walletSettings =
SettingsSectionData(S.current.wallet_settings, "assets/new-ui/wallet-setting.svg", [
+ SettingsListItem(
+ "assets/new-ui/settings_row_icons/accounts.svg",
+ S.current.accounts,
+ Routes.accountCustomizer,
+ condition: _hasAccounts,
+ routeArgsBuilder: (vm) => vm,
+ ),
SettingsListItem(
"assets/new-ui/settings_row_icons/nodes.svg", S.current.nodes, Routes.manageNodes),
SettingsListItem(
@@ -148,7 +159,11 @@ class SettingsMainPage extends StatelessWidget {
conditionToDetermineIfToUse2FA: item.use2fa(dashboardViewModel),
route: item.route);
} else {
- Navigator.of(context).pushNamed(item.route, arguments: item.routeArgs);
+ Navigator.of(context).pushNamed(
+ item.route,
+ arguments:
+ item.routeArgsBuilder?.call(dashboardViewModel) ?? item.routeArgs,
+ );
}
}
})
diff --git a/lib/new-ui/widgets/coins_page/assets_history/assets_top_bar.dart b/lib/new-ui/widgets/coins_page/assets_history/assets_top_bar.dart
index e04a179a..cceb6ac0 100644
--- a/lib/new-ui/widgets/coins_page/assets_history/assets_top_bar.dart
+++ b/lib/new-ui/widgets/coins_page/assets_history/assets_top_bar.dart
@@ -26,7 +26,12 @@ class AssetsTopBar extends StatelessWidget {
return SliverToBoxAdapter(
child: Padding(
- padding: const EdgeInsets.only(top: 32.0, bottom: 0.0, left: 12.0, right: 18.0),
+ padding: const EdgeInsets.only(
+ top: 20,
+ bottom: 0,
+ left: 12,
+ right: 18,
+ ),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
diff --git a/lib/new-ui/widgets/coins_page/assets_history/history_top_bar.dart b/lib/new-ui/widgets/coins_page/assets_history/history_top_bar.dart
index 85865109..0203d59b 100644
--- a/lib/new-ui/widgets/coins_page/assets_history/history_top_bar.dart
+++ b/lib/new-ui/widgets/coins_page/assets_history/history_top_bar.dart
@@ -20,7 +20,7 @@ class HistoryTopBar extends StatelessWidget {
onTap: onTap,
behavior: HitTestBehavior.opaque,
child: Padding(
- padding: const EdgeInsets.only(left: 16, right: 16, top: 24),
+ padding: const EdgeInsets.only(left: 16, right: 16, top: 20),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.vertical(
diff --git a/lib/new-ui/widgets/coins_page/top_bar_widget/lightning_switcher.dart b/lib/new-ui/widgets/coins_page/top_bar_widget/lightning_switcher.dart
index 6ea00051..35a29cde 100644
--- a/lib/new-ui/widgets/coins_page/top_bar_widget/lightning_switcher.dart
+++ b/lib/new-ui/widgets/coins_page/top_bar_widget/lightning_switcher.dart
@@ -30,64 +30,59 @@ class LightningSwitcher extends StatelessWidget {
shape:
RoundedSuperellipseBorder(borderRadius: BorderRadiusGeometry.circular(900.0)),
color: Theme.of(context).colorScheme.surfaceContainer),
- width: 70,
+ width: 63,
height: 36,
- padding: EdgeInsets.symmetric(vertical: 2),
+ padding: const EdgeInsets.all(4.5),
child: Stack(
children: [
AnimatedContainer(
alignment: Alignment.centerRight,
- margin: EdgeInsets.only(left: lightningMode ? 36 : 2),
- duration: Duration(milliseconds: 250),
+ margin: EdgeInsets.only(left: lightningMode ? 27 : 0),
+ duration: const Duration(milliseconds: 250),
curve: Curves.easeOutCubic,
- width: 32,
- height: 32,
- // height: double.infinity,
+ width: 27,
+ height: 27,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(9999990.0)),
color: Theme.of(context).colorScheme.primary),
),
- Container(
- child: Row(
- spacing: 2.0,
- children: [
- SizedBox(),
- AnimatedSwitcher(
- duration: Duration(milliseconds: 150),
- transitionBuilder: (child, animation) =>
- FadeTransition(opacity: animation, child: child),
- child: CakeImageWidget(
- imageUrl: 'assets/new-ui/switcher-bitcoin.svg',
- key: ValueKey(lightningMode),
- width: 32,
- height: 32,
- colorFilter: ColorFilter.mode(
- lightningMode
- ? Theme.of(context).colorScheme.primary
- : Theme.of(context).colorScheme.surfaceContainer,
- BlendMode.srcIn,
- ),
+ Row(
+ children: [
+ AnimatedSwitcher(
+ duration: const Duration(milliseconds: 150),
+ transitionBuilder: (child, animation) =>
+ FadeTransition(opacity: animation, child: child),
+ child: CakeImageWidget(
+ imageUrl: "assets/new-ui/switcher-bitcoin.svg",
+ key: ValueKey(lightningMode),
+ width: 27,
+ height: 27,
+ colorFilter: ColorFilter.mode(
+ lightningMode
+ ? Theme.of(context).colorScheme.primary
+ : Theme.of(context).colorScheme.surfaceContainer,
+ BlendMode.srcIn,
),
),
- AnimatedSwitcher(
- duration: Duration(milliseconds: 150),
- transitionBuilder: (child, animation) =>
- FadeTransition(opacity: animation, child: child),
- child: CakeImageWidget(
- imageUrl: 'assets/new-ui/switcher-lightning.svg',
- key: ValueKey(lightningMode),
- width: 32,
- height: 32,
- colorFilter: ColorFilter.mode(
- lightningMode
- ? Theme.of(context).colorScheme.surfaceContainer
- : Theme.of(context).colorScheme.primary,
- BlendMode.srcIn,
- ),
+ ),
+ AnimatedSwitcher(
+ duration: const Duration(milliseconds: 150),
+ transitionBuilder: (child, animation) =>
+ FadeTransition(opacity: animation, child: child),
+ child: CakeImageWidget(
+ imageUrl: "assets/new-ui/switcher-lightning.svg",
+ key: ValueKey(lightningMode),
+ width: 27,
+ height: 27,
+ colorFilter: ColorFilter.mode(
+ lightningMode
+ ? Theme.of(context).colorScheme.surfaceContainer
+ : Theme.of(context).colorScheme.primary,
+ BlendMode.srcIn,
),
),
- ],
- ),
+ ),
+ ],
),
],
),
diff --git a/lib/new-ui/widgets/coins_page/top_bar_widget/sync_bar.dart b/lib/new-ui/widgets/coins_page/top_bar_widget/sync_bar.dart
index f38f72b0..9bf3ec04 100644
--- a/lib/new-ui/widgets/coins_page/top_bar_widget/sync_bar.dart
+++ b/lib/new-ui/widgets/coins_page/top_bar_widget/sync_bar.dart
@@ -3,6 +3,7 @@ import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/settings/manage_nodes_page.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
+import "package:cake_wallet/themes/core/theme_extension.dart";
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:cw_core/sync_status.dart';
import "package:flutter/cupertino.dart";
@@ -11,11 +12,11 @@ import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
class SyncBar extends StatelessWidget {
- SyncBar({
- super.key,
+ const SyncBar({
required this.dashboardViewModel,
required this.isSyncHeavy,
required this.showSyncedMessage,
+ super.key,
});
final DashboardViewModel dashboardViewModel;
@@ -46,106 +47,126 @@ class SyncBar extends StatelessWidget {
@override
Widget build(BuildContext context) => Observer(
- builder: (_) {
- final status = dashboardViewModel.status;
- final Widget? icon = _getIcon(context, status.runtimeType);
-
- return Expanded(
- child: Stack(
- alignment: Alignment.centerLeft,
- children: [
- if (!_showFullBar()) _buildCompactBar(context),
- if (_showFullBar())
- // A single node: the localized status text (plus any active
- // Tor/MWEB/Silent Payments badge) is the label, and the hint says
- // where tapping leads. Everything inside is redundant with it.
- Semantics(
- button: true,
- label: _statusSemanticsLabel(context, status),
- hint: S.of(context).manage_nodes,
- onTap: () => _openNodeManagement(context),
- child: ExcludeSemantics(
- child: GestureDetector(
- onTap: () => _openNodeManagement(context),
- child: AnimatedSwitcher(
- duration: Duration(milliseconds: 100),
- child: Container(
- key: ValueKey(status.runtimeType),
- height: 36,
- decoration: BoxDecoration(
- borderRadius: BorderRadius.circular(9999),
- border: _getBorder(context, status.runtimeType),
- color: _getBackgroundColor(context, status.runtimeType),
- ),
- child: Row(
- spacing: 10,
- mainAxisAlignment: MainAxisAlignment.center,
- crossAxisAlignment: CrossAxisAlignment.center,
- mainAxisSize: MainAxisSize.max,
- children: [
- if (icon != null) icon,
- // if (dashboardViewModel.silentPaymentsScanningActive &&
- // progressStatuses.contains(status.runtimeType)) ...[
- // Text(
- // "${(status.progress() * 100).toInt()}%",
- // style: TextStyle(fontSize: 12, color: Color(0xFFEFBA5E)),
- // ),
- // Text(
- // "·",
- // style: TextStyle(fontSize: 12),
- // )
- // ],
- Text(
- syncStatusTitle(status,
- dashboardViewModel.settingsStore.syncStatusDisplayMode),
- style: _getTextStyle(context, status.runtimeType),
+ builder: (_) {
+ final status = dashboardViewModel.status;
+ if (!_showFullBar) {
+ return _buildCompactBar(context);
+ }
+
+ final Widget? icon = _getIcon(context, status.runtimeType);
+ final statusTitle = _statusTitle(context, status);
+ final silentPaymentsProgress = _silentPaymentsProgress(status);
+ final barHeight = silentPaymentsProgress == null ? 36.0 : 40.0;
+
+ // A single node: the localized status text (plus any active
+ // Tor/MWEB/Silent Payments badge) is the label, and the hint says
+ // where tapping leads. Everything inside is redundant with it.
+ return Semantics(
+ button: true,
+ label: _statusSemanticsLabel(context, status, statusTitle),
+ value: silentPaymentsProgress,
+ hint: S.of(context).manage_nodes,
+ onTap: () => _openNodeManagement(context),
+ child: ExcludeSemantics(
+ child: GestureDetector(
+ onTap: () => _openNodeManagement(context),
+ child: AnimatedSwitcher(
+ duration: const Duration(milliseconds: 100),
+ child: OverflowBox(
+ key: ValueKey(status.runtimeType),
+ alignment: Alignment.center,
+ minHeight: barHeight,
+ maxHeight: barHeight,
+ child: Container(
+ height: barHeight,
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(9999),
+ border: _getBorder(context, status.runtimeType),
+ color: _getBackgroundColor(context, status.runtimeType),
+ ),
+ padding: const EdgeInsets.symmetric(horizontal: 12),
+ child: Row(
+ spacing: 8,
+ mainAxisAlignment: MainAxisAlignment.center,
+ crossAxisAlignment: CrossAxisAlignment.center,
+ mainAxisSize: MainAxisSize.max,
+ children: [
+ if (icon != null) icon,
+ if (silentPaymentsProgress != null) ...[
+ Text(
+ silentPaymentsProgress,
+ style: TextStyle(
+ fontSize: 12,
+ color: context.customColors.warningOutlineColor,
),
- ],
+ ),
+ Text(
+ "·",
+ style: TextStyle(
+ fontSize: 12,
+ color: Theme.of(context).colorScheme.onSurfaceVariant,
+ ),
+ ),
+ ],
+ Flexible(
+ child: Text(
+ statusTitle,
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ style: _getTextStyle(context, status.runtimeType),
+ ),
),
- ),
+ ],
),
),
),
),
- ],
- ),
- );
- },
- );
+ ),
+ ),
+ );
+ },
+ );
- void _openNodeManagement(BuildContext context) =>
- CupertinoScaffold.showCupertinoModalBottomSheet(
- context: context,
- barrierColor: Colors.black.withAlpha(85),
- builder: (context) => FractionallySizedBox(
- child: Material(
- child: getIt.get<ManageNodesPage>(param1: false),
- )));
+ void _openNodeManagement(BuildContext context) {
+ CupertinoScaffold.showCupertinoModalBottomSheet(
+ context: context,
+ barrierColor: Colors.black.withAlpha(85),
+ builder: (context) => FractionallySizedBox(
+ child: Material(
+ child: getIt.get<ManageNodesPage>(param1: false),
+ ),
+ ),
+ );
+ }
/// Compact mode shows sync state with a pulsing dot (and a Tor glyph) only, so
/// the whole row needs a text equivalent.
Widget _buildCompactBar(BuildContext context) {
final row = Row(
mainAxisSize: MainAxisSize.min,
+ mainAxisAlignment: MainAxisAlignment.start,
spacing: 6,
children: [
if (dashboardViewModel.isTorEnabled)
- CakeImageWidget(
+ const CakeImageWidget(
imageUrl: "assets/new-ui/tor.svg",
width: 20,
height: 20,
),
if (_showDot()) const CupertinoActivityIndicator(radius: 8),
- if(_showLightSyncCheck()) const Icon(Icons.check, color: syncedColor, size: 18),
- ],
+ if (_showLightSyncCheck()) const Icon(Icons.check, color: syncedColor, size: 18),
+ ],
);
final label = _joinLabels([
if (dashboardViewModel.isTorEnabled) S.of(context).tor_connection,
if (_showDot()) S.of(context).synchronizing,
+ if (_showLightSyncCheck()) S.of(context).sync_synced,
]);
- if (label.isEmpty) return row;
+ if (label.isEmpty) {
+ return row;
+ }
return Semantics(label: label, child: ExcludeSemantics(child: row));
}
@@ -153,11 +174,17 @@ class SyncBar extends StatelessWidget {
bool get _isShowingSyncedMessage =>
showSyncedMessage && dashboardViewModel.status.runtimeType == SyncedSyncStatus;
- String _statusSemanticsLabel(BuildContext context, SyncStatus status) {
+ String _statusTitle(BuildContext context, SyncStatus status) {
+ final title = syncStatusTitle(status, dashboardViewModel.settingsStore.syncStatusDisplayMode);
+
+ return title.isNotEmpty ? title : S.of(context).synchronizing;
+ }
+
+ String _statusSemanticsLabel(BuildContext context, SyncStatus status, String statusTitle) {
final isFailure = failStatuses.contains(status.runtimeType);
return _joinLabels([
- syncStatusTitle(status, dashboardViewModel.settingsStore.syncStatusDisplayMode),
+ statusTitle,
if (!isFailure && dashboardViewModel.isTorEnabled) S.of(context).tor_connection,
if (!isFailure && dashboardViewModel.hasMweb) S.of(context).litecoin_mweb,
if (!isFailure && dashboardViewModel.hasSilentPayments) S.of(context).silent_payments,
@@ -166,6 +193,16 @@ class SyncBar extends StatelessWidget {
String _joinLabels(List<String> parts) => parts.where((part) => part.isNotEmpty).join(", ");
+ String? _silentPaymentsProgress(SyncStatus status) {
+ if (!dashboardViewModel.hasSilentPayments ||
+ !dashboardViewModel.silentPaymentsScanningActive ||
+ status is! SyncingSyncStatus) {
+ return null;
+ }
+
+ return "${(status.progress() * 100).toInt()}%";
+ }
+
Color? _getBackgroundColor(BuildContext context, Type status) {
if (failStatuses.contains(status)) {
return Theme.of(context).colorScheme.errorContainer.withAlpha(64);
@@ -192,14 +229,12 @@ class SyncBar extends StatelessWidget {
color = Theme.of(context).colorScheme.onSurfaceVariant;
}
- return TextStyle(
- fontSize: 12, fontWeight: FontWeight.w400, color: color);
+ return TextStyle(fontSize: 12, fontWeight: FontWeight.w400, color: color);
}
Widget? _getIcon(BuildContext context, Type status) {
-
- if(status == SyncedSyncStatus) {
- return Icon(Icons.check, color: syncedColor, size: 12);
+ if (status == SyncedSyncStatus) {
+ return const Icon(Icons.check, color: syncedColor, size: 12);
}
if (status == LostConnectionSyncStatus) {
@@ -219,22 +254,35 @@ class SyncBar extends StatelessWidget {
final List<Widget> children = [];
if (dashboardViewModel.isTorEnabled) {
- children.add(CakeImageWidget(
+ children.add(
+ const CakeImageWidget(
imageUrl: "assets/new-ui/tor_sync.svg",
- colorFilter: ColorFilter.mode(Color(0xFF8A38F5), BlendMode.srcIn)));
+ colorFilter: ColorFilter.mode(Color(0xFF8A38F5), BlendMode.srcIn),
+ ),
+ );
}
if (dashboardViewModel.hasMweb) {
- children.add(CakeImageWidget(
- imageUrl: "assets/new-ui/mweb_sync.svg",
- colorFilter:
- ColorFilter.mode(Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn),
- ));
+ children.add(
+ CakeImageWidget(
+ imageUrl: "assets/new-ui/mweb_sync.svg",
+ colorFilter:
+ ColorFilter.mode(Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn),
+ ),
+ );
}
if (dashboardViewModel.hasSilentPayments) {
- children.add(CakeImageWidget(
- imageUrl: "assets/new-ui/silent_sync.svg",
- colorFilter: ColorFilter.mode(Color(0xFFEFBA5E), BlendMode.srcIn),
- ));
+ children.add(
+ CakeImageWidget(
+ imageUrl: "assets/new-ui/silent_sync.svg",
+ width: 16,
+ height: 16,
+ colorFilter: ColorFilter.mode(context.customColors.warningOutlineColor, BlendMode.srcIn),
+ ),
+ );
+ }
+
+ if (children.isEmpty) {
+ return null;
}
return Row(
@@ -243,15 +291,16 @@ class SyncBar extends StatelessWidget {
);
}
- bool _showFullBar() {
- if (dashboardViewModel.status.runtimeType == SyncedSyncStatus)
+ bool get _showFullBar {
+ if (dashboardViewModel.status.runtimeType == SyncedSyncStatus) {
return isSyncHeavy && _isShowingSyncedMessage;
+ }
+
return isSyncHeavy || failStatuses.contains(dashboardViewModel.status.runtimeType);
}
bool _showDot() =>
!isSyncHeavy && progressStatuses.contains(dashboardViewModel.status.runtimeType);
- bool _showLightSyncCheck() =>
- !isSyncHeavy && _isShowingSyncedMessage;
+ bool _showLightSyncCheck() => !isSyncHeavy && _isShowingSyncedMessage;
}
diff --git a/lib/new-ui/widgets/coins_page/top_bar_widget/top_bar.dart b/lib/new-ui/widgets/coins_page/top_bar_widget/top_bar.dart
index 21035c25..026af46b 100644
--- a/lib/new-ui/widgets/coins_page/top_bar_widget/top_bar.dart
+++ b/lib/new-ui/widgets/coins_page/top_bar_widget/top_bar.dart
@@ -5,6 +5,7 @@ import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/top_bar_widget/chain_icon.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/top_bar_widget/lightning_switcher.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/top_bar_widget/sync_bar.dart';
+import "package:cake_wallet/new-ui/widgets/coins_page/wallet_info.dart";
import 'package:cake_wallet/new-ui/widgets/modern_button.dart';
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:cw_core/sync_status.dart';
@@ -15,11 +16,11 @@ import 'package:mobx/mobx.dart';
class TopBar extends StatefulWidget {
const TopBar({
- super.key,
required this.lightningMode,
required this.onLightningSwitchPress,
required this.dashboardViewModel,
required this.onSettingsButtonPress,
+ super.key,
});
final bool lightningMode;
@@ -36,25 +37,59 @@ class _TopBarState extends State<TopBar> {
bool showSyncedMessage = false;
Timer? syncedMessageTimer;
- late final ReactionDisposer? _statusReactionDisposer;
+ ReactionDisposer? _statusReactionDisposer;
+
+ bool get replacesWalletName {
+ final status = widget.dashboardViewModel.status.runtimeType;
+ if (status == SyncedSyncStatus) {
+ return showSyncedMessage;
+ }
+
+ return widget.dashboardViewModel.isSyncHeavy ||
+ SyncBar.progressStatuses.contains(status) ||
+ SyncBar.failStatuses.contains(status);
+ }
@override
void initState() {
super.initState();
+ _bindStatusReaction();
+ }
+ @override
+ void didUpdateWidget(covariant TopBar oldWidget) {
+ super.didUpdateWidget(oldWidget);
+
+ if (identical(oldWidget.dashboardViewModel, widget.dashboardViewModel)) {
+ return;
+ }
+
+ syncedMessageTimer?.cancel();
+ syncedMessageTimer = null;
+ showSyncedMessage = false;
+ _statusReactionDisposer?.call();
+ _bindStatusReaction();
+ }
+
+ void _bindStatusReaction() {
_statusReactionDisposer = reaction(
(_) => widget.dashboardViewModel.status.runtimeType,
(status) {
syncedMessageTimer?.cancel();
+ syncedMessageTimer = null;
if (status == SyncedSyncStatus) {
- if(mounted) {
+ if (mounted) {
setState(() => showSyncedMessage = true);
}
- syncedMessageTimer =
- Timer(syncedMessageDuration, () => setState(() => showSyncedMessage = false));
+ syncedMessageTimer = Timer(syncedMessageDuration, () {
+ syncedMessageTimer = null;
+ if (mounted) {
+ setState(() => showSyncedMessage = false);
+ }
+ });
} else {
- if(mounted) {
+ if (mounted) {
setState(() => showSyncedMessage = false);
}
}
@@ -65,51 +100,90 @@ class _TopBarState extends State<TopBar> {
@override
void dispose() {
syncedMessageTimer?.cancel();
- _statusReactionDisposer?.reaction.dispose();
+ syncedMessageTimer = null;
+ _statusReactionDisposer?.call();
super.dispose();
}
@override
Widget build(BuildContext context) => Padding(
- padding: EdgeInsets.only(
- bottom: 10, left: 18, right: 18, top: 10 + _additionalTopPadding(context)),
- child: Observer(
- builder: (_) => Row(
- spacing: 12,
- children: [
- (widget.dashboardViewModel.hasLightning)
- ? LightningSwitcher(
- lightningMode: widget.lightningMode,
- onLightningSwitchPress: widget.onLightningSwitchPress,
- )
- : ChainIcon(
- iconPath: widget.dashboardViewModel.wallet.currency.flatIconPath ?? "",
- dashboardViewModel: widget.dashboardViewModel,
- isSyncHeavy: widget.dashboardViewModel.isSyncHeavy,
- showSyncedMessage: showSyncedMessage),
- SyncBar(
- dashboardViewModel: widget.dashboardViewModel,
- isSyncHeavy: widget.dashboardViewModel.isSyncHeavy,
- showSyncedMessage: showSyncedMessage,
- ),
- ModernButton.svg(
- iconColor: Theme.of(context).colorScheme.primary,
- size: 36,
- onPressed: () {
- HapticFeedback.mediumImpact();
- widget.onSettingsButtonPress();
- },
- svgPath: "assets/new-ui/top-settings.svg",
- semanticLabel: S.of(context).settings_title,
- ),
- ],
+ padding: EdgeInsets.only(left: 18, right: 18, top: 10 + _additionalTopPadding(context)),
+ child: Observer(
+ builder: (_) => Row(
+ spacing: 12,
+ children: [
+ widget.dashboardViewModel.hasLightning
+ ? LightningSwitcher(
+ lightningMode: widget.lightningMode,
+ onLightningSwitchPress: widget.onLightningSwitchPress,
+ )
+ : ChainIcon(
+ iconPath: widget.dashboardViewModel.wallet.currency.flatIconPath ?? "",
+ dashboardViewModel: widget.dashboardViewModel,
+ isSyncHeavy: widget.dashboardViewModel.isSyncHeavy,
+ showSyncedMessage: showSyncedMessage,
+ ),
+ Expanded(
+ child: SizedBox(
+ height: 36,
+ child: AnimatedSwitcher(
+ duration: const Duration(milliseconds: 150),
+ reverseDuration: Duration.zero,
+ layoutBuilder: (currentChild, previousChildren) => Stack(
+ alignment: Alignment.centerLeft,
+ children: [
+ ...previousChildren,
+ if (currentChild != null) currentChild,
+ ],
+ ),
+ child: replacesWalletName
+ ? SyncBar(
+ dashboardViewModel: widget.dashboardViewModel,
+ isSyncHeavy: widget.dashboardViewModel.isSyncHeavy,
+ showSyncedMessage: showSyncedMessage,
+ )
+ : Row(
+ children: [
+ Expanded(
+ child: WalletInfoBar(
+ name: widget.dashboardViewModel.wallet.name,
+ hardwareWalletType:
+ widget.dashboardViewModel.wallet.hardwareWalletType,
+ ),
+ ),
+ if (widget.dashboardViewModel.isTorEnabled) ...[
+ const SizedBox(width: 6),
+ SyncBar(
+ dashboardViewModel: widget.dashboardViewModel,
+ isSyncHeavy: widget.dashboardViewModel.isSyncHeavy,
+ showSyncedMessage: showSyncedMessage,
+ ),
+ ],
+ ],
+ ),
+ ),
+ ),
+ ),
+ ModernButton.svg(
+ iconColor: Theme.of(context).colorScheme.primary,
+ size: 36,
+ onPressed: () {
+ HapticFeedback.mediumImpact();
+ widget.onSettingsButtonPress();
+ },
+ svgPath: "assets/new-ui/top-settings.svg",
+ semanticLabel: S.of(context).settings_title,
+ ),
+ ],
+ ),
),
- ),
- );
+ );
//FIXME remove after this gets fixed flutter-side
double _additionalTopPadding(BuildContext context) {
- if (Platform.isIOS && MediaQuery.of(context).viewPadding.top < 12) return 24;
+ if (Platform.isIOS && MediaQuery.of(context).viewPadding.top < 12) {
+ return 24;
+ }
return 0;
}
diff --git a/lib/new-ui/widgets/coins_page/wallet_info.dart b/lib/new-ui/widgets/coins_page/wallet_info.dart
index dcdbc23a..b7478abc 100644
--- a/lib/new-ui/widgets/coins_page/wallet_info.dart
+++ b/lib/new-ui/widgets/coins_page/wallet_info.dart
@@ -1,102 +1,67 @@
import 'package:cake_wallet/generated/i18n.dart';
-import 'package:cake_wallet/new-ui/widgets/modern_button.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:flutter/material.dart';
-import 'package:flutter/services.dart';
-import 'package:flutter_svg/flutter_svg.dart';
class WalletInfoBar extends StatelessWidget {
- const WalletInfoBar(
- {super.key,
- required this.lightningMode,
- required this.name,
- required this.hardwareWalletType,
- required this.onCustomizeButtonTap,
- required this.hasCustomize});
+ const WalletInfoBar({
+ required this.name,
+ required this.hardwareWalletType,
+ super.key,
+ });
- final bool lightningMode;
final String name;
final HardwareWalletType? hardwareWalletType;
- final bool hasCustomize;
- final VoidCallback onCustomizeButtonTap;
-
- void _openAccountCustomizer() {
- if (hasCustomize) {
- onCustomizeButtonTap();
- HapticFeedback.mediumImpact();
- }
- }
@override
Widget build(BuildContext context) {
- // The row, the hardware-wallet glyph and the inner accounts button are one
- // control for a screen reader: a single labeled node opening the customizer.
final semanticsLabel =
hardwareWalletType == null ? name : "$name, ${S.of(context).hardware_wallet}";
- final row = GestureDetector(
- onTap: _openAccountCustomizer,
- child: Row(
- mainAxisSize: MainAxisSize.min,
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- AnimatedSwitcher(
- duration: Duration(milliseconds: 150),
- transitionBuilder: (child, animation) {
- return SizeTransition(
+ return Semantics(
+ label: semanticsLabel,
+ child: ExcludeSemantics(
+ child: Row(
+ children: [
+ AnimatedSwitcher(
+ duration: const Duration(milliseconds: 150),
+ transitionBuilder: (child, animation) => SizeTransition(
axis: Axis.horizontal,
sizeFactor: animation,
child: FadeTransition(opacity: animation, child: child),
- );
- },
- child: hardwareWalletIcon == null
- ? const SizedBox.shrink(key: ValueKey("empty"))
- : Padding(
- padding: const EdgeInsets.only(right: 8),
- child: CakeImageWidget(
- imageUrl: hardwareWalletIcon!,
- key: ValueKey("hardware_wallet_icon"),
- width: 24,
- height: 24,
- colorFilter: ColorFilter.mode(
- Theme.of(context).colorScheme.onSurfaceVariant,
- BlendMode.srcIn,
+ ),
+ child: hardwareWalletIcon == null
+ ? const SizedBox.shrink(key: ValueKey("empty"))
+ : Padding(
+ padding: const EdgeInsets.only(right: 4),
+ child: CakeImageWidget(
+ imageUrl: hardwareWalletIcon!,
+ key: const ValueKey("hardware_wallet_icon"),
+ width: 24,
+ height: 24,
+ colorFilter: ColorFilter.mode(
+ Theme.of(context).colorScheme.onSurfaceVariant,
+ BlendMode.srcIn,
+ ),
),
),
- ),
- ),
- Text(
- name,
- style: Theme.of(context)
- .textTheme
- .titleLarge
- ?.copyWith(color: Theme.of(context).colorScheme.onSurface),
- ),
- if (hasCustomize) ...[
- SizedBox(width: 8),
- ModernButton.svg(
- size: 24,
- onPressed: _openAccountCustomizer,
- svgPath: "assets/new-ui/icon-accounts.svg",
- semanticLabel: S.of(context).wallet_accounts,
- )
- ]
- ],
+ ),
+ Expanded(
+ child: Text(
+ name,
+ maxLines: 1,
+ softWrap: false,
+ overflow: TextOverflow.ellipsis,
+ style: Theme.of(context)
+ .textTheme
+ .titleMedium
+ ?.copyWith(color: Theme.of(context).colorScheme.onSurface),
+ ),
+ ),
+ ],
+ ),
),
);
-
- if (!hasCustomize) {
- return Semantics(label: semanticsLabel, child: ExcludeSemantics(child: row));
- }
-
- return Semantics(
- button: true,
- label: semanticsLabel,
- hint: S.of(context).wallet_accounts,
- onTap: _openAccountCustomizer,
- child: ExcludeSemantics(child: row),
- );
}
String? get hardwareWalletIcon {
diff --git a/lib/router.dart b/lib/router.dart
index 3760d1e1..f265ff29 100644
--- a/lib/router.dart
+++ b/lib/router.dart
@@ -4,6 +4,7 @@ import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart';
import "package:cake_wallet/core/auth_service.dart";
import 'package:cake_wallet/core/new_wallet_arguments.dart';
import 'package:cake_wallet/new-ui/new_dashboard.dart';
+import "package:cake_wallet/new-ui/pages/account_customizer.dart";
import 'package:cake_wallet/new-ui/pages/about_page.dart';
import 'package:cake_wallet/new-ui/pages/bridge/bridge_history_page.dart';
import 'package:cake_wallet/new-ui/pages/bridge/bridge_network_page.dart';
@@ -670,6 +671,13 @@ Route<dynamic> createRoute(RouteSettings settings) {
builder: (_) => getIt.get<MoneroAccountEditOrCreatePage>(
param1: settings.arguments as AccountListItem?));
+ case Routes.accountCustomizer:
+ return handleRouteWithPlatformAwareness(
+ (_) => getIt.get<AccountCustomizer>(
+ param1: settings.arguments! as DashboardViewModel,
+ ),
+ );
+
case Routes.nanoAccountCreation:
return CupertinoPageRoute<String>(
builder: (_) =>
diff --git a/lib/routes.dart b/lib/routes.dart
index 9417a3e5..96283c9b 100644
--- a/lib/routes.dart
+++ b/lib/routes.dart
@@ -32,6 +32,7 @@ class Routes {
static const login = '/login';
static const splash = '/splash';
static const accountCreation = '/account_new';
+ static const accountCustomizer = "/account_customizer";
static const nanoAccountCreation = '/nano_account_new';
static const addressBook = '/address_book';
static const pickerAddressBook = '/picker_address_book';
diff --git a/res/pictures/settings_row_icons/accounts.svg b/res/pictures/settings_row_icons/accounts.svg
new file mode 100644
index 00000000..6afc7088
--- /dev/null
+++ b/res/pictures/settings_row_icons/accounts.svg
@@ -0,0 +1,28 @@
+<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="Setttings Icons" clip-path="url(#clip0_0_342)">
+<path id="bg" d="M0 8C0 5.19974 0 3.79961 0.544967 2.73005C1.02433 1.78924 1.78924 1.02433 2.73005 0.544967C3.79961 0 5.19974 0 8 0H16C18.8003 0 20.2004 0 21.27 0.544967C22.2108 1.02433 22.9757 1.78924 23.455 2.73005C24 3.79961 24 5.19974 24 8V16C24 18.8003 24 20.2004 23.455 21.27C22.9757 22.2108 22.2108 22.9757 21.27 23.455C20.2004 24 18.8003 24 16 24H8C5.19974 24 3.79961 24 2.73005 23.455C1.78924 22.9757 1.02433 22.2108 0.544967 21.27C0 20.2004 0 18.8003 0 16V8Z" fill="url(#paint0_linear_0_342)"/>
+<g id="BUTTON">
+<g id="Union">
+<path d="M16.4007 4.38126C16.7865 4.38126 17.1565 4.53448 17.4293 4.80725C17.7021 5.08003 17.8553 5.45008 17.8553 5.83584H6.21905C6.21905 5.45007 6.37227 5.08003 6.64505 4.80725C6.91782 4.5345 7.28776 4.38126 7.6735 4.38126H16.4007Z" fill="url(#paint1_linear_0_342)"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M20.0371 17.4313C20.0371 17.8171 19.8839 18.1871 19.6111 18.4599C19.3383 18.7327 18.9683 18.8859 18.5825 18.8859H5.49169C5.10595 18.8859 4.73601 18.7327 4.46324 18.4599C4.19046 18.1871 4.03711 17.8171 4.03711 17.4313V8.74487C4.03711 8.35911 4.19047 7.9892 4.46324 7.71642C4.73602 7.44365 5.10594 7.29029 5.49169 7.29028H18.5825C18.9683 7.29028 19.3383 7.44364 19.6111 7.71642C19.8839 7.98919 20.0371 8.35913 20.0371 8.74487V17.4313ZM5.49169 17.4313H18.5825V8.74487H5.49169V17.4313Z" fill="url(#paint2_linear_0_342)"/>
+</g>
+</g>
+</g>
+<defs>
+<linearGradient id="paint0_linear_0_342" x1="12" y1="0" x2="12" y2="24" gradientUnits="userSpaceOnUse">
+<stop stop-color="#009393"/>
+<stop offset="1" stop-color="#005173"/>
+</linearGradient>
+<linearGradient id="paint1_linear_0_342" x1="12.0371" y1="4.38126" x2="12.0371" y2="18.8859" gradientUnits="userSpaceOnUse">
+<stop stop-color="white"/>
+<stop offset="1" stop-color="#88FCFF"/>
+</linearGradient>
+<linearGradient id="paint2_linear_0_342" x1="12.0371" y1="4.38126" x2="12.0371" y2="18.8859" gradientUnits="userSpaceOnUse">
+<stop stop-color="white"/>
+<stop offset="1" stop-color="#88FCFF"/>
+</linearGradient>
+<clipPath id="clip0_0_342">
+<rect width="24" height="24" fill="white"/>
+</clipPath>
+</defs>
+</svg>
diff --git a/test/new-ui/pages/settings_page_test.dart b/test/new-ui/pages/settings_page_test.dart
new file mode 100644
index 00000000..c06cc679
--- /dev/null
+++ b/test/new-ui/pages/settings_page_test.dart
@@ -0,0 +1,125 @@
+import "package:cake_wallet/core/auth_service.dart";
+import "package:cake_wallet/di.dart";
+import "package:cake_wallet/generated/i18n.dart";
+import "package:cake_wallet/locales/locale.dart";
+import "package:cake_wallet/new-ui/pages/account_customizer.dart";
+import "package:cake_wallet/new-ui/pages/settings_page.dart";
+import "package:cake_wallet/routes.dart";
+import "package:cake_wallet/view_model/dashboard/balance_view_model.dart";
+import "package:cake_wallet/view_model/dashboard/dashboard_view_model.dart";
+import "package:cake_wallet/view_model/monero_account_list/account_list_item.dart";
+import "package:cake_wallet/view_model/monero_account_list/monero_account_list_view_model.dart";
+import "package:cw_core/balance.dart";
+import "package:cw_core/transaction_history.dart";
+import "package:cw_core/transaction_info.dart";
+import "package:cw_core/wallet_base.dart";
+import "package:cw_core/wallet_type.dart";
+import "package:flutter/material.dart";
+import "package:flutter_test/flutter_test.dart";
+import "package:mocktail/mocktail.dart";
+
+class MockBalanceViewModel extends Mock implements BalanceViewModel {}
+
+class MockDashboardViewModel extends Mock implements DashboardViewModel {}
+
+class _MockAccountListViewModel extends Mock implements MoneroAccountListViewModel {}
+
+class _MockAuthService extends Mock implements AuthService {}
+
+class _MockWallet extends Mock
+ implements WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> {}
+
+void main() {
+ late MockBalanceViewModel balanceViewModel;
+ late MockDashboardViewModel dashboardViewModel;
+
+ setUpAll(() => S.delegate.load(const Locale("en")));
+
+ setUp(() {
+ balanceViewModel = MockBalanceViewModel();
+ dashboardViewModel = MockDashboardViewModel();
+ when(() => dashboardViewModel.balanceViewModel).thenReturn(balanceViewModel);
+ });
+
+ SettingsListItem accountsItem() => SettingsSectionData.walletSettings.items.singleWhere(
+ (item) => item.route == Routes.accountCustomizer,
+ );
+
+ test("Accounts is the first Wallet Settings item and routes with the dashboard", () {
+ final item = accountsItem();
+
+ expect(SettingsSectionData.walletSettings.items.first, same(item));
+ expect(item.iconPath, "assets/new-ui/settings_row_icons/accounts.svg");
+ expect(item.title, S.current.accounts);
+ expect(item.route, Routes.accountCustomizer);
+ expect(item.routeArgs, isNull);
+ expect(item.routeArgsBuilder, isNotNull);
+ expect(item.routeArgsBuilder!(dashboardViewModel), same(dashboardViewModel));
+ });
+
+ test("Accounts visibility follows account support", () {
+ final item = accountsItem();
+
+ when(() => balanceViewModel.hasAccounts).thenReturn(true);
+ expect(item.condition(dashboardViewModel), isTrue);
+
+ when(() => balanceViewModel.hasAccounts).thenReturn(false);
+ expect(item.condition(dashboardViewModel), isFalse);
+ });
+
+ testWidgets("Accounts opens through the settings modal with the active dashboard",
+ (tester) async {
+ final accountListViewModel = _MockAccountListViewModel();
+ final authService = _MockAuthService();
+ final wallet = _MockWallet();
+ DashboardViewModel? routedDashboardViewModel;
+
+ when(() => balanceViewModel.hasAccounts).thenReturn(true);
+ when(() => dashboardViewModel.hasLightning).thenReturn(false);
+ when(() => dashboardViewModel.hasWalletConnect).thenReturn(false);
+ when(() => dashboardViewModel.wallet).thenReturn(wallet);
+ when(() => dashboardViewModel.loadCardDesigns()).thenAnswer((_) async {});
+ when(() => wallet.type).thenReturn(WalletType.monero);
+ when(() => wallet.hardwareWalletType).thenReturn(null);
+ when(() => accountListViewModel.accounts).thenReturn(const []);
+ when(() => accountListViewModel.selected).thenReturn(
+ AccountListItem(label: "Primary", id: 0, isSelected: true),
+ );
+
+ getIt.pushNewScope(scopeName: "settings-accounts-route-test");
+ getIt.registerFactoryParam<AccountCustomizer, DashboardViewModel, void>(
+ (routedViewModel, _) {
+ routedDashboardViewModel = routedViewModel;
+ return AccountCustomizer(
+ accountListViewModel: accountListViewModel,
+ dashboardViewModel: routedViewModel,
+ );
+ },
+ );
+
+ try {
+ await tester.pumpWidget(
+ MaterialApp(
+ localizationsDelegates: localizationDelegates,
+ supportedLocales: S.delegate.supportedLocales,
+ home: NewSettingsPage(
+ dashboardViewModel: dashboardViewModel,
+ authService: authService,
+ ),
+ ),
+ );
+ await tester.pumpAndSettle();
+
+ await tester.tap(find.text(S.current.accounts));
+ await tester.pumpAndSettle();
+
+ expect(routedDashboardViewModel, same(dashboardViewModel));
+ expect(find.byType(AccountCustomizer), findsOneWidget);
+ expect(tester.takeException(), isNull);
+ } finally {
+ await tester.pumpWidget(const SizedBox.shrink());
+ await tester.pump();
+ await getIt.popScope();
+ }
+ });
+}
diff --git a/test/new-ui/widgets/coins_page/top_bar_widget/sync_bar_test.dart b/test/new-ui/widgets/coins_page/top_bar_widget/sync_bar_test.dart
new file mode 100644
index 00000000..ac5fe1db
--- /dev/null
+++ b/test/new-ui/widgets/coins_page/top_bar_widget/sync_bar_test.dart
@@ -0,0 +1,198 @@
+import "package:cake_wallet/di.dart";
+import "package:cake_wallet/entities/sync_status_display_mode.dart";
+import "package:cake_wallet/generated/i18n.dart";
+import "package:cake_wallet/locales/locale.dart";
+import "package:cake_wallet/new-ui/widgets/coins_page/top_bar_widget/sync_bar.dart";
+import "package:cake_wallet/src/widgets/cake_image_widget.dart";
+import "package:cake_wallet/store/settings_store.dart";
+import "package:cake_wallet/themes/core/theme_store.dart";
+import "package:cake_wallet/view_model/dashboard/dashboard_view_model.dart";
+import "package:cw_core/sync_status.dart";
+import "package:flutter/material.dart";
+import "package:flutter_test/flutter_test.dart";
+import "package:mobx/mobx.dart" show Observable;
+import "package:mocktail/mocktail.dart";
+
+class _ObservableDashboardViewModel extends Mock implements DashboardViewModel {
+ _ObservableDashboardViewModel(SyncStatus status) : _status = Observable(status);
+
+ final Observable<SyncStatus> _status;
+
+ @override
+ SyncStatus get status => _status.value;
+}
+
+class _MockSettingsStore extends Mock implements SettingsStore {}
+
+class _UnknownSyncStatus extends SyncStatus {
+ const _UnknownSyncStatus();
+
+ @override
+ double progress() => 0;
+}
+
+void main() {
+ late bool registeredThemeStore;
+
+ setUpAll(() {
+ registeredThemeStore = !getIt.isRegistered<ThemeStore>();
+ if (registeredThemeStore) {
+ getIt.registerSingleton(ThemeStore());
+ }
+ });
+
+ tearDownAll(() async {
+ if (registeredThemeStore) {
+ await getIt.unregister<ThemeStore>();
+ }
+ });
+
+ testWidgets("unknown sync-heavy statuses render a localized fallback", (tester) async {
+ const status = _UnknownSyncStatus();
+ final dashboardViewModel = _ObservableDashboardViewModel(status);
+ final settingsStore = _MockSettingsStore();
+
+ when(() => dashboardViewModel.settingsStore).thenReturn(settingsStore);
+ when(() => dashboardViewModel.isTorEnabled).thenReturn(false);
+ when(() => dashboardViewModel.hasMweb).thenReturn(false);
+ when(() => dashboardViewModel.hasSilentPayments).thenReturn(false);
+ when(() => settingsStore.syncStatusDisplayMode)
+ .thenReturn(SyncStatusDisplayMode.blocksRemaining);
+
+ await tester.pumpWidget(
+ MaterialApp(
+ localizationsDelegates: localizationDelegates,
+ supportedLocales: S.delegate.supportedLocales,
+ home: Scaffold(
+ body: SyncBar(
+ dashboardViewModel: dashboardViewModel,
+ isSyncHeavy: true,
+ showSyncedMessage: false,
+ ),
+ ),
+ ),
+ );
+ await tester.pump();
+
+ expect(find.text(S.current.synchronizing), findsOneWidget);
+ expect(find.bySemanticsLabel(S.current.synchronizing), findsOneWidget);
+ });
+
+ testWidgets("active Silent Payments syncing shows integer progress and its icon in a 40px pill",
+ (tester) async {
+ final dashboardViewModel = _ObservableDashboardViewModel(SyncingSyncStatus(100, 0.24));
+ final settingsStore = _MockSettingsStore();
+
+ when(() => dashboardViewModel.settingsStore).thenReturn(settingsStore);
+ when(() => dashboardViewModel.isTorEnabled).thenReturn(false);
+ when(() => dashboardViewModel.hasMweb).thenReturn(false);
+ when(() => dashboardViewModel.hasSilentPayments).thenReturn(true);
+ when(() => dashboardViewModel.silentPaymentsScanningActive).thenReturn(true);
+ when(() => settingsStore.syncStatusDisplayMode)
+ .thenReturn(SyncStatusDisplayMode.blocksRemaining);
+
+ await tester.pumpWidget(
+ MaterialApp(
+ localizationsDelegates: localizationDelegates,
+ supportedLocales: S.delegate.supportedLocales,
+ home: Scaffold(
+ body: Center(
+ child: SizedBox(
+ width: 210,
+ height: 36,
+ child: SyncBar(
+ dashboardViewModel: dashboardViewModel,
+ isSyncHeavy: true,
+ showSyncedMessage: false,
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ await tester.pump();
+
+ final progressText = tester.widget<Text>(find.text("24%"));
+ final pill = find.descendant(
+ of: find.byType(SyncBar),
+ matching: find.byWidgetPredicate(
+ (widget) =>
+ widget is Container &&
+ widget.constraints?.minHeight == 40 &&
+ widget.constraints?.maxHeight == 40,
+ ),
+ );
+ final silentPaymentsIcon = tester.widget<CakeImageWidget>(
+ find.byWidgetPredicate(
+ (widget) => widget is CakeImageWidget && widget.imageUrl == "assets/new-ui/silent_sync.svg",
+ ),
+ );
+
+ final warningOutlineColor = getIt<ThemeStore>().currentTheme.customColors.warningOutlineColor;
+
+ expect(progressText.style?.color, warningOutlineColor);
+ expect(find.text("·"), findsOneWidget);
+ expect(find.text(S.current.Blocks_remaining("100")), findsOneWidget);
+ expect(tester.getSize(pill).height, 40);
+ expect(Size(silentPaymentsIcon.width!, silentPaymentsIcon.height!), const Size(16, 16));
+ expect(
+ silentPaymentsIcon.colorFilter,
+ ColorFilter.mode(warningOutlineColor, BlendMode.srcIn),
+ );
+ expect(tester.getSemantics(find.text("24%")).value, "24%");
+ });
+
+ testWidgets("Silent Payments progress requires support, active scanning, and syncing",
+ (tester) async {
+ final testCases = <({bool hasSilentPayments, bool scanningActive, SyncStatus status})>[
+ (
+ hasSilentPayments: false,
+ scanningActive: true,
+ status: SyncingSyncStatus(100, 0.24),
+ ),
+ (
+ hasSilentPayments: true,
+ scanningActive: false,
+ status: SyncingSyncStatus(100, 0.24),
+ ),
+ (
+ hasSilentPayments: true,
+ scanningActive: true,
+ status: AttemptingScanSyncStatus(),
+ ),
+ ];
+
+ for (final testCase in testCases) {
+ final dashboardViewModel = _ObservableDashboardViewModel(testCase.status);
+ final settingsStore = _MockSettingsStore();
+
+ when(() => dashboardViewModel.settingsStore).thenReturn(settingsStore);
+ when(() => dashboardViewModel.isTorEnabled).thenReturn(false);
+ when(() => dashboardViewModel.hasMweb).thenReturn(false);
+ when(() => dashboardViewModel.hasSilentPayments).thenReturn(testCase.hasSilentPayments);
+ when(() => dashboardViewModel.silentPaymentsScanningActive)
+ .thenReturn(testCase.scanningActive);
+ when(() => settingsStore.syncStatusDisplayMode)
+ .thenReturn(SyncStatusDisplayMode.blocksRemaining);
+
+ await tester.pumpWidget(
+ MaterialApp(
+ localizationsDelegates: localizationDelegates,
+ supportedLocales: S.delegate.supportedLocales,
+ home: Scaffold(
+ body: SyncBar(
+ dashboardViewModel: dashboardViewModel,
+ isSyncHeavy: true,
+ showSyncedMessage: false,
+ ),
+ ),
+ ),
+ );
+ await tester.pump();
+
+ expect(find.text("24%"), findsNothing, reason: "$testCase");
+ expect(find.text("0%"), findsNothing, reason: "$testCase");
+ expect(find.text("·"), findsNothing, reason: "$testCase");
+ }
+ });
+}
diff --git a/test/new-ui/widgets/coins_page/top_bar_widget/top_bar_test.dart b/test/new-ui/widgets/coins_page/top_bar_widget/top_bar_test.dart
new file mode 100644
index 00000000..367f0613
--- /dev/null
+++ b/test/new-ui/widgets/coins_page/top_bar_widget/top_bar_test.dart
@@ -0,0 +1,387 @@
+import "package:cake_wallet/di.dart";
+import "package:cake_wallet/entities/sync_status_display_mode.dart";
+import "package:cake_wallet/generated/i18n.dart";
+import "package:cake_wallet/locales/locale.dart";
+import "package:cake_wallet/new-ui/widgets/coins_page/top_bar_widget/lightning_switcher.dart";
+import "package:cake_wallet/new-ui/widgets/coins_page/top_bar_widget/sync_bar.dart";
+import "package:cake_wallet/new-ui/widgets/coins_page/top_bar_widget/top_bar.dart";
+import "package:cake_wallet/new-ui/widgets/coins_page/wallet_info.dart";
+import "package:cake_wallet/new-ui/widgets/modern_button.dart";
+import "package:cake_wallet/src/widgets/cake_image_widget.dart";
+import "package:cake_wallet/store/settings_store.dart";
+import "package:cake_wallet/themes/core/theme_store.dart";
+import "package:cake_wallet/view_model/dashboard/dashboard_view_model.dart";
+import "package:cw_core/balance.dart";
+import "package:cw_core/crypto_currency.dart";
+import "package:cw_core/sync_status.dart";
+import "package:cw_core/transaction_history.dart";
+import "package:cw_core/transaction_info.dart";
+import "package:cw_core/wallet_base.dart";
+import "package:flutter/material.dart";
+import "package:flutter_test/flutter_test.dart";
+import "package:mobx/mobx.dart" show Observable, runInAction;
+import "package:mocktail/mocktail.dart";
+
+class _MockWallet extends Mock
+ implements WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> {}
+
+class _ObservableDashboardViewModel extends Mock implements DashboardViewModel {
+ _ObservableDashboardViewModel(SyncStatus initialStatus)
+ : _status = Observable<SyncStatus>(initialStatus);
+
+ final Observable<SyncStatus> _status;
+
+ @override
+ SyncStatus get status => _status.value;
+
+ void setStatus(SyncStatus status) => runInAction(() => _status.value = status);
+}
+
+class _MockSettingsStore extends Mock implements SettingsStore {}
+
+class _UnknownSyncStatus extends SyncStatus {
+ const _UnknownSyncStatus();
+
+ @override
+ double progress() => 0;
+}
+
+void _stubDashboardViewModel(
+ _ObservableDashboardViewModel dashboardViewModel,
+ _MockWallet wallet,
+ String walletName, {
+ bool hasLightning = true,
+ bool isSyncHeavy = false,
+ bool hasSilentPayments = false,
+ bool silentPaymentsScanningActive = false,
+}) {
+ when(() => dashboardViewModel.hasLightning).thenReturn(hasLightning);
+ when(() => dashboardViewModel.isSyncHeavy).thenReturn(isSyncHeavy);
+ when(() => dashboardViewModel.isTorEnabled).thenReturn(false);
+ when(() => dashboardViewModel.hasMweb).thenReturn(false);
+ when(() => dashboardViewModel.hasSilentPayments).thenReturn(hasSilentPayments);
+ when(() => dashboardViewModel.silentPaymentsScanningActive)
+ .thenReturn(silentPaymentsScanningActive);
+ when(() => dashboardViewModel.wallet).thenReturn(wallet);
+ when(() => wallet.name).thenReturn(walletName);
+ when(() => wallet.hardwareWalletType).thenReturn(null);
+ final settingsStore = _MockSettingsStore();
+ when(() => dashboardViewModel.settingsStore).thenReturn(settingsStore);
+ when(() => settingsStore.syncStatusDisplayMode).thenReturn(SyncStatusDisplayMode.blocksRemaining);
+}
+
+Widget _buildTopBar(DashboardViewModel dashboardViewModel) => TopBar(
+ lightningMode: false,
+ onLightningSwitchPress: () {},
+ dashboardViewModel: dashboardViewModel,
+ onSettingsButtonPress: () {},
+ );
+
+Widget _buildTestApp(Widget child) => MaterialApp(
+ localizationsDelegates: localizationDelegates,
+ supportedLocales: S.delegate.supportedLocales,
+ home: Scaffold(body: child),
+ );
+
+void main() {
+ late bool registeredThemeStore;
+
+ setUpAll(() {
+ registeredThemeStore = !getIt.isRegistered<ThemeStore>();
+ if (registeredThemeStore) {
+ getIt.registerSingleton(ThemeStore());
+ }
+ });
+
+ tearDownAll(() async {
+ if (registeredThemeStore) {
+ await getIt.unregister<ThemeStore>();
+ }
+ });
+
+ for (final isSyncHeavy in [false, true]) {
+ testWidgets(
+ "keeps the name hidden until Synced expires, then restores it (heavy: $isSyncHeavy)",
+ (tester) async {
+ const walletName = "My Bitcoin Wallet";
+ final wallet = _MockWallet();
+ final dashboardViewModel = _ObservableDashboardViewModel(SyncingSyncStatus(100, 0.5));
+
+ _stubDashboardViewModel(dashboardViewModel, wallet, walletName, isSyncHeavy: isSyncHeavy);
+
+ await tester.pumpWidget(_buildTestApp(_buildTopBar(dashboardViewModel)));
+ await tester.pump();
+
+ expect(find.text(walletName), findsNothing);
+ expect(find.byType(SyncBar), findsOneWidget);
+
+ dashboardViewModel.setStatus(SyncedSyncStatus());
+ await tester.pump();
+
+ expect(find.text(walletName), findsNothing);
+ expect(find.byIcon(Icons.check), findsOneWidget);
+ expect(tester.widget<SyncBar>(find.byType(SyncBar)).showSyncedMessage, isTrue);
+
+ await tester.pump(const Duration(seconds: 3) - const Duration(milliseconds: 1));
+
+ expect(find.text(walletName), findsNothing);
+ expect(find.byIcon(Icons.check), findsOneWidget);
+
+ await tester.pump(const Duration(milliseconds: 1));
+
+ expect(find.text(walletName), findsOneWidget);
+
+ await tester.pump(const Duration(milliseconds: 150));
+ expect(find.byType(SyncBar), findsNothing);
+
+ dashboardViewModel.setStatus(SyncingSyncStatus(100, 0.5));
+ await tester.pump();
+ await tester.pump();
+ expect(find.text(walletName), findsNothing);
+ });
+ }
+
+ testWidgets("replaces the wallet name for progress and failure statuses in all wallets",
+ (tester) async {
+ final statuses = <SyncStatus>[
+ SyncingSyncStatus(100, 0.5),
+ const NotConnectedSyncStatus(),
+ SyncronizingSyncStatus(),
+ AttemptingSyncStatus(),
+ StartingScanSyncStatus(0),
+ AttemptingScanSyncStatus(),
+ SyncedTipSyncStatus(100),
+ ProcessingSyncStatus(),
+ ConnectingSyncStatus(),
+ ConnectedSyncStatus(),
+ FailedSyncStatus(),
+ LostConnectionSyncStatus(),
+ TimedOutSyncStatus(),
+ UnsupportedSyncStatus(),
+ const _UnknownSyncStatus(),
+ SyncedSyncStatus(),
+ ];
+
+ for (final isSyncHeavy in [false, true]) {
+ for (final status in statuses) {
+ final dashboardViewModel = _ObservableDashboardViewModel(status);
+ _stubDashboardViewModel(
+ dashboardViewModel,
+ _MockWallet(),
+ "Wallet",
+ isSyncHeavy: isSyncHeavy,
+ );
+
+ await tester.pumpWidget(_buildTestApp(_buildTopBar(dashboardViewModel)));
+ await tester.pump(const Duration(milliseconds: 150));
+
+ final showsName = status.runtimeType == SyncedSyncStatus ||
+ (status is _UnknownSyncStatus && !isSyncHeavy);
+ expect(
+ find.text("Wallet"),
+ showsName ? findsOneWidget : findsNothing,
+ reason: "$status, sync-heavy: $isSyncHeavy",
+ );
+ expect(find.byType(SyncBar), showsName ? findsNothing : findsOneWidget);
+ }
+ }
+ });
+
+ testWidgets("rebinds to a replacement view model and cancels its stale Synced timer",
+ (tester) async {
+ const firstWalletName = "First Wallet";
+ const secondWalletName = "Second Wallet";
+ final firstWallet = _MockWallet();
+ final secondWallet = _MockWallet();
+ final firstViewModel = _ObservableDashboardViewModel(SyncingSyncStatus(100, 0.5));
+ final secondViewModel = _ObservableDashboardViewModel(SyncingSyncStatus(100, 0.5));
+ DashboardViewModel activeViewModel = firstViewModel;
+ late StateSetter setHarnessState;
+
+ _stubDashboardViewModel(firstViewModel, firstWallet, firstWalletName);
+ _stubDashboardViewModel(secondViewModel, secondWallet, secondWalletName);
+
+ await tester.pumpWidget(
+ _buildTestApp(
+ StatefulBuilder(
+ builder: (context, setState) {
+ setHarnessState = setState;
+ return _buildTopBar(activeViewModel);
+ },
+ ),
+ ),
+ );
+ await tester.pump();
+
+ firstViewModel.setStatus(SyncedSyncStatus());
+ await tester.pump();
+ expect(tester.widget<SyncBar>(find.byType(SyncBar)).showSyncedMessage, isTrue);
+
+ await tester.pump(const Duration(seconds: 1));
+ setHarnessState(() => activeViewModel = secondViewModel);
+ await tester.pump();
+ expect(tester.widget<SyncBar>(find.byType(SyncBar)).showSyncedMessage, isFalse);
+
+ firstViewModel.setStatus(SyncingSyncStatus(50, 0.75));
+ await tester.pump();
+ firstViewModel.setStatus(SyncedSyncStatus());
+ await tester.pump();
+ expect(tester.widget<SyncBar>(find.byType(SyncBar)).showSyncedMessage, isFalse);
+
+ secondViewModel.setStatus(SyncedSyncStatus());
+ await tester.pump();
+ expect(tester.widget<SyncBar>(find.byType(SyncBar)).showSyncedMessage, isTrue);
+
+ await tester.pump(const Duration(seconds: 2));
+ expect(tester.widget<SyncBar>(find.byType(SyncBar)).showSyncedMessage, isTrue);
+
+ await tester.pump(const Duration(seconds: 1));
+ await tester.pump(const Duration(milliseconds: 150));
+ expect(find.text(secondWalletName), findsOneWidget);
+ expect(find.byType(SyncBar), findsNothing);
+ });
+
+ testWidgets("cancels the Synced timer when disposed", (tester) async {
+ final wallet = _MockWallet();
+ final dashboardViewModel = _ObservableDashboardViewModel(SyncingSyncStatus(100, 0.5));
+
+ _stubDashboardViewModel(dashboardViewModel, wallet, "Wallet");
+
+ await tester.pumpWidget(_buildTestApp(_buildTopBar(dashboardViewModel)));
+ await tester.pump();
+
+ dashboardViewModel.setStatus(SyncedSyncStatus());
+ await tester.pump();
+ expect(tester.widget<SyncBar>(find.byType(SyncBar)).showSyncedMessage, isTrue);
+
+ await tester.pumpWidget(const SizedBox.shrink());
+
+ // Flutter reports any timer that survives disposal as pending when this test ends.
+ expect(tester.takeException(), isNull);
+ });
+
+ testWidgets("uses natural control widths in the synced Bitcoin header", (tester) async {
+ await tester.binding.setSurfaceSize(const Size(376, 200));
+ addTearDown(() => tester.binding.setSurfaceSize(null));
+
+ final wallet = _MockWallet();
+ final dashboardViewModel = _ObservableDashboardViewModel(SyncedSyncStatus());
+
+ _stubDashboardViewModel(dashboardViewModel, wallet, "My Bitcoin Wallet");
+
+ await tester.pumpWidget(
+ _buildTestApp(
+ Align(
+ alignment: Alignment.topCenter,
+ child: _buildTopBar(dashboardViewModel),
+ ),
+ ),
+ );
+ await tester.pump();
+
+ final switcher = find.byType(LightningSwitcher);
+ final walletInfo = find.byType(WalletInfoBar);
+ final settingsButton = find.byType(ModernButton);
+ final switcherIcons = find.descendant(
+ of: switcher,
+ matching: find.byWidgetPredicate(
+ (widget) =>
+ widget is CakeImageWidget &&
+ widget.imageUrl?.startsWith("assets/new-ui/switcher-") == true,
+ ),
+ );
+
+ expect(tester.getSize(switcher), const Size(63, 36));
+ expect(switcherIcons, findsNWidgets(2));
+ for (final icon in tester.widgetList<CakeImageWidget>(switcherIcons)) {
+ expect(Size(icon.width!, icon.height!), const Size(27, 27));
+ }
+ expect(tester.getSize(walletInfo).width, 217);
+ expect(tester.getRect(settingsButton).right, 358);
+ });
+
+ testWidgets("fits the syncing pill between the controls at different screen widths",
+ (tester) async {
+ await tester.binding.setSurfaceSize(const Size(376, 200));
+ addTearDown(() => tester.binding.setSurfaceSize(null));
+
+ final wallet = _MockWallet();
+ final dashboardViewModel = _ObservableDashboardViewModel(SyncingSyncStatus(100, 0.24));
+
+ _stubDashboardViewModel(
+ dashboardViewModel,
+ wallet,
+ "My Bitcoin Wallet",
+ isSyncHeavy: true,
+ hasSilentPayments: true,
+ silentPaymentsScanningActive: true,
+ );
+
+ await tester.pumpWidget(
+ _buildTestApp(
+ Align(
+ alignment: Alignment.topCenter,
+ child: _buildTopBar(dashboardViewModel),
+ ),
+ ),
+ );
+ await tester.pump();
+
+ final pill = find.descendant(
+ of: find.byType(SyncBar),
+ matching: find.byWidgetPredicate(
+ (widget) =>
+ widget is Container &&
+ widget.constraints?.minHeight == 40 &&
+ widget.constraints?.maxHeight == 40,
+ ),
+ );
+
+ for (final width in [320.0, 376.0, 600.0]) {
+ await tester.binding.setSurfaceSize(Size(width, 200));
+ await tester.pump();
+
+ final switcherRect = tester.getRect(find.byType(LightningSwitcher));
+ final syncBarRect = tester.getRect(find.byType(SyncBar));
+ final settingsRect = tester.getRect(find.byType(ModernButton));
+ final pillRect = tester.getRect(pill);
+
+ expect(switcherRect.left, 18);
+ expect(settingsRect.right, width - 18);
+ expect(syncBarRect.height, 36);
+ expect(pillRect.height, 40);
+ expect(pillRect.left - switcherRect.right, closeTo(12, 0.001));
+ expect(settingsRect.left - pillRect.right, closeTo(12, 0.001));
+ expect(pillRect.center.dy, closeTo(syncBarRect.center.dy, 0.001));
+ expect(tester.takeException(), isNull);
+ }
+ });
+
+ testWidgets("keeps the non-Lightning header geometry unchanged", (tester) async {
+ await tester.binding.setSurfaceSize(const Size(376, 200));
+ addTearDown(() => tester.binding.setSurfaceSize(null));
+
+ final wallet = _MockWallet();
+ final dashboardViewModel = _ObservableDashboardViewModel(SyncedSyncStatus());
+
+ _stubDashboardViewModel(
+ dashboardViewModel,
+ wallet,
+ "My Monero Wallet",
+ hasLightning: false,
+ );
+ when(() => wallet.currency).thenReturn(CryptoCurrency.xmr);
+
+ await tester.pumpWidget(
+ _buildTestApp(
+ Align(
+ alignment: Alignment.topCenter,
+ child: _buildTopBar(dashboardViewModel),
+ ),
+ ),
+ );
+ await tester.pump();
+
+ expect(tester.getSize(find.byType(WalletInfoBar)).width, 244);
+ });
+}
diff --git a/test/new-ui/widgets/coins_page/wallet_info_test.dart b/test/new-ui/widgets/coins_page/wallet_info_test.dart
new file mode 100644
index 00000000..c51a1c02
--- /dev/null
+++ b/test/new-ui/widgets/coins_page/wallet_info_test.dart
@@ -0,0 +1,62 @@
+import "package:cake_wallet/generated/i18n.dart";
+import "package:cake_wallet/locales/locale.dart";
+import "package:cake_wallet/new-ui/widgets/coins_page/wallet_info.dart";
+import "package:cake_wallet/new-ui/widgets/modern_button.dart";
+import "package:cake_wallet/src/widgets/cake_image_widget.dart";
+import "package:cw_core/wallet_info.dart";
+import "package:flutter/material.dart";
+import "package:flutter_test/flutter_test.dart";
+
+void main() {
+ testWidgets("keeps a hardware wallet name on one ellipsized line", (tester) async {
+ const walletName =
+ "My exceptionally long Ledger hardware wallet name that cannot fit in the header";
+ final semanticsHandle = tester.ensureSemantics();
+
+ await tester.pumpWidget(
+ MaterialApp(
+ localizationsDelegates: localizationDelegates,
+ supportedLocales: S.delegate.supportedLocales,
+ home: const Scaffold(
+ body: Center(
+ child: SizedBox(
+ width: 160,
+ child: WalletInfoBar(
+ name: walletName,
+ hardwareWalletType: HardwareWalletType.ledger,
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ await tester.pumpAndSettle();
+
+ final name = tester.widget<Text>(find.text(walletName));
+ expect(name.maxLines, 1);
+ expect(name.softWrap, isFalse);
+ expect(name.overflow, TextOverflow.ellipsis);
+ expect(name.style?.fontSize, 16);
+
+ final hardwareIcon = tester.widget<CakeImageWidget>(
+ find.byKey(const ValueKey("hardware_wallet_icon")),
+ );
+ expect(hardwareIcon.imageUrl, "assets/new-ui/hardware_wallets/device_ledger_nano_x.svg");
+ expect(hardwareIcon.width, 24);
+ expect(hardwareIcon.height, 24);
+ expect(
+ tester.getRect(find.text(walletName)).left -
+ tester.getRect(find.byKey(const ValueKey("hardware_wallet_icon"))).right,
+ 4,
+ );
+
+ expect(
+ find.bySemanticsLabel("$walletName, ${S.current.hardware_wallet}"),
+ findsOneWidget,
+ );
+ expect(find.byType(ModernButton), findsNothing);
+ expect(find.bySemanticsLabel(S.current.wallet_accounts), findsNothing);
+ expect(tester.takeException(), isNull);
+ semanticsHandle.dispose();
+ });
+}
Why this scored 12/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.