mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-26 06:08:40 +01:00
CW-1468 Home Screen Top Bar Layout (#3576)
* Improve homescreen layout and account access * Add homescreen layout regression tests * address feedback
This commit is contained in:
@@ -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!));
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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);
|
||||
builder: (_) {
|
||||
final status = dashboardViewModel.status;
|
||||
if (!_showFullBar) {
|
||||
return _buildCompactBar(context);
|
||||
}
|
||||
|
||||
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),
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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: (_) =>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
@@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user