CW-1513/CW-1610: Add account archival and UI improvements (#3600)

* add ability to hide accounts

* remove import

* merge

* merge

* fix loading accounts when styleSettings are not saved yet

* merge

* auto-reformat

* small fixes

* proper reload on card unhide

* block hiding last card

* fixup

* revert formatting-only changes

* liner fixes

* fix overflows

* card layout as separate class to prevent index errors

* fix mini card on addresses page

* block opening customizers if wallet is syncing

* switch to BlocProvider.value to prevent auto dispose

* imports

* fix fiat balance

* Improve homescreen layout and account access

* Add homescreen layout regression tests

* improve account management w/ education + archival flows

* reorganize wallet-specific settings

* preserve account archival during dismissal

* fix: align account visuals with design feedback

* minor account & archival visual corrections

* simplify wallet settings + account UI state

* generalise education carousel & dismissal tracking

* refactor account UI flows + minor improvements

* minor refactors and improvement

* pass account balances as Money and format at display

* use fresh full balance for account archival check

* refactor BalanceCard w/ MoneyText + cleanup

* minor card_customizer cleanup

* rework card customizer flow

---------

Co-authored-by: Robert Malikowski <malikowskirobert@gmail.com>
Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
This commit is contained in:
Patrick
2026-10-10 02:07:41 +02:00
committed by GitHub
co-authored by Robert Malikowski Omar Hatem
parent 9f1f82fdd4
commit 20f1ceb651
108 changed files with 6632 additions and 1392 deletions
+31 -13
View File
@@ -295,7 +295,6 @@ import 'dogecoin/dogecoin.dart';
import 'new-ui/viewmodels/card_customizer/card_customizer_bloc.dart';
import 'new-ui/widgets/addresses_page/address_info.dart';
import 'src/screens/buy/buy_sell_page.dart';
import "src/screens/settings/widgets/account_creation_modal.dart";
final getIt = GetIt.instance;
@@ -561,14 +560,15 @@ Future<void> setup({
sharedPreferences: getIt.get<SharedPreferences>(),
keyService: getIt.get<KeyService>()));
getIt.registerFactoryParam<CardCustomizerBloc, bool, BitcoinAmountDisplayMode?>(
(lightningMode, displayMode) {
getIt.registerFactoryParam<CardCustomizerBloc, CardCustomizerBlocParams, void>(
(params, _) {
final wallet = getIt.get<AppStore>().wallet!;
return CardCustomizerBloc(wallet,
lightningMode: lightningMode,
canHide: params.canHide,
lightningMode: params.lightningMode,
displaySats: wallet.type == WalletType.bitcoin &&
(displayMode == BitcoinAmountDisplayMode.satoshi ||
(displayMode == BitcoinAmountDisplayMode.satoshiForLightning && lightningMode)));
(params.amountDisplayMode == BitcoinAmountDisplayMode.satoshi ||
(params.amountDisplayMode == BitcoinAmountDisplayMode.satoshiForLightning && params.lightningMode)));
});
getIt.registerLazySingleton<PriceStore>(() => PriceStore(settingsStore: getIt.get<SettingsStore>()));
@@ -896,7 +896,7 @@ Future<void> setup({
if (wallet.type == WalletType.monero ||
wallet.type == WalletType.wownero ||
wallet.type == WalletType.haven) {
return MoneroAccountListViewModel(wallet, getIt.get<SettingsStore>());
return MoneroAccountListViewModel(wallet);
}
throw Exception(
'Unexpected wallet type: ${wallet.type} for generate Monero AccountListViewModel');
@@ -905,7 +905,7 @@ Future<void> setup({
getIt.registerFactory<BitcoinAccountListViewModel>(() {
final wallet = getIt.get<AppStore>().wallet!;
if (wallet.type == WalletType.bitcoin) {
return BitcoinAccountListViewModel(wallet, getIt.get<SettingsStore>());
return BitcoinAccountListViewModel(wallet);
}
throw Exception(
'Unexpected wallet type: ${wallet.type} for generate Bitcoin AccountListViewModel');
@@ -927,11 +927,29 @@ Future<void> setup({
});
getIt.registerFactoryParam<MoneroAccountEditOrCreateViewModel, AccountListItem?, void>(
(AccountListItem? account, _) => MoneroAccountEditOrCreateViewModel(
monero!.getAccountList(getIt.get<AppStore>().wallet!),
wownero?.getAccountList(getIt.get<AppStore>().wallet!),
wallet: getIt.get<AppStore>().wallet!,
accountListItem: account));
(AccountListItem? account, _) {
final wallet = getIt.get<AppStore>().wallet!;
if (wallet.type == WalletType.monero) {
return MoneroAccountEditOrCreateViewModel(
monero!.getAccountList(wallet),
null,
wallet: wallet,
accountListItem: account,
);
}
if (wallet.type == WalletType.wownero) {
return MoneroAccountEditOrCreateViewModel(
null,
wownero!.getAccountList(wallet),
wallet: wallet,
accountListItem: account,
);
}
throw StateError("Account creation is unavailable for ${wallet.type}");
});
getIt.registerFactory<BitcoinAccountEditOrCreateViewModel>(
() => BitcoinAccountEditOrCreateViewModel(
+2
View File
@@ -3,6 +3,8 @@ import "package:cw_core/wallet_type.dart";
class PreferencesKey {
static const currentWalletType = 'current_wallet_type';
static const currentWalletName = 'current_wallet_name';
static const accountsHomePromoDismissed = 'accounts_home_promo_dismissed';
static String educationDismissed(String educationId) => "education_dismissed_$educationId";
static const currentNodeIdKey = 'current_node_id';
static const currentBitcoinElectrumSererIdKey = 'current_node_id_btc';
static const currentLitecoinElectrumSererIdKey = 'current_node_id_ltc';
+4
View File
@@ -146,6 +146,10 @@ class CWMonero extends Monero {
@override
MoneroAccountList getAccountList(Object wallet) => CWMoneroAccountList(wallet);
@override
Money getAccountFullBalance(int accountIndex) =>
monero_wallet_api.getFullBalance(accountIndex: accountIndex);
@override
MoneroSubaddressList getSubaddressList(Object wallet) => CWMoneroSubaddressList(wallet);
@@ -0,0 +1,128 @@
import "package:cake_wallet/generated/i18n.dart";
import "package:cake_wallet/new-ui/pages/education_page.dart";
import "package:cake_wallet/src/widgets/cake_image_widget.dart";
import "package:flutter/material.dart";
class AccountEducationPage extends EducationPage {
const AccountEducationPage({required super.settingsStore, super.key});
@override
String get educationId => "accounts";
@override
String get title => S.current.accounts;
@override
String get iconPath => "assets/new-ui/settings_row_icons/accounts.svg";
@override
String get completionLabel => S.current.accounts_education_understand_continue;
@override
String progressLabel(int current, int total) =>
S.current.accounts_education_progress("$current", "$total");
@override
List<EducationSlide> get slides => <EducationSlide>[
EducationSlide(
distributeChildren: true,
children: [
EducationText(text: S.current.accounts_education_organize_title),
const CakeImageWidget(
imageUrl: "assets/new-ui/account_education/accounts_overview.svg",
key: ValueKey("accounts-education-overview-image"),
width: 133,
height: 324,
fit: BoxFit.contain,
),
EducationText(
text: S.current.accounts_education_organize_description,
secondary: true,
),
],
),
EducationSlide(
children: [
const CakeImageWidget(
imageUrl: "assets/new-ui/account_education/recovery_shield.svg",
key: ValueKey("accounts-education-recovery-image"),
width: 213,
height: 213,
fit: BoxFit.contain,
),
const SizedBox(height: 25.5),
EducationText(
text: S.current.accounts_education_recovery_title,
highlightedText: S.current.accounts_education_recovery_highlight,
),
const SizedBox(height: 25.5),
EducationText(
text: S.current.accounts_education_recovery_description,
secondary: true,
),
],
),
EducationSlide(
children: [
const CakeImageWidget(
imageUrl: "assets/new-ui/account_education/account_order.svg",
key: ValueKey("accounts-education-order-image"),
width: 309,
height: 48,
fit: BoxFit.contain,
),
const SizedBox(height: 51),
EducationText(text: S.current.accounts_education_order_title),
const SizedBox(height: 51),
const CakeImageWidget(
imageUrl: "assets/new-ui/account_education/ordered_account.svg",
key: ValueKey("accounts-education-ordered-account-image"),
width: 160,
height: 99,
fit: BoxFit.contain,
),
const SizedBox(height: 51),
Column(
mainAxisSize: MainAxisSize.min,
children: [
EducationText(
text: S.current.accounts_education_order_description,
highlightedText: S.current.accounts_education_order_highlight,
),
const SizedBox(height: 25.5),
EducationText(
text: S.current.accounts_education_order_restore_description,
secondary: true,
),
],
),
],
),
EducationSlide(
children: [
const CakeImageWidget(
imageUrl: "assets/new-ui/account_education/archive_warning.svg",
key: ValueKey("accounts-education-archive-image"),
width: 232,
height: 101,
fit: BoxFit.contain,
),
const SizedBox(height: 51),
Column(
mainAxisSize: MainAxisSize.min,
children: [
EducationText(
text: S.current.accounts_education_archive_title,
warning: true,
),
const SizedBox(height: 13),
EducationText(
text: S.current.accounts_education_archive_description,
secondary: true,
),
],
),
],
),
];
}
+19 -8
View File
@@ -2,7 +2,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/widgets/long_press_menu/long_press_popup.dart';
import 'package:cake_wallet/new-ui/widgets/addresses_page/address_info.dart';
import 'package:cake_wallet/new-ui/widgets/addresses_page/address_label_input.dart';
@@ -16,7 +15,6 @@ import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart';
import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart';
import 'package:cake_wallet/wownero/wownero.dart';
import 'package:cw_core/card_design.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart';
@@ -69,6 +67,7 @@ class _NewAddressesPageState extends State<NewAddressesPage> {
late final TextEditingController _searchController;
CardDesign? design;
ReactionDisposer? _cardDesignsDisposer;
void updateItems() {
setState(() {
@@ -86,20 +85,32 @@ class _NewAddressesPageState extends State<NewAddressesPage> {
setState(() {});
});
loadCardDesign();
WidgetsBinding.instance.addPostFrameCallback((_) {
reaction((_) {
final index = widget.dashboardViewModel.accountListViewModel?.selectedAccount?.id ?? 0;
final designs = widget.dashboardViewModel.cardDesigns;
return (index >= 0 && index < designs.length) ? designs[index] : null;
}, (value) {
if (!mounted) return;
_cardDesignsDisposer = reaction((_) => widget.dashboardViewModel.currentCardDesign, (value) {
if (!mounted) return;
setState(() {
design = value;
design = widget.dashboardViewModel.currentCardDesign;
});
});
});
}
Future<void> loadCardDesign() async {
await widget.dashboardViewModel.loadCardDesigns();
if (!mounted) return;
setState(() {
design = widget.dashboardViewModel.currentCardDesign;
});
}
@override
void dispose() {
_cardDesignsDisposer?.call();
super.dispose();
}
@override
Widget build(BuildContext context) {
final filteredItems = items.where((item) {
+407 -292
View File
@@ -1,75 +1,96 @@
import "dart:async";
import "dart:math";
import "package:cake_wallet/di.dart";
import "package:cake_wallet/generated/i18n.dart";
import "package:cake_wallet/new-ui/modal_navigator.dart";
import "package:modal_bottom_sheet/modal_bottom_sheet.dart";
import "package:cake_wallet/generated/i18n.dart";
import "package:cake_wallet/new-ui/viewmodels/card_customizer/card_customizer_bloc.dart";
import "package:cake_wallet/new-ui/widgets/account_confirmation_content.dart";
import "package:cake_wallet/new-ui/widgets/coins_page/cards/balance_card.dart";
import "package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
import "package:cake_wallet/src/widgets/alert_with_two_actions.dart";
import "package:cake_wallet/src/widgets/base_alert_dialog.dart";
import "package:cake_wallet/src/widgets/cake_image_widget.dart";
import "package:cake_wallet/src/widgets/new_list_row/list_Item_style_wrapper.dart";
import "package:cake_wallet/utils/show_pop_up.dart";
import "package:cake_wallet/view_model/dashboard/dashboard_view_model.dart";
import "package:cake_wallet/view_model/wallet_account_list/account_list_item.dart";
import "package:cw_core/card_design.dart";
import "package:flutter/cupertino.dart";
import "package:flutter/material.dart";
import "package:flutter_bloc/flutter_bloc.dart";
import "package:modal_bottom_sheet/modal_bottom_sheet.dart";
class CardCustomizer extends StatefulWidget {
const CardCustomizer({required this.cryptoTitle, required this.cryptoName, super.key});
const CardCustomizer({
required this.cryptoTitle,
required this.cryptoName,
this.account,
this.fiatBalance,
super.key,
});
final String cryptoTitle;
final String cryptoName;
final AccountListItem? account;
final String? fiatBalance;
static Future<void> show({
static Future<bool> show({
required BuildContext context,
required DashboardViewModel dashboardViewModel,
required bool lightningMode,
bool asModalSheet = true,
VoidCallback? onSaved,
required AccountListItem? account,
bool lightningMode = false,
bool canHide = false,
bool asPage = false,
}) async {
final bloc = getIt.get<CardCustomizerBloc>(
param1: lightningMode,
param2: dashboardViewModel.settingsStore.displayAmountsInSatoshi,
param1: CardCustomizerBlocParams(
lightningMode: lightningMode,
amountDisplayMode: dashboardViewModel.settingsStore.displayAmountsInSatoshi,
canHide: canHide,
),
);
final customizer = BlocProvider(
create: (_) => bloc,
if (bloc.state is CardCustomizerNotLoaded) {
await bloc.stream.firstWhere((state) => state is! CardCustomizerNotLoaded);
}
if (!context.mounted) {
await bloc.close();
return false;
}
final balanceViewModel = dashboardViewModel.balanceViewModel;
final customizer = BlocProvider.value(
value: bloc,
child: Material(
child: CardCustomizer(
cryptoTitle: dashboardViewModel.wallet.currency.fullName ??
dashboardViewModel.wallet.currency.name,
cryptoName: dashboardViewModel.wallet.currency.name,
account: account,
fiatBalance: account == null ? null : balanceViewModel.accountFiatBalance(account),
),
),
);
if (asModalSheet) {
await CupertinoScaffold.showCupertinoModalBottomSheet(
barrierColor: Colors.black.withAlpha(60),
context: context,
builder: (context) => ModalNavigator(
parentContext: context,
heightMode: ModalHeightModes.fullScreen,
rootPage: customizer,
),
);
} else {
await Navigator.of(context).push(
MaterialPageRoute<void>(builder: (_) => customizer),
);
}
final archived = await (asPage
? Navigator.of(context).push<bool>(CupertinoPageRoute(builder: (_) => customizer))
: CupertinoScaffold.showCupertinoModalBottomSheet<bool>(
barrierColor: Colors.black.withAlpha(60),
context: context,
builder: (context) => ModalNavigator(
parentContext: context,
heightMode: ModalHeightModes.fullScreen,
rootPage: customizer,
),
));
// Save edits before AccountHidden writes the hidden state.
bloc.add(DesignSaved());
await bloc.stream.firstWhere(
(state) => state is CardCustomizerSaved,
orElse: () => bloc.state,
);
await dashboardViewModel.loadCardDesigns();
await dashboardViewModel.accountListViewModel?.reload();
onSaved?.call();
await bloc.stream.firstWhere((state) => state is CardCustomizerSaved);
if (archived == true) {
bloc.add(AccountHidden());
await bloc.stream.firstWhere((state) => state is CardCustomizerSaved);
}
await bloc.close();
return archived == true;
}
@override
@@ -78,9 +99,10 @@ class CardCustomizer extends StatefulWidget {
class _CardCustomizerState extends State<CardCustomizer> {
final accountNameController = TextEditingController();
bool editEnabled = false;
late final CardCustomizerBloc bloc;
bool get _isAccount => widget.account != null;
@override
void initState() {
super.initState();
@@ -88,18 +110,7 @@ class _CardCustomizerState extends State<CardCustomizer> {
bloc = context.read<CardCustomizerBloc>();
if (bloc.state is! CardCustomizerNotLoaded) {
editEnabled = bloc.state.accountName.isNotEmpty;
accountNameController.text = bloc.state.accountName;
} else {
late final StreamSubscription sub;
sub = bloc.stream.listen((state) {
if (state is! CardCustomizerNotLoaded) {
setState(() {
editEnabled = state.accountName.isNotEmpty;
});
sub.cancel();
}
});
}
}
@@ -118,6 +129,46 @@ class _CardCustomizerState extends State<CardCustomizer> {
return design;
}
Future<void> _requestArchive() async {
final account = AccountListItem(
id: widget.account!.id,
label: bloc.state.accountName,
balance: widget.account!.balance,
);
final isFunded = account.isFunded;
final confirmed = await showPopUp<bool>(
context: context,
barrierDismissible: false,
builder: (dialogContext) => AlertWithTwoActions(
alertTitle: isFunded
? S.of(dialogContext).archive_account_confirmation_title
: S.of(dialogContext).archive_account_title,
alertContent: "",
alertContentTextWidget: ArchiveConfirmationContent(
account: account,
fiatBalance: widget.fiatBalance,
isFunded: isFunded,
),
leftButtonText: S.of(dialogContext).cancel,
rightButtonText: S.of(dialogContext).continue_text,
leftAlertButtonStyle: AlertButtonStyle.primary(dialogContext),
rightAlertButtonStyle: AlertButtonStyle.secondary(dialogContext),
actionLeftButton: () => Navigator.of(dialogContext).pop(false),
actionRightButton: () => Navigator.of(dialogContext).pop(true),
),
);
if (confirmed != true || !mounted) {
return;
}
Navigator.of(context).pop(true);
}
@override
void dispose() {
accountNameController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) => BlocListener<CardCustomizerBloc, CardCustomizerState>(
listenWhen: (previous, current) => previous.accountName != current.accountName,
@@ -128,288 +179,352 @@ class _CardCustomizerState extends State<CardCustomizer> {
},
child: BlocBuilder<CardCustomizerBloc, CardCustomizerState>(
builder: (context, state) {
if (state is CardCustomizerNotLoaded) return SizedBox.shrink();
if (state is CardCustomizerNotLoaded) {
return const SizedBox.shrink();
}
return PopScope(
child: SingleChildScrollView(
child: SafeArea(
child: Column(
spacing: 25.0,
spacing: 25,
mainAxisSize: MainAxisSize.min,
children: [
ModalTopBar(
title: editEnabled ? S.of(context).edit_account : S.of(context).edit_card,
leadingIcon: Icon(Icons.close),
leadingSemanticLabel: S.of(context).close,
// trailingIcon: editEnabled ? Icon(Icons.delete_forever) : null,
title: _isAccount ? S.of(context).edit_account : S.of(context).edit_card,
subtitle: _isAccount ? "#${widget.account!.id + 1}" : null,
leadingIcon:
Icon(_isAccount ? Icons.arrow_back_ios_new : Icons.close, size: 18),
leadingSemanticLabel:
_isAccount ? S.of(context).seed_alert_back : S.of(context).close,
onLeadingPressed: () => Navigator.of(context).maybePop(),
// onTrailingPressed: () {},
),
if (editEnabled)
BalanceCard(
width: min(MediaQuery.of(context).size.width * 0.87, 768),
selected: true,
designSwitchDuration: const Duration(milliseconds: 300),
accountIndex: _isAccount ? widget.account!.id + 1 : null,
accountName: !_isAccount
? ""
: state.accountName.trim().isEmpty
? S.of(context).unnamed_account
: state.accountName,
accountBalance: widget.account?.balance,
balance: _isAccount ? "" : "0.00",
fiatBalance: widget.fiatBalance ?? "",
assetName: state.displaySats ? "sats" : widget.cryptoName,
capitalizeAssetName: !state.displaySats,
design: state.selectedDesign,
),
if (_isAccount)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 18.0),
padding: const EdgeInsets.symmetric(horizontal: 18),
child: Column(
spacing: 8.0,
spacing: 8,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(S.of(context).account_name),
TextField(
maxLength: 32,
decoration: InputDecoration(counterText: ""),
decoration: const InputDecoration(counterText: ""),
onChanged: (value) {
context.read<CardCustomizerBloc>().add(AccountNameChanged(value));
},
controller: accountNameController,
)
),
],
),
),
BalanceCard(
width: min(MediaQuery.of(context).size.width * 0.87, 768),
selected: true,
designSwitchDuration: Duration(milliseconds: 300),
accountName: editEnabled ? state.accountName : "",
balance: "0.00",
assetName: state.displaySats ? "sats" : widget.cryptoName,
capitalizeAssetName: !state.displaySats,
design: state.selectedDesign,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 18.0),
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
spacing: 8.0,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(S.of(context).card_style),
SizedBox(
height: 63,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: state.availableDesigns.length,
separatorBuilder: (context, index) =>
const SizedBox(width: 8),
itemBuilder: (context, index) => GestureDetector(
onTap: () {
context
.read<CardCustomizerBloc>()
.add(CardDesignSelected(index));
},
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
decoration: ShapeDecoration(
shape: RoundedSuperellipseBorder(
side: BorderSide(
color: ((index ==
state.selectedDesignIndex)
padding: const EdgeInsets.symmetric(horizontal: 18),
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(12),
child: Column(
spacing: 8,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(S.of(context).card_style),
SizedBox(
height: 63,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: state.availableDesigns.length,
separatorBuilder: (context, index) =>
const SizedBox(width: 8),
itemBuilder: (context, index) => GestureDetector(
onTap: () {
context
.read<CardCustomizerBloc>()
.add(CardDesignSelected(index));
},
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
decoration: ShapeDecoration(
shape: RoundedSuperellipseBorder(
side: BorderSide(
color: ((index == state.selectedDesignIndex)
? Theme.of(context).colorScheme.onSurface
: Colors.transparent),
width: 1,
),
borderRadius: BorderRadiusGeometry.circular(12),
),
),
child: AnimatedScale(
duration: const Duration(milliseconds: 200),
scale: index == state.selectedDesignIndex ? 0.94 : 1,
child: BalanceCard(
width: 96,
borderRadius: 10,
selected: false,
designSwitchDuration:
const Duration(milliseconds: 300),
design: _cardStylePreviewDesign(state, index),
),
),
),
),
),
),
],
),
),
AnimatedSwitcher(
duration: const Duration(milliseconds: 350),
switchInCurve: Curves.easeOut,
switchOutCurve: Curves.easeIn,
transitionBuilder: (child, animation) => FadeTransition(
opacity: animation,
child: SizeTransition(
sizeFactor: animation,
axisAlignment: -1,
child: child,
),
),
child: _showIconStylePanel(state)
? Column(
key: const ValueKey("icon_style_panel"),
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Column(
spacing: 8,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(S.of(context).icon_style),
SizedBox(
height: 48,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: state.availableIconPaths.length,
separatorBuilder: (context, _) =>
const SizedBox(width: 8),
itemBuilder: (context, index) {
final icon = state.availableIconPaths[index];
final isSelected =
index == state.selectedIconIndex;
return GestureDetector(
onTap: () {
context
.read<CardCustomizerBloc>()
.add(IconStyleSelected(index));
},
child: AnimatedContainer(
duration:
const Duration(milliseconds: 200),
width: 48,
height: 48,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(18),
color: Theme.of(context).brightness ==
Brightness.light
? Theme.of(context)
.colorScheme
.onSurfaceVariant
.withAlpha(64)
: Theme.of(context)
.colorScheme
.surfaceContainerHigh,
border: Border.all(
color: isSelected
? Theme.of(context)
.colorScheme
.onSurface
: Colors.transparent),
width: 1),
borderRadius:
BorderRadiusGeometry.circular(12),
),
),
child: AnimatedScale(
duration: const Duration(milliseconds: 200),
scale: index == state.selectedDesignIndex
? 0.94
: 1,
child: BalanceCard(
width: 96,
borderRadius: 10,
selected: false,
designSwitchDuration:
const Duration(milliseconds: 300),
design:
_cardStylePreviewDesign(state, index),
),
),
),
)),
),
],
)),
AnimatedSwitcher(
duration: const Duration(milliseconds: 350),
switchInCurve: Curves.easeOut,
switchOutCurve: Curves.easeIn,
transitionBuilder: (child, Animation<double> animation) =>
FadeTransition(
opacity: animation,
child: SizeTransition(
sizeFactor: animation,
axisAlignment: -1,
child: child,
),
),
child: _showIconStylePanel(state)
? Column(
key: const ValueKey("icon_style_panel"),
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Column(
spacing: 8.0,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(S.of(context).icon_style),
SizedBox(
height: 48,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: state.availableIconPaths.length,
separatorBuilder: (context, _) =>
const SizedBox(width: 8.0),
itemBuilder: (context, index) {
final icon =
state.availableIconPaths[index];
final isSelected =
index == state.selectedIconIndex;
return GestureDetector(
onTap: () {
context
.read<CardCustomizerBloc>()
.add(IconStyleSelected(index));
},
child: AnimatedContainer(
duration:
const Duration(milliseconds: 200),
width: 48,
height: 48,
decoration: BoxDecoration(
borderRadius:
BorderRadius.circular(18),
color: Theme.of(context).brightness ==
Brightness.light
? Theme.of(context)
.colorScheme
.onSurfaceVariant
.withAlpha(64)
: Theme.of(context)
.colorScheme
.surfaceContainerHigh,
border: Border.all(
color: isSelected
? Theme.of(context)
.colorScheme
.onSurface
: Colors.transparent,
width: 2,
),
),
child: Padding(
padding: const EdgeInsets.all(10.0),
child: CakeImageWidget(
imageUrl: icon.path),
: Colors.transparent,
width: 2,
),
),
);
},
),
child: Padding(
padding: const EdgeInsets.all(10),
child: CakeImageWidget(
imageUrl: icon.path,
),
),
),
);
},
),
],
),
],
),
),
],
)
: const SizedBox.shrink(key: ValueKey("icon_style_hidden")),
),
const SizedBox(height: 8),
Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
spacing: 8,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(S.of(context).color),
SizedBox(
width: double.infinity,
child: Wrap(
direction: Axis.horizontal,
spacing: 4,
runSpacing: 8,
children: List.generate(
state.availableColors.length,
(index) => Material(
borderRadius: BorderRadius.circular(999999999),
child: InkWell(
borderRadius: BorderRadius.circular(999999999),
onTap: () {
context
.read<CardCustomizerBloc>()
.add(ColorSelected(index));
},
child: Stack(
children: [
AnimatedOpacity(
duration: const Duration(milliseconds: 200),
opacity:
index == state.selectedColorIndex ? 1 : 0,
child: Container(
width: 32,
height: 32,
decoration: BoxDecoration(
borderRadius:
BorderRadius.circular(99999999),
border: Border.all(
color: Theme.of(context)
.colorScheme
.onSurface,
),
),
),
),
AnimatedScale(
duration: const Duration(milliseconds: 200),
scale: index == state.selectedColorIndex
? 0.8
: 1,
child: Container(
width: 32,
height: 32,
decoration: BoxDecoration(
borderRadius:
BorderRadius.circular(99999999),
gradient: state.availableColors[index],
),
),
),
],
),
),
),
],
)
: const SizedBox.shrink(key: ValueKey("icon_style_hidden")),
),
),
),
],
),
),
const SizedBox(height: 8),
Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(16)),
child: Padding(
padding: const EdgeInsets.all(12.0),
),
],
),
),
),
if (bloc.canHide)
Padding(
padding: const EdgeInsets.fromLTRB(18, 0, 18, 24),
child: ListItemStyleWrapper(
isFirstInSection: true,
isLastInSection: true,
onTap: _requestArchive,
builder: (context, textStyle, labelStyle) => ConstrainedBox(
constraints: const BoxConstraints(minHeight: 40),
child: Row(
children: [
Padding(
padding: const EdgeInsets.all(2.5),
child: CakeImageWidget(
imageUrl: "assets/new-ui/archived.svg",
width: 19,
height: 19,
color: Theme.of(context).colorScheme.primary,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
spacing: 8.0,
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 4,
children: [
Text(S.of(context).color),
Container(
width: double.infinity,
child: Wrap(
direction: Axis.horizontal,
spacing: 4, // space between items in a row
runSpacing: 8,
children: List.generate(
state.availableColors.length,
(index) => Material(
borderRadius:
BorderRadius.circular(999999999),
child: InkWell(
borderRadius:
BorderRadius.circular(999999999),
onTap: () {
context
.read<CardCustomizerBloc>()
.add(ColorSelected(index));
},
child: Stack(
children: [
AnimatedOpacity(
duration: const Duration(
milliseconds: 200),
opacity: index ==
state.selectedColorIndex
? 1
: 0,
child: Container(
width: 32,
height: 32,
decoration: BoxDecoration(
borderRadius:
BorderRadius.circular(
99999999),
border: Border.all(
color: Theme.of(context)
.colorScheme
.onSurface))),
),
AnimatedScale(
duration: const Duration(
milliseconds: 200),
scale: index ==
state.selectedColorIndex
? 0.8
: 1,
child: Container(
width: 32,
height: 32,
decoration: BoxDecoration(
borderRadius:
BorderRadius.circular(
99999999,
),
gradient: state
.availableColors[index]),
),
),
],
),
),
)),
)),
Text(
S.of(context).archive_account,
style: textStyle.copyWith(
height: 18 / 14,
letterSpacing: -0.07,
),
),
Text(
S.of(context).archive_account_reversible,
style: labelStyle.copyWith(
fontSize: 12,
fontWeight: FontWeight.w400,
height: 15 / 12,
letterSpacing: -0.06,
),
),
],
),
),
)
],
const SizedBox(width: 8),
SizedBox.square(
dimension: 16,
child: Align(
alignment: Alignment.centerRight,
child: CakeImageWidget(
imageUrl: "assets/new-ui/arrow_right.svg",
width: 7,
height: 12,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
],
),
),
)),
),
),
],
),
),
+246
View File
@@ -0,0 +1,246 @@
import "package:cake_wallet/generated/i18n.dart";
import "package:cake_wallet/new-ui/widgets/modern_button.dart";
import "package:cake_wallet/new-ui/widgets/new_primary_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_extension.dart";
import "package:flutter/material.dart";
import "package:modal_bottom_sheet/modal_bottom_sheet.dart";
abstract class EducationPage extends StatefulWidget {
const EducationPage({required this.settingsStore, super.key});
final SettingsStore settingsStore;
String get educationId;
String get title;
String get iconPath;
String get completionLabel;
List<EducationSlide> get slides;
String progressLabel(int current, int total);
bool get isDismissed => settingsStore.isEducationDismissed(educationId);
Future<void> show(BuildContext context) async {
await showCupertinoModalBottomSheet<void>(
useRootNavigator: true,
context: context,
backgroundColor: Colors.transparent,
builder: (_) => this,
);
await settingsStore.dismissEducation(educationId);
}
@override
State<EducationPage> createState() => _EducationPageState();
}
class _EducationPageState extends State<EducationPage> {
final PageController _pageController = PageController();
int _currentPage = 0;
bool _isCompleting = false;
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
void _complete() {
if (_isCompleting) {
return;
}
_isCompleting = true;
Navigator.of(context).pop();
}
Future<void> _continue(int pageCount) async {
if (_currentPage == pageCount - 1) {
_complete();
return;
}
await _pageController.nextPage(
duration: const Duration(milliseconds: 250),
curve: Curves.easeOutCubic,
);
}
@override
Widget build(BuildContext context) {
final pages = widget.slides;
return Material(
color: Colors.transparent,
child: Container(
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: const BorderRadius.vertical(top: Radius.circular(30)),
),
child: SafeArea(
top: false,
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(18),
child: Row(
children: [
CakeImageWidget(
imageUrl: widget.iconPath,
width: 24,
height: 24,
),
const SizedBox(width: 8),
Text(widget.title, style: Theme.of(context).textTheme.headlineMedium),
const Spacer(),
ModernButton(
size: 36,
icon: const Icon(Icons.close),
semanticLabel: S.of(context).close,
onPressed: _complete,
),
],
),
),
Expanded(
child: PageView(
controller: _pageController,
onPageChanged: (page) => setState(() => _currentPage = page),
children: pages,
),
),
Semantics(
label: widget.progressLabel(_currentPage + 1, pages.length),
child: ExcludeSemantics(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(
pages.length,
(index) => AnimatedContainer(
key: ValueKey("education-dot-$index"),
duration: const Duration(milliseconds: 200),
width: 8,
height: 8,
margin: const EdgeInsets.symmetric(horizontal: 4),
decoration: BoxDecoration(
color: index == _currentPage
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.surfaceContainerHighest,
shape: BoxShape.circle,
),
),
),
),
),
),
const SizedBox(height: 24),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 18),
child: NewPrimaryButton(
onPressed: () => _continue(pages.length),
text: _currentPage == pages.length - 1
? widget.completionLabel
: S.of(context).continue_text,
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
),
),
const SizedBox(height: 12),
],
),
),
),
);
}
}
class EducationSlide extends StatelessWidget {
const EducationSlide({
required this.children,
super.key,
this.distributeChildren = false,
});
final List<Widget> children;
final bool distributeChildren;
@override
Widget build(BuildContext context) => LayoutBuilder(
builder: (context, constraints) => SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 8),
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: constraints.maxHeight - 16),
child: distributeChildren
? IntrinsicHeight(
child: Column(
children: [
for (var index = 0; index < children.length; index++) ...[
children[index],
if (index < children.length - 1) ...[
const SizedBox(height: 24),
const Spacer(),
],
],
],
),
)
: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: children,
),
),
),
);
}
class EducationText extends StatelessWidget {
const EducationText({
required this.text,
super.key,
this.highlightedText,
this.secondary = false,
this.warning = false,
});
final String text;
final String? highlightedText;
final bool secondary;
final bool warning;
@override
Widget build(BuildContext context) {
final style = Theme.of(context).textTheme.bodyLarge?.copyWith(
fontSize: 14,
fontWeight: FontWeight.w400,
color: warning
? context.customColors.warningOutlineColor
: secondary
? Theme.of(context).colorScheme.onSurfaceVariant
: Theme.of(context).colorScheme.onSurface,
);
final highlight = highlightedText;
final highlightStart = highlight == null ? -1 : text.indexOf(highlight);
if (highlight == null || highlightStart < 0) {
return Text(text, textAlign: TextAlign.center, style: style);
}
final highlightEnd = highlightStart + highlight.length;
return Text.rich(
TextSpan(
style: style,
children: [
TextSpan(text: text.substring(0, highlightStart)),
TextSpan(
text: text.substring(highlightStart, highlightEnd),
style: TextStyle(color: Theme.of(context).colorScheme.primary),
),
TextSpan(text: text.substring(highlightEnd)),
],
),
textAlign: TextAlign.center,
);
}
}
+480
View File
@@ -0,0 +1,480 @@
import "dart:async";
import "package:cake_wallet/core/utilities.dart";
import "package:cake_wallet/generated/i18n.dart";
import "package:cake_wallet/new-ui/widgets/account_confirmation_content.dart";
import "package:cake_wallet/new-ui/widgets/modal_page_wrapper.dart";
import "package:cake_wallet/new-ui/widgets/money/money_text.dart";
import "package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
import "package:cake_wallet/src/widgets/alert_with_two_actions.dart";
import "package:cake_wallet/src/widgets/base_alert_dialog.dart";
import "package:cake_wallet/src/widgets/cake_image_widget.dart";
import "package:cake_wallet/utils/show_pop_up.dart";
import "package:cake_wallet/view_model/dashboard/dashboard_view_model.dart";
import "package:cake_wallet/view_model/wallet_account_list/account_list_item.dart";
import "package:cake_wallet/view_model/wallet_account_list/wallet_account_list_view_model.dart";
import "package:cw_core/balance_card_layout.dart";
import "package:cw_core/balance_card_style_settings.dart";
import "package:flutter/cupertino.dart";
import "package:flutter/material.dart";
import "package:flutter_mobx/flutter_mobx.dart";
class HiddenAccountsPage extends StatefulWidget {
const HiddenAccountsPage({
required this.accountListViewModel,
required this.dashboardViewModel,
super.key,
});
final WalletAccountListViewModel accountListViewModel;
final DashboardViewModel dashboardViewModel;
@override
State<HiddenAccountsPage> createState() => _HiddenAccountsPageState();
}
class _HiddenAccountsPageState extends State<HiddenAccountsPage> {
final List<AccountListItem> _items = [];
final Set<int> _accountsBeingRestored = <int>{};
bool _isLoading = true;
int get _walletInfoId => widget.dashboardViewModel.wallet.walletInfo.internalId;
@override
void initState() {
super.initState();
unawaited(_loadAccounts());
}
Future<BalanceCardLayout> _layout() async => BalanceCardLayout.resolve(
accountIndices: widget.accountListViewModel.accounts.map((account) => account.id).toList(),
settings: await BalanceCardStyleSettings.getAll(_walletInfoId),
);
Future<void> _loadAccounts() async {
final accounts = widget.accountListViewModel.accounts;
final layout = await _layout();
final hiddenAccounts = <AccountListItem>[];
for (final accountIndex in layout.hidden) {
final account = accounts.firstWhereOrNull((account) => account.id == accountIndex);
if (account != null) {
hiddenAccounts.add(account);
}
}
if (!mounted) {
return;
}
setState(() {
_items
..clear()
..addAll(hiddenAccounts);
_isLoading = false;
});
}
Future<bool> _confirmUnarchival(AccountListItem account) async {
final isFunded = account.isFunded;
final result = await showPopUp<bool>(
context: context,
barrierDismissible: false,
builder: (dialogContext) => AlertWithTwoActions(
alertTitle: S.of(dialogContext).unarchive_account_title,
alertContent: "",
alertContentTextWidget: Column(
mainAxisSize: MainAxisSize.min,
children: [
AccountSummary(account: account),
const SizedBox(height: 20),
Text(
isFunded
? S.of(dialogContext).unarchive_account_funds_description
: S.of(dialogContext).unarchive_account_description,
textAlign: TextAlign.center,
style: Theme.of(dialogContext).textTheme.bodyMedium,
),
if (isFunded) ...[
const SizedBox(height: 20),
AccountFundsSummary(
account: account,
fiatBalance: widget.dashboardViewModel.balanceViewModel.accountFiatBalance(account),
borderColor: Theme.of(dialogContext).colorScheme.primary,
),
],
],
),
leftButtonText: S.of(dialogContext).cancel,
rightButtonText: S.of(dialogContext).continue_text,
leftAlertButtonStyle: AlertButtonStyle.secondary(dialogContext),
rightAlertButtonStyle: AlertButtonStyle.primary(dialogContext),
actionLeftButton: () => Navigator.of(dialogContext).pop(false),
actionRightButton: () => Navigator.of(dialogContext).pop(true),
),
);
return result ?? false;
}
Future<void> _unarchive(AccountListItem account) async {
final accountForConfirmation =
widget.accountListViewModel.accounts.firstWhereOrNull((item) => item.id == account.id) ??
account;
final confirmed = await _confirmUnarchival(accountForConfirmation);
if (!confirmed || !mounted) {
return;
}
setState(() => _accountsBeingRestored.add(account.id));
try {
final restored = (await _layout()).unhiding(account.id);
await BalanceCardStyleSettings.setVisibleOrder(_walletInfoId, restored.orders);
final latestAccount =
widget.accountListViewModel.accounts.firstWhereOrNull((item) => item.id == account.id) ??
account;
await widget.accountListViewModel.select(latestAccount);
await widget.dashboardViewModel.loadCardDesigns();
await _loadAccounts();
} finally {
if (mounted) {
setState(() => _accountsBeingRestored.remove(account.id));
}
}
}
@override
Widget build(BuildContext context) => ModalPageWrapper(
topBar: ModalTopBar(
title: S.of(context).archived_accounts,
leadingIcon: const Icon(Icons.arrow_back_ios_new, size: 18),
leadingSemanticLabel: S.of(context).seed_alert_back,
onLeadingPressed: Navigator.of(context).pop,
),
content: _isLoading
? const SizedBox(
height: 320,
child: Center(child: CupertinoActivityIndicator()),
)
: _items.isEmpty
? const _EmptyArchiveView()
: _PopulatedArchiveView(
accounts: _items,
accountsBeingRestored: _accountsBeingRestored,
dashboardViewModel: widget.dashboardViewModel,
onUnarchive: _unarchive,
),
);
}
class _EmptyArchiveView extends StatelessWidget {
const _EmptyArchiveView();
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return SizedBox(
height: MediaQuery.sizeOf(context).height - 180,
child: Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_ArchiveIcon(size: 50, color: theme.colorScheme.onSurfaceVariant),
const SizedBox(height: 12),
Text(
S.of(context).no_archived_accounts,
textAlign: TextAlign.center,
style: theme.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w500),
),
const SizedBox(height: 12),
Text(
S.of(context).no_archived_accounts_description,
textAlign: TextAlign.center,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
],
),
),
),
);
}
}
class _PopulatedArchiveView extends StatelessWidget {
const _PopulatedArchiveView({
required this.accounts,
required this.accountsBeingRestored,
required this.dashboardViewModel,
required this.onUnarchive,
});
final List<AccountListItem> accounts;
final Set<int> accountsBeingRestored;
final DashboardViewModel dashboardViewModel;
final Future<void> Function(AccountListItem account) onUnarchive;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final fundedAccounts = accounts.where((account) => account.isFunded).toList(growable: false);
final emptyAccounts =
accounts.where((account) => !account.isFunded).toList(growable: false);
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_ArchiveIcon(size: 50, color: theme.colorScheme.primary),
const SizedBox(height: 12),
Padding(
padding: const EdgeInsets.fromLTRB(12, 0, 12, 36),
child: Text(
S.of(context).select_account_to_unarchive,
textAlign: TextAlign.center,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
),
if (fundedAccounts.isNotEmpty) ...[
_ArchiveSectionHeader(
title: S.of(context).funded_accounts,
description: S.of(context).funded_accounts_description,
),
const SizedBox(height: 12),
_accountRows(context, fundedAccounts),
],
if (fundedAccounts.isNotEmpty && emptyAccounts.isNotEmpty) const SizedBox(height: 28),
if (emptyAccounts.isNotEmpty) ...[
_ArchiveSectionHeader(title: S.of(context).empty_accounts),
const SizedBox(height: 12),
_accountRows(context, emptyAccounts),
],
],
);
}
Widget _accountRows(BuildContext context, List<AccountListItem> sectionAccounts) => Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(18),
),
clipBehavior: Clip.antiAlias,
child: Column(
children: [
for (var index = 0; index < sectionAccounts.length; index++) ...[
_ArchivedAccountRow(
account: sectionAccounts[index],
dashboardViewModel: dashboardViewModel,
isBusy: accountsBeingRestored.contains(sectionAccounts[index].id),
onTap: accountsBeingRestored.contains(sectionAccounts[index].id)
? null
: () => onUnarchive(sectionAccounts[index]),
),
if (index < sectionAccounts.length - 1)
Divider(
height: 1,
indent: 48,
color: Theme.of(context).colorScheme.outlineVariant,
),
],
],
),
);
}
class _ArchivedAccountRow extends StatelessWidget {
const _ArchivedAccountRow({
required this.account,
required this.dashboardViewModel,
required this.isBusy,
required this.onTap,
});
final AccountListItem account;
final DashboardViewModel dashboardViewModel;
final bool isBusy;
final VoidCallback? onTap;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final label = account.label.trim();
return Material(
color: theme.colorScheme.surfaceContainer,
child: Semantics(
button: true,
child: InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
child: Row(
children: [
CakeImageWidget(
imageUrl: "assets/new-ui/account.svg",
width: 24,
height: 24,
color: theme.colorScheme.onSurfaceVariant,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text.rich(
TextSpan(
children: [
TextSpan(
text: "${account.id + 1}. ",
style: TextStyle(color: theme.colorScheme.primary),
),
TextSpan(
text: label.isEmpty ? S.of(context).unnamed_account : label,
),
],
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodyMedium,
),
],
),
),
const SizedBox(width: 8),
_AccountBalanceTrailing(
account: account,
dashboardViewModel: dashboardViewModel,
isBusy: isBusy,
),
],
),
),
),
),
);
}
}
class _ArchiveSectionHeader extends StatelessWidget {
const _ArchiveSectionHeader({required this.title, this.description});
final String title;
final String? description;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: theme.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600),
),
if (description != null) ...[
const SizedBox(height: 4),
Text(
description!,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
],
],
),
);
}
}
class _AccountBalanceTrailing extends StatelessWidget {
const _AccountBalanceTrailing({
required this.account,
required this.dashboardViewModel,
required this.isBusy,
});
final AccountListItem account;
final DashboardViewModel dashboardViewModel;
final bool isBusy;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
if (isBusy) {
return const CupertinoActivityIndicator(radius: 10);
}
return Row(
mainAxisSize: MainAxisSize.min,
children: [
if (account.isFunded) ...[
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 142),
child: Observer(
builder: (_) {
final fiatBalance = dashboardViewModel.balanceViewModel.accountFiatBalance(account);
return Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
MoneyText(
account.balance,
fractionalDigits: account.balance.decimals,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodySmall,
),
if (fiatBalance != null) ...[
const SizedBox(height: 2),
Text(
fiatBalance,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
],
],
);
},
),
),
const SizedBox(width: 8),
],
Icon(
Icons.arrow_forward_ios_rounded,
size: 14,
color: theme.colorScheme.onSurfaceVariant,
),
],
);
}
}
class _ArchiveIcon extends StatelessWidget {
const _ArchiveIcon({required this.size, required this.color});
final double size;
final Color color;
@override
Widget build(BuildContext context) => SizedBox.square(
dimension: size,
child: Center(
child: CakeImageWidget(
imageUrl: "assets/new-ui/archived.svg",
width: size * 19 / 24,
height: size * 19 / 24,
color: color,
),
),
);
}
+49 -2
View File
@@ -7,6 +7,10 @@ import "package:cake_wallet/generated/i18n.dart";
import "package:cake_wallet/main.dart";
import "package:cake_wallet/new-ui/modal_navigator.dart";
import "package:cake_wallet/new-ui/pages/card_customizer.dart";
import "package:cake_wallet/new-ui/pages/account_education_page.dart";
import "package:cake_wallet/new-ui/widgets/coins_page/accounts_promo.dart";
import "package:cake_wallet/src/widgets/alert_with_one_action.dart";
import "package:cw_core/sync_status.dart";
import "package:cake_wallet/new-ui/pages/seed/seed_backup_reminder_page.dart";
import "package:cake_wallet/new-ui/pages/send_page.dart";
import "package:cake_wallet/new-ui/pages/settings_page.dart";
@@ -214,6 +218,14 @@ class _NewHomePageState extends State<NewHomePage> with RouteAware {
),
],
),
if (AccountsPromo.supportsWallet(widget.dashboardViewModel.wallet.type) &&
widget.dashboardViewModel.accountListViewModel != null &&
!_lightningMode)
AccountsPromo(
settingsStore: widget.dashboardViewModel.settingsStore,
walletName: walletTypeToString(widget.dashboardViewModel.wallet.type),
onTap: _openAccountsFromPromo,
),
Column(
children: [
CoinActionRow(
@@ -264,11 +276,18 @@ class _NewHomePageState extends State<NewHomePage> with RouteAware {
);
Future<void> openCardCustomizer() async {
if (!_checkReadyToManage()) return;
await CardCustomizer.show(
context: context,
dashboardViewModel: widget.dashboardViewModel,
account: !widget.dashboardViewModel.lightningMode &&
widget.dashboardViewModel.isMultiAccountsEnabled
? widget.dashboardViewModel.accountListViewModel?.selectedAccount
: null,
lightningMode: widget.dashboardViewModel.lightningMode,
);
await widget.dashboardViewModel.accountListViewModel?.reload();
await widget.dashboardViewModel.loadCardDesigns();
}
void openSeedBackupReminder() {
@@ -283,7 +302,9 @@ class _NewHomePageState extends State<NewHomePage> with RouteAware {
}
Future<void> openAccountCustomizer() async {
if (widget.dashboardViewModel.accountListViewModel == null) return;
if (widget.dashboardViewModel.accountListViewModel == null || !_checkReadyToManage()) return;
final page = getIt.get<WalletAccountsPage>(param1: widget.dashboardViewModel);
await CupertinoScaffold.showCupertinoModalBottomSheet(
barrierColor: Colors.black.withAlpha(60),
@@ -292,10 +313,36 @@ class _NewHomePageState extends State<NewHomePage> with RouteAware {
parentContext: context,
heightMode: ModalHeightModes.fullScreen,
rootPage: Material(
child: getIt.get<WalletAccountsPage>(param1: widget.dashboardViewModel),
child: page,
),
),
);
await widget.dashboardViewModel.loadCardDesigns();
}
Future<void> _openAccountsFromPromo() async {
if (!AccountsPromo.supportsWallet(widget.dashboardViewModel.wallet.type) ||
widget.dashboardViewModel.accountListViewModel == null || !_checkReadyToManage()) return;
await AccountEducationPage(
settingsStore: widget.dashboardViewModel.settingsStore,
).show(context);
if (mounted) await openAccountCustomizer();
}
bool _checkReadyToManage() {
if (widget.dashboardViewModel.status is! SyncedSyncStatus) {
showDialog(
context: context,
builder: (context) => AlertWithOneAction(
alertTitle: S.of(context).wallet_is_syncing,
alertContent: S.of(context).cannot_manage_accounts_during_sync,
buttonText: S.of(context).ok,
buttonAction: Navigator.of(context).pop,
),
);
return false;
}
return true;
}
}
+403 -144
View File
@@ -1,30 +1,33 @@
import 'package:cake_wallet/core/auth_service.dart';
import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item.dart';
import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item_regular_row.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/modal_navigator.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:cake_wallet/src/widgets/new_list_row/new_list_section.dart';
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:cw_core/wallet_info.dart';
import "package:cake_wallet/core/auth_service.dart";
import "package:cake_wallet/entities/new_ui_entities/list_item/list_item.dart";
import "package:cake_wallet/entities/new_ui_entities/list_item/list_item_regular_row.dart";
import "package:cake_wallet/generated/i18n.dart";
import "package:cake_wallet/new-ui/modal_navigator.dart";
import "package:cake_wallet/new-ui/pages/coin_control_page.dart";
import "package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
import "package:cake_wallet/routes.dart";
import "package:cake_wallet/src/widgets/cake_image_widget.dart";
import "package:cake_wallet/src/widgets/new_list_row/new_list_section.dart";
import "package:cake_wallet/view_model/dashboard/dashboard_view_model.dart";
import "package:cw_core/wallet_info.dart";
import "package:cw_core/wallet_type.dart";
import 'package:flutter/material.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
import "package:flutter/material.dart";
import "package:modal_bottom_sheet/modal_bottom_sheet.dart";
bool _trueFunc(DashboardViewModel _) => true;
bool _falseFunc(DashboardViewModel _) => false;
bool _isBtc(DashboardViewModel vm) => vm.wallet.type == WalletType.bitcoin;
bool _alwaysVisible(DashboardViewModel _) => true;
bool _hasLightning(DashboardViewModel vm) => vm.hasLightning;
bool _hasSilentPayments(DashboardViewModel vm) => vm.hasSilentPayments;
bool _hasMweb(DashboardViewModel vm) => vm.hasMweb;
bool _hasWalletConnect(DashboardViewModel vm) => vm.hasWalletConnect;
bool _hasAccounts(DashboardViewModel vm) => vm.wallet.hasAccountsSupport;
bool _hasCoinControl(DashboardViewModel vm) => vm.wallet.hasCoinControl;
bool _requiresKeyImageSync(DashboardViewModel vm) =>
vm.wallet.type == WalletType.monero &&
[HardwareWalletType.cupcake, HardwareWalletType.trezor].contains(vm.wallet.hardwareWalletType);
@@ -34,16 +37,20 @@ class SettingsListItem {
this.iconPath,
this.title,
this.route, {
this.isCore = false,
this.requireAuth = false,
this.use2fa = _falseFunc,
this.condition = _trueFunc,
this.use2fa = _neverUse2fa,
this.condition = _alwaysVisible,
this.routeArgs,
this.routeArgsBuilder,
});
static bool _neverUse2fa(DashboardViewModel _) => false;
final String iconPath;
final String title;
final String route;
final bool isCore;
final Object? routeArgs;
final Object? Function(DashboardViewModel)? routeArgsBuilder;
final bool requireAuth;
@@ -52,72 +59,165 @@ class SettingsListItem {
}
class SettingsSectionData {
const SettingsSectionData(this.title, this.titleIconPath, this.items);
const SettingsSectionData(
this.title,
this.titleIconPath,
this.groups, {
this.titleColor,
this.headerSpacing = 16,
});
final String title;
final String titleIconPath;
final List<SettingsListItem> items;
final Map<String, List<ListItem>> groups;
final Color? titleColor;
final double headerSpacing;
}
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.walletAccountsPage,
condition: (vm) => vm.wallet.hasAccountsSupport,
routeArgsBuilder: (vm) => vm,
),
SettingsListItem(
"assets/new-ui/settings_row_icons/nodes.svg", S.current.nodes, Routes.manageNodes),
SettingsListItem(
"assets/new-ui/settings_row_icons/privacy.svg", S.current.privacy, Routes.privacyPage),
SettingsListItem("assets/new-ui/settings_row_icons/seed.svg", S.current.recovery_and_keys,
Routes.showKeysDisclaimer),
SettingsListItem("assets/new-ui/settings_row_icons/lightning_username.svg",
"Lightning ${S.current.username}", Routes.lightningUsernamePage,
condition: _hasLightning),
SettingsListItem("assets/new-ui/settings_row_icons/wc.svg", S.current.walletConnect,
class WalletSettingsResolver {
const WalletSettingsResolver();
String titleFor(WalletType walletType, S strings) =>
'${walletTypeToString(walletType)} ${strings.settings_title}'.trim();
String iconPathFor(WalletType walletType) =>
walletType == WalletType.robinhood
? "assets/new-ui/crypto_full_icons/robinhood.svg"
: "assets/new-ui/network_icons/${walletType.name.toLowerCase()}.svg";
List<List<SettingsListItem>> resolveSections(DashboardViewModel viewModel) {
final visibleItems = <SettingsListItem>[
SettingsListItem(
"assets/new-ui/settings_row_icons/accounts.svg",
viewModel.wallet.type == WalletType.bitcoin ? S.current.accounts_onchain : S.current.accounts,
Routes.walletAccountsPage,
isCore: true,
condition: _hasAccounts,
routeArgsBuilder: (vm) => vm,
),
SettingsListItem(
"assets/new-ui/settings_row_icons/nodes.svg",
S.current.nodes,
Routes.manageNodes,
isCore: true,
),
SettingsListItem(
"assets/new-ui/settings_row_icons/coin-control.svg",
S.current.coin_control_settings,
Routes.unspentCoinsList,
isCore: true,
condition: _hasCoinControl,
routeArgs: const CoinControlPageArgs(canEdit: false),
),
SettingsListItem(
"assets/new-ui/settings_row_icons/lightning_username.svg",
"Lightning ${S.current.username}",
Routes.lightningUsernamePage,
condition: _hasLightning,
),
SettingsListItem(
"assets/new-ui/settings_row_icons/silent-payments.svg",
S.current.silent_payments,
Routes.silentPaymentsSettings,
condition: _hasSilentPayments,
),
SettingsListItem(
"assets/new-ui/settings_row_icons/mweb.svg",
S.current.litecoin_mweb,
Routes.mwebSettings,
condition: _hasMweb,
),
SettingsListItem(
"assets/new-ui/settings_row_icons/wc.svg",
S.current.walletConnect,
Routes.walletConnectConnectionsListing,
condition: _hasWalletConnect),
//SettingsListItem("assets/new-ui/settings_row_icons/silent-payments.svg", S.current.silent_payments_settings, Routes.silentPaymentsSettings, condition: _isBtc),
//SettingsListItem("assets/new-ui/settings_row_icons/mweb.svg", S.current.litecoin_mweb_settings, Routes.mwebSettings, condition: _hasMweb),
SettingsListItem(
"assets/new-ui/settings_row_icons/sync-balance.svg",
S.current.resync_device,
Routes.syncKeyImagesDevices,
routeArgs: {'export-outputs': 'export-outputs'},
condition: _requiresKeyImageSync,
),
SettingsListItem(
"assets/new-ui/settings_row_icons/other.svg", S.current.other, Routes.otherSettingsPage),
]);
condition: _hasWalletConnect,
),
SettingsListItem(
"assets/new-ui/settings_row_icons/sync-balance.svg",
S.current.resync_device,
Routes.syncKeyImagesDevices,
routeArgs: const {"export-outputs": "export-outputs"},
condition: _requiresKeyImageSync,
),
].where((item) => item.condition(viewModel)).toList();
static SettingsSectionData appSettings =
SettingsSectionData(S.current.app_settings, "assets/new-ui/cake-setting.svg", [
SettingsListItem("assets/new-ui/settings_row_icons/connections.svg", S.current.connections,
Routes.connectionSync),
// SettingsListItem("assets/new-ui/settings_row_icons/defaults.svg", "Defaults", ""),
SettingsListItem("assets/new-ui/settings_row_icons/display.svg", S.current.display,
Routes.displaySettingsPage),
SettingsListItem("assets/new-ui/settings_row_icons/security.svg", S.current.security,
Routes.securityBackupPage),
SettingsListItem("assets/new-ui/settings_row_icons/backup.svg", S.current.backup, Routes.backup,
requireAuth: true,
use2fa: (vm) => vm.settingsStore.shouldRequireTOTP2FAForAllSecurityAndBackupSettings),
]);
final coreItems = visibleItems.where((item) => item.isCore).toList();
final featureItems = visibleItems.where((item) => !item.isCore).toList();
static SettingsSectionData otherSettings = SettingsSectionData("", "", [
SettingsListItem(
"assets/new-ui/settings_row_icons/support.svg", S.current.settings_support, Routes.support),
SettingsListItem(
"assets/new-ui/settings_row_icons/info.svg", S.current.about, Routes.aboutPage),
]);
return [
if (coreItems.isNotEmpty) coreItems,
if (featureItems.isNotEmpty) featureItems,
];
}
}
static List<SettingsSectionData> all = [walletSettings, appSettings, otherSettings];
class SettingsPageSectionsResolver {
const SettingsPageSectionsResolver();
List<SettingsListItem> walletGeneral(S strings) => [
SettingsListItem(
'assets/new-ui/settings_row_icons/privacy.svg',
strings.privacy,
Routes.privacyPage,
),
SettingsListItem(
'assets/new-ui/settings_row_icons/seed.svg',
strings.recovery_and_keys,
Routes.showKeysDisclaimer,
),
SettingsListItem(
'assets/new-ui/settings_row_icons/other.svg',
strings.other,
Routes.otherSettingsPage,
),
];
List<SettingsListItem> appSettings() => [
SettingsListItem(
"assets/new-ui/settings_row_icons/connections.svg",
S.current.connections,
Routes.connectionSync,
),
SettingsListItem(
"assets/new-ui/settings_row_icons/display.svg",
S.current.display,
Routes.displaySettingsPage,
),
SettingsListItem(
"assets/new-ui/settings_row_icons/security.svg",
S.current.security,
Routes.securityBackupPage,
),
SettingsListItem(
"assets/new-ui/settings_row_icons/backup.svg",
S.current.backup,
Routes.backup,
requireAuth: true,
use2fa: (vm) => vm.settingsStore.shouldRequireTOTP2FAForAllSecurityAndBackupSettings,
),
];
List<SettingsListItem> supportAndAbout(S strings) => [
SettingsListItem(
'assets/new-ui/settings_row_icons/support.svg',
strings.settings_support,
Routes.support,
),
SettingsListItem(
'assets/new-ui/settings_row_icons/info.svg',
strings.about,
Routes.aboutPage,
),
];
}
class NewSettingsPage extends StatelessWidget {
const NewSettingsPage({super.key, required this.dashboardViewModel, required this.authService});
const NewSettingsPage({
super.key,
required this.dashboardViewModel,
required this.authService,
});
final DashboardViewModel dashboardViewModel;
final AuthService authService;
@@ -125,89 +225,248 @@ class NewSettingsPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ModalNavigator(
parentContext: context,
rootPage: SettingsMainPage(
dashboardViewModel: dashboardViewModel,
authService: authService,
));
parentContext: context,
rootPage: SettingsMainPage(
dashboardViewModel: dashboardViewModel,
authService: authService,
),
);
}
}
class SettingsMainPage extends StatelessWidget {
const SettingsMainPage({super.key, required this.dashboardViewModel, required this.authService});
const SettingsMainPage({
super.key,
required this.dashboardViewModel,
required this.authService,
});
static const _walletSettingsResolver = WalletSettingsResolver();
static const _sectionsResolver = SettingsPageSectionsResolver();
final DashboardViewModel dashboardViewModel;
final AuthService authService;
@override
Widget build(BuildContext context) => Container(
color: Theme.of(context).colorScheme.surface,
child: Column(
children: [
ModalTopBar(
title: S.of(context).settings_title,
leadingIcon: const Icon(Icons.close),
leadingSemanticLabel: S.of(context).close,
onLeadingPressed: Navigator.of(context, rootNavigator: true).pop,
),
Expanded(
child: ListView(
controller: ModalScrollController.of(context),
padding: const EdgeInsets.fromLTRB(18, 0, 18, 32),
children: [
for (final (index, section) in _sections(context).indexed) ...[
if (index > 0) ...[
const SizedBox(height: 24),
Divider(color: Theme.of(context).colorScheme.surfaceContainerHighest),
const SizedBox(height: 24),
],
_SettingsSectionHeader(
iconPath: section.titleIconPath,
title: section.title,
foregroundColor: section.titleColor,
),
SizedBox(height: section.headerSpacing),
for (final (groupIndex, group) in section.groups.entries.indexed) ...[
if (groupIndex > 0) const SizedBox(height: 24),
NewListSections(sections: {group.key: group.value}),
],
],
],
),
),
],
),
);
List<SettingsSectionData> _sections(BuildContext context) => [
SettingsSectionData(
dashboardViewModel.wallet.name,
"assets/new-ui/wallet-setting.svg",
{
"wallet_type_settings": [
ListItemRegularRow(
keyValue: "wallet_type_settings",
label: _walletSettingsResolver.titleFor(
dashboardViewModel.wallet.type,
S.of(context),
),
iconPath: _walletSettingsResolver.iconPathFor(dashboardViewModel.wallet.type),
onTap: () => Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => WalletSettingsPage(
dashboardViewModel: dashboardViewModel,
authService: authService,
),
),
),
),
],
"wallet_general": _buildRows(context, _sectionsResolver.walletGeneral(S.of(context))),
},
titleColor: Theme.of(context).colorScheme.primary,
headerSpacing: 20,
),
SettingsSectionData(
S.of(context).app_settings,
"assets/new-ui/cake-setting.svg",
{
"app_settings": _buildRows(context, _sectionsResolver.appSettings()),
"support_and_about":
_buildRows(context, _sectionsResolver.supportAndAbout(S.of(context))),
},
),
];
List<ListItem> _buildRows(BuildContext context, List<SettingsListItem> items) => items
.where((item) => item.condition(dashboardViewModel))
.map((item) => _buildRow(context, item))
.toList();
ListItemRegularRow _buildRow(BuildContext context, SettingsListItem item) => ListItemRegularRow(
keyValue: item.route,
label: item.title,
iconPath: item.iconPath,
onTap: () => _openItem(context, item),
);
Future<void> _openItem(BuildContext context, SettingsListItem item) async {
final arguments = item.routeArgsBuilder?.call(dashboardViewModel) ?? item.routeArgs;
if (item.requireAuth) {
await authService.authenticateAction(
context,
conditionToDetermineIfToUse2FA: item.use2fa(dashboardViewModel),
route: item.route,
arguments: arguments,
);
return;
}
await Navigator.of(context).pushNamed(item.route, arguments: arguments);
}
}
class WalletSettingsPage extends StatelessWidget {
const WalletSettingsPage({
super.key,
required this.dashboardViewModel,
required this.authService,
});
static const _resolver = WalletSettingsResolver();
final DashboardViewModel dashboardViewModel;
final AuthService authService;
@override
Widget build(BuildContext context) {
List<ListItem> buildItems(SettingsSectionData section) => section.items
.map((item) => item.condition(dashboardViewModel)
? ListItemRegularRow(
keyValue: item.route,
label: item.title,
iconPath: item.iconPath,
onTap: () {
if (item.route.isNotEmpty) {
if (item.requireAuth) {
authService.authenticateAction(context,
conditionToDetermineIfToUse2FA: item.use2fa(dashboardViewModel),
route: item.route);
} else {
Navigator.of(context).pushNamed(
item.route,
arguments:
item.routeArgsBuilder?.call(dashboardViewModel) ?? item.routeArgs,
);
}
}
})
: null)
.whereType<ListItem>()
.toList();
final strings = S.of(context);
final sections = _resolver.resolveSections(dashboardViewModel);
return Container(
color: Theme.of(context).colorScheme.surface,
child: Column(children: [
ModalTopBar(
title: S.of(context).settings_title,
leadingIcon: Icon(Icons.close),
leadingSemanticLabel: S.of(context).close,
onLeadingPressed: Navigator.of(context, rootNavigator: true).pop,
onTrailingPressed: () {},
),
Expanded(
child: ListView(controller: ModalScrollController.of(context), children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
child: Column(
spacing: 16,
children: SettingsSectionData.all.expand((section) {
return [
if (section.titleIconPath.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Row(
spacing: 8,
children: [
CakeImageWidget(
imageUrl: section.titleIconPath,
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.onSurfaceVariant,
BlendMode.srcIn,
)),
Text(section.title, style: Theme.of(context).textTheme.titleMedium),
],
),
),
NewListSections(sections: {section.title: buildItems(section)}),
];
}).toList(),
),
child: Column(
children: [
ModalTopBar(
title: _resolver.titleFor(dashboardViewModel.wallet.type, strings),
titleLeadingWidget: CakeImageWidget(
imageUrl: _resolver.iconPathFor(dashboardViewModel.wallet.type),
width: 24,
height: 24,
),
]),
),
]),
leadingIcon: const Icon(Icons.arrow_back_ios_new),
leadingSemanticLabel: strings.seed_alert_back,
onLeadingPressed: Navigator.of(context).pop,
),
Expanded(
child: ListView(
controller: ModalScrollController.of(context),
padding: const EdgeInsets.fromLTRB(18, 0, 18, 32),
children: [
for (var index = 0; index < sections.length; index++) ...[
if (index > 0) const SizedBox(height: 24),
NewListSections(
sections: {
'wallet_settings_$index': sections[index]
.map(
(item) => ListItemRegularRow(
keyValue: item.route,
label: item.title,
iconPath: item.iconPath,
onTap: () => _openItem(context, item),
),
)
.toList(),
},
),
],
],
),
),
],
),
);
}
Future<void> _openItem(BuildContext context, SettingsListItem item) async {
final arguments = item.routeArgsBuilder?.call(dashboardViewModel) ?? item.routeArgs;
if (item.requireAuth) {
await authService.authenticateAction(
context,
conditionToDetermineIfToUse2FA: item.use2fa(dashboardViewModel),
route: item.route,
arguments: arguments,
);
return;
}
await Navigator.of(context).pushNamed(item.route, arguments: arguments);
}
}
class _SettingsSectionHeader extends StatelessWidget {
const _SettingsSectionHeader({
required this.iconPath,
required this.title,
this.foregroundColor,
});
final String iconPath;
final String title;
final Color? foregroundColor;
@override
Widget build(BuildContext context) {
final color = foregroundColor ?? Theme.of(context).colorScheme.onSurfaceVariant;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row(
children: [
CakeImageWidget(
imageUrl: iconPath,
height: 20,
width: 20,
colorFilter: ColorFilter.mode(color, BlendMode.srcIn),
),
const SizedBox(width: 8),
Expanded(
child: Text(
title,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.titleMedium?.copyWith(color: color),
),
),
],
),
);
}
}
+393 -416
View File
@@ -1,41 +1,45 @@
import "dart:async";
import "dart:math";
import "dart:ui";
import "package:cake_wallet/di.dart";
import "package:cake_wallet/core/utilities.dart";
import "package:cake_wallet/core/amount_parsing_proxy.dart";
import "package:cake_wallet/entities/bitcoin_amount_display_mode.dart";
import "package:cake_wallet/entities/new_ui_entities/list_item/list_item_toggle.dart";
import "package:cake_wallet/generated/i18n.dart";
import "package:cake_wallet/new-ui/pages/card_customizer.dart";
import "package:cake_wallet/new-ui/widgets/coins_page/cards/balance_card.dart";
import "package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
import "package:cake_wallet/src/screens/settings/widgets/account_creation_modal.dart";
import "package:cake_wallet/src/widgets/alert_with_one_action.dart";
import "package:cake_wallet/src/widgets/alert_with_two_actions.dart";
import "package:cake_wallet/src/widgets/new_list_row/new_list_section.dart";
import "package:cake_wallet/utils/show_pop_up.dart";
import "package:cake_wallet/src/screens/settings/widgets/account_creation_modal.dart";
import "package:cw_core/wallet_type.dart";
import "package:mobx/mobx.dart";
import "package:cake_wallet/generated/i18n.dart";
import "package:cake_wallet/new-ui/pages/account_education_page.dart";
import "package:cake_wallet/new-ui/pages/card_customizer.dart";
import "package:cake_wallet/new-ui/pages/hidden_accounts.dart";
import "package:cake_wallet/new-ui/widgets/coins_page/cards/balance_card.dart";
import "package:cake_wallet/new-ui/widgets/modern_button.dart";
import "package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
import "package:cake_wallet/src/widgets/alert_with_one_action.dart";
import "package:cake_wallet/view_model/dashboard/dashboard_view_model.dart";
import "package:cake_wallet/view_model/wallet_account_list/account_edit_or_create_view_model.dart";
import "package:cake_wallet/view_model/wallet_account_list/account_list_item.dart";
import "package:cake_wallet/view_model/wallet_account_list/account_edit_or_create_view_model.dart";
import "package:cake_wallet/view_model/wallet_account_list/wallet_account_list_view_model.dart";
import "package:cw_core/balance_card_layout.dart";
import "package:cw_core/balance_card_style_settings.dart";
import "package:cw_core/card_design.dart";
import "package:cw_core/sync_status.dart";
import "package:cw_core/utils/print_verbose.dart";
import "package:cw_core/wallet_type.dart";
import "package:flutter/cupertino.dart";
import "package:flutter/material.dart";
import "package:mobx/mobx.dart";
import "package:modal_bottom_sheet/modal_bottom_sheet.dart";
class AccountCustomizerListItem {
AccountCustomizerListItem({
const AccountCustomizerListItem({
required this.card,
required this.order,
required this.accountListItem,
required this.settings,
});
final BalanceCard card;
final int order;
final AccountListItem accountListItem;
final BalanceCardStyleSettings? settings;
}
class WalletAccountsPage extends StatefulWidget {
@@ -55,315 +59,229 @@ class WalletAccountsPage extends StatefulWidget {
}
class _WalletAccountsPageState extends State<WalletAccountsPage> {
static const double _kStackVisibleFactor = 0.2;
late final double cardWidth = MediaQuery.of(context).size.width * 0.9;
final List<AccountCustomizerListItem> _items = [];
bool _hasArchivedAccounts = false;
bool _loading = true;
ReactionDisposer? _accountsReaction;
WalletAccountListViewModel get accountListViewModel =>
widget.dashboardViewModel.accountListViewModel ?? widget.accountListViewModel;
bool get _isMultiAccountsEnabled =>
widget.dashboardViewModel.isMultiAccountsEnabled;
String get _assetName => widget.dashboardViewModel.appStore.amountParsingProxy
.getCryptoSymbol(accountListViewModel.currency);
bool get _isMultiAccountsEnabled => widget.dashboardViewModel.isMultiAccountsEnabled;
String get _assetName => AmountParsingProxy(
widget.dashboardViewModel.settingsStore.displayAmountsInSatoshi,
).getCryptoSymbol(widget.accountListViewModel.currency);
bool get _capitalizeAssetName =>
widget.dashboardViewModel.wallet.type != WalletType.bitcoin ||
widget.dashboardViewModel.settingsStore.displayAmountsInSatoshi !=
BitcoinAmountDisplayMode.satoshi;
widget.dashboardViewModel.settingsStore.displayAmountsInSatoshi !=
BitcoinAmountDisplayMode.satoshi;
double get cardWidth => min(MediaQuery.sizeOf(context).width * 0.9, 768);
@override
void initState() {
super.initState();
_accountsReaction = reaction(
(_) => accountListViewModel.accounts
(_) => widget.accountListViewModel.accounts
.map((account) => "${account.id}:${account.label}:${account.balance}")
.join(","),
(_) {
if (!mounted) return;
loadCards();
if (mounted) unawaited(loadCards());
},
);
WidgetsBinding.instance.addPostFrameCallback((_) => unawaited(_initialize()));
}
WidgetsBinding.instance.addPostFrameCallback((_) async {
await widget.dashboardViewModel.loadCardDesigns();
if (!mounted) return;
loadCards();
Future<void> _initialize() async {
await widget.dashboardViewModel.loadCardDesigns();
if (!mounted) return;
await _bringActiveAccountToFront();
if (!mounted || !_isMultiAccountsEnabled || _items.isEmpty) {
return;
}
if (!_isMultiAccountsEnabled) return;
final activeId = accountListViewModel.selectedAccount?.id;
for (int i = 0; i < _items.length - 1; i++) {
if (activeId != null && _items[i].accountListItem.id == activeId) {
final lastIndex = _items.length - 1;
final temp = _items[i];
_items[i] = _items[lastIndex];
_items[lastIndex] = temp;
await saveCardOrder();
await widget.dashboardViewModel.loadCardDesigns();
loadCards();
break;
}
}
});
final educationPage = AccountEducationPage(
settingsStore: widget.dashboardViewModel.settingsStore,
);
if (!educationPage.isDismissed) {
await educationPage.show(context);
}
}
@override
void dispose() {
_accountsReaction?.call();
accountListViewModel.reload();
saveCardOrder().then((value) => widget.dashboardViewModel.loadCardDesigns());
super.dispose();
}
void loadCards() {
final allAccounts = accountListViewModel.accounts;
final visible = widget.dashboardViewModel.visibleAccounts;
int get _walletInfoId => widget.dashboardViewModel.wallet.walletInfo.internalId;
if (visible.isEmpty && _items.isNotEmpty) return;
Future<void> _bringActiveAccountToFront() async {
await loadCards();
_items.clear();
if (!_isMultiAccountsEnabled) {
if (visible.isNotEmpty) {
final account = visible.first;
final realIndex = allAccounts.indexWhere((a) => a.id == account.id);
final design = (realIndex >= 0 &&
realIndex < widget.dashboardViewModel.cardDesigns.length)
? widget.dashboardViewModel.cardDesigns[realIndex]
: CardDesign.genericDefault;
_items.add(AccountCustomizerListItem(
card: BalanceCard(
accountName: account.label,
accountIndex: account.id,
balance: account.balance ?? "0.00",
accountBalance: account.balance ?? "0.00",
designSwitchDuration: Duration.zero,
assetName: _assetName,
capitalizeAssetName: _capitalizeAssetName,
selected: true,
width: cardWidth,
design: design,
),
order: realIndex >= 0 ? realIndex : 0,
accountListItem: account,
));
}
if (mounted) setState(() {});
final activeAccount =
widget.accountListViewModel.accounts.firstWhereOrNull((account) => account.isSelected);
if (activeAccount == null || _items.isEmpty) {
return;
}
for (int i = 0; i < allAccounts.length; i++) {
final realIndex = widget.dashboardViewModel.cardOrder[i];
final index = _items.indexWhere((item) => item.accountListItem.id == activeAccount.id);
if (index == -1 || index == _items.length - 1 || !mounted) {
return;
}
if (realIndex == null || realIndex < 0 || realIndex >= allAccounts.length) {
// db order broken.
reset();
break;
reorder(index, _items.length);
await saveCardOrder();
await widget.dashboardViewModel.loadCardDesigns();
}
Future<void> loadCards() async {
if (!mounted) return;
final accounts = widget.accountListViewModel.accounts;
final unnamedAccount = S.of(context).unnamed_account;
final resolvedCardWidth = cardWidth;
final walletCurrency = widget.dashboardViewModel.wallet.currency;
final styleSettings = await BalanceCardStyleSettings.getAll(_walletInfoId);
final layout = BalanceCardLayout.resolve(
accountIndices: accounts.map((account) => account.id).toList(),
settings: styleSettings,
);
final List<AccountCustomizerListItem> newItems = [];
for (int position = 0; position < layout.visible.length; position++) {
final accountIndex = layout.visible[position];
final account = accounts.firstWhereOrNull((item) => item.id == accountIndex);
if (account == null) {
continue;
}
_items.add(AccountCustomizerListItem(
final setting = layout.settingFor(accountIndex);
final isFrontCard = position == layout.visible.length - 1;
final accountLabel = account.label.trim().isEmpty ? unnamedAccount : account.label;
newItems.add(
AccountCustomizerListItem(
card: BalanceCard(
accountName: allAccounts[realIndex].label,
accountIndex: allAccounts[realIndex].id,
balance: allAccounts[realIndex].balance ?? "0.00",
accountBalance: allAccounts[realIndex].balance ?? "0.00",
accountName: "${account.id + 1}. $accountLabel",
accountBalance: account.balance,
fiatBalance:
widget.dashboardViewModel.balanceViewModel.accountFiatBalance(account) ?? "",
designSwitchDuration: Duration.zero,
assetName: _assetName,
capitalizeAssetName: _capitalizeAssetName,
selected: i == allAccounts.length - 1,
width: cardWidth,
design: widget.dashboardViewModel.cardDesigns[realIndex],
onCustomizeTapped: isFrontCard ? _openCardCustomizer : null,
selected: isFrontCard,
width: resolvedCardWidth,
design: CardDesign.fromStyleSettings(setting, walletCurrency),
),
order: realIndex,
accountListItem: allAccounts[realIndex]));
accountListItem: account,
settings: setting,
),
);
}
if (mounted) {
setState(() {});
setState(() {
_items
..clear()
..addAll(newItems);
_hasArchivedAccounts = layout.hidden.isNotEmpty;
_loading = false;
});
}
if (layout.needsRepair) {
await BalanceCardStyleSettings.setVisibleOrder(_walletInfoId, layout.orders);
}
}
@override
Widget build(BuildContext context) {
final showResetButton = _isMultiAccountsEnabled && _items.length > 1;
final showAccountsToggle = widget.dashboardViewModel.canToggleMultiAccounts;
final isToggleEnabled = widget.dashboardViewModel.isMultiAccountsEnabled;
if (_items.isEmpty && !showAccountsToggle) return const SizedBox.shrink();
return Container(
decoration: BoxDecoration(
Widget build(BuildContext context) => Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: const BorderRadius.vertical(top: Radius.circular(24))),
child: Column(
children: [
ModalTopBar(
title: S.of(context).wallet_accounts,
leadingIcon: const Icon(Icons.close),
leadingSemanticLabel: S.of(context).close,
onLeadingPressed: Navigator.of(context).maybePop,
trailingIcon: showResetButton ? const Icon(Icons.refresh) : null,
trailingSemanticLabel: S.of(context).reset,
onTrailingPressed: showResetDialog,
),
const SizedBox(height: 24),
if (showAccountsToggle)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: NewListSections(
sections: {
"": [
ListItemToggle(
keyValue: S.of(context).multiple_accounts,
label: S.of(context).multiple_accounts,
value: isToggleEnabled,
onChanged: (val) async {
await widget.dashboardViewModel.setMultiAccountsEnabled(val);
if (!mounted) return;
loadCards();
}),
],
},
),
),
if (_isMultiAccountsEnabled && _items.isNotEmpty)
Expanded(
child: Column(
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
),
child: Column(
children: [
ModalTopBar(
title: S.of(context).accounts,
leadingIcon: const Icon(Icons.close),
leadingSemanticLabel: S.of(context).close,
onLeadingPressed: Navigator.of(context).maybePop,
trailingWidget: Row(
mainAxisSize: MainAxisSize.min,
spacing: 8,
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 24.0),
child: Text(
S.of(context).account_customizer_desc,
style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant),
),
ModernButton(
semanticLabel: S.of(context).accounts_help,
icon: const Icon(Icons.question_mark),
size: 36,
iconSize: 19,
onPressed: () => AccountEducationPage(
settingsStore: widget.dashboardViewModel.settingsStore,
).show(context),
),
Expanded(
child: Stack(
children: [
ReorderableListView.builder(
padding: const EdgeInsets.only(bottom: 196),
scrollController: ModalScrollController.of(context),
onReorder: reorder,
proxyDecorator: (child, index, animation) => AnimatedBuilder(
animation: animation,
builder: (context, _) {
final animValue = Curves.easeOutCubic.transform(animation.value);
final scale = lerpDouble(1, 1.05, animValue)!;
return Opacity(
opacity: 1 - animValue.clamp(0.0, 0.1),
child: Center(
child: SizedBox(
width: cardWidth,
child: Transform.scale(
scale: scale,
child: child,
),
),
),
);
},
child: _items[index].card,
),
itemCount: _items.length,
itemBuilder: (context, int index) {
final card = _items[index].card;
final selectedItemIndex = _items.length - 1;
return Container(
key: ValueKey(index),
child: Semantics(
button: true,
selected: selectedItemIndex == index,
label: _items[index].accountListItem.label,
onTap: () => reorder(index, _items.length),
child: GestureDetector(
excludeFromSemantics: true,
onTap: () => reorder(index, _items.length),
child: Align(
alignment: Alignment.topCenter,
heightFactor: _kStackVisibleFactor,
child: card,
),
),
),
);
},
),
SafeArea(
child: Padding(
padding: const EdgeInsets.only(bottom: 50),
child: Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Material(
color: Colors.transparent,
child: MergeSemantics(
child: Semantics(
button: true,
child: InkWell(
borderRadius: BorderRadius.circular(999999),
onTap: _showAddAccountModal,
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(999999),
),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 18),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 8,
children: [
Icon(
Icons.add,
size: 28,
color: Theme.of(context).colorScheme.primary,
),
Text(
S.of(context).add_account,
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.w500,
),
)
],
),
),
),
),
),
),
),
),
),
),
),
],
if (_isMultiAccountsEnabled)
ModernButton.svg(
semanticLabel: S.of(context).archived_accounts,
svgPath: "assets/new-ui/archived.svg",
size: 36,
iconSize: 19,
backgroundColor:
_hasArchivedAccounts ? Theme.of(context).colorScheme.primary : null,
iconColor:
_hasArchivedAccounts ? Theme.of(context).colorScheme.onPrimary : null,
onPressed: _openArchivedAccounts,
),
),
],
),
),
],
),
);
}
if (widget.dashboardViewModel.canToggleMultiAccounts)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: NewListSections(sections: {
"": [
ListItemToggle(
keyValue: S.of(context).multiple_accounts,
label: S.of(context).multiple_accounts,
value: _isMultiAccountsEnabled,
onChanged: (value) async {
await widget.dashboardViewModel.setMultiAccountsEnabled(value);
if (mounted) await _initialize();
},
),
],
}),
),
if (_isMultiAccountsEnabled)
Padding(
padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 18),
child: Text(
S.of(context).account_customizer_desc,
textAlign: TextAlign.center,
style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant),
),
),
Expanded(
child: !_isMultiAccountsEnabled
? const SizedBox.shrink()
: _loading
? const Center(child: CupertinoActivityIndicator())
: _items.isEmpty
? const SizedBox.shrink()
: _AccountCards(
items: _items,
cardWidth: cardWidth,
onReorder: reorder,
onAddAccount: _showAddAccountModal,
),
),
],
),
);
bool _checkReadyToManage() {
if (widget.dashboardViewModel.wallet.type == WalletType.bitcoin) {
return true;
}
if (widget.dashboardViewModel.status is! SyncedSyncStatus) {
showDialog(
context: context,
@@ -380,33 +298,48 @@ class _WalletAccountsPageState extends State<WalletAccountsPage> {
}
Future<void> _showAddAccountModal() async {
if (!_checkReadyToManage()) return;
final createViewModel = getIt.get<WalletAccountEditOrCreateViewModel>();
final res = await showCupertinoModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
builder: (context) => Material(
child: AccountCreationModal(
viewModel: createViewModel,
),
));
if (res != null && res is bool && res == true) {
await accountListViewModel.reload();
await Future<void>.delayed(Duration.zero);
final accounts = accountListViewModel.accounts;
if (accounts.isNotEmpty) {
final newestAccount = accounts.reduce((a, b) => a.id > b.id ? a : b);
await accountListViewModel.select(newestAccount);
}
await widget.dashboardViewModel.loadCardDesigns();
loadCards();
if (mounted) setState(() {});
if (!_checkReadyToManage()) {
return;
}
final modal = AccountCreationModal(viewModel: widget.accountEditOrCreateViewModel);
final res = await showCupertinoModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
expand: true,
enableDrag: false,
builder: (_) => Material(child: modal),
);
if (res == true && mounted) {
await widget.accountListViewModel.reload();
final accounts = widget.accountListViewModel.accounts;
if (accounts.isNotEmpty) {
await widget.accountListViewModel.select(
accounts.reduce((a, b) => a.id > b.id ? a : b),
);
}
await widget.dashboardViewModel.loadCardDesigns();
await loadCards();
await saveCardOrder();
}
}
Future<void> _openArchivedAccounts() async {
await Navigator.of(context).push<void>(
CupertinoPageRoute(
builder: (context) => Material(
child: HiddenAccountsPage(
accountListViewModel: widget.accountListViewModel,
dashboardViewModel: widget.dashboardViewModel,
),
),
),
);
if (!mounted) {
return;
}
await widget.dashboardViewModel.loadCardDesigns();
await loadCards();
}
Future<void> _openCardCustomizer() async {
@@ -414,49 +347,34 @@ class _WalletAccountsPageState extends State<WalletAccountsPage> {
return;
}
await CardCustomizer.show(
final account = _items.last.accountListItem;
await widget.accountListViewModel.select(account);
if (!mounted) return;
final archived = await CardCustomizer.show(
context: context,
dashboardViewModel: widget.dashboardViewModel,
lightningMode: widget.dashboardViewModel.lightningMode,
asModalSheet: false,
onSaved: _reloadAccountsAndCards,
account: account,
canHide: _items.length > 1,
asPage: true,
);
}
Future<void> _reloadAccountsAndCards() async {
await accountListViewModel.reload();
if (archived && _items.length > 1) {
final nextAccount = _items[_items.length - 2].accountListItem;
await widget.accountListViewModel.select(
widget.accountListViewModel.accounts
.firstWhereOrNull((item) => item.id == nextAccount.id) ??
nextAccount,
);
}
await widget.accountListViewModel.reload();
await widget.dashboardViewModel.loadCardDesigns();
if (!mounted) return;
loadCards();
}
void _onCardsReordered(int oldRealIndex, int newRealIndex) {
final accounts = accountListViewModel.accounts;
if (oldRealIndex < 0 ||
newRealIndex < 0 ||
oldRealIndex >= accounts.length ||
newRealIndex >= accounts.length) {
if (!mounted) {
return;
}
final oldAccountId = accounts[oldRealIndex].id;
final newAccountId = accounts[newRealIndex].id;
final from = _items.indexWhere((item) => item.accountListItem.id == oldAccountId);
final to = _items.indexWhere((item) => item.accountListItem.id == newAccountId);
if (from < 0 || to < 0 || from == to) return;
final insertIndex = from < to ? to + 1 : to;
reorder(from, insertIndex);
saveCardOrder().then((_) {
if (!mounted) return;
widget.dashboardViewModel.loadCardDesigns();
});
await loadCards();
}
void reorder(int oldIndex, int newIndex) {
if (!_isMultiAccountsEnabled) return;
setState(() {
if (oldIndex < newIndex) {
newIndex -= 1;
@@ -468,100 +386,159 @@ class _WalletAccountsPageState extends State<WalletAccountsPage> {
// necessary to copy all this to keep constant constructor for BalanceCard
for (int i = 0; i < _items.length; i++) {
_items[i] = AccountCustomizerListItem(
card: BalanceCard(
accountName: _items[i].card.accountName,
balance: _items[i].card.balance,
accountIndex: _items[i].card.accountIndex,
accountBalance: _items[i].card.accountBalance,
assetName: _items[i].card.assetName,
capitalizeAssetName: _items[i].card.capitalizeAssetName,
designSwitchDuration: _items[i].card.designSwitchDuration,
onCustomizeTapped: (i == _items.length - 1) ? _openCardCustomizer : null,
selected: i == _items.length - 1,
width: _items[i].card.width,
design: _items[i].card.design,
),
order: i,
accountListItem: _items[i].accountListItem);
card: BalanceCard(
accountName: _items[i].card.accountName,
balance: _items[i].card.balance,
accountIndex: _items[i].card.accountIndex,
accountBalance: _items[i].card.accountBalance,
fiatBalance: _items[i].card.fiatBalance,
assetName: _items[i].card.assetName,
capitalizeAssetName: _items[i].card.capitalizeAssetName,
designSwitchDuration: _items[i].card.designSwitchDuration,
onCustomizeTapped: (i == _items.length - 1) ? _openCardCustomizer : null,
selected: i == _items.length - 1,
width: _items[i].card.width,
design: _items[i].card.design,
),
accountListItem: _items[i].accountListItem,
settings: _items[i].settings,
);
}
if (newIndex == _items.length - 1 || oldIndex == _items.length - 1) {
accountListViewModel.select(_items[_items.length - 1].accountListItem);
widget.accountListViewModel.select(_items[_items.length - 1].accountListItem);
}
}
Future<void> saveCardOrder() async {
if (!_isMultiAccountsEnabled) return;
for (int orderIndex = 0; orderIndex < _items.length; orderIndex++) {
final item = _items[orderIndex];
printV("${item.accountListItem.id}: $orderIndex");
final existing = await BalanceCardStyleSettings.get(
widget.dashboardViewModel.wallet.walletInfo.internalId,
item.accountListItem.id,
);
await BalanceCardStyleSettings.fromCardDesign(
walletInfoId: widget.dashboardViewModel.wallet.walletInfo.internalId,
accountIndex: item.accountListItem.id,
cardOrder: orderIndex,
design: item.card.design,
iconStyleIndex: existing?.iconStyleIndex ?? 0,
gradientIndexOverride: existing?.gradientIndex)
.insert();
}
}
Future<void> showResetDialog() async {
final res = await showPopUp(
context: context,
builder: (context) => AlertWithTwoActions(
alertTitle: S.of(context).reset,
alertContent: S.of(context).card_order_reset_desc,
leftButtonText: S.of(context).yes,
rightButtonText: S.of(context).no,
actionLeftButton: () {
Navigator.of(context).pop(true);
},
actionRightButton: Navigator.of(context).pop));
if (res != null && res is bool && res) {
reset();
}
}
Future<void> reset() async {
_items.clear();
final accounts = accountListViewModel.accounts;
for (int i = 0; i < accounts.length; i++) {
_items.add(AccountCustomizerListItem(
card: BalanceCard(
accountName: accounts[i].label,
accountIndex: accounts[i].id,
balance: accounts[i].balance ?? "0.00",
accountBalance: accounts[i].balance ?? "0.00",
assetName: _assetName,
capitalizeAssetName: _capitalizeAssetName,
selected: i == accounts.length - 1,
designSwitchDuration: const Duration(milliseconds: 200),
width: cardWidth,
design: i >= widget.dashboardViewModel.cardDesigns.length
? CardDesign.genericDefault
: widget.dashboardViewModel.cardDesigns[i],
),
order: i,
accountListItem: accounts[i]));
}
await saveCardOrder();
if (accounts.isNotEmpty) {
await accountListViewModel.select(accounts.last);
}
await widget.dashboardViewModel.loadCardDesigns();
if (mounted) setState(() {});
await BalanceCardStyleSettings.setOrder(_walletInfoId, {
for (int position = 0; position < _items.length; position++)
_items[position].accountListItem.id: position,
});
}
}
class _AccountCards extends StatelessWidget {
const _AccountCards({
required this.items,
required this.cardWidth,
required this.onReorder,
required this.onAddAccount,
});
static const double _kStackVisibleFactor = 0.2;
final List<AccountCustomizerListItem> items;
final double cardWidth;
final ReorderCallback onReorder;
final VoidCallback onAddAccount;
@override
Widget build(BuildContext context) => Stack(
children: [
ReorderableListView.builder(
padding: const EdgeInsets.only(bottom: 196),
scrollController: ModalScrollController.of(context),
onReorder: onReorder,
proxyDecorator: (child, index, animation) => AnimatedBuilder(
animation: animation,
builder: (context, _) {
final animValue = Curves.easeOutCubic.transform(animation.value);
final scale = lerpDouble(1, 1.05, animValue)!;
return Opacity(
opacity: 1 - animValue.clamp(0.0, 0.1),
child: Center(
child: SizedBox(
width: cardWidth,
child: Transform.scale(
scale: scale,
child: child,
),
),
),
);
},
child: items[index].card,
),
itemCount: items.length,
itemBuilder: (context, index) {
final selectedItemIndex = items.length - 1;
return Container(
key: ValueKey(items[index].accountListItem.id),
child: Semantics(
button: true,
selected: selectedItemIndex == index,
label: "${items[index].accountListItem.id + 1}. "
"${items[index].accountListItem.label.trim().isEmpty ? S.of(context).unnamed_account : items[index].accountListItem.label}",
onTap: () => onReorder(index, items.length),
child: GestureDetector(
excludeFromSemantics: true,
onTap: () {
onReorder(index, items.length);
},
child: Align(
alignment: Alignment.topCenter,
heightFactor: _kStackVisibleFactor,
child: items[index].card,
),
),
),
);
},
),
SafeArea(
child: Padding(
padding: const EdgeInsets.only(bottom: 50),
child: Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Material(
color: Colors.transparent,
child: MergeSemantics(
child: Semantics(
button: true,
child: InkWell(
borderRadius: BorderRadius.circular(999999),
onTap: onAddAccount,
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(999999),
),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 18),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 8,
children: [
Icon(
Icons.add,
size: 28,
color: Theme.of(context).colorScheme.primary,
),
Text(
S.of(context).add_account,
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.w500,
),
),
],
),
),
),
),
),
),
),
),
),
),
),
],
);
}
@@ -1,38 +1,50 @@
import 'package:bloc/bloc.dart';
import "package:bloc/bloc.dart";
import "package:cake_wallet/entities/bitcoin_amount_display_mode.dart";
import "package:cake_wallet/bitcoin/bitcoin.dart";
import 'package:cake_wallet/monero/monero.dart';
import 'package:cake_wallet/wownero/wownero.dart';
import "package:cake_wallet/monero/monero.dart";
import "package:cake_wallet/wownero/wownero.dart" as wow;
import "package:cw_core/balance_card_style_settings.dart";
import 'package:cw_core/card_design.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/wallet_base.dart';
import "package:cw_core/card_design.dart";
import "package:cw_core/crypto_currency.dart";
import "package:cw_core/wallet_base.dart";
import "package:cw_core/wallet_type.dart";
import 'package:flutter/src/painting/gradient.dart';
import 'package:meta/meta.dart';
import "package:flutter/src/painting/gradient.dart";
import "package:meta/meta.dart";
part 'card_customizer_event.dart';
part 'card_customizer_state.dart';
part "card_customizer_event.dart";
part "card_customizer_state.dart";
class CardCustomizerBlocParams {
CardCustomizerBlocParams({required this.lightningMode, required this.canHide, this.amountDisplayMode});
final bool lightningMode;
final BitcoinAmountDisplayMode? amountDisplayMode;
final bool canHide;
}
class CardCustomizerBloc extends Bloc<CardCustomizerEvent, CardCustomizerState> {
final WalletBase _wallet;
final bool lightningMode;
final bool displaySats;
CardCustomizerBloc(this._wallet, {this.lightningMode = false, this.displaySats = false})
CardCustomizerBloc(this._wallet, {this.lightningMode = false, this.displaySats = false, this.canHide = false})
: super(CardCustomizerNotLoaded(
0, 0, [CardDesign.genericDefault], [], "", -1, displaySats, 0)) {
0, 0, [CardDesign.genericDefault], [], "", -1, displaySats, 0,),) {
on<_Init>(_init);
on<CardDesignSelected>(_onDesignSelected);
on<ColorSelected>(_onColorSelected);
on<IconStyleSelected>(_onIconStyleSelected);
on<DesignSaved>(_onDesignSaved);
on<AccountHidden>(_onAccountHidden);
on<AccountNameChanged>(_onAccountNameChanged);
add(_Init());
}
bool get _accountNameEnabled =>
!lightningMode && _wallet.walletInfo.isMultiAccountsEnabled == true;
final WalletBase _wallet;
final bool lightningMode;
final bool displaySats;
final bool canHide;
bool get _accountNameEnabled => !lightningMode &&
(_wallet.hasNativeAccounts || _wallet.walletInfo.isMultiAccountsEnabled == true);
List<Gradient> _updateAvailableColors(CardDesign currentDesign) {
final list = List<Gradient>.from(CardDesign.allGradients, growable: true);
@@ -42,8 +54,21 @@ class CardCustomizerBloc extends Bloc<CardCustomizerEvent, CardCustomizerState>
return list;
}
Future<BalanceCardStyleSettings?> _loadCurrentDesignSettings(int accountIndex) async {
return (await BalanceCardStyleSettings.get(_wallet.walletInfo.internalId, accountIndex));
Future<BalanceCardStyleSettings?> _loadCurrentDesignSettings(int accountIndex) =>
BalanceCardStyleSettings.get(_wallet.walletInfo.internalId, accountIndex);
Future<int> _cardOrderFor(BalanceCardStyleSettings? currentDesignSettings) async {
if (currentDesignSettings != null) {
return currentDesignSettings.cardOrder;
}
var highest = -1;
for (final setting in await BalanceCardStyleSettings.getAll(_wallet.walletInfo.internalId)) {
if (setting.cardOrder > highest) {
highest = setting.cardOrder;
}
}
return highest + 1;
}
List<CardDesign> _initAvailableDesigns({bool lightningMode = false}) {
@@ -53,8 +78,9 @@ class CardCustomizerBloc extends Bloc<CardCustomizerEvent, CardCustomizerState>
ret.add(CardDesign.gradientOnlyDesign);
ret.add(CardDesign.forCurrencyIcon(curr));
if (CardDesign.specialDesignsForCurrencies[curr] != null)
if (CardDesign.specialDesignsForCurrencies[curr] != null) {
ret.add(CardDesign.forCurrencySpecial(curr));
}
return ret;
}
@@ -76,8 +102,12 @@ class CardCustomizerBloc extends Bloc<CardCustomizerEvent, CardCustomizerState>
BalanceCardStyleSettings? settings,
List<CardIconPath> availableIconPaths,
) {
if (settings == null || availableIconPaths.isEmpty) return 0;
if (settings.iconStyleIndex >= availableIconPaths.length) return 0;
if (settings == null || availableIconPaths.isEmpty) {
return 0;
}
if (settings.iconStyleIndex >= availableIconPaths.length) {
return 0;
}
return settings.iconStyleIndex;
}
@@ -86,7 +116,7 @@ class CardCustomizerBloc extends Bloc<CardCustomizerEvent, CardCustomizerState>
return ret == -1 ? CardDesign.allGradients.length : ret;
}
void _init(_Init event, Emitter<CardCustomizerState> emit) async {
Future<void> _init(_Init event, Emitter<CardCustomizerState> emit) async {
final accountInfo = await _getCurrentAccountInfo();
final accountName = accountInfo.accountName;
final accountIndex = accountInfo.accountIndex;
@@ -108,9 +138,9 @@ class CardCustomizerBloc extends Bloc<CardCustomizerEvent, CardCustomizerState>
accountName,
accountIndex,
displaySats,
currentDesignSettings?.cardOrder ?? 0,
await _cardOrderFor(currentDesignSettings),
availableIconPaths: availableIconPaths,
selectedIconIndex: selectedIconIndex));
selectedIconIndex: selectedIconIndex,),);
}
Future<({String accountName, int accountIndex})> _getCurrentAccountInfo() async {
@@ -134,7 +164,7 @@ class CardCustomizerBloc extends Bloc<CardCustomizerEvent, CardCustomizerState>
}
if (_wallet.type == WalletType.wownero) {
final account = wownero!.getCurrentAccount(_wallet);
final account = wow.wownero!.getCurrentAccount(_wallet);
return (accountName: account.label, accountIndex: account.id);
}
@@ -160,7 +190,7 @@ class CardCustomizerBloc extends Bloc<CardCustomizerEvent, CardCustomizerState>
emit(state.copyWith(
selectedDesignIndex: event.newDesignIndex,
availableColors: newColors,
selectedColorIndex: newColorIndex));
selectedColorIndex: newColorIndex,),);
}
void _onColorSelected(ColorSelected event, Emitter<CardCustomizerState> emit) {
@@ -176,18 +206,20 @@ class CardCustomizerBloc extends Bloc<CardCustomizerEvent, CardCustomizerState>
}
Future<void> _onDesignSaved(DesignSaved event, Emitter<CardCustomizerState> emit) async {
final currentSettings = await _loadCurrentDesignSettings(state.accountIndex);
await BalanceCardStyleSettings.fromCardDesign(
walletInfoId: _wallet.walletInfo.internalId,
accountIndex: state.accountIndex,
cardOrder: state.cardOrder,
design: state.selectedDesign,
iconStyleIndex: state.selectedIconIndex,
gradientIndexOverride: state.selectedColorIndex)
.insert();
walletInfoId: _wallet.walletInfo.internalId,
accountIndex: state.accountIndex,
cardOrder: state.cardOrder,
design: state.selectedDesign,
iconStyleIndex: state.selectedIconIndex,
gradientIndexOverride: state.selectedColorIndex,
hidden: currentSettings?.hidden ?? false,
).insert();
await saveAccountName();
emit(CardCustomizerSaved(
emit(
CardCustomizerSaved(
state.selectedDesignIndex,
state.selectedColorIndex,
state.availableDesigns,
@@ -197,7 +229,34 @@ class CardCustomizerBloc extends Bloc<CardCustomizerEvent, CardCustomizerState>
state.displaySats,
state.cardOrder,
availableIconPaths: state.availableIconPaths,
selectedIconIndex: state.selectedIconIndex));
selectedIconIndex: state.selectedIconIndex,
),
);
}
Future<void> _onAccountHidden(AccountHidden event, Emitter<CardCustomizerState> emit) async {
await BalanceCardStyleSettings.fromCardDesign(
walletInfoId: _wallet.walletInfo.internalId,
accountIndex: state.accountIndex,
cardOrder: state.cardOrder,
design: state.selectedDesign,
iconStyleIndex: state.selectedIconIndex,
gradientIndexOverride: state.selectedColorIndex,
hidden: true,
).insert();
emit(
CardCustomizerSaved(
state.selectedDesignIndex,
state.selectedColorIndex,
state.availableDesigns,
state.availableColors,
state.accountName,
state.accountIndex,
state.displaySats,
state.cardOrder,
),
);
}
Future<void> saveAccountName() async {
@@ -205,12 +264,9 @@ class CardCustomizerBloc extends Bloc<CardCustomizerEvent, CardCustomizerState>
if (_wallet.type == WalletType.monero) {
await saveMoneroAccountName();
}
if (_wallet.type == WalletType.wownero) {
} else if (_wallet.type == WalletType.wownero) {
await saveWowneroAccountName();
}
if (_wallet.type == WalletType.bitcoin && !lightningMode) {
} else if (_wallet.type == WalletType.bitcoin) {
await saveBitcoinAccountName();
}
}
@@ -218,19 +274,21 @@ class CardCustomizerBloc extends Bloc<CardCustomizerEvent, CardCustomizerState>
Future<void> saveMoneroAccountName() async {
final MoneroAccountList moneroAccountList = monero!.getAccountList(_wallet);
await moneroAccountList.setLabelAccount(_wallet,
accountIndex: state.accountIndex, label: state.accountName);
accountIndex: state.accountIndex, label: state.accountName,);
await _wallet.save();
}
Future<void> saveWowneroAccountName() async {
final WowneroAccountList wowneroAccountList = wownero!.getAccountList(_wallet);
await wowneroAccountList.setLabelAccount(_wallet,
accountIndex: state.accountIndex, label: state.accountName);
final accountList = wow.wownero!.getAccountList(_wallet);
await accountList.setLabelAccount(
_wallet,
accountIndex: state.accountIndex,
label: state.accountName,
);
await _wallet.save();
}
Future<void> saveBitcoinAccountName() async {
await _wallet.walletInfo.renameAccount(
accountIndex: state.accountIndex,
@@ -1,4 +1,4 @@
part of 'card_customizer_bloc.dart';
part of "card_customizer_bloc.dart";
@immutable
sealed class CardCustomizerEvent {}
@@ -6,27 +6,29 @@ sealed class CardCustomizerEvent {}
class _Init extends CardCustomizerEvent {}
class CardDesignSelected extends CardCustomizerEvent {
final int newDesignIndex;
CardDesignSelected(this.newDesignIndex);
final int newDesignIndex;
}
class ColorSelected extends CardCustomizerEvent {
final int newColorIndex;
ColorSelected(this.newColorIndex);
final int newColorIndex;
}
class AccountNameChanged extends CardCustomizerEvent {
final String newAccountName;
AccountNameChanged(this.newAccountName);
final String newAccountName;
}
class DesignSaved extends CardCustomizerEvent {}
class IconStyleSelected extends CardCustomizerEvent {
final int iconIndex;
IconStyleSelected(this.iconIndex);
final int iconIndex;
}
class AccountHidden extends CardCustomizerEvent {}
@@ -0,0 +1,174 @@
import "package:cake_wallet/generated/i18n.dart";
import "package:cake_wallet/new-ui/widgets/money/money_text.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/wallet_account_list/account_list_item.dart";
import "package:flutter/material.dart";
class ArchiveConfirmationContent extends StatelessWidget {
const ArchiveConfirmationContent({
required this.account,
required this.fiatBalance,
required this.isFunded,
});
final AccountListItem account;
final String? fiatBalance;
final bool isFunded;
@override
Widget build(BuildContext context) => Column(
mainAxisSize: MainAxisSize.min,
children: [
if (isFunded) ...[
Text(
S.of(context).archive_account_funds_title,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: context.customColors.warningOutlineColor,
),
),
const SizedBox(height: 20),
AccountFundsSummary(
account: account,
fiatBalance: fiatBalance,
borderColor: context.customColors.warningOutlineColor,
),
const SizedBox(height: 24),
Text(
S.of(context).archive_account_move_funds,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: context.customColors.warningOutlineColor,
),
),
const SizedBox(height: 20),
Text(
S.of(context).archive_account_funded_disclaimer,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
] else ...[
AccountSummary(account: account),
const SizedBox(height: 24),
Text(
S.of(context).archive_account_empty_disclaimer,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 12),
Text(
S.of(context).archive_account_restore_hint,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
],
);
}
class AccountSummary extends StatelessWidget {
const AccountSummary({required this.account});
final AccountListItem account;
@override
Widget build(BuildContext context) {
final label = account.label.trim();
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(18),
),
child: Row(
children: [
CakeImageWidget(
imageUrl: "assets/new-ui/account.svg",
width: 24,
height: 24,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
const SizedBox(width: 12),
Expanded(
child: Text.rich(
TextSpan(
children: [
TextSpan(
text: "${account.id + 1}. ",
style: TextStyle(color: Theme.of(context).colorScheme.primary),
),
TextSpan(
text: label.isEmpty ? S.of(context).unnamed_account : label,
),
],
),
style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w500),
),
),
],
),
);
}
}
class AccountFundsSummary extends StatelessWidget {
const AccountFundsSummary({
required this.account,
required this.fiatBalance,
required this.borderColor,
});
final AccountListItem account;
final String? fiatBalance;
final Color borderColor;
@override
Widget build(BuildContext context) => Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
border: Border.all(color: borderColor),
borderRadius: BorderRadius.circular(18),
),
child: Row(
children: [
CakeImageWidget(
imageUrl: account.balance.currency.iconPath ??
"assets/new-ui/crypto_full_icons/${account.balance.currency.name.toLowerCase()}.svg",
width: 24,
height: 24,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MoneyText(
account.balance,
fractionalDigits: account.balance.decimals,
style: Theme.of(context).textTheme.bodyMedium,
),
if (fiatBalance != null) ...[
const SizedBox(height: 2),
Text(
fiatBalance!,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
],
),
),
],
),
);
}
@@ -0,0 +1,109 @@
import "package:cake_wallet/generated/i18n.dart";
import "package:cake_wallet/src/widgets/cake_image_widget.dart";
import "package:cake_wallet/store/settings_store.dart";
import "package:cw_core/wallet_type.dart";
import "package:flutter/material.dart";
import "package:flutter_mobx/flutter_mobx.dart";
class AccountsPromo extends StatelessWidget {
const AccountsPromo({
required this.settingsStore,
required this.walletName,
required this.onTap,
super.key,
});
final SettingsStore settingsStore;
final String walletName;
final VoidCallback onTap;
static bool supportsWallet(WalletType walletType) => walletType == WalletType.bitcoin;
@override
Widget build(BuildContext context) => Observer(
builder: (_) {
if (settingsStore.accountsHomePromoDismissed) {
return const SizedBox.shrink();
}
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
children: [
Material(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(18),
child: InkWell(
borderRadius: BorderRadius.circular(18),
onTap: onTap,
child: Container(
constraints: const BoxConstraints(minHeight: 64),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
decoration: BoxDecoration(
border: Border.all(color: Theme.of(context).colorScheme.outlineVariant),
borderRadius: BorderRadius.circular(18),
),
child: Row(
children: [
const CakeImageWidget(
imageUrl: "assets/new-ui/settings_row_icons/accounts.svg",
width: 24,
height: 24,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
S.of(context).accounts_home_promo_title(walletName),
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 2),
Text(
S.of(context).accounts_home_promo_description,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
),
const SizedBox(width: 8),
Icon(
Icons.arrow_forward_ios,
size: 16,
color: Theme.of(context).colorScheme.primary,
),
],
),
),
),
),
const SizedBox(height: 12),
Semantics(
button: true,
child: InkWell(
borderRadius: BorderRadius.circular(8),
onTap: () => settingsStore.accountsHomePromoDismissed = true,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: Text(
S.of(context).do_not_show_anymore,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.w500,
),
),
),
),
),
],
),
);
},
);
}
@@ -1,7 +1,9 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:cw_core/card_design.dart';
import 'package:flutter/material.dart';
import "package:cake_wallet/generated/i18n.dart";
import "package:cake_wallet/new-ui/widgets/money/money_text.dart";
import "package:cake_wallet/src/widgets/cake_image_widget.dart";
import "package:cw_core/amount/money.dart";
import "package:cw_core/card_design.dart";
import "package:flutter/material.dart";
class BalanceCardAction {
final String label;
@@ -22,7 +24,7 @@ class BalanceCard extends StatelessWidget {
this.borderRadius = 20,
this.selected = false,
this.accountName = "",
this.accountBalance = "",
this.accountBalance,
this.balance = "",
this.fiatBalance = "",
this.assetName = "",
@@ -37,7 +39,7 @@ class BalanceCard extends StatelessWidget {
final double width;
final double borderRadius;
final Gradient? gradient;
final String accountBalance;
final Money? accountBalance;
final String accountName;
final String balance;
final String fiatBalance;
@@ -62,7 +64,7 @@ class BalanceCard extends StatelessWidget {
final leadText = fiatFirst ? S.of(context).wallet_balance : accountName;
final bool showText = accountBalance.isNotEmpty ||
final bool showText = accountBalance != null ||
leadText.isNotEmpty ||
balance.isNotEmpty ||
fiatBalance.isNotEmpty ||
@@ -102,7 +104,7 @@ class BalanceCard extends StatelessWidget {
),
)
: const SizedBox.shrink(
key: ValueKey('svgFullOff'),
key: ValueKey("svgFullOff"),
),
),
Padding(
@@ -117,7 +119,7 @@ class BalanceCard extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
children: [
if (leadText.isNotEmpty || accountBalance.isNotEmpty)
if (leadText.isNotEmpty || accountBalance != null)
Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
@@ -153,8 +155,10 @@ class BalanceCard extends StatelessWidget {
// readers, so drop it while it is invisible.
child: ExcludeSemantics(
excluding: selected,
child: Text(
child: MoneyText.optional(
accountBalance,
fractionalDigits: accountBalance?.decimals ?? 8,
showSymbol: false,
style: TextStyle(color: design.colors.textColor, fontSize: 14),
),
),
@@ -190,7 +194,13 @@ class BalanceCard extends StatelessWidget {
fontSize: 28,
fontWeight: FontWeight.w500,
letterSpacing: -0.4),
child: Text(fiatFirst ? fiatBalance : balance),
child: !fiatFirst && balance.isEmpty
? MoneyText.optional(
accountBalance,
fractionalDigits: accountBalance?.decimals ?? 8,
showSymbol: false,
)
: Text(fiatFirst ? fiatBalance : balance),
),
AnimatedDefaultTextStyle(
duration: designSwitchDuration,
@@ -270,7 +280,7 @@ class BalanceCard extends StatelessWidget {
child: _CornerSvgIcon(design: design, iconWidth: iconWidth),
)
: const SizedBox.shrink(
key: ValueKey('svgIconOff'),
key: ValueKey("svgIconOff"),
),
),
],
@@ -1,4 +1,6 @@
import 'dart:math';
import 'package:cake_wallet/core/utilities.dart';
import 'package:cw_core/balance_card_layout.dart';
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/di.dart';
@@ -9,7 +11,6 @@ import 'package:cake_wallet/new-ui/modal_navigator.dart';
import 'package:cake_wallet/new-ui/pages/send_page.dart';
import 'package:cake_wallet/new-ui/widgets/buy_sell/buy_sell_selector_modal.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/utils/feature_flag.dart';
import 'package:cake_wallet/utils/payment_request.dart';
import 'package:cake_wallet/utils/responsive_layout_util.dart';
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
@@ -60,9 +61,9 @@ class _CardsViewState extends State<CardsView> {
_selectedIndex = _fallbackSelectedIndex;
_cardOrderReaction = reaction(
(_) => widget.dashboardViewModel.cardOrder.values.toList(),
(_) => setState(() {
_selectedIndex = _fallbackSelectedIndex;
}),
(_) {
if (mounted) setState(() => _selectedIndex = _fallbackSelectedIndex);
},
);
}
@@ -96,20 +97,18 @@ class _CardsViewState extends State<CardsView> {
final isSelected = _selectedIndex == visualIndex;
final accounts = widget.accountListViewModel?.accounts;
final cardLabel = (!_hideAccountInfo && accounts != null && realIndex < accounts.length)
? accounts[realIndex].label
: S.of(context).balance;
final account = accounts?.firstWhereOrNull((account) => account.id == realIndex);
final cardLabel = !_hideAccountInfo && account != null ? account.label : S.of(context).balance;
void onCardTap() {
Future<void> onCardTap() async {
// printV(visualIndex);
if (compactMode && visualIndex != 0) {
widget.onCompactModeBackgroundCardsTapped();
} else if (!compactMode) {
setState(() {
if (widget.accountListViewModel != null && !widget.lightningMode)
widget.accountListViewModel!.select(widget.accountListViewModel!.accounts[realIndex]);
_selectedIndex = visualIndex;
});
if (account != null && !_hideAccountInfo) {
await widget.accountListViewModel!.select(account);
}
if (mounted) setState(() => _selectedIndex = visualIndex);
}
}
@@ -149,10 +148,8 @@ class _CardsViewState extends State<CardsView> {
onTap: onCardTap,
onLongPress: onCardLongPress,
child: Observer(builder: (_) {
if (realIndex >= (widget.accountListViewModel?.accounts.length ?? 1)) {
return Container();
}
final account = widget.accountListViewModel?.accounts[realIndex];
final account = widget.accountListViewModel?.accounts
.firstWhereOrNull((account) => account.id == realIndex);
// The second balance should always be the lightning balance
// printV(widget.dashboardViewModel.balanceViewModel.formattedBalances.first.availableBalance);
@@ -180,25 +177,17 @@ class _CardsViewState extends State<CardsView> {
// the card designs is empty if widget gets built before it loads.
// should get populated before user sees anything
final CardDesign cardDesign;
if (widget.dashboardViewModel.cardDesigns.isEmpty ||
realIndex >= widget.dashboardViewModel.cardDesigns.length)
cardDesign = CardDesign.genericDefault;
else if (widget.lightningMode)
// the lightning design is always the last one (after all bitcoin accounts)
cardDesign = widget.dashboardViewModel.cardDesigns.last;
else
cardDesign = widget.dashboardViewModel.cardDesigns[realIndex];
final designIndex = widget.dashboardViewModel.cardAccountIndices.indexOf(
widget.lightningMode ? -2 : realIndex,
);
final cardDesign = designIndex < 0 ||
designIndex >= widget.dashboardViewModel.cardDesigns.length
? CardDesign.genericDefault
: widget.dashboardViewModel.cardDesigns[designIndex];
final String accountName;
final String accountBalance;
if (account == null || _hideAccountInfo) {
accountName = "";
accountBalance = "";
} else {
accountName = account.label;
accountBalance = account.balance ?? "0.00";
}
final showAccount = account != null && !_hideAccountInfo;
final accountName = showAccount ? account.label : "";
final accountBalance = showAccount ? account.balance : null;
final assetName = widget.dashboardViewModel.balanceViewModel.showCombinedBalance
? ""
@@ -288,28 +277,13 @@ class _CardsViewState extends State<CardsView> {
}
int _visibleCardsCount() {
if (widget.dashboardViewModel.wallet.type != WalletType.bitcoin) {
return widget.dashboardViewModel.cardDesigns.length;
}
if (widget.lightningMode || !widget.dashboardViewModel.isMultiAccountsEnabled) {
return 1;
}
return widget.accountListViewModel?.accounts.length ?? 1;
}
int? _visualIndexForSelectedAccount(Map<int, int> order) {
final vm = widget.accountListViewModel;
if (vm == null || _hideAccountInfo) return null;
final selectedId = vm.selectedAccount?.id;
final realIndex = vm.accounts.indexWhere((a) => a.id == selectedId);
if (realIndex < 0) return null;
for (final entry in order.entries) {
if (entry.value == realIndex) return entry.key;
if (entry.value == selectedId) return entry.key;
}
return null;
}
@@ -319,27 +293,15 @@ class _CardsViewState extends State<CardsView> {
final parentWidth = MediaQuery.of(context).size.width;
final children = <Widget>[];
int numCards = _visibleCardsCount();
if (numCards == 0) numCards = 1;
if (_selectedIndex >= numCards) {
_selectedIndex = 0;
}
Map<int, int> order = widget.dashboardViewModel.cardOrder.length != numCards
? Map<int, int>.fromEntries(
List.generate(numCards, (i) => MapEntry(i, i)),
)
: widget.dashboardViewModel.cardOrder;
for (int i = min(numCards - 1, maxCards); i >= 0; i--) {
int visualIndex = (_selectedIndex - i + numCards) % numCards;
if (order[visualIndex] == null) {
order = Map<int, int>.fromEntries(
List.generate(numCards, (i) => MapEntry(i, i)),
);
}
final stack = _hideAccountInfo
? const [0]
: BalanceCardLayout.stackFromOrder(widget.dashboardViewModel.cardOrder);
if (stack.isEmpty) return const SizedBox.shrink();
final numCards = stack.length;
if (_selectedIndex < 0 || _selectedIndex >= numCards) {
_selectedIndex = numCards - 1;
}
final order = {for (int i = 0; i < stack.length; i++) i: stack[i]};
final followIndex = _visualIndexForSelectedAccount(order);
if (followIndex != null) _selectedIndex = followIndex;
@@ -461,4 +423,4 @@ class _CardsViewState extends State<CardsView> {
);
}
}
}
}
@@ -16,6 +16,7 @@ class ModalTopBar extends StatelessWidget {
this.bottomText,
this.leadingWidget,
this.trailingWidget,
this.titleLeadingWidget,
this.leadingSemanticLabel,
this.trailingSemanticLabel}) {
if (leadingIcon != null && leadingWidget != null) {
@@ -36,6 +37,7 @@ class ModalTopBar extends StatelessWidget {
final Widget? trailingIcon;
final Widget? leadingWidget;
final Widget? trailingWidget;
final Widget? titleLeadingWidget;
/// Accessible name for the leading chrome button. Required (and must be
/// non-empty) whenever a [leadingIcon] is supplied, because the icon alone
@@ -53,6 +55,51 @@ class ModalTopBar extends StatelessWidget {
@override
Widget build(BuildContext context) {
final hasBottomText = bottomText != null && bottomText!.isNotEmpty;
final titleContent = Column(
mainAxisSize: MainAxisSize.min,
children: [
AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
child: Semantics(
key: ValueKey(title),
header: title.isNotEmpty,
// Android reads the heading from headingLevel since the
// Flutter 3.41 engine; header: alone only covers iOS.
headingLevel: title.isNotEmpty ? 1 : null,
child: titleLeadingWidget == null
? Text(
title,
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
fontSize: hasBottomText ? 16 : 18,
),
)
: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
spacing: 12,
children: [
ExcludeSemantics(child: titleLeadingWidget),
Flexible(
child: Text(
title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
fontSize: hasBottomText ? 16 : 18,
),
),
),
],
),
),
),
if (hasBottomText)
Text(
bottomText!,
style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant),
),
],
);
return Padding(
padding: padding ?? EdgeInsets.all(18),
child: Stack(
@@ -60,36 +107,14 @@ class ModalTopBar extends StatelessWidget {
children: [
Positioned(
top: hasBottomText ? -4 : 6,
left: titleLeadingWidget == null ? null : buttonSize + 8,
right: titleLeadingWidget == null ? null : buttonSize + 8,
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
spacing: 4,
children: [
Column(
children: [
AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
child: Semantics(
key: ValueKey(title),
header: title.isNotEmpty,
// Android reads the heading from headingLevel since the
// Flutter 3.41 engine; header: alone only covers iOS.
headingLevel: title.isNotEmpty ? 1 : null,
child: Text(
title,
style: TextStyle(
fontSize: hasBottomText ? 16 : 18, fontWeight: FontWeight.w600),
),
),
),
if (hasBottomText)
Text(
bottomText!,
style: TextStyle(
fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant),
),
],
),
if (titleLeadingWidget == null) titleContent else Expanded(child: titleContent),
if (subtitle != null && subtitle!.isNotEmpty)
Text(subtitle!,
style: Theme.of(context)
+2 -5
View File
@@ -129,7 +129,6 @@ import "package:cake_wallet/view_model/dashboard/nft_send_view_model.dart";
import 'package:cake_wallet/view_model/dashboard/nft_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/sign_view_model.dart';
import 'package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart';
import 'package:cake_wallet/view_model/wallet_account_list/account_list_item.dart';
import 'package:cake_wallet/view_model/hardware_wallet/trezor_connect_view_model.dart';
import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model.dart';
import 'package:cake_wallet/view_model/wallet_groups_display_view_model.dart';
@@ -138,7 +137,6 @@ import 'package:cake_wallet/view_model/wallet_hardware_restore_view_model.dart';
import 'package:cake_wallet/view_model/wallet_new_vm.dart';
import 'package:cake_wallet/wallet_type_utils.dart';
import 'package:cake_wallet/wallet_types.g.dart';
import 'package:cw_core/nano_account.dart';
import 'package:cw_core/node.dart';
import 'package:cw_core/transaction_info.dart';
import 'package:cw_core/unspent_coin_type.dart';
@@ -848,9 +846,8 @@ Route<dynamic> createRoute(RouteSettings settings) {
case Routes.walletAccountsPage:
final dashboardViewModel = settings.arguments as DashboardViewModel;
return handleRouteWithPlatformAwareness(
(_) => getIt.get<WalletAccountsPage>(param1: dashboardViewModel),
);
final page = getIt.get<WalletAccountsPage>(param1: dashboardViewModel);
return handleRouteWithPlatformAwareness((_) => page);
case Routes.managePowNodes:
return MaterialPageRoute<void>(builder: (_) => getIt.get<ManageNodesPage>(param1: true));
@@ -1,11 +1,8 @@
import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item_regular_row.dart';
import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item_toggle.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/pages/coin_control_page.dart';
import 'package:cake_wallet/new-ui/widgets/modal_header.dart';
import 'package:cake_wallet/new-ui/widgets/modal_page_wrapper.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/widgets/new_list_row/new_list_section.dart';
import 'package:cake_wallet/view_model/settings/privacy_settings_view_model.dart';
@@ -75,29 +72,6 @@ class PrivacyPage extends BasePage {
_privacySettingsViewModel.setUseLightning(val);
}),
],
"": [
if (_privacySettingsViewModel.hasSilentPaymentsScanning)
ListItemRegularRow(
iconPath: "assets/new-ui/settings_row_icons/silent-payments.svg",
keyValue: "silent_payments",
label: S.of(context).silent_payments,
onTap: () =>
Navigator.of(context).pushNamed(Routes.silentPaymentsSettings)),
if (_privacySettingsViewModel.hasMWEB)
ListItemRegularRow(
iconPath: "assets/new-ui/settings_row_icons/mweb.svg",
keyValue: "mweb",
label: "MWEB",
onTap: () => Navigator.of(context).pushNamed(Routes.mwebSettings)),
if (_privacySettingsViewModel.hasCoinControl)
ListItemRegularRow(
iconPath: "assets/new-ui/settings_row_icons/coin-control.svg",
keyValue: "coin_control",
label: "Coin Control",
onTap: () => Navigator.of(context).pushNamed(Routes.unspentCoinsList,
arguments:
CoinControlPageArgs(canEdit: false, coinTypeToSpendFrom: null))),
],
}),
],
);
@@ -1,20 +1,16 @@
import "package:cake_wallet/core/execution_state.dart";
import "package:cake_wallet/generated/i18n.dart";
import "package:cake_wallet/new-ui/widgets/modal_grab_handle.dart";
import "package:cake_wallet/new-ui/widgets/new_primary_button.dart";
import "package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
import "package:cake_wallet/src/widgets/alert_with_one_action.dart";
import "package:cake_wallet/src/widgets/cake_image_widget.dart";
import "package:cake_wallet/view_model/wallet_account_list/account_edit_or_create_view_model.dart";
import "package:cw_core/generate_name.dart";
import "package:flutter/material.dart";
import "package:flutter/services.dart";
import "package:flutter_mobx/flutter_mobx.dart";
class AccountCreationModal extends StatefulWidget {
const AccountCreationModal({
required this.viewModel,
super.key,
});
const AccountCreationModal({required this.viewModel, super.key});
final WalletAccountEditOrCreateViewModel viewModel;
@@ -26,14 +22,59 @@ class _AccountCreationModalState extends State<AccountCreationModal> {
static const int maxAccountNameLength = 25;
final TextEditingController _controller = TextEditingController();
bool _loading = false;
@override
void initState() {
super.initState();
_controller.text = widget.viewModel.label;
_controller.addListener(() {
widget.viewModel.label = _controller.text;
});
bool get _canContinue =>
_controller.text.trim().isNotEmpty && _controller.text.length <= maxAccountNameLength;
Future<void> _generateAccountName() async {
final generatedName = await generateName();
if (!mounted) {
return;
}
setState(() => _controller.text = generatedName.length > maxAccountNameLength
? generatedName.substring(0, maxAccountNameLength)
: generatedName);
}
Future<void> _save() async {
if (_loading || !_canContinue) {
return;
}
setState(() => _loading = true);
widget.viewModel.label = _controller.text;
late final String errorMessage;
try {
await widget.viewModel.save();
if (!mounted) {
return;
}
final state = widget.viewModel.state;
if (state is ExecutedSuccessfullyState) {
Navigator.of(context).pop(true);
return;
}
errorMessage = state is FailureState ? state.error : S.of(context).error_while_processing;
} catch (error) {
errorMessage = error.toString();
}
if (!mounted) {
return;
}
setState(() => _loading = false);
await showDialog<void>(
context: context,
builder: (dialogContext) => AlertWithOneAction(
alertTitle: S.of(dialogContext).error,
alertContent: errorMessage,
buttonText: S.of(dialogContext).ok,
buttonAction: Navigator.of(dialogContext).pop,
),
);
}
@override
@@ -42,105 +83,117 @@ class _AccountCreationModalState extends State<AccountCreationModal> {
super.dispose();
}
Future<void> _onSubmit() async {
if (widget.viewModel.state is IsExecutingState) return;
if (_controller.text.trim().isEmpty || _controller.text.length > maxAccountNameLength) return;
await widget.viewModel.save();
if (!mounted) return;
if (widget.viewModel.state is ExecutedSuccessfullyState) {
Navigator.of(context).pop(true);
}
}
@override
Widget build(BuildContext context) => Container(
Widget build(BuildContext context) => PopScope(
canPop: !_loading,
child: _buildContent(context),
);
Widget _buildContent(BuildContext context) => Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: const BorderRadius.vertical(top: Radius.circular(30))),
color: Theme.of(context).colorScheme.surface,
borderRadius: const BorderRadius.vertical(top: Radius.circular(30)),
),
child: SafeArea(
top: false,
child: Padding(
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
padding: EdgeInsets.only(bottom: MediaQuery.viewInsetsOf(context).bottom),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const ModalGrabHandle(),
ModalTopBar(
title: widget.viewModel.isEdit
? S.of(context).edit_account
: S.of(context).create_account,
title: S.of(context).create_account,
trailingIcon: const Icon(Icons.close),
trailingSemanticLabel: S.of(context).close,
onTrailingPressed: Navigator.of(context).maybePop,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 18),
child: Column(
spacing: 50,
children: [
const SizedBox(),
Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(16),
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(18, 32, 18, 24),
child: Column(
spacing: 24,
children: [
const CakeImageWidget(
imageUrl: "assets/new-ui/account_education/create_account.svg",
width: 125,
height: 125,
),
child: Row(
children: [
Expanded(
child: TextField(
controller: _controller,
maxLength: maxAccountNameLength,
inputFormatters: [
LengthLimitingTextInputFormatter(maxAccountNameLength),
],
decoration: InputDecoration(
hintText: S.of(context).account_name,
counterText: "",
),
),
),
Padding(
padding: const EdgeInsets.all(12),
child: GestureDetector(
onTap: () async {
final generated = await generateName();
_controller.text = generated.length > maxAccountNameLength
? generated.substring(0, maxAccountNameLength)
: generated;
},
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(5)),
child: CakeImageWidget(
imageUrl: "assets/new-ui/randomize.svg",
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.primary, BlendMode.srcIn),
Text(
S.of(context).account_creation_description,
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant,
fontSize: 14,
),
),
Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(16),
),
child: Row(
children: [
Expanded(
child: TextField(
controller: _controller,
maxLength: maxAccountNameLength,
inputFormatters: [
LengthLimitingTextInputFormatter(maxAccountNameLength)
],
textInputAction: TextInputAction.done,
onChanged: (_) => setState(() {}),
decoration: InputDecoration(
hintText: S.of(context).account_name,
counterText: "",
),
),
),
)
],
),
),
ValueListenableBuilder<TextEditingValue>(
valueListenable: _controller,
builder: (context, value, _) => Observer(
builder: (_) => NewPrimaryButton(
onPressed: _onSubmit,
text: S.of(context).continue_text,
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
disabled: value.text.trim().isEmpty ||
value.text.length > maxAccountNameLength,
isLoading: widget.viewModel.state is IsExecutingState,
Padding(
padding: const EdgeInsets.all(12),
child: Semantics(
button: true,
label: S.of(context).generate_name,
onTap: _generateAccountName,
child: ExcludeSemantics(
child: GestureDetector(
onTap: _generateAccountName,
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(5),
),
child: CakeImageWidget(
imageUrl: "assets/new-ui/randomize.svg",
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.primary,
BlendMode.srcIn,
),
),
),
),
),
),
),
],
),
),
),
const SizedBox(),
],
],
),
),
)
),
Padding(
padding: const EdgeInsets.fromLTRB(18, 0, 18, 12),
child: SizedBox(
width: double.infinity,
child: NewPrimaryButton(
onPressed: _save,
text: S.of(context).continue_text,
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
isLoading: _loading,
disabled: !_canContinue,
),
),
),
],
),
),
+17
View File
@@ -153,6 +153,7 @@ abstract class SettingsStoreBase with Store {
required this.hasEnabledMwebBefore,
required this.mwebNodeUri,
required this.mwebAdDismissed,
required this.accountsHomePromoDismissed,
required this.balanceHideCounter,
required this.zcashMigrationModalViewed,
required bool initialEnableAutomaticNodeSwitching,
@@ -313,6 +314,9 @@ abstract class SettingsStoreBase with Store {
reaction((_) => mwebAdDismissed,
(val) => sharedPreferences.setBool(PreferencesKey.mwebAdDismissed, val));
reaction((_) => accountsHomePromoDismissed,
(val) => sharedPreferences.setBool(PreferencesKey.accountsHomePromoDismissed, val));
priority.observe((change) {
final String? key;
switch (change.key) {
@@ -1105,6 +1109,9 @@ abstract class SettingsStoreBase with Store {
@observable
bool mwebAdDismissed;
@observable
bool accountsHomePromoDismissed;
@observable
bool zcashMigrationModalViewed;
@@ -1191,6 +1198,12 @@ abstract class SettingsStoreBase with Store {
Future<void> setShouldShowReceiveWarning(bool value) async =>
_sharedPreferences.setBool(PreferencesKey.shouldShowReceiveWarning, value);
bool isEducationDismissed(String educationId) =>
_sharedPreferences.getBool(PreferencesKey.educationDismissed(educationId)) ?? false;
Future<void> dismissEducation(String educationId) =>
_sharedPreferences.setBool(PreferencesKey.educationDismissed(educationId), true);
static Future<SettingsStore> load(
{required bool isBitcoinBuyEnabled,
FiatCurrency initialFiatCurrency = FiatCurrency.usd,
@@ -1692,6 +1705,9 @@ abstract class SettingsStoreBase with Store {
final mwebAdDismissed =
await sharedPreferences.getBool(PreferencesKey.mwebAdDismissed) ?? false;
final accountsHomePromoDismissed =
sharedPreferences.getBool(PreferencesKey.accountsHomePromoDismissed) ?? false;
final balanceHideCounter =
await sharedPreferences.getInt(PreferencesKey.balanceHideCounter) ?? 0;
@@ -1815,6 +1831,7 @@ abstract class SettingsStoreBase with Store {
shouldShowRepWarning: shouldShowRepWarning,
initialBuiltinTor: builtinTor,
mwebAdDismissed: mwebAdDismissed,
accountsHomePromoDismissed: accountsHomePromoDismissed,
balanceHideCounter: balanceHideCounter,
zcashMigrationModalViewed: zcashMigrationModalViewed
);
@@ -11,6 +11,7 @@ import 'package:cake_wallet/evm/evm.dart';
import 'package:cake_wallet/solana/solana.dart';
import 'package:cake_wallet/tron/tron.dart';
import 'package:cake_wallet/zano/zano.dart';
import 'package:cake_wallet/view_model/wallet_account_list/account_list_item.dart';
import 'package:cw_core/amount/money.dart';
import 'package:cw_core/crypto_amount_format.dart';
import 'package:cw_core/transaction_history.dart';
@@ -546,6 +547,21 @@ abstract class BalanceViewModelBase with Store {
}
}
String? accountFiatBalance(AccountListItem account) {
if (isFiatDisabled) {
return null;
}
final fiat = settingsStore.fiatCurrency.title;
final value = savedDisplayMode == BalanceDisplayMode.hiddenBalance
? "●●●●●"
: calculateFiatAmount(
price: price,
cryptoAmount: account.balance.toString(),
).withLocalSeperator(settingsStore.languageCode);
return "$value $fiat";
}
String _getFiatBalance({required double price, Money? cryptoAmount}) {
if (cryptoAmount == null) {
return '0.00';
@@ -9,6 +9,7 @@ import 'package:cake_wallet/view_model/wallet_account_list/wallet_account_list_v
import 'package:cake_wallet/view_model/wallet_account_list/account_list_item.dart';
import 'package:cw_core/account.dart';
import 'package:cake_wallet/view_model/dashboard/date_section_item.dart';
import "package:cw_core/balance_card_layout.dart";
import "package:cw_core/balance_card_style_settings.dart";
import 'package:cake_wallet/core/trade_monitor.dart';
import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart';
@@ -110,7 +111,8 @@ abstract class DashboardViewModelBase with Store {
showDecredInfoCard = wallet.type == WalletType.decred &&
(sharedPreferences.getBool(PreferencesKey.showDecredInfoCard) ?? true);
showSeedBackupReminder = wallet.walletInfo.showSeedBackupReminder;
isMultiAccountsEnabled = wallet.walletInfo.isMultiAccountsEnabled == true;
isMultiAccountsEnabled =
wallet.hasNativeAccounts || wallet.walletInfo.isMultiAccountsEnabled == true;
name = wallet.name;
type = wallet.type;
isShowFirstYatIntroduction = false;
@@ -426,72 +428,86 @@ abstract class DashboardViewModelBase with Store {
}
}
List<int> get cardAccountIndices {
if (wallet.type == WalletType.monero) {
return monero!.getAccountList(wallet).accounts.map((account) => account.id).toList();
}
if (wallet.type == WalletType.wownero) {
return wow.wownero!.getAccountList(wallet).accounts.map((account) => account.id).toList();
}
if (wallet.type == WalletType.bitcoin) {
final accounts = accountListViewModel?.accounts;
return [
if (accounts == null || accounts.isEmpty) 0 else ...accounts.map((account) => account.id),
-2,
];
}
return const [-1];
}
@action
Future<void> loadCardDesigns() async {
final walletCardStyleSettings =
await BalanceCardStyleSettings.getAll(wallet.walletInfo.internalId);
final btcAccounts =
wallet.type == WalletType.bitcoin ? await wallet.walletInfo.getAccounts() : null;
final lightningCardIndex = btcAccounts?.length;
if (wallet.type == WalletType.bitcoin) {
await accountListViewModel?.reload();
}
final accountIndices = cardAccountIndices;
final settings = await BalanceCardStyleSettings.getAll(wallet.walletInfo.internalId);
if (wallet.type != WalletType.bitcoin && accountIndices.contains(0) &&
!settings.any((setting) => setting.accountIndex == 0)) {
final primarySettings = settings.where((setting) => setting.accountIndex == -1)
.firstOrNull?.copyWith(accountIndex: 0);
if (primarySettings != null) {
await primarySettings.insert();
settings.add(primarySettings);
}
}
final layout = BalanceCardLayout.resolve(
accountIndices: accountIndices.where((index) => index != -2).toList(),
settings: settings,
);
int numAccounts = 1;
cardDesigns
..clear()
..addAll(accountIndices.map((accountIndex) => CardDesign.fromStyleSettings(
settings.where((setting) => setting.accountIndex == accountIndex).firstOrNull,
accountIndex == -2 ? CryptoCurrency.btcln : wallet.currency,
)));
cardOrder = {
for (int position = 0; position < layout.visible.length; position++)
position: layout.visible[position],
}.asObservable();
if (layout.needsRepair) {
await BalanceCardStyleSettings.setVisibleOrder(wallet.walletInfo.internalId, layout.orders);
}
if (isMultiAccountsEnabled && !lightningMode && layout.visible.isNotEmpty &&
!layout.visible.contains(accountListViewModel?.selectedAccount?.id)) {
final visibleAccount = accountListViewModel?.accounts
.where((account) => account.id == layout.visible.last).firstOrNull;
if (visibleAccount != null) {
await accountListViewModel!.select(visibleAccount);
}
}
}
int get currentCardAccountIndex {
if (wallet.type == WalletType.monero) {
numAccounts = monero!.getAccountList(wallet).accounts.length;
} else if (wallet.type == WalletType.wownero) {
numAccounts = wow.wownero!.getAccountList(wallet).accounts.length;
} else if (wallet.type == WalletType.bitcoin) {
numAccounts = btcAccounts!.length + 1; // adding 1 for the lightning account
return monero!.getCurrentAccount(wallet).id;
}
final usesAccountIndices = wallet.type == WalletType.bitcoin || balanceViewModel.hasAccounts;
cardDesigns.clear();
final newOrder = <int, int>{};
final orderableCount = (wallet.type == WalletType.bitcoin) ? numAccounts - 1 : numAccounts;
for (int i = 0; i < numAccounts; i++) {
late final int index;
final isLightning = wallet.type == WalletType.bitcoin && i == lightningCardIndex;
if (isLightning) {
index = -2;
} else if (usesAccountIndices) {
index = i;
} else {
index = -1;
}
final setting = walletCardStyleSettings.where((e) => e.accountIndex == index).firstOrNull ??
(index == 0 && wallet.type != WalletType.bitcoin
? walletCardStyleSettings.where((e) => e.accountIndex == -1).firstOrNull
: null);
final curr = isLightning ? CryptoCurrency.btcln : wallet.currency;
cardDesigns.add(CardDesign.fromStyleSettings(setting, curr));
if (isLightning) {
continue;
}
if (setting?.cardOrder != null &&
setting!.cardOrder >= 0 &&
setting.cardOrder < orderableCount) {
newOrder[setting.cardOrder] = i;
}
if (wallet.type == WalletType.wownero) {
return wow.wownero!.getCurrentAccount(wallet).id;
}
// making sure ALL accounts have numbers, even the ones that existed before this feature was a thing
for (int i = 0; i < orderableCount; i++) {
if (!newOrder.containsValue(i)) {
int free = 0;
while (newOrder.containsKey(free)) {
free++;
}
newOrder[free] = i;
}
if (wallet.type == WalletType.bitcoin) {
return lightningMode ? -2 : wallet.walletInfo.currentAccountIndex;
}
cardOrder = newOrder.asObservable();
return -1;
}
CardDesign get currentCardDesign {
final designIndex = cardAccountIndices.indexOf(currentCardAccountIndex);
return cardDesigns.isEmpty || designIndex >= cardDesigns.length
? CardDesign.fromStyleSettings(null, wallet.currency)
: cardDesigns[designIndex == -1 ? 0 : designIndex];
}
void _transactionDisposerCallback(int _) async {
@@ -925,7 +941,9 @@ abstract class DashboardViewModelBase with Store {
List<AccountListItem> get visibleAccounts {
final all = accountListViewModel?.accounts;
if (all == null || all.isEmpty) return const <AccountListItem>[];
if (isMultiAccountsEnabled) return all.toList(growable: false);
if (isMultiAccountsEnabled) {
return all.where((account) => cardOrder.containsValue(account.id)).toList(growable: false);
}
final primary = all.where((a) => a.id == 0).firstOrNull ?? all.first;
return <AccountListItem>[primary];
}
@@ -1353,7 +1371,13 @@ abstract class DashboardViewModelBase with Store {
// Lightning belongs to the primary account only
if (lightningMode) {
unawaited(_selectPrimaryAccount());
unawaited(_selectPrimaryAccount().then((_) async {
if (!lightningMode && isMultiAccountsEnabled) {
await loadCardDesigns();
}
}));
} else {
unawaited(loadCardDesigns());
}
}
@@ -1382,7 +1406,8 @@ abstract class DashboardViewModelBase with Store {
this.wallet = wallet;
type = wallet.type;
name = wallet.name;
isMultiAccountsEnabled = wallet.walletInfo.isMultiAccountsEnabled == true;
isMultiAccountsEnabled =
wallet.hasNativeAccounts || wallet.walletInfo.isMultiAccountsEnabled == true;
_onAccountChangeReaction?.reaction.dispose();
_onAccountChangeReaction = null;
+1 -10
View File
@@ -444,16 +444,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
@computed
bool get hasCoinControl =>
[
WalletType.bitcoin,
WalletType.litecoin,
WalletType.monero,
WalletType.wownero,
WalletType.decred,
WalletType.bitcoinCash,
WalletType.dogecoin
].contains(wallet.type) &&
coinTypeToSpendFrom != UnspentCoinType.lightning;
wallet.hasCoinControl && coinTypeToSpendFrom != UnspentCoinType.lightning;
@computed
bool get hasFees => feesViewModel.hasFees && coinTypeToSpendFrom != UnspentCoinType.lightning;
@@ -54,15 +54,7 @@ abstract class PrivacySettingsViewModelBase with Store {
].contains(_wallet.type);
@computed
bool get hasCoinControl => [
WalletType.bitcoin,
WalletType.litecoin,
WalletType.monero,
WalletType.wownero,
WalletType.decred,
WalletType.bitcoinCash,
WalletType.dogecoin
].contains(_wallet.type);
bool get hasCoinControl => _wallet.hasCoinControl;
bool get isMoneroWallet => _wallet.type == WalletType.monero;
@@ -1,8 +1,17 @@
import "package:cw_core/amount/money.dart";
class AccountListItem {
AccountListItem({required this.label, required this.id, this.balance, this.isSelected = false});
AccountListItem({
required this.label,
required this.id,
required this.balance,
this.isSelected = false,
});
final String label;
final int id;
final bool isSelected;
final String? balance;
final Money balance;
bool get isFunded => balance.sign > 0;
}
@@ -1,24 +1,22 @@
import 'dart:async';
import "dart:async";
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/core/amount_parsing_proxy.dart';
import 'package:cake_wallet/entities/balance_display_mode.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/view_model/wallet_account_list/account_list_item.dart';
import 'package:cake_wallet/view_model/wallet_account_list/wallet_account_list_view_model.dart';
import 'package:collection/collection.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:mobx/mobx.dart';
import "package:cake_wallet/bitcoin/bitcoin.dart";
import "package:cake_wallet/view_model/wallet_account_list/account_list_item.dart";
import "package:cake_wallet/view_model/wallet_account_list/wallet_account_list_view_model.dart";
import "package:collection/collection.dart";
import "package:cw_core/amount/money.dart";
import "package:cw_core/crypto_currency.dart";
import "package:cw_core/wallet_base.dart";
import "package:cw_core/wallet_info.dart";
import "package:mobx/mobx.dart";
part 'bitcoin_account_list_view_model.g.dart';
part "bitcoin_account_list_view_model.g.dart";
class BitcoinAccountListViewModel = BitcoinAccountListViewModelBase
with _$BitcoinAccountListViewModel;
abstract class BitcoinAccountListViewModelBase with Store implements WalletAccountListViewModel {
BitcoinAccountListViewModelBase(this._wallet, this.settingsStore) {
BitcoinAccountListViewModelBase(this._wallet) {
unawaited(_loadAccounts());
reaction(
@@ -26,14 +24,8 @@ abstract class BitcoinAccountListViewModelBase with Store implements WalletAccou
(snapshot) => unawaited(_refreshBalances(snapshot)),
equals: const MapEquality<int, Object>().equals,
);
reaction(
(_) => settingsStore.balanceDisplayMode,
(_) => unawaited(_recomputeBalanceStrings()),
);
}
final SettingsStore settingsStore;
final WalletBase _wallet;
List<WalletInfoAccount>? _cachedStoredAccounts;
@@ -71,14 +63,6 @@ abstract class BitcoinAccountListViewModelBase with Store implements WalletAccou
return _runLoad(() async => cached);
}
@action
Future<void> _recomputeBalanceStrings() {
final cached = _cachedStoredAccounts;
if (cached == null) return _loadAccounts();
return _runLoad(() async => cached);
}
Future<void> _runLoad(Future<List<WalletInfoAccount>> Function() fetch) {
final inProgress = _loadAccountsFuture;
if (inProgress != null) {
@@ -99,7 +83,7 @@ abstract class BitcoinAccountListViewModelBase with Store implements WalletAccou
.map((account) => AccountListItem(
id: account.accountIndex,
label: account.label,
balance: _balanceForAccount(account.accountIndex),
balance: _fullBalance(account.accountIndex),
isSelected: account.accountIndex == currentAccountIndex,
))
.toList();
@@ -115,16 +99,12 @@ abstract class BitcoinAccountListViewModelBase with Store implements WalletAccou
});
}
Money _fullBalance(int accountIndex) {
final balance = bitcoin!.balanceForAccount(_wallet, accountIndex);
return balance.confirmed + balance.unconfirmed;
}
@override
@action
Future<void> reload() => _loadAccounts();
String _balanceForAccount(int accountIndex) {
if (settingsStore.balanceDisplayMode == BalanceDisplayMode.hiddenBalance) {
return '●●●●●●';
}
final balance = bitcoin!.balanceForAccount(_wallet, accountIndex);
return AmountParsingProxy(settingsStore.displayAmountsInSatoshi).asDisplayString(balance.available);
}
}
@@ -2,6 +2,7 @@ import 'dart:math';
import 'package:cake_wallet/view_model/wallet_account_list/account_edit_or_create_view_model.dart';
import 'package:cake_wallet/wownero/wownero.dart';
import 'package:cw_core/balance_card_layout.dart';
import 'package:cw_core/balance_card_style_settings.dart';
import 'package:cw_core/card_design.dart';
import 'package:cw_core/wallet_base.dart';
@@ -36,7 +37,7 @@ abstract class MoneroAccountEditOrCreateViewModelBase
@observable
String label;
final MoneroAccountList _moneroAccountList;
final MoneroAccountList? _moneroAccountList;
final WowneroAccountList? _wowneroAccountList;
final AccountListItem? _accountListItem;
final WalletBase _wallet;
@@ -51,13 +52,22 @@ abstract class MoneroAccountEditOrCreateViewModelBase
return ret.isNotEmpty ? ret : CardDesign.allGradients;
}
Future<void> _saveRandomCardDesign() async {
Future<void> _saveRandomCardDesign(int accountIndex) async {
final gradients = await _getUsableCardGradients();
final accountIndices = _wallet.type == WalletType.monero
? _moneroAccountList!.accounts.map((account) => account.id).toList()
: _wowneroAccountList!.accounts.map((account) => account.id).toList();
final layout = BalanceCardLayout.resolve(
accountIndices: accountIndices,
settings: await BalanceCardStyleSettings.getAll(_wallet.walletInfo.internalId),
);
await BalanceCardStyleSettings.fromCardDesign(
walletInfoId: _wallet.walletInfo.internalId,
accountIndex: _moneroAccountList.accounts.length,
cardOrder: _moneroAccountList.accounts.length,
accountIndex: accountIndex,
cardOrder: layout.visible.length,
design: CardDesign.specialDesignsForCurrencies[_wallet.currency]!
.withGradient(gradients[Random().nextInt(gradients.length)]))
.insert();
@@ -67,7 +77,11 @@ abstract class MoneroAccountEditOrCreateViewModelBase
try {
state = IsExecutingState();
if (!isEdit) await _saveRandomCardDesign();
if (!isEdit) {
await _saveRandomCardDesign(_wallet.type == WalletType.monero
? _moneroAccountList!.accounts.length
: _wowneroAccountList!.accounts.length);
}
if (_wallet.type == WalletType.monero) {
await saveMonero();
@@ -84,13 +98,13 @@ abstract class MoneroAccountEditOrCreateViewModelBase
Future<void> saveMonero() async {
if (_accountListItem != null) {
await _moneroAccountList.setLabelAccount(
await _moneroAccountList!.setLabelAccount(
_wallet,
accountIndex: _accountListItem.id,
label: label,
);
} else {
await _moneroAccountList.addAccount(
await _moneroAccountList!.addAccount(
_wallet,
label: label,
);
@@ -1,22 +1,18 @@
import 'package:cake_wallet/entities/balance_display_mode.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/view_model/wallet_account_list/wallet_account_list_view_model.dart';
import 'package:cake_wallet/wownero/wownero.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:mobx/mobx.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cake_wallet/view_model/wallet_account_list/account_list_item.dart';
import 'package:cake_wallet/monero/monero.dart';
import "package:cake_wallet/monero/monero.dart";
import "package:cake_wallet/view_model/wallet_account_list/account_list_item.dart";
import "package:cake_wallet/view_model/wallet_account_list/wallet_account_list_view_model.dart";
import "package:cake_wallet/wownero/wownero.dart";
import "package:cw_core/crypto_currency.dart";
import "package:cw_core/wallet_base.dart";
import "package:cw_core/wallet_type.dart";
import "package:mobx/mobx.dart";
part 'monero_account_list_view_model.g.dart';
part "monero_account_list_view_model.g.dart";
class MoneroAccountListViewModel = MoneroAccountListViewModelBase with _$MoneroAccountListViewModel;
abstract class MoneroAccountListViewModelBase with Store implements WalletAccountListViewModel {
MoneroAccountListViewModelBase(this._wallet, this.settingsStore);
final SettingsStore settingsStore;
MoneroAccountListViewModelBase(this._wallet);
@override
AccountListItem? get selectedAccount => selected;
@@ -25,7 +21,6 @@ abstract class MoneroAccountListViewModelBase with Store implements WalletAccoun
@computed
List<AccountListItem> get accounts {
final hideBalance = settingsStore.balanceDisplayMode == BalanceDisplayMode.hiddenBalance;
if (_wallet.type == WalletType.monero) {
return monero!
.getAccountList(_wallet)
@@ -33,7 +28,7 @@ abstract class MoneroAccountListViewModelBase with Store implements WalletAccoun
.map((acc) => AccountListItem(
label: acc.label,
id: acc.id,
balance: hideBalance ? '●●●●●●' : acc.balance,
balance: monero!.getAccountFullBalance(acc.id),
isSelected: acc.id == monero!.getCurrentAccount(_wallet).id))
.toList();
}
@@ -45,19 +40,16 @@ abstract class MoneroAccountListViewModelBase with Store implements WalletAccoun
.map((acc) => AccountListItem(
label: acc.label,
id: acc.id,
balance: hideBalance ? '●●●●●●' : acc.balance,
balance: wownero!.getAccountFullBalance(acc.id),
isSelected: acc.id == wownero!.getCurrentAccount(_wallet).id))
.toList();
}
throw Exception('Unexpected wallet type: ${_wallet.type} for monero');
throw Exception("Unexpected wallet type: ${_wallet.type} for monero");
}
@computed
AccountListItem get selected {
final currentId = monero!.getCurrentAccount(_wallet).id;
return accounts.firstWhere((item) => item.id == currentId);
}
AccountListItem get selected => accounts.firstWhere((item) => item.isSelected);
final WalletBase _wallet;
@@ -71,7 +63,7 @@ abstract class MoneroAccountListViewModelBase with Store implements WalletAccoun
_wallet,
item.id,
item.label,
item.balance,
item.balance.toString(),
);
}
@@ -80,7 +72,7 @@ abstract class MoneroAccountListViewModelBase with Store implements WalletAccoun
_wallet,
item.id,
item.label,
item.balance,
item.balance.toString(),
);
}
}
@@ -1,5 +1,5 @@
import 'package:cake_wallet/view_model/wallet_account_list/account_list_item.dart';
import 'package:cw_core/crypto_currency.dart';
import "package:cake_wallet/view_model/wallet_account_list/account_list_item.dart";
import "package:cw_core/crypto_currency.dart";
abstract class WalletAccountListViewModel {
List<AccountListItem> get accounts;
+6
View File
@@ -136,6 +136,12 @@ class CWWownero extends Wownero {
@override
WowneroAccountList getAccountList(Object wallet) => CWWowneroAccountList(wallet);
@override
Money getAccountFullBalance(int accountIndex) => Money.fromInt(
wownero_wallet_api.getFullBalance(accountIndex: accountIndex),
CryptoCurrency.wow,
);
@override
WowneroSubaddressList getSubaddressList(Object wallet) => CWWowneroSubaddressList(wallet);