Cw 1551 quick bitcoin wallet sync (#3446)

* add Wallet Accounts page and integrate UI

* account model refactor

* add walletInfoAccount table and API

* add WalletAccountList API

* add multi-account support for Electrum wallets

* add Bitcoin lightning card and card style handling

* refine account filtering and card selection

* update current account balance on wallet changes

* update configure.dar

* Use named parameters for card design

* Add Bitcoin account handling and refactor lookup

* refactor dashboard

* refactor cards UI and account balance handling

* Improve Bitcoin account selection and balances

* restrict unspent coin usage to current account

* filter transactions by current Bitcoin account

* refactor electrum wallet

* refactor address generation

* add accountIndex to address fetch logic

* Update configure.dart

* reload transactions on Bitcoin account change

* restore multiple BTC accounts and improve fetch

* show account balance with in btc

* track account index in Bitcoin transactions

* discover btc BIP39 accounts during restore

* fix account switching and card ordering bugs

* fix card customizer

* limit account name length in modal

* update BTC account list balance display

* auto-reformat

* formating fix

* skip HD map validation

* fix 0 balance issue

* minor fix

* quick bitcoin wallet sync

* restore shuffle change output

* restore address lookup fix

* Update electrum.dart

* remove account customizer

* Merge branch 'CW-1142-Add-accounts-feature-to-BTC' into CW-1551-Quick-Bitcoin-Wallet-Sync

* probe only segwit in account discovery

* add account discovery limit and tracking

* Improve Electrum sync account handling [skip ci]

* remove legacy monero account and address list UI

* rework BTC account balance refres

* avoid unnecessary BTC account reloads

* refactor account header in addresses page

* fix blank popup when editing an account

* fix: dedupe UTXO balance

* minor fix

* add Bitcoin current account API

* Apply batched suggestions from code review [skip ci]

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* address PR review comments [skip ci]

* remove account customizer and fix accounts init

* restrict extra BTC accounts to SegWit

* add BTC multi-account toggle

* Fix BTC sync gating and account balance update

* minor fix

* Update electrum_wallet.dart

* store selected account on WalletInfo

* Refine wallet account reset visibility

* refactor BTC address prep and move accounts page

* refactor account stack and customizer routing

* migrate legacy Lightning card styles

* unify dashboard account change state

* fix account routing and Electrum balances

* fix account setup and wallet cards UI

* disable Bitcoin Accounts for all hardware

* merge conflicts fix

* minor fixes

* update cards_view [skip ci]

* reverted cards view update [skip ci]

* fix asset name

* moved the transaction filtering into the electrum wallet

* localization

* disable accounts for watch-only wallets

* fix: stop card render from switching the wallet account

* fix: restore Lightning transactions in tx history

* minor fix

* fix: skip duplicate legacy address generation [skip ci]

* fix: correct account balance/design mismatch bugs

* addressing review comments

---------

Co-authored-by: Robert Malikowski <malikowskirobert@gmail.com>
Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
This commit is contained in:
Serhii
2026-10-03 00:44:57 +02:00
committed by GitHub
co-authored by Omar Hatem Robert Malikowski
parent 0503d54290
commit d7ebf42814
84 changed files with 3991 additions and 2186 deletions
+36 -1
View File
@@ -256,7 +256,10 @@ class CWBitcoin extends Bitcoin {
List<BitcoinUnspent> getUnspents(Object wallet,
{UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any}) {
final bitcoinWallet = wallet as ElectrumWallet;
return bitcoinWallet.unspentCoins.where((element) {
final coins = bitcoinWallet.unspentCoinsForCurrentAccount;
return coins.where((element) {
switch (coinTypeToSpendFrom) {
case UnspentCoinType.mweb:
return element.bitcoinAddressRecord.type == SegwitAddresType.mweb;
@@ -919,4 +922,36 @@ class CWBitcoin extends Bitcoin {
}
return null;
}
@override
ElectrumBalance balanceForAccount(Object wallet, int accountIndex) {
final bitcoinWallet = wallet as ElectrumWallet;
return bitcoinWallet.balanceForAccount(accountIndex);
}
@override
Map<int, Object> accountBalancesSnapshot(Object wallet) {
final bitcoinWallet = wallet as ElectrumWallet;
return Map<int, Object>.of(bitcoinWallet.accountBalances);
}
@override
List<TransactionInfo> getCurrentAccountBitcoinTransactions(Object wallet) =>
(wallet as ElectrumWallet).currentAccountBitcoinTransactions;
@override
Future<void> setCurrentAccount(Object wallet, int accountIndex) async {
final bitcoinWallet = wallet as ElectrumWallet;
await bitcoinWallet.setCurrentAccount(accountIndex);
}
@override
Future<Account> getCurrentAccount(Object wallet) async {
final electrumWallet = wallet as ElectrumWallet;
final acc = await electrumWallet.loadCurrentAccount();
return Account(
id: acc.accountIndex,
label: acc.label,
balance: electrumWallet.balanceForAccount(acc.accountIndex).confirmed.toString());
}
}
+78 -19
View File
@@ -55,7 +55,6 @@ import 'package:cake_wallet/nano/nano.dart';
import 'package:cake_wallet/new-ui/model/charts/price_store.dart';
import 'package:cake_wallet/new-ui/new_dashboard.dart';
import 'package:cake_wallet/new-ui/pages/about_page.dart';
import 'package:cake_wallet/new-ui/pages/account_customizer.dart';
import 'package:cake_wallet/new-ui/pages/bridge/bridge_amount_page.dart';
import 'package:cake_wallet/new-ui/pages/bridge/bridge_network_page.dart';
import 'package:cake_wallet/new-ui/pages/bridge/bridge_receiving_wallet_page.dart';
@@ -131,6 +130,7 @@ import 'package:cake_wallet/src/screens/settings/privacy_page.dart';
import 'package:cake_wallet/src/screens/settings/security_backup_page.dart';
import 'package:cake_wallet/src/screens/settings/silent_payments_logs_page.dart';
import 'package:cake_wallet/src/screens/settings/silent_payments_settings.dart';
import 'package:cake_wallet/new-ui/pages/wallet_accounts_page.dart';
import 'package:cake_wallet/src/screens/setup_2fa/modify_2fa_page.dart';
import 'package:cake_wallet/src/screens/setup_2fa/setup_2fa.dart';
import 'package:cake_wallet/src/screens/setup_2fa/setup_2fa_enter_code_page.dart';
@@ -214,11 +214,14 @@ import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart';
import 'package:cake_wallet/view_model/hardware_wallet/trezor_connect_view_model.dart';
import 'package:cake_wallet/view_model/integrations/deuro_view_model.dart';
import 'package:cake_wallet/view_model/link_view_model.dart';
import 'package:cake_wallet/view_model/monero_account_list/account_list_item.dart';
import 'package:cake_wallet/view_model/monero_account_list/monero_account_edit_or_create_view_model.dart';
import 'package:cake_wallet/view_model/monero_account_list/monero_account_list_view_model.dart';
import 'package:cake_wallet/view_model/nano_account_list/nano_account_edit_or_create_view_model.dart';
import 'package:cake_wallet/view_model/nano_account_list/nano_account_list_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/bitcoin_account_list/bitcoin_account_edit_or_create_view_model.dart';
import 'package:cake_wallet/view_model/wallet_account_list/bitcoin_account_list/bitcoin_account_list_view_model.dart';
import 'package:cake_wallet/view_model/wallet_account_list/monero_account_list/monero_account_edit_or_create_view_model.dart';
import 'package:cake_wallet/view_model/wallet_account_list/monero_account_list/monero_account_list_view_model.dart';
import 'package:cake_wallet/view_model/wallet_account_list/nano_account_list/nano_account_edit_or_create_view_model.dart';
import 'package:cake_wallet/view_model/wallet_account_list/nano_account_list/nano_account_list_view_model.dart';
import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model.dart';
import 'package:cake_wallet/view_model/node_list/node_list_view_model.dart';
import 'package:cake_wallet/view_model/order_details_view_model.dart';
@@ -248,6 +251,7 @@ import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_details_view_
import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_item.dart';
import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_list_view_model.dart';
import 'package:cake_wallet/view_model/bridge/bridge_view_model.dart';
import 'package:cake_wallet/view_model/wallet_account_list/wallet_account_list_view_model.dart';
import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_edit_or_create_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';
@@ -290,6 +294,9 @@ import 'buy/meld/meld_buy_provider.dart';
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;
@@ -535,6 +542,13 @@ Future<void> setup({
getIt.registerFactory(() => DashboardViewModel(
tradeMonitor: getIt.get<TradeMonitor>(),
balanceViewModel: getIt.get<BalanceViewModel>(),
accountListViewModelFactory: () {
final wallet = getIt.get<AppStore>().wallet;
if (wallet == null) return null;
final hasAccounts = wallet.hasAccountsSupport;
if (!hasAccounts) return null;
return getIt.get<WalletAccountListViewModel>();
},
appStore: getIt.get<AppStore>(),
tradesStore: getIt.get<TradesStore>(),
ordersStore: getIt.get<OrdersStore>(),
@@ -560,21 +574,11 @@ Future<void> setup({
getIt.registerLazySingleton<PriceStore>(() => PriceStore());
getIt.registerFactory<ChartsBloc>(
() => ChartsBloc(appStore: getIt.get<AppStore>(), priceStore: getIt.get<PriceStore>()));
() => ChartsBloc(appStore: getIt.get<AppStore>(), priceStore: getIt.get<PriceStore>()));
getIt.registerFactory<ChartsPage>(() => ChartsPage(
chartsBloc: getIt.get<ChartsBloc>(),
));
getIt.registerFactory<AccountCreationModal>(() => AccountCreationModal(
accountEditOrCreateViewModel: getIt.get<MoneroAccountEditOrCreateViewModel>()));
getIt.registerFactoryParam<AccountCustomizer, DashboardViewModel, void>(
(dashboardViewModel, _) => AccountCustomizer(
accountListViewModel: getIt.get<MoneroAccountListViewModel>(),
dashboardViewModel: dashboardViewModel,
),
);
chartsBloc: getIt.get<ChartsBloc>(),
));
getIt.registerFactory<LightningUsernameBloc>(
() => LightningUsernameBloc(getIt.get<AppStore>().wallet!));
@@ -898,6 +902,30 @@ Future<void> setup({
'Unexpected wallet type: ${wallet.type} for generate Monero AccountListViewModel');
});
getIt.registerFactory<BitcoinAccountListViewModel>(() {
final wallet = getIt.get<AppStore>().wallet!;
if (wallet.type == WalletType.bitcoin) {
return BitcoinAccountListViewModel(wallet, getIt.get<SettingsStore>());
}
throw Exception(
'Unexpected wallet type: ${wallet.type} for generate Bitcoin AccountListViewModel');
});
getIt.registerFactory<WalletAccountListViewModel>(() {
final wallet = getIt.get<AppStore>().wallet!;
switch (wallet.type) {
case WalletType.bitcoin:
return getIt.get<BitcoinAccountListViewModel>();
case WalletType.monero:
case WalletType.wownero:
case WalletType.haven:
return getIt.get<MoneroAccountListViewModel>();
default:
throw Exception('Unsupported wallet type for WalletAccountListViewModel: ${wallet.type}');
}
});
getIt.registerFactoryParam<MoneroAccountEditOrCreateViewModel, AccountListItem?, void>(
(AccountListItem? account, _) => MoneroAccountEditOrCreateViewModel(
monero!.getAccountList(getIt.get<AppStore>().wallet!),
@@ -905,6 +933,12 @@ Future<void> setup({
wallet: getIt.get<AppStore>().wallet!,
accountListItem: account));
getIt.registerFactory<BitcoinAccountEditOrCreateViewModel>(
() => BitcoinAccountEditOrCreateViewModel(
wallet: getIt.get<AppStore>().wallet!,
),
);
getIt.registerFactoryParam<NanoAccountEditOrCreateViewModel, NanoAccount?, void>(
(NanoAccount? account, _) =>
NanoAccountEditOrCreateViewModel(nano!.getAccountList(getIt.get<AppStore>().wallet!),
@@ -1002,6 +1036,31 @@ Future<void> setup({
getIt.registerFactory(() => SecurityBackupPage(getIt.get<SecuritySettingsViewModel>(),
getIt.get<AuthService>(), getIt.get<AppStore>().wallet!.isHardwareWallet));
getIt.registerFactory<WalletAccountEditOrCreateViewModel>(() {
final wallet = getIt.get<AppStore>().wallet!;
switch (wallet.type) {
case WalletType.bitcoin:
return getIt.get<BitcoinAccountEditOrCreateViewModel>();
case WalletType.monero:
case WalletType.wownero:
case WalletType.haven:
return getIt.get<MoneroAccountEditOrCreateViewModel>();
default:
throw Exception(
'Unsupported wallet type for WalletAccountEditOrCreateViewModel: ${wallet.type}');
}
});
getIt.registerFactoryParam<WalletAccountsPage, DashboardViewModel, void>(
(dashboardViewModel, _) => WalletAccountsPage(
dashboardViewModel: dashboardViewModel,
accountListViewModel:
dashboardViewModel.accountListViewModel ?? getIt.get<WalletAccountListViewModel>(),
accountEditOrCreateViewModel: getIt.get<WalletAccountEditOrCreateViewModel>(),
),
);
getIt.registerFactory(() => PrivacyPage(getIt.get<PrivacySettingsViewModel>()));
-1
View File
@@ -117,7 +117,6 @@ class PreferencesKey {
static const showDecredInfoCard = 'show_decred_info_card';
static const forceDecentralizedExchanges = 'force_decentralized_exchanges';
static const decentralizedExchangesPromptDismissed = 'decentralized_exchanges_prompt_dismissed';
static String moneroWalletUpdateV1Key(String name) =>
'${PreferencesKey.moneroWalletPasswordUpdateV1Base}_${name}';
-505
View File
@@ -1,505 +0,0 @@
import 'dart:ui';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/pages/card_customizer.dart';
import 'package:cake_wallet/new-ui/viewmodels/card_customizer/card_customizer_bloc.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/cards/balance_card.dart';
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/alert_with_two_actions.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/monero_account_list/account_list_item.dart';
import 'package:cake_wallet/view_model/monero_account_list/monero_account_edit_or_create_view_model.dart';
import 'package:cake_wallet/view_model/monero_account_list/monero_account_list_view_model.dart';
import 'package:cw_core/balance_card_style_settings.dart';
import 'package:cw_core/card_design.dart';
import 'package:cw_core/generate_name.dart';
import 'package:cw_core/sync_status.dart';
import 'package:cw_core/utils/print_verbose.dart';
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 AccountCustomizerListItem {
final BalanceCard card;
final int order;
final AccountListItem accountListItem;
AccountCustomizerListItem(
{required this.card, required this.order, required this.accountListItem});
}
class AccountCustomizer extends StatefulWidget {
const AccountCustomizer(
{super.key, required this.accountListViewModel, required this.dashboardViewModel});
final MoneroAccountListViewModel accountListViewModel;
final DashboardViewModel dashboardViewModel;
@override
State<AccountCustomizer> createState() => _AccountCustomizerState();
}
class _AccountCustomizerState extends State<AccountCustomizer> {
static const double _kStackVisibleFactor = 0.2;
late final double cardWidth = MediaQuery.of(context).size.width * 0.9;
final List<AccountCustomizerListItem> _items = [];
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
loadCards();
final activeId = widget.accountListViewModel.selected.id;
for (int i = 0; i < _items.length - 1; i++) {
if (_items[i].accountListItem.id == activeId) {
final lastIndex = _items.length - 1;
final temp = _items[i];
_items[i] = _items[lastIndex];
_items[lastIndex] = temp;
saveCardOrder();
widget.dashboardViewModel.loadCardDesigns();
break;
}
}
});
}
@override
void dispose() {
saveCardOrder().then((value) => widget.dashboardViewModel.loadCardDesigns());
super.dispose();
}
void loadCards() {
_items.clear();
final accounts = widget.accountListViewModel.accounts;
for (int i = 0; i < accounts.length; i++) {
final index = widget.dashboardViewModel.cardOrder[i];
if (index == null || index >= accounts.length) {
// db order broken.
reset();
break;
}
_items.add(AccountCustomizerListItem(
card: BalanceCard(
accountName: accounts[index].label,
accountIndex: accounts[index].id,
balance: accounts[index].balance ?? "0.00",
accountBalance: accounts[index].balance ?? "0.00",
designSwitchDuration: Duration.zero,
assetName: widget.accountListViewModel.currency.title,
onCustomizeTapped: (i == accounts.length - 1) ? _openCardCustomizer : null,
selected: i == accounts.length - 1,
width: cardWidth,
design: widget.dashboardViewModel.cardDesigns[index],
),
order: index,
accountListItem: accounts[index]));
}
setState(() {});
}
@override
Widget build(BuildContext context) {
if (_items.isEmpty) return SizedBox.shrink();
return Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.vertical(top: Radius.circular(24))),
child: Column(
children: [
ModalTopBar(
title: S.of(context).wallet_accounts,
leadingIcon: Icon(Icons.close),
leadingSemanticLabel: S.of(context).close,
onLeadingPressed: Navigator.of(context).maybePop,
trailingIcon: Icon(Icons.refresh),
trailingSemanticLabel: S.of(context).reset,
onTrailingPressed: showResetDialog,
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 24.0),
child: Text(
S.of(context).account_customizer_desc,
style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant),
),
),
Expanded(
child: Stack(
children: [
ReorderableListView.builder(
padding: EdgeInsets.only(bottom: 196),
scrollController: ModalScrollController.of(context),
onReorder: reorder,
proxyDecorator: (child, index, animation) {
return 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: (BuildContext context, int index) {
final card = _items[index].card;
// The stack is ordered bottom to top, so the last item
// is the account currently in front — the selected one.
final selectedItemIndex = _items.length - 1;
return Container(
key: ValueKey(index),
// One labeled, selectable node per account; the card's own
// texts stay reachable underneath it.
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: EdgeInsets.only(bottom: 50),
child: Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0),
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.0),
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),
)
],
),
),
),
),
),
),
),
)))),
],
),
),
],
),
);
}
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;
}
Future<void> _showAddAccountModal() async {
if (!_checkReadyToManage()) {
return;
}
final res = await showCupertinoModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
builder: (context) {
final modal = getIt.get<AccountCreationModal>();
return Material(child: modal);
});
if (res != null && res is bool && res == true) {
await widget.dashboardViewModel.loadCardDesigns();
loadCards();
await saveCardOrder();
}
}
void _openCardCustomizer() {
if (!_checkReadyToManage()) {
return;
}
final bloc = getIt.get<CardCustomizerBloc>(param1: false);
Navigator.of(context).push(CupertinoPageRoute(
builder: (context) {
return BlocProvider(
create: (context) => bloc,
child: Material(
child: CardCustomizer(
cryptoTitle: widget.dashboardViewModel.wallet.currency.fullName ??
widget.dashboardViewModel.wallet.currency.name,
cryptoName: widget.dashboardViewModel.wallet.currency.name,
),
),
);
},
)).then((_) async {
bloc.add(DesignSaved());
await bloc.stream.firstWhere((item) => item is CardCustomizerSaved);
await widget.dashboardViewModel.loadCardDesigns();
loadCards();
});
}
void reorder(int oldIndex, int newIndex) {
setState(() {
if (oldIndex < newIndex) {
newIndex -= 1;
}
final AccountCustomizerListItem item = _items.removeAt(oldIndex);
_items.insert(newIndex, item);
});
// 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,
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);
}
if (newIndex == _items.length - 1 || oldIndex == _items.length - 1) {
widget.accountListViewModel.select(_items[_items.length - 1].accountListItem);
}
}
Future<void> saveCardOrder() async {
for (int i = 0; i < _items.length; i++) {
final idx = _items.indexWhere((element) => element.accountListItem.id == i);
printV("$i: $idx");
await BalanceCardStyleSettings.fromCardDesign(
walletInfoId: widget.dashboardViewModel.wallet.walletInfo.internalId,
accountIndex: i,
cardOrder: idx,
design: _items[idx].card.design)
.insert();
}
}
Future<void> showResetDialog() async {
final res = await showPopUp(
context: context,
builder: (context) {
return 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 = widget.accountListViewModel.accounts;
for (int i = 0; i < widget.accountListViewModel.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: widget.accountListViewModel.currency.title,
selected: true,
designSwitchDuration: Duration(milliseconds: 200),
width: cardWidth,
design: i >= widget.dashboardViewModel.cardDesigns.length
? CardDesign.genericDefault
: widget.dashboardViewModel.cardDesigns[i],
),
order: i,
accountListItem: accounts[i]));
}
saveCardOrder();
setState(() {});
}
}
class AccountCreationModal extends StatefulWidget {
const AccountCreationModal({super.key, required this.accountEditOrCreateViewModel});
final MoneroAccountEditOrCreateViewModel accountEditOrCreateViewModel;
@override
State<AccountCreationModal> createState() => _AccountCreationModalState();
}
class _AccountCreationModalState extends State<AccountCreationModal> {
final TextEditingController _controller = TextEditingController();
bool _loading = false;
Future<void> _generateAccountName() async => _controller.text = await generateName();
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.vertical(top: Radius.circular(30))),
child: SafeArea(
top: false,
child: Padding(
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ModalGrabHandle(),
ModalTopBar(title: S.of(context).create_account),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 18.0),
child: Column(
spacing: 50,
children: [
SizedBox(),
Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(16)),
child: Row(
children: [
Expanded(
child: TextField(
controller: _controller,
decoration: InputDecoration(hintText: S.of(context).account_name),
),
),
Padding(
padding: const EdgeInsets.all(12.0),
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),
),
),
),
),
),
)
],
),
),
NewPrimaryButton(
onPressed: () async {
if (_loading) return;
setState(() {
_loading = true;
});
widget.accountEditOrCreateViewModel.label = _controller.text;
await widget.accountEditOrCreateViewModel.save();
Navigator.of(context).pop(true);
},
text: S.of(context).continue_text,
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
isLoading: _loading,
),
SizedBox(),
],
),
)
],
),
),
));
}
}
+9 -9
View File
@@ -87,7 +87,12 @@ class _NewAddressesPageState extends State<NewAddressesPage> {
});
WidgetsBinding.instance.addPostFrameCallback((_) {
reaction((context) => widget.dashboardViewModel.cardDesigns.first, (value) {
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;
setState(() {
design = value;
});
@@ -130,8 +135,7 @@ class _NewAddressesPageState extends State<NewAddressesPage> {
child: Column(
spacing: 16,
children: [
if (widget.dashboardViewModel.type == WalletType.monero ||
widget.dashboardViewModel.type == WalletType.wownero)
if (widget.dashboardViewModel.isMultiAccountsEnabled)
Observer(
builder: (_) => AccountPreviewHeader(
dashboardViewModel: widget.dashboardViewModel,
@@ -271,8 +275,7 @@ class AccountPreviewHeader extends StatelessWidget {
final CardDesign? design;
@override
Widget build(BuildContext context) {
return Container(
Widget build(BuildContext context) => Container(
height: 64,
width: MediaQuery.of(context).size.width * 0.9,
decoration: BoxDecoration(
@@ -295,9 +298,7 @@ class AccountPreviewHeader extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
monero?.getCurrentAccount(dashboardViewModel.wallet).label ??
wownero?.getCurrentAccount(dashboardViewModel.wallet).label ??
"",
dashboardViewModel.accountListViewModel?.selectedAccount?.label ?? '',
style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.primary),
),
Text(
@@ -325,7 +326,6 @@ class AccountPreviewHeader extends StatelessWidget {
),
),
);
}
}
class AddressRow extends StatelessWidget {
+342 -281
View File
@@ -1,21 +1,77 @@
import 'dart:async';
import 'dart:math';
import "dart:async";
import "dart:math";
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/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/cake_image_widget.dart';
import 'package:cw_core/card_design.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import "package:cake_wallet/di.dart";
import "package:cake_wallet/generated/i18n.dart";
import "package:cake_wallet/new-ui/modal_navigator.dart";
import "package:cake_wallet/new-ui/viewmodels/card_customizer/card_customizer_bloc.dart";
import "package:cake_wallet/new-ui/widgets/coins_page/cards/balance_card.dart";
import "package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
import "package:cake_wallet/src/widgets/cake_image_widget.dart";
import "package:cake_wallet/view_model/dashboard/dashboard_view_model.dart";
import "package:cw_core/card_design.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({super.key, required this.cryptoTitle, required this.cryptoName});
const CardCustomizer({required this.cryptoTitle, required this.cryptoName, super.key});
final String cryptoTitle;
final String cryptoName;
static Future<void> show({
required BuildContext context,
required DashboardViewModel dashboardViewModel,
required bool lightningMode,
bool asModalSheet = true,
VoidCallback? onSaved,
}) async {
final bloc = getIt.get<CardCustomizerBloc>(
param1: lightningMode,
param2: dashboardViewModel.settingsStore.displayAmountsInSatoshi,
);
final customizer = BlocProvider(
create: (_) => bloc,
child: Material(
child: CardCustomizer(
cryptoTitle: dashboardViewModel.wallet.currency.fullName ??
dashboardViewModel.wallet.currency.name,
cryptoName: dashboardViewModel.wallet.currency.name,
),
),
);
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),
);
}
bloc.add(DesignSaved());
await bloc.stream.firstWhere(
(state) => state is CardCustomizerSaved,
orElse: () => bloc.state,
);
await dashboardViewModel.loadCardDesigns();
await dashboardViewModel.accountListViewModel?.reload();
onSaved?.call();
}
@override
State<CardCustomizer> createState() => _CardCustomizerState();
}
@@ -63,298 +119,303 @@ class _CardCustomizerState extends State<CardCustomizer> {
}
@override
Widget build(BuildContext context) {
return BlocListener<CardCustomizerBloc, CardCustomizerState>(
listenWhen: (previous, current) => previous.accountName != current.accountName,
listener: (context, state) {
if (accountNameController.text != state.accountName) {
accountNameController.text = state.accountName;
}
},
child: BlocBuilder<CardCustomizerBloc, CardCustomizerState>(
builder: (context, state) {
if (state is CardCustomizerNotLoaded) return SizedBox.shrink();
return PopScope(
child: SingleChildScrollView(
child: SafeArea(
child: Column(
spacing: 25.0,
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,
onLeadingPressed: () => Navigator.of(context).maybePop(),
// onTrailingPressed: () {},
),
if (editEnabled)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 18.0),
child: Column(
spacing: 8.0,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(S.of(context).account_name),
TextField(
maxLength: 32,
decoration: InputDecoration(counterText: ""),
onChanged: (value) {
context.read<CardCustomizerBloc>().add(AccountNameChanged(value));
},
controller: accountNameController,
)
],
),
Widget build(BuildContext context) => BlocListener<CardCustomizerBloc, CardCustomizerState>(
listenWhen: (previous, current) => previous.accountName != current.accountName,
listener: (context, state) {
if (accountNameController.text != state.accountName) {
accountNameController.text = state.accountName;
}
},
child: BlocBuilder<CardCustomizerBloc, CardCustomizerState>(
builder: (context, state) {
if (state is CardCustomizerNotLoaded) return SizedBox.shrink();
return PopScope(
child: SingleChildScrollView(
child: SafeArea(
child: Column(
spacing: 25.0,
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,
onLeadingPressed: () => Navigator.of(context).maybePop(),
// onTrailingPressed: () {},
),
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),
),
if (editEnabled)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 18.0),
child: Column(
spacing: 8.0,
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),
Container(
height: 63,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: state.availableDesigns.length,
separatorBuilder: (context, index) {
return SizedBox(width: 8.0);
},
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
context
.read<CardCustomizerBloc>()
.add(CardDesignSelected(index));
},
child: AnimatedContainer(
duration: Duration(milliseconds: 300),
decoration: ShapeDecoration(
shape: RoundedSuperellipseBorder(
side: BorderSide(
color:
((index == state.selectedDesignIndex)
Text(S.of(context).account_name),
TextField(
maxLength: 32,
decoration: 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)
? Theme.of(context)
.colorScheme
.onSurface
: Colors.transparent),
width: 1),
borderRadius:
BorderRadiusGeometry.circular(12),
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),
),
),
),
),
child: AnimatedScale(
duration: Duration(milliseconds: 200),
scale: index == state.selectedDesignIndex
? 0.94
: 1,
child: BalanceCard(
width: 96,
borderRadius: 10,
selected: false,
designSwitchDuration:
Duration(milliseconds: 300),
design: _cardStylePreviewDesign(state, index),
),
),
),
);
}),
),
],
)),
AnimatedSwitcher(
duration: const Duration(milliseconds: 350),
switchInCurve: Curves.easeOut,
switchOutCurve: Curves.easeIn,
transitionBuilder: (Widget child, Animation<double> animation) {
return FadeTransition(
)),
),
],
)),
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
),
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
.onSurface
: Colors.transparent,
width: 2,
.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),
),
),
child: Padding(
padding: const EdgeInsets.all(10.0),
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.0),
child: Column(
spacing: 8.0,
crossAxisAlignment: CrossAxisAlignment.start,
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]),
),
),
],
),
),
);
},
),
),
],
),
),
],
)
: const SizedBox.shrink(key: ValueKey('icon_style_hidden')),
),
SizedBox(height: 8),
Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(16)),
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
spacing: 8.0,
crossAxisAlignment: CrossAxisAlignment.start,
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) {
return Material(
borderRadius: BorderRadius.circular(999999999),
child: InkWell(
borderRadius: BorderRadius.circular(999999999),
onTap: () {
context
.read<CardCustomizerBloc>()
.add(ColorSelected(index));
},
child: Stack(
children: [
AnimatedOpacity(
duration: 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: 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]),
),
),
],
),
),
);
}),
)),
],
)),
)),
],
),
),
),
)
],
),
)),
],
)
],
),
)),
],
),
),
),
),
);
},
),
);
}
);
},
),
);
}
+205 -235
View File
@@ -1,35 +1,40 @@
import "dart:async";
import 'package:cake_wallet/core/auth_service.dart';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/generated/i18n.dart';
import "package:cake_wallet/bitcoin/bitcoin.dart";
import "package:cake_wallet/core/auth_service.dart";
import "package:cake_wallet/di.dart";
import "package:cake_wallet/generated/i18n.dart";
import "package:cake_wallet/main.dart";
import 'package:cake_wallet/new-ui/modal_navigator.dart';
import 'package:cake_wallet/new-ui/pages/account_customizer.dart';
import 'package:cake_wallet/new-ui/pages/card_customizer.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/seed/seed_backup_reminder_page.dart";
import 'package:cake_wallet/new-ui/pages/settings_page.dart';
import 'package:cake_wallet/new-ui/viewmodels/card_customizer/card_customizer_bloc.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/action_row/coin_action_row.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/assets_history_section.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/cards/cards_view.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/mweb_ad.dart';
import "package:cake_wallet/new-ui/pages/send_page.dart";
import "package:cake_wallet/new-ui/pages/settings_page.dart";
import "package:cake_wallet/new-ui/pages/wallet_accounts_page.dart";
import "package:cake_wallet/new-ui/widgets/buy_sell/buy_sell_selector_modal.dart";
import "package:cake_wallet/new-ui/widgets/coins_page/action_row/coin_action_row.dart";
import "package:cake_wallet/new-ui/widgets/coins_page/assets_history/assets_history_section.dart";
import "package:cake_wallet/new-ui/widgets/coins_page/cards/balance_card.dart";
import "package:cake_wallet/new-ui/widgets/coins_page/cards/cards_view.dart";
import "package:cake_wallet/new-ui/widgets/coins_page/mweb_ad.dart";
import "package:cake_wallet/new-ui/widgets/coins_page/seed_backup_reminder_card.dart";
import 'package:cake_wallet/new-ui/widgets/coins_page/top_bar_widget/top_bar.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/unconfirmed_balance_widget.dart';
import "package:cake_wallet/new-ui/widgets/coins_page/top_bar_widget/top_bar.dart";
import "package:cake_wallet/new-ui/widgets/coins_page/unconfirmed_balance_widget.dart";
import "package:cake_wallet/new-ui/widgets/coins_page/zcash_migration_modal.dart";
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/nft_view_model.dart';
import 'package:cake_wallet/view_model/monero_account_list/monero_account_list_view_model.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:mobx/mobx.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
import "package:cake_wallet/routes.dart";
import "package:cake_wallet/utils/payment_request.dart";
import "package:cake_wallet/view_model/dashboard/dashboard_view_model.dart";
import "package:cake_wallet/view_model/dashboard/nft_view_model.dart";
import "package:cw_core/unspent_coin_type.dart";
import "package:cw_core/wallet_type.dart";
import "package:flutter/cupertino.dart";
import "package:flutter/material.dart";
import "package:flutter_mobx/flutter_mobx.dart";
import "package:mobx/mobx.dart";
import "package:modal_bottom_sheet/modal_bottom_sheet.dart";
class NewHomePage extends StatefulWidget {
NewHomePage({super.key, required this.dashboardViewModel, required this.nftViewModel});
const NewHomePage({required this.dashboardViewModel, required this.nftViewModel, super.key});
final DashboardViewModel dashboardViewModel;
final NFTViewModel nftViewModel;
@@ -39,20 +44,15 @@ class NewHomePage extends StatefulWidget {
}
class _NewHomePageState extends State<NewHomePage> with RouteAware {
MoneroAccountListViewModel? accountListViewModel;
bool _lightningMode = false;
late final ReactionDisposer _walletReaction;
late final ReactionDisposer _migrationReaction;
@override
void initState() {
super.initState();
_setAccountViewModel();
_walletReaction = reaction((_) => widget.dashboardViewModel.wallet, (_) {
_setAccountViewModel();
setState(() {
_lightningMode = false;
});
setState(() {});
});
_migrationReaction = reaction((_) => widget.dashboardViewModel.isMigratingToIronwood, (val) {
@@ -91,178 +91,183 @@ class _NewHomePageState extends State<NewHomePage> with RouteAware {
@override
void didPopNext() => widget.dashboardViewModel.loadSeedBackupReminder();
void _setAccountViewModel() {
accountListViewModel = widget.dashboardViewModel.balanceViewModel.hasAccounts
? getIt.get<MoneroAccountListViewModel>()
: null;
}
@override
Widget build(BuildContext context) {
return Container(
height: MediaQuery.of(context).size.height,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Theme.of(context).colorScheme.surface,
Theme.of(context).colorScheme.surfaceDim,
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
Widget build(BuildContext context) => Container(
height: MediaQuery.of(context).size.height,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Theme.of(context).colorScheme.surface,
Theme.of(context).colorScheme.surfaceDim,
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
),
child: Stack(
children: [
CustomScrollView(
physics: BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()),
slivers: [
SliverPadding(
padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top),
sliver: CupertinoSliverRefreshControl(
refreshTriggerPullDistance: 160,
refreshIndicatorExtent: 90,
onRefresh: () {
unawaited(widget.nftViewModel.getNFTAssetByWallet());
child: Stack(
children: [
CustomScrollView(
physics: const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()),
slivers: [
SliverPadding(
padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top),
sliver: CupertinoSliverRefreshControl(
refreshTriggerPullDistance: 160,
refreshIndicatorExtent: 90,
onRefresh: () {
unawaited(widget.nftViewModel.getNFTAssetByWallet());
return widget.dashboardViewModel.refreshDashboard();
},
return widget.dashboardViewModel.refreshDashboard();
},
),
),
),
SliverToBoxAdapter(
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.start,
spacing: 24.0,
children: [
TopBar(
key: ValueKey(widget.dashboardViewModel.wallet.id),
dashboardViewModel: widget.dashboardViewModel,
lightningMode: _lightningMode,
onLightningSwitchPress: () {
setState(() {
_lightningMode = !_lightningMode;
});
},
onSettingsButtonPress: () async {
await CupertinoScaffold.showCupertinoModalBottomSheet(
context: context,
barrierColor: Colors.black.withAlpha(85),
builder: (context) => FractionallySizedBox(
child: Material(
child: NewSettingsPage(
SliverToBoxAdapter(
child: Observer(
builder: (_) {
final _lightningMode = widget.dashboardViewModel.lightningMode;
return Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.start,
spacing: 24.0,
children: [
TopBar(
key: ValueKey(widget.dashboardViewModel.wallet.id),
dashboardViewModel: widget.dashboardViewModel,
authService: getIt.get<AuthService>(),
))),
);
if (!mounted) {
return;
}
_setAccountViewModel();
await widget.dashboardViewModel.loadCardDesigns();
if (mounted) {
setState(() {});
}
},
),
Column(
spacing: 20,
children: [
Column(
children: [
Observer(
builder: (_) => CardsView(
key: ValueKey(widget.dashboardViewModel.wallet.name),
onCustomizeTapped: openCardCustomizer,
dashboardViewModel: widget.dashboardViewModel,
accountListViewModel: accountListViewModel,
onCompactModeBackgroundCardsTapped: openAccountCustomizer,
lightningMode: _lightningMode,
onLightningSwitchPress: () {
widget.dashboardViewModel.toggleLightningMode();
},
onSettingsButtonPress: () {
CupertinoScaffold.showCupertinoModalBottomSheet(
context: context,
barrierColor: Colors.black.withAlpha(85),
builder: (context) => FractionallySizedBox(
child: Material(
child: NewSettingsPage(
dashboardViewModel: widget.dashboardViewModel,
authService: getIt.get<AuthService>(),
),
),
),
).then((_) async {
widget.dashboardViewModel.accountListViewModel?.reload();
await Future<void>.delayed(Duration.zero);
await widget.dashboardViewModel.loadCardDesigns();
if (mounted) setState(() {});
});
},
),
Column(
children: [
AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
switchInCurve: Curves.easeOut,
switchOutCurve: Curves.easeOut,
layoutBuilder: (currentChild, previousChildren) => Stack(
// the front (selected) card sits at the bottom of the stack box
alignment: Alignment.bottomCenter,
children: <Widget>[
...previousChildren,
if (currentChild != null) currentChild,
],
),
child: KeyedSubtree(
key: ValueKey(_lightningMode),
child: CardsView(
key: ValueKey(
"${widget.dashboardViewModel.wallet.name}_${_lightningMode}_${widget.dashboardViewModel.accountListViewModel?.accounts.length ?? 0}_${widget.dashboardViewModel.cardDesigns.length}"),
onCustomizeTapped: openCardCustomizer,
dashboardViewModel: widget.dashboardViewModel,
accountListViewModel:
widget.dashboardViewModel.accountListViewModel,
onCompactModeBackgroundCardsTapped: openAccountCustomizer,
lightningMode: _lightningMode),
),
),
),
Observer(builder: (_) {
return AnimatedSize(
duration: Duration(milliseconds: 150),
curve: Curves.easeInOutCubic,
child: (widget.dashboardViewModel.shouldShowBalanceHiddenMessage)
? Column(
children: [
SizedBox(
height: 12,
width: double.infinity,
),
Text(
S.of(context).long_press_show_balance,
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onSurfaceVariant),
)
],
)
: SizedBox(width: double.infinity),
);
}),
UnconfirmedBalanceWidget(
dashboardViewModel: widget.dashboardViewModel,
),
],
),
Observer(
builder: (_) {
return Column(
children: [
CoinActionRow(
lightningMode: _lightningMode,
showSwap: widget.dashboardViewModel.isEnabledSwapAction,
walletType: widget.dashboardViewModel.wallet.type,
),
MwebAd(
dashboardViewModel: widget.dashboardViewModel,
),
SeedBackupReminderCard(
dashboardViewModel: widget.dashboardViewModel,
onTap: openSeedBackupReminder,
),
],
);
},
),
],
),
],
Observer(
builder: (_) => AnimatedSize(
duration: const Duration(milliseconds: 150),
curve: Curves.easeInOutCubic,
child: (widget.dashboardViewModel
.shouldShowBalanceHiddenMessage)
? Column(
children: [
const SizedBox(
height: 12,
width: double.infinity,
),
Text(
S.of(context).long_press_show_balance,
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onSurfaceVariant),
)
],
)
: const SizedBox(width: double.infinity),
)),
UnconfirmedBalanceWidget(
dashboardViewModel: widget.dashboardViewModel),
SeedBackupReminderCard(
dashboardViewModel: widget.dashboardViewModel,
onTap: openSeedBackupReminder,
),
],
),
Column(
children: [
CoinActionRow(
lightningMode: _lightningMode,
showSwap: widget.dashboardViewModel.isEnabledSwapAction,
walletType: widget.dashboardViewModel.wallet.type,
),
MwebAd(
dashboardViewModel: widget.dashboardViewModel,
),
],
)
],
);
},
),
),
),
Observer(
builder: (_) => AssetsHistorySection(
nftViewModel: widget.nftViewModel,
dashboardViewModel: widget.dashboardViewModel,
Observer(
builder: (_) => AssetsHistorySection(
nftViewModel: widget.nftViewModel,
dashboardViewModel: widget.dashboardViewModel,
),
),
const SliverToBoxAdapter(
child: SizedBox(height: 80.0),
)
]),
Container(
height: (MediaQuery.of(context).padding.top),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: <Color>[
Theme.of(context).colorScheme.surface.withAlpha(5),
Theme.of(context).colorScheme.surface.withAlpha(25),
Theme.of(context).colorScheme.surface.withAlpha(50),
Theme.of(context).colorScheme.surface.withAlpha(100),
Theme.of(context).colorScheme.surface.withAlpha(150),
Theme.of(context).colorScheme.surface.withAlpha(175),
Theme.of(context).colorScheme.surface.withAlpha(200),
],
),
SliverToBoxAdapter(
child: SizedBox(height: 80.0),
)
]),
Container(
height: (MediaQuery.of(context).padding.top),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: <Color>[
Theme.of(context).colorScheme.surface.withAlpha(5),
Theme.of(context).colorScheme.surface.withAlpha(25),
Theme.of(context).colorScheme.surface.withAlpha(50),
Theme.of(context).colorScheme.surface.withAlpha(100),
Theme.of(context).colorScheme.surface.withAlpha(150),
Theme.of(context).colorScheme.surface.withAlpha(175),
Theme.of(context).colorScheme.surface.withAlpha(200),
],
),
),
),
],
),
],
),
);
Future<void> openCardCustomizer() async {
await CardCustomizer.show(
context: context,
dashboardViewModel: widget.dashboardViewModel,
lightningMode: widget.dashboardViewModel.lightningMode,
);
}
@@ -278,54 +283,19 @@ class _NewHomePageState extends State<NewHomePage> with RouteAware {
}
Future<void> openAccountCustomizer() async {
final accountList = accountListViewModel;
if (accountList == null) {
return;
}
if (widget.dashboardViewModel.accountListViewModel == null) return;
await CupertinoScaffold.showCupertinoModalBottomSheet(
barrierColor: Colors.black.withAlpha(60),
context: context,
builder: (context) {
return ModalNavigator(
parentContext: context,
heightMode: ModalHeightModes.fullScreen,
rootPage: Material(
child: AccountCustomizer(
accountListViewModel: accountList,
dashboardViewModel: widget.dashboardViewModel,
),
),
);
},
builder: (context) => ModalNavigator(
parentContext: context,
heightMode: ModalHeightModes.fullScreen,
rootPage: Material(
child: getIt.get<WalletAccountsPage>(param1: widget.dashboardViewModel),
),
),
);
await widget.dashboardViewModel.loadCardDesigns();
}
void openCardCustomizer() async {
final bloc = getIt.get<CardCustomizerBloc>(
param1: _lightningMode,
param2: widget.dashboardViewModel.settingsStore.displayAmountsInSatoshi);
await CupertinoScaffold.showCupertinoModalBottomSheet(
barrierColor: Colors.black.withAlpha(60),
context: context,
builder: (context) {
return ModalNavigator(
parentContext: context,
heightMode: ModalHeightModes.fullScreen,
rootPage: BlocProvider(
create: (context) => bloc,
child: Material(
child: CardCustomizer(
cryptoTitle: widget.dashboardViewModel.wallet.currency.fullName ??
widget.dashboardViewModel.wallet.currency.name,
cryptoName: widget.dashboardViewModel.wallet.currency.name,
)),
));
},
);
bloc.add(DesignSaved());
await bloc.stream.firstWhere((s) => s is CardCustomizerSaved);
widget.dashboardViewModel.loadCardDesigns();
}
}
+2 -4
View File
@@ -25,8 +25,6 @@ bool _hasMweb(DashboardViewModel vm) => vm.hasMweb;
bool _hasWalletConnect(DashboardViewModel vm) => vm.hasWalletConnect;
bool _hasAccounts(DashboardViewModel vm) => vm.balanceViewModel.hasAccounts;
bool _requiresKeyImageSync(DashboardViewModel vm) =>
vm.wallet.type == WalletType.monero &&
[HardwareWalletType.cupcake, HardwareWalletType.trezor].contains(vm.wallet.hardwareWalletType);
@@ -65,8 +63,8 @@ class SettingsSectionData {
SettingsListItem(
"assets/new-ui/settings_row_icons/accounts.svg",
S.current.accounts,
Routes.accountCustomizer,
condition: _hasAccounts,
Routes.walletAccountsPage,
condition: (vm) => vm.wallet.hasAccountsSupport,
routeArgsBuilder: (vm) => vm,
),
SettingsListItem(
+567
View File
@@ -0,0 +1,567 @@
import "dart:async";
import "dart:ui";
import "package:cake_wallet/di.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/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/wallet_account_list_view_model.dart";
import "package:cw_core/balance_card_style_settings.dart";
import "package:cw_core/card_design.dart";
import "package:cw_core/sync_status.dart";
import "package:cw_core/utils/print_verbose.dart";
import "package:cw_core/wallet_type.dart";
import "package:flutter/material.dart";
import "package:mobx/mobx.dart";
import "package:modal_bottom_sheet/modal_bottom_sheet.dart";
class AccountCustomizerListItem {
AccountCustomizerListItem({
required this.card,
required this.order,
required this.accountListItem,
});
final BalanceCard card;
final int order;
final AccountListItem accountListItem;
}
class WalletAccountsPage extends StatefulWidget {
const WalletAccountsPage({
required this.accountListViewModel,
required this.accountEditOrCreateViewModel,
required this.dashboardViewModel,
super.key,
});
final WalletAccountListViewModel accountListViewModel;
final WalletAccountEditOrCreateViewModel accountEditOrCreateViewModel;
final DashboardViewModel dashboardViewModel;
@override
State<WalletAccountsPage> createState() => _WalletAccountsPageState();
}
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 = [];
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 _capitalizeAssetName =>
widget.dashboardViewModel.wallet.type != WalletType.bitcoin ||
widget.dashboardViewModel.settingsStore.displayAmountsInSatoshi !=
BitcoinAmountDisplayMode.satoshi;
@override
void initState() {
super.initState();
_accountsReaction = reaction(
(_) => accountListViewModel.accounts
.map((account) => "${account.id}:${account.label}:${account.balance}")
.join(","),
(_) {
if (!mounted) return;
loadCards();
},
);
WidgetsBinding.instance.addPostFrameCallback((_) async {
await widget.dashboardViewModel.loadCardDesigns();
if (!mounted) return;
loadCards();
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;
}
}
});
}
@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;
if (visible.isEmpty && _items.isNotEmpty) return;
_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(() {});
return;
}
for (int i = 0; i < allAccounts.length; i++) {
final realIndex = widget.dashboardViewModel.cardOrder[i];
if (realIndex == null || realIndex < 0 || realIndex >= allAccounts.length) {
// db order broken.
reset();
break;
}
_items.add(AccountCustomizerListItem(
card: BalanceCard(
accountName: allAccounts[realIndex].label,
accountIndex: allAccounts[realIndex].id,
balance: allAccounts[realIndex].balance ?? "0.00",
accountBalance: allAccounts[realIndex].balance ?? "0.00",
designSwitchDuration: Duration.zero,
assetName: _assetName,
capitalizeAssetName: _capitalizeAssetName,
selected: i == allAccounts.length - 1,
width: cardWidth,
design: widget.dashboardViewModel.cardDesigns[realIndex],
),
order: realIndex,
accountListItem: allAccounts[realIndex]));
}
if (mounted) {
setState(() {});
}
}
@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(
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(
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),
),
),
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,
),
)
],
),
),
),
),
),
),
),
),
),
),
),
],
),
),
],
),
),
],
),
);
}
bool _checkReadyToManage() {
if (widget.dashboardViewModel.wallet.type == WalletType.bitcoin) {
return true;
}
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;
}
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(() {});
}
}
Future<void> _openCardCustomizer() async {
if (!_checkReadyToManage()) {
return;
}
await CardCustomizer.show(
context: context,
dashboardViewModel: widget.dashboardViewModel,
lightningMode: widget.dashboardViewModel.lightningMode,
asModalSheet: false,
onSaved: _reloadAccountsAndCards,
);
}
Future<void> _reloadAccountsAndCards() async {
await 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) {
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();
});
}
void reorder(int oldIndex, int newIndex) {
if (!_isMultiAccountsEnabled) return;
setState(() {
if (oldIndex < newIndex) {
newIndex -= 1;
}
final AccountCustomizerListItem item = _items.removeAt(oldIndex);
_items.insert(newIndex, item);
});
// 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);
}
if (newIndex == _items.length - 1 || oldIndex == _items.length - 1) {
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(() {});
}
}
@@ -1,4 +1,5 @@
import 'package:bloc/bloc.dart';
import "package:cake_wallet/bitcoin/bitcoin.dart";
import 'package:cake_wallet/monero/monero.dart';
import 'package:cake_wallet/wownero/wownero.dart';
import "package:cw_core/balance_card_style_settings.dart";
@@ -30,6 +31,9 @@ class CardCustomizerBloc extends Bloc<CardCustomizerEvent, CardCustomizerState>
add(_Init());
}
bool get _accountNameEnabled =>
!lightningMode && _wallet.walletInfo.isMultiAccountsEnabled == true;
List<Gradient> _updateAvailableColors(CardDesign currentDesign) {
final list = List<Gradient>.from(CardDesign.allGradients, growable: true);
if (CardDesign.specialDesignsForCurrencies[_wallet.currency] != null) {
@@ -83,23 +87,9 @@ class CardCustomizerBloc extends Bloc<CardCustomizerEvent, CardCustomizerState>
}
void _init(_Init event, Emitter<CardCustomizerState> emit) async {
late final account;
if (_wallet.type == WalletType.monero) {
account = monero!.getCurrentAccount(_wallet);
} else if (_wallet.type == WalletType.wownero) {
account = wownero!.getCurrentAccount(_wallet);
} else {
account = null;
}
final accountName = (account?.label ?? "") as String;
late final int accountIndex;
if (account != null) {
accountIndex = account.id as int;
} else if (lightningMode) {
accountIndex = 0;
} else {
accountIndex = -1;
}
final accountInfo = await _getCurrentAccountInfo();
final accountName = accountInfo.accountName;
final accountIndex = accountInfo.accountIndex;
final curr = lightningMode ? CryptoCurrency.btcln : _wallet.currency;
final currentDesignSettings = await _loadCurrentDesignSettings(accountIndex);
final currentDesign = CardDesign.fromStyleSettings(currentDesignSettings, curr);
@@ -123,6 +113,39 @@ class CardCustomizerBloc extends Bloc<CardCustomizerEvent, CardCustomizerState>
selectedIconIndex: selectedIconIndex));
}
Future<({String accountName, int accountIndex})> _getCurrentAccountInfo() async {
if (lightningMode) {
return (accountName: "", accountIndex: -2);
}
final account = await _resolveCurrentAccount();
if (!_accountNameEnabled) {
return (accountName: "", accountIndex: account.accountIndex);
}
return account;
}
Future<({String accountName, int accountIndex})> _resolveCurrentAccount() async {
if (_wallet.type == WalletType.monero) {
final account = monero!.getCurrentAccount(_wallet);
return (accountName: account.label, accountIndex: account.id);
}
if (_wallet.type == WalletType.wownero) {
final account = wownero!.getCurrentAccount(_wallet);
return (accountName: account.label, accountIndex: account.id);
}
if (_wallet.type == WalletType.bitcoin) {
final account = await bitcoin!.getCurrentAccount(_wallet);
return (accountName: account.label, accountIndex: account.id);
}
return (accountName: "", accountIndex: -1);
}
void _onDesignSelected(CardDesignSelected event, Emitter<CardCustomizerState> emit) {
final newColors = _updateAvailableColors(state.availableDesigns[event.newDesignIndex]);
late final int newColorIndex;
@@ -152,32 +175,34 @@ class CardCustomizerBloc extends Bloc<CardCustomizerEvent, CardCustomizerState>
emit(state.copyWith(accountName: event.newAccountName));
}
void _onDesignSaved(DesignSaved event, Emitter<CardCustomizerState> emit) {
BalanceCardStyleSettings.fromCardDesign(
Future<void> _onDesignSaved(DesignSaved 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)
.insert()
.then((value) {
emit(CardCustomizerSaved(
state.selectedDesignIndex,
state.selectedColorIndex,
state.availableDesigns,
state.availableColors,
state.accountName,
state.accountIndex,
state.displaySats,
state.cardOrder,
availableIconPaths: state.availableIconPaths,
selectedIconIndex: state.selectedIconIndex));
});
saveAccountName();
.insert();
await saveAccountName();
emit(CardCustomizerSaved(
state.selectedDesignIndex,
state.selectedColorIndex,
state.availableDesigns,
state.availableColors,
state.accountName,
state.accountIndex,
state.displaySats,
state.cardOrder,
availableIconPaths: state.availableIconPaths,
selectedIconIndex: state.selectedIconIndex));
}
Future<void> saveAccountName() async {
if (!_accountNameEnabled) return;
if (_wallet.type == WalletType.monero) {
await saveMoneroAccountName();
}
@@ -185,6 +210,9 @@ class CardCustomizerBloc extends Bloc<CardCustomizerEvent, CardCustomizerState>
if (_wallet.type == WalletType.wownero) {
await saveWowneroAccountName();
}
if (_wallet.type == WalletType.bitcoin && !lightningMode) {
await saveBitcoinAccountName();
}
}
Future<void> saveMoneroAccountName() async {
@@ -202,4 +230,11 @@ class CardCustomizerBloc extends Bloc<CardCustomizerEvent, CardCustomizerState>
await _wallet.save();
}
Future<void> saveBitcoinAccountName() async {
await _wallet.walletInfo.renameAccount(
accountIndex: state.accountIndex,
label: state.accountName,
);
}
}
@@ -215,10 +215,12 @@ class BalanceCard extends StatelessWidget {
child: AnimatedSwitcher(
duration: designSwitchDuration,
layoutBuilder: (currentChild, previousChildren) {
final currentKey = currentChild?.key;
return Stack(
alignment: Alignment.centerLeft,
children: <Widget>[
...previousChildren,
for (final child in previousChildren)
if (child.key != currentKey) child,
if (currentChild != null) currentChild,
],
);
@@ -13,7 +13,7 @@ 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';
import 'package:cake_wallet/view_model/monero_account_list/monero_account_list_view_model.dart';
import 'package:cake_wallet/view_model/wallet_account_list/wallet_account_list_view_model.dart';
import 'package:cw_core/card_design.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/unspent_coin_type.dart';
@@ -30,14 +30,14 @@ import 'balance_card.dart';
class CardsView extends StatefulWidget {
const CardsView(
{super.key,
required this.dashboardViewModel,
required this.accountListViewModel,
required this.lightningMode,
required this.onCompactModeBackgroundCardsTapped,
required this.onCustomizeTapped});
required this.dashboardViewModel,
required this.accountListViewModel,
required this.lightningMode,
required this.onCompactModeBackgroundCardsTapped,
required this.onCustomizeTapped});
final DashboardViewModel dashboardViewModel;
final MoneroAccountListViewModel? accountListViewModel;
final WalletAccountListViewModel? accountListViewModel;
final VoidCallback onCompactModeBackgroundCardsTapped;
final VoidCallback onCustomizeTapped;
final bool lightningMode;
@@ -49,22 +49,39 @@ class CardsView extends StatefulWidget {
class _CardsViewState extends State<CardsView> {
late int _selectedIndex;
bool isFirstBuild = true;
ReactionDisposer? _cardOrderReaction;
int get _fallbackSelectedIndex =>
max(0, widget.dashboardViewModel.cardOrder.length - 1);
@override
void initState() {
super.initState();
_selectedIndex = widget.dashboardViewModel.cardOrder.length - 1;
reaction(
(_) => widget.dashboardViewModel.cardOrder.values.toList(),
(_) => setState(() {
_selectedIndex = widget.dashboardViewModel.cardOrder.length - 1;
}));
_selectedIndex = _fallbackSelectedIndex;
_cardOrderReaction = reaction(
(_) => widget.dashboardViewModel.cardOrder.values.toList(),
(_) => setState(() {
_selectedIndex = _fallbackSelectedIndex;
}),
);
}
@override
void dispose() {
_cardOrderReaction?.call();
super.dispose();
}
static const Duration animDuration = Duration(milliseconds: 200);
static const int compactModeTreshold = 4;
static const int maxCards = 5;
/// No account name on the card: Lightning card, or Bitcoin with multi-accounts off.
bool get _hideAccountInfo =>
widget.lightningMode ||
(widget.dashboardViewModel.wallet.type == WalletType.bitcoin &&
!widget.dashboardViewModel.isMultiAccountsEnabled);
Widget _buildCard(int visualIndex, int realIndex, int numCards, double parentWidth,
Map<int, int> order, bool compactMode, double overlapAmount) {
final baseTop = overlapAmount * (numCards - 1);
@@ -79,7 +96,7 @@ class _CardsViewState extends State<CardsView> {
final isSelected = _selectedIndex == visualIndex;
final accounts = widget.accountListViewModel?.accounts;
final cardLabel = (accounts != null && realIndex < accounts.length)
final cardLabel = (!_hideAccountInfo && accounts != null && realIndex < accounts.length)
? accounts[realIndex].label
: S.of(context).balance;
@@ -89,7 +106,7 @@ class _CardsViewState extends State<CardsView> {
widget.onCompactModeBackgroundCardsTapped();
} else if (!compactMode) {
setState(() {
if (widget.accountListViewModel != null)
if (widget.accountListViewModel != null && !widget.lightningMode)
widget.accountListViewModel!.select(widget.accountListViewModel!.accounts[realIndex]);
_selectedIndex = visualIndex;
});
@@ -121,9 +138,9 @@ class _CardsViewState extends State<CardsView> {
label: cardLabel,
hint: isSelected
? (widget.dashboardViewModel.balanceViewModel.displayMode ==
BalanceDisplayMode.hiddenBalance
? S.of(context).long_press_show_balance
: S.of(context).long_press_hide_balance)
BalanceDisplayMode.hiddenBalance
? S.of(context).long_press_show_balance
: S.of(context).long_press_hide_balance)
: null,
onTap: onCardTap,
onLongPress: isSelected ? onCardLongPress : null,
@@ -168,13 +185,14 @@ class _CardsViewState extends State<CardsView> {
realIndex >= widget.dashboardViewModel.cardDesigns.length)
cardDesign = CardDesign.genericDefault;
else if (widget.lightningMode)
cardDesign = widget.dashboardViewModel.cardDesigns[realIndex + 1];
// the lightning design is always the last one (after all bitcoin accounts)
cardDesign = widget.dashboardViewModel.cardDesigns.last;
else
cardDesign = widget.dashboardViewModel.cardDesigns[realIndex];
final String accountName;
final String accountBalance;
if (account == null) {
if (account == null || _hideAccountInfo) {
accountName = "";
accountBalance = "";
} else {
@@ -188,30 +206,30 @@ class _CardsViewState extends State<CardsView> {
final List<BalanceCardAction> actions = widget.lightningMode
? [
BalanceCardAction(
label: S.current.bitcoin_lightning_deposit,
icon: Icons.arrow_downward,
onTap: depositToL2,
),
BalanceCardAction(
label: S.current.bitcoin_lightning_withdraw,
icon: Icons.arrow_upward,
onTap: withdrawFromL2,
)
]
BalanceCardAction(
label: S.current.bitcoin_lightning_deposit,
icon: Icons.arrow_downward,
onTap: depositToL2,
),
BalanceCardAction(
label: S.current.bitcoin_lightning_withdraw,
icon: Icons.arrow_upward,
onTap: withdrawFromL2,
)
]
: widget.dashboardViewModel.isEnabledTradeAction
? [
BalanceCardAction(
label: S.current.buy,
icon: Icons.arrow_forward_ios_rounded,
iconSize: 12,
onTap: () {
showModalBottomSheet(
context: context, builder: (context) => BuySellSelectorModal());
},
)
]
: [];
? [
BalanceCardAction(
label: S.current.buy,
icon: Icons.arrow_forward_ios_rounded,
iconSize: 12,
onTap: () {
showModalBottomSheet(
context: context, builder: (context) => BuySellSelectorModal());
},
)
]
: [];
return BalanceCard(
width: effectiveCardWidth,
@@ -263,31 +281,55 @@ class _CardsViewState extends State<CardsView> {
double _getBoxHeight(int numCards, double overlapAmount) {
return
/* height of initial card */
(2 / 3.2) * (effectiveCardWidth) +
/* height of bg card * amount of bg cards */
overlapAmount * ((numCards) - 1);
/* height of initial card */
(2 / 3.2) * (effectiveCardWidth) +
/* height of bg card * amount of bg cards */
overlapAmount * ((numCards) - 1);
}
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;
}
return null;
}
@override
Widget build(BuildContext context) {
return Observer(builder: (context) {
Widget build(BuildContext context) => Observer(builder: (context) {
final parentWidth = MediaQuery.of(context).size.width;
final children = <Widget>[];
int numCards = widget.dashboardViewModel.wallet.type == WalletType.bitcoin
? 1
: widget.dashboardViewModel.cardDesigns.length;
int numCards = _visibleCardsCount();
if (numCards == 0) numCards = 1;
if (_selectedIndex >= (numCards)) {
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)),
)
List.generate(numCards, (i) => MapEntry(i, i)),
)
: widget.dashboardViewModel.cardOrder;
for (int i = min(numCards - 1, maxCards); i >= 0; i--) {
@@ -299,6 +341,9 @@ class _CardsViewState extends State<CardsView> {
}
}
final followIndex = _visualIndexForSelectedAccount(order);
if (followIndex != null) _selectedIndex = followIndex;
final bool compactMode = numCards >= compactModeTreshold;
final double overlapAmount = compactMode ? 5.0 : 46.0;
for (int i = min(numCards - 1, maxCards); i >= 0; i--) {
@@ -306,14 +351,6 @@ class _CardsViewState extends State<CardsView> {
int realIndex = order[visualIndex]!;
if (visualIndex == _selectedIndex &&
widget.accountListViewModel != null &&
realIndex < widget.accountListViewModel!.accounts.length &&
widget.accountListViewModel?.selected.label !=
widget.accountListViewModel?.accounts[realIndex].label) {
widget.accountListViewModel!.select(widget.accountListViewModel!.accounts[realIndex]);
}
children.add(_buildCard(
visualIndex, realIndex, numCards, parentWidth, order, compactMode, overlapAmount));
}
@@ -331,7 +368,6 @@ class _CardsViewState extends State<CardsView> {
),
);
});
}
Future<void> depositToL2() async {
PaymentRequest? paymentRequest = null;
@@ -343,7 +379,7 @@ class _CardsViewState extends State<CardsView> {
}
} else if (widget.dashboardViewModel.type == WalletType.bitcoin) {
final depositAddress =
await bitcoin!.getUnusedSpakDepositAddress(widget.dashboardViewModel.wallet);
await bitcoin!.getUnusedSpakDepositAddress(widget.dashboardViewModel.wallet);
if ((depositAddress?.isNotEmpty ?? false)) {
paymentRequest = PaymentRequest.fromUri(Uri.parse("bitcoin:$depositAddress"));
}
@@ -352,10 +388,10 @@ class _CardsViewState extends State<CardsView> {
if (widget.dashboardViewModel.type == WalletType.bitcoin) {
final page = getIt.get<NewSendPage>(
param1: SendPageParams(
initialPaymentRequest: paymentRequest,
unspentCoinType: UnspentCoinType.nonMweb,
mode: SendPageModes.lightningDeposit,
));
initialPaymentRequest: paymentRequest,
unspentCoinType: UnspentCoinType.nonMweb,
mode: SendPageModes.lightningDeposit,
));
showCupertinoModalBottomSheet(
context: context,
barrierColor: Colors.black.withAlpha(128),
@@ -399,10 +435,10 @@ class _CardsViewState extends State<CardsView> {
if (widget.dashboardViewModel.type == WalletType.bitcoin) {
final page = getIt.get<NewSendPage>(
param1: SendPageParams(
initialPaymentRequest: paymentRequest,
unspentCoinType: unspentCoinType,
mode: SendPageModes.lightningWithdrawal,
));
initialPaymentRequest: paymentRequest,
unspentCoinType: unspentCoinType,
mode: SendPageModes.lightningWithdrawal,
));
showCupertinoModalBottomSheet(
context: context,
barrierColor: Colors.black.withAlpha(128),
@@ -425,4 +461,4 @@ class _CardsViewState extends State<CardsView> {
);
}
}
}
}
@@ -16,14 +16,12 @@ import 'package:mobx/mobx.dart';
class TopBar extends StatefulWidget {
const TopBar({
required this.lightningMode,
required this.onLightningSwitchPress,
required this.dashboardViewModel,
required this.onSettingsButtonPress,
super.key,
});
final bool lightningMode;
final VoidCallback onLightningSwitchPress;
final VoidCallback onSettingsButtonPress;
final DashboardViewModel dashboardViewModel;
@@ -73,8 +71,8 @@ class _TopBarState extends State<TopBar> {
void _bindStatusReaction() {
_statusReactionDisposer = reaction(
(_) => widget.dashboardViewModel.status.runtimeType,
(status) {
(_) => widget.dashboardViewModel.status.runtimeType,
(status) {
syncedMessageTimer?.cancel();
syncedMessageTimer = null;
@@ -107,78 +105,78 @@ class _TopBarState extends State<TopBar> {
@override
Widget build(BuildContext context) => Padding(
padding: EdgeInsets.only(left: 18, right: 18, top: 10 + _additionalTopPadding(context)),
child: Observer(
builder: (_) => Row(
spacing: 12,
children: [
widget.dashboardViewModel.hasLightning
? LightningSwitcher(
lightningMode: widget.lightningMode,
onLightningSwitchPress: widget.onLightningSwitchPress,
)
: ChainIcon(
iconPath: widget.dashboardViewModel.wallet.currency.flatIconPath ?? "",
dashboardViewModel: widget.dashboardViewModel,
isSyncHeavy: widget.dashboardViewModel.isSyncHeavy,
showSyncedMessage: showSyncedMessage,
padding: EdgeInsets.only(
bottom: 10, left: 18, right: 18, top: 10 + _additionalTopPadding(context)),
child: Observer(
builder: (_) => Row(
spacing: 12,
children: [
widget.dashboardViewModel.hasLightning
? LightningSwitcher(
lightningMode: widget.dashboardViewModel.lightningMode,
onLightningSwitchPress: widget.onLightningSwitchPress,
)
: ChainIcon(
iconPath: widget.dashboardViewModel.wallet.currency.flatIconPath ?? "",
dashboardViewModel: widget.dashboardViewModel,
isSyncHeavy: widget.dashboardViewModel.isSyncHeavy,
showSyncedMessage: showSyncedMessage,
),
Expanded(
child: SizedBox(
height: 36,
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 150),
reverseDuration: Duration.zero,
layoutBuilder: (currentChild, previousChildren) => Stack(
alignment: Alignment.centerLeft,
children: [
...previousChildren,
if (currentChild != null) currentChild,
],
),
child: replacesWalletName
? SyncBar(
dashboardViewModel: widget.dashboardViewModel,
isSyncHeavy: widget.dashboardViewModel.isSyncHeavy,
showSyncedMessage: showSyncedMessage,
)
: Row(
children: [
Expanded(
child: WalletInfoBar(
name: widget.dashboardViewModel.wallet.name,
hardwareWalletType: widget.dashboardViewModel.wallet.hardwareWalletType,
),
),
Expanded(
child: SizedBox(
height: 36,
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 150),
reverseDuration: Duration.zero,
layoutBuilder: (currentChild, previousChildren) => Stack(
alignment: Alignment.centerLeft,
children: [
...previousChildren,
if (currentChild != null) currentChild,
],
),
child: replacesWalletName
? SyncBar(
dashboardViewModel: widget.dashboardViewModel,
isSyncHeavy: widget.dashboardViewModel.isSyncHeavy,
showSyncedMessage: showSyncedMessage,
)
: Row(
children: [
Expanded(
child: WalletInfoBar(
name: widget.dashboardViewModel.wallet.name,
hardwareWalletType:
widget.dashboardViewModel.wallet.hardwareWalletType,
),
),
if (widget.dashboardViewModel.isTorEnabled) ...[
const SizedBox(width: 6),
SyncBar(
dashboardViewModel: widget.dashboardViewModel,
isSyncHeavy: widget.dashboardViewModel.isSyncHeavy,
showSyncedMessage: showSyncedMessage,
),
],
],
),
),
if (widget.dashboardViewModel.isTorEnabled) ...[
const SizedBox(width: 6),
SyncBar(
dashboardViewModel: widget.dashboardViewModel,
isSyncHeavy: widget.dashboardViewModel.isSyncHeavy,
showSyncedMessage: showSyncedMessage,
),
],
],
),
),
ModernButton.svg(
key: const ValueKey("home_page_settings_button_key"),
iconColor: Theme.of(context).colorScheme.primary,
size: 36,
onPressed: () {
HapticFeedback.mediumImpact();
widget.onSettingsButtonPress();
},
svgPath: "assets/new-ui/top-settings.svg",
semanticLabel: S.of(context).settings_title,
),
],
),
),
),
);
ModernButton.svg(
key: const ValueKey("home_page_settings_button_key"),
iconColor: Theme.of(context).colorScheme.primary,
size: 36,
onPressed: () {
HapticFeedback.mediumImpact();
widget.onSettingsButtonPress();
},
svgPath: "assets/new-ui/top-settings.svg",
semanticLabel: S.of(context).settings_title,
),
],
),
),
);
//FIXME remove after this gets fixed flutter-side
double _additionalTopPadding(BuildContext context) {
@@ -188,4 +186,4 @@ class _TopBarState extends State<TopBar> {
return 0;
}
}
}
@@ -4,11 +4,7 @@ import 'package:cw_core/wallet_info.dart';
import 'package:flutter/material.dart';
class WalletInfoBar extends StatelessWidget {
const WalletInfoBar({
required this.name,
required this.hardwareWalletType,
super.key,
});
const WalletInfoBar({required this.name, required this.hardwareWalletType, super.key});
final String name;
final HardwareWalletType? hardwareWalletType;
@@ -22,6 +18,8 @@ class WalletInfoBar extends StatelessWidget {
label: semanticsLabel,
child: ExcludeSemantics(
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimatedSwitcher(
duration: const Duration(milliseconds: 150),
@@ -33,7 +31,7 @@ class WalletInfoBar extends StatelessWidget {
child: hardwareWalletIcon == null
? const SizedBox.shrink(key: ValueKey("empty"))
: Padding(
padding: const EdgeInsets.only(right: 4),
padding: const EdgeInsets.only(right: 8),
child: CakeImageWidget(
imageUrl: hardwareWalletIcon!,
key: const ValueKey("hardware_wallet_icon"),
+8 -9
View File
@@ -4,7 +4,6 @@ import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart';
import "package:cake_wallet/core/auth_service.dart";
import 'package:cake_wallet/core/new_wallet_arguments.dart';
import 'package:cake_wallet/new-ui/new_dashboard.dart';
import "package:cake_wallet/new-ui/pages/account_customizer.dart";
import 'package:cake_wallet/new-ui/pages/about_page.dart';
import 'package:cake_wallet/new-ui/pages/bridge/bridge_history_page.dart';
import 'package:cake_wallet/new-ui/pages/bridge/bridge_network_page.dart';
@@ -51,6 +50,7 @@ import 'package:cake_wallet/src/screens/dev/monero_background_sync.dart';
import 'package:cake_wallet/src/screens/dev/moneroc_cache_debug.dart';
import 'package:cake_wallet/src/screens/dev/moneroc_call_profiler.dart';
import 'package:cake_wallet/src/screens/dev/network_requests.dart';
import 'package:cake_wallet/new-ui/pages/wallet_accounts_page.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:cake_wallet/utils/feature_flag.dart';
import 'package:cake_wallet/src/screens/dev/qr_tools_page.dart';
@@ -129,8 +129,8 @@ 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/monero_account_list/account_list_item.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';
import 'package:cake_wallet/view_model/seed_settings_view_model.dart';
@@ -617,13 +617,6 @@ Route<dynamic> createRoute(RouteSettings settings) {
param1: args?['editingNode'] as Node?, param2: args?['isSelected'] as bool?);
return CupertinoPageRoute<void>(builder: (_) => page);
case Routes.accountCustomizer:
return handleRouteWithPlatformAwareness(
(_) => getIt.get<AccountCustomizer>(
param1: settings.arguments! as DashboardViewModel,
),
);
case Routes.addressBook:
return handleRouteWithPlatformAwareness(
(context) => getIt.get<ContactListPage>(),
@@ -853,6 +846,12 @@ Route<dynamic> createRoute(RouteSettings settings) {
case Routes.manageNodes:
return MaterialPageRoute<void>(builder: (_) => getIt.get<ManageNodesPage>(param1: false));
case Routes.walletAccountsPage:
final dashboardViewModel = settings.arguments as DashboardViewModel;
return handleRouteWithPlatformAwareness(
(_) => getIt.get<WalletAccountsPage>(param1: dashboardViewModel),
);
case Routes.managePowNodes:
return MaterialPageRoute<void>(builder: (_) => getIt.get<ManageNodesPage>(param1: true));
+1 -2
View File
@@ -31,8 +31,6 @@ class Routes {
static const newPowNode = '/new_pow_node_list';
static const login = '/login';
static const splash = '/splash';
static const accountCreation = '/account_new';
static const accountCustomizer = "/account_customizer";
static const nanoAccountCreation = '/nano_account_new';
static const addressBook = '/address_book';
static const pickerAddressBook = '/picker_address_book';
@@ -112,6 +110,7 @@ class Routes {
static const setup2faInfoPage = '/setup_2fa_info_page';
static const homeSettings = '/home_settings';
static const editToken = '/edit_token';
static const walletAccountsPage = '/wallet_accounts_page';
static const manageNodes = '/manage_nodes';
static const managePowNodes = '/manage_pow_nodes';
static const walletConnectConnectionsListing = '/wallet-connect-connections-listing';
@@ -0,0 +1,149 @@
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/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,
});
final WalletAccountEditOrCreateViewModel viewModel;
@override
State<AccountCreationModal> createState() => _AccountCreationModalState();
}
class _AccountCreationModalState extends State<AccountCreationModal> {
static const int maxAccountNameLength = 25;
final TextEditingController _controller = TextEditingController();
@override
void initState() {
super.initState();
_controller.text = widget.viewModel.label;
_controller.addListener(() {
widget.viewModel.label = _controller.text;
});
}
@override
void dispose() {
_controller.dispose();
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(
decoration: BoxDecoration(
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),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const ModalGrabHandle(),
ModalTopBar(
title: widget.viewModel.isEdit
? S.of(context).edit_account
: S.of(context).create_account,
),
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),
),
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),
),
),
),
)
],
),
),
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,
),
),
),
const SizedBox(),
],
),
)
],
),
),
),
);
}
@@ -159,7 +159,7 @@ abstract class BalanceViewModelBase with Store {
bool get isEVMCompatible => isEVMCompatibleChain(wallet.type);
@computed
bool get hasAccounts => [WalletType.monero, WalletType.wownero].contains(wallet.type);
bool get hasAccounts => wallet.hasAccountsSupport;
@computed
SortBalanceBy get sortBalanceBy => settingsStore.sortBalanceBy;
@@ -5,6 +5,9 @@ import 'dart:io' show Platform;
import 'package:cake_wallet/.secrets.g.dart' as secrets;
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/core/key_service.dart';
import 'package:cake_wallet/view_model/wallet_account_list/wallet_account_list_view_model.dart';
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_style_settings.dart";
import 'package:cake_wallet/core/trade_monitor.dart';
@@ -77,6 +80,7 @@ class DashboardViewModel = DashboardViewModelBase with _$DashboardViewModel;
abstract class DashboardViewModelBase with Store {
DashboardViewModelBase(
{required this.balanceViewModel,
required this.accountListViewModelFactory,
required this.tradeMonitor,
required this.appStore,
required this.tradesStore,
@@ -106,7 +110,7 @@ abstract class DashboardViewModelBase with Store {
showDecredInfoCard = wallet.type == WalletType.decred &&
(sharedPreferences.getBool(PreferencesKey.showDecredInfoCard) ?? true);
showSeedBackupReminder = wallet.walletInfo.showSeedBackupReminder;
isMultiAccountsEnabled = wallet.walletInfo.isMultiAccountsEnabled == true;
name = wallet.name;
type = wallet.type;
isShowFirstYatIntroduction = false;
@@ -115,6 +119,7 @@ abstract class DashboardViewModelBase with Store {
unawaited(isBackgroundSyncEnabled());
unawaited(isBatteryOptimizationEnabled());
unawaited(_loadConstraints());
accountListViewModel = accountListViewModelFactory();
final _wallet = wallet;
loadFilterItems();
@@ -122,8 +127,7 @@ abstract class DashboardViewModelBase with Store {
if (_wallet.type == WalletType.monero) {
subname = monero!.getCurrentAccount(_wallet).label;
_onMoneroAccountChangeReaction = reaction(
(_) => monero!.getMoneroWalletDetails(wallet).account,
_onAccountChangeReaction = reaction((_) => monero!.getMoneroWalletDetails(wallet).account,
(Account account) => _onMoneroAccountChange(_wallet));
_onMoneroBalanceChangeReaction = reaction(
@@ -151,9 +155,9 @@ abstract class DashboardViewModelBase with Store {
} else if (_wallet.type == WalletType.wownero) {
subname = wow.wownero!.getCurrentAccount(_wallet).label;
_onMoneroAccountChangeReaction = reaction(
_onAccountChangeReaction = reaction(
(_) => wow.wownero!.getWowneroWalletDetails(wallet).account,
(wow.Account account) => _onMoneroAccountChange(_wallet));
(Account account) => _onMoneroAccountChange(_wallet));
_onMoneroBalanceChangeReaction = reaction(
(_) => wow.wownero!.getWowneroWalletDetails(wallet).balance,
@@ -179,21 +183,12 @@ abstract class DashboardViewModelBase with Store {
),
);
} else {
final sortedTransactions = [...wallet.transactionHistory.transactions.values];
sortedTransactions.sort((a, b) => a.date.compareTo(b.date));
transactions = ObservableList.of(
sortedTransactions.map(
(transaction) => TransactionListItem(
transaction: transaction,
balanceViewModel: balanceViewModel,
appStore: appStore,
key: ValueKey('${_wallet.type.name}_transaction_history_item_${transaction.id}_key'),
),
),
);
subname = "";
_reloadTransactions();
}
_setupBitcoinAccountChangeReaction(_wallet);
// TODO: nano sub-account generation is disabled:
// if (_wallet.type == WalletType.nano || _wallet.type == WalletType.banano) {
// subname = nano!.getCurrentAccount(_wallet).label;
@@ -201,7 +196,9 @@ abstract class DashboardViewModelBase with Store {
_walletChangeDisposer?.reaction.dispose();
_walletChangeDisposer = reaction((_) => appStore.wallet, (wallet) {
accountListViewModel = accountListViewModelFactory();
_onWalletChange(wallet);
resetLightningMode();
_checkMweb();
loadCardDesigns();
showDecredInfoCard = wallet?.type == WalletType.decred &&
@@ -362,8 +359,14 @@ abstract class DashboardViewModelBase with Store {
transactions.clear();
final filteredTransactions = wallet.type == WalletType.bitcoin
? bitcoin!.getCurrentAccountBitcoinTransactions(wallet)
: wallet.transactionHistory.transactions.values.toList();
filteredTransactions.sort((a, b) => a.date.compareTo(b.date));
transactions.addAll(
wallet.transactionHistory.transactions.values.map(
filteredTransactions.map(
(transaction) => TransactionListItem(
transaction: transaction,
balanceViewModel: balanceViewModel,
@@ -425,60 +428,67 @@ abstract class DashboardViewModelBase with Store {
@action
Future<void> loadCardDesigns() async {
final accountStyleSettings =
final walletCardStyleSettings =
await BalanceCardStyleSettings.getAll(wallet.walletInfo.internalId);
final btcAccounts =
wallet.type == WalletType.bitcoin ? await wallet.walletInfo.getAccounts() : null;
final lightningCardIndex = btcAccounts?.length;
late final int numAccounts;
int numAccounts = 1;
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) {
// bitcoin and lightning
numAccounts = 2;
} else {
numAccounts = 1;
numAccounts = btcAccounts!.length + 1; // adding 1 for the lightning account
}
final usesAccountIndices = wallet.type == WalletType.bitcoin || balanceViewModel.hasAccounts;
cardDesigns.clear();
Map<int, int> newOrder = {};
final newOrder = <int, int>{};
final orderableCount = (wallet.type == WalletType.bitcoin) ? numAccounts - 1 : numAccounts;
for (int i = 0; i < numAccounts; i++) {
late final int index;
if (balanceViewModel.hasAccounts) {
final isLightning = wallet.type == WalletType.bitcoin && i == lightningCardIndex;
if (isLightning) {
index = -2;
} else if (usesAccountIndices) {
index = i;
} else if (wallet.type == WalletType.bitcoin && i == 1) {
index = 0;
} else {
index = -1;
}
final setting = accountStyleSettings.where((e) => e.accountIndex == index).firstOrNull;
final setting = walletCardStyleSettings.where((e) => e.accountIndex == index).firstOrNull ??
(index == 0 && wallet.type != WalletType.bitcoin
? walletCardStyleSettings.where((e) => e.accountIndex == -1).firstOrNull
: null);
late final CryptoCurrency curr;
if (wallet.type == WalletType.bitcoin && i == 1) {
curr = CryptoCurrency.btcln;
} else {
curr = wallet.currency;
}
final curr = isLightning ? CryptoCurrency.btcln : wallet.currency;
cardDesigns.add(CardDesign.fromStyleSettings(setting, curr));
if (setting?.cardOrder != null) {
newOrder[setting!.cardOrder] = i;
if (isLightning) {
continue;
}
if (setting?.cardOrder != null &&
setting!.cardOrder >= 0 &&
setting.cardOrder < orderableCount) {
newOrder[setting.cardOrder] = i;
}
}
// making sure ALL accounts have numbers, even the ones that existed before this feature was a thing
for (int i = 0; i < numAccounts; i++) {
if (!newOrder.containsKey(i) && !(wallet.type != WalletType.bitcoin && i == 1)) {
for (int i = 0; i < orderableCount; i++) {
if (!newOrder.containsValue(i)) {
int free = 0;
while (newOrder.containsValue(free)) {
while (newOrder.containsKey(free)) {
free++;
}
if (wallet.type == WalletType.bitcoin) {
newOrder[free] = 0;
} else {
newOrder[free] = i;
}
newOrder[free] = i;
}
}
cardOrder = newOrder.asObservable();
@@ -496,19 +506,22 @@ abstract class DashboardViewModelBase with Store {
: wallet.type == WalletType.wownero
? wow.wownero!.getCurrentAccount(wallet).id
: null;
final List<TransactionInfo> relevantTxs = [];
for (final tx in appStore.wallet!.transactionHistory.transactions.values) {
bool isRelevant = true;
if (wallet.type == WalletType.monero) {
isRelevant = monero!.getTransactionInfoAccountId(tx) == currentAccountId;
} else if (wallet.type == WalletType.wownero) {
isRelevant = wow.wownero!.getTransactionInfoAccountId(tx) == currentAccountId;
}
final allTxs = appStore.wallet!.transactionHistory.transactions.values;
final List<TransactionInfo> relevantTxs;
if (isRelevant) {
relevantTxs.add(tx);
}
if (wallet.type == WalletType.monero) {
relevantTxs = allTxs
.where((tx) => monero!.getTransactionInfoAccountId(tx) == currentAccountId)
.toList();
} else if (wallet.type == WalletType.wownero) {
relevantTxs = allTxs
.where((tx) => wow.wownero!.getTransactionInfoAccountId(tx) == currentAccountId)
.toList();
} else if (wallet.type == WalletType.bitcoin) {
relevantTxs = bitcoin!.getCurrentAccountBitcoinTransactions(wallet);
} else {
relevantTxs = allTxs.toList();
}
// printV("Transaction disposer callback (relevantTxs: ${relevantTxs.length} current: ${transactions.length})");
@@ -600,6 +613,12 @@ abstract class DashboardViewModelBase with Store {
@observable
ObservableMap<int, int> cardOrder;
@observable
bool lightningMode = false;
@observable
WalletAccountListViewModel? accountListViewModel;
@computed
bool get isDarkTheme => appStore.themeStore.currentTheme.isDark;
@@ -896,6 +915,42 @@ abstract class DashboardViewModelBase with Store {
@observable
late bool showSeedBackupReminder;
@computed
bool get canToggleMultiAccounts => wallet.canToggleMultiAccounts;
@observable
late bool isMultiAccountsEnabled;
@computed
List<AccountListItem> get visibleAccounts {
final all = accountListViewModel?.accounts;
if (all == null || all.isEmpty) return const <AccountListItem>[];
if (isMultiAccountsEnabled) return all.toList(growable: false);
final primary = all.where((a) => a.id == 0).firstOrNull ?? all.first;
return <AccountListItem>[primary];
}
@action
Future<void> setMultiAccountsEnabled(bool value) async {
if (!canToggleMultiAccounts) {
return;
}
if (!value) {
await _selectPrimaryAccount();
}
wallet.walletInfo.isMultiAccountsEnabled = value;
await wallet.walletInfo.save();
isMultiAccountsEnabled = value;
await accountListViewModel?.reload();
await loadCardDesigns();
if (value) {
unawaited(wallet.startSync());
}
}
@computed
bool get hasBalance => wallet.balance.values.any(
(balance) =>
@@ -1161,6 +1216,8 @@ abstract class DashboardViewModelBase with Store {
bitcoin!.updatePayjoinState(wallet, true);
}
final WalletAccountListViewModel? Function() accountListViewModelFactory;
BalanceViewModel balanceViewModel;
TradeMonitor tradeMonitor;
@@ -1215,7 +1272,7 @@ abstract class DashboardViewModelBase with Store {
@computed
bool get isEnabledBulletinAction => !settingsStore.disableBulletin;
ReactionDisposer? _onMoneroAccountChangeReaction;
ReactionDisposer? _onAccountChangeReaction;
ReactionDisposer? _onMoneroBalanceChangeReaction;
@@ -1289,6 +1346,31 @@ abstract class DashboardViewModelBase with Store {
}
}
@action
void toggleLightningMode() {
lightningMode = !lightningMode;
// Lightning belongs to the primary account only
if (lightningMode) {
unawaited(_selectPrimaryAccount());
}
}
Future<void> _selectPrimaryAccount() async {
if (wallet.type != WalletType.bitcoin) return;
final vm = accountListViewModel;
if (vm == null || vm.selectedAccount?.id == 0) return;
final primary = vm.accounts.where((a) => a.id == 0).firstOrNull;
if (primary != null) {
await vm.select(primary);
}
}
@action
void resetLightningMode() => lightningMode = false;
@action
void _onWalletChange(
WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo>? wallet) {
@@ -1299,16 +1381,19 @@ abstract class DashboardViewModelBase with Store {
this.wallet = wallet;
type = wallet.type;
name = wallet.name;
isMultiAccountsEnabled = wallet.walletInfo.isMultiAccountsEnabled == true;
_onAccountChangeReaction?.reaction.dispose();
_onAccountChangeReaction = null;
loadFilterItems();
if (wallet.type == WalletType.monero) {
subname = monero!.getCurrentAccount(wallet).label;
_onMoneroAccountChangeReaction?.reaction.dispose();
_onAccountChangeReaction?.reaction.dispose();
_onMoneroBalanceChangeReaction?.reaction.dispose();
_onMoneroAccountChangeReaction = reaction(
(_) => monero!.getMoneroWalletDetails(wallet).account,
_onAccountChangeReaction = reaction((_) => monero!.getMoneroWalletDetails(wallet).account,
(Account account) => _onMoneroAccountChange(wallet));
_onMoneroBalanceChangeReaction = reaction(
@@ -1319,12 +1404,12 @@ abstract class DashboardViewModelBase with Store {
} else if (wallet.type == WalletType.wownero) {
subname = wow.wownero!.getCurrentAccount(wallet).label;
_onMoneroAccountChangeReaction?.reaction.dispose();
_onAccountChangeReaction?.reaction.dispose();
_onMoneroBalanceChangeReaction?.reaction.dispose();
_onMoneroAccountChangeReaction = reaction(
_onAccountChangeReaction = reaction(
(_) => wow.wownero!.getWowneroWalletDetails(wallet).account,
(wow.Account account) => _onMoneroAccountChange(wallet));
(Account account) => _onMoneroAccountChange(wallet));
_onMoneroBalanceChangeReaction = reaction(
(_) => wow.wownero!.getWowneroWalletDetails(wallet).balance,
@@ -1335,7 +1420,7 @@ abstract class DashboardViewModelBase with Store {
// FIX-ME: Check for side effects
// subname = null;
subname = '';
_setupBitcoinAccountChangeReaction(wallet);
_reloadTransactions();
}
@@ -1378,6 +1463,21 @@ abstract class DashboardViewModelBase with Store {
}, _transactionDisposerCallback, delay: 300);
}
void _setupBitcoinAccountChangeReaction(WalletBase wallet) {
if (wallet.type != WalletType.bitcoin) {
return;
}
_onAccountChangeReaction?.reaction.dispose();
_onAccountChangeReaction = reaction(
(_) => accountListViewModel?.selectedAccount?.id,
(_) {
_reloadTransactions();
},
fireImmediately: true,
);
}
@action
void _onMoneroAccountChange(WalletBase wallet) {
if (wallet.type == WalletType.monero) {
@@ -55,10 +55,6 @@ class TransactionListItem extends ActionListItem with Keyable {
}
String get formattedTitle {
if (balanceViewModel.wallet.type == WalletType.bitcoin &&
transaction.additionalInfo['hasMissingInputTx'] == true) {
return 'Transaction has missing data';
}
if (transaction.additionalInfo['isIronwoodMigration'] == true) {
return 'Migration';
@@ -0,0 +1,9 @@
import 'package:cake_wallet/core/execution_state.dart';
abstract class WalletAccountEditOrCreateViewModel {
bool get isEdit;
ExecutionState get state;
String get label;
set label(String value);
Future<void> save();
}
@@ -0,0 +1,92 @@
import 'dart:math';
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/core/execution_state.dart';
import 'package:cake_wallet/view_model/wallet_account_list/account_edit_or_create_view_model.dart';
import 'package:cw_core/balance_card_style_settings.dart';
import 'package:cw_core/card_design.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:flutter/material.dart';
import 'package:mobx/mobx.dart';
part 'bitcoin_account_edit_or_create_view_model.g.dart';
class BitcoinAccountEditOrCreateViewModel = BitcoinAccountEditOrCreateViewModelBase
with _$BitcoinAccountEditOrCreateViewModel;
abstract class BitcoinAccountEditOrCreateViewModelBase
with Store
implements WalletAccountEditOrCreateViewModel {
BitcoinAccountEditOrCreateViewModelBase({required WalletBase wallet})
: state = InitialExecutionState(),
label = "",
_wallet = wallet;
@override
bool get isEdit => false;
@override
@observable
ExecutionState state;
@override
@observable
String label;
final WalletBase _wallet;
Future<List<Gradient>> _getUsableCardGradients() async {
final List<Gradient> ret = List<Gradient>.from(CardDesign.allGradients);
final designs = (await BalanceCardStyleSettings.getAll(_wallet.walletInfo.internalId))
.map((item) => CardDesign.fromStyleSettings(item, _wallet.currency));
for (final design in designs) {
ret.remove(design.gradient);
}
return ret.isNotEmpty ? ret : CardDesign.allGradients;
}
Future<void> _saveRandomCardDesign(int accountIndex) async {
final gradients = await _getUsableCardGradients();
final accounts = await _wallet.walletInfo.getAccounts();
await BalanceCardStyleSettings.fromCardDesign(
walletInfoId: _wallet.walletInfo.internalId,
accountIndex: accountIndex,
cardOrder: accounts.length - 1,
design: CardDesign.specialDesignsForCurrencies[_wallet.currency]!
.withGradient(gradients[Random().nextInt(gradients.length)]))
.insert();
}
@override
Future<void> save() async {
try {
state = IsExecutingState();
final accountIndex = await _createAccount(label);
await _saveRandomCardDesign(accountIndex);
await _wallet.save();
state = ExecutedSuccessfullyState();
} catch (e) {
state = FailureState(e.toString());
}
}
Future<int> _createAccount(String label) async {
final accounts = await _wallet.walletInfo.getAccounts();
final nextIndex = accounts.isEmpty
? 0
: accounts.map((e) => e.accountIndex).reduce((a, b) => a > b ? a : b) + 1;
await _wallet.walletInfo.addAccount(
accountIndex: nextIndex,
label: label.isEmpty ? "Account $nextIndex" : label,
);
await bitcoin!.setCurrentAccount(_wallet, nextIndex);
return nextIndex;
}
}
@@ -0,0 +1,130 @@
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';
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) {
unawaited(_loadAccounts());
reaction(
(_) => bitcoin!.accountBalancesSnapshot(_wallet),
(snapshot) => unawaited(_refreshBalances(snapshot)),
equals: const MapEquality<int, Object>().equals,
);
reaction(
(_) => settingsStore.balanceDisplayMode,
(_) => unawaited(_recomputeBalanceStrings()),
);
}
final SettingsStore settingsStore;
final WalletBase _wallet;
List<WalletInfoAccount>? _cachedStoredAccounts;
Future<void>? _loadAccountsFuture;
CryptoCurrency get currency => _wallet.currency;
@override
@observable
ObservableList<AccountListItem> accounts = ObservableList<AccountListItem>();
@override
@observable
AccountListItem? selectedAccount;
@override
@action
Future<void> select(AccountListItem account) async {
await bitcoin!.setCurrentAccount(_wallet, account.id);
runInAction(() => selectedAccount = account);
await reload();
}
@action
Future<void> _loadAccounts() => _runLoad(() => _wallet.walletInfo.getAccounts());
@action
Future<void> _refreshBalances(Map<int, Object> balancesSnapshot) {
final cached = _cachedStoredAccounts;
final hasUnknownAccount = cached == null ||
balancesSnapshot.keys
.any((accountIndex) => !cached.any((a) => a.accountIndex == accountIndex));
if (hasUnknownAccount) return _loadAccounts();
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) {
return inProgress;
}
final future = _applyAccounts(fetch);
_loadAccountsFuture = future;
return future.whenComplete(() => _loadAccountsFuture = null);
}
Future<void> _applyAccounts(Future<List<WalletInfoAccount>> Function() fetch) async {
final storedAccounts = await fetch();
_cachedStoredAccounts = storedAccounts;
final currentAccountIndex = _wallet.walletInfo.currentAccountIndex;
final items = storedAccounts
.map((account) => AccountListItem(
id: account.accountIndex,
label: account.label,
balance: _balanceForAccount(account.accountIndex),
isSelected: account.accountIndex == currentAccountIndex,
))
.toList();
runInAction(() {
accounts = ObservableList<AccountListItem>.of(items);
selectedAccount = items.isEmpty
? null
: items.firstWhere(
(account) => account.isSelected,
orElse: () => items.first,
);
});
}
@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);
}
}
@@ -1,5 +1,6 @@
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_style_settings.dart';
import 'package:cw_core/card_design.dart';
@@ -9,14 +10,16 @@ import 'package:flutter/material.dart';
import 'package:mobx/mobx.dart';
import 'package:cake_wallet/core/execution_state.dart';
import 'package:cake_wallet/monero/monero.dart';
import 'package:cake_wallet/view_model/monero_account_list/account_list_item.dart';
import 'package:cake_wallet/view_model/wallet_account_list/account_list_item.dart';
part 'monero_account_edit_or_create_view_model.g.dart';
class MoneroAccountEditOrCreateViewModel = MoneroAccountEditOrCreateViewModelBase
with _$MoneroAccountEditOrCreateViewModel;
abstract class MoneroAccountEditOrCreateViewModelBase with Store {
abstract class MoneroAccountEditOrCreateViewModelBase
with Store
implements WalletAccountEditOrCreateViewModel {
MoneroAccountEditOrCreateViewModelBase(this._moneroAccountList, this._wowneroAccountList,
{required WalletBase wallet, AccountListItem? accountListItem})
: state = InitialExecutionState(),
@@ -61,49 +64,51 @@ abstract class MoneroAccountEditOrCreateViewModelBase with Store {
}
Future<void> save() async {
await _saveRandomCardDesign();
if (_wallet.type == WalletType.monero) {
await saveMonero();
}
try {
state = IsExecutingState();
if (_wallet.type == WalletType.wownero) {
await saveWownero();
if (!isEdit) await _saveRandomCardDesign();
if (_wallet.type == WalletType.monero) {
await saveMonero();
} else if (_wallet.type == WalletType.wownero) {
await saveWownero();
}
await _wallet.save();
state = ExecutedSuccessfullyState();
} catch (e) {
state = FailureState(e.toString());
}
}
Future<void> saveMonero() async {
try {
state = IsExecutingState();
if (_accountListItem != null) {
await _moneroAccountList.setLabelAccount(_wallet,
accountIndex: _accountListItem.id, label: label);
} else {
await _moneroAccountList.addAccount(_wallet, label: label);
}
await _wallet.save();
state = ExecutedSuccessfullyState();
} catch (e) {
state = FailureState(e.toString());
if (_accountListItem != null) {
await _moneroAccountList.setLabelAccount(
_wallet,
accountIndex: _accountListItem.id,
label: label,
);
} else {
await _moneroAccountList.addAccount(
_wallet,
label: label,
);
}
}
Future<void> saveWownero() async {
try {
state = IsExecutingState();
if (_accountListItem != null) {
await _wowneroAccountList?.setLabelAccount(_wallet,
accountIndex: _accountListItem.id, label: label);
} else {
await _wowneroAccountList?.addAccount(_wallet, label: label);
}
await _wallet.save();
state = ExecutedSuccessfullyState();
} catch (e) {
state = FailureState(e.toString());
if (_accountListItem != null) {
await _wowneroAccountList?.setLabelAccount(
_wallet,
accountIndex: _accountListItem.id,
label: label,
);
} else {
await _wowneroAccountList?.addAccount(
_wallet,
label: label,
);
}
}
}
@@ -1,29 +1,25 @@
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/monero_account_list/account_list_item.dart';
import 'package:cake_wallet/view_model/wallet_account_list/account_list_item.dart';
import 'package:cake_wallet/monero/monero.dart';
part 'monero_account_list_view_model.g.dart';
class MoneroAccountListViewModel = MoneroAccountListViewModelBase with _$MoneroAccountListViewModel;
abstract class MoneroAccountListViewModelBase with Store {
MoneroAccountListViewModelBase(this._wallet, this.settingsStore) : scrollOffsetFromTop = 0;
abstract class MoneroAccountListViewModelBase with Store implements WalletAccountListViewModel {
MoneroAccountListViewModelBase(this._wallet, this.settingsStore);
final SettingsStore settingsStore;
@observable
double scrollOffsetFromTop;
@action
void setScrollOffsetFromTop(double scrollOffsetFromTop) {
this.scrollOffsetFromTop = scrollOffsetFromTop;
}
@override
AccountListItem? get selectedAccount => selected;
CryptoCurrency get currency => _wallet.currency;
@@ -65,7 +61,11 @@ abstract class MoneroAccountListViewModelBase with Store {
final WalletBase _wallet;
void select(AccountListItem item) {
@override
Future<void> reload() async {}
@override
Future<void> select(AccountListItem item) async {
if (_wallet.type == WalletType.monero) {
monero!.setCurrentAccount(
_wallet,
@@ -0,0 +1,14 @@
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;
AccountListItem? get selectedAccount;
CryptoCurrency get currency;
Future<void> select(AccountListItem account);
Future<void> reload();
}