From 0fcfd76afd75b91665e25804859497aa46a051aa Mon Sep 17 00:00:00 2001 From: David Adegoke <64401859+Blazebrain@users.noreply.github.com> Date: Thu, 7 Nov 2024 15:46:08 +0100 Subject: [PATCH] Automated Integration Tests Flows (#1686) * feat: Integration tests setup and tests for Disclaimer, Welcome and Setup Pin Code pages * feat: Integration test flow from start to restoring a wallet successfully done * test: Dashboard view test and linking to flow * feat: Testing the Exchange flow section, selecting sending and receiving currencies * test: Successfully create an exchange section * feat: Implement flow up to sending section * test: Complete Exchange flow * fix dependency issue * test: Final cleanups * feat: Add CI to run automated integration tests withan android emulator * feat: Adjust Automated integration test CI to run on ubuntu 20.04-a * fix: Move integration test CI into PR test build CI * ci: Add automated test ci which is a streamlined replica of pr test build ci * ci: Re-add step to access branch name * ci: Add KVM * ci: Add filepath to trigger the test run from * ci: Add required key * ci: Add required key * ci: Add missing secret key * ci: Add missing secret key * ci: Add nano secrets to workflow * ci: Switch step to free space on runner * ci: Remove timeout from workflow * ci: Confirm impact that removing copy_monero_deps would have on entire workflow time * ci: Update CI and temporarily remove cache related to emulator * ci: Remove dynamic java version * ci: Temporarily switch CI * ci: Switch to 11.x jdk * ci: Temporarily switch CI * ci: Revert ubuntu version * ci: Add more api levels * ci: Add more target options * ci: Settled on stable emulator matrix options * ci: Add more target options * ci: Modify flow * ci: Streamline api levels to 28 and 29 * ci: One more trial * ci: Switch to flutter drive * ci: Reduce options * ci: Remove haven from test * ci: Check for solana in list * ci: Adjust amounts and currencies for exchange flow * ci: Set write response on failure to true * ci: Split ci to funds and non funds related tests * test: Test for Send flow scenario and minor restructuring for test folders and files * chore: cleanup * ci: Pause CI for now * ci: Pause CI for now * ci: Pause CI for now * test: Restore wallets integration automated tests * Fix: Add keys back to currency amount textfield widget * fix: Switch variable name * fix: remove automation for now * tests: Automated tests for Create wallets flow * tests: Further optimize common flows * tests: Add missing await for call * tests: Confirm Seeds Display Properly WIP * tests: Confirm Seeds Display Correctly Automated Tests * fix: Add missing pubspec params for bitcoin and bitcoin_cash * feat: Automated Tests for Transaction History Flow * fix: Add missing pubspec parameter * feat: Automated Integration Tests for Transaction History flow * test: Updating send page robot and also syncing branch with main * test: Modifying tests to flow with wallet grouping implementation * fix: Issue with transaction history test * fix: Modifications to the PR and add automated confirmation for checking that all wallet types are restored or created correctly * test: Attempting automation for testing * fix: Issue from merge conflicts * test: Remove automation of test in this PR --------- Co-authored-by: OmarHatem --- cw_bitcoin/pubspec.yaml | 1 + cw_bitcoin_cash/pubspec.yaml | 1 + cw_core/pubspec.yaml | 1 + .../components/common_test_cases.dart | 121 ++++- .../components/common_test_constants.dart | 2 +- .../components/common_test_flows.dart | 291 +++++++++++- integration_test/funds_related_tests.dart | 9 +- .../robots/dashboard_menu_widget_robot.dart | 39 ++ .../robots/dashboard_page_robot.dart | 62 ++- .../robots/exchange_page_robot.dart | 4 +- .../robots/new_wallet_page_robot.dart | 35 ++ .../robots/pin_code_widget_robot.dart | 11 +- .../robots/pre_seed_page_robot.dart | 20 + .../restore_from_seed_or_key_robot.dart | 17 + .../robots/restore_options_page_robot.dart | 6 +- .../security_and_backup_page_robot.dart | 24 + integration_test/robots/send_page_robot.dart | 9 +- .../robots/transactions_page_robot.dart | 286 +++++++++++ .../wallet_group_description_page_robot.dart | 32 ++ .../robots/wallet_keys_robot.dart | 162 +++++++ .../robots/wallet_list_page_robot.dart | 27 ++ .../robots/wallet_seed_page_robot.dart | 57 +++ .../test_suites/confirm_seeds_flow_test.dart | 107 +++++ .../test_suites/create_wallet_flow_test.dart | 57 +++ .../test_suites/exchange_flow_test.dart | 61 +-- ...estore_wallet_through_seeds_flow_test.dart | 63 +++ .../test_suites/send_flow_test.dart | 37 +- .../transaction_history_flow_test.dart | 70 +++ ios/Podfile.lock | 12 +- lib/src/screens/InfoPage.dart | 13 +- lib/src/screens/dashboard/dashboard_page.dart | 2 +- .../dashboard/pages/nft_details_page.dart | 5 +- .../dashboard/pages/transactions_page.dart | 214 +++++---- .../widgets/anonpay_transaction_row.dart | 1 + .../dashboard/widgets/date_section_raw.dart | 41 +- .../screens/dashboard/widgets/header_row.dart | 3 +- .../dashboard/widgets/menu_widget.dart | 3 +- .../screens/dashboard/widgets/order_row.dart | 78 ++- .../screens/dashboard/widgets/trade_row.dart | 1 + .../dashboard/widgets/transaction_raw.dart | 103 ++-- .../screens/new_wallet/new_wallet_page.dart | 18 +- .../wallet_group_description_page.dart | 2 + .../screens/restore/restore_options_page.dart | 38 +- .../wallet_restore_from_seed_form.dart | 2 + lib/src/screens/seed/pre_seed_page.dart | 10 +- lib/src/screens/seed/wallet_seed_page.dart | 95 ++-- .../settings/security_backup_page.dart | 50 +- .../widgets/settings_cell_with_arrow.dart | 7 +- .../widgets/settings_picker_cell.dart | 24 +- .../widgets/settings_switcher_cell.dart | 3 +- .../setup_2fa/setup_2fa_info_page.dart | 8 +- .../blockexplorer_list_item.dart | 9 +- .../rbf_details_list_fee_picker_item.dart | 26 +- .../standart_list_item.dart | 7 +- .../textfield_list_item.dart | 10 +- .../transaction_details_list_item.dart | 7 +- .../transaction_details_page.dart | 60 +-- .../transaction_expandable_list_item.dart | 9 +- .../widgets/textfield_list_row.dart | 14 +- .../screens/wallet_keys/wallet_keys_page.dart | 36 +- .../screens/wallet_list/wallet_list_page.dart | 2 + lib/src/widgets/blockchain_height_widget.dart | 3 + lib/src/widgets/dashboard_card_widget.dart | 1 + lib/src/widgets/option_tile.dart | 18 +- lib/src/widgets/picker.dart | 3 + lib/src/widgets/seed_language_selector.dart | 11 +- lib/src/widgets/setting_actions.dart | 12 + lib/src/widgets/standard_list.dart | 8 +- .../anonpay/anonpay_transactions_store.dart | 6 +- lib/store/dashboard/orders_store.dart | 24 +- lib/store/dashboard/trades_store.dart | 19 +- lib/utils/date_formatter.dart | 26 +- lib/utils/image_utill.dart | 4 + .../dashboard/action_list_item.dart | 5 + .../anonpay_transaction_list_item.dart | 2 +- .../dashboard/dashboard_view_model.dart | 106 +++-- .../dashboard/date_section_item.dart | 2 +- .../dashboard/formatted_item_list.dart | 15 +- lib/view_model/dashboard/order_list_item.dart | 4 +- lib/view_model/dashboard/trade_list_item.dart | 6 +- .../dashboard/transaction_list_item.dart | 8 +- .../transaction_details_view_model.dart | 449 ++++++++++++++---- lib/view_model/wallet_keys_view_model.dart | 181 ++++--- tool/utils/secret_key.dart | 23 + 84 files changed, 2716 insertions(+), 745 deletions(-) create mode 100644 integration_test/robots/dashboard_menu_widget_robot.dart create mode 100644 integration_test/robots/new_wallet_page_robot.dart create mode 100644 integration_test/robots/pre_seed_page_robot.dart create mode 100644 integration_test/robots/security_and_backup_page_robot.dart create mode 100644 integration_test/robots/transactions_page_robot.dart create mode 100644 integration_test/robots/wallet_group_description_page_robot.dart create mode 100644 integration_test/robots/wallet_keys_robot.dart create mode 100644 integration_test/robots/wallet_list_page_robot.dart create mode 100644 integration_test/robots/wallet_seed_page_robot.dart create mode 100644 integration_test/test_suites/confirm_seeds_flow_test.dart create mode 100644 integration_test/test_suites/create_wallet_flow_test.dart create mode 100644 integration_test/test_suites/restore_wallet_through_seeds_flow_test.dart create mode 100644 integration_test/test_suites/transaction_history_flow_test.dart diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index 7a1576a98..bff6104ac 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -73,6 +73,7 @@ dependency_overrides: # The following section is specific to Flutter. flutter: + uses-material-design: true # To add assets to your package, add an assets section, like this: # assets: diff --git a/cw_bitcoin_cash/pubspec.yaml b/cw_bitcoin_cash/pubspec.yaml index 14ff407e2..9a5c4f14f 100644 --- a/cw_bitcoin_cash/pubspec.yaml +++ b/cw_bitcoin_cash/pubspec.yaml @@ -49,6 +49,7 @@ dependency_overrides: # The following section is specific to Flutter packages. flutter: + uses-material-design: true # To add assets to your package, add an assets section, like this: # assets: diff --git a/cw_core/pubspec.yaml b/cw_core/pubspec.yaml index 070779caa..6e32c2ba1 100644 --- a/cw_core/pubspec.yaml +++ b/cw_core/pubspec.yaml @@ -47,6 +47,7 @@ dependency_overrides: # The following section is specific to Flutter. flutter: + uses-material-design: true # To add assets to your package, add an assets section, like this: # assets: diff --git a/integration_test/components/common_test_cases.dart b/integration_test/components/common_test_cases.dart index 2e2991804..83bbb0449 100644 --- a/integration_test/components/common_test_cases.dart +++ b/integration_test/components/common_test_cases.dart @@ -10,10 +10,16 @@ class CommonTestCases { hasType(); } - Future tapItemByKey(String key, {bool shouldPumpAndSettle = true}) async { + Future tapItemByKey( + String key, { + bool shouldPumpAndSettle = true, + int pumpDuration = 100, + }) async { final widget = find.byKey(ValueKey(key)); await tester.tap(widget); - shouldPumpAndSettle ? await tester.pumpAndSettle() : await tester.pump(); + shouldPumpAndSettle + ? await tester.pumpAndSettle(Duration(milliseconds: pumpDuration)) + : await tester.pump(); } Future tapItemByFinder(Finder finder, {bool shouldPumpAndSettle = true}) async { @@ -31,6 +37,11 @@ class CommonTestCases { expect(typeWidget, findsOneWidget); } + bool isKeyPresent(String key) { + final typeWidget = find.byKey(ValueKey(key)); + return typeWidget.tryEvaluate(); + } + void hasValueKey(String key) { final typeWidget = find.byKey(ValueKey(key)); expect(typeWidget, findsOneWidget); @@ -53,33 +64,86 @@ class CommonTestCases { await tester.pumpAndSettle(); } - Future scrollUntilVisible(String childKey, String parentScrollableKey, - {double delta = 300}) async { - final scrollableWidget = find.descendant( - of: find.byKey(Key(parentScrollableKey)), + Future dragUntilVisible(String childKey, String parentKey) async { + await tester.pumpAndSettle(); + + final itemFinder = find.byKey(ValueKey(childKey)); + final listFinder = find.byKey(ValueKey(parentKey)); + + // Check if the widget is already in the widget tree + if (tester.any(itemFinder)) { + // Widget is already built and in the tree + tester.printToConsole('Child is already present'); + return; + } + + // We can adjust this as needed + final maxScrolls = 200; + + int scrolls = 0; + bool found = false; + + // We start by scrolling down + bool scrollDown = true; + + // Flag to check if we've already reversed direction + bool reversedDirection = false; + + // Find the Scrollable associated with the Parent Ad + final scrollableFinder = find.descendant( + of: listFinder, matching: find.byType(Scrollable), ); - final isAlreadyVisibile = isWidgetVisible(find.byKey(ValueKey(childKey))); - - if (isAlreadyVisibile) return; - - await tester.scrollUntilVisible( - find.byKey(ValueKey(childKey)), - delta, - scrollable: scrollableWidget, + // Ensure that the Scrollable is found + expect( + scrollableFinder, + findsOneWidget, + reason: 'Scrollable descendant of the Parent Widget not found.', ); - } - bool isWidgetVisible(Finder finder) { - try { - final Element element = finder.evaluate().single; - final RenderBox renderBox = element.renderObject as RenderBox; - return renderBox.paintBounds - .shift(renderBox.localToGlobal(Offset.zero)) - .overlaps(tester.binding.renderViews.first.paintBounds); - } catch (e) { - return false; + // Get the initial scroll position + final scrollableState = tester.state(scrollableFinder); + double previousScrollPosition = scrollableState.position.pixels; + + while (!found && scrolls < maxScrolls) { + tester.printToConsole('Scrolling ${scrollDown ? 'down' : 'up'}, attempt $scrolls'); + + // Perform the drag in the current direction + await tester.drag( + scrollableFinder, + scrollDown ? const Offset(0, -100) : const Offset(0, 100), + ); + await tester.pumpAndSettle(); + scrolls++; + + // Update the scroll position after the drag + final currentScrollPosition = scrollableState.position.pixels; + + if (currentScrollPosition == previousScrollPosition) { + // Cannot scroll further in this direction + if (reversedDirection) { + // We've already tried both directions + tester.printToConsole('Cannot scroll further in both directions. Widget not found.'); + break; + } else { + // Reverse the scroll direction + scrollDown = !scrollDown; + reversedDirection = true; + tester.printToConsole('Reached the end, reversing direction'); + } + } else { + // Continue scrolling in the current direction + previousScrollPosition = currentScrollPosition; + } + + // Check if the widget is now in the widget tree + found = tester.any(itemFinder); + } + + if (!found) { + tester.printToConsole('Widget not found after scrolling in both directions.'); + return; } } @@ -91,6 +155,15 @@ class CommonTestCases { await tester.pumpAndSettle(); } + void findWidgetViaDescendant({ + required FinderBase of, + required FinderBase matching, + }) { + final textWidget = find.descendant(of: of, matching: matching); + + expect(textWidget, findsOneWidget); + } + Future defaultSleepTime({int seconds = 2}) async => await Future.delayed(Duration(seconds: seconds)); } diff --git a/integration_test/components/common_test_constants.dart b/integration_test/components/common_test_constants.dart index d8381973e..302d52189 100644 --- a/integration_test/components/common_test_constants.dart +++ b/integration_test/components/common_test_constants.dart @@ -9,5 +9,5 @@ class CommonTestConstants { static final String testWalletName = 'Integrated Testing Wallet'; static final CryptoCurrency testReceiveCurrency = CryptoCurrency.sol; static final CryptoCurrency testDepositCurrency = CryptoCurrency.usdtSol; - static final String testWalletAddress = 'An2Y2fsUYKfYvN1zF89GAqR1e6GUMBg3qA83Y5ZWDf8L'; + static final String testWalletAddress = '5v9gTW1yWPffhnbNKuvtL2frevAf4HpBMw8oYnfqUjhm'; } diff --git a/integration_test/components/common_test_flows.dart b/integration_test/components/common_test_flows.dart index 807509de9..82f714da0 100644 --- a/integration_test/components/common_test_flows.dart +++ b/integration_test/components/common_test_flows.dart @@ -1,41 +1,65 @@ -import 'package:flutter/foundation.dart'; +import 'package:cake_wallet/entities/seed_type.dart'; +import 'package:cake_wallet/reactions/bip39_wallet_utils.dart'; +import 'package:cake_wallet/wallet_types.g.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:cake_wallet/.secrets.g.dart' as secrets; import 'package:cake_wallet/main.dart' as app; +import '../robots/dashboard_page_robot.dart'; import '../robots/disclaimer_page_robot.dart'; +import '../robots/new_wallet_page_robot.dart'; import '../robots/new_wallet_type_page_robot.dart'; +import '../robots/pre_seed_page_robot.dart'; import '../robots/restore_from_seed_or_key_robot.dart'; import '../robots/restore_options_page_robot.dart'; import '../robots/setup_pin_code_robot.dart'; +import '../robots/wallet_group_description_page_robot.dart'; +import '../robots/wallet_list_page_robot.dart'; +import '../robots/wallet_seed_page_robot.dart'; import '../robots/welcome_page_robot.dart'; import 'common_test_cases.dart'; +import 'package:cake_wallet/.secrets.g.dart' as secrets; + import 'common_test_constants.dart'; class CommonTestFlows { CommonTestFlows(this._tester) : _commonTestCases = CommonTestCases(_tester), _welcomePageRobot = WelcomePageRobot(_tester), + _preSeedPageRobot = PreSeedPageRobot(_tester), _setupPinCodeRobot = SetupPinCodeRobot(_tester), + _dashboardPageRobot = DashboardPageRobot(_tester), + _newWalletPageRobot = NewWalletPageRobot(_tester), _disclaimerPageRobot = DisclaimerPageRobot(_tester), + _walletSeedPageRobot = WalletSeedPageRobot(_tester), + _walletListPageRobot = WalletListPageRobot(_tester), _newWalletTypePageRobot = NewWalletTypePageRobot(_tester), _restoreOptionsPageRobot = RestoreOptionsPageRobot(_tester), - _restoreFromSeedOrKeysPageRobot = RestoreFromSeedOrKeysPageRobot(_tester); + _restoreFromSeedOrKeysPageRobot = RestoreFromSeedOrKeysPageRobot(_tester), + _walletGroupDescriptionPageRobot = WalletGroupDescriptionPageRobot(_tester); final WidgetTester _tester; final CommonTestCases _commonTestCases; final WelcomePageRobot _welcomePageRobot; + final PreSeedPageRobot _preSeedPageRobot; final SetupPinCodeRobot _setupPinCodeRobot; + final NewWalletPageRobot _newWalletPageRobot; + final DashboardPageRobot _dashboardPageRobot; final DisclaimerPageRobot _disclaimerPageRobot; + final WalletSeedPageRobot _walletSeedPageRobot; + final WalletListPageRobot _walletListPageRobot; final NewWalletTypePageRobot _newWalletTypePageRobot; final RestoreOptionsPageRobot _restoreOptionsPageRobot; final RestoreFromSeedOrKeysPageRobot _restoreFromSeedOrKeysPageRobot; + final WalletGroupDescriptionPageRobot _walletGroupDescriptionPageRobot; + //* ========== Handles flow to start the app afresh and accept disclaimer ============= Future startAppFlow(Key key) async { await app.main(topLevelKey: ValueKey('send_flow_test_app_key')); - + await _tester.pumpAndSettle(); // --------- Disclaimer Page ------------ @@ -46,56 +70,275 @@ class CommonTestFlows { await _disclaimerPageRobot.tapAcceptButton(); } - Future restoreWalletThroughSeedsFlow() async { - await _welcomeToRestoreFromSeedsPath(); - await _restoreFromSeeds(); + //* ========== Handles flow from welcome to creating a new wallet =============== + Future welcomePageToCreateNewWalletFlow( + WalletType walletTypeToCreate, + List walletPin, + ) async { + await _welcomeToCreateWalletPath(walletTypeToCreate, walletPin); + + await _generateNewWalletDetails(); + + await _confirmPreSeedInfo(); + + await _confirmWalletDetails(); } - Future restoreWalletThroughKeysFlow() async { - await _welcomeToRestoreFromSeedsPath(); + //* ========== Handles flow from welcome to restoring wallet from seeds =============== + Future welcomePageToRestoreWalletThroughSeedsFlow( + WalletType walletTypeToRestore, + String walletSeed, + List walletPin, + ) async { + await _welcomeToRestoreFromSeedsOrKeysPath(walletTypeToRestore, walletPin); + await _restoreFromSeeds(walletTypeToRestore, walletSeed); + } + + //* ========== Handles flow from welcome to restoring wallet from keys =============== + Future welcomePageToRestoreWalletThroughKeysFlow( + WalletType walletTypeToRestore, + List walletPin, + ) async { + await _welcomeToRestoreFromSeedsOrKeysPath(walletTypeToRestore, walletPin); await _restoreFromKeys(); } - Future _welcomeToRestoreFromSeedsPath() async { - // --------- Welcome Page --------------- - await _welcomePageRobot.navigateToRestoreWalletPage(); + //* ========== Handles switching to wallet list or menu from dashboard =============== + Future switchToWalletMenuFromDashboardPage() async { + _tester.printToConsole('Switching to Wallet Menu'); + await _dashboardPageRobot.openDrawerMenu(); - // ----------- Restore Options Page ----------- - // Route to restore from seeds page to continue flow - await _restoreOptionsPageRobot.navigateToRestoreFromSeedsPage(); + await _dashboardPageRobot.dashboardMenuWidgetRobot.navigateToWalletMenu(); + } + void confirmAllAvailableWalletTypeIconsDisplayCorrectly() { + for (var walletType in availableWalletTypes) { + final imageUrl = walletTypeToCryptoCurrency(walletType).iconPath; + + final walletIconFinder = find.image( + Image.asset( + imageUrl!, + width: 32, + height: 32, + ).image, + ); + + expect(walletIconFinder, findsAny); + } + } + + //* ========== Handles creating new wallet flow from wallet list/menu =============== + Future createNewWalletFromWalletMenu(WalletType walletTypeToCreate) async { + _tester.printToConsole('Creating ${walletTypeToCreate.name} Wallet'); + await _walletListPageRobot.navigateToCreateNewWalletPage(); + await _commonTestCases.defaultSleepTime(); + + await _selectWalletTypeForWallet(walletTypeToCreate); + await _commonTestCases.defaultSleepTime(); + + // ---- Wallet Group/New Seed Implementation Comes here + await _walletGroupDescriptionPageFlow(true, walletTypeToCreate); + + await _generateNewWalletDetails(); + + await _confirmPreSeedInfo(); + + await _confirmWalletDetails(); + await _commonTestCases.defaultSleepTime(); + } + + Future _walletGroupDescriptionPageFlow(bool isNewSeed, WalletType walletType) async { + if (!isBIP39Wallet(walletType)) return; + + await _walletGroupDescriptionPageRobot.isWalletGroupDescriptionPage(); + + if (isNewSeed) { + await _walletGroupDescriptionPageRobot.navigateToCreateNewSeedPage(); + } else { + await _walletGroupDescriptionPageRobot.navigateToChooseWalletGroup(); + } + } + + //* ========== Handles restore wallet flow from wallet list/menu =============== + Future restoreWalletFromWalletMenu(WalletType walletType, String walletSeed) async { + _tester.printToConsole('Restoring ${walletType.name} Wallet'); + await _walletListPageRobot.navigateToRestoreWalletOptionsPage(); + await _commonTestCases.defaultSleepTime(); + + await _restoreOptionsPageRobot.navigateToRestoreFromSeedsOrKeysPage(); + await _commonTestCases.defaultSleepTime(); + + await _selectWalletTypeForWallet(walletType); + await _commonTestCases.defaultSleepTime(); + + await _restoreFromSeeds(walletType, walletSeed); + await _commonTestCases.defaultSleepTime(); + } + + //* ========== Handles setting up pin code for wallet on first install =============== + Future setupPinCodeForWallet(List pin) async { // ----------- SetupPinCode Page ------------- // Confirm initial defaults - Widgets to be displayed etc await _setupPinCodeRobot.isSetupPinCodePage(); - await _setupPinCodeRobot.enterPinCode(CommonTestConstants.pin, true); - await _setupPinCodeRobot.enterPinCode(CommonTestConstants.pin, false); + await _setupPinCodeRobot.enterPinCode(pin); + await _setupPinCodeRobot.enterPinCode(pin); await _setupPinCodeRobot.tapSuccessButton(); + } + Future _welcomeToCreateWalletPath( + WalletType walletTypeToCreate, + List pin, + ) async { + await _welcomePageRobot.navigateToCreateNewWalletPage(); + + await setupPinCodeForWallet(pin); + + await _selectWalletTypeForWallet(walletTypeToCreate); + } + + Future _welcomeToRestoreFromSeedsOrKeysPath( + WalletType walletTypeToRestore, + List pin, + ) async { + await _welcomePageRobot.navigateToRestoreWalletPage(); + + await _restoreOptionsPageRobot.navigateToRestoreFromSeedsOrKeysPage(); + + await setupPinCodeForWallet(pin); + + await _selectWalletTypeForWallet(walletTypeToRestore); + } + + //* ============ Handles New Wallet Type Page ================== + Future _selectWalletTypeForWallet(WalletType type) async { // ----------- NewWalletType Page ------------- // Confirm scroll behaviour works properly - await _newWalletTypePageRobot - .findParticularWalletTypeInScrollableList(CommonTestConstants.testWalletType); + await _newWalletTypePageRobot.findParticularWalletTypeInScrollableList(type); // Select a wallet and route to next page - await _newWalletTypePageRobot.selectWalletType(CommonTestConstants.testWalletType); + await _newWalletTypePageRobot.selectWalletType(type); await _newWalletTypePageRobot.onNextButtonPressed(); } - Future _restoreFromSeeds() async { + //* ============ Handles New Wallet Page ================== + Future _generateNewWalletDetails() async { + await _newWalletPageRobot.isNewWalletPage(); + + await _newWalletPageRobot.generateWalletName(); + + await _newWalletPageRobot.onNextButtonPressed(); + } + + //* ============ Handles Pre Seed Page ===================== + Future _confirmPreSeedInfo() async { + await _preSeedPageRobot.isPreSeedPage(); + + await _preSeedPageRobot.onConfirmButtonPressed(); + } + + //* ============ Handles Wallet Seed Page ================== + Future _confirmWalletDetails() async { + await _walletSeedPageRobot.isWalletSeedPage(); + + _walletSeedPageRobot.confirmWalletDetailsDisplayCorrectly(); + + _walletSeedPageRobot.confirmWalletSeedReminderDisplays(); + + await _walletSeedPageRobot.onCopySeedsButtonPressed(); + + await _walletSeedPageRobot.onNextButtonPressed(); + + await _walletSeedPageRobot.onConfirmButtonOnSeedAlertDialogPressed(); + } + + //* Main Restore Actions - On the RestoreFromSeed/Keys Page - Restore from Seeds Action + Future _restoreFromSeeds(WalletType type, String walletSeed) async { // ----------- RestoreFromSeedOrKeys Page ------------- - await _restoreFromSeedOrKeysPageRobot.enterWalletNameText(CommonTestConstants.testWalletName); - await _restoreFromSeedOrKeysPageRobot.enterSeedPhraseForWalletRestore(secrets.solanaTestWalletSeeds); + + await _restoreFromSeedOrKeysPageRobot.selectWalletNameFromAvailableOptions(); + await _restoreFromSeedOrKeysPageRobot.enterSeedPhraseForWalletRestore(walletSeed); + + final numberOfWords = walletSeed.split(' ').length; + + if (numberOfWords == 25 && (type == WalletType.monero)) { + await _restoreFromSeedOrKeysPageRobot + .chooseSeedTypeForMoneroOrWowneroWallets(MoneroSeedType.legacy); + + // Using a constant value of 2831400 for the blockheight as its the restore blockheight for our testing wallet + await _restoreFromSeedOrKeysPageRobot + .enterBlockHeightForWalletRestore(secrets.moneroTestWalletBlockHeight); + } + await _restoreFromSeedOrKeysPageRobot.onRestoreWalletButtonPressed(); } + //* Main Restore Actions - On the RestoreFromSeed/Keys Page - Restore from Keys Action Future _restoreFromKeys() async { await _commonTestCases.swipePage(); await _commonTestCases.defaultSleepTime(); - await _restoreFromSeedOrKeysPageRobot.enterWalletNameText(CommonTestConstants.testWalletName); + await _restoreFromSeedOrKeysPageRobot.selectWalletNameFromAvailableOptions( + isSeedFormEntry: false, + ); await _restoreFromSeedOrKeysPageRobot.enterSeedPhraseForWalletRestore(''); await _restoreFromSeedOrKeysPageRobot.onRestoreWalletButtonPressed(); } + + //* ====== Utility Function to get test wallet seeds for each wallet type ======== + String getWalletSeedsByWalletType(WalletType walletType) { + switch (walletType) { + case WalletType.monero: + return secrets.moneroTestWalletSeeds; + case WalletType.bitcoin: + return secrets.bitcoinTestWalletSeeds; + case WalletType.ethereum: + return secrets.ethereumTestWalletSeeds; + case WalletType.litecoin: + return secrets.litecoinTestWalletSeeds; + case WalletType.bitcoinCash: + return secrets.bitcoinCashTestWalletSeeds; + case WalletType.polygon: + return secrets.polygonTestWalletSeeds; + case WalletType.solana: + return secrets.solanaTestWalletSeeds; + case WalletType.tron: + return secrets.tronTestWalletSeeds; + case WalletType.nano: + return secrets.nanoTestWalletSeeds; + case WalletType.wownero: + return secrets.wowneroTestWalletSeeds; + default: + return ''; + } + } + + //* ====== Utility Function to get test receive address for each wallet type ======== + String getReceiveAddressByWalletType(WalletType walletType) { + switch (walletType) { + case WalletType.monero: + return secrets.moneroTestWalletReceiveAddress; + case WalletType.bitcoin: + return secrets.bitcoinTestWalletReceiveAddress; + case WalletType.ethereum: + return secrets.ethereumTestWalletReceiveAddress; + case WalletType.litecoin: + return secrets.litecoinTestWalletReceiveAddress; + case WalletType.bitcoinCash: + return secrets.bitcoinCashTestWalletReceiveAddress; + case WalletType.polygon: + return secrets.polygonTestWalletReceiveAddress; + case WalletType.solana: + return secrets.solanaTestWalletReceiveAddress; + case WalletType.tron: + return secrets.tronTestWalletReceiveAddress; + case WalletType.nano: + return secrets.nanoTestWalletReceiveAddress; + case WalletType.wownero: + return secrets.wowneroTestWalletReceiveAddress; + default: + return ''; + } + } } diff --git a/integration_test/funds_related_tests.dart b/integration_test/funds_related_tests.dart index 9d97d47f8..db24fbc0b 100644 --- a/integration_test/funds_related_tests.dart +++ b/integration_test/funds_related_tests.dart @@ -9,6 +9,7 @@ import 'robots/dashboard_page_robot.dart'; import 'robots/exchange_confirm_page_robot.dart'; import 'robots/exchange_page_robot.dart'; import 'robots/exchange_trade_page_robot.dart'; +import 'package:cake_wallet/.secrets.g.dart' as secrets; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); @@ -32,7 +33,11 @@ void main() { await commonTestFlows.startAppFlow(ValueKey('funds_exchange_test_app_key')); - await commonTestFlows.restoreWalletThroughSeedsFlow(); + await commonTestFlows.welcomePageToRestoreWalletThroughSeedsFlow( + CommonTestConstants.testWalletType, + secrets.solanaTestWalletSeeds, + CommonTestConstants.pin, + ); // ----------- RestoreFromSeedOrKeys Page ------------- await dashboardPageRobot.navigateToExchangePage(); @@ -59,7 +64,7 @@ void main() { final onAuthPage = authPageRobot.onAuthPage(); if (onAuthPage) { - await authPageRobot.enterPinCode(CommonTestConstants.pin, false); + await authPageRobot.enterPinCode(CommonTestConstants.pin); } // ----------- Exchange Confirm Page ------------- diff --git a/integration_test/robots/dashboard_menu_widget_robot.dart b/integration_test/robots/dashboard_menu_widget_robot.dart new file mode 100644 index 000000000..f48033dda --- /dev/null +++ b/integration_test/robots/dashboard_menu_widget_robot.dart @@ -0,0 +1,39 @@ +import 'package:cake_wallet/src/screens/dashboard/widgets/menu_widget.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../components/common_test_cases.dart'; + +class DashboardMenuWidgetRobot { + DashboardMenuWidgetRobot(this.tester) : commonTestCases = CommonTestCases(tester); + + final WidgetTester tester; + late CommonTestCases commonTestCases; + + Future hasMenuWidget() async { + commonTestCases.hasType(); + } + + void displaysTheCorrectWalletNameAndSubName() { + final menuWidgetState = tester.state(find.byType(MenuWidget)); + + final walletName = menuWidgetState.widget.dashboardViewModel.name; + commonTestCases.hasText(walletName); + + final walletSubName = menuWidgetState.widget.dashboardViewModel.subname; + if (walletSubName.isNotEmpty) { + commonTestCases.hasText(walletSubName); + } + } + + Future navigateToWalletMenu() async { + await commonTestCases.tapItemByKey('dashboard_page_menu_widget_wallet_menu_button_key'); + await commonTestCases.defaultSleepTime(); + } + + Future navigateToSecurityAndBackupPage() async { + await commonTestCases.tapItemByKey( + 'dashboard_page_menu_widget_security_and_backup_button_key', + ); + await commonTestCases.defaultSleepTime(); + } +} diff --git a/integration_test/robots/dashboard_page_robot.dart b/integration_test/robots/dashboard_page_robot.dart index fc917c3b2..bc5f411ad 100644 --- a/integration_test/robots/dashboard_page_robot.dart +++ b/integration_test/robots/dashboard_page_robot.dart @@ -1,20 +1,44 @@ import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/screens/dashboard/dashboard_page.dart'; +import 'package:cake_wallet/src/screens/dashboard/pages/balance_page.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter_test/flutter_test.dart'; import '../components/common_test_cases.dart'; +import 'dashboard_menu_widget_robot.dart'; class DashboardPageRobot { - DashboardPageRobot(this.tester) : commonTestCases = CommonTestCases(tester); + DashboardPageRobot(this.tester) + : commonTestCases = CommonTestCases(tester), + dashboardMenuWidgetRobot = DashboardMenuWidgetRobot(tester); final WidgetTester tester; + final DashboardMenuWidgetRobot dashboardMenuWidgetRobot; late CommonTestCases commonTestCases; Future isDashboardPage() async { await commonTestCases.isSpecificPage(); } + Future confirmWalletTypeIsDisplayedCorrectly( + WalletType type, { + bool isHaven = false, + }) async { + final cryptoBalanceWidget = + tester.widget(find.byType(CryptoBalanceWidget)); + final hasAccounts = cryptoBalanceWidget.dashboardViewModel.balanceViewModel.hasAccounts; + + if (hasAccounts) { + final walletName = cryptoBalanceWidget.dashboardViewModel.name; + commonTestCases.hasText(walletName); + } else { + final walletName = walletTypeToString(type); + final assetName = isHaven ? '$walletName Assets' : walletName; + commonTestCases.hasText(assetName); + } + await commonTestCases.defaultSleepTime(seconds: 5); + } + void confirmServiceUpdateButtonDisplays() { commonTestCases.hasValueKey('dashboard_page_services_update_button_key'); } @@ -27,30 +51,40 @@ class DashboardPageRobot { commonTestCases.hasValueKey('dashboard_page_wallet_menu_button_key'); } - Future confirmRightCryptoAssetTitleDisplaysPerPageView(WalletType type, - {bool isHaven = false}) async { + Future confirmRightCryptoAssetTitleDisplaysPerPageView( + WalletType type, { + bool isHaven = false, + }) async { //Balance Page - final walletName = walletTypeToString(type); - final assetName = isHaven ? '$walletName Assets' : walletName; - commonTestCases.hasText(assetName); + await confirmWalletTypeIsDisplayedCorrectly(type, isHaven: isHaven); // Swipe to Cake features Page - await commonTestCases.swipeByPageKey(key: 'dashboard_page_view_key', swipeRight: false); - await commonTestCases.defaultSleepTime(); + await swipeDashboardTab(false); commonTestCases.hasText('Cake ${S.current.features}'); // Swipe back to balance - await commonTestCases.swipeByPageKey(key: 'dashboard_page_view_key'); - await commonTestCases.defaultSleepTime(); + await swipeDashboardTab(true); // Swipe to Transactions Page - await commonTestCases.swipeByPageKey(key: 'dashboard_page_view_key'); - await commonTestCases.defaultSleepTime(); + await swipeDashboardTab(true); commonTestCases.hasText(S.current.transactions); // Swipe back to balance - await commonTestCases.swipeByPageKey(key: 'dashboard_page_view_key', swipeRight: false); - await commonTestCases.defaultSleepTime(seconds: 5); + await swipeDashboardTab(false); + await commonTestCases.defaultSleepTime(seconds: 3); + } + + Future swipeDashboardTab(bool swipeRight) async { + await commonTestCases.swipeByPageKey( + key: 'dashboard_page_view_key', + swipeRight: swipeRight, + ); + await commonTestCases.defaultSleepTime(); + } + + Future openDrawerMenu() async { + await commonTestCases.tapItemByKey('dashboard_page_wallet_menu_button_key'); + await commonTestCases.defaultSleepTime(); } Future navigateToBuyPage() async { diff --git a/integration_test/robots/exchange_page_robot.dart b/integration_test/robots/exchange_page_robot.dart index b439e4791..e01b2df9c 100644 --- a/integration_test/robots/exchange_page_robot.dart +++ b/integration_test/robots/exchange_page_robot.dart @@ -123,7 +123,7 @@ class ExchangePageRobot { return; } - await commonTestCases.scrollUntilVisible( + await commonTestCases.dragUntilVisible( 'picker_items_index_${depositCurrency.name}_button_key', 'picker_scrollbar_key', ); @@ -149,7 +149,7 @@ class ExchangePageRobot { return; } - await commonTestCases.scrollUntilVisible( + await commonTestCases.dragUntilVisible( 'picker_items_index_${receiveCurrency.name}_button_key', 'picker_scrollbar_key', ); diff --git a/integration_test/robots/new_wallet_page_robot.dart b/integration_test/robots/new_wallet_page_robot.dart new file mode 100644 index 000000000..f8deb00ae --- /dev/null +++ b/integration_test/robots/new_wallet_page_robot.dart @@ -0,0 +1,35 @@ +import 'package:cake_wallet/src/screens/new_wallet/new_wallet_page.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../components/common_test_cases.dart'; + +class NewWalletPageRobot { + NewWalletPageRobot(this.tester) : commonTestCases = CommonTestCases(tester); + + final WidgetTester tester; + late CommonTestCases commonTestCases; + + Future isNewWalletPage() async { + await commonTestCases.isSpecificPage(); + } + + Future enterWalletName(String walletName) async { + await commonTestCases.enterText( + walletName, + 'new_wallet_page_wallet_name_textformfield_key', + ); + await commonTestCases.defaultSleepTime(); + } + + Future generateWalletName() async { + await commonTestCases.tapItemByKey( + 'new_wallet_page_wallet_name_textformfield_generate_name_button_key', + ); + await commonTestCases.defaultSleepTime(); + } + + Future onNextButtonPressed() async { + await commonTestCases.tapItemByKey('new_wallet_page_confirm_button_key'); + await commonTestCases.defaultSleepTime(); + } +} diff --git a/integration_test/robots/pin_code_widget_robot.dart b/integration_test/robots/pin_code_widget_robot.dart index b6805e9e0..18dc5fba4 100644 --- a/integration_test/robots/pin_code_widget_robot.dart +++ b/integration_test/robots/pin_code_widget_robot.dart @@ -24,13 +24,12 @@ class PinCodeWidgetRobot { commonTestCases.hasValueKey('pin_code_button_0_key'); } - Future pushPinButton(int index) async { - await commonTestCases.tapItemByKey('pin_code_button_${index}_key'); - } - - Future enterPinCode(List pinCode, bool isFirstEntry) async { + Future enterPinCode(List pinCode, {int pumpDuration = 100}) async { for (int pin in pinCode) { - await pushPinButton(pin); + await commonTestCases.tapItemByKey( + 'pin_code_button_${pin}_key', + pumpDuration: pumpDuration, + ); } await commonTestCases.defaultSleepTime(); diff --git a/integration_test/robots/pre_seed_page_robot.dart b/integration_test/robots/pre_seed_page_robot.dart new file mode 100644 index 000000000..01be1249c --- /dev/null +++ b/integration_test/robots/pre_seed_page_robot.dart @@ -0,0 +1,20 @@ +import 'package:cake_wallet/src/screens/seed/pre_seed_page.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../components/common_test_cases.dart'; + +class PreSeedPageRobot { + PreSeedPageRobot(this.tester) : commonTestCases = CommonTestCases(tester); + + final WidgetTester tester; + late CommonTestCases commonTestCases; + + Future isPreSeedPage() async { + await commonTestCases.isSpecificPage(); + } + + Future onConfirmButtonPressed() async { + await commonTestCases.tapItemByKey('pre_seed_page_button_key'); + await commonTestCases.defaultSleepTime(); + } +} diff --git a/integration_test/robots/restore_from_seed_or_key_robot.dart b/integration_test/robots/restore_from_seed_or_key_robot.dart index 43a65095d..9d973061b 100644 --- a/integration_test/robots/restore_from_seed_or_key_robot.dart +++ b/integration_test/robots/restore_from_seed_or_key_robot.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/entities/seed_type.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/screens/restore/wallet_restore_page.dart'; import 'package:cake_wallet/src/widgets/validable_annotated_editable_text.dart'; @@ -70,6 +71,22 @@ class RestoreFromSeedOrKeysPageRobot { await tester.pumpAndSettle(); } + Future enterBlockHeightForWalletRestore(String blockHeight) async { + await commonTestCases.enterText( + blockHeight, + 'wallet_restore_from_seed_blockheight_textfield_key', + ); + await tester.pumpAndSettle(); + } + + Future chooseSeedTypeForMoneroOrWowneroWallets(MoneroSeedType selectedType) async { + await commonTestCases.tapItemByKey('wallet_restore_from_seed_seedtype_picker_button_key'); + + await commonTestCases.defaultSleepTime(); + + await commonTestCases.tapItemByKey('picker_items_index_${selectedType.title}_button_key'); + } + Future onPasteSeedPhraseButtonPressed() async { await commonTestCases.tapItemByKey('wallet_restore_from_seed_wallet_seeds_paste_button_key'); } diff --git a/integration_test/robots/restore_options_page_robot.dart b/integration_test/robots/restore_options_page_robot.dart index b3cefc90c..cd1919609 100644 --- a/integration_test/robots/restore_options_page_robot.dart +++ b/integration_test/robots/restore_options_page_robot.dart @@ -14,14 +14,14 @@ class RestoreOptionsPageRobot { } void hasRestoreOptionsButton() { - commonTestCases.hasValueKey('restore_options_from_seeds_button_key'); + commonTestCases.hasValueKey('restore_options_from_seeds_or_keys_button_key'); commonTestCases.hasValueKey('restore_options_from_backup_button_key'); commonTestCases.hasValueKey('restore_options_from_hardware_wallet_button_key'); commonTestCases.hasValueKey('restore_options_from_qr_button_key'); } - Future navigateToRestoreFromSeedsPage() async { - await commonTestCases.tapItemByKey('restore_options_from_seeds_button_key'); + Future navigateToRestoreFromSeedsOrKeysPage() async { + await commonTestCases.tapItemByKey('restore_options_from_seeds_or_keys_button_key'); await commonTestCases.defaultSleepTime(); } diff --git a/integration_test/robots/security_and_backup_page_robot.dart b/integration_test/robots/security_and_backup_page_robot.dart new file mode 100644 index 000000000..eb7c1bc87 --- /dev/null +++ b/integration_test/robots/security_and_backup_page_robot.dart @@ -0,0 +1,24 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/settings/security_backup_page.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../components/common_test_cases.dart'; + +class SecurityAndBackupPageRobot { + SecurityAndBackupPageRobot(this.tester) : commonTestCases = CommonTestCases(tester); + + final WidgetTester tester; + final CommonTestCases commonTestCases; + + Future isSecurityAndBackupPage() async { + await commonTestCases.isSpecificPage(); + } + + void hasTitle() { + commonTestCases.hasText(S.current.security_and_backup); + } + + Future navigateToShowKeysPage() async { + await commonTestCases.tapItemByKey('security_backup_page_show_keys_button_key'); + } +} diff --git a/integration_test/robots/send_page_robot.dart b/integration_test/robots/send_page_robot.dart index 971556620..20cef948d 100644 --- a/integration_test/robots/send_page_robot.dart +++ b/integration_test/robots/send_page_robot.dart @@ -84,7 +84,7 @@ class SendPageRobot { return; } - await commonTestCases.scrollUntilVisible( + await commonTestCases.dragUntilVisible( 'picker_items_index_${receiveCurrency.name}_button_key', 'picker_scrollbar_key', ); @@ -117,7 +117,7 @@ class SendPageRobot { return; } - await commonTestCases.scrollUntilVisible( + await commonTestCases.dragUntilVisible( 'picker_items_index_${priority.title}_button_key', 'picker_scrollbar_key', ); @@ -198,7 +198,7 @@ class SendPageRobot { tester.printToConsole('Starting inner _handleAuth loop checks'); try { - await authPageRobot.enterPinCode(CommonTestConstants.pin, false); + await authPageRobot.enterPinCode(CommonTestConstants.pin, pumpDuration: 500); tester.printToConsole('Auth done'); await tester.pump(); @@ -213,6 +213,7 @@ class SendPageRobot { } Future handleSendResult() async { + await tester.pump(); tester.printToConsole('Inside handle function'); bool hasError = false; @@ -287,6 +288,8 @@ class SendPageRobot { // Loop to wait for the operation to commit transaction await _waitForCommitTransactionCompletion(); + await tester.pump(); + await commonTestCases.defaultSleepTime(seconds: 4); } else { await commonTestCases.defaultSleepTime(); diff --git a/integration_test/robots/transactions_page_robot.dart b/integration_test/robots/transactions_page_robot.dart new file mode 100644 index 000000000..40a49928f --- /dev/null +++ b/integration_test/robots/transactions_page_robot.dart @@ -0,0 +1,286 @@ +import 'dart:async'; + +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/dashboard/pages/transactions_page.dart'; +import 'package:cake_wallet/utils/date_formatter.dart'; +import 'package:cake_wallet/view_model/dashboard/action_list_item.dart'; +import 'package:cake_wallet/view_model/dashboard/anonpay_transaction_list_item.dart'; +import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; +import 'package:cake_wallet/view_model/dashboard/date_section_item.dart'; +import 'package:cake_wallet/view_model/dashboard/order_list_item.dart'; +import 'package:cake_wallet/view_model/dashboard/trade_list_item.dart'; +import 'package:cake_wallet/view_model/dashboard/transaction_list_item.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/sync_status.dart'; +import 'package:cw_core/transaction_direction.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:intl/intl.dart'; + +import '../components/common_test_cases.dart'; + +class TransactionsPageRobot { + TransactionsPageRobot(this.tester) : commonTestCases = CommonTestCases(tester); + + final WidgetTester tester; + late CommonTestCases commonTestCases; + + Future isTransactionsPage() async { + await commonTestCases.isSpecificPage(); + } + + Future confirmTransactionsPageConstantsDisplayProperly() async { + await commonTestCases.defaultSleepTime(); + + final transactionsPage = tester.widget(find.byType(TransactionsPage)); + final dashboardViewModel = transactionsPage.dashboardViewModel; + if (dashboardViewModel.status is SyncingSyncStatus) { + commonTestCases.hasValueKey('transactions_page_syncing_alert_card_key'); + commonTestCases.hasText(S.current.syncing_wallet_alert_title); + commonTestCases.hasText(S.current.syncing_wallet_alert_content); + } + + commonTestCases.hasValueKey('transactions_page_header_row_key'); + commonTestCases.hasText(S.current.transactions); + commonTestCases.hasValueKey('transactions_page_header_row_transaction_filter_button_key'); + } + + Future confirmTransactionHistoryListDisplaysCorrectly(bool hasTxHistoryWhileSyncing) async { + // Retrieve the TransactionsPage widget and its DashboardViewModel + final transactionsPage = tester.widget(find.byType(TransactionsPage)); + final dashboardViewModel = transactionsPage.dashboardViewModel; + + // Define a timeout to prevent infinite loops + // Putting at one hour for cases like monero that takes time to sync + final timeout = Duration(hours: 1); + final pollingInterval = Duration(seconds: 2); + final endTime = DateTime.now().add(timeout); + + while (DateTime.now().isBefore(endTime)) { + final isSynced = dashboardViewModel.status is SyncedSyncStatus; + final itemsLoaded = dashboardViewModel.items.isNotEmpty; + + // Perform item checks if items are loaded + if (itemsLoaded) { + await _performItemChecks(dashboardViewModel); + } else { + // Verify placeholder when items are not loaded + _verifyPlaceholder(); + } + + // Determine if we should exit the loop + if (_shouldExitLoop(hasTxHistoryWhileSyncing, isSynced, itemsLoaded)) { + break; + } + + // Pump the UI and wait for the next polling interval + await tester.pump(pollingInterval); + } + + // After the loop, verify that both status is synced and items are loaded + if (!_isFinalStateValid(dashboardViewModel)) { + throw TimeoutException('Dashboard did not sync and load items within the allotted time.'); + } + } + + bool _shouldExitLoop(bool hasTxHistoryWhileSyncing, bool isSynced, bool itemsLoaded) { + if (hasTxHistoryWhileSyncing) { + // When hasTxHistoryWhileSyncing is true, exit when status is synced + return isSynced; + } else { + // When hasTxHistoryWhileSyncing is false, exit when status is synced and items are loaded + return isSynced && itemsLoaded; + } + } + + void _verifyPlaceholder() { + commonTestCases.hasValueKey('transactions_page_placeholder_transactions_text_key'); + commonTestCases.hasText(S.current.placeholder_transactions); + } + + bool _isFinalStateValid(DashboardViewModel dashboardViewModel) { + final isSynced = dashboardViewModel.status is SyncedSyncStatus; + final itemsLoaded = dashboardViewModel.items.isNotEmpty; + return isSynced && itemsLoaded; + } + + Future _performItemChecks(DashboardViewModel dashboardViewModel) async { + List items = dashboardViewModel.items; + for (var item in items) { + final keyId = (item.key as ValueKey).value; + tester.printToConsole('\n'); + tester.printToConsole(keyId); + + await commonTestCases.dragUntilVisible(keyId, 'transactions_page_list_view_builder_key'); + await tester.pump(); + + final isWidgetVisible = tester.any(find.byKey(ValueKey(keyId))); + if (!isWidgetVisible) { + tester.printToConsole('Moving to next visible item on list'); + continue; + } + ; + await tester.pump(); + + if (item is DateSectionItem) { + await _verifyDateSectionItem(item); + } else if (item is TransactionListItem) { + tester.printToConsole(item.formattedTitle); + tester.printToConsole(item.formattedFiatAmount); + tester.printToConsole('\n'); + await _verifyTransactionListItemDisplay(item, dashboardViewModel); + } else if (item is AnonpayTransactionListItem) { + await _verifyAnonpayTransactionListItemDisplay(item); + } else if (item is TradeListItem) { + await _verifyTradeListItemDisplay(item); + } else if (item is OrderListItem) { + await _verifyOrderListItemDisplay(item); + } + } + } + + Future _verifyDateSectionItem(DateSectionItem item) async { + final title = DateFormatter.convertDateTimeToReadableString(item.date); + tester.printToConsole(title); + await tester.pump(); + + commonTestCases.findWidgetViaDescendant( + of: find.byKey(item.key), + matching: find.text(title), + ); + } + + Future _verifyTransactionListItemDisplay( + TransactionListItem item, + DashboardViewModel dashboardViewModel, + ) async { + final keyId = + '${dashboardViewModel.type.name}_transaction_history_item_${item.transaction.id}_key'; + + if (item.hasTokens && item.assetOfTransaction == null) return; + + //* ==============Confirm it has the right key for this item ======== + commonTestCases.hasValueKey(keyId); + + //* ======Confirm it displays the properly formatted amount========== + commonTestCases.findWidgetViaDescendant( + of: find.byKey(ValueKey(keyId)), + matching: find.text(item.formattedCryptoAmount), + ); + + //* ======Confirm it displays the properly formatted title=========== + final transactionType = dashboardViewModel.getTransactionType(item.transaction); + + final title = item.formattedTitle + item.formattedStatus + transactionType; + + commonTestCases.findWidgetViaDescendant( + of: find.byKey(ValueKey(keyId)), + matching: find.text(title), + ); + + //* ======Confirm it displays the properly formatted date============ + final formattedDate = DateFormat('HH:mm').format(item.transaction.date); + commonTestCases.findWidgetViaDescendant( + of: find.byKey(ValueKey(keyId)), + matching: find.text(formattedDate), + ); + + //* ======Confirm it displays the properly formatted fiat amount===== + final formattedFiatAmount = + dashboardViewModel.balanceViewModel.isFiatDisabled ? '' : item.formattedFiatAmount; + if (formattedFiatAmount.isNotEmpty) { + commonTestCases.findWidgetViaDescendant( + of: find.byKey(ValueKey(keyId)), + matching: find.text(formattedFiatAmount), + ); + } + + //* ======Confirm it displays the right image based on the transaction direction===== + final imageToUse = item.transaction.direction == TransactionDirection.incoming + ? 'assets/images/down_arrow.png' + : 'assets/images/up_arrow.png'; + + find.widgetWithImage(Container, AssetImage(imageToUse)); + } + + Future _verifyAnonpayTransactionListItemDisplay(AnonpayTransactionListItem item) async { + final keyId = 'anonpay_invoice_transaction_list_item_${item.transaction.invoiceId}_key'; + + //* ==============Confirm it has the right key for this item ======== + commonTestCases.hasValueKey(keyId); + + //* ==============Confirm it displays the correct provider ========================= + commonTestCases.hasText(item.transaction.provider); + + //* ===========Confirm it displays the properly formatted amount with currency ======== + final currency = item.transaction.fiatAmount != null + ? item.transaction.fiatEquiv ?? '' + : CryptoCurrency.fromFullName(item.transaction.coinTo).name.toUpperCase(); + + final amount = + item.transaction.fiatAmount?.toString() ?? (item.transaction.amountTo?.toString() ?? ''); + + final amountCurrencyText = amount + ' ' + currency; + + commonTestCases.hasText(amountCurrencyText); + + //* ======Confirm it displays the properly formatted date================= + final formattedDate = DateFormat('HH:mm').format(item.transaction.createdAt); + commonTestCases.hasText(formattedDate); + + //* ===============Confirm it displays the right image==================== + find.widgetWithImage(ClipRRect, AssetImage('assets/images/trocador.png')); + } + + Future _verifyTradeListItemDisplay(TradeListItem item) async { + final keyId = 'trade_list_item_${item.trade.id}_key'; + + //* ==============Confirm it has the right key for this item ======== + commonTestCases.hasValueKey(keyId); + + //* ==============Confirm it displays the correct provider ========================= + final conversionFlow = '${item.trade.from.toString()} → ${item.trade.to.toString()}'; + + commonTestCases.hasText(conversionFlow); + + //* ===========Confirm it displays the properly formatted amount with its crypto tag ======== + + final amountCryptoText = item.tradeFormattedAmount + ' ' + item.trade.from.toString(); + + commonTestCases.hasText(amountCryptoText); + + //* ======Confirm it displays the properly formatted date================= + final createdAtFormattedDate = + item.trade.createdAt != null ? DateFormat('HH:mm').format(item.trade.createdAt!) : null; + + if (createdAtFormattedDate != null) { + commonTestCases.hasText(createdAtFormattedDate); + } + + //* ===============Confirm it displays the right image==================== + commonTestCases.hasValueKey(item.trade.provider.image); + } + + Future _verifyOrderListItemDisplay(OrderListItem item) async { + final keyId = 'order_list_item_${item.order.id}_key'; + + //* ==============Confirm it has the right key for this item ======== + commonTestCases.hasValueKey(keyId); + + //* ==============Confirm it displays the correct provider ========================= + final orderFlow = '${item.order.from!} → ${item.order.to}'; + + commonTestCases.hasText(orderFlow); + + //* ===========Confirm it displays the properly formatted amount with its crypto tag ======== + + final amountCryptoText = item.orderFormattedAmount + ' ' + item.order.to!; + + commonTestCases.hasText(amountCryptoText); + + //* ======Confirm it displays the properly formatted date================= + final createdAtFormattedDate = DateFormat('HH:mm').format(item.order.createdAt); + + commonTestCases.hasText(createdAtFormattedDate); + } +} diff --git a/integration_test/robots/wallet_group_description_page_robot.dart b/integration_test/robots/wallet_group_description_page_robot.dart new file mode 100644 index 000000000..57500dc3c --- /dev/null +++ b/integration_test/robots/wallet_group_description_page_robot.dart @@ -0,0 +1,32 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/new_wallet/wallet_group_description_page.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../components/common_test_cases.dart'; + +class WalletGroupDescriptionPageRobot { + WalletGroupDescriptionPageRobot(this.tester) : commonTestCases = CommonTestCases(tester); + + final WidgetTester tester; + final CommonTestCases commonTestCases; + + Future isWalletGroupDescriptionPage() async { + await commonTestCases.isSpecificPage(); + } + + void hasTitle() { + commonTestCases.hasText(S.current.wallet_group); + } + + Future navigateToCreateNewSeedPage() async { + await commonTestCases.tapItemByKey( + 'wallet_group_description_page_create_new_seed_button_key', + ); + } + + Future navigateToChooseWalletGroup() async { + await commonTestCases.tapItemByKey( + 'wallet_group_description_page_choose_wallet_group_button_key', + ); + } +} diff --git a/integration_test/robots/wallet_keys_robot.dart b/integration_test/robots/wallet_keys_robot.dart new file mode 100644 index 000000000..f6aeb3a66 --- /dev/null +++ b/integration_test/robots/wallet_keys_robot.dart @@ -0,0 +1,162 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/reactions/wallet_connect.dart'; +import 'package:cake_wallet/src/screens/wallet_keys/wallet_keys_page.dart'; +import 'package:cake_wallet/store/app_store.dart'; +import 'package:cw_core/monero_wallet_keys.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:cw_monero/monero_wallet.dart'; +import 'package:cw_wownero/wownero_wallet.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:polyseed/polyseed.dart'; + +import '../components/common_test_cases.dart'; + +class WalletKeysAndSeedPageRobot { + WalletKeysAndSeedPageRobot(this.tester) : commonTestCases = CommonTestCases(tester); + + final WidgetTester tester; + final CommonTestCases commonTestCases; + + Future isWalletKeysAndSeedPage() async { + await commonTestCases.isSpecificPage(); + } + + void hasTitle() { + final walletKeysPage = tester.widget(find.byType(WalletKeysPage)); + final walletKeysViewModel = walletKeysPage.walletKeysViewModel; + commonTestCases.hasText(walletKeysViewModel.title); + } + + void hasShareWarning() { + commonTestCases.hasText(S.current.do_not_share_warning_text.toUpperCase()); + } + + Future confirmWalletCredentials(WalletType walletType) async { + final walletKeysPage = tester.widget(find.byType(WalletKeysPage)); + final walletKeysViewModel = walletKeysPage.walletKeysViewModel; + + final appStore = walletKeysViewModel.appStore; + final walletName = walletType.name; + bool hasSeed = appStore.wallet!.seed != null; + bool hasHexSeed = appStore.wallet!.hexSeed != null; + bool hasPrivateKey = appStore.wallet!.privateKey != null; + + if (walletType == WalletType.monero) { + final moneroWallet = appStore.wallet as MoneroWallet; + final lang = PolyseedLang.getByPhrase(moneroWallet.seed); + final legacySeed = moneroWallet.seedLegacy(lang.nameEnglish); + + _confirmMoneroWalletCredentials( + appStore, + walletName, + moneroWallet.seed, + legacySeed, + ); + } + + if (walletType == WalletType.wownero) { + final wowneroWallet = appStore.wallet as WowneroWallet; + final lang = PolyseedLang.getByPhrase(wowneroWallet.seed); + final legacySeed = wowneroWallet.seedLegacy(lang.nameEnglish); + + _confirmMoneroWalletCredentials( + appStore, + walletName, + wowneroWallet.seed, + legacySeed, + ); + } + + if (walletType == WalletType.bitcoin || + walletType == WalletType.litecoin || + walletType == WalletType.bitcoinCash) { + commonTestCases.hasText(appStore.wallet!.seed!); + tester.printToConsole('$walletName wallet has seeds properly displayed'); + } + + if (isEVMCompatibleChain(walletType) || + walletType == WalletType.solana || + walletType == WalletType.tron) { + if (hasSeed) { + commonTestCases.hasText(appStore.wallet!.seed!); + tester.printToConsole('$walletName wallet has seeds properly displayed'); + } + if (hasPrivateKey) { + commonTestCases.hasText(appStore.wallet!.privateKey!); + tester.printToConsole('$walletName wallet has private key properly displayed'); + } + } + + if (walletType == WalletType.nano || walletType == WalletType.banano) { + if (hasSeed) { + commonTestCases.hasText(appStore.wallet!.seed!); + tester.printToConsole('$walletName wallet has seeds properly displayed'); + } + if (hasHexSeed) { + commonTestCases.hasText(appStore.wallet!.hexSeed!); + tester.printToConsole('$walletName wallet has hexSeed properly displayed'); + } + if (hasPrivateKey) { + commonTestCases.hasText(appStore.wallet!.privateKey!); + tester.printToConsole('$walletName wallet has private key properly displayed'); + } + } + + await commonTestCases.defaultSleepTime(seconds: 5); + } + + void _confirmMoneroWalletCredentials( + AppStore appStore, + String walletName, + String seed, + String legacySeed, + ) { + final keys = appStore.wallet!.keys as MoneroWalletKeys; + + final hasPublicSpendKey = commonTestCases.isKeyPresent( + '${walletName}_wallet_public_spend_key_item_key', + ); + final hasPrivateSpendKey = commonTestCases.isKeyPresent( + '${walletName}_wallet_private_spend_key_item_key', + ); + final hasPublicViewKey = commonTestCases.isKeyPresent( + '${walletName}_wallet_public_view_key_item_key', + ); + final hasPrivateViewKey = commonTestCases.isKeyPresent( + '${walletName}_wallet_private_view_key_item_key', + ); + final hasSeeds = seed.isNotEmpty; + final hasSeedLegacy = Polyseed.isValidSeed(seed); + + if (hasPublicSpendKey) { + commonTestCases.hasText(keys.publicSpendKey); + tester.printToConsole('$walletName wallet has public spend key properly displayed'); + } + if (hasPrivateSpendKey) { + commonTestCases.hasText(keys.privateSpendKey); + tester.printToConsole('$walletName wallet has private spend key properly displayed'); + } + if (hasPublicViewKey) { + commonTestCases.hasText(keys.publicViewKey); + tester.printToConsole('$walletName wallet has public view key properly displayed'); + } + if (hasPrivateViewKey) { + commonTestCases.hasText(keys.privateViewKey); + tester.printToConsole('$walletName wallet has private view key properly displayed'); + } + if (hasSeeds) { + commonTestCases.hasText(seed); + tester.printToConsole('$walletName wallet has seeds properly displayed'); + } + if (hasSeedLegacy) { + commonTestCases.hasText(legacySeed); + tester.printToConsole('$walletName wallet has legacy seeds properly displayed'); + } + } + + Future backToDashboard() async { + tester.printToConsole('Going back to dashboard from credentials page'); + await commonTestCases.goBack(); + await commonTestCases.goBack(); + } +} diff --git a/integration_test/robots/wallet_list_page_robot.dart b/integration_test/robots/wallet_list_page_robot.dart new file mode 100644 index 000000000..b46d4ca95 --- /dev/null +++ b/integration_test/robots/wallet_list_page_robot.dart @@ -0,0 +1,27 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../components/common_test_cases.dart'; + +class WalletListPageRobot { + WalletListPageRobot(this.tester) : commonTestCases = CommonTestCases(tester); + + final WidgetTester tester; + late CommonTestCases commonTestCases; + + Future isWalletListPage() async { + await commonTestCases.isSpecificPage(); + } + + void displaysCorrectTitle() { + commonTestCases.hasText(S.current.wallets); + } + + Future navigateToCreateNewWalletPage() async { + commonTestCases.tapItemByKey('wallet_list_page_create_new_wallet_button_key'); + } + + Future navigateToRestoreWalletOptionsPage() async { + commonTestCases.tapItemByKey('wallet_list_page_restore_wallet_button_key'); + } +} diff --git a/integration_test/robots/wallet_seed_page_robot.dart b/integration_test/robots/wallet_seed_page_robot.dart new file mode 100644 index 000000000..d52f3b1ec --- /dev/null +++ b/integration_test/robots/wallet_seed_page_robot.dart @@ -0,0 +1,57 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/seed/wallet_seed_page.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../components/common_test_cases.dart'; + +class WalletSeedPageRobot { + WalletSeedPageRobot(this.tester) : commonTestCases = CommonTestCases(tester); + + final WidgetTester tester; + late CommonTestCases commonTestCases; + + Future isWalletSeedPage() async { + await commonTestCases.isSpecificPage(); + } + + Future onNextButtonPressed() async { + await commonTestCases.tapItemByKey('wallet_seed_page_next_button_key'); + await commonTestCases.defaultSleepTime(); + } + + Future onConfirmButtonOnSeedAlertDialogPressed() async { + await commonTestCases.tapItemByKey('wallet_seed_page_seed_alert_confirm_button_key'); + await commonTestCases.defaultSleepTime(); + } + + Future onBackButtonOnSeedAlertDialogPressed() async { + await commonTestCases.tapItemByKey('wallet_seed_page_seed_alert_back_button_key'); + await commonTestCases.defaultSleepTime(); + } + + void confirmWalletDetailsDisplayCorrectly() { + final walletSeedPage = tester.widget(find.byType(WalletSeedPage)); + + final walletSeedViewModel = walletSeedPage.walletSeedViewModel; + + final walletName = walletSeedViewModel.name; + final walletSeeds = walletSeedViewModel.seed; + + commonTestCases.hasText(walletName); + commonTestCases.hasText(walletSeeds); + } + + void confirmWalletSeedReminderDisplays() { + commonTestCases.hasText(S.current.seed_reminder); + } + + Future onSaveSeedsButtonPressed() async { + await commonTestCases.tapItemByKey('wallet_seed_page_save_seeds_button_key'); + await commonTestCases.defaultSleepTime(); + } + + Future onCopySeedsButtonPressed() async { + await commonTestCases.tapItemByKey('wallet_seed_page_copy_seeds_button_key'); + await commonTestCases.defaultSleepTime(); + } +} diff --git a/integration_test/test_suites/confirm_seeds_flow_test.dart b/integration_test/test_suites/confirm_seeds_flow_test.dart new file mode 100644 index 000000000..bf6fd5a5f --- /dev/null +++ b/integration_test/test_suites/confirm_seeds_flow_test.dart @@ -0,0 +1,107 @@ +import 'package:cake_wallet/wallet_types.g.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import '../components/common_test_constants.dart'; +import '../components/common_test_flows.dart'; +import '../robots/auth_page_robot.dart'; +import '../robots/dashboard_page_robot.dart'; +import '../robots/security_and_backup_page_robot.dart'; +import '../robots/wallet_keys_robot.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + AuthPageRobot authPageRobot; + CommonTestFlows commonTestFlows; + DashboardPageRobot dashboardPageRobot; + WalletKeysAndSeedPageRobot walletKeysAndSeedPageRobot; + SecurityAndBackupPageRobot securityAndBackupPageRobot; + + testWidgets( + 'Confirm if the seeds display properly', + (tester) async { + authPageRobot = AuthPageRobot(tester); + commonTestFlows = CommonTestFlows(tester); + dashboardPageRobot = DashboardPageRobot(tester); + walletKeysAndSeedPageRobot = WalletKeysAndSeedPageRobot(tester); + securityAndBackupPageRobot = SecurityAndBackupPageRobot(tester); + + // Start the app + await commonTestFlows.startAppFlow( + ValueKey('confirm_creds_display_correctly_flow_app_key'), + ); + + await commonTestFlows.welcomePageToCreateNewWalletFlow( + WalletType.solana, + CommonTestConstants.pin, + ); + + await dashboardPageRobot.confirmWalletTypeIsDisplayedCorrectly(WalletType.solana); + + await _confirmSeedsFlowForWalletType( + WalletType.solana, + authPageRobot, + dashboardPageRobot, + securityAndBackupPageRobot, + walletKeysAndSeedPageRobot, + tester, + ); + + // Do the same for other available wallet types + for (var walletType in availableWalletTypes) { + if (walletType == WalletType.solana) { + continue; + } + + await commonTestFlows.switchToWalletMenuFromDashboardPage(); + + await commonTestFlows.createNewWalletFromWalletMenu(walletType); + + await dashboardPageRobot.confirmWalletTypeIsDisplayedCorrectly(walletType); + + await _confirmSeedsFlowForWalletType( + walletType, + authPageRobot, + dashboardPageRobot, + securityAndBackupPageRobot, + walletKeysAndSeedPageRobot, + tester, + ); + } + + await Future.delayed(Duration(seconds: 15)); + }, + ); +} + +Future _confirmSeedsFlowForWalletType( + WalletType walletType, + AuthPageRobot authPageRobot, + DashboardPageRobot dashboardPageRobot, + SecurityAndBackupPageRobot securityAndBackupPageRobot, + WalletKeysAndSeedPageRobot walletKeysAndSeedPageRobot, + WidgetTester tester, +) async { + await dashboardPageRobot.openDrawerMenu(); + await dashboardPageRobot.dashboardMenuWidgetRobot.navigateToSecurityAndBackupPage(); + + await securityAndBackupPageRobot.navigateToShowKeysPage(); + + final onAuthPage = authPageRobot.onAuthPage(); + if (onAuthPage) { + await authPageRobot.enterPinCode(CommonTestConstants.pin); + } + + await tester.pumpAndSettle(); + + await walletKeysAndSeedPageRobot.isWalletKeysAndSeedPage(); + walletKeysAndSeedPageRobot.hasTitle(); + walletKeysAndSeedPageRobot.hasShareWarning(); + + walletKeysAndSeedPageRobot.confirmWalletCredentials(walletType); + + await walletKeysAndSeedPageRobot.backToDashboard(); +} diff --git a/integration_test/test_suites/create_wallet_flow_test.dart b/integration_test/test_suites/create_wallet_flow_test.dart new file mode 100644 index 000000000..2a50dbbe8 --- /dev/null +++ b/integration_test/test_suites/create_wallet_flow_test.dart @@ -0,0 +1,57 @@ +import 'package:cake_wallet/wallet_types.g.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import '../components/common_test_constants.dart'; +import '../components/common_test_flows.dart'; +import '../robots/dashboard_page_robot.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + CommonTestFlows commonTestFlows; + DashboardPageRobot dashboardPageRobot; + + testWidgets( + 'Create Wallet Flow', + (tester) async { + commonTestFlows = CommonTestFlows(tester); + dashboardPageRobot = DashboardPageRobot(tester); + + // Start the app + await commonTestFlows.startAppFlow( + ValueKey('create_wallets_through_seeds_test_app_key'), + ); + + await commonTestFlows.welcomePageToCreateNewWalletFlow( + WalletType.solana, + CommonTestConstants.pin, + ); + + // Confirm it actually restores a solana wallet + await dashboardPageRobot.confirmWalletTypeIsDisplayedCorrectly(WalletType.solana); + + // Do the same for other available wallet types + for (var walletType in availableWalletTypes) { + if (walletType == WalletType.solana) { + continue; + } + + await commonTestFlows.switchToWalletMenuFromDashboardPage(); + + await commonTestFlows.createNewWalletFromWalletMenu(walletType); + + await dashboardPageRobot.confirmWalletTypeIsDisplayedCorrectly(walletType); + } + + // Goes to the wallet menu and provides a confirmation that all the wallets were correctly restored + await commonTestFlows.switchToWalletMenuFromDashboardPage(); + + commonTestFlows.confirmAllAvailableWalletTypeIconsDisplayCorrectly(); + + await Future.delayed(Duration(seconds: 5)); + }, + ); +} diff --git a/integration_test/test_suites/exchange_flow_test.dart b/integration_test/test_suites/exchange_flow_test.dart index 6c993634c..c36ef9396 100644 --- a/integration_test/test_suites/exchange_flow_test.dart +++ b/integration_test/test_suites/exchange_flow_test.dart @@ -9,6 +9,7 @@ import '../robots/dashboard_page_robot.dart'; import '../robots/exchange_confirm_page_robot.dart'; import '../robots/exchange_page_robot.dart'; import '../robots/exchange_trade_page_robot.dart'; +import 'package:cake_wallet/.secrets.g.dart' as secrets; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); @@ -20,40 +21,42 @@ void main() { ExchangeTradePageRobot exchangeTradePageRobot; ExchangeConfirmPageRobot exchangeConfirmPageRobot; - group('Exchange Flow Tests', () { - testWidgets('Exchange flow', (tester) async { - authPageRobot = AuthPageRobot(tester); - commonTestFlows = CommonTestFlows(tester); - exchangePageRobot = ExchangePageRobot(tester); - dashboardPageRobot = DashboardPageRobot(tester); - exchangeTradePageRobot = ExchangeTradePageRobot(tester); - exchangeConfirmPageRobot = ExchangeConfirmPageRobot(tester); + testWidgets('Exchange flow', (tester) async { + authPageRobot = AuthPageRobot(tester); + commonTestFlows = CommonTestFlows(tester); + exchangePageRobot = ExchangePageRobot(tester); + dashboardPageRobot = DashboardPageRobot(tester); + exchangeTradePageRobot = ExchangeTradePageRobot(tester); + exchangeConfirmPageRobot = ExchangeConfirmPageRobot(tester); - await commonTestFlows.startAppFlow(ValueKey('exchange_app_test_key')); - await commonTestFlows.restoreWalletThroughSeedsFlow(); - await dashboardPageRobot.navigateToExchangePage(); + await commonTestFlows.startAppFlow(ValueKey('exchange_app_test_key')); + await commonTestFlows.welcomePageToRestoreWalletThroughSeedsFlow( + CommonTestConstants.testWalletType, + secrets.solanaTestWalletSeeds, + CommonTestConstants.pin, + ); + await dashboardPageRobot.navigateToExchangePage(); - // ----------- Exchange Page ------------- - await exchangePageRobot.selectDepositCurrency(CommonTestConstants.testDepositCurrency); - await exchangePageRobot.selectReceiveCurrency(CommonTestConstants.testReceiveCurrency); + // ----------- Exchange Page ------------- + await exchangePageRobot.selectDepositCurrency(CommonTestConstants.testDepositCurrency); + await exchangePageRobot.selectReceiveCurrency(CommonTestConstants.testReceiveCurrency); - await exchangePageRobot.enterDepositAmount(CommonTestConstants.exchangeTestAmount); - await exchangePageRobot.enterDepositRefundAddress( - depositAddress: CommonTestConstants.testWalletAddress, - ); - await exchangePageRobot.enterReceiveAddress(CommonTestConstants.testWalletAddress); - - await exchangePageRobot.onExchangeButtonPressed(); + await exchangePageRobot.enterDepositAmount(CommonTestConstants.exchangeTestAmount); + await exchangePageRobot.enterDepositRefundAddress( + depositAddress: CommonTestConstants.testWalletAddress, + ); + await exchangePageRobot.enterReceiveAddress(CommonTestConstants.testWalletAddress); - await exchangePageRobot.handleErrors(CommonTestConstants.exchangeTestAmount); + await exchangePageRobot.onExchangeButtonPressed(); - final onAuthPage = authPageRobot.onAuthPage(); - if (onAuthPage) { - await authPageRobot.enterPinCode(CommonTestConstants.pin, false); - } + await exchangePageRobot.handleErrors(CommonTestConstants.exchangeTestAmount); - await exchangeConfirmPageRobot.onSavedTradeIdButtonPressed(); - await exchangeTradePageRobot.onGotItButtonPressed(); - }); + final onAuthPage = authPageRobot.onAuthPage(); + if (onAuthPage) { + await authPageRobot.enterPinCode(CommonTestConstants.pin); + } + + await exchangeConfirmPageRobot.onSavedTradeIdButtonPressed(); + await exchangeTradePageRobot.onGotItButtonPressed(); }); } diff --git a/integration_test/test_suites/restore_wallet_through_seeds_flow_test.dart b/integration_test/test_suites/restore_wallet_through_seeds_flow_test.dart new file mode 100644 index 000000000..a810aa722 --- /dev/null +++ b/integration_test/test_suites/restore_wallet_through_seeds_flow_test.dart @@ -0,0 +1,63 @@ +import 'package:cake_wallet/wallet_types.g.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import '../components/common_test_constants.dart'; +import '../components/common_test_flows.dart'; +import '../robots/dashboard_page_robot.dart'; +import 'package:cake_wallet/.secrets.g.dart' as secrets; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + CommonTestFlows commonTestFlows; + DashboardPageRobot dashboardPageRobot; + + testWidgets( + 'Restoring Wallets Through Seeds', + (tester) async { + commonTestFlows = CommonTestFlows(tester); + dashboardPageRobot = DashboardPageRobot(tester); + + // Start the app + await commonTestFlows.startAppFlow( + ValueKey('restore_wallets_through_seeds_test_app_key'), + ); + + // Restore the first wallet type: Solana + await commonTestFlows.welcomePageToRestoreWalletThroughSeedsFlow( + WalletType.solana, + secrets.solanaTestWalletSeeds, + CommonTestConstants.pin, + ); + + // Confirm it actually restores a solana wallet + await dashboardPageRobot.confirmWalletTypeIsDisplayedCorrectly(WalletType.solana); + + // Do the same for other available wallet types + for (var walletType in availableWalletTypes) { + if (walletType == WalletType.solana) { + continue; + } + + await commonTestFlows.switchToWalletMenuFromDashboardPage(); + + await commonTestFlows.restoreWalletFromWalletMenu( + walletType, + commonTestFlows.getWalletSeedsByWalletType(walletType), + ); + + await dashboardPageRobot.confirmWalletTypeIsDisplayedCorrectly(walletType); + } + + // Goes to the wallet menu and provides a visual confirmation that all the wallets were correctly restored + await commonTestFlows.switchToWalletMenuFromDashboardPage(); + + commonTestFlows.confirmAllAvailableWalletTypeIconsDisplayCorrectly(); + + await Future.delayed(Duration(seconds: 5)); + }, + ); +} diff --git a/integration_test/test_suites/send_flow_test.dart b/integration_test/test_suites/send_flow_test.dart index 38ac1574f..7a46435b8 100644 --- a/integration_test/test_suites/send_flow_test.dart +++ b/integration_test/test_suites/send_flow_test.dart @@ -6,6 +6,7 @@ import '../components/common_test_constants.dart'; import '../components/common_test_flows.dart'; import '../robots/dashboard_page_robot.dart'; import '../robots/send_page_robot.dart'; +import 'package:cake_wallet/.secrets.g.dart' as secrets; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); @@ -14,28 +15,30 @@ void main() { CommonTestFlows commonTestFlows; DashboardPageRobot dashboardPageRobot; - group('Send Flow Tests', () { - testWidgets('Send flow', (tester) async { - commonTestFlows = CommonTestFlows(tester); - sendPageRobot = SendPageRobot(tester: tester); - dashboardPageRobot = DashboardPageRobot(tester); + testWidgets('Send flow', (tester) async { + commonTestFlows = CommonTestFlows(tester); + sendPageRobot = SendPageRobot(tester: tester); + dashboardPageRobot = DashboardPageRobot(tester); - await commonTestFlows.startAppFlow(ValueKey('send_test_app_key')); - await commonTestFlows.restoreWalletThroughSeedsFlow(); - await dashboardPageRobot.navigateToSendPage(); + await commonTestFlows.startAppFlow(ValueKey('send_test_app_key')); + await commonTestFlows.welcomePageToRestoreWalletThroughSeedsFlow( + CommonTestConstants.testWalletType, + secrets.solanaTestWalletSeeds, + CommonTestConstants.pin, + ); + await dashboardPageRobot.navigateToSendPage(); - await sendPageRobot.enterReceiveAddress(CommonTestConstants.testWalletAddress); - await sendPageRobot.selectReceiveCurrency(CommonTestConstants.testReceiveCurrency); - await sendPageRobot.enterAmount(CommonTestConstants.sendTestAmount); - await sendPageRobot.selectTransactionPriority(); + await sendPageRobot.enterReceiveAddress(CommonTestConstants.testWalletAddress); + await sendPageRobot.selectReceiveCurrency(CommonTestConstants.testReceiveCurrency); + await sendPageRobot.enterAmount(CommonTestConstants.sendTestAmount); + await sendPageRobot.selectTransactionPriority(); - await sendPageRobot.onSendButtonPressed(); + await sendPageRobot.onSendButtonPressed(); - await sendPageRobot.handleSendResult(); + await sendPageRobot.handleSendResult(); - await sendPageRobot.onSendButtonOnConfirmSendingDialogPressed(); + await sendPageRobot.onSendButtonOnConfirmSendingDialogPressed(); - await sendPageRobot.onSentDialogPopUp(); - }); + await sendPageRobot.onSentDialogPopUp(); }); } diff --git a/integration_test/test_suites/transaction_history_flow_test.dart b/integration_test/test_suites/transaction_history_flow_test.dart new file mode 100644 index 000000000..8af6d39fd --- /dev/null +++ b/integration_test/test_suites/transaction_history_flow_test.dart @@ -0,0 +1,70 @@ +import 'package:cw_core/wallet_type.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import '../components/common_test_constants.dart'; +import '../components/common_test_flows.dart'; +import '../robots/dashboard_page_robot.dart'; +import '../robots/transactions_page_robot.dart'; +import 'package:cake_wallet/.secrets.g.dart' as secrets; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + CommonTestFlows commonTestFlows; + DashboardPageRobot dashboardPageRobot; + TransactionsPageRobot transactionsPageRobot; + + /// Two Test Scenarios + /// - Fully Synchronizes and display the transaction history either immediately or few seconds after fully synchronizing + /// - Displays the transaction history progressively as synchronizing happens + testWidgets('Transaction history flow', (tester) async { + commonTestFlows = CommonTestFlows(tester); + dashboardPageRobot = DashboardPageRobot(tester); + transactionsPageRobot = TransactionsPageRobot(tester); + + await commonTestFlows.startAppFlow( + ValueKey('confirm_creds_display_correctly_flow_app_key'), + ); + + /// Test Scenario 1 - Displays transaction history list after fully synchronizing. + /// + /// For Solana/Tron WalletTypes. + await commonTestFlows.welcomePageToRestoreWalletThroughSeedsFlow( + WalletType.solana, + secrets.solanaTestWalletSeeds, + CommonTestConstants.pin, + ); + + await dashboardPageRobot.confirmWalletTypeIsDisplayedCorrectly(WalletType.solana); + + await dashboardPageRobot.swipeDashboardTab(true); + + await transactionsPageRobot.isTransactionsPage(); + + await transactionsPageRobot.confirmTransactionsPageConstantsDisplayProperly(); + + await transactionsPageRobot.confirmTransactionHistoryListDisplaysCorrectly(false); + + /// Test Scenario 2 - Displays transaction history list while synchronizing. + /// + /// For bitcoin/Monero/Wownero WalletTypes. + await commonTestFlows.switchToWalletMenuFromDashboardPage(); + + await commonTestFlows.restoreWalletFromWalletMenu( + WalletType.bitcoin, + secrets.bitcoinTestWalletSeeds, + ); + + await dashboardPageRobot.confirmWalletTypeIsDisplayedCorrectly(WalletType.bitcoin); + + await dashboardPageRobot.swipeDashboardTab(true); + + await transactionsPageRobot.isTransactionsPage(); + + await transactionsPageRobot.confirmTransactionsPageConstantsDisplayProperly(); + + await transactionsPageRobot.confirmTransactionHistoryListDisplaysCorrectly(true); + }); +} diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 322ef6f86..470bdf4e7 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -109,15 +109,15 @@ PODS: - FlutterMacOS - permission_handler_apple (9.1.1): - Flutter - - Protobuf (3.27.2) + - Protobuf (3.28.2) - ReachabilitySwift (5.2.3) - reactive_ble_mobile (0.0.1): - Flutter - Protobuf (~> 3.5) - SwiftProtobuf (~> 1.0) - - SDWebImage (5.19.4): - - SDWebImage/Core (= 5.19.4) - - SDWebImage/Core (5.19.4) + - SDWebImage (5.19.7): + - SDWebImage/Core (= 5.19.7) + - SDWebImage/Core (5.19.7) - sensitive_clipboard (0.0.1): - Flutter - share_plus (0.0.1): @@ -271,10 +271,10 @@ SPEC CHECKSUMS: package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 - Protobuf: fb2c13674723f76ff6eede14f78847a776455fa2 + Protobuf: 28c89b24435762f60244e691544ed80f50d82701 ReachabilitySwift: 7f151ff156cea1481a8411701195ac6a984f4979 reactive_ble_mobile: 9ce6723d37ccf701dbffd202d487f23f5de03b4c - SDWebImage: 066c47b573f408f18caa467d71deace7c0f8280d + SDWebImage: 8a6b7b160b4d710e2a22b6900e25301075c34cb3 sensitive_clipboard: d4866e5d176581536c27bb1618642ee83adca986 share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 diff --git a/lib/src/screens/InfoPage.dart b/lib/src/screens/InfoPage.dart index 5398df22c..84b9e8632 100644 --- a/lib/src/screens/InfoPage.dart +++ b/lib/src/screens/InfoPage.dart @@ -21,6 +21,7 @@ abstract class InfoPage extends BasePage { String get pageTitle; String get pageDescription; String get buttonText; + Key? get buttonKey; void Function(BuildContext) get onPressed; @override @@ -39,15 +40,14 @@ abstract class InfoPage extends BasePage { alignment: Alignment.center, padding: EdgeInsets.all(24), child: ConstrainedBox( - constraints: BoxConstraints( - maxWidth: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint), + constraints: + BoxConstraints(maxWidth: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint), child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: ConstrainedBox( - constraints: BoxConstraints( - maxHeight: MediaQuery.of(context).size.height * 0.3), + constraints: BoxConstraints(maxHeight: MediaQuery.of(context).size.height * 0.3), child: AspectRatio(aspectRatio: 1, child: image), ), ), @@ -61,14 +61,13 @@ abstract class InfoPage extends BasePage { height: 1.7, fontSize: 14, fontWeight: FontWeight.normal, - color: Theme.of(context) - .extension()! - .secondaryTextColor, + color: Theme.of(context).extension()!.secondaryTextColor, ), ), ), ), PrimaryButton( + key: buttonKey, onPressed: () => onPressed(context), text: buttonText, color: Theme.of(context).primaryColor, diff --git a/lib/src/screens/dashboard/dashboard_page.dart b/lib/src/screens/dashboard/dashboard_page.dart index 953463269..9c012d518 100644 --- a/lib/src/screens/dashboard/dashboard_page.dart +++ b/lib/src/screens/dashboard/dashboard_page.dart @@ -140,7 +140,7 @@ class _DashboardPageView extends BasePage { bool get resizeToAvoidBottomInset => false; @override - Widget get endDrawer => MenuWidget(dashboardViewModel); + Widget get endDrawer => MenuWidget(dashboardViewModel, ValueKey('dashboard_page_drawer_menu_widget_key')); @override Widget leading(BuildContext context) { diff --git a/lib/src/screens/dashboard/pages/nft_details_page.dart b/lib/src/screens/dashboard/pages/nft_details_page.dart index 15d2a2b5c..b8352a672 100644 --- a/lib/src/screens/dashboard/pages/nft_details_page.dart +++ b/lib/src/screens/dashboard/pages/nft_details_page.dart @@ -28,7 +28,10 @@ class NFTDetailsPage extends BasePage { bool get resizeToAvoidBottomInset => false; @override - Widget get endDrawer => MenuWidget(dashboardViewModel); + Widget get endDrawer => MenuWidget( + dashboardViewModel, + ValueKey('nft_details_page_menu_widget_key'), + ); @override Widget trailing(BuildContext context) { diff --git a/lib/src/screens/dashboard/pages/transactions_page.dart b/lib/src/screens/dashboard/pages/transactions_page.dart index b6d1c286b..0db9ac35b 100644 --- a/lib/src/screens/dashboard/pages/transactions_page.dart +++ b/lib/src/screens/dashboard/pages/transactions_page.dart @@ -1,11 +1,13 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/anonpay_transaction_row.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/order_row.dart'; +import 'package:cake_wallet/src/screens/dashboard/widgets/trade_row.dart'; import 'package:cake_wallet/themes/extensions/placeholder_theme.dart'; import 'package:cake_wallet/src/widgets/dashboard_card_widget.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/view_model/dashboard/anonpay_transaction_list_item.dart'; import 'package:cake_wallet/view_model/dashboard/order_list_item.dart'; +import 'package:cake_wallet/view_model/dashboard/trade_list_item.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/sync_status.dart'; import 'package:cw_core/wallet_type.dart'; @@ -14,9 +16,7 @@ import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/header_row.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/date_section_raw.dart'; -import 'package:cake_wallet/src/screens/dashboard/widgets/trade_row.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/transaction_raw.dart'; -import 'package:cake_wallet/view_model/dashboard/trade_list_item.dart'; import 'package:cake_wallet/view_model/dashboard/transaction_list_item.dart'; import 'package:cake_wallet/view_model/dashboard/date_section_item.dart'; import 'package:intl/intl.dart'; @@ -49,6 +49,7 @@ class TransactionsPage extends StatelessWidget { return Padding( padding: const EdgeInsets.fromLTRB(24, 0, 24, 8), child: DashBoardRoundedCardWidget( + key: ValueKey('transactions_page_syncing_alert_card_key'), onTap: () { try { final uri = Uri.parse( @@ -64,82 +65,93 @@ class TransactionsPage extends StatelessWidget { return Container(); } }), - HeaderRow(dashboardViewModel: dashboardViewModel), - Expanded(child: Observer(builder: (_) { - final items = dashboardViewModel.items; + HeaderRow( + dashboardViewModel: dashboardViewModel, + key: ValueKey('transactions_page_header_row_key'), + ), + Expanded( + child: Observer( + builder: (_) { + final items = dashboardViewModel.items; - return items.isNotEmpty - ? ListView.builder( - itemCount: items.length, - itemBuilder: (context, index) { - final item = items[index]; + return items.isNotEmpty + ? ListView.builder( + key: ValueKey('transactions_page_list_view_builder_key'), + itemCount: items.length, + itemBuilder: (context, index) { + final item = items[index]; - if (item is DateSectionItem) { - return DateSectionRaw(date: item.date); - } - - if (item is TransactionListItem) { - if (item.hasTokens && item.assetOfTransaction == null) { - return Container(); - } - - final transaction = item.transaction; - final transactionType = dashboardViewModel.getTransactionType(transaction); - - List tags = []; - if (dashboardViewModel.type == WalletType.bitcoin) { - if (bitcoin!.txIsReceivedSilentPayment(transaction)) { - tags.add(S.of(context).silent_payment); + if (item is DateSectionItem) { + return DateSectionRaw(date: item.date, key: item.key); } - } - if (dashboardViewModel.type == WalletType.litecoin) { - if (bitcoin!.txIsMweb(transaction)) { - tags.add("MWEB"); + + if (item is TransactionListItem) { + if (item.hasTokens && item.assetOfTransaction == null) { + return Container(); + } + + final transaction = item.transaction; + final transactionType = + dashboardViewModel.getTransactionType(transaction); + + List tags = []; + if (dashboardViewModel.type == WalletType.bitcoin) { + if (bitcoin!.txIsReceivedSilentPayment(transaction)) { + tags.add(S.of(context).silent_payment); + } + } + if (dashboardViewModel.type == WalletType.litecoin) { + if (bitcoin!.txIsMweb(transaction)) { + tags.add("MWEB"); + } + } + + return Observer( + builder: (_) => TransactionRow( + key: item.key, + onTap: () => Navigator.of(context) + .pushNamed(Routes.transactionDetails, arguments: transaction), + direction: transaction.direction, + formattedDate: DateFormat('HH:mm').format(transaction.date), + formattedAmount: item.formattedCryptoAmount, + formattedFiatAmount: + dashboardViewModel.balanceViewModel.isFiatDisabled + ? '' + : item.formattedFiatAmount, + isPending: transaction.isPending, + title: + item.formattedTitle + item.formattedStatus + transactionType, + tags: tags, + ), + ); } - } - return Observer( - builder: (_) => TransactionRow( - onTap: () => Navigator.of(context) - .pushNamed(Routes.transactionDetails, arguments: transaction), - direction: transaction.direction, - formattedDate: DateFormat('HH:mm').format(transaction.date), - formattedAmount: item.formattedCryptoAmount, - formattedFiatAmount: - dashboardViewModel.balanceViewModel.isFiatDisabled - ? '' - : item.formattedFiatAmount, - isPending: transaction.isPending, - title: - item.formattedTitle + item.formattedStatus + transactionType, - tags: tags, - ), - ); - } + if (item is AnonpayTransactionListItem) { + final transactionInfo = item.transaction; - if (item is AnonpayTransactionListItem) { - final transactionInfo = item.transaction; + return AnonpayTransactionRow( + key: item.key, + onTap: () => Navigator.of(context).pushNamed( + Routes.anonPayDetailsPage, + arguments: transactionInfo), + currency: transactionInfo.fiatAmount != null + ? transactionInfo.fiatEquiv ?? '' + : CryptoCurrency.fromFullName(transactionInfo.coinTo) + .name + .toUpperCase(), + provider: transactionInfo.provider, + amount: transactionInfo.fiatAmount?.toString() ?? + (transactionInfo.amountTo?.toString() ?? ''), + createdAt: DateFormat('HH:mm').format(transactionInfo.createdAt), + ); + } - return AnonpayTransactionRow( - onTap: () => Navigator.of(context) - .pushNamed(Routes.anonPayDetailsPage, arguments: transactionInfo), - currency: transactionInfo.fiatAmount != null - ? transactionInfo.fiatEquiv ?? '' - : CryptoCurrency.fromFullName(transactionInfo.coinTo) - .name - .toUpperCase(), - provider: transactionInfo.provider, - amount: transactionInfo.fiatAmount?.toString() ?? - (transactionInfo.amountTo?.toString() ?? ''), - createdAt: DateFormat('HH:mm').format(transactionInfo.createdAt), - ); - } + if (item is TradeListItem) { + final trade = item.trade; - if (item is TradeListItem) { - final trade = item.trade; - - return Observer( - builder: (_) => TradeRow( + return Observer( + builder: (_) => TradeRow( + key: item.key, onTap: () => Navigator.of(context) .pushNamed(Routes.tradeDetails, arguments: trade), provider: trade.provider, @@ -148,36 +160,44 @@ class TransactionsPage extends StatelessWidget { createdAtFormattedDate: trade.createdAt != null ? DateFormat('HH:mm').format(trade.createdAt!) : null, - formattedAmount: item.tradeFormattedAmount)); - } + formattedAmount: item.tradeFormattedAmount, + ), + ); + } - if (item is OrderListItem) { - final order = item.order; + if (item is OrderListItem) { + final order = item.order; - return Observer( - builder: (_) => OrderRow( - onTap: () => Navigator.of(context) - .pushNamed(Routes.orderDetails, arguments: order), - provider: order.provider, - from: order.from!, - to: order.to!, - createdAtFormattedDate: - DateFormat('HH:mm').format(order.createdAt), - formattedAmount: item.orderFormattedAmount, - )); - } + return Observer( + builder: (_) => OrderRow( + key: item.key, + onTap: () => Navigator.of(context) + .pushNamed(Routes.orderDetails, arguments: order), + provider: order.provider, + from: order.from!, + to: order.to!, + createdAtFormattedDate: + DateFormat('HH:mm').format(order.createdAt), + formattedAmount: item.orderFormattedAmount, + ), + ); + } - return Container(color: Colors.transparent, height: 1); - }) - : Center( - child: Text( - S.of(context).placeholder_transactions, - style: TextStyle( - fontSize: 14, - color: Theme.of(context).extension()!.color), - ), - ); - })) + return Container(color: Colors.transparent, height: 1); + }) + : Center( + child: Text( + key: ValueKey('transactions_page_placeholder_transactions_text_key'), + S.of(context).placeholder_transactions, + style: TextStyle( + fontSize: 14, + color: Theme.of(context).extension()!.color, + ), + ), + ); + }, + ), + ) ], ), ), diff --git a/lib/src/screens/dashboard/widgets/anonpay_transaction_row.dart b/lib/src/screens/dashboard/widgets/anonpay_transaction_row.dart index cb8bef0b7..64d4bfe85 100644 --- a/lib/src/screens/dashboard/widgets/anonpay_transaction_row.dart +++ b/lib/src/screens/dashboard/widgets/anonpay_transaction_row.dart @@ -9,6 +9,7 @@ class AnonpayTransactionRow extends StatelessWidget { required this.currency, required this.onTap, required this.amount, + super.key, }); final VoidCallback? onTap; diff --git a/lib/src/screens/dashboard/widgets/date_section_raw.dart b/lib/src/screens/dashboard/widgets/date_section_raw.dart index 73f9f03a1..02fcef7f4 100644 --- a/lib/src/screens/dashboard/widgets/date_section_raw.dart +++ b/lib/src/screens/dashboard/widgets/date_section_raw.dart @@ -1,42 +1,27 @@ import 'package:flutter/material.dart'; import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; -import 'package:intl/intl.dart'; -import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/utils/date_formatter.dart'; class DateSectionRaw extends StatelessWidget { - DateSectionRaw({required this.date}); + DateSectionRaw({required this.date, super.key}); final DateTime date; @override Widget build(BuildContext context) { - final nowDate = DateTime.now(); - final diffDays = date.difference(nowDate).inDays; - final isToday = nowDate.day == date.day && - nowDate.month == date.month && - nowDate.year == date.year; - final dateSectionDateFormat = DateFormatter.withCurrentLocal(hasTime: false); - var title = ""; - - if (isToday) { - title = S.of(context).today; - } else if (diffDays == 0) { - title = S.of(context).yesterday; - } else if (diffDays > -7 && diffDays < 0) { - final dateFormat = DateFormat.EEEE(); - title = dateFormat.format(date); - } else { - title = dateSectionDateFormat.format(date); - } + final title = DateFormatter.convertDateTimeToReadableString(date); return Container( - height: 35, - alignment: Alignment.center, - color: Colors.transparent, - child: Text(title, - style: TextStyle( - fontSize: 12, - color: Theme.of(context).extension()!.dateSectionRowColor))); + height: 35, + alignment: Alignment.center, + color: Colors.transparent, + child: Text( + title, + style: TextStyle( + fontSize: 12, + color: Theme.of(context).extension()!.dateSectionRowColor, + ), + ), + ); } } diff --git a/lib/src/screens/dashboard/widgets/header_row.dart b/lib/src/screens/dashboard/widgets/header_row.dart index cb4f67fc2..f1f3f0889 100644 --- a/lib/src/screens/dashboard/widgets/header_row.dart +++ b/lib/src/screens/dashboard/widgets/header_row.dart @@ -7,7 +7,7 @@ import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart'; class HeaderRow extends StatelessWidget { - HeaderRow({required this.dashboardViewModel}); + HeaderRow({required this.dashboardViewModel, super.key}); final DashboardViewModel dashboardViewModel; @@ -34,6 +34,7 @@ class HeaderRow extends StatelessWidget { Semantics( container: true, child: GestureDetector( + key: ValueKey('transactions_page_header_row_transaction_filter_button_key'), onTap: () { showPopUp( context: context, diff --git a/lib/src/screens/dashboard/widgets/menu_widget.dart b/lib/src/screens/dashboard/widgets/menu_widget.dart index 6d8379b29..f0e330f04 100644 --- a/lib/src/screens/dashboard/widgets/menu_widget.dart +++ b/lib/src/screens/dashboard/widgets/menu_widget.dart @@ -9,7 +9,7 @@ import 'package:cw_core/wallet_type.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; class MenuWidget extends StatefulWidget { - MenuWidget(this.dashboardViewModel); + MenuWidget(this.dashboardViewModel, Key? key); final DashboardViewModel dashboardViewModel; @@ -193,6 +193,7 @@ class MenuWidgetState extends State { final isLastTile = index == itemCount - 1; return SettingActionButton( + key: item.key, isLastTile: isLastTile, tileHeight: tileHeight, selectionActive: false, diff --git a/lib/src/screens/dashboard/widgets/order_row.dart b/lib/src/screens/dashboard/widgets/order_row.dart index 8adc6e0d5..221ea5689 100644 --- a/lib/src/screens/dashboard/widgets/order_row.dart +++ b/lib/src/screens/dashboard/widgets/order_row.dart @@ -12,7 +12,10 @@ class OrderRow extends StatelessWidget { required this.to, required this.createdAtFormattedDate, this.onTap, - this.formattedAmount}); + this.formattedAmount, + super.key, + }); + final VoidCallback? onTap; final BuyProviderDescription provider; final String from; @@ -22,8 +25,7 @@ class OrderRow extends StatelessWidget { @override Widget build(BuildContext context) { - final iconColor = - Theme.of(context).extension()!.iconColor; + final iconColor = Theme.of(context).extension()!.iconColor; final providerIcon = getBuyProviderIcon(provider, iconColor: iconColor); @@ -36,46 +38,42 @@ class OrderRow extends StatelessWidget { mainAxisSize: MainAxisSize.max, crossAxisAlignment: CrossAxisAlignment.center, children: [ - if (providerIcon != null) Padding( - padding: EdgeInsets.only(right: 12), - child: providerIcon, - ), + if (providerIcon != null) + Padding( + padding: EdgeInsets.only(right: 12), + child: providerIcon, + ), Expanded( child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text('$from → $to', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: Theme.of(context).extension()!.textColor - )), - formattedAmount != null - ? Text(formattedAmount! + ' ' + to, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: Theme.of(context).extension()!.textColor - )) - : Container() - ]), - SizedBox(height: 5), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(createdAtFormattedDate, - style: TextStyle( - fontSize: 14, - color: Theme.of(context).extension()!.dateSectionRowColor)) - ]) - ], - ) - ) + mainAxisSize: MainAxisSize.min, + children: [ + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + Text('$from → $to', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context).extension()!.textColor)), + formattedAmount != null + ? Text(formattedAmount! + ' ' + to, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: + Theme.of(context).extension()!.textColor)) + : Container() + ]), + SizedBox(height: 5), + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + Text(createdAtFormattedDate, + style: TextStyle( + fontSize: 14, + color: + Theme.of(context).extension()!.dateSectionRowColor)) + ]) + ], + )) ], ), )); } -} \ No newline at end of file +} diff --git a/lib/src/screens/dashboard/widgets/trade_row.dart b/lib/src/screens/dashboard/widgets/trade_row.dart index 7c809aa9d..84a5d2beb 100644 --- a/lib/src/screens/dashboard/widgets/trade_row.dart +++ b/lib/src/screens/dashboard/widgets/trade_row.dart @@ -13,6 +13,7 @@ class TradeRow extends StatelessWidget { required this.createdAtFormattedDate, this.onTap, this.formattedAmount, + super.key, }); final VoidCallback? onTap; diff --git a/lib/src/screens/dashboard/widgets/transaction_raw.dart b/lib/src/screens/dashboard/widgets/transaction_raw.dart index b18131f3d..2d7cbb809 100644 --- a/lib/src/screens/dashboard/widgets/transaction_raw.dart +++ b/lib/src/screens/dashboard/widgets/transaction_raw.dart @@ -14,6 +14,7 @@ class TransactionRow extends StatelessWidget { required this.tags, required this.title, required this.onTap, + super.key, }); final VoidCallback onTap; @@ -28,33 +29,36 @@ class TransactionRow extends StatelessWidget { @override Widget build(BuildContext context) { return InkWell( - onTap: onTap, - child: Container( - padding: EdgeInsets.fromLTRB(24, 8, 24, 8), - color: Colors.transparent, - child: Row( - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Container( - height: 36, - width: 36, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Theme.of(context).extension()!.rowsColor), - child: Image.asset(direction == TransactionDirection.incoming - ? 'assets/images/down_arrow.png' - : 'assets/images/up_arrow.png'), - ), - SizedBox(width: 12), - Expanded( - child: Column( + onTap: onTap, + child: Container( + padding: EdgeInsets.fromLTRB(24, 8, 24, 8), + color: Colors.transparent, + child: Row( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + height: 36, + width: 36, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Theme.of(context).extension()!.rowsColor), + child: Image.asset(direction == TransactionDirection.incoming + ? 'assets/images/down_arrow.png' + : 'assets/images/up_arrow.png'), + ), + SizedBox(width: 12), + Expanded( + child: Column( mainAxisSize: MainAxisSize.min, children: [ - Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row( - children: [ - Text(title, + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Text( + title, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w500, @@ -65,28 +69,39 @@ class TransactionRow extends StatelessWidget { ), Text(formattedAmount, style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: Theme.of(context).extension()!.textColor)) - ]), + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context).extension()!.textColor, + ), + ) + ], + ), SizedBox(height: 5), - Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text(formattedDate, + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(formattedDate, + style: TextStyle( + fontSize: 14, + color: Theme.of(context) + .extension()! + .dateSectionRowColor)), + Text( + formattedFiatAmount, style: TextStyle( - fontSize: 14, - color: - Theme.of(context).extension()!.dateSectionRowColor)), - Text(formattedFiatAmount, - style: TextStyle( - fontSize: 14, - color: - Theme.of(context).extension()!.dateSectionRowColor)) - ]) + fontSize: 14, + color: Theme.of(context).extension()!.dateSectionRowColor, + ), + ) + ], + ), ], - )) - ], - ), - )); + ), + ) + ], + ), + ), + ); } } diff --git a/lib/src/screens/new_wallet/new_wallet_page.dart b/lib/src/screens/new_wallet/new_wallet_page.dart index 929e3027a..387904df0 100644 --- a/lib/src/screens/new_wallet/new_wallet_page.dart +++ b/lib/src/screens/new_wallet/new_wallet_page.dart @@ -112,10 +112,13 @@ class _WalletNameFormState extends State { context: context, builder: (_) { return AlertWithOneAction( - alertTitle: S.current.new_wallet, - alertContent: state.error, - buttonText: S.of(context).ok, - buttonAction: () => Navigator.of(context).pop()); + key: ValueKey('new_wallet_page_failure_dialog_key'), + buttonKey: ValueKey('new_wallet_page_failure_dialog_button_key'), + alertTitle: S.current.new_wallet, + alertContent: state.error, + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop(), + ); }); } }); @@ -152,6 +155,7 @@ class _WalletNameFormState extends State { child: Column( children: [ TextFormField( + key: ValueKey('new_wallet_page_wallet_name_textformfield_key'), onChanged: (value) => _walletNewVM.name = value, controller: _nameController, textAlign: TextAlign.center, @@ -182,6 +186,8 @@ class _WalletNameFormState extends State { suffixIcon: Semantics( label: S.of(context).generate_name, child: IconButton( + key: ValueKey( + 'new_wallet_page_wallet_name_textformfield_generate_name_button_key'), onPressed: () async { final rName = await generateName(); FocusManager.instance.primaryFocus?.unfocus(); @@ -297,6 +303,7 @@ class _WalletNameFormState extends State { builder: (BuildContext build) => Padding( padding: EdgeInsets.only(top: 24), child: SelectButton( + key: ValueKey('new_wallet_page_monero_seed_type_button_key'), text: widget._seedSettingsViewModel.moneroSeedType.title, onTap: () async { await showPopUp( @@ -318,6 +325,7 @@ class _WalletNameFormState extends State { padding: EdgeInsets.only(top: 10), child: SeedLanguageSelector( key: _languageSelectorKey, + buttonKey: ValueKey('new_wallet_page_seed_language_selector_button_key'), initialSelected: defaultSeedLanguage, seedType: _walletNewVM.hasSeedType ? widget._seedSettingsViewModel.moneroSeedType @@ -336,6 +344,7 @@ class _WalletNameFormState extends State { Observer( builder: (context) { return LoadingPrimaryButton( + key: ValueKey('new_wallet_page_confirm_button_key'), onPressed: _confirmForm, text: S.of(context).seed_language_next, color: Colors.green, @@ -347,6 +356,7 @@ class _WalletNameFormState extends State { ), const SizedBox(height: 25), GestureDetector( + key: ValueKey('new_wallet_page_advanced_settings_button_key'), onTap: () { Navigator.of(context).pushNamed(Routes.advancedPrivacySettings, arguments: { "type": _walletNewVM.type, diff --git a/lib/src/screens/new_wallet/wallet_group_description_page.dart b/lib/src/screens/new_wallet/wallet_group_description_page.dart index d0936b640..e892e0d49 100644 --- a/lib/src/screens/new_wallet/wallet_group_description_page.dart +++ b/lib/src/screens/new_wallet/wallet_group_description_page.dart @@ -66,6 +66,7 @@ class WalletGroupDescriptionPage extends BasePage { ), ), PrimaryButton( + key: ValueKey('wallet_group_description_page_create_new_seed_button_key'), onPressed: () => Navigator.of(context).pushNamed( Routes.newWallet, arguments: NewWalletArguments(type: selectedWalletType), @@ -76,6 +77,7 @@ class WalletGroupDescriptionPage extends BasePage { ), SizedBox(height: 12), PrimaryButton( + key: ValueKey('wallet_group_description_page_choose_wallet_group_button_key'), onPressed: () => Navigator.of(context).pushNamed( Routes.walletGroupsDisplayPage, arguments: selectedWalletType, diff --git a/lib/src/screens/restore/restore_options_page.dart b/lib/src/screens/restore/restore_options_page.dart index 57f5ec727..299846a72 100644 --- a/lib/src/screens/restore/restore_options_page.dart +++ b/lib/src/screens/restore/restore_options_page.dart @@ -56,7 +56,8 @@ class _RestoreOptionsBodyState extends State<_RestoreOptionsBody> { } if (isMoneroOnly) { - return DeviceConnectionType.supportedConnectionTypes(WalletType.monero, Platform.isIOS).isNotEmpty; + return DeviceConnectionType.supportedConnectionTypes(WalletType.monero, Platform.isIOS) + .isNotEmpty; } return true; @@ -80,13 +81,12 @@ class _RestoreOptionsBodyState extends State<_RestoreOptionsBody> { child: Column( children: [ OptionTile( - key: ValueKey('restore_options_from_seeds_button_key'), - onPressed: () => - Navigator.pushNamed( - context, - Routes.restoreWalletFromSeedKeys, - arguments: widget.isNewInstall, - ), + key: ValueKey('restore_options_from_seeds_or_keys_button_key'), + onPressed: () => Navigator.pushNamed( + context, + Routes.restoreWalletFromSeedKeys, + arguments: widget.isNewInstall, + ), image: imageSeedKeys, title: S.of(context).restore_title_from_seed_keys, description: S.of(context).restore_description_from_seed_keys, @@ -107,7 +107,8 @@ class _RestoreOptionsBodyState extends State<_RestoreOptionsBody> { padding: EdgeInsets.only(top: 24), child: OptionTile( key: ValueKey('restore_options_from_hardware_wallet_button_key'), - onPressed: () => Navigator.pushNamed(context, Routes.restoreWalletFromHardwareWallet, + onPressed: () => Navigator.pushNamed( + context, Routes.restoreWalletFromHardwareWallet, arguments: widget.isNewInstall), image: imageLedger, title: S.of(context).restore_title_from_hardware_wallet, @@ -120,9 +121,9 @@ class _RestoreOptionsBodyState extends State<_RestoreOptionsBody> { key: ValueKey('restore_options_from_qr_button_key'), onPressed: () => _onScanQRCode(context), icon: Icon( - Icons.qr_code_rounded, - color: imageColor, - size: 50, + Icons.qr_code_rounded, + color: imageColor, + size: 50, ), title: S.of(context).scan_qr_code, description: S.of(context).cold_or_recover_wallet), @@ -149,20 +150,20 @@ class _RestoreOptionsBodyState extends State<_RestoreOptionsBody> { buttonAction: () => Navigator.of(context).pop()); }); }); - } Future _onScanQRCode(BuildContext context) async { - final isCameraPermissionGranted = await PermissionHandler.checkPermission(Permission.camera, context); + final isCameraPermissionGranted = + await PermissionHandler.checkPermission(Permission.camera, context); if (!isCameraPermissionGranted) return; bool isPinSet = false; if (widget.isNewInstall) { await Navigator.pushNamed(context, Routes.setupPin, arguments: (PinCodeState setupPinContext, String _) { - setupPinContext.close(); - isPinSet = true; - }); + setupPinContext.close(); + isPinSet = true; + }); } if (!widget.isNewInstall || isPinSet) { try { @@ -174,7 +175,8 @@ class _RestoreOptionsBodyState extends State<_RestoreOptionsBody> { }); final restoreWallet = await WalletRestoreFromQRCode.scanQRCodeForRestoring(context); - final restoreFromQRViewModel = getIt.get(param1: restoreWallet.type); + final restoreFromQRViewModel = + getIt.get(param1: restoreWallet.type); await restoreFromQRViewModel.create(restoreWallet: restoreWallet); if (restoreFromQRViewModel.state is FailureState) { diff --git a/lib/src/screens/restore/wallet_restore_from_seed_form.dart b/lib/src/screens/restore/wallet_restore_from_seed_form.dart index c8a75cf40..1684f6f92 100644 --- a/lib/src/screens/restore/wallet_restore_from_seed_form.dart +++ b/lib/src/screens/restore/wallet_restore_from_seed_form.dart @@ -191,6 +191,7 @@ class WalletRestoreFromSeedFormState extends State { ), if (widget.type == WalletType.monero || widget.type == WalletType.wownero) GestureDetector( + key: ValueKey('wallet_restore_from_seed_seedtype_picker_button_key'), onTap: () async { await showPopUp( context: context, @@ -264,6 +265,7 @@ class WalletRestoreFromSeedFormState extends State { BlockchainHeightWidget( focusNode: widget.blockHeightFocusNode, key: blockchainHeightKey, + blockHeightTextFieldKey: ValueKey('wallet_restore_from_seed_blockheight_textfield_key'), onHeightOrDateEntered: widget.onHeightOrDateEntered, hasDatePicker: widget.type == WalletType.monero || widget.type == WalletType.wownero, walletType: widget.type, diff --git a/lib/src/screens/seed/pre_seed_page.dart b/lib/src/screens/seed/pre_seed_page.dart index 730dfa5f8..23de4564f 100644 --- a/lib/src/screens/seed/pre_seed_page.dart +++ b/lib/src/screens/seed/pre_seed_page.dart @@ -15,13 +15,15 @@ class PreSeedPage extends InfoPage { String get pageTitle => S.current.pre_seed_title; @override - String get pageDescription => - S.current.pre_seed_description(seedPhraseLength.toString()); + String get pageDescription => S.current.pre_seed_description(seedPhraseLength.toString()); @override String get buttonText => S.current.pre_seed_button_text; @override - void Function(BuildContext) get onPressed => (BuildContext context) => - Navigator.of(context).popAndPushNamed(Routes.seed, arguments: true); + Key? get buttonKey => ValueKey('pre_seed_page_button_key'); + + @override + void Function(BuildContext) get onPressed => + (BuildContext context) => Navigator.of(context).popAndPushNamed(Routes.seed, arguments: true); } diff --git a/lib/src/screens/seed/wallet_seed_page.dart b/lib/src/screens/seed/wallet_seed_page.dart index 200b87b7d..10160839c 100644 --- a/lib/src/screens/seed/wallet_seed_page.dart +++ b/lib/src/screens/seed/wallet_seed_page.dart @@ -33,16 +33,22 @@ class WalletSeedPage extends BasePage { void onClose(BuildContext context) async { if (isNewWalletCreated) { final confirmed = await showPopUp( - context: context, - builder: (BuildContext context) { - return AlertWithTwoActions( - alertTitle: S.of(context).seed_alert_title, - alertContent: S.of(context).seed_alert_content, - leftButtonText: S.of(context).seed_alert_back, - rightButtonText: S.of(context).seed_alert_yes, - actionLeftButton: () => Navigator.of(context).pop(false), - actionRightButton: () => Navigator.of(context).pop(true)); - }) ?? + context: context, + builder: (BuildContext context) { + return AlertWithTwoActions( + alertDialogKey: ValueKey('wallet_seed_page_seed_alert_dialog_key'), + alertRightActionButtonKey: + ValueKey('wallet_seed_page_seed_alert_confirm_button_key'), + alertLeftActionButtonKey: ValueKey('wallet_seed_page_seed_alert_back_button_key'), + alertTitle: S.of(context).seed_alert_title, + alertContent: S.of(context).seed_alert_content, + leftButtonText: S.of(context).seed_alert_back, + rightButtonText: S.of(context).seed_alert_yes, + actionLeftButton: () => Navigator.of(context).pop(false), + actionRightButton: () => Navigator.of(context).pop(true), + ); + }, + ) ?? false; if (confirmed) { @@ -62,6 +68,7 @@ class WalletSeedPage extends BasePage { Widget trailing(BuildContext context) { return isNewWalletCreated ? GestureDetector( + key: ValueKey('wallet_seed_page_next_button_key'), onTap: () => onClose(context), child: Container( width: 100, @@ -74,9 +81,9 @@ class WalletSeedPage extends BasePage { child: Text( S.of(context).seed_language_next, style: TextStyle( - fontSize: 14, fontWeight: FontWeight.w600, color: Theme.of(context) - .extension()! - .buttonTextColor), + fontSize: 14, + fontWeight: FontWeight.w600, + color: Theme.of(context).extension()!.buttonTextColor), ), ), ) @@ -93,7 +100,8 @@ class WalletSeedPage extends BasePage { padding: EdgeInsets.all(24), alignment: Alignment.center, child: ConstrainedBox( - constraints: BoxConstraints(maxWidth: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint), + constraints: + BoxConstraints(maxWidth: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint), child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -106,6 +114,7 @@ class WalletSeedPage extends BasePage { crossAxisAlignment: CrossAxisAlignment.center, children: [ Text( + key: ValueKey('wallet_seed_page_wallet_name_text_key'), walletSeedViewModel.name, style: TextStyle( fontSize: 20, @@ -115,12 +124,14 @@ class WalletSeedPage extends BasePage { Padding( padding: EdgeInsets.only(top: 20, left: 16, right: 16), child: Text( + key: ValueKey('wallet_seed_page_wallet_seed_text_key'), walletSeedViewModel.seed, textAlign: TextAlign.center, style: TextStyle( fontSize: 14, fontWeight: FontWeight.normal, - color: Theme.of(context).extension()!.secondaryTextColor), + color: + Theme.of(context).extension()!.secondaryTextColor), ), ) ], @@ -132,12 +143,18 @@ class WalletSeedPage extends BasePage { ? Padding( padding: EdgeInsets.only(bottom: 43, left: 43, right: 43), child: Text( + key: ValueKey( + 'wallet_seed_page_wallet_seed_reminder_text_key', + ), S.of(context).seed_reminder, textAlign: TextAlign.center, style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.normal, - color: Theme.of(context).extension()!.detailsTitlesColor), + fontSize: 12, + fontWeight: FontWeight.normal, + color: Theme.of(context) + .extension()! + .detailsTitlesColor, + ), ), ) : Offstage(), @@ -145,9 +162,10 @@ class WalletSeedPage extends BasePage { mainAxisSize: MainAxisSize.max, children: [ Flexible( - child: Container( - padding: EdgeInsets.only(right: 8.0), - child: PrimaryButton( + child: Container( + padding: EdgeInsets.only(right: 8.0), + child: PrimaryButton( + key: ValueKey('wallet_seed_page_save_seeds_button_key'), onPressed: () { ShareUtil.share( text: walletSeedViewModel.seed, @@ -156,22 +174,29 @@ class WalletSeedPage extends BasePage { }, text: S.of(context).save, color: Colors.green, - textColor: Colors.white), - )), + textColor: Colors.white, + ), + ), + ), Flexible( - child: Container( - padding: EdgeInsets.only(left: 8.0), - child: Builder( + child: Container( + padding: EdgeInsets.only(left: 8.0), + child: Builder( builder: (context) => PrimaryButton( - onPressed: () { - ClipboardUtil.setSensitiveDataToClipboard( - ClipboardData(text: walletSeedViewModel.seed)); - showBar(context, S.of(context).copied_to_clipboard); - }, - text: S.of(context).copy, - color: Theme.of(context).extension()!.indicatorsColor, - textColor: Colors.white)), - )) + key: ValueKey('wallet_seed_page_copy_seeds_button_key'), + onPressed: () { + ClipboardUtil.setSensitiveDataToClipboard( + ClipboardData(text: walletSeedViewModel.seed), + ); + showBar(context, S.of(context).copied_to_clipboard); + }, + text: S.of(context).copy, + color: Theme.of(context).extension()!.indicatorsColor, + textColor: Colors.white, + ), + ), + ), + ) ], ) ], diff --git a/lib/src/screens/settings/security_backup_page.dart b/lib/src/screens/settings/security_backup_page.dart index 04ae53d77..bb1c00ff5 100644 --- a/lib/src/screens/settings/security_backup_page.dart +++ b/lib/src/screens/settings/security_backup_page.dart @@ -16,7 +16,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; class SecurityBackupPage extends BasePage { - SecurityBackupPage(this._securitySettingsViewModel, this._authService, [this._isHardwareWallet = false]); + SecurityBackupPage(this._securitySettingsViewModel, this._authService, + [this._isHardwareWallet = false]); final AuthService _authService; @@ -30,10 +31,13 @@ class SecurityBackupPage extends BasePage { @override Widget body(BuildContext context) { return Container( - padding: EdgeInsets.only(top: 10), - child: Column(mainAxisSize: MainAxisSize.min, children: [ + padding: EdgeInsets.only(top: 10), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ if (!_isHardwareWallet) SettingsCellWithArrow( + key: ValueKey('security_backup_page_show_keys_button_key'), title: S.current.show_keys, handler: (_) => _authService.authenticateAction( context, @@ -44,15 +48,17 @@ class SecurityBackupPage extends BasePage { ), if (!SettingsStoreBase.walletPasswordDirectInput) SettingsCellWithArrow( + key: ValueKey('security_backup_page_create_backup_button_key'), title: S.current.create_backup, handler: (_) => _authService.authenticateAction( context, route: Routes.backup, - conditionToDetermineIfToUse2FA: _securitySettingsViewModel - .shouldRequireTOTP2FAForAllSecurityAndBackupSettings, + conditionToDetermineIfToUse2FA: + _securitySettingsViewModel.shouldRequireTOTP2FAForAllSecurityAndBackupSettings, ), ), SettingsCellWithArrow( + key: ValueKey('security_backup_page_change_pin_button_key'), title: S.current.settings_change_pin, handler: (_) => _authService.authenticateAction( context, @@ -60,28 +66,30 @@ class SecurityBackupPage extends BasePage { arguments: (PinCodeState setupPinContext, String _) { setupPinContext.close(); }, - conditionToDetermineIfToUse2FA: _securitySettingsViewModel - .shouldRequireTOTP2FAForAllSecurityAndBackupSettings, + conditionToDetermineIfToUse2FA: + _securitySettingsViewModel.shouldRequireTOTP2FAForAllSecurityAndBackupSettings, ), ), if (DeviceInfo.instance.isMobile || Platform.isMacOS || Platform.isLinux) Observer(builder: (_) { return SettingsSwitcherCell( + key: ValueKey('security_backup_page_allow_biometrics_button_key'), title: S.current.settings_allow_biometrical_authentication, value: _securitySettingsViewModel.allowBiometricalAuthentication, onValueChange: (BuildContext context, bool value) { if (value) { - _authService.authenticateAction(context, - onAuthSuccess: (isAuthenticatedSuccessfully) async { - if (isAuthenticatedSuccessfully) { - if (await _securitySettingsViewModel.biometricAuthenticated()) { + _authService.authenticateAction( + context, + onAuthSuccess: (isAuthenticatedSuccessfully) async { + if (isAuthenticatedSuccessfully) { + if (await _securitySettingsViewModel.biometricAuthenticated()) { + _securitySettingsViewModel + .setAllowBiometricalAuthentication(isAuthenticatedSuccessfully); + } + } else { _securitySettingsViewModel .setAllowBiometricalAuthentication(isAuthenticatedSuccessfully); } - } else { - _securitySettingsViewModel - .setAllowBiometricalAuthentication(isAuthenticatedSuccessfully); - } }, conditionToDetermineIfToUse2FA: _securitySettingsViewModel .shouldRequireTOTP2FAForAllSecurityAndBackupSettings, @@ -93,6 +101,7 @@ class SecurityBackupPage extends BasePage { }), Observer(builder: (_) { return SettingsPickerCell( + key: ValueKey('security_backup_page_require_pin_after_button_key'), title: S.current.require_pin_after, items: PinCodeRequiredDuration.values, selectedItem: _securitySettingsViewModel.pinCodeRequiredDuration, @@ -104,14 +113,15 @@ class SecurityBackupPage extends BasePage { Observer( builder: (context) { return SettingsCellWithArrow( + key: ValueKey('security_backup_page_totp_2fa_button_key'), title: _securitySettingsViewModel.useTotp2FA ? S.current.modify_2fa : S.current.setup_2fa, - handler: (_) => _authService.authenticateAction( - context, - route: _securitySettingsViewModel.useTotp2FA - ? Routes.modify2FAPage - : Routes.setup2faInfoPage, + handler: (_) => _authService.authenticateAction( + context, + route: _securitySettingsViewModel.useTotp2FA + ? Routes.modify2FAPage + : Routes.setup2faInfoPage, conditionToDetermineIfToUse2FA: _securitySettingsViewModel .shouldRequireTOTP2FAForAllSecurityAndBackupSettings, ), diff --git a/lib/src/screens/settings/widgets/settings_cell_with_arrow.dart b/lib/src/screens/settings/widgets/settings_cell_with_arrow.dart index f0e19a715..cb4f9dc78 100644 --- a/lib/src/screens/settings/widgets/settings_cell_with_arrow.dart +++ b/lib/src/screens/settings/widgets/settings_cell_with_arrow.dart @@ -3,8 +3,11 @@ import 'package:cake_wallet/src/widgets/standard_list.dart'; import 'package:cake_wallet/themes/extensions/transaction_trade_theme.dart'; class SettingsCellWithArrow extends StandardListRow { - SettingsCellWithArrow({required String title, required Function(BuildContext context)? handler}) - : super(title: title, isSelected: false, onTap: handler); + SettingsCellWithArrow({ + required String title, + required Function(BuildContext context)? handler, + Key? key, + }) : super(title: title, isSelected: false, onTap: handler, key: key); @override Widget buildTrailing(BuildContext context) => Image.asset('assets/images/select_arrow.png', diff --git a/lib/src/screens/settings/widgets/settings_picker_cell.dart b/lib/src/screens/settings/widgets/settings_picker_cell.dart index 8e0492330..765ac2991 100644 --- a/lib/src/screens/settings/widgets/settings_picker_cell.dart +++ b/lib/src/screens/settings/widgets/settings_picker_cell.dart @@ -5,19 +5,21 @@ import 'package:cake_wallet/src/widgets/picker.dart'; import 'package:cake_wallet/src/widgets/standard_list.dart'; class SettingsPickerCell extends StandardListRow { - SettingsPickerCell( - {required String title, - required this.selectedItem, - required this.items, - this.displayItem, - this.images, - this.searchHintText, - this.isGridView = false, - this.matchingCriteria, - this.onItemSelected}) - : super( + SettingsPickerCell({ + required String title, + required this.selectedItem, + required this.items, + this.displayItem, + this.images, + this.searchHintText, + this.isGridView = false, + this.matchingCriteria, + this.onItemSelected, + Key? key, + }) : super( title: title, isSelected: false, + key: key, onTap: (BuildContext context) async { final selectedAtIndex = items.indexOf(selectedItem); diff --git a/lib/src/screens/settings/widgets/settings_switcher_cell.dart b/lib/src/screens/settings/widgets/settings_switcher_cell.dart index 0e5c04524..6a3c8b4a0 100644 --- a/lib/src/screens/settings/widgets/settings_switcher_cell.dart +++ b/lib/src/screens/settings/widgets/settings_switcher_cell.dart @@ -10,7 +10,8 @@ class SettingsSwitcherCell extends StandardListRow { Decoration? decoration, this.leading, void Function(BuildContext context)? onTap, - }) : super(title: title, isSelected: false, decoration: decoration, onTap: onTap); + Key? key, + }) : super(title: title, isSelected: false, decoration: decoration, onTap: onTap, key: key); final bool value; final void Function(BuildContext context, bool value)? onValueChange; diff --git a/lib/src/screens/setup_2fa/setup_2fa_info_page.dart b/lib/src/screens/setup_2fa/setup_2fa_info_page.dart index ff6187665..8aa0ac3c9 100644 --- a/lib/src/screens/setup_2fa/setup_2fa_info_page.dart +++ b/lib/src/screens/setup_2fa/setup_2fa_info_page.dart @@ -4,7 +4,6 @@ import 'package:cake_wallet/src/screens/InfoPage.dart'; import 'package:flutter/cupertino.dart'; class Setup2FAInfoPage extends InfoPage { - @override String get pageTitle => S.current.pre_seed_title; @@ -15,6 +14,9 @@ class Setup2FAInfoPage extends InfoPage { String get buttonText => S.current.understand; @override - void Function(BuildContext) get onPressed => (BuildContext context) => - Navigator.of(context).popAndPushNamed(Routes.setup_2faPage); + Key? get buttonKey => ValueKey('setup_2fa_info_page_button_key'); + + @override + void Function(BuildContext) get onPressed => + (BuildContext context) => Navigator.of(context).popAndPushNamed(Routes.setup_2faPage); } diff --git a/lib/src/screens/transaction_details/blockexplorer_list_item.dart b/lib/src/screens/transaction_details/blockexplorer_list_item.dart index d5a70daa7..0126a147a 100644 --- a/lib/src/screens/transaction_details/blockexplorer_list_item.dart +++ b/lib/src/screens/transaction_details/blockexplorer_list_item.dart @@ -1,7 +1,12 @@ import 'package:cake_wallet/src/screens/transaction_details/transaction_details_list_item.dart'; +import 'package:flutter/foundation.dart'; class BlockExplorerListItem extends TransactionDetailsListItem { - BlockExplorerListItem({required String title, required String value, required this.onTap}) - : super(title: title, value: value); + BlockExplorerListItem({ + required String title, + required String value, + required this.onTap, + Key? key, + }) : super(title: title, value: value, key: key); final Function() onTap; } diff --git a/lib/src/screens/transaction_details/rbf_details_list_fee_picker_item.dart b/lib/src/screens/transaction_details/rbf_details_list_fee_picker_item.dart index db3d94500..96ddc94cd 100644 --- a/lib/src/screens/transaction_details/rbf_details_list_fee_picker_item.dart +++ b/lib/src/screens/transaction_details/rbf_details_list_fee_picker_item.dart @@ -1,18 +1,20 @@ import 'package:cake_wallet/src/screens/transaction_details/transaction_details_list_item.dart'; +import 'package:flutter/widgets.dart'; class StandardPickerListItem extends TransactionDetailsListItem { - StandardPickerListItem( - {required String title, - required String value, - required this.items, - required this.displayItem, - required this.onSliderChanged, - required this.onItemSelected, - required this.selectedIdx, - required this.customItemIndex, - this.maxValue, - required this.customValue}) - : super(title: title, value: value); + StandardPickerListItem({ + required String title, + required String value, + required this.items, + required this.displayItem, + required this.onSliderChanged, + required this.onItemSelected, + required this.selectedIdx, + required this.customItemIndex, + this.maxValue, + required this.customValue, + Key? key, + }) : super(title: title, value: value, key: key); final List items; final String Function(T item, double sliderValue) displayItem; diff --git a/lib/src/screens/transaction_details/standart_list_item.dart b/lib/src/screens/transaction_details/standart_list_item.dart index 705daf970..673eabe42 100644 --- a/lib/src/screens/transaction_details/standart_list_item.dart +++ b/lib/src/screens/transaction_details/standart_list_item.dart @@ -1,6 +1,9 @@ import 'package:cake_wallet/src/screens/transaction_details/transaction_details_list_item.dart'; class StandartListItem extends TransactionDetailsListItem { - StandartListItem({required String title, required String value}) - : super(title: title, value: value); + StandartListItem({ + required String super.title, + required String super.value, + super.key, + }); } diff --git a/lib/src/screens/transaction_details/textfield_list_item.dart b/lib/src/screens/transaction_details/textfield_list_item.dart index 49ef2705e..846f9acd5 100644 --- a/lib/src/screens/transaction_details/textfield_list_item.dart +++ b/lib/src/screens/transaction_details/textfield_list_item.dart @@ -1,11 +1,17 @@ import 'package:cake_wallet/src/screens/transaction_details/transaction_details_list_item.dart'; +import 'package:flutter/foundation.dart'; class TextFieldListItem extends TransactionDetailsListItem { TextFieldListItem({ required String title, required String value, - required this.onSubmitted}) - : super(title: title, value: value); + required this.onSubmitted, + Key? key, + }) : super( + title: title, + value: value, + key: key, + ); final Function(String value) onSubmitted; } \ No newline at end of file diff --git a/lib/src/screens/transaction_details/transaction_details_list_item.dart b/lib/src/screens/transaction_details/transaction_details_list_item.dart index 8a8449350..446383d72 100644 --- a/lib/src/screens/transaction_details/transaction_details_list_item.dart +++ b/lib/src/screens/transaction_details/transaction_details_list_item.dart @@ -1,6 +1,9 @@ +import 'package:flutter/foundation.dart'; + abstract class TransactionDetailsListItem { - TransactionDetailsListItem({required this.title, required this.value}); + TransactionDetailsListItem({required this.title, required this.value, this.key}); final String title; final String value; -} \ No newline at end of file + final Key? key; +} diff --git a/lib/src/screens/transaction_details/transaction_details_page.dart b/lib/src/screens/transaction_details/transaction_details_page.dart index 1b088fc31..9484bf4da 100644 --- a/lib/src/screens/transaction_details/transaction_details_page.dart +++ b/lib/src/screens/transaction_details/transaction_details_page.dart @@ -33,38 +33,42 @@ class TransactionDetailsPage extends BasePage { children: [ Expanded( child: SectionStandardList( - sectionCount: 1, - itemCounter: (int _) => transactionDetailsViewModel.items.length, - itemBuilder: (__, index) { - final item = transactionDetailsViewModel.items[index]; + sectionCount: 1, + itemCounter: (int _) => transactionDetailsViewModel.items.length, + itemBuilder: (__, index) { + final item = transactionDetailsViewModel.items[index]; - if (item is StandartListItem) { - return GestureDetector( - onTap: () { - Clipboard.setData(ClipboardData(text: item.value)); - showBar(context, S.of(context).transaction_details_copied(item.title)); - }, - child: ListRow(title: '${item.title}:', value: item.value), - ); - } + if (item is StandartListItem) { + return GestureDetector( + key: item.key, + onTap: () { + Clipboard.setData(ClipboardData(text: item.value)); + showBar(context, S.of(context).transaction_details_copied(item.title)); + }, + child: ListRow(title: '${item.title}:', value: item.value), + ); + } - if (item is BlockExplorerListItem) { - return GestureDetector( - onTap: item.onTap, - child: ListRow(title: '${item.title}:', value: item.value), - ); - } + if (item is BlockExplorerListItem) { + return GestureDetector( + key: item.key, + onTap: item.onTap, + child: ListRow(title: '${item.title}:', value: item.value), + ); + } - if (item is TextFieldListItem) { - return TextFieldListRow( - title: item.title, - value: item.value, - onSubmitted: item.onSubmitted, - ); - } + if (item is TextFieldListItem) { + return TextFieldListRow( + key: item.key, + title: item.title, + value: item.value, + onSubmitted: item.onSubmitted, + ); + } - return Container(); - }), + return Container(); + }, + ), ), Observer( builder: (_) { diff --git a/lib/src/screens/transaction_details/transaction_expandable_list_item.dart b/lib/src/screens/transaction_details/transaction_expandable_list_item.dart index e87405de3..db6cf22ae 100644 --- a/lib/src/screens/transaction_details/transaction_expandable_list_item.dart +++ b/lib/src/screens/transaction_details/transaction_expandable_list_item.dart @@ -1,7 +1,12 @@ import 'package:cake_wallet/src/screens/transaction_details/transaction_details_list_item.dart'; +import 'package:flutter/foundation.dart'; class StandardExpandableListItem extends TransactionDetailsListItem { - StandardExpandableListItem({required String title, required this.expandableItems}) - : super(title: title, value: ''); + StandardExpandableListItem({ + required String title, + required this.expandableItems, + Key? key, + }) : super(title: title, value: '', key: key); + final List expandableItems; } diff --git a/lib/src/screens/transaction_details/widgets/textfield_list_row.dart b/lib/src/screens/transaction_details/widgets/textfield_list_row.dart index a86645ecb..24016a293 100644 --- a/lib/src/screens/transaction_details/widgets/textfield_list_row.dart +++ b/lib/src/screens/transaction_details/widgets/textfield_list_row.dart @@ -4,12 +4,14 @@ import 'package:cake_wallet/themes/extensions/transaction_trade_theme.dart'; import 'package:flutter/material.dart'; class TextFieldListRow extends StatefulWidget { - TextFieldListRow( - {required this.title, - required this.value, - this.titleFontSize = 14, - this.valueFontSize = 16, - this.onSubmitted}); + TextFieldListRow({ + required this.title, + required this.value, + this.titleFontSize = 14, + this.valueFontSize = 16, + this.onSubmitted, + super.key, + }); final String title; final String value; diff --git a/lib/src/screens/wallet_keys/wallet_keys_page.dart b/lib/src/screens/wallet_keys/wallet_keys_page.dart index 5117f152f..fac760516 100644 --- a/lib/src/screens/wallet_keys/wallet_keys_page.dart +++ b/lib/src/screens/wallet_keys/wallet_keys_page.dart @@ -25,23 +25,25 @@ class WalletKeysPage extends BasePage { @override Widget trailing(BuildContext context) => IconButton( - onPressed: () async { - final url = await walletKeysViewModel.url; + key: ValueKey('wallet_keys_page_fullscreen_qr_button_key'), + onPressed: () async { + final url = await walletKeysViewModel.url; - BrightnessUtil.changeBrightnessForFunction(() async { - await Navigator.pushNamed( - context, - Routes.fullscreenQR, - arguments: QrViewData(data: url.toString(), version: QrVersions.auto), - ); - }); - }, - splashColor: Colors.transparent, - highlightColor: Colors.transparent, - hoverColor: Colors.transparent, - icon: Image.asset( - 'assets/images/qr_code_icon.png', - )); + BrightnessUtil.changeBrightnessForFunction(() async { + await Navigator.pushNamed( + context, + Routes.fullscreenQR, + arguments: QrViewData(data: url.toString(), version: QrVersions.auto), + ); + }); + }, + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + hoverColor: Colors.transparent, + icon: Image.asset( + 'assets/images/qr_code_icon.png', + ), + ); @override Widget body(BuildContext context) { @@ -60,6 +62,7 @@ class WalletKeysPage extends BasePage { child: Padding( padding: const EdgeInsets.all(8.0), child: AutoSizeText( + key: ValueKey('wallet_keys_page_share_warning_text_key'), S.of(context).do_not_share_warning_text.toUpperCase(), textAlign: TextAlign.center, maxLines: 4, @@ -92,6 +95,7 @@ class WalletKeysPage extends BasePage { final item = walletKeysViewModel.items[index]; return GestureDetector( + key: item.key, onTap: () { ClipboardUtil.setSensitiveDataToClipboard(ClipboardData(text: item.value)); showBar(context, S.of(context).copied_key_to_clipboard(item.title)); diff --git a/lib/src/screens/wallet_list/wallet_list_page.dart b/lib/src/screens/wallet_list/wallet_list_page.dart index afb86cf0a..a9a9d9413 100644 --- a/lib/src/screens/wallet_list/wallet_list_page.dart +++ b/lib/src/screens/wallet_list/wallet_list_page.dart @@ -318,6 +318,7 @@ class WalletListBodyState extends State { child: Column( children: [ PrimaryImageButton( + key: ValueKey('wallet_list_page_create_new_wallet_button_key'), onPressed: () { //TODO(David): Find a way to optimize this if (isSingleCoin) { @@ -359,6 +360,7 @@ class WalletListBodyState extends State { ), SizedBox(height: 10.0), PrimaryImageButton( + key: ValueKey('wallet_list_page_restore_wallet_button_key'), onPressed: () { if (widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets) { widget.authService.authenticateAction( diff --git a/lib/src/widgets/blockchain_height_widget.dart b/lib/src/widgets/blockchain_height_widget.dart index 9d66c1789..1c6b6da02 100644 --- a/lib/src/widgets/blockchain_height_widget.dart +++ b/lib/src/widgets/blockchain_height_widget.dart @@ -23,6 +23,7 @@ class BlockchainHeightWidget extends StatefulWidget { this.doSingleScan = false, this.bitcoinMempoolAPIEnabled, required this.walletType, + this.blockHeightTextFieldKey, }) : super(key: key); final Function(int)? onHeightChange; @@ -35,6 +36,7 @@ class BlockchainHeightWidget extends StatefulWidget { final Future? bitcoinMempoolAPIEnabled; final Function()? toggleSingleScan; final WalletType walletType; + final Key? blockHeightTextFieldKey; @override State createState() => BlockchainHeightState(); @@ -81,6 +83,7 @@ class BlockchainHeightState extends State { child: Container( padding: EdgeInsets.only(top: 20.0, bottom: 10.0), child: BaseTextFormField( + key: widget.blockHeightTextFieldKey, focusNode: widget.focusNode, controller: restoreHeightController, keyboardType: diff --git a/lib/src/widgets/dashboard_card_widget.dart b/lib/src/widgets/dashboard_card_widget.dart index d9b545040..dc223eb02 100644 --- a/lib/src/widgets/dashboard_card_widget.dart +++ b/lib/src/widgets/dashboard_card_widget.dart @@ -15,6 +15,7 @@ class DashBoardRoundedCardWidget extends StatelessWidget { this.icon, this.onClose, this.customBorder, + super.key, }); final VoidCallback onTap; diff --git a/lib/src/widgets/option_tile.dart b/lib/src/widgets/option_tile.dart index 31f958f54..c2d8b9506 100644 --- a/lib/src/widgets/option_tile.dart +++ b/lib/src/widgets/option_tile.dart @@ -2,14 +2,14 @@ import 'package:cake_wallet/themes/extensions/option_tile_theme.dart'; import 'package:flutter/material.dart'; class OptionTile extends StatelessWidget { - const OptionTile( - {required this.onPressed, - this.image, - this.icon, - required this.title, - required this.description, - super.key}) - : assert(image!=null || icon!=null); + const OptionTile({ + required this.onPressed, + this.image, + this.icon, + required this.title, + required this.description, + super.key, + }) : assert(image != null || icon != null); final VoidCallback onPressed; final Image? image; @@ -34,7 +34,7 @@ class OptionTile extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ - icon ?? image!, + icon ?? image!, Expanded( child: Padding( padding: EdgeInsets.only(left: 16), diff --git a/lib/src/widgets/picker.dart b/lib/src/widgets/picker.dart index 801a79595..ed5c05be6 100644 --- a/lib/src/widgets/picker.dart +++ b/lib/src/widgets/picker.dart @@ -2,6 +2,7 @@ import 'dart:math'; +import 'package:cake_wallet/entities/seed_type.dart'; import 'package:cake_wallet/src/widgets/search_bar_widget.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cw_core/transaction_priority.dart'; @@ -310,6 +311,8 @@ class _PickerState extends State> { itemName = item.name; } else if (item is TransactionPriority) { itemName = item.title; + } else if (item is MoneroSeedType) { + itemName = item.title; } else { itemName = ''; } diff --git a/lib/src/widgets/seed_language_selector.dart b/lib/src/widgets/seed_language_selector.dart index 87f3aa573..711bff390 100644 --- a/lib/src/widgets/seed_language_selector.dart +++ b/lib/src/widgets/seed_language_selector.dart @@ -6,12 +6,16 @@ import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:flutter/material.dart'; class SeedLanguageSelector extends StatefulWidget { - SeedLanguageSelector( - {Key? key, required this.initialSelected, this.seedType = MoneroSeedType.defaultSeedType}) - : super(key: key); + SeedLanguageSelector({ + required this.initialSelected, + this.seedType = MoneroSeedType.defaultSeedType, + this.buttonKey, + Key? key, + }) : super(key: key); final String initialSelected; final MoneroSeedType seedType; + final Key? buttonKey; @override SeedLanguageSelectorState createState() => SeedLanguageSelectorState(selected: initialSelected); @@ -25,6 +29,7 @@ class SeedLanguageSelectorState extends State { @override Widget build(BuildContext context) { return SelectButton( + key: widget.buttonKey, image: null, text: "${seedLanguages.firstWhere((e) => e.name == selected).nameLocalized} (${S.of(context).seed_language})", diff --git a/lib/src/widgets/setting_actions.dart b/lib/src/widgets/setting_actions.dart index b9af97f32..048d006cc 100644 --- a/lib/src/widgets/setting_actions.dart +++ b/lib/src/widgets/setting_actions.dart @@ -5,9 +5,11 @@ import 'package:flutter/material.dart'; class SettingActions { final String Function(BuildContext) name; final String image; + final Key key; final void Function(BuildContext) onTap; SettingActions._({ + required this.key, required this.name, required this.image, required this.onTap, @@ -39,6 +41,7 @@ class SettingActions { ]; static SettingActions silentPaymentsSettingAction = SettingActions._( + key: ValueKey('dashboard_page_menu_widget_silent_payment_settings_button_key'), name: (context) => S.of(context).silent_payments_settings, image: 'assets/images/bitcoin_menu.png', onTap: (BuildContext context) { @@ -48,6 +51,7 @@ class SettingActions { ); static SettingActions litecoinMwebSettingAction = SettingActions._( + key: ValueKey('dashboard_page_menu_widget_litecoin_mweb_settings_button_key'), name: (context) => S.of(context).litecoin_mweb_settings, image: 'assets/images/litecoin_menu.png', onTap: (BuildContext context) { @@ -57,6 +61,7 @@ class SettingActions { ); static SettingActions connectionSettingAction = SettingActions._( + key: ValueKey('dashboard_page_menu_widget_connection_and_sync_settings_button_key'), name: (context) => S.of(context).connection_sync, image: 'assets/images/nodes_menu.png', onTap: (BuildContext context) { @@ -66,6 +71,7 @@ class SettingActions { ); static SettingActions walletSettingAction = SettingActions._( + key: ValueKey('dashboard_page_menu_widget_wallet_menu_button_key'), name: (context) => S.of(context).wallets, image: 'assets/images/wallet_menu.png', onTap: (BuildContext context) { @@ -75,6 +81,7 @@ class SettingActions { ); static SettingActions addressBookSettingAction = SettingActions._( + key: ValueKey('dashboard_page_menu_widget_address_book_button_key'), name: (context) => S.of(context).address_book_menu, image: 'assets/images/open_book_menu.png', onTap: (BuildContext context) { @@ -84,6 +91,7 @@ class SettingActions { ); static SettingActions securityBackupSettingAction = SettingActions._( + key: ValueKey('dashboard_page_menu_widget_security_and_backup_button_key'), name: (context) => S.of(context).security_and_backup, image: 'assets/images/key_menu.png', onTap: (BuildContext context) { @@ -93,6 +101,7 @@ class SettingActions { ); static SettingActions privacySettingAction = SettingActions._( + key: ValueKey('dashboard_page_menu_widget_privacy_settings_button_key'), name: (context) => S.of(context).privacy, image: 'assets/images/privacy_menu.png', onTap: (BuildContext context) { @@ -102,6 +111,7 @@ class SettingActions { ); static SettingActions displaySettingAction = SettingActions._( + key: ValueKey('dashboard_page_menu_widget_display_settings_button_key'), name: (context) => S.of(context).display_settings, image: 'assets/images/eye_menu.png', onTap: (BuildContext context) { @@ -111,6 +121,7 @@ class SettingActions { ); static SettingActions otherSettingAction = SettingActions._( + key: ValueKey('dashboard_page_menu_widget_other_settings_button_key'), name: (context) => S.of(context).other_settings, image: 'assets/images/settings_menu.png', onTap: (BuildContext context) { @@ -120,6 +131,7 @@ class SettingActions { ); static SettingActions supportSettingAction = SettingActions._( + key: ValueKey('dashboard_page_menu_widget_support_settings_button_key'), name: (context) => S.of(context).settings_support, image: 'assets/images/question_mark.png', onTap: (BuildContext context) { diff --git a/lib/src/widgets/standard_list.dart b/lib/src/widgets/standard_list.dart index c1fcae052..0780d64cd 100644 --- a/lib/src/widgets/standard_list.dart +++ b/lib/src/widgets/standard_list.dart @@ -4,7 +4,13 @@ import 'package:cake_wallet/src/widgets/standard_list_status_row.dart'; import 'package:flutter/material.dart'; class StandardListRow extends StatelessWidget { - StandardListRow({required this.title, required this.isSelected, this.onTap, this.decoration}); + StandardListRow({ + required this.title, + required this.isSelected, + this.onTap, + this.decoration, + super.key, + }); final String title; final bool isSelected; diff --git a/lib/store/anonpay/anonpay_transactions_store.dart b/lib/store/anonpay/anonpay_transactions_store.dart index c6f05b993..d0384ca5a 100644 --- a/lib/store/anonpay/anonpay_transactions_store.dart +++ b/lib/store/anonpay/anonpay_transactions_store.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart'; import 'package:cake_wallet/view_model/dashboard/anonpay_transaction_list_item.dart'; +import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; @@ -27,7 +28,10 @@ abstract class AnonpayTransactionsStoreBase with Store { Future updateTransactionList() async { transactions = anonpayInvoiceInfoSource.values .map( - (transaction) => AnonpayTransactionListItem(transaction: transaction), + (transaction) => AnonpayTransactionListItem( + transaction: transaction, + key: ValueKey('anonpay_invoice_transaction_list_item_${transaction.invoiceId}_key'), + ), ) .toList(); } diff --git a/lib/store/dashboard/orders_store.dart b/lib/store/dashboard/orders_store.dart index b5ec658f7..d91ad3512 100644 --- a/lib/store/dashboard/orders_store.dart +++ b/lib/store/dashboard/orders_store.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:cake_wallet/buy/order.dart'; import 'package:cake_wallet/view_model/dashboard/order_list_item.dart'; +import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/store/settings_store.dart'; @@ -10,12 +11,10 @@ part 'orders_store.g.dart'; class OrdersStore = OrdersStoreBase with _$OrdersStore; abstract class OrdersStoreBase with Store { - OrdersStoreBase({required this.ordersSource, - required this.settingsStore}) - : orders = [], - orderId = '' { - _onOrdersChanged = - ordersSource.watch().listen((_) async => await updateOrderList()); + OrdersStoreBase({required this.ordersSource, required this.settingsStore}) + : orders = [], + orderId = '' { + _onOrdersChanged = ordersSource.watch().listen((_) async => await updateOrderList()); updateOrderList(); } @@ -38,8 +37,11 @@ abstract class OrdersStoreBase with Store { void setOrder(Order order) => this.order = order; @action - Future updateOrderList() async => orders = - ordersSource.values.map((order) => OrderListItem( - order: order, - settingsStore: settingsStore)).toList(); -} \ No newline at end of file + Future updateOrderList() async => orders = ordersSource.values + .map((order) => OrderListItem( + order: order, + settingsStore: settingsStore, + key: ValueKey('order_list_item_${order.id}_key'), + )) + .toList(); +} diff --git a/lib/store/dashboard/trades_store.dart b/lib/store/dashboard/trades_store.dart index 72442b46f..d0a4592e3 100644 --- a/lib/store/dashboard/trades_store.dart +++ b/lib/store/dashboard/trades_store.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:cake_wallet/exchange/trade.dart'; import 'package:cake_wallet/view_model/dashboard/trade_list_item.dart'; +import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/store/settings_store.dart'; @@ -11,9 +12,8 @@ class TradesStore = TradesStoreBase with _$TradesStore; abstract class TradesStoreBase with Store { TradesStoreBase({required this.tradesSource, required this.settingsStore}) - : trades = [] { - _onTradesChanged = - tradesSource.watch().listen((_) async => await updateTradeList()); + : trades = [] { + _onTradesChanged = tradesSource.watch().listen((_) async => await updateTradeList()); updateTradeList(); } @@ -31,8 +31,11 @@ abstract class TradesStoreBase with Store { void setTrade(Trade trade) => this.trade = trade; @action - Future updateTradeList() async => trades = - tradesSource.values.map((trade) => TradeListItem( - trade: trade, - settingsStore: settingsStore)).toList(); -} \ No newline at end of file + Future updateTradeList() async => trades = tradesSource.values + .map((trade) => TradeListItem( + trade: trade, + settingsStore: settingsStore, + key: ValueKey('trade_list_item_${trade.id}_key'), + )) + .toList(); +} diff --git a/lib/utils/date_formatter.dart b/lib/utils/date_formatter.dart index 9cff68614..58e59966a 100644 --- a/lib/utils/date_formatter.dart +++ b/lib/utils/date_formatter.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/generated/i18n.dart'; import 'package:intl/intl.dart'; import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/store/settings_store.dart'; @@ -5,8 +6,7 @@ import 'package:cake_wallet/store/settings_store.dart'; class DateFormatter { static String currentLocalFormat({bool hasTime = true, bool reverse = false}) { final isUSA = getIt.get().languageCode.toLowerCase() == 'en'; - final format = - isUSA ? usaStyleFormat(hasTime, reverse) : regularStyleFormat(hasTime, reverse); + final format = isUSA ? usaStyleFormat(hasTime, reverse) : regularStyleFormat(hasTime, reverse); return format; } @@ -20,4 +20,26 @@ class DateFormatter { static String regularStyleFormat(bool hasTime, bool reverse) => hasTime ? (reverse ? 'HH:mm dd.MM.yyyy' : 'dd.MM.yyyy, HH:mm') : 'dd.MM.yyyy'; + + static String convertDateTimeToReadableString(DateTime date) { + final nowDate = DateTime.now(); + final diffDays = date.difference(nowDate).inDays; + final isToday = + nowDate.day == date.day && nowDate.month == date.month && nowDate.year == date.year; + final dateSectionDateFormat = withCurrentLocal(hasTime: false); + var title = ""; + + if (isToday) { + title = S.current.today; + } else if (diffDays == 0) { + title = S.current.yesterday; + } else if (diffDays > -7 && diffDays < 0) { + final dateFormat = DateFormat.EEEE(); + title = dateFormat.format(date); + } else { + title = dateSectionDateFormat.format(date); + } + + return title; + } } diff --git a/lib/utils/image_utill.dart b/lib/utils/image_utill.dart index ef3775e4c..746fde453 100644 --- a/lib/utils/image_utill.dart +++ b/lib/utils/image_utill.dart @@ -11,6 +11,7 @@ class ImageUtil { if (isNetworkImage) { return isSvg ? SvgPicture.network( + key: ValueKey(imagePath), imagePath, height: _height, width: _width, @@ -23,6 +24,7 @@ class ImageUtil { ), ) : Image.network( + key: ValueKey(imagePath), imagePath, height: _height, width: _width, @@ -58,12 +60,14 @@ class ImageUtil { height: _height, width: _width, placeholderBuilder: (_) => Icon(Icons.error), + key: ValueKey(imagePath), ) : Image.asset( imagePath, height: _height, width: _width, errorBuilder: (_, __, ___) => Icon(Icons.error), + key: ValueKey(imagePath), ); } } diff --git a/lib/view_model/dashboard/action_list_item.dart b/lib/view_model/dashboard/action_list_item.dart index b03bd1bdc..1ee4e6a3c 100644 --- a/lib/view_model/dashboard/action_list_item.dart +++ b/lib/view_model/dashboard/action_list_item.dart @@ -1,3 +1,8 @@ +import 'package:flutter/foundation.dart'; + abstract class ActionListItem { + ActionListItem({required this.key}); + DateTime get date; + Key key; } \ No newline at end of file diff --git a/lib/view_model/dashboard/anonpay_transaction_list_item.dart b/lib/view_model/dashboard/anonpay_transaction_list_item.dart index 261e49070..a54a4b334 100644 --- a/lib/view_model/dashboard/anonpay_transaction_list_item.dart +++ b/lib/view_model/dashboard/anonpay_transaction_list_item.dart @@ -2,7 +2,7 @@ import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart'; import 'package:cake_wallet/view_model/dashboard/action_list_item.dart'; class AnonpayTransactionListItem extends ActionListItem { - AnonpayTransactionListItem({required this.transaction}); + AnonpayTransactionListItem({required this.transaction, required super.key}); final AnonpayInvoiceInfo transaction; diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 423babefc..6cf7c49d0 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -48,6 +48,7 @@ import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:eth_sig_util/util/utils.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:http/http.dart' as http; import 'package:mobx/mobx.dart'; @@ -182,10 +183,16 @@ abstract class DashboardViewModelBase with Store { final sortedTransactions = [..._accountTransactions]; sortedTransactions.sort((a, b) => a.date.compareTo(b.date)); - transactions = ObservableList.of(sortedTransactions.map((transaction) => TransactionListItem( - transaction: transaction, - balanceViewModel: balanceViewModel, - settingsStore: appStore.settingsStore))); + transactions = ObservableList.of( + sortedTransactions.map( + (transaction) => TransactionListItem( + transaction: transaction, + balanceViewModel: balanceViewModel, + settingsStore: appStore.settingsStore, + key: ValueKey('monero_transaction_history_item_${transaction.id}_key'), + ), + ), + ); } else if (_wallet.type == WalletType.wownero) { subname = wow.wownero!.getCurrentAccount(_wallet).label; @@ -206,18 +213,30 @@ abstract class DashboardViewModelBase with Store { final sortedTransactions = [..._accountTransactions]; sortedTransactions.sort((a, b) => a.date.compareTo(b.date)); - transactions = ObservableList.of(sortedTransactions.map((transaction) => TransactionListItem( - transaction: transaction, - balanceViewModel: balanceViewModel, - settingsStore: appStore.settingsStore))); + transactions = ObservableList.of( + sortedTransactions.map( + (transaction) => TransactionListItem( + transaction: transaction, + balanceViewModel: balanceViewModel, + settingsStore: appStore.settingsStore, + key: ValueKey('wownero_transaction_history_item_${transaction.id}_key'), + ), + ), + ); } 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, - settingsStore: appStore.settingsStore))); + transactions = ObservableList.of( + sortedTransactions.map( + (transaction) => TransactionListItem( + transaction: transaction, + balanceViewModel: balanceViewModel, + settingsStore: appStore.settingsStore, + key: ValueKey('${_wallet.type.name}_transaction_history_item_${transaction.id}_key'), + ), + ), + ); } // TODO: nano sub-account generation is disabled: @@ -234,9 +253,13 @@ abstract class DashboardViewModelBase with Store { appStore.wallet!.transactionHistory.transactions, transactions, (TransactionInfo? transaction) => TransactionListItem( - transaction: transaction!, - balanceViewModel: balanceViewModel, - settingsStore: appStore.settingsStore), filter: (TransactionInfo? transaction) { + transaction: transaction!, + balanceViewModel: balanceViewModel, + settingsStore: appStore.settingsStore, + key: ValueKey( + '${_wallet.type.name}_transaction_history_item_${transaction.id}_key', + ), + ), filter: (TransactionInfo? transaction) { if (transaction == null) { return false; } @@ -650,20 +673,29 @@ abstract class DashboardViewModelBase with Store { transactions.clear(); - transactions.addAll(wallet.transactionHistory.transactions.values.map((transaction) => - TransactionListItem( - transaction: transaction, - balanceViewModel: balanceViewModel, - settingsStore: appStore.settingsStore))); + transactions.addAll( + wallet.transactionHistory.transactions.values.map( + (transaction) => TransactionListItem( + transaction: transaction, + balanceViewModel: balanceViewModel, + settingsStore: appStore.settingsStore, + key: ValueKey('${wallet.type.name}_transaction_history_item_${transaction.id}_key'), + ), + ), + ); } connectMapToListWithTransform( appStore.wallet!.transactionHistory.transactions, transactions, (TransactionInfo? transaction) => TransactionListItem( - transaction: transaction!, - balanceViewModel: balanceViewModel, - settingsStore: appStore.settingsStore), filter: (TransactionInfo? tx) { + transaction: transaction!, + balanceViewModel: balanceViewModel, + settingsStore: appStore.settingsStore, + key: ValueKey( + '${wallet.type.name}_transaction_history_item_${transaction.id}_key', + ), + ), filter: (TransactionInfo? tx) { if (tx == null) { return false; } @@ -703,10 +735,16 @@ abstract class DashboardViewModelBase with Store { monero!.getTransactionInfoAccountId(tx) == monero!.getCurrentAccount(wallet).id) .toList(); - transactions.addAll(_accountTransactions.map((transaction) => TransactionListItem( - transaction: transaction, - balanceViewModel: balanceViewModel, - settingsStore: appStore.settingsStore))); + transactions.addAll( + _accountTransactions.map( + (transaction) => TransactionListItem( + transaction: transaction, + balanceViewModel: balanceViewModel, + settingsStore: appStore.settingsStore, + key: ValueKey('monero_transaction_history_item_${transaction.id}_key'), + ), + ), + ); } else if (wallet.type == WalletType.wownero) { final _accountTransactions = wow.wownero! .getTransactionHistory(wallet) @@ -717,10 +755,16 @@ abstract class DashboardViewModelBase with Store { wow.wownero!.getCurrentAccount(wallet).id) .toList(); - transactions.addAll(_accountTransactions.map((transaction) => TransactionListItem( - transaction: transaction, - balanceViewModel: balanceViewModel, - settingsStore: appStore.settingsStore))); + transactions.addAll( + _accountTransactions.map( + (transaction) => TransactionListItem( + transaction: transaction, + balanceViewModel: balanceViewModel, + settingsStore: appStore.settingsStore, + key: ValueKey('wownero_transaction_history_item_${transaction.id}_key'), + ), + ), + ); } } diff --git a/lib/view_model/dashboard/date_section_item.dart b/lib/view_model/dashboard/date_section_item.dart index 0b361ecce..75250a7ea 100644 --- a/lib/view_model/dashboard/date_section_item.dart +++ b/lib/view_model/dashboard/date_section_item.dart @@ -1,7 +1,7 @@ import 'package:cake_wallet/view_model/dashboard/action_list_item.dart'; class DateSectionItem extends ActionListItem { - DateSectionItem(this.date); + DateSectionItem(this.date, {required super.key}); @override final DateTime date; diff --git a/lib/view_model/dashboard/formatted_item_list.dart b/lib/view_model/dashboard/formatted_item_list.dart index a1cbbbf7d..04464ca89 100644 --- a/lib/view_model/dashboard/formatted_item_list.dart +++ b/lib/view_model/dashboard/formatted_item_list.dart @@ -1,5 +1,6 @@ import 'package:cake_wallet/view_model/dashboard/action_list_item.dart'; import 'package:cake_wallet/view_model/dashboard/date_section_item.dart'; +import 'package:flutter/foundation.dart'; List formattedItemsList(List items) { final formattedList = []; @@ -11,7 +12,12 @@ List formattedItemsList(List items) { if (lastDate == null) { lastDate = transaction.date; - formattedList.add(DateSectionItem(transaction.date)); + formattedList.add( + DateSectionItem( + transaction.date, + key: ValueKey('date_section_item_${transaction.date.microsecondsSinceEpoch}_key'), + ), + ); formattedList.add(transaction); continue; } @@ -26,7 +32,12 @@ List formattedItemsList(List items) { } lastDate = transaction.date; - formattedList.add(DateSectionItem(transaction.date)); + formattedList.add( + DateSectionItem( + transaction.date, + key: ValueKey('date_section_item_${transaction.date.microsecondsSinceEpoch}_key'), + ), + ); formattedList.add(transaction); } diff --git a/lib/view_model/dashboard/order_list_item.dart b/lib/view_model/dashboard/order_list_item.dart index 9120cc1a6..52adf53a6 100644 --- a/lib/view_model/dashboard/order_list_item.dart +++ b/lib/view_model/dashboard/order_list_item.dart @@ -6,7 +6,9 @@ import 'package:cake_wallet/entities/balance_display_mode.dart'; class OrderListItem extends ActionListItem { OrderListItem({ required this.order, - required this.settingsStore}); + required this.settingsStore, + required super.key, + }); final Order order; final SettingsStore settingsStore; diff --git a/lib/view_model/dashboard/trade_list_item.dart b/lib/view_model/dashboard/trade_list_item.dart index 964ba4ffa..55ae4e99f 100644 --- a/lib/view_model/dashboard/trade_list_item.dart +++ b/lib/view_model/dashboard/trade_list_item.dart @@ -4,7 +4,11 @@ import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/view_model/dashboard/action_list_item.dart'; class TradeListItem extends ActionListItem { - TradeListItem({required this.trade, required this.settingsStore}); + TradeListItem({ + required this.trade, + required this.settingsStore, + required super.key, + }); final Trade trade; final SettingsStore settingsStore; diff --git a/lib/view_model/dashboard/transaction_list_item.dart b/lib/view_model/dashboard/transaction_list_item.dart index b218c8e9f..d9b361fd2 100644 --- a/lib/view_model/dashboard/transaction_list_item.dart +++ b/lib/view_model/dashboard/transaction_list_item.dart @@ -22,8 +22,12 @@ import 'package:cw_core/keyable.dart'; import 'package:cw_core/wallet_type.dart'; class TransactionListItem extends ActionListItem with Keyable { - TransactionListItem( - {required this.transaction, required this.balanceViewModel, required this.settingsStore}); + TransactionListItem({ + required this.transaction, + required this.balanceViewModel, + required this.settingsStore, + required super.key, + }); final TransactionInfo transaction; final BalanceViewModel balanceViewModel; diff --git a/lib/view_model/transaction_details_view_model.dart b/lib/view_model/transaction_details_view_model.dart index da6699ae6..a189ebe6c 100644 --- a/lib/view_model/transaction_details_view_model.dart +++ b/lib/view_model/transaction_details_view_model.dart @@ -20,6 +20,7 @@ import 'package:cake_wallet/view_model/send/send_view_model.dart'; import 'package:collection/collection.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_priority.dart'; +import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; import 'package:intl/src/intl/date_format.dart'; import 'package:mobx/mobx.dart'; @@ -52,7 +53,7 @@ abstract class TransactionDetailsViewModelBase with Store { break; case WalletType.bitcoin: _addElectrumListItems(tx, dateFormat); - if(!canReplaceByFee)_checkForRBF(tx); + if (!canReplaceByFee) _checkForRBF(tx); break; case WalletType.litecoin: case WalletType.bitcoinCash: @@ -83,8 +84,7 @@ abstract class TransactionDetailsViewModelBase with Store { break; } - final descriptionKey = - '${transactionInfo.txHash}_${wallet.walletAddresses.primaryAddress}'; + final descriptionKey = '${transactionInfo.txHash}_${wallet.walletAddresses.primaryAddress}'; final description = transactionDescriptionBox.values.firstWhere( (val) => val.id == descriptionKey || val.id == transactionInfo.txHash, orElse: () => TransactionDescription(id: descriptionKey)); @@ -93,15 +93,20 @@ abstract class TransactionDetailsViewModelBase with Store { final recipientAddress = description.recipientAddress; if (recipientAddress?.isNotEmpty ?? false) { - items.add(StandartListItem( - title: S.current.transaction_details_recipient_address, - value: recipientAddress!)); + items.add( + StandartListItem( + title: S.current.transaction_details_recipient_address, + value: recipientAddress!, + key: ValueKey('standard_list_item_${recipientAddress}_key'), + ), + ); } } final type = wallet.type; - items.add(BlockExplorerListItem( + items.add( + BlockExplorerListItem( title: S.current.view_in_block_explorer, value: _explorerDescription(type), onTap: () async { @@ -109,9 +114,13 @@ abstract class TransactionDetailsViewModelBase with Store { final uri = Uri.parse(_explorerUrl(type, tx.txHash)); if (await canLaunchUrl(uri)) await launchUrl(uri, mode: LaunchMode.externalApplication); } catch (e) {} - })); + }, + key: ValueKey('block_explorer_list_item_${type.name}_wallet_type_key'), + ), + ); - items.add(TextFieldListItem( + items.add( + TextFieldListItem( title: S.current.note_tap_to_change, value: description.note, onSubmitted: (value) { @@ -122,7 +131,10 @@ abstract class TransactionDetailsViewModelBase with Store { } else { transactionDescriptionBox.add(description); } - })); + }, + key: ValueKey('textfield_list_item_note_entry_key'), + ), + ); } final TransactionInfo transactionInfo; @@ -209,14 +221,38 @@ abstract class TransactionDetailsViewModelBase with Store { final addressIndex = tx.additionalInfo['addressIndex'] as int; final feeFormatted = tx.feeFormatted(); final _items = [ - StandartListItem(title: S.current.transaction_details_transaction_id, value: tx.txHash), StandartListItem( - title: S.current.transaction_details_date, value: dateFormat.format(tx.date)), - StandartListItem(title: S.current.transaction_details_height, value: '${tx.height}'), - StandartListItem(title: S.current.transaction_details_amount, value: tx.amountFormatted()), + title: S.current.transaction_details_transaction_id, + value: tx.txHash, + key: ValueKey('standard_list_item_transaction_details_id_key'), + ), + StandartListItem( + title: S.current.transaction_details_date, + value: dateFormat.format(tx.date), + key: ValueKey('standard_list_item_transaction_details_date_key'), + ), + StandartListItem( + title: S.current.transaction_details_height, + value: '${tx.height}', + key: ValueKey('standard_list_item_transaction_details_height_key'), + ), + StandartListItem( + title: S.current.transaction_details_amount, + value: tx.amountFormatted(), + key: ValueKey('standard_list_item_transaction_details_amount_key'), + ), if (feeFormatted != null) - StandartListItem(title: S.current.transaction_details_fee, value: feeFormatted), - if (key?.isNotEmpty ?? false) StandartListItem(title: S.current.transaction_key, value: key!), + StandartListItem( + title: S.current.transaction_details_fee, + value: feeFormatted, + key: ValueKey('standard_list_item_transaction_details_fee_key'), + ), + if (key?.isNotEmpty ?? false) + StandartListItem( + title: S.current.transaction_key, + value: key!, + key: ValueKey('standard_list_item_transaction_key'), + ), ]; if (tx.direction == TransactionDirection.incoming) { @@ -226,14 +262,21 @@ abstract class TransactionDetailsViewModelBase with Store { if (address.isNotEmpty) { isRecipientAddressShown = true; - _items.add(StandartListItem( - title: S.current.transaction_details_recipient_address, - value: address, - )); + _items.add( + StandartListItem( + title: S.current.transaction_details_recipient_address, + value: address, + key: ValueKey('standard_list_item_transaction_details_recipient_address_key'), + ), + ); } if (label.isNotEmpty) { - _items.add(StandartListItem(title: S.current.address_label, value: label)); + _items.add(StandartListItem( + title: S.current.address_label, + value: label, + key: ValueKey('standard_list_item_address_label_key'), + )); } } catch (e) { print(e.toString()); @@ -245,14 +288,37 @@ abstract class TransactionDetailsViewModelBase with Store { void _addElectrumListItems(TransactionInfo tx, DateFormat dateFormat) { final _items = [ - StandartListItem(title: S.current.transaction_details_transaction_id, value: tx.txHash), StandartListItem( - title: S.current.transaction_details_date, value: dateFormat.format(tx.date)), - StandartListItem(title: S.current.confirmations, value: tx.confirmations.toString()), - StandartListItem(title: S.current.transaction_details_height, value: '${tx.height}'), - StandartListItem(title: S.current.transaction_details_amount, value: tx.amountFormatted()), + title: S.current.transaction_details_transaction_id, + value: tx.txHash, + key: ValueKey('standard_list_item_transaction_details_id_key'), + ), + StandartListItem( + title: S.current.transaction_details_date, + value: dateFormat.format(tx.date), + key: ValueKey('standard_list_item_transaction_details_date_key'), + ), + StandartListItem( + title: S.current.confirmations, + value: tx.confirmations.toString(), + key: ValueKey('standard_list_item_transaction_confirmations_key'), + ), + StandartListItem( + title: S.current.transaction_details_height, + value: '${tx.height}', + key: ValueKey('standard_list_item_transaction_details_height_key'), + ), + StandartListItem( + title: S.current.transaction_details_amount, + value: tx.amountFormatted(), + key: ValueKey('standard_list_item_transaction_details_amount_key'), + ), if (tx.feeFormatted()?.isNotEmpty ?? false) - StandartListItem(title: S.current.transaction_details_fee, value: tx.feeFormatted()!), + StandartListItem( + title: S.current.transaction_details_fee, + value: tx.feeFormatted()!, + key: ValueKey('standard_list_item_transaction_details_fee_key'), + ), ]; items.addAll(_items); @@ -260,30 +326,80 @@ abstract class TransactionDetailsViewModelBase with Store { void _addHavenListItems(TransactionInfo tx, DateFormat dateFormat) { items.addAll([ - StandartListItem(title: S.current.transaction_details_transaction_id, value: tx.txHash), StandartListItem( - title: S.current.transaction_details_date, value: dateFormat.format(tx.date)), - StandartListItem(title: S.current.transaction_details_height, value: '${tx.height}'), - StandartListItem(title: S.current.transaction_details_amount, value: tx.amountFormatted()), + title: S.current.transaction_details_transaction_id, + value: tx.txHash, + key: ValueKey('standard_list_item_transaction_details_id_key'), + ), + StandartListItem( + title: S.current.transaction_details_date, + value: dateFormat.format(tx.date), + key: ValueKey('standard_list_item_transaction_details_date_key'), + ), + StandartListItem( + title: S.current.transaction_details_height, + value: '${tx.height}', + key: ValueKey('standard_list_item_transaction_details_height_key'), + ), + StandartListItem( + title: S.current.transaction_details_amount, + value: tx.amountFormatted(), + key: ValueKey('standard_list_item_transaction_details_amount_key'), + ), if (tx.feeFormatted()?.isNotEmpty ?? false) - StandartListItem(title: S.current.transaction_details_fee, value: tx.feeFormatted()!), + StandartListItem( + title: S.current.transaction_details_fee, + value: tx.feeFormatted()!, + key: ValueKey('standard_list_item_transaction_details_fee_key'), + ), ]); } void _addEthereumListItems(TransactionInfo tx, DateFormat dateFormat) { final _items = [ - StandartListItem(title: S.current.transaction_details_transaction_id, value: tx.txHash), StandartListItem( - title: S.current.transaction_details_date, value: dateFormat.format(tx.date)), - StandartListItem(title: S.current.confirmations, value: tx.confirmations.toString()), - StandartListItem(title: S.current.transaction_details_height, value: '${tx.height}'), - StandartListItem(title: S.current.transaction_details_amount, value: tx.amountFormatted()), + title: S.current.transaction_details_transaction_id, + value: tx.txHash, + key: ValueKey('standard_list_item_transaction_details_id_key'), + ), + StandartListItem( + title: S.current.transaction_details_date, + value: dateFormat.format(tx.date), + key: ValueKey('standard_list_item_transaction_details_date_key'), + ), + StandartListItem( + title: S.current.confirmations, + value: tx.confirmations.toString(), + key: ValueKey('standard_list_item_transaction_confirmations_key'), + ), + StandartListItem( + title: S.current.transaction_details_height, + value: '${tx.height}', + key: ValueKey('standard_list_item_transaction_details_height_key'), + ), + StandartListItem( + title: S.current.transaction_details_amount, + value: tx.amountFormatted(), + key: ValueKey('standard_list_item_transaction_details_amount_key'), + ), if (tx.feeFormatted()?.isNotEmpty ?? false) - StandartListItem(title: S.current.transaction_details_fee, value: tx.feeFormatted()!), + StandartListItem( + title: S.current.transaction_details_fee, + value: tx.feeFormatted()!, + key: ValueKey('standard_list_item_transaction_details_fee_key'), + ), if (showRecipientAddress && tx.to != null) - StandartListItem(title: S.current.transaction_details_recipient_address, value: tx.to!), + StandartListItem( + title: S.current.transaction_details_recipient_address, + value: tx.to!, + key: ValueKey('standard_list_item_transaction_details_recipient_address_key'), + ), if (tx.direction == TransactionDirection.incoming && tx.from != null) - StandartListItem(title: S.current.transaction_details_source_address, value: tx.from!), + StandartListItem( + title: S.current.transaction_details_source_address, + value: tx.from!, + key: ValueKey('standard_list_item_transaction_details_source_address_key'), + ), ]; items.addAll(_items); @@ -291,16 +407,43 @@ abstract class TransactionDetailsViewModelBase with Store { void _addNanoListItems(TransactionInfo tx, DateFormat dateFormat) { final _items = [ - StandartListItem(title: S.current.transaction_details_transaction_id, value: tx.txHash), - if (showRecipientAddress && tx.to != null) - StandartListItem(title: S.current.transaction_details_recipient_address, value: tx.to!), - if (showRecipientAddress && tx.from != null) - StandartListItem(title: S.current.transaction_details_source_address, value: tx.from!), - StandartListItem(title: S.current.transaction_details_amount, value: tx.amountFormatted()), StandartListItem( - title: S.current.transaction_details_date, value: dateFormat.format(tx.date)), - StandartListItem(title: S.current.confirmed_tx, value: (tx.confirmations > 0).toString()), - StandartListItem(title: S.current.transaction_details_height, value: '${tx.height}'), + title: S.current.transaction_details_transaction_id, + value: tx.txHash, + key: ValueKey('standard_list_item_transaction_details_id_key'), + ), + if (showRecipientAddress && tx.to != null) + StandartListItem( + title: S.current.transaction_details_recipient_address, + value: tx.to!, + key: ValueKey('standard_list_item_transaction_details_recipient_address_key'), + ), + if (showRecipientAddress && tx.from != null) + StandartListItem( + title: S.current.transaction_details_source_address, + value: tx.from!, + key: ValueKey('standard_list_item_transaction_details_source_address_key'), + ), + StandartListItem( + title: S.current.transaction_details_amount, + value: tx.amountFormatted(), + key: ValueKey('standard_list_item_transaction_details_amount_key'), + ), + StandartListItem( + title: S.current.transaction_details_date, + value: dateFormat.format(tx.date), + key: ValueKey('standard_list_item_transaction_details_date_key'), + ), + StandartListItem( + title: S.current.confirmed_tx, + value: (tx.confirmations > 0).toString(), + key: ValueKey('standard_list_item_transaction_confirmed_key'), + ), + StandartListItem( + title: S.current.transaction_details_height, + value: '${tx.height}', + key: ValueKey('standard_list_item_transaction_details_height_key'), + ), ]; items.addAll(_items); @@ -308,18 +451,49 @@ abstract class TransactionDetailsViewModelBase with Store { void _addPolygonListItems(TransactionInfo tx, DateFormat dateFormat) { final _items = [ - StandartListItem(title: S.current.transaction_details_transaction_id, value: tx.txHash), StandartListItem( - title: S.current.transaction_details_date, value: dateFormat.format(tx.date)), - StandartListItem(title: S.current.confirmations, value: tx.confirmations.toString()), - StandartListItem(title: S.current.transaction_details_height, value: '${tx.height}'), - StandartListItem(title: S.current.transaction_details_amount, value: tx.amountFormatted()), + title: S.current.transaction_details_transaction_id, + value: tx.txHash, + key: ValueKey('standard_list_item_transaction_details_id_key'), + ), + StandartListItem( + title: S.current.transaction_details_date, + value: dateFormat.format(tx.date), + key: ValueKey('standard_list_item_transaction_details_date_key'), + ), + StandartListItem( + title: S.current.confirmations, + value: tx.confirmations.toString(), + key: ValueKey('standard_list_item_transaction_confirmations_key'), + ), + StandartListItem( + title: S.current.transaction_details_height, + value: '${tx.height}', + key: ValueKey('standard_list_item_transaction_details_height_key'), + ), + StandartListItem( + title: S.current.transaction_details_amount, + value: tx.amountFormatted(), + key: ValueKey('standard_list_item_transaction_details_amount_key'), + ), if (tx.feeFormatted()?.isNotEmpty ?? false) - StandartListItem(title: S.current.transaction_details_fee, value: tx.feeFormatted()!), + StandartListItem( + title: S.current.transaction_details_fee, + value: tx.feeFormatted()!, + key: ValueKey('standard_list_item_transaction_details_fee_key'), + ), if (showRecipientAddress && tx.to != null && tx.direction == TransactionDirection.outgoing) - StandartListItem(title: S.current.transaction_details_recipient_address, value: tx.to!), + StandartListItem( + title: S.current.transaction_details_recipient_address, + value: tx.to!, + key: ValueKey('standard_list_item_transaction_details_recipient_address_key'), + ), if (tx.direction == TransactionDirection.incoming && tx.from != null) - StandartListItem(title: S.current.transaction_details_source_address, value: tx.from!), + StandartListItem( + title: S.current.transaction_details_source_address, + value: tx.from!, + key: ValueKey('standard_list_item_transaction_details_source_address_key'), + ), ]; items.addAll(_items); @@ -327,16 +501,39 @@ abstract class TransactionDetailsViewModelBase with Store { void _addSolanaListItems(TransactionInfo tx, DateFormat dateFormat) { final _items = [ - StandartListItem(title: S.current.transaction_details_transaction_id, value: tx.txHash), StandartListItem( - title: S.current.transaction_details_date, value: dateFormat.format(tx.date)), - StandartListItem(title: S.current.transaction_details_amount, value: tx.amountFormatted()), + title: S.current.transaction_details_transaction_id, + value: tx.txHash, + key: ValueKey('standard_list_item_transaction_details_id_key'), + ), + StandartListItem( + title: S.current.transaction_details_date, + value: dateFormat.format(tx.date), + key: ValueKey('standard_list_item_transaction_details_date_key'), + ), + StandartListItem( + title: S.current.transaction_details_amount, + value: tx.amountFormatted(), + key: ValueKey('standard_list_item_transaction_details_amount_key'), + ), if (tx.feeFormatted()?.isNotEmpty ?? false) - StandartListItem(title: S.current.transaction_details_fee, value: tx.feeFormatted()!), + StandartListItem( + title: S.current.transaction_details_fee, + value: tx.feeFormatted()!, + key: ValueKey('standard_list_item_transaction_details_fee_key'), + ), if (showRecipientAddress && tx.to != null) - StandartListItem(title: S.current.transaction_details_recipient_address, value: tx.to!), + StandartListItem( + title: S.current.transaction_details_recipient_address, + value: tx.to!, + key: ValueKey('standard_list_item_transaction_details_recipient_address_key'), + ), if (tx.from != null) - StandartListItem(title: S.current.transaction_details_source_address, value: tx.from!), + StandartListItem( + title: S.current.transaction_details_source_address, + value: tx.from!, + key: ValueKey('standard_list_item_transaction_details_source_address_key'), + ), ]; items.addAll(_items); @@ -354,7 +551,13 @@ abstract class TransactionDetailsViewModelBase with Store { newFee = bitcoin!.getFeeAmountForPriority( wallet, bitcoin!.getBitcoinTransactionPriorityMedium(), inputsCount, outputsCount); - RBFListItems.add(StandartListItem(title: S.current.old_fee, value: tx.feeFormatted() ?? '0.0')); + RBFListItems.add( + StandartListItem( + title: S.current.old_fee, + value: tx.feeFormatted() ?? '0.0', + key: ValueKey('standard_list_item_rbf_old_fee_key'), + ), + ); if (transactionInfo.fee != null && rawTransaction.isNotEmpty) { final size = bitcoin!.getTransactionVSize(wallet, rawTransaction); @@ -371,7 +574,9 @@ abstract class TransactionDetailsViewModelBase with Store { final customItemIndex = customItem != null ? priorities.indexOf(customItem) : null; final maxCustomFeeRate = sendViewModel.maxCustomFeeRate?.toDouble(); - RBFListItems.add(StandardPickerListItem( + RBFListItems.add( + StandardPickerListItem( + key: ValueKey('standard_picker_list_item_transaction_priorities_key'), title: S.current.estimated_new_fee, value: bitcoin!.formatterBitcoinAmountToString(amount: newFee) + ' ${walletTypeToCryptoCurrency(wallet.type)}', @@ -387,42 +592,73 @@ abstract class TransactionDetailsViewModelBase with Store { onItemSelected: (dynamic item, double sliderValue) { transactionPriority = item as TransactionPriority; return setNewFee(value: sliderValue, priority: transactionPriority!); - })); + }, + ), + ); if (transactionInfo.inputAddresses != null && transactionInfo.inputAddresses!.isNotEmpty) { - RBFListItems.add(StandardExpandableListItem( - title: S.current.inputs, expandableItems: transactionInfo.inputAddresses!)); + RBFListItems.add( + StandardExpandableListItem( + key: ValueKey('standard_expandable_list_item_transaction_input_addresses_key'), + title: S.current.inputs, + expandableItems: transactionInfo.inputAddresses!, + ), + ); } if (transactionInfo.outputAddresses != null && transactionInfo.outputAddresses!.isNotEmpty) { final outputAddresses = transactionInfo.outputAddresses!.map((element) { if (element.contains('OP_RETURN:') && element.length > 40) { - return element.substring(0, 40) + '...'; + return element.substring(0, 40) + '...'; } return element; }).toList(); RBFListItems.add( - StandardExpandableListItem(title: S.current.outputs, expandableItems: outputAddresses)); + StandardExpandableListItem( + title: S.current.outputs, + expandableItems: outputAddresses, + key: ValueKey('standard_expandable_list_item_transaction_output_addresses_key'), + ), + ); } } void _addTronListItems(TransactionInfo tx, DateFormat dateFormat) { final _items = [ - StandartListItem(title: S.current.transaction_details_transaction_id, value: tx.txHash), StandartListItem( - title: S.current.transaction_details_date, value: dateFormat.format(tx.date)), - StandartListItem(title: S.current.transaction_details_amount, value: tx.amountFormatted()), + title: S.current.transaction_details_transaction_id, + value: tx.txHash, + key: ValueKey('standard_list_item_transaction_details_id_key'), + ), + StandartListItem( + title: S.current.transaction_details_date, + value: dateFormat.format(tx.date), + key: ValueKey('standard_list_item_transaction_details_date_key'), + ), + StandartListItem( + title: S.current.transaction_details_amount, + value: tx.amountFormatted(), + key: ValueKey('standard_list_item_transaction_details_amount_key'), + ), if (tx.feeFormatted()?.isNotEmpty ?? false) - StandartListItem(title: S.current.transaction_details_fee, value: tx.feeFormatted()!), + StandartListItem( + title: S.current.transaction_details_fee, + value: tx.feeFormatted()!, + key: ValueKey('standard_list_item_transaction_details_fee_key'), + ), if (showRecipientAddress && tx.to != null) StandartListItem( - title: S.current.transaction_details_recipient_address, - value: tron!.getTronBase58Address(tx.to!, wallet)), + title: S.current.transaction_details_recipient_address, + value: tron!.getTronBase58Address(tx.to!, wallet), + key: ValueKey('standard_list_item_transaction_details_recipient_address_key'), + ), if (tx.from != null) StandartListItem( - title: S.current.transaction_details_source_address, - value: tron!.getTronBase58Address(tx.from!, wallet)), + title: S.current.transaction_details_source_address, + value: tron!.getTronBase58Address(tx.from!, wallet), + key: ValueKey('standard_list_item_transaction_details_source_address_key'), + ), ]; items.addAll(_items); @@ -455,7 +691,7 @@ abstract class TransactionDetailsViewModelBase with Store { return bitcoin!.formatterBitcoinAmountToString(amount: newFee); } - void replaceByFee(String newFee) => sendViewModel.replaceByFee(transactionInfo, newFee,); + void replaceByFee(String newFee) => sendViewModel.replaceByFee(transactionInfo, newFee); @computed String get pendingTransactionFiatAmountValueFormatted => sendViewModel.isFiatDisabled @@ -473,14 +709,38 @@ abstract class TransactionDetailsViewModelBase with Store { final addressIndex = tx.additionalInfo['addressIndex'] as int; final feeFormatted = tx.feeFormatted(); final _items = [ - StandartListItem(title: S.current.transaction_details_transaction_id, value: tx.txHash), StandartListItem( - title: S.current.transaction_details_date, value: dateFormat.format(tx.date)), - StandartListItem(title: S.current.transaction_details_height, value: '${tx.height}'), - StandartListItem(title: S.current.transaction_details_amount, value: tx.amountFormatted()), + title: S.current.transaction_details_transaction_id, + value: tx.txHash, + key: ValueKey('standard_list_item_transaction_details_id_key'), + ), + StandartListItem( + title: S.current.transaction_details_date, + value: dateFormat.format(tx.date), + key: ValueKey('standard_list_item_transaction_details_date_key'), + ), + StandartListItem( + title: S.current.transaction_details_height, + value: '${tx.height}', + key: ValueKey('standard_list_item_transaction_details_height_key'), + ), + StandartListItem( + title: S.current.transaction_details_amount, + value: tx.amountFormatted(), + key: ValueKey('standard_list_item_transaction_details_amount_key'), + ), if (feeFormatted != null) - StandartListItem(title: S.current.transaction_details_fee, value: feeFormatted), - if (key?.isNotEmpty ?? false) StandartListItem(title: S.current.transaction_key, value: key!), + StandartListItem( + title: S.current.transaction_details_fee, + value: feeFormatted, + key: ValueKey('standard_list_item_transaction_details_fee_key'), + ), + if (key?.isNotEmpty ?? false) + StandartListItem( + title: S.current.transaction_key, + value: key!, + key: ValueKey('standard_list_item_transaction_key'), + ), ]; if (tx.direction == TransactionDirection.incoming) { @@ -490,14 +750,23 @@ abstract class TransactionDetailsViewModelBase with Store { if (address.isNotEmpty) { isRecipientAddressShown = true; - _items.add(StandartListItem( - title: S.current.transaction_details_recipient_address, - value: address, - )); + _items.add( + StandartListItem( + title: S.current.transaction_details_recipient_address, + value: address, + key: ValueKey('standard_list_item_transaction_details_recipient_address_key'), + ), + ); } if (label.isNotEmpty) { - _items.add(StandartListItem(title: S.current.address_label, value: label)); + _items.add( + StandartListItem( + title: S.current.address_label, + value: label, + key: ValueKey('standard_list_item_address_label_key'), + ), + ); } } catch (e) { print(e.toString()); diff --git a/lib/view_model/wallet_keys_view_model.dart b/lib/view_model/wallet_keys_view_model.dart index 9921ae30a..6455fc0e3 100644 --- a/lib/view_model/wallet_keys_view_model.dart +++ b/lib/view_model/wallet_keys_view_model.dart @@ -10,6 +10,7 @@ import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cw_monero/monero_wallet.dart'; +import 'package:flutter/foundation.dart'; import 'package:mobx/mobx.dart'; import 'package:polyseed/polyseed.dart'; @@ -24,6 +25,7 @@ abstract class WalletKeysViewModelBase with Store { _appStore.wallet!.type == WalletType.bitcoinCash ? S.current.wallet_seed : S.current.wallet_keys, + _walletName = _appStore.wallet!.type.name, _restoreHeight = _appStore.wallet!.walletInfo.restoreHeight, _restoreHeightByTransactions = 0, items = ObservableList() { @@ -38,12 +40,10 @@ abstract class WalletKeysViewModelBase with Store { _appStore.wallet!.type == WalletType.wownero) { final accountTransactions = _getWalletTransactions(_appStore.wallet!); if (accountTransactions.isNotEmpty) { - final incomingAccountTransactions = accountTransactions - .where((tx) => tx.direction == TransactionDirection.incoming); + final incomingAccountTransactions = + accountTransactions.where((tx) => tx.direction == TransactionDirection.incoming); if (incomingAccountTransactions.isNotEmpty) { - incomingAccountTransactions - .toList() - .sort((a, b) => a.date.compareTo(b.date)); + incomingAccountTransactions.toList().sort((a, b) => a.date.compareTo(b.date)); _restoreHeightByTransactions = _getRestoreHeightByTransactions( _appStore.wallet!.type, incomingAccountTransactions.first.date); } @@ -55,6 +55,10 @@ abstract class WalletKeysViewModelBase with Store { final String title; + final String _walletName; + + AppStore get appStore => _appStore; + final AppStore _appStore; final int _restoreHeight; @@ -70,37 +74,56 @@ abstract class WalletKeysViewModelBase with Store { items.addAll([ if (keys['publicSpendKey'] != null) StandartListItem( - title: S.current.spend_key_public, - value: keys['publicSpendKey']!), + key: ValueKey('${_walletName}_wallet_public_spend_key_item_key'), + title: S.current.spend_key_public, + value: keys['publicSpendKey']!, + ), if (keys['privateSpendKey'] != null) StandartListItem( - title: S.current.spend_key_private, - value: keys['privateSpendKey']!), + key: ValueKey('${_walletName}_wallet_private_spend_key_item_key'), + title: S.current.spend_key_private, + value: keys['privateSpendKey']!, + ), if (keys['publicViewKey'] != null) StandartListItem( - title: S.current.view_key_public, value: keys['publicViewKey']!), + key: ValueKey('${_walletName}_wallet_public_view_key_item_key'), + title: S.current.view_key_public, + value: keys['publicViewKey']!, + ), if (keys['privateViewKey'] != null) StandartListItem( - title: S.current.view_key_private, - value: keys['privateViewKey']!), + key: ValueKey('${_walletName}_wallet_private_view_key_item_key'), + title: S.current.view_key_private, + value: keys['privateViewKey']!, + ), if (_appStore.wallet!.seed!.isNotEmpty) - StandartListItem(title: S.current.wallet_seed, value: _appStore.wallet!.seed!), + StandartListItem( + key: ValueKey('${_walletName}_wallet_seed_item_key'), + title: S.current.wallet_seed, + value: _appStore.wallet!.seed!, + ), ]); - if (_appStore.wallet?.seed != null && - Polyseed.isValidSeed(_appStore.wallet!.seed!)) { + if (_appStore.wallet?.seed != null && Polyseed.isValidSeed(_appStore.wallet!.seed!)) { final lang = PolyseedLang.getByPhrase(_appStore.wallet!.seed!); - items.add(StandartListItem( + items.add( + StandartListItem( + key: ValueKey('${_walletName}_wallet_seed_legacy_item_key'), title: S.current.wallet_seed_legacy, - value: (_appStore.wallet as MoneroWalletBase) - .seedLegacy(lang.nameEnglish))); + value: (_appStore.wallet as MoneroWalletBase).seedLegacy(lang.nameEnglish), + ), + ); } final restoreHeight = monero!.getRestoreHeight(_appStore.wallet!); if (restoreHeight != null) { - items.add(StandartListItem( + items.add( + StandartListItem( + key: ValueKey('${_walletName}_wallet_restore_height_item_key'), title: S.current.wallet_recovery_height, - value: restoreHeight.toString())); + value: restoreHeight.toString(), + ), + ); } } @@ -110,21 +133,34 @@ abstract class WalletKeysViewModelBase with Store { items.addAll([ if (keys['publicSpendKey'] != null) StandartListItem( - title: S.current.spend_key_public, - value: keys['publicSpendKey']!), + key: ValueKey('${_walletName}_wallet_public_spend_key_item_key'), + title: S.current.spend_key_public, + value: keys['publicSpendKey']!, + ), if (keys['privateSpendKey'] != null) StandartListItem( - title: S.current.spend_key_private, - value: keys['privateSpendKey']!), + key: ValueKey('${_walletName}_wallet_private_spend_key_item_key'), + title: S.current.spend_key_private, + value: keys['privateSpendKey']!, + ), if (keys['publicViewKey'] != null) StandartListItem( - title: S.current.view_key_public, value: keys['publicViewKey']!), + key: ValueKey('${_walletName}_wallet_public_view_key_item_key'), + title: S.current.view_key_public, + value: keys['publicViewKey']!, + ), if (keys['privateViewKey'] != null) StandartListItem( - title: S.current.view_key_private, - value: keys['privateViewKey']!), + key: ValueKey('${_walletName}_wallet_private_view_key_item_key'), + title: S.current.view_key_private, + value: keys['privateViewKey']!, + ), if (_appStore.wallet!.seed!.isNotEmpty) - StandartListItem(title: S.current.wallet_seed, value: _appStore.wallet!.seed!), + StandartListItem( + key: ValueKey('${_walletName}_wallet_seed_item_key'), + title: S.current.wallet_seed, + value: _appStore.wallet!.seed!, + ), ]); } @@ -134,29 +170,45 @@ abstract class WalletKeysViewModelBase with Store { items.addAll([ if (keys['publicSpendKey'] != null) StandartListItem( - title: S.current.spend_key_public, - value: keys['publicSpendKey']!), + key: ValueKey('${_walletName}_wallet_public_spend_key_item_key'), + title: S.current.spend_key_public, + value: keys['publicSpendKey']!, + ), if (keys['privateSpendKey'] != null) StandartListItem( - title: S.current.spend_key_private, - value: keys['privateSpendKey']!), + key: ValueKey('${_walletName}_wallet_private_spend_key_item_key'), + title: S.current.spend_key_private, + value: keys['privateSpendKey']!, + ), if (keys['publicViewKey'] != null) StandartListItem( - title: S.current.view_key_public, value: keys['publicViewKey']!), + key: ValueKey('${_walletName}_wallet_public_view_key_item_key'), + title: S.current.view_key_public, + value: keys['publicViewKey']!, + ), if (keys['privateViewKey'] != null) StandartListItem( - title: S.current.view_key_private, - value: keys['privateViewKey']!), + key: ValueKey('${_walletName}_wallet_private_view_key_item_key'), + title: S.current.view_key_private, + value: keys['privateViewKey']!, + ), if (_appStore.wallet!.seed!.isNotEmpty) - StandartListItem(title: S.current.wallet_seed, value: _appStore.wallet!.seed!), + StandartListItem( + key: ValueKey('${_walletName}_wallet_seed_item_key'), + title: S.current.wallet_seed, + value: _appStore.wallet!.seed!, + ), ]); - if (_appStore.wallet?.seed != null && - Polyseed.isValidSeed(_appStore.wallet!.seed!)) { + if (_appStore.wallet?.seed != null && Polyseed.isValidSeed(_appStore.wallet!.seed!)) { final lang = PolyseedLang.getByPhrase(_appStore.wallet!.seed!); - items.add(StandartListItem( + items.add( + StandartListItem( + key: ValueKey('${_walletName}_wallet_seed_legacy_item_key'), title: S.current.wallet_seed_legacy, - value: wownero!.getLegacySeed(_appStore.wallet!, lang.nameEnglish))); + value: wownero!.getLegacySeed(_appStore.wallet!, lang.nameEnglish), + ), + ); } } @@ -173,7 +225,10 @@ abstract class WalletKeysViewModelBase with Store { // if (keys['publicKey'] != null) // StandartListItem(title: S.current.public_key, value: keys['publicKey']!), StandartListItem( - title: S.current.wallet_seed, value: _appStore.wallet!.seed!), + key: ValueKey('${_walletName}_wallet_seed_item_key'), + title: S.current.wallet_seed, + value: _appStore.wallet!.seed!, + ), ]); } @@ -183,31 +238,43 @@ abstract class WalletKeysViewModelBase with Store { items.addAll([ if (_appStore.wallet!.privateKey != null) StandartListItem( - title: S.current.private_key, - value: _appStore.wallet!.privateKey!), + key: ValueKey('${_walletName}_wallet_private_key_item_key'), + title: S.current.private_key, + value: _appStore.wallet!.privateKey!, + ), if (_appStore.wallet!.seed != null) StandartListItem( - title: S.current.wallet_seed, value: _appStore.wallet!.seed!), + key: ValueKey('${_walletName}_wallet_seed_item_key'), + title: S.current.wallet_seed, + value: _appStore.wallet!.seed!, + ), ]); } - bool nanoBased = _appStore.wallet!.type == WalletType.nano || - _appStore.wallet!.type == WalletType.banano; + bool nanoBased = + _appStore.wallet!.type == WalletType.nano || _appStore.wallet!.type == WalletType.banano; if (nanoBased) { // we always have the hex version of the seed and private key: items.addAll([ if (_appStore.wallet!.seed != null) StandartListItem( - title: S.current.wallet_seed, value: _appStore.wallet!.seed!), + key: ValueKey('${_walletName}_wallet_seed_item_key'), + title: S.current.wallet_seed, + value: _appStore.wallet!.seed!, + ), if (_appStore.wallet!.hexSeed != null) StandartListItem( - title: S.current.seed_hex_form, - value: _appStore.wallet!.hexSeed!), + key: ValueKey('${_walletName}_wallet_hex_seed_key'), + title: S.current.seed_hex_form, + value: _appStore.wallet!.hexSeed!, + ), if (_appStore.wallet!.privateKey != null) StandartListItem( - title: S.current.private_key, - value: _appStore.wallet!.privateKey!), + key: ValueKey('${_walletName}_wallet_private_key_item_key'), + title: S.current.private_key, + value: _appStore.wallet!.privateKey!, + ), ]); } } @@ -273,8 +340,7 @@ abstract class WalletKeysViewModelBase with Store { if (_appStore.wallet!.seed != null) 'seed': _appStore.wallet!.seed!, if (_appStore.wallet!.seed == null && _appStore.wallet!.hexSeed != null) 'hexSeed': _appStore.wallet!.hexSeed!, - if (_appStore.wallet!.seed == null && - _appStore.wallet!.privateKey != null) + if (_appStore.wallet!.seed == null && _appStore.wallet!.privateKey != null) 'private_key': _appStore.wallet!.privateKey!, if (restoreHeightResult != null) ...{'height': restoreHeightResult}, if (_appStore.wallet!.passphrase != null) 'passphrase': _appStore.wallet!.passphrase! @@ -292,11 +358,7 @@ abstract class WalletKeysViewModelBase with Store { } else if (wallet.type == WalletType.haven) { return haven!.getTransactionHistory(wallet).transactions.values.toList(); } else if (wallet.type == WalletType.wownero) { - return wownero! - .getTransactionHistory(wallet) - .transactions - .values - .toList(); + return wownero!.getTransactionHistory(wallet).transactions.values.toList(); } return []; } @@ -312,6 +374,5 @@ abstract class WalletKeysViewModelBase with Store { return 0; } - String getRoundedRestoreHeight(int height) => - ((height / 1000).floor() * 1000).toString(); + String getRoundedRestoreHeight(int height) => ((height / 1000).floor() * 1000).toString(); } diff --git a/tool/utils/secret_key.dart b/tool/utils/secret_key.dart index d3b652935..88cbbfce3 100644 --- a/tool/utils/secret_key.dart +++ b/tool/utils/secret_key.dart @@ -44,12 +44,35 @@ class SecretKey { SecretKey('cakePayApiKey', () => ''), SecretKey('CSRFToken', () => ''), SecretKey('authorization', () => ''), + SecretKey('moneroTestWalletSeeds', () => ''), + SecretKey('moneroLegacyTestWalletSeeds ', () => ''), + SecretKey('bitcoinTestWalletSeeds', () => ''), + SecretKey('ethereumTestWalletSeeds', () => ''), + SecretKey('litecoinTestWalletSeeds', () => ''), + SecretKey('bitcoinCashTestWalletSeeds', () => ''), + SecretKey('polygonTestWalletSeeds', () => ''), + SecretKey('solanaTestWalletSeeds', () => ''), + SecretKey('polygonTestWalletSeeds', () => ''), + SecretKey('tronTestWalletSeeds', () => ''), + SecretKey('nanoTestWalletSeeds', () => ''), + SecretKey('wowneroTestWalletSeeds', () => ''), + SecretKey('moneroTestWalletReceiveAddress', () => ''), + SecretKey('bitcoinTestWalletReceiveAddress', () => ''), + SecretKey('ethereumTestWalletReceiveAddress', () => ''), + SecretKey('litecoinTestWalletReceiveAddress', () => ''), + SecretKey('bitco inCashTestWalletReceiveAddress', () => ''), + SecretKey('polygonTestWalletReceiveAddress', () => ''), + SecretKey('solanaTestWalletReceiveAddress', () => ''), + SecretKey('tronTestWalletReceiveAddress', () => ''), + SecretKey('nanoTestWalletReceiveAddress', () => ''), + SecretKey('wowneroTestWalletReceiveAddress', () => ''), SecretKey('etherScanApiKey', () => ''), SecretKey('polygonScanApiKey', () => ''), SecretKey('letsExchangeBearerToken', () => ''), SecretKey('letsExchangeAffiliateId', () => ''), SecretKey('stealthExBearerToken', () => ''), SecretKey('stealthExAdditionalFeePercent', () => ''), + SecretKey('moneroTestWalletBlockHeight', () => ''), ]; static final evmChainsSecrets = [