mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-28 06:28:41 +01:00
CW-1513/CW-1610: Add account archival and UI improvements (#3600)
* add ability to hide accounts * remove import * merge * merge * fix loading accounts when styleSettings are not saved yet * merge * auto-reformat * small fixes * proper reload on card unhide * block hiding last card * fixup * revert formatting-only changes * liner fixes * fix overflows * card layout as separate class to prevent index errors * fix mini card on addresses page * block opening customizers if wallet is syncing * switch to BlocProvider.value to prevent auto dispose * imports * fix fiat balance * Improve homescreen layout and account access * Add homescreen layout regression tests * improve account management w/ education + archival flows * reorganize wallet-specific settings * preserve account archival during dismissal * fix: align account visuals with design feedback * minor account & archival visual corrections * simplify wallet settings + account UI state * generalise education carousel & dismissal tracking * refactor account UI flows + minor improvements * minor refactors and improvement * pass account balances as Money and format at display * use fresh full balance for account archival check * refactor BalanceCard w/ MoneyText + cleanup * minor card_customizer cleanup * rework card customizer flow --------- Co-authored-by: Robert Malikowski <malikowskirobert@gmail.com> Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
This commit is contained in:
co-authored by
Robert Malikowski
Omar Hatem
parent
9f1f82fdd4
commit
20f1ceb651
@@ -0,0 +1,215 @@
|
||||
import "package:cake_wallet/core/key_service.dart";
|
||||
import "package:cake_wallet/core/trade_monitor.dart";
|
||||
import "package:cake_wallet/store/anonpay/anonpay_transactions_store.dart";
|
||||
import "package:cake_wallet/store/app_store.dart";
|
||||
import "package:cake_wallet/store/dashboard/order_filter_store.dart";
|
||||
import "package:cake_wallet/store/dashboard/orders_store.dart";
|
||||
import "package:cake_wallet/store/dashboard/payjoin_transactions_store.dart";
|
||||
import "package:cake_wallet/store/dashboard/trade_filter_store.dart";
|
||||
import "package:cake_wallet/store/dashboard/trades_store.dart";
|
||||
import "package:cake_wallet/store/dashboard/transaction_filter_store.dart";
|
||||
import "package:cake_wallet/store/settings_store.dart";
|
||||
import "package:cake_wallet/view_model/dashboard/balance_view_model.dart";
|
||||
import "package:cake_wallet/view_model/dashboard/dashboard_view_model.dart";
|
||||
import "package:cake_wallet/wownero/wownero.dart" as wow;
|
||||
import "package:cw_core/account.dart";
|
||||
import "package:cw_core/balance.dart";
|
||||
import "package:cw_core/balance_card_style_settings.dart";
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
import "package:cw_core/db/sqlite.dart" as sqlite;
|
||||
import "package:cw_core/transaction_history.dart";
|
||||
import "package:cw_core/transaction_info.dart";
|
||||
import "package:cw_core/wallet_base.dart";
|
||||
import "package:cw_core/wallet_info.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
import "package:mobx/mobx.dart" show ObservableList, ObservableMap;
|
||||
import "package:mocktail/mocktail.dart";
|
||||
import "package:shared_preferences/shared_preferences.dart";
|
||||
import "package:sqflite_common_ffi/sqflite_ffi.dart";
|
||||
|
||||
class _MockBalanceViewModel extends Mock implements BalanceViewModel {}
|
||||
|
||||
class _MockTradeMonitor extends Mock implements TradeMonitor {}
|
||||
|
||||
class _MockAppStore extends Mock implements AppStore {}
|
||||
|
||||
class _MockTradesStore extends Mock implements TradesStore {}
|
||||
|
||||
class _MockTradeFilterStore extends Mock implements TradeFilterStore {}
|
||||
|
||||
class _MockOrderFilterStore extends Mock implements OrderFilterStore {}
|
||||
|
||||
class _MockTransactionFilterStore extends Mock implements TransactionFilterStore {}
|
||||
|
||||
class _MockSettingsStore extends Mock implements SettingsStore {}
|
||||
|
||||
class _MockOrdersStore extends Mock implements OrdersStore {}
|
||||
|
||||
class _MockAnonpayTransactionsStore extends Mock implements AnonpayTransactionsStore {}
|
||||
|
||||
class _MockPayjoinTransactionsStore extends Mock implements PayjoinTransactionsStore {}
|
||||
|
||||
class _MockKeyService extends Mock implements KeyService {}
|
||||
|
||||
class _MockWallet extends Mock
|
||||
implements WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> {}
|
||||
|
||||
class _MockWalletInfo extends Mock implements WalletInfo {}
|
||||
|
||||
class _MockTransactionHistory extends Mock implements TransactionHistoryBase<TransactionInfo> {}
|
||||
|
||||
class _MockWownero extends Mock implements wow.Wownero {}
|
||||
|
||||
class _MockWowneroAccountList extends Mock implements wow.WowneroAccountList {}
|
||||
|
||||
class _MockWowneroWalletDetails extends Mock implements wow.WowneroWalletDetails {}
|
||||
|
||||
class _MockWowneroBalance extends Mock implements wow.WowneroBalance {}
|
||||
|
||||
class _DashboardViewModelProbe extends DashboardViewModelBase {
|
||||
_DashboardViewModelProbe({
|
||||
required super.balanceViewModel,
|
||||
required super.accountListViewModelFactory,
|
||||
required super.tradeMonitor,
|
||||
required super.appStore,
|
||||
required super.tradesStore,
|
||||
required super.tradeFilterStore,
|
||||
required super.orderFilterStore,
|
||||
required super.transactionFilterStore,
|
||||
required super.settingsStore,
|
||||
required super.ordersStore,
|
||||
required super.anonpayTransactionsStore,
|
||||
required super.payjoinTransactionsStore,
|
||||
required super.sharedPreferences,
|
||||
required super.keyService,
|
||||
});
|
||||
|
||||
bool _loadRealCards = false;
|
||||
|
||||
void enableRealCardLoading() => _loadRealCards = true;
|
||||
|
||||
@override
|
||||
void loadFilterItems() {}
|
||||
|
||||
@override
|
||||
Future<void> loadCardDesigns() => _loadRealCards ? super.loadCardDesigns() : Future<void>.value();
|
||||
|
||||
@override
|
||||
Future<bool> isBackgroundSyncEnabled() async => false;
|
||||
|
||||
@override
|
||||
Future<bool> isBatteryOptimizationEnabled() async => false;
|
||||
}
|
||||
|
||||
void main() {
|
||||
late DatabaseFactory? originalDatabaseFactory;
|
||||
late Database? originalDatabase;
|
||||
late wow.Wownero? originalWownero;
|
||||
|
||||
setUpAll(() {
|
||||
originalDatabaseFactory = databaseFactoryOrNull;
|
||||
sqfliteFfiInit();
|
||||
databaseFactory = databaseFactoryFfi;
|
||||
});
|
||||
|
||||
tearDownAll(() {
|
||||
databaseFactoryOrNull = originalDatabaseFactory;
|
||||
});
|
||||
|
||||
setUp(() async {
|
||||
originalDatabase = sqlite.db;
|
||||
originalWownero = wow.wownero;
|
||||
sqlite.db = await databaseFactory.openDatabase(inMemoryDatabasePath);
|
||||
await sqlite.db!.execute("""
|
||||
CREATE TABLE ${BalanceCardStyleSettings.tableName} (
|
||||
walletInfoId INTEGER,
|
||||
accountIndex INTEGER DEFAULT -1,
|
||||
gradientIndex INTEGER DEFAULT -1,
|
||||
useSpecialDesign BOOLEAN DEFAULT FALSE,
|
||||
hidden BOOLEAN DEFAULT FALSE,
|
||||
backgroundImagePath TEXT DEFAULT "",
|
||||
iconStyleIndex INTEGER DEFAULT 0,
|
||||
isGradientOnly BOOLEAN DEFAULT FALSE,
|
||||
cardOrder INTEGER DEFAULT 0,
|
||||
PRIMARY KEY (walletInfoId, accountIndex)
|
||||
)
|
||||
""");
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
wow.wownero = originalWownero;
|
||||
await sqlite.db?.close();
|
||||
sqlite.db = originalDatabase;
|
||||
});
|
||||
|
||||
test("Wownero keeps every account card and maps the selected account", () async {
|
||||
SharedPreferences.setMockInitialValues(<String, Object>{});
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
final wallet = _MockWallet();
|
||||
final walletInfo = _MockWalletInfo();
|
||||
final transactionHistory = _MockTransactionHistory();
|
||||
final appStore = _MockAppStore();
|
||||
final settingsStore = _MockSettingsStore();
|
||||
final tradesStore = _MockTradesStore();
|
||||
final tradeMonitor = _MockTradeMonitor();
|
||||
final wowneroAdapter = _MockWownero();
|
||||
final accountList = _MockWowneroAccountList();
|
||||
final walletDetails = _MockWowneroWalletDetails();
|
||||
final wowneroBalance = _MockWowneroBalance();
|
||||
final accounts = ObservableList.of(<Account>[
|
||||
Account(id: 0, label: "Primary", balance: "1.0"),
|
||||
Account(id: 1, label: "Savings", balance: "2.0"),
|
||||
]);
|
||||
|
||||
wow.wownero = wowneroAdapter;
|
||||
when(() => appStore.wallet).thenReturn(wallet);
|
||||
when(() => wallet.hasNativeAccounts).thenReturn(true);
|
||||
when(() => wallet.name).thenReturn("Wownero Wallet");
|
||||
when(() => wallet.id).thenReturn("wownero-wallet");
|
||||
when(() => wallet.type).thenReturn(WalletType.wownero);
|
||||
when(() => wallet.currency).thenReturn(CryptoCurrency.wow);
|
||||
when(() => wallet.walletInfo).thenReturn(walletInfo);
|
||||
when(() => walletInfo.internalId).thenReturn(42);
|
||||
when(() => walletInfo.showSeedBackupReminder).thenReturn(false);
|
||||
when(() => wallet.transactionHistory).thenReturn(transactionHistory);
|
||||
when(() => transactionHistory.transactions)
|
||||
.thenReturn(ObservableMap<String, TransactionInfo>());
|
||||
when(() => wowneroAdapter.getAccountList(wallet)).thenReturn(accountList);
|
||||
when(() => accountList.accounts).thenReturn(accounts);
|
||||
when(() => wowneroAdapter.getCurrentAccount(wallet)).thenReturn(accounts[1]);
|
||||
when(() => wowneroAdapter.getWowneroWalletDetails(wallet)).thenReturn(walletDetails);
|
||||
when(() => walletDetails.account).thenReturn(accounts[1]);
|
||||
when(() => walletDetails.balance).thenReturn(wowneroBalance);
|
||||
when(() => settingsStore.mwebAlwaysScan).thenReturn(false);
|
||||
when(() => tradesStore.trades).thenReturn(const []);
|
||||
when(() => tradeMonitor.monitorActiveTrades(any())).thenReturn(null);
|
||||
|
||||
final viewModel = _DashboardViewModelProbe(
|
||||
balanceViewModel: _MockBalanceViewModel(),
|
||||
accountListViewModelFactory: () => null,
|
||||
tradeMonitor: tradeMonitor,
|
||||
appStore: appStore,
|
||||
tradesStore: tradesStore,
|
||||
tradeFilterStore: _MockTradeFilterStore(),
|
||||
orderFilterStore: _MockOrderFilterStore(),
|
||||
transactionFilterStore: _MockTransactionFilterStore(),
|
||||
settingsStore: settingsStore,
|
||||
ordersStore: _MockOrdersStore(),
|
||||
anonpayTransactionsStore: _MockAnonpayTransactionsStore(),
|
||||
payjoinTransactionsStore: _MockPayjoinTransactionsStore(),
|
||||
sharedPreferences: sharedPreferences,
|
||||
keyService: _MockKeyService(),
|
||||
);
|
||||
|
||||
expect(viewModel.cardAccountIndices, <int>[0, 1]);
|
||||
expect(viewModel.currentCardAccountIndex, 1);
|
||||
|
||||
viewModel.enableRealCardLoading();
|
||||
await viewModel.loadCardDesigns();
|
||||
|
||||
expect(viewModel.cardOrder, <int, int>{0: 0, 1: 1});
|
||||
expect(viewModel.cardDesigns, hasLength(2));
|
||||
expect(viewModel.currentCardDesign, same(viewModel.cardDesigns[1]));
|
||||
});
|
||||
}
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
import "package:cake_wallet/core/execution_state.dart";
|
||||
import "package:cake_wallet/monero/monero.dart" as xmr;
|
||||
import "package:cake_wallet/view_model/wallet_account_list/monero_account_list/monero_account_edit_or_create_view_model.dart";
|
||||
import "package:cake_wallet/wownero/wownero.dart" as wow;
|
||||
import "package:cw_core/account.dart";
|
||||
import "package:cw_core/balance.dart";
|
||||
import "package:cw_core/balance_card_style_settings.dart";
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
import "package:cw_core/db/sqlite.dart" as sqlite;
|
||||
import "package:cw_core/transaction_history.dart";
|
||||
import "package:cw_core/transaction_info.dart";
|
||||
import "package:cw_core/wallet_base.dart";
|
||||
import "package:cw_core/wallet_info.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
import "package:mobx/mobx.dart" show ObservableList;
|
||||
import "package:mocktail/mocktail.dart";
|
||||
import "package:sqflite_common_ffi/sqflite_ffi.dart";
|
||||
|
||||
class _MockWallet extends Mock
|
||||
implements WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> {}
|
||||
|
||||
class _MockWalletInfo extends Mock implements WalletInfo {}
|
||||
|
||||
class _MockMoneroAccountList extends Mock implements xmr.MoneroAccountList {}
|
||||
|
||||
class _MockWowneroAccountList extends Mock implements wow.WowneroAccountList {}
|
||||
|
||||
void main() {
|
||||
late DatabaseFactory? originalDatabaseFactory;
|
||||
late Database? originalDatabase;
|
||||
late _MockWallet wallet;
|
||||
late _MockWalletInfo walletInfo;
|
||||
|
||||
setUpAll(() {
|
||||
originalDatabaseFactory = databaseFactoryOrNull;
|
||||
sqfliteFfiInit();
|
||||
databaseFactory = databaseFactoryFfi;
|
||||
});
|
||||
|
||||
tearDownAll(() {
|
||||
databaseFactoryOrNull = originalDatabaseFactory;
|
||||
});
|
||||
|
||||
setUp(() async {
|
||||
originalDatabase = sqlite.db;
|
||||
|
||||
sqlite.db = await databaseFactory.openDatabase(inMemoryDatabasePath);
|
||||
await sqlite.db!.execute("""
|
||||
CREATE TABLE ${BalanceCardStyleSettings.tableName} (
|
||||
walletInfoId INTEGER,
|
||||
accountIndex INTEGER DEFAULT -1,
|
||||
gradientIndex INTEGER DEFAULT -1,
|
||||
hidden BOOLEAN DEFAULT FALSE,
|
||||
useSpecialDesign BOOLEAN DEFAULT FALSE,
|
||||
backgroundImagePath TEXT DEFAULT "",
|
||||
iconStyleIndex INTEGER DEFAULT 0,
|
||||
isGradientOnly BOOLEAN DEFAULT FALSE,
|
||||
cardOrder INTEGER DEFAULT 0,
|
||||
PRIMARY KEY (walletInfoId, accountIndex)
|
||||
)
|
||||
""");
|
||||
|
||||
wallet = _MockWallet();
|
||||
walletInfo = _MockWalletInfo();
|
||||
when(() => wallet.walletInfo).thenReturn(walletInfo);
|
||||
when(() => walletInfo.internalId).thenReturn(42);
|
||||
when(() => wallet.save()).thenAnswer((_) async {});
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await sqlite.db?.close();
|
||||
sqlite.db = originalDatabase;
|
||||
});
|
||||
|
||||
test("creates a Wownero account and stores its card design at the next Wownero index", () async {
|
||||
final accountList = _MockWowneroAccountList();
|
||||
final accounts = ObservableList<Account>.of([
|
||||
Account(id: 0, label: "Primary"),
|
||||
Account(id: 1, label: "Savings"),
|
||||
]);
|
||||
when(() => wallet.type).thenReturn(WalletType.wownero);
|
||||
when(() => wallet.currency).thenReturn(CryptoCurrency.wow);
|
||||
when(() => accountList.accounts).thenReturn(accounts);
|
||||
when(() => accountList.addAccount(wallet, label: "Spending")).thenAnswer((_) async {
|
||||
accounts.add(Account(id: 2, label: "Spending"));
|
||||
});
|
||||
|
||||
final viewModel = MoneroAccountEditOrCreateViewModel(
|
||||
null,
|
||||
accountList,
|
||||
wallet: wallet,
|
||||
)..label = "Spending";
|
||||
|
||||
await viewModel.save();
|
||||
|
||||
expect(viewModel.state, isA<ExecutedSuccessfullyState>());
|
||||
verify(() => accountList.addAccount(wallet, label: "Spending")).called(1);
|
||||
verify(() => wallet.save()).called(1);
|
||||
|
||||
final cardDesign = await BalanceCardStyleSettings.get(42, 2);
|
||||
expect(cardDesign, isNotNull);
|
||||
expect(cardDesign!.accountIndex, 2);
|
||||
expect(cardDesign.cardOrder, 2);
|
||||
});
|
||||
|
||||
test("creates a Monero account and stores its card design at the next Monero index", () async {
|
||||
final accountList = _MockMoneroAccountList();
|
||||
final accounts = ObservableList<Account>.of([
|
||||
Account(id: 0, label: "Primary"),
|
||||
]);
|
||||
when(() => wallet.type).thenReturn(WalletType.monero);
|
||||
when(() => wallet.currency).thenReturn(CryptoCurrency.xmr);
|
||||
when(() => accountList.accounts).thenReturn(accounts);
|
||||
when(() => accountList.addAccount(wallet, label: "Savings")).thenAnswer((_) async {
|
||||
accounts.add(Account(id: 1, label: "Savings"));
|
||||
});
|
||||
|
||||
final viewModel = MoneroAccountEditOrCreateViewModel(
|
||||
accountList,
|
||||
null,
|
||||
wallet: wallet,
|
||||
)..label = "Savings";
|
||||
|
||||
await viewModel.save();
|
||||
|
||||
expect(viewModel.state, isA<ExecutedSuccessfullyState>());
|
||||
verify(() => accountList.addAccount(wallet, label: "Savings")).called(1);
|
||||
verify(() => wallet.save()).called(1);
|
||||
|
||||
final cardDesign = await BalanceCardStyleSettings.get(42, 1);
|
||||
expect(cardDesign, isNotNull);
|
||||
expect(cardDesign!.accountIndex, 1);
|
||||
expect(cardDesign.cardOrder, 1);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import "package:cake_wallet/monero/monero.dart" as xmr;
|
||||
import "package:cake_wallet/view_model/wallet_account_list/monero_account_list/monero_account_list_view_model.dart";
|
||||
import "package:cake_wallet/wownero/wownero.dart" as wow;
|
||||
import "package:cw_core/account.dart";
|
||||
import "package:cw_core/amount/money.dart";
|
||||
import "package:cw_core/balance.dart";
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
import "package:cw_core/transaction_history.dart";
|
||||
import "package:cw_core/transaction_info.dart";
|
||||
import "package:cw_core/wallet_base.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
import "package:mobx/mobx.dart" show ObservableList;
|
||||
import "package:mocktail/mocktail.dart";
|
||||
|
||||
class _MockWallet extends Mock
|
||||
implements WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> {}
|
||||
|
||||
class _MockWownero extends Mock implements wow.Wownero {}
|
||||
|
||||
class _MockWowneroAccountList extends Mock implements wow.WowneroAccountList {}
|
||||
|
||||
void main() {
|
||||
late xmr.Monero? originalMonero;
|
||||
late wow.Wownero? originalWownero;
|
||||
late _MockWallet wallet;
|
||||
late _MockWownero wowneroAdapter;
|
||||
late _MockWowneroAccountList accountList;
|
||||
|
||||
setUp(() {
|
||||
originalMonero = xmr.monero;
|
||||
originalWownero = wow.wownero;
|
||||
|
||||
wallet = _MockWallet();
|
||||
wowneroAdapter = _MockWownero();
|
||||
accountList = _MockWowneroAccountList();
|
||||
|
||||
xmr.monero = null;
|
||||
wow.wownero = wowneroAdapter;
|
||||
|
||||
when(() => wallet.type).thenReturn(WalletType.wownero);
|
||||
when(() => wallet.currency).thenReturn(CryptoCurrency.wow);
|
||||
when(() => wowneroAdapter.getAccountList(wallet)).thenReturn(accountList);
|
||||
when(() => wowneroAdapter.getCurrentAccount(wallet))
|
||||
.thenReturn(Account(id: 1, label: "Selected", balance: "2.00000000001"));
|
||||
when(() => accountList.accounts).thenReturn(
|
||||
ObservableList.of([
|
||||
Account(id: 0, label: "Primary", balance: "0.0"),
|
||||
Account(id: 1, label: "Selected", balance: "2.00000000001"),
|
||||
]),
|
||||
);
|
||||
when(() => wowneroAdapter.getAccountFullBalance(0)).thenReturn(Money.zero(CryptoCurrency.wow));
|
||||
when(() => wowneroAdapter.getAccountFullBalance(1))
|
||||
.thenReturn(Money.fromInt(200000000001, CryptoCurrency.wow));
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
xmr.monero = originalMonero;
|
||||
wow.wownero = originalWownero;
|
||||
});
|
||||
|
||||
test("Wownero accounts retain exact amounts and select through the Wownero adapter", () async {
|
||||
final viewModel = MoneroAccountListViewModel(wallet);
|
||||
|
||||
expect(viewModel.selected.id, 1);
|
||||
expect(viewModel.selected.label, "Selected");
|
||||
expect(viewModel.accounts.first.isFunded, isFalse);
|
||||
expect(viewModel.selected.balance.amount, BigInt.from(200000000001));
|
||||
await viewModel.select(viewModel.selected);
|
||||
verify(() => wowneroAdapter.setCurrentAccount(wallet, 1, "Selected", "2.00000000001"))
|
||||
.called(1);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user