From cc5a2c7f5001c28cb9306ba912dc915c1dba5b8d Mon Sep 17 00:00:00 2001 From: Willy <11148913+willyfromtheblock@users.noreply.github.com> Date: Thu, 30 Nov 2023 19:09:44 +0100 Subject: [PATCH] 1.2.3 (#243) * version bump * upgrade coinlib_flutter to * skeleton for experimental features settings * allow to activate and deactivate experimental features * replace tab class with enum * allow option to add watchonly wallet * introduce watchOnly for wallet * don't count watchOnly towards nOfWalletOfLetterCode * wallets found in scan can not by watchOnly * show watch only icon in wallet list * fix tests * don't generate unused address for watchOnly wallet * bottom navigation for watchOnly wallet * reduce menu items for watchOnly * pass watchOnly to address tab * introduce watchOnly address book * move addr book search bar to walletHome appBar * first sucessful watch only wallet * make whole "watch only" line clickable * bring over slidable from addr book * introduce isWatchOnly to address model * return isWatchOnly addresses from getWatchedWalletScriptHashes * refactor addressbook slidable * fix layouting and colors for new addr book * properly remove watchOnly addresses * show place holders if no addresses are configured * migrate edit dialog and 2dos * changelog * upgrade dart and linter * fix onSurface colors * fix appBarTheme in light mode * fix send tab for regular wallets * 2do * more theming 2dos * fix e2e new key test and 2dos for theming * adapt snack bar colors to material 3 * changelog * add cancel button to wallet reset * give ModalBottomSheetContainer responsive width * 2do is checked * fix addr book issue in dark mode * make watched icon explicitly black for dark mode * upgrade coinlib * convert slidable to expandable * finalize watch only address book --- CHANGELOG.md | 5 + assets/translations/en.json | 5 + lib/main.dart | 2 +- lib/models/experimental_features.dart | 1 + lib/models/hive/app_options.dart | 12 + lib/models/hive/app_options.g.dart | 10 +- lib/models/hive/coin_wallet.dart | 13 + lib/models/hive/coin_wallet.g.dart | 7 +- lib/models/hive/wallet_address.dart | 3 + lib/models/hive/wallet_address.g.dart | 7 +- lib/providers/app_settings_provider.dart | 11 + lib/providers/wallet_provider.dart | 79 +++++- lib/screens/about.dart | 2 +- lib/screens/auth_jail.dart | 8 +- lib/screens/changelog.dart | 2 +- lib/screens/qrcode_scanner.dart | 4 +- lib/screens/router_master.dart | 7 +- lib/screens/secure_storage_error_screen.dart | 2 +- .../app_settings_experimental_features.dart | 95 +++++++ .../settings/app_settings_notifications.dart | 2 +- lib/screens/settings/app_settings_screen.dart | 4 +- .../settings/app_settings_wallet_order.dart | 2 +- .../settings/server_settings/server_add.dart | 2 +- .../server_settings/server_settings.dart | 2 +- lib/screens/settings/settings_helpers.dart | 16 +- .../app_settings_wallet_scanner.dart | 3 +- lib/screens/setup/setup_auth.dart | 2 +- lib/screens/setup/setup_create_wallet.dart | 2 +- lib/screens/setup/setup_data_feeds.dart | 2 +- lib/screens/setup/setup_import_seed.dart | 2 +- lib/screens/setup/setup_landing.dart | 10 +- lib/screens/setup/setup_language.dart | 2 +- lib/screens/setup/setup_legal.dart | 2 +- lib/screens/wallet/address_selector.dart | 6 +- lib/screens/wallet/import_paper_wallet.dart | 2 +- lib/screens/wallet/import_wif.dart | 2 +- .../wallet/transaction_confirmation.dart | 4 +- lib/screens/wallet/transaction_details.dart | 2 +- lib/screens/wallet/wallet_home.dart | 170 ++++++++--- lib/screens/wallet/wallet_list.dart | 35 ++- lib/screens/wallet/wallet_sign_message.dart | 2 +- lib/screens/wallet/wallet_verify_message.dart | 2 +- lib/tools/app_routes.dart | 7 + lib/tools/app_themes.dart | 15 +- lib/widgets/buttons.dart | 17 +- lib/widgets/double_tab_to_clipboard.dart | 4 +- lib/widgets/expanded_section.dart | 3 +- lib/widgets/loading_indicator.dart | 2 +- lib/widgets/logout_dialog.dart | 2 +- lib/widgets/logout_dialog_dummy.dart | 2 +- lib/widgets/service_container.dart | 13 +- lib/widgets/settings/settings_auth.dart | 4 +- .../settings/settings_price_ticker.dart | 4 +- lib/widgets/setup/session_slider.dart | 2 +- lib/widgets/setup_progress.dart | 5 +- .../addresses_tab_expandable.dart | 265 ++++++++++++++++++ .../addresses_tab_expandable_icon.dart | 31 ++ .../addresses_tab_watch_only.dart | 265 ++++++++++++++++++ lib/widgets/wallet/addresses_tab.dart | 34 +-- lib/widgets/wallet/new_wallet.dart | 30 +- lib/widgets/wallet/receive_tab.dart | 14 +- lib/widgets/wallet/send_tab.dart | 18 +- lib/widgets/wallet/send_tab_management.dart | 4 +- lib/widgets/wallet/send_tab_navigator.dart | 4 +- lib/widgets/wallet/transactions_list.dart | 14 +- lib/widgets/wallet/wallet_balance_header.dart | 4 +- lib/widgets/wallet/wallet_balance_price.dart | 4 +- .../wallet/wallet_home_connection.dart | 3 +- lib/widgets/wallet/wallet_home_qr.dart | 2 +- .../wallet/wallet_reset_bottom_sheet.dart | 9 + pubspec.lock | 58 ++-- pubspec.yaml | 8 +- test/transaction_building_test.dart | 2 + test_driver/key_new.dart | 1 + 74 files changed, 1181 insertions(+), 222 deletions(-) create mode 100644 lib/models/experimental_features.dart create mode 100644 lib/screens/settings/app_settings_experimental_features.dart create mode 100644 lib/widgets/wallet/address_book/addresses_tab_expandable.dart create mode 100644 lib/widgets/wallet/address_book/addresses_tab_expandable_icon.dart create mode 100644 lib/widgets/wallet/address_book/addresses_tab_watch_only.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index b151a57d..7f682456 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +### **1.2.3** (2023-11-30) +* Experimental features are now available in Settings +* "Watch only" wallets are the first available feature that can be enabled. These wallets can only monitor the balance of an address and cannot spend coins. +* App now uses Material 3 design language + ### **1.2.2** (2023-09-27) * Change the options for purchasing Peercoin on some devices diff --git a/assets/translations/en.json b/assets/translations/en.json index 7045fe01..e7f464ba 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -90,8 +90,12 @@ "app_settings_price_feed_disable_button": "Disable Price Feed API", "app_settings_price_feed_search": "Search currency code", "app_settings_language_search": "Search language", + "app_settings_experimental_features": "Experimental Features", + "app_settings_experimental_feature_watchOnlyWallets": "Watch only wallets", "app_settings_price_alert_content": "This will enable our privacy friendly price data feed.\nIt can be disabled anytime.", "app_wallets": "Wallets", + "watch_only": "Watch only", + "addresses_none": "No addresses found", "authenticate_biometric_hint": "Verify identity", "authenticate_biometric_reason": "Please authenticate.", "authenticate_biometric_title": "Authentication required", @@ -319,6 +323,7 @@ "receive_website_faucet": "You can receive free testnet coins on this faucet.", "scan_qr": "Scan QR-Code", "search": "Search", + "search_address": "Search address", "select_coin": "Please select a coin", "send": "Send", "send_address": "Address", diff --git a/lib/main.dart b/lib/main.dart index 7ba7dd42..a8cfa716 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -183,7 +183,7 @@ void main() async { } class PeercoinApp extends StatelessWidget { - const PeercoinApp({Key? key}) : super(key: key); + const PeercoinApp({super.key}); @override Widget build(BuildContext context) { diff --git a/lib/models/experimental_features.dart b/lib/models/experimental_features.dart new file mode 100644 index 00000000..f350446e --- /dev/null +++ b/lib/models/experimental_features.dart @@ -0,0 +1 @@ +enum ExperimentalFeatures { watchOnlyWallets } diff --git a/lib/models/hive/app_options.dart b/lib/models/hive/app_options.dart index 4df04c95..2824639f 100644 --- a/lib/models/hive/app_options.dart +++ b/lib/models/hive/app_options.dart @@ -42,6 +42,9 @@ class AppOptionsStore extends HiveObject { @HiveField(10, defaultValue: []) List _walletOrder = []; + @HiveField(11, defaultValue: []) + List _activatedExperimentalFeatures = []; + AppOptionsStore(this._allowBiometrics); bool get allowBiometrics { @@ -142,4 +145,13 @@ class AppOptionsStore extends HiveObject { List get walletOrder { return _walletOrder; } + + set activatedExperimentalFeatures(List newFeatures) { + _activatedExperimentalFeatures = newFeatures; + save(); + } + + List get activatedExperimentalFeatures { + return _activatedExperimentalFeatures; + } } diff --git a/lib/models/hive/app_options.g.dart b/lib/models/hive/app_options.g.dart index aa1141f4..33bc04da 100644 --- a/lib/models/hive/app_options.g.dart +++ b/lib/models/hive/app_options.g.dart @@ -30,13 +30,15 @@ class AppOptionsStoreAdapter extends TypeAdapter { .._periodicReminterItemsNextView = fields[9] == null ? {} : (fields[9] as Map).cast() .._walletOrder = - fields[10] == null ? [] : (fields[10] as List).cast(); + fields[10] == null ? [] : (fields[10] as List).cast() + .._activatedExperimentalFeatures = + fields[11] == null ? [] : (fields[11] as List).cast(); } @override void write(BinaryWriter writer, AppOptionsStore obj) { writer - ..writeByte(11) + ..writeByte(12) ..writeByte(0) ..write(obj._authenticationOptions) ..writeByte(1) @@ -58,7 +60,9 @@ class AppOptionsStoreAdapter extends TypeAdapter { ..writeByte(9) ..write(obj._periodicReminterItemsNextView) ..writeByte(10) - ..write(obj._walletOrder); + ..write(obj._walletOrder) + ..writeByte(11) + ..write(obj._activatedExperimentalFeatures); } @override diff --git a/lib/models/hive/coin_wallet.dart b/lib/models/hive/coin_wallet.dart index 8f494902..93c23771 100644 --- a/lib/models/hive/coin_wallet.dart +++ b/lib/models/hive/coin_wallet.dart @@ -45,12 +45,16 @@ class CoinWallet extends HiveObject { @HiveField(10, defaultValue: false) bool _dueForRescan = false; + @HiveField(11, defaultValue: false) + bool _watchOnly = false; + CoinWallet( this._name, this._title, this._letterCode, this._walletIndex, this._dueForRescan, + this._watchOnly, ); set addNewAddress(WalletAddress newAddress) { @@ -70,6 +74,10 @@ class CoinWallet extends HiveObject { return _balance; } + bool get watchOnly { + return _watchOnly; + } + set balance(int newBalance) { _balance = newBalance; save(); @@ -179,6 +187,11 @@ class CoinWallet extends HiveObject { save(); } + void removeTransaction(WalletTransaction tx) { + _transactions.removeWhere((element) => element.txid == tx.txid); + save(); + } + void putUtxo(WalletUtxo newUtxo) { _utxos.add(newUtxo); save(); diff --git a/lib/models/hive/coin_wallet.g.dart b/lib/models/hive/coin_wallet.g.dart index bd14c1ce..0dbeeeff 100644 --- a/lib/models/hive/coin_wallet.g.dart +++ b/lib/models/hive/coin_wallet.g.dart @@ -22,6 +22,7 @@ class CoinWalletAdapter extends TypeAdapter { fields[1] as String, fields[9] == null ? 0 : fields[9] as int, fields[10] == null ? false : fields[10] as bool, + fields[11] == null ? false : fields[11] as bool, ) .._addresses = (fields[2] as List).cast() .._transactions = (fields[3] as List).cast() @@ -35,7 +36,7 @@ class CoinWalletAdapter extends TypeAdapter { @override void write(BinaryWriter writer, CoinWallet obj) { writer - ..writeByte(11) + ..writeByte(12) ..writeByte(0) ..write(obj._name) ..writeByte(1) @@ -57,7 +58,9 @@ class CoinWalletAdapter extends TypeAdapter { ..writeByte(9) ..write(obj._walletIndex) ..writeByte(10) - ..write(obj._dueForRescan); + ..write(obj._dueForRescan) + ..writeByte(11) + ..write(obj._watchOnly); } @override diff --git a/lib/models/hive/wallet_address.dart b/lib/models/hive/wallet_address.dart index b020fb7e..4d63d09a 100644 --- a/lib/models/hive/wallet_address.dart +++ b/lib/models/hive/wallet_address.dart @@ -21,6 +21,8 @@ class WalletAddress extends HiveObject { int notificationBackendCount = 0; @HiveField(8, defaultValue: false) bool isWatched = false; + @HiveField(9, defaultValue: false) + bool isWatchOnly = false; WalletAddress({ required this.address, @@ -29,6 +31,7 @@ class WalletAddress extends HiveObject { required this.status, required this.isOurs, required this.wif, + required this.isWatchOnly, }); set newStatus(String? newStatus) { diff --git a/lib/models/hive/wallet_address.g.dart b/lib/models/hive/wallet_address.g.dart index 5d7f70d5..6408dbc8 100644 --- a/lib/models/hive/wallet_address.g.dart +++ b/lib/models/hive/wallet_address.g.dart @@ -23,6 +23,7 @@ class WalletAddressAdapter extends TypeAdapter { status: fields[2] as String?, isOurs: fields[4] == null ? true : fields[4] as bool, wif: fields[5] == null ? '' : fields[5] as String, + isWatchOnly: fields[9] == null ? false : fields[9] as bool, ) .._isChangeAddr = fields[6] as bool? ..notificationBackendCount = fields[7] == null ? 0 : fields[7] as int @@ -32,7 +33,7 @@ class WalletAddressAdapter extends TypeAdapter { @override void write(BinaryWriter writer, WalletAddress obj) { writer - ..writeByte(9) + ..writeByte(10) ..writeByte(0) ..write(obj.address) ..writeByte(1) @@ -50,7 +51,9 @@ class WalletAddressAdapter extends TypeAdapter { ..writeByte(7) ..write(obj.notificationBackendCount) ..writeByte(8) - ..write(obj.isWatched); + ..write(obj.isWatched) + ..writeByte(9) + ..write(obj.isWatchOnly); } @override diff --git a/lib/providers/app_settings_provider.dart b/lib/providers/app_settings_provider.dart index bc661abf..17bea10c 100644 --- a/lib/providers/app_settings_provider.dart +++ b/lib/providers/app_settings_provider.dart @@ -147,4 +147,15 @@ class AppSettingsProvider with ChangeNotifier { _appOptions.walletOrder = newOrder; notifyListeners(); } + + List get activatedExperimentalFeatures { + return _appOptions.activatedExperimentalFeatures; + } + + void setActivatedExperimentalFeatures( + List newFeatures, + ) { + _appOptions.activatedExperimentalFeatures = newFeatures; + notifyListeners(); + } } diff --git a/lib/providers/wallet_provider.dart b/lib/providers/wallet_provider.dart index d00d0e82..e70fa0b6 100644 --- a/lib/providers/wallet_provider.dart +++ b/lib/providers/wallet_provider.dart @@ -127,16 +127,20 @@ class WalletProvider with ChangeNotifier { required String title, required String letterCode, required bool isImportedSeed, + required bool watchOnly, }) async { final box = await _encryptedBox.getWalletBox(); final nOfWalletOfLetterCode = availableWalletValues - .where((element) => element.letterCode == letterCode) + .where( + (element) => + element.letterCode == letterCode && element.watchOnly == false, + ) .length; LoggerWrapper.logInfo( 'WalletProvider', 'addWallet', - '$name $title $letterCode $nOfWalletOfLetterCode', + '$name - $title - $letterCode - $nOfWalletOfLetterCode', ); await box.put( @@ -147,6 +151,7 @@ class WalletProvider with ChangeNotifier { letterCode, nOfWalletOfLetterCode, isImportedSeed, + watchOnly, ), ); notifyListeners(); @@ -215,6 +220,7 @@ class WalletProvider with ChangeNotifier { status: null, isOurs: true, wif: wif, + isWatchOnly: false, ); await openWallet.save(); @@ -242,6 +248,10 @@ class WalletProvider with ChangeNotifier { final openWallet = getSpecificCoinWallet(identifier); final hdWallet = await getHdWallet(identifier); + if (openWallet.watchOnly == true) { + return; + } + if (openWallet.addresses.isEmpty && openWallet.walletIndex == 0) { //generate new address from master at wallet index 0 openWallet.addNewAddress = WalletAddress( @@ -251,6 +261,7 @@ class WalletProvider with ChangeNotifier { status: null, isOurs: true, wif: getWifFromHDPrivateKey(identifier, hdWallet), + isWatchOnly: false, ); setUnusedAddress( identifier: identifier, @@ -300,6 +311,7 @@ class WalletProvider with ChangeNotifier { status: null, isOurs: true, wif: getWifFromHDPrivateKey(identifier, newHdWallet), + isWatchOnly: false, ); setUnusedAddress( @@ -918,7 +930,7 @@ class WalletProvider with ChangeNotifier { final script = Script([ _opReturn, - ScriptPushData(utf8.encode(opReturn) as Uint8List), + ScriptPushData(utf8.encode(opReturn)), ]); txOutputs.add( @@ -1066,10 +1078,13 @@ class WalletProvider with ChangeNotifier { (element) => element.address == addr.address, ); - if (addr.isWatched || + bool isWatchedCheck = addr.isWatched || + addr.isWatchOnly || utxoRes != null && utxoRes.value > 0 || addr.address == getUnusedAddress(identifier) || - addr.status == 'hasUtxo') { + addr.status == 'hasUtxo'; + + if (isWatchedCheck == true) { answerMap[addr.address] = getScriptHash(identifier, addr.address); } } @@ -1149,7 +1164,11 @@ class WalletProvider with ChangeNotifier { notifyListeners(); } - void updateLabel(String identifier, String address, String label) { + void updateOrCreateAddressLabel({ + required String identifier, + required String address, + required String label, + }) { final openWallet = getSpecificCoinWallet(identifier); final addr = openWallet.addresses.firstWhereOrNull( (element) => element.address == address, @@ -1166,6 +1185,7 @@ class WalletProvider with ChangeNotifier { status: null, isOurs: false, wif: '', + isWatchOnly: false, ); } @@ -1173,6 +1193,27 @@ class WalletProvider with ChangeNotifier { notifyListeners(); } + void createWatchOnlyAddres({ + required String identifier, + required String address, + required String label, + }) { + final openWallet = getSpecificCoinWallet(identifier); + + openWallet.addNewAddress = WalletAddress( + address: address, + addressBookName: label, + used: false, + status: null, + isOurs: true, + wif: '', + isWatchOnly: true, + ); + + openWallet.save(); + notifyListeners(); + } + Future addAddressFromScan({ required String identifier, required String address, @@ -1193,6 +1234,7 @@ class WalletProvider with ChangeNotifier { identifier: identifier, address: address, ), + isWatchOnly: false, ); } else { await updateAddressStatus(identifier, address, status); @@ -1223,6 +1265,31 @@ class WalletProvider with ChangeNotifier { notifyListeners(); } + Future removeWatchOnlyAddress( + String identifier, + WalletAddress addr, + ) async { + final openWallet = getSpecificCoinWallet(identifier); + openWallet.removeAddress(addr); + + //clear utxos + openWallet.clearUtxo(addr.address); + + //remove tx that are related to this address + final tx = List.from( + openWallet.transactions + .where((element) => element.address == addr.address), + ); + for (final element in tx) { + openWallet.removeTransaction(element); + } + + //update balance + await updateWalletBalance(identifier); + + notifyListeners(); + } + String getLabelForAddress(String identifier, String address) { final openWallet = getSpecificCoinWallet(identifier); final addr = openWallet.addresses.firstWhereOrNull( diff --git a/lib/screens/about.dart b/lib/screens/about.dart index 8940851d..ad49a2ea 100644 --- a/lib/screens/about.dart +++ b/lib/screens/about.dart @@ -14,7 +14,7 @@ import '../tools/app_localizations.dart'; import '../tools/app_routes.dart'; class AboutScreen extends StatefulWidget { - const AboutScreen({Key? key}) : super(key: key); + const AboutScreen({super.key}); @override State createState() => _AboutScreenState(); diff --git a/lib/screens/auth_jail.dart b/lib/screens/auth_jail.dart index bf8a4318..581e724c 100644 --- a/lib/screens/auth_jail.dart +++ b/lib/screens/auth_jail.dart @@ -15,9 +15,9 @@ class AuthJailScreen extends StatefulWidget { final bool jailedFromHome; const AuthJailScreen({ - Key? key, + super.key, this.jailedFromHome = false, - }) : super(key: key); + }); } class _AuthJailState extends State { @@ -98,8 +98,8 @@ class _AuthJailState extends State { @override Widget build(BuildContext context) { - return WillPopScope( - onWillPop: () async => false, + return PopScope( + canPop: false, child: Scaffold( body: Container( color: Theme.of(context).primaryColor, diff --git a/lib/screens/changelog.dart b/lib/screens/changelog.dart index 6a74aba5..84772f43 100644 --- a/lib/screens/changelog.dart +++ b/lib/screens/changelog.dart @@ -4,7 +4,7 @@ import 'package:flutter_markdown/flutter_markdown.dart'; import '../tools/app_localizations.dart'; class ChangeLogScreen extends StatelessWidget { - const ChangeLogScreen({Key? key}) : super(key: key); + const ChangeLogScreen({super.key}); @override Widget build(BuildContext context) { diff --git a/lib/screens/qrcode_scanner.dart b/lib/screens/qrcode_scanner.dart index f0fc7af2..01898b3d 100644 --- a/lib/screens/qrcode_scanner.dart +++ b/lib/screens/qrcode_scanner.dart @@ -5,8 +5,8 @@ import 'package:qr_code_scanner/qr_code_scanner.dart'; class QRScanner extends StatefulWidget { const QRScanner({ - Key? key, - }) : super(key: key); + super.key, + }); @override State createState() => _QRScannerState(); diff --git a/lib/screens/router_master.dart b/lib/screens/router_master.dart index 960795a1..73421290 100644 --- a/lib/screens/router_master.dart +++ b/lib/screens/router_master.dart @@ -11,8 +11,11 @@ class RouterMaster extends StatefulWidget { final Widget widget; final RouteTypes routeType; - const RouterMaster({Key? key, required this.widget, required this.routeType}) - : super(key: key); + const RouterMaster({ + super.key, + required this.widget, + required this.routeType, + }); @override State createState() => _RouterMasterState(); diff --git a/lib/screens/secure_storage_error_screen.dart b/lib/screens/secure_storage_error_screen.dart index 30e82bce..4669ca14 100644 --- a/lib/screens/secure_storage_error_screen.dart +++ b/lib/screens/secure_storage_error_screen.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import '../tools/app_localizations.dart'; class SecureStorageFailedScreen extends StatelessWidget { - const SecureStorageFailedScreen({Key? key}) : super(key: key); + const SecureStorageFailedScreen({super.key}); @override Widget build(BuildContext context) { diff --git a/lib/screens/settings/app_settings_experimental_features.dart b/lib/screens/settings/app_settings_experimental_features.dart new file mode 100644 index 00000000..fe7fd8c4 --- /dev/null +++ b/lib/screens/settings/app_settings_experimental_features.dart @@ -0,0 +1,95 @@ +import 'package:flutter/material.dart'; +import 'package:peercoin/models/experimental_features.dart'; +import 'package:provider/provider.dart'; + +import '../../providers/app_settings_provider.dart'; +import '../../tools/app_localizations.dart'; +import '../../widgets/service_container.dart'; +import 'settings_helpers.dart'; + +class AppSettingsExperimentalFeaturesScreen extends StatefulWidget { + const AppSettingsExperimentalFeaturesScreen({super.key}); + + @override + State createState() => + _AppSettingsExperimentalFeaturesScreenState(); +} + +class _AppSettingsExperimentalFeaturesScreenState + extends State { + bool _initial = true; + + late AppSettingsProvider _settings; + + @override + void didChangeDependencies() async { + if (_initial == true) { + _settings = Provider.of(context); + + setState(() { + _initial = false; + }); + } + + super.didChangeDependencies(); + } + + void _saveFeature(String featureName, bool newState) async { + final features = _settings.activatedExperimentalFeatures; + if (newState == true) { + features.add(featureName); + } else { + features.remove(featureName); + } + _settings.setActivatedExperimentalFeatures(features); + + saveSnack(context); + } + + @override + Widget build(BuildContext context) { + if (_initial == true) { + return const Scaffold( + body: Center( + child: CircularProgressIndicator(), + ), + ); + } + + return Scaffold( + appBar: AppBar( + centerTitle: true, + title: Text( + AppLocalizations.instance.translate( + 'app_settings_experimental_features', + ), + ), + ), + body: SingleChildScrollView( + child: Align( + child: PeerContainer( + noSpacers: true, + child: Column( + children: ExperimentalFeatures.values + .map( + (e) => SwitchListTile( + title: Text( + AppLocalizations.instance.translate( + 'app_settings_experimental_feature_${e.name}', + ), + ), + value: _settings.activatedExperimentalFeatures + .contains(e.name), + onChanged: (newState) { + _saveFeature(e.name, newState); + }, + ), + ) + .toList(), + ), + ), + ), + ), + ); + } +} diff --git a/lib/screens/settings/app_settings_notifications.dart b/lib/screens/settings/app_settings_notifications.dart index 2d6fc540..3e13304b 100644 --- a/lib/screens/settings/app_settings_notifications.dart +++ b/lib/screens/settings/app_settings_notifications.dart @@ -10,7 +10,7 @@ import '../../tools/background_sync.dart'; import '../../widgets/buttons.dart'; class AppSettingsNotificationsScreen extends StatefulWidget { - const AppSettingsNotificationsScreen({Key? key}) : super(key: key); + const AppSettingsNotificationsScreen({super.key}); @override State createState() => diff --git a/lib/screens/settings/app_settings_screen.dart b/lib/screens/settings/app_settings_screen.dart index a8611abc..2a6ecb3c 100644 --- a/lib/screens/settings/app_settings_screen.dart +++ b/lib/screens/settings/app_settings_screen.dart @@ -19,7 +19,7 @@ import '../../widgets/service_container.dart'; import '../about.dart'; class AppSettingsScreen extends StatefulWidget { - const AppSettingsScreen({Key? key}) : super(key: key); + const AppSettingsScreen({super.key}); @override State createState() => _AppSettingsScreenState(); @@ -114,7 +114,7 @@ class _AppSettingsScreenState extends State { ), ); }, - ).toList(), + ), ExpansionTile( title: Text( AppLocalizations.instance.translate('app_settings_seed'), diff --git a/lib/screens/settings/app_settings_wallet_order.dart b/lib/screens/settings/app_settings_wallet_order.dart index 5957ed74..cc1c2cdf 100644 --- a/lib/screens/settings/app_settings_wallet_order.dart +++ b/lib/screens/settings/app_settings_wallet_order.dart @@ -8,7 +8,7 @@ import '../../../widgets/service_container.dart'; import '../../../tools/app_localizations.dart'; class AppSettingsWalletOrderScreen extends StatefulWidget { - const AppSettingsWalletOrderScreen({Key? key}) : super(key: key); + const AppSettingsWalletOrderScreen({super.key}); @override State createState() => diff --git a/lib/screens/settings/server_settings/server_add.dart b/lib/screens/settings/server_settings/server_add.dart index fd3fa515..43c2b61e 100644 --- a/lib/screens/settings/server_settings/server_add.dart +++ b/lib/screens/settings/server_settings/server_add.dart @@ -20,7 +20,7 @@ import '../../../widgets/loading_indicator.dart'; import '../../../widgets/service_container.dart'; class ServerAddScreen extends StatefulWidget { - const ServerAddScreen({Key? key}) : super(key: key); + const ServerAddScreen({super.key}); @override State createState() => _ServerAddScreenState(); diff --git a/lib/screens/settings/server_settings/server_settings.dart b/lib/screens/settings/server_settings/server_settings.dart index be216ffb..8af4862f 100644 --- a/lib/screens/settings/server_settings/server_settings.dart +++ b/lib/screens/settings/server_settings/server_settings.dart @@ -10,7 +10,7 @@ import '../../../tools/app_localizations.dart'; import '../../../tools/app_routes.dart'; class ServerSettingsScreen extends StatefulWidget { - const ServerSettingsScreen({Key? key}) : super(key: key); + const ServerSettingsScreen({super.key}); @override State createState() => _ServerSettingsScreenState(); diff --git a/lib/screens/settings/settings_helpers.dart b/lib/screens/settings/settings_helpers.dart index cd1b0224..745cba64 100644 --- a/lib/screens/settings/settings_helpers.dart +++ b/lib/screens/settings/settings_helpers.dart @@ -4,18 +4,28 @@ import 'package:flutter/material.dart'; import '../../tools/app_localizations.dart'; import '../../tools/app_routes.dart'; -const Map availableSettings = { +final Map _availableSettings = { 'app_settings_language': Routes.appSettingsLanguage, - if (!kIsWeb) 'app_settings_default_wallet': Routes.appSettingsDefaultWallet, - if (!kIsWeb) 'app_settings_notifications': Routes.appSettingsNotifications, + 'app_settings_default_wallet': Routes.appSettingsDefaultWallet, + 'app_settings_notifications': Routes.appSettingsNotifications, 'app_settings_wallet_order': Routes.appSettingsWalletOrder, 'wallet_scan': Routes.appSettingsWalletScanner, 'app_settings_auth_header': Routes.appSettingsAuthentication, 'app_settings_price_feed': Routes.appSettingsPriceFeed, 'app_settings_theme': Routes.appSettingsAppTheme, 'server_settings_title': Routes.serverSettingsHome, + 'app_settings_experimental_features': Routes.appSettingsExperimentalFeatures, }; +get availableSettings { + if (kIsWeb == true) { + //these settings are not available on web + _availableSettings.remove('app_settings_default_wallet'); + _availableSettings.remove('app_settings_notifications'); + } + return _availableSettings; +} + void saveSnack(context) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( diff --git a/lib/screens/settings/wallet_scan/app_settings_wallet_scanner.dart b/lib/screens/settings/wallet_scan/app_settings_wallet_scanner.dart index 1b735f3a..34526f87 100644 --- a/lib/screens/settings/wallet_scan/app_settings_wallet_scanner.dart +++ b/lib/screens/settings/wallet_scan/app_settings_wallet_scanner.dart @@ -13,7 +13,7 @@ import '../../../providers/wallet_provider.dart'; import '../../../tools/app_localizations.dart'; class AppSettingsWalletScanner extends StatefulWidget { - const AppSettingsWalletScanner({Key? key}) : super(key: key); + const AppSettingsWalletScanner({super.key}); @override State createState() => @@ -205,6 +205,7 @@ class _AppSettingsWalletScannerState extends State { title: title, letterCode: coin.letterCode, isImportedSeed: true, + watchOnly: false, ); _addToLog( AppLocalizations.instance.translate( diff --git a/lib/screens/setup/setup_auth.dart b/lib/screens/setup/setup_auth.dart index 573371ba..31448b72 100644 --- a/lib/screens/setup/setup_auth.dart +++ b/lib/screens/setup/setup_auth.dart @@ -13,7 +13,7 @@ import '../../widgets/setup/session_slider.dart'; import 'setup_landing.dart'; class SetupAuthScreen extends StatefulWidget { - const SetupAuthScreen({Key? key}) : super(key: key); + const SetupAuthScreen({super.key}); @override State createState() => _SetupAuthScreenState(); diff --git a/lib/screens/setup/setup_create_wallet.dart b/lib/screens/setup/setup_create_wallet.dart index 5e487f75..43f64c91 100644 --- a/lib/screens/setup/setup_create_wallet.dart +++ b/lib/screens/setup/setup_create_wallet.dart @@ -18,7 +18,7 @@ import '../../widgets/logout_dialog_dummy.dart' if (dart.library.html) '../../widgets/logout_dialog.dart'; class SetupCreateWalletScreen extends StatefulWidget { - const SetupCreateWalletScreen({Key? key}) : super(key: key); + const SetupCreateWalletScreen({super.key}); @override State createState() => diff --git a/lib/screens/setup/setup_data_feeds.dart b/lib/screens/setup/setup_data_feeds.dart index fda35d18..c64b4f13 100644 --- a/lib/screens/setup/setup_data_feeds.dart +++ b/lib/screens/setup/setup_data_feeds.dart @@ -12,7 +12,7 @@ import '../../widgets/buttons.dart'; import 'setup_landing.dart'; class SetupDataFeedsScreen extends StatefulWidget { - const SetupDataFeedsScreen({Key? key}) : super(key: key); + const SetupDataFeedsScreen({super.key}); @override State createState() => _SetupDataFeedsScreenState(); diff --git a/lib/screens/setup/setup_import_seed.dart b/lib/screens/setup/setup_import_seed.dart index bf3d3e8f..46a146cc 100644 --- a/lib/screens/setup/setup_import_seed.dart +++ b/lib/screens/setup/setup_import_seed.dart @@ -13,7 +13,7 @@ import '../../widgets/buttons.dart'; import 'setup_landing.dart'; class SetupImportSeedScreen extends StatefulWidget { - const SetupImportSeedScreen({Key? key}) : super(key: key); + const SetupImportSeedScreen({super.key}); @override State createState() => _SetupImportSeedState(); diff --git a/lib/screens/setup/setup_landing.dart b/lib/screens/setup/setup_landing.dart index f656435a..bd542618 100644 --- a/lib/screens/setup/setup_landing.dart +++ b/lib/screens/setup/setup_landing.dart @@ -8,7 +8,7 @@ import '../../widgets/buttons.dart'; import '../../widgets/setup_progress.dart'; class SetupLandingScreen extends StatefulWidget { - const SetupLandingScreen({Key? key}) : super(key: key); + const SetupLandingScreen({super.key}); @override State createState() => _SetupLandingScreenState(); @@ -153,10 +153,10 @@ class PeerExplanationText extends StatelessWidget { final int maxLines; const PeerExplanationText({ - Key? key, + super.key, required this.text, this.maxLines = 1, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -176,9 +176,9 @@ class PeerExplanationText extends StatelessWidget { class PeerProgress extends StatelessWidget { final int step; const PeerProgress({ - Key? key, + super.key, required this.step, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/screens/setup/setup_language.dart b/lib/screens/setup/setup_language.dart index 44e4ae35..82c30887 100644 --- a/lib/screens/setup/setup_language.dart +++ b/lib/screens/setup/setup_language.dart @@ -5,7 +5,7 @@ import 'package:shared_preferences/shared_preferences.dart'; import '../../tools/app_localizations.dart'; class SetupLanguageScreen extends StatefulWidget { - const SetupLanguageScreen({Key? key}) : super(key: key); + const SetupLanguageScreen({super.key}); @override State createState() => _SetupLanguageScreenState(); diff --git a/lib/screens/setup/setup_legal.dart b/lib/screens/setup/setup_legal.dart index 79a9ac79..fe734eab 100644 --- a/lib/screens/setup/setup_legal.dart +++ b/lib/screens/setup/setup_legal.dart @@ -9,7 +9,7 @@ import '../../widgets/buttons.dart'; import 'setup_landing.dart'; class SetupLegalScreen extends StatefulWidget { - const SetupLegalScreen({Key? key}) : super(key: key); + const SetupLegalScreen({super.key}); @override State createState() => _SetupLegalScreenState(); diff --git a/lib/screens/wallet/address_selector.dart b/lib/screens/wallet/address_selector.dart index 9214a70c..4e81ebcd 100644 --- a/lib/screens/wallet/address_selector.dart +++ b/lib/screens/wallet/address_selector.dart @@ -5,7 +5,7 @@ import '../../models/hive/wallet_address.dart'; import '../../tools/app_localizations.dart'; class AddressSelectorScreen extends StatefulWidget { - const AddressSelectorScreen({Key? key}) : super(key: key); + const AddressSelectorScreen({super.key}); @override State createState() => _AddressSelectorScreenState(); @@ -111,8 +111,8 @@ class _AddressSelectorScreenState extends State { @override Widget build(BuildContext context) { - return WillPopScope( - onWillPop: _onWillPop, + return PopScope( + onPopInvoked: (_) => _onWillPop, child: Scaffold( appBar: AppBar( centerTitle: true, diff --git a/lib/screens/wallet/import_paper_wallet.dart b/lib/screens/wallet/import_paper_wallet.dart index 4e3af38d..a2edfa32 100644 --- a/lib/screens/wallet/import_paper_wallet.dart +++ b/lib/screens/wallet/import_paper_wallet.dart @@ -15,7 +15,7 @@ import '../../widgets/buttons.dart'; import '../../widgets/loading_indicator.dart'; class ImportPaperWalletScreen extends StatefulWidget { - const ImportPaperWalletScreen({Key? key}) : super(key: key); + const ImportPaperWalletScreen({super.key}); @override State createState() => diff --git a/lib/screens/wallet/import_wif.dart b/lib/screens/wallet/import_wif.dart index aa12c3bd..e2eecee6 100644 --- a/lib/screens/wallet/import_wif.dart +++ b/lib/screens/wallet/import_wif.dart @@ -16,7 +16,7 @@ import '../../widgets/buttons.dart'; import '../../widgets/service_container.dart'; class ImportWifScreen extends StatefulWidget { - const ImportWifScreen({Key? key}) : super(key: key); + const ImportWifScreen({super.key}); @override State createState() => _ImportWifScreenState(); diff --git a/lib/screens/wallet/transaction_confirmation.dart b/lib/screens/wallet/transaction_confirmation.dart index adcf627e..2d0f8df3 100644 --- a/lib/screens/wallet/transaction_confirmation.dart +++ b/lib/screens/wallet/transaction_confirmation.dart @@ -52,8 +52,8 @@ class TransactionConfirmationArguments { class TransactionConfirmationScreen extends StatefulWidget { const TransactionConfirmationScreen({ - Key? key, - }) : super(key: key); + super.key, + }); @override State createState() => diff --git a/lib/screens/wallet/transaction_details.dart b/lib/screens/wallet/transaction_details.dart index 176b946b..9629f588 100644 --- a/lib/screens/wallet/transaction_details.dart +++ b/lib/screens/wallet/transaction_details.dart @@ -13,7 +13,7 @@ import '../../widgets/buttons.dart'; import '../../widgets/service_container.dart'; class TransactionDetails extends StatelessWidget { - const TransactionDetails({Key? key}) : super(key: key); + const TransactionDetails({super.key}); @override Widget build(BuildContext context) { diff --git a/lib/screens/wallet/wallet_home.dart b/lib/screens/wallet/wallet_home.dart index cb4403c7..4015e284 100644 --- a/lib/screens/wallet/wallet_home.dart +++ b/lib/screens/wallet/wallet_home.dart @@ -1,10 +1,12 @@ import 'dart:io'; +import 'package:app_bar_with_search_switch/app_bar_with_search_switch.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:loader_overlay/loader_overlay.dart'; import 'package:peercoin/data_sources/electrum_backend.dart'; import 'package:peercoin/providers/server_provider.dart'; +import 'package:peercoin/widgets/wallet/address_book/addresses_tab_watch_only.dart'; import 'package:peercoin/widgets/wallet/wallet_reset_bottom_sheet.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -28,7 +30,7 @@ import '../../widgets/wallet/transactions_list.dart'; import '../../widgets/wallet/wallet_rescan_bottom_sheet.dart'; class WalletHomeScreen extends StatefulWidget { - const WalletHomeScreen({Key? key}) : super(key: key); + const WalletHomeScreen({super.key}); @override State createState() => _WalletHomeState(); @@ -38,7 +40,7 @@ class _WalletHomeState extends State with WidgetsBindingObserver { bool _initial = true; String _unusedAddress = ''; - int _pageIndex = 1; + WalletTab _selectedTab = WalletTab.transactions; int _latestBlock = 0; late CoinWallet _wallet; late BackendConnectionState _connectionState = BackendConnectionState.waiting; @@ -50,11 +52,12 @@ class _WalletHomeState extends State late ServerProvider _servers; String? _address; String? _label; + String _searchString = ''; - void changeIndex(int i, [String? addr, String? lab]) { + void changeTab(WalletTab t, [String? addr, String? lab]) { setState(() { - _pageIndex = i; - if (i == Tabs.send) { + _selectedTab = t; + if (_selectedTab == WalletTab.send) { //Passes address from addresses_tab to send_tab (send to) _address = addr; _label = lab; @@ -208,7 +211,7 @@ class _WalletHomeState extends State } if (arguments.containsKey('pushedAddress')) { - changeIndex(Tabs.send, arguments['pushedAddress']); + changeTab(WalletTab.send, arguments['pushedAddress']); } //check if wallet is due for rescan @@ -247,7 +250,8 @@ class _WalletHomeState extends State } //try to rebroadcast pending tx rebroadCastUnsendTx(); - } else if (_listenedAddresses.contains(_unusedAddress) == false) { + } else if (_listenedAddresses.contains(_unusedAddress) == false && + _wallet.watchOnly == false) { //subscribe to newly created addresses LoggerWrapper.logInfo( 'WalletHome', @@ -500,6 +504,45 @@ class _WalletHomeState extends State } List _calcPopupMenuItems(BuildContext context) { + if (_wallet.watchOnly == true) { + return [ + PopupMenuButton( + onSelected: (dynamic value) => selectPopUpMenuItem(value), + itemBuilder: (_) { + return [ + PopupMenuItem( + value: 'change_title', + child: ListTile( + leading: Icon( + Icons.edit, + color: Theme.of(context).colorScheme.secondary, + ), + title: Text( + AppLocalizations.instance.translate( + 'wallet_pop_menu_change_title', + ), + ), + ), + ), + PopupMenuItem( + value: 'reset_wallet', + child: ListTile( + leading: Icon( + Icons.restore, + color: Theme.of(context).colorScheme.secondary, + ), + title: Text( + AppLocalizations.instance.translate( + 'sign_reset_button', + ), + ), + ), + ), + ]; + }, + ), + ]; + } return [ PopupMenuButton( onSelected: (dynamic value) => selectPopUpMenuItem(value), @@ -597,34 +640,46 @@ class _WalletHomeState extends State } BottomNavigationBar _calcBottomNavBar(BuildContext context) { - final back = Theme.of(context).primaryColor; + final bgColor = Theme.of(context).primaryColor; return BottomNavigationBar( unselectedItemColor: Theme.of(context).disabledColor, selectedItemColor: Colors.white, - onTap: (index) => changeIndex(index), - currentIndex: _pageIndex, + onTap: (index) { + if (_wallet.watchOnly == true) { + if (index == 0 || index == 3) { + return; + } + } + changeTab(WalletTab.values[index]); + }, + currentIndex: _selectedTab.index, items: [ BottomNavigationBarItem( - icon: const Icon(Icons.download_rounded), + icon: _wallet.watchOnly + ? const SizedBox() + : const Icon(Icons.download_rounded), label: AppLocalizations.instance.translate('wallet_bottom_nav_receive'), - backgroundColor: back, + backgroundColor: bgColor, ), BottomNavigationBarItem( icon: const Icon(Icons.list_rounded), + tooltip: 'Transactions', label: AppLocalizations.instance.translate('wallet_bottom_nav_tx'), - backgroundColor: back, + backgroundColor: bgColor, ), BottomNavigationBarItem( tooltip: 'Address Book', icon: const Icon(Icons.menu_book_rounded), label: AppLocalizations.instance.translate('wallet_bottom_nav_addr'), - backgroundColor: back, + backgroundColor: bgColor, ), BottomNavigationBarItem( - icon: const Icon(Icons.upload_rounded), + icon: _wallet.watchOnly + ? const SizedBox() + : const Icon(Icons.upload_rounded), label: AppLocalizations.instance.translate('wallet_bottom_nav_send'), - backgroundColor: back, + backgroundColor: bgColor, ), ], ); @@ -632,8 +687,8 @@ class _WalletHomeState extends State Widget _calcBody() { Widget body; - switch (_pageIndex) { - case Tabs.receive: + switch (_selectedTab) { + case WalletTab.receive: body = Expanded( child: ReceiveTab( connectionState: _connectionState, @@ -642,7 +697,7 @@ class _WalletHomeState extends State ), ); break; - case Tabs.transactions: + case WalletTab.transactions: body = Expanded( child: TransactionList( walletTransactions: _walletTransactions, @@ -651,24 +706,30 @@ class _WalletHomeState extends State ), ); break; - case Tabs.addresses: + case WalletTab.addresses: body = Expanded( - child: AddressTab( - walletName: _wallet.name, - title: _wallet.title, - walletAddresses: _wallet.addresses, - changeIndex: changeIndex, - ), + child: _wallet.watchOnly + ? AddressesTabWatchOnly( + walletName: _wallet.name, + walletAddresses: _wallet.addresses, + changeTab: changeTab, + searchString: _searchString, + ) + : AddressTab( + walletName: _wallet.name, + walletAddresses: _wallet.addresses, + changeTab: changeTab, + ), ); break; - case Tabs.send: + case WalletTab.send: body = Expanded( child: SendTab( address: _address, label: _label, wallet: _wallet, connectionState: _connectionState, - changeIndex: changeIndex, + changeTab: changeTab, ), ); break; @@ -679,16 +740,45 @@ class _WalletHomeState extends State return body; } + AppBarWithSearchSwitch addressTabWatchOnlySearchAppBar() { + return AppBarWithSearchSwitch( + closeOnSubmit: true, + clearOnClose: true, + fieldHintText: AppLocalizations.instance.translate('search_address'), + onChanged: (text) { + setState(() { + _searchString = text; + }); + }, + onCleared: () => setState(() { + _searchString = ''; + }), + appBarBuilder: (context) { + return AppBar( + centerTitle: true, + title: Text( + AppLocalizations.instance.translate('search_address'), + ), + actions: const [ + AppBarSearchButton(), + ], + ); + }, + ); + } + @override Widget build(BuildContext context) { return Scaffold( bottomNavigationBar: _calcBottomNavBar(context), - appBar: AppBar( - centerTitle: true, - elevation: 1, - title: Text(_wallet.title), - actions: _calcPopupMenuItems(context), - ), + appBar: _selectedTab == WalletTab.addresses + ? addressTabWatchOnlySearchAppBar() + : AppBar( + centerTitle: true, + elevation: 1, + title: Text(_wallet.title), + actions: _calcPopupMenuItems(context), + ), body: _initial ? const Center( child: LoadingIndicator(), @@ -704,14 +794,12 @@ class _WalletHomeState extends State ); } - // TODO check cursive roboto on iOS // TODO wallet list: make larger list prettier } -class Tabs { - Tabs._(); - static const int receive = 0; - static const int transactions = 1; - static const int addresses = 2; - static const int send = 3; +enum WalletTab { + receive, + transactions, + addresses, + send, } diff --git a/lib/screens/wallet/wallet_list.dart b/lib/screens/wallet/wallet_list.dart index d05a4582..e325a6d7 100644 --- a/lib/screens/wallet/wallet_list.dart +++ b/lib/screens/wallet/wallet_list.dart @@ -38,10 +38,10 @@ class WalletListScreen extends StatefulWidget { State createState() => _WalletListScreenState(); const WalletListScreen({ - Key? key, + super.key, this.fromColdStart = false, this.walletToOpenDirectly = '', - }) : super(key: key); + }); } class _WalletListScreenState extends State @@ -452,14 +452,29 @@ class _WalletListScreenState extends State ); }, child: ListTile( - leading: CircleAvatar( - backgroundColor: Colors.white, - child: Image.asset( - AvailableCoins.getSpecificCoin( - wallet.name, - ).iconPath, - width: 20, - ), + leading: Stack( + children: [ + CircleAvatar( + backgroundColor: Colors.white, + child: Image.asset( + AvailableCoins + .getSpecificCoin( + wallet.name, + ).iconPath, + width: 20, + ), + ), + if (wallet.watchOnly) + const Positioned( + right: 0, + bottom: 0, + child: Icon( + Icons.visibility, + size: 16, + color: Colors.black, + ), + ), + ], ), title: Text( wallet.title, diff --git a/lib/screens/wallet/wallet_sign_message.dart b/lib/screens/wallet/wallet_sign_message.dart index 65b71b20..d55f5816 100644 --- a/lib/screens/wallet/wallet_sign_message.dart +++ b/lib/screens/wallet/wallet_sign_message.dart @@ -15,7 +15,7 @@ import '../../widgets/double_tab_to_clipboard.dart'; import '../../widgets/service_container.dart'; class WalletMessageSigningScreen extends StatefulWidget { - const WalletMessageSigningScreen({Key? key}) : super(key: key); + const WalletMessageSigningScreen({super.key}); @override State createState() => diff --git a/lib/screens/wallet/wallet_verify_message.dart b/lib/screens/wallet/wallet_verify_message.dart index bab1f3f2..03f0f37a 100644 --- a/lib/screens/wallet/wallet_verify_message.dart +++ b/lib/screens/wallet/wallet_verify_message.dart @@ -12,7 +12,7 @@ import '../../widgets/buttons.dart'; import '../../widgets/service_container.dart'; class WaleltMessagesVerificationScreen extends StatefulWidget { - const WaleltMessagesVerificationScreen({Key? key}) : super(key: key); + const WaleltMessagesVerificationScreen({super.key}); @override State createState() => diff --git a/lib/tools/app_routes.dart b/lib/tools/app_routes.dart index 87210431..c6560e2b 100644 --- a/lib/tools/app_routes.dart +++ b/lib/tools/app_routes.dart @@ -5,6 +5,7 @@ import 'package:peercoin/screens/wallet/wallet_verify_message.dart'; import '../screens/settings/app_settings_app_theme.dart'; import '../screens/settings/app_settings_authentication.dart'; import '../screens/settings/app_settings_default_wallet.dart'; +import '../screens/settings/app_settings_experimental_features.dart'; import '../screens/settings/app_settings_language.dart'; import '../screens/settings/app_settings_notifications.dart'; import '../screens/settings/app_settings_price_feed.dart'; @@ -45,6 +46,8 @@ class Routes { '/app-settings-authentication'; static const String appSettingsWalletOrder = '/app-settings-wallet-order'; static const String appSettingsWalletScanner = '/settings-wallet-scanner'; + static const String appSettingsExperimentalFeatures = + '/app-settings-experimental-features'; static const String qrScan = '/qr-scan'; static const String setupAuth = '/setup-auth'; static const String setupCreateWallet = '/setup-create-wallet'; @@ -189,6 +192,10 @@ class Routes { widget: AppSettingsWalletOrderScreen(), routeType: RouteTypes.requiresSetupFinished, ), + Routes.appSettingsExperimentalFeatures: (context) => const RouterMaster( + widget: AppSettingsExperimentalFeaturesScreen(), + routeType: RouteTypes.requiresSetupFinished, + ), }; } } diff --git a/lib/tools/app_themes.dart b/lib/tools/app_themes.dart index b4244513..139385fb 100644 --- a/lib/tools/app_themes.dart +++ b/lib/tools/app_themes.dart @@ -25,6 +25,10 @@ class MyTheme { borderRadius: BorderRadius.circular(20), ), ), + appBarTheme: AppBarTheme( + color: LightColors.green, + foregroundColor: LightColors.white, + ), cardTheme: CardTheme( elevation: 2, shadowColor: LightColors.darkGreen, @@ -35,6 +39,10 @@ class MyTheme { borderRadius: BorderRadius.circular(20), ), ), + snackBarTheme: SnackBarThemeData( + backgroundColor: LightColors.green, + contentTextStyle: TextStyle(color: LightColors.white), + ), elevatedButtonTheme: ElevatedButtonThemeData( style: ElevatedButton.styleFrom( foregroundColor: LightColors.green, @@ -59,7 +67,7 @@ class MyTheme { error: LightColors.red, onPrimary: LightColors.white, onSecondary: LightColors.blackGreen, - onSurface: LightColors.green, + onSurface: LightColors.black, onBackground: LightColors.green, onError: LightColors.red, brightness: Brightness.light, @@ -85,6 +93,9 @@ class MyTheme { ), sliderTheme: SliderThemeData(valueIndicatorColor: DarkColors.darkBlue), unselectedWidgetColor: DarkColors.white, + radioTheme: RadioThemeData( + fillColor: MaterialStateProperty.all(DarkColors.white), + ), textTheme: TextTheme( titleLarge: TextStyle(color: DarkColors.white), headlineSmall: TextStyle(color: DarkColors.white), @@ -147,7 +158,7 @@ class MyTheme { error: DarkColors.red, onPrimary: DarkColors.green, onSecondary: DarkColors.green, - onSurface: DarkColors.green, + onSurface: DarkColors.white, onBackground: DarkColors.green, onError: DarkColors.red, brightness: Brightness.dark, diff --git a/lib/widgets/buttons.dart b/lib/widgets/buttons.dart index fce3a6c9..9156d8ae 100644 --- a/lib/widgets/buttons.dart +++ b/lib/widgets/buttons.dart @@ -9,12 +9,12 @@ class PeerButton extends StatelessWidget { final bool small; final bool active; const PeerButton({ - Key? key, + super.key, required this.text, required this.action, this.small = false, this.active = true, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -49,8 +49,7 @@ class PeerButton extends StatelessWidget { class PeerButtonBorder extends StatelessWidget { final Function() action; final String text; - const PeerButtonBorder({Key? key, required this.text, required this.action}) - : super(key: key); + const PeerButtonBorder({super.key, required this.text, required this.action}); @override Widget build(BuildContext context) { @@ -92,11 +91,11 @@ class PeerButtonSetup extends StatelessWidget { final bool active; const PeerButtonSetup({ - Key? key, + super.key, required this.text, required this.action, this.active = true, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -138,11 +137,11 @@ class PeerButtonSetupLoading extends StatelessWidget { final bool loading; const PeerButtonSetupLoading({ - Key? key, + super.key, required this.text, required this.action, this.loading = false, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -193,7 +192,7 @@ class PeerButtonSetupLoading extends StatelessWidget { } class PeerButtonSetupBack extends StatelessWidget { - const PeerButtonSetupBack({Key? key}) : super(key: key); + const PeerButtonSetupBack({super.key}); @override Widget build(BuildContext context) { diff --git a/lib/widgets/double_tab_to_clipboard.dart b/lib/widgets/double_tab_to_clipboard.dart index 3993b557..205032f9 100644 --- a/lib/widgets/double_tab_to_clipboard.dart +++ b/lib/widgets/double_tab_to_clipboard.dart @@ -8,10 +8,10 @@ class DoubleTabToClipboard extends StatelessWidget { final String clipBoardData; const DoubleTabToClipboard({ - Key? key, + super.key, required this.clipBoardData, required this.child, - }) : super(key: key); + }); static void tapEvent(BuildContext context, String clipBoardData) { ScaffoldMessenger.of(context).showSnackBar( diff --git a/lib/widgets/expanded_section.dart b/lib/widgets/expanded_section.dart index d936763a..3f240f20 100644 --- a/lib/widgets/expanded_section.dart +++ b/lib/widgets/expanded_section.dart @@ -3,8 +3,7 @@ import 'package:flutter/material.dart'; class ExpandedSection extends StatefulWidget { final Widget child; final bool expand; - const ExpandedSection({Key? key, this.expand = false, required this.child}) - : super(key: key); + const ExpandedSection({super.key, this.expand = false, required this.child}); @override State createState() => _ExpandedSectionState(); diff --git a/lib/widgets/loading_indicator.dart b/lib/widgets/loading_indicator.dart index c8c28ebd..ebb0408f 100644 --- a/lib/widgets/loading_indicator.dart +++ b/lib/widgets/loading_indicator.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; class LoadingIndicator extends StatelessWidget { - const LoadingIndicator({Key? key}) : super(key: key); + const LoadingIndicator({super.key}); @override Widget build(BuildContext context) { diff --git a/lib/widgets/logout_dialog.dart b/lib/widgets/logout_dialog.dart index b9143474..4c626569 100644 --- a/lib/widgets/logout_dialog.dart +++ b/lib/widgets/logout_dialog.dart @@ -10,7 +10,7 @@ import 'package:shared_preferences/shared_preferences.dart'; import '../tools/app_localizations.dart'; class LogoutDialog extends StatelessWidget { - const LogoutDialog({Key? key}) : super(key: key); + const LogoutDialog({super.key}); @override Widget build(BuildContext context) { diff --git a/lib/widgets/logout_dialog_dummy.dart b/lib/widgets/logout_dialog_dummy.dart index 5354681f..29a23020 100644 --- a/lib/widgets/logout_dialog_dummy.dart +++ b/lib/widgets/logout_dialog_dummy.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; class LogoutDialog extends StatelessWidget { - const LogoutDialog({Key? key}) : super(key: key); + const LogoutDialog({super.key}); static Future clearData() { return Future.delayed(const Duration(seconds: 0)); diff --git a/lib/widgets/service_container.dart b/lib/widgets/service_container.dart index eb33178b..1a3845f9 100644 --- a/lib/widgets/service_container.dart +++ b/lib/widgets/service_container.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; class PeerServiceTitle extends StatelessWidget { final String title; - const PeerServiceTitle({Key? key, required this.title}) : super(key: key); + const PeerServiceTitle({super.key, required this.title}); @override Widget build(BuildContext context) { @@ -44,11 +44,11 @@ class PeerContainer extends StatelessWidget { final bool noSpacers; const PeerContainer({ - Key? key, + super.key, required this.child, this.isTransparent = false, this.noSpacers = false, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -74,13 +74,16 @@ class ModalBottomSheetContainer extends StatelessWidget { final Widget child; const ModalBottomSheetContainer({ - Key? key, + super.key, required this.child, - }) : super(key: key); + }); @override Widget build(BuildContext context) { return Container( + width: MediaQuery.of(context).size.width > 1200 + ? MediaQuery.of(context).size.width / 2 + : MediaQuery.of(context).size.width, decoration: BoxDecoration( color: Theme.of(context).colorScheme.background, borderRadius: const BorderRadius.vertical( diff --git a/lib/widgets/settings/settings_auth.dart b/lib/widgets/settings/settings_auth.dart index 0a5446bc..cdfda46d 100644 --- a/lib/widgets/settings/settings_auth.dart +++ b/lib/widgets/settings/settings_auth.dart @@ -21,8 +21,8 @@ class SettingsAuth extends StatelessWidget { this._settings, this._saveSnack, this._authenticationOptions, { - Key? key, - }) : super(key: key); + super.key, + }); void changePIN(BuildContext context, bool biometricsAllowed) async { await Auth.requireAuth( diff --git a/lib/widgets/settings/settings_price_ticker.dart b/lib/widgets/settings/settings_price_ticker.dart index b5aaa9db..36714ccc 100644 --- a/lib/widgets/settings/settings_price_ticker.dart +++ b/lib/widgets/settings/settings_price_ticker.dart @@ -15,8 +15,8 @@ class SettingsPriceTicker extends StatefulWidget { this._settings, this._saveSnack, this._searchString, { - Key? key, - }) : super(key: key); + super.key, + }); @override State createState() => _SettingsPriceTickerState(); diff --git a/lib/widgets/setup/session_slider.dart b/lib/widgets/setup/session_slider.dart index 40ebb866..e3c86cbd 100644 --- a/lib/widgets/setup/session_slider.dart +++ b/lib/widgets/setup/session_slider.dart @@ -5,7 +5,7 @@ import '../../tools/app_localizations.dart'; import '../../tools/logger_wrapper.dart'; class SetupSessionSlider extends StatefulWidget { - const SetupSessionSlider({Key? key}) : super(key: key); + const SetupSessionSlider({super.key}); @override State createState() => _SetupSessionSliderState(); diff --git a/lib/widgets/setup_progress.dart b/lib/widgets/setup_progress.dart index bdf08331..0a9390bd 100644 --- a/lib/widgets/setup_progress.dart +++ b/lib/widgets/setup_progress.dart @@ -9,9 +9,8 @@ class SetupProgressIndicator extends StatelessWidget const SetupProgressIndicator( this.step, { - Key? key, - }) : preferredSize = const Size.fromHeight(50.0), - super(key: key); + super.key, + }) : preferredSize = const Size.fromHeight(50.0); @override Widget build(BuildContext context) { return Container( diff --git a/lib/widgets/wallet/address_book/addresses_tab_expandable.dart b/lib/widgets/wallet/address_book/addresses_tab_expandable.dart new file mode 100644 index 00000000..96a1309a --- /dev/null +++ b/lib/widgets/wallet/address_book/addresses_tab_expandable.dart @@ -0,0 +1,265 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../../../models/hive/wallet_address.dart'; +import '../../../providers/wallet_provider.dart'; +import '../../../tools/app_localizations.dart'; +import '../../double_tab_to_clipboard.dart'; +import '../wallet_home_qr.dart'; +import 'addresses_tab_expandable_icon.dart'; + +class AddressTabExpandable extends StatelessWidget { + final WalletAddress walletAddress; + final String walletName; + final AddressTabSlideableType type; + final Function applyFilterCallback; + final double balance; + final String balanceUnit; + + const AddressTabExpandable({ + super.key, + required this.walletAddress, + required this.walletName, + required this.type, + required this.balance, + required this.applyFilterCallback, + required this.balanceUnit, + }); + + @override + Widget build(BuildContext context) { + return Align( + child: SizedBox( + width: MediaQuery.of(context).size.width > 1200 + ? MediaQuery.of(context).size.width / 3 + : MediaQuery.of(context).size.width, + child: DoubleTabToClipboard( + clipBoardData: walletAddress.address, + child: Card( + elevation: 0, + child: ClipRect( + child: ExpansionTile( + title: Text( + walletAddress.address, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + subtitle: Text( + walletAddress.addressBookName.isEmpty + ? AppLocalizations.instance + .translate('addressbook_no_label') + : walletAddress.addressBookName, + style: TextStyle( + color: Theme.of(context).colorScheme.secondary, + fontSize: 12, + ), + ), + key: Key(walletAddress.address), + children: [ + Column( + children: [ + Text( + '$balance $balanceUnit', + style: const TextStyle(fontSize: 16), + ), + ], + ), + const SizedBox( + height: 10, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + AddressesTabExpandableIcon( + action: () => + _addressEditDialog(context, walletAddress), + icon: Icon( + Icons.edit, + color: Theme.of(context).primaryColor, + ), + caption: AppLocalizations.instance + .translate('addressbook_swipe_edit'), + ), + AddressesTabExpandableIcon( + action: () => WalletHomeQr.showQrDialog( + context, + walletAddress.address, + ), + icon: Icon( + Icons.share, + color: Theme.of(context).primaryColor, + ), + caption: AppLocalizations.instance + .translate('addressbook_swipe_share'), + ), + AddressesTabExpandableIcon( + action: () async => await _showDeleteDialog(context), + icon: Icon( + Icons.delete, + color: Theme.of(context).colorScheme.error, + ), + caption: AppLocalizations.instance + .translate('addressbook_swipe_delete'), + ), + ], + ), + const SizedBox( + height: 10, + ), + // IconSlideAction( + // caption: AppLocalizations.instance + // .translate('addressbook_swipe_edit'), + // color: Theme.of(context).primaryColor, + // icon: Icons.edit, + // onTap: + // ), + // IconSlideAction( + // caption: AppLocalizations.instance + // .translate('addressbook_swipe_share'), + // color: Theme.of(context).colorScheme.background, + // iconWidget: Icon( + // Icons.share, + // color: Theme.of(context).colorScheme.secondary, + // ), + // onTap: + // ), + // IconSlideAction( + // caption: AppLocalizations.instance + // .translate('addressbook_swipe_delete'), + // color: Theme.of(context).colorScheme.error, + // iconWidget: const Icon( + // Icons.delete, + // color: Colors.white, + // ), + // onTap: () async {}, + // ), + ], + ), + ), + ), + ), + ), + ); + } + + Future _showDeleteDialog(BuildContext context) async { + await showDialog( + context: context, + builder: (_) => AlertDialog( + title: Text( + AppLocalizations.instance.translate( + 'addressbook_dialog_remove_title', + ), + ), + content: Text(walletAddress.address), + actions: [ + TextButton.icon( + label: Text( + AppLocalizations.instance.translate( + 'server_settings_alert_cancel', + ), + ), + icon: const Icon(Icons.cancel), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + TextButton.icon( + label: Text( + AppLocalizations.instance.translate( + 'jail_dialog_button', + ), + ), + icon: const Icon(Icons.check), + onPressed: () async { + await _performAddressDelete(context); + }, + ), + ], + ), + ); + } + + Future _addressEditDialog( + BuildContext context, + WalletAddress address, + ) async { + var textFieldController = TextEditingController(); + textFieldController.text = address.addressBookName; + return showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text( + '${AppLocalizations.instance.translate('addressbook_edit_dialog_title')} ${address.address}', + textAlign: TextAlign.center, + ), + content: TextField( + controller: textFieldController, + maxLength: 32, + decoration: InputDecoration( + hintText: AppLocalizations.instance + .translate('addressbook_edit_dialog_input'), + ), + ), + actions: [ + TextButton( + onPressed: () { + Navigator.pop(context); + }, + child: Text( + AppLocalizations.instance + .translate('server_settings_alert_cancel'), + ), + ), + TextButton( + onPressed: () { + context.read().updateOrCreateAddressLabel( + identifier: walletName, + address: address.address, + label: textFieldController.text, + ); + Navigator.pop(context); + }, + child: Text( + AppLocalizations.instance.translate('jail_dialog_button'), + ), + ), + ], + ); + }, + ); + } + + Future _performAddressDelete(BuildContext context) async { + final WalletProvider walletProvider = context.read(); + + await walletProvider.removeWatchOnlyAddress( + walletName, + walletAddress, + ); + if (!context.mounted) return; + + applyFilterCallback(); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + AppLocalizations.instance.translate( + 'addressbook_dialog_remove_snack', + ), + textAlign: TextAlign.center, + ), + duration: const Duration( + seconds: 5, + ), + ), + ); + Navigator.of(context).pop(); + } +} + +enum AddressTabSlideableType { receive, send, watchOnly } diff --git a/lib/widgets/wallet/address_book/addresses_tab_expandable_icon.dart b/lib/widgets/wallet/address_book/addresses_tab_expandable_icon.dart new file mode 100644 index 00000000..c313295f --- /dev/null +++ b/lib/widgets/wallet/address_book/addresses_tab_expandable_icon.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; + +class AddressesTabExpandableIcon extends StatelessWidget { + final Function action; + final Icon icon; + final String caption; + const AddressesTabExpandableIcon({ + super.key, + required this.action, + required this.icon, + required this.caption, + }); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + IconButton( + onPressed: () => action(), + icon: icon, + ), + Text( + caption, + style: TextStyle( + color: Theme.of(context).colorScheme.secondary, + ), + ), + ], + ); + } +} diff --git a/lib/widgets/wallet/address_book/addresses_tab_watch_only.dart b/lib/widgets/wallet/address_book/addresses_tab_watch_only.dart new file mode 100644 index 00000000..9724e714 --- /dev/null +++ b/lib/widgets/wallet/address_book/addresses_tab_watch_only.dart @@ -0,0 +1,265 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../../../models/available_coins.dart'; +import '../../../models/coin.dart'; +import '../../../models/hive/wallet_address.dart'; +import '../../../providers/wallet_provider.dart'; +import '../../../tools/app_localizations.dart'; +import '../../../tools/validators.dart'; +import '../addresses_tab.dart'; +import 'addresses_tab_expandable.dart'; + +class AddressesTabWatchOnly extends AddressTab { + final String searchString; + + const AddressesTabWatchOnly({ + super.key, + required super.walletName, + required super.walletAddresses, + required super.changeTab, + required this.searchString, + }); + + @override + State createState() => _AddressesTabWatchOnlyState(); +} + +class _AddressesTabWatchOnlyState extends State { + bool _initial = true; + List _filteredWatchOnlyReceivingAddresses = []; + late Coin _availableCoin; + late WalletProvider _walletProvider; + final Map _addressBalanceMap = {}; + late int _decimalProduct; + + @override + void didChangeDependencies() async { + if (_initial == true) { + _availableCoin = AvailableCoins.getSpecificCoin(widget.walletName); + _walletProvider = Provider.of(context); + + await _fillAddressBalanceMap(); + + _decimalProduct = AvailableCoins.getDecimalProduct( + identifier: widget.walletName, + ); + + setState(() { + _initial = false; + }); + } + super.didChangeDependencies(); + } + + Future _fillAddressBalanceMap() async { + final utxos = await _walletProvider.getWalletUtxos(widget.walletName); + if (_addressBalanceMap.isNotEmpty) { + _addressBalanceMap.clear(); + } + for (var tx in utxos) { + if (tx.value > 0) { + if (_addressBalanceMap[tx.address] != null) { + _addressBalanceMap[tx.address] += tx.value; + } else { + _addressBalanceMap[tx.address] = tx.value; + } + } + } + } + + void _applyFilter() { + setState(() { + _filteredWatchOnlyReceivingAddresses = widget.walletAddresses + .where( + (element) => + element.address.contains(widget.searchString) || + element.addressBookName.contains(widget.searchString), + ) + .toList(); + }); + } + + Future _addressAddDialog(BuildContext context) async { + var labelController = TextEditingController(); + var addressController = TextEditingController(); + final formKey = GlobalKey(); + + return showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text( + AppLocalizations.instance.translate('addressbook_add_new'), + textAlign: TextAlign.center, + ), + content: Form( + key: formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextFormField( + controller: addressController, + decoration: InputDecoration( + hintText: + AppLocalizations.instance.translate('send_address'), + ), + validator: (value) { + if (value!.isEmpty) { + return AppLocalizations.instance + .translate('send_enter_address'); + } + var sanitized = value.trim(); + if (validateAddress( + sanitized, + _availableCoin.networkType, + ) == + false) { + return AppLocalizations.instance + .translate('send_invalid_address'); + } + //check if already exists + if (widget.walletAddresses + .any((element) => element.address == value)) { + return 'Address already exists'; + } + return null; + }, + ), + TextFormField( + controller: labelController, + maxLength: 32, + decoration: InputDecoration( + hintText: AppLocalizations.instance.translate('send_label'), + ), + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () { + Navigator.pop(context); + }, + child: Text( + AppLocalizations.instance + .translate('server_settings_alert_cancel'), + ), + ), + TextButton( + onPressed: () { + if (formKey.currentState!.validate()) { + formKey.currentState!.save(); + context.read().createWatchOnlyAddres( + identifier: widget.walletName, + address: addressController.text, + label: labelController.text == '' + ? '' + : labelController.text, + ); + _applyFilter(); + Navigator.pop(context); + } + }, + child: Text( + AppLocalizations.instance.translate('jail_dialog_button'), + ), + ), + ], + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + _applyFilter(); + _fillAddressBalanceMap(); + + return Scaffold( + backgroundColor: Theme.of(context).primaryColor, + body: SingleChildScrollView( + child: Align( + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.all(20), + child: ElevatedButton( + style: ElevatedButton.styleFrom( + foregroundColor: + Theme.of(context).colorScheme.background, + backgroundColor: + Theme.of(context).colorScheme.background, + fixedSize: Size( + MediaQuery.of(context).size.width > 1200 + ? MediaQuery.of(context).size.width / 5 + : MediaQuery.of(context).size.width / 3, + 40, + ), + shape: RoundedRectangleBorder( + //to set border radius to button + borderRadius: BorderRadius.circular(10), + side: BorderSide( + width: 2, + color: Theme.of(context).primaryColor, + ), + ), + elevation: 0, + ), + onPressed: () { + _addressAddDialog(context); + }, + child: Text( + AppLocalizations.instance + .translate('addressbook_new_button'), + style: TextStyle( + fontWeight: FontWeight.bold, + letterSpacing: 1.4, + fontSize: 16, + color: Theme.of(context).primaryColor, + ), + ), + ), + ), + ], + ), + if (_filteredWatchOnlyReceivingAddresses.isEmpty) + Column( + children: [ + Image.asset( + 'assets/img/list-empty.png', + height: MediaQuery.of(context).size.height / 4, + ), + Center( + child: Text( + AppLocalizations.instance.translate('addresses_none'), + style: TextStyle( + fontSize: 16, + fontStyle: FontStyle.italic, + color: Theme.of(context).colorScheme.background, + ), + ), + ), + ], + ), + for (var addr in _filteredWatchOnlyReceivingAddresses) + AddressTabExpandable( + walletAddress: addr, + walletName: widget.walletName, + type: AddressTabSlideableType.watchOnly, + applyFilterCallback: _applyFilter, + balance: _addressBalanceMap.containsKey(addr.address) + ? _addressBalanceMap[addr.address] / _decimalProduct + : 0, + balanceUnit: _availableCoin.letterCode, + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/widgets/wallet/addresses_tab.dart b/lib/widgets/wallet/addresses_tab.dart index cc4dedfa..cd5590c3 100644 --- a/lib/widgets/wallet/addresses_tab.dart +++ b/lib/widgets/wallet/addresses_tab.dart @@ -20,17 +20,15 @@ import 'wallet_home_qr.dart'; class AddressTab extends StatefulWidget { final String walletName; - final String title; final List walletAddresses; - final Function changeIndex; + final Function changeTab; const AddressTab({ required this.walletName, - required this.title, required this.walletAddresses, - required this.changeIndex, - Key? key, - }) : super(key: key); + required this.changeTab, + super.key, + }); @override State createState() => _AddressTabState(); @@ -64,8 +62,8 @@ class _AddressTabState extends State { if (_initial) { applyFilter(); _availableCoin = AvailableCoins.getSpecificCoin(widget.walletName); - _walletProvider = Provider.of(context); _connection = Provider.of(context); + _walletProvider = Provider.of(context); _listenedAddresses = _connection.listenedAddresses; _decimalProduct = AvailableCoins.getDecimalProduct( identifier: widget.walletName, @@ -195,10 +193,10 @@ class _AddressTabState extends State { ), TextButton( onPressed: () { - context.read().updateLabel( - widget.walletName, - address.address, - textFieldController.text, + context.read().updateOrCreateAddressLabel( + identifier: widget.walletName, + address: address.address, + label: textFieldController.text, ); Navigator.pop(context); }, @@ -412,10 +410,12 @@ class _AddressTabState extends State { onPressed: () { if (formKey.currentState!.validate()) { formKey.currentState!.save(); - context.read().updateLabel( - widget.walletName, - addressController.text, - labelController.text == '' ? '' : labelController.text, + context.read().updateOrCreateAddressLabel( + identifier: widget.walletName, + address: addressController.text, + label: labelController.text == '' + ? '' + : labelController.text, ); applyFilter(); Navigator.pop(context); @@ -487,8 +487,8 @@ class _AddressTabState extends State { Icons.send, color: Theme.of(context).colorScheme.background, ), - onTap: () => widget.changeIndex( - Tabs.send, + onTap: () => widget.changeTab( + WalletTab.send, addr.address, addr.addressBookName, ), diff --git a/lib/widgets/wallet/new_wallet.dart b/lib/widgets/wallet/new_wallet.dart index 9662310c..c35c62c9 100644 --- a/lib/widgets/wallet/new_wallet.dart +++ b/lib/widgets/wallet/new_wallet.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:peercoin/models/experimental_features.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -10,7 +11,7 @@ import '../../tools/app_localizations.dart'; import '../../tools/auth.dart'; class NewWalletDialog extends StatefulWidget { - const NewWalletDialog({Key? key}) : super(key: key); + const NewWalletDialog({super.key}); @override State createState() => _NewWalletDialogState(); @@ -21,6 +22,7 @@ Map _availableCoins = AvailableCoins.availableCoins; class _NewWalletDialogState extends State { String _coin = ''; bool _initial = true; + bool _watchOnly = false; late AppSettingsProvider _appSettings; Future addWallet() async { @@ -45,6 +47,7 @@ class _NewWalletDialogState extends State { title: title, letterCode: letterCode, isImportedSeed: prefs.getBool('importedSeed') == true, + watchOnly: _watchOnly, ); //add to order list @@ -128,6 +131,31 @@ class _NewWalletDialogState extends State { ); } + if (_appSettings.activatedExperimentalFeatures + .contains(ExperimentalFeatures.watchOnlyWallets.name)) { + list.add( + SimpleDialogOption( + child: GestureDetector( + onTap: () => setState(() { + _watchOnly = !_watchOnly; + }), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Checkbox( + value: _watchOnly, + onChanged: (e) => setState(() { + _watchOnly = e!; + }), + ), + Text(AppLocalizations.instance.translate('watch_only')), + ], + ), + ), + ), + ); + } + return SimpleDialog( title: Text(AppLocalizations.instance.translate('add_new_wallet')), children: list, diff --git a/lib/widgets/wallet/receive_tab.dart b/lib/widgets/wallet/receive_tab.dart index 88a87de8..90b4e38a 100644 --- a/lib/widgets/wallet/receive_tab.dart +++ b/lib/widgets/wallet/receive_tab.dart @@ -26,8 +26,8 @@ class ReceiveTab extends StatefulWidget { required this.unusedAddress, required this.connectionState, required this.wallet, - Key? key, - }) : super(key: key); + super.key, + }); @override State createState() => _ReceiveTabState(); @@ -323,10 +323,12 @@ class _ReceiveTabState extends State { .translate('receive_share'), action: () async { if (labelController.text != '') { - context.read().updateLabel( - widget.wallet.name, - widget.unusedAddress, - labelController.text, + context + .read() + .updateOrCreateAddressLabel( + identifier: widget.wallet.name, + address: widget.unusedAddress, + label: labelController.text, ); } await ShareWrapper.share( diff --git a/lib/widgets/wallet/send_tab.dart b/lib/widgets/wallet/send_tab.dart index 4193c675..23b82b28 100644 --- a/lib/widgets/wallet/send_tab.dart +++ b/lib/widgets/wallet/send_tab.dart @@ -36,19 +36,19 @@ import '../../tools/logger_wrapper.dart'; import '../../tools/price_ticker.dart'; class SendTab extends StatefulWidget { - final Function changeIndex; + final Function changeTab; final String? address; final String? label; final BackendConnectionState connectionState; final CoinWallet wallet; const SendTab({ - required this.changeIndex, + required this.changeTab, this.address, this.label, required this.wallet, required this.connectionState, - Key? key, - }) : super(key: key); + super.key, + }); @override State createState() => _SendTabState(); @@ -760,7 +760,7 @@ class _SendTabState extends State { decimalProduct: _decimalProduct, coinLetterCode: widget.wallet.letterCode, coinIdentifier: widget.wallet.name, - callBackAfterSend: () => widget.changeIndex(Tabs.transactions), + callBackAfterSend: () => widget.changeTab(WalletTab.transactions), fiatPricePerCoin: _coinValue, fiatCode: _appSettings.selectedCurrency, ), @@ -770,10 +770,10 @@ class _SendTabState extends State { _labelControllerList.asMap().forEach( (index, controller) { if (controller.text != '') { - _walletProvider.updateLabel( - widget.wallet.name, - _addressControllerList[index].text, - controller.text, + _walletProvider.updateOrCreateAddressLabel( + identifier: widget.wallet.name, + address: _addressControllerList[index].text, + label: controller.text, ); } }, diff --git a/lib/widgets/wallet/send_tab_management.dart b/lib/widgets/wallet/send_tab_management.dart index 8ca42f3e..73427ef8 100644 --- a/lib/widgets/wallet/send_tab_management.dart +++ b/lib/widgets/wallet/send_tab_management.dart @@ -4,11 +4,11 @@ import '../../tools/app_localizations.dart'; class SendTabAddressManagement extends StatelessWidget { const SendTabAddressManagement({ - Key? key, + super.key, required this.onAdd, required this.onDelete, required this.numberOfRecipients, - }) : super(key: key); + }); final Function onAdd; final Function onDelete; final int numberOfRecipients; diff --git a/lib/widgets/wallet/send_tab_navigator.dart b/lib/widgets/wallet/send_tab_navigator.dart index a2c90dd0..d3b30000 100644 --- a/lib/widgets/wallet/send_tab_navigator.dart +++ b/lib/widgets/wallet/send_tab_navigator.dart @@ -3,11 +3,11 @@ import 'package:peercoin/tools/app_localizations.dart'; class SendTabNavigator extends StatelessWidget { const SendTabNavigator({ - Key? key, + super.key, required this.currentIndex, required this.numberOfRecipients, required this.raiseNewindex, - }) : super(key: key); + }); final int currentIndex; final int numberOfRecipients; final Function raiseNewindex; diff --git a/lib/widgets/wallet/transactions_list.dart b/lib/widgets/wallet/transactions_list.dart index 36661992..ba84c365 100644 --- a/lib/widgets/wallet/transactions_list.dart +++ b/lib/widgets/wallet/transactions_list.dart @@ -22,8 +22,8 @@ class TransactionList extends StatefulWidget { required this.walletTransactions, required this.wallet, required this.connectionState, - Key? key, - }) : super(key: key); + super.key, + }); @override State createState() => _TransactionListState(); @@ -60,14 +60,14 @@ class _TransactionListState extends State { if (tx.confirmations == -1) { return const Text( 'X', - textScaleFactor: 0.9, + textScaler: TextScaler.linear(0.9), style: TextStyle(color: Colors.red), ); } return tx.broadCasted == false ? Text( '?', - textScaleFactor: 0.9, + textScaler: const TextScaler.linear(0.9), style: TextStyle( color: Theme.of(context).colorScheme.secondary, ), @@ -205,7 +205,7 @@ class _TransactionListState extends State { ? FontWeight.w500 : FontWeight.w300, ), - textScaleFactor: 0.8, + textScaler: const TextScaler.linear(0.8), ), ], ), @@ -213,7 +213,7 @@ class _TransactionListState extends State { child: Text( filteredTx[i - 1].txid, overflow: TextOverflow.ellipsis, - textScaleFactor: 0.9, + textScaler: const TextScaler.linear(0.9), ), ), subtitle: Center( @@ -222,7 +222,7 @@ class _TransactionListState extends State { filteredTx[i - 1].address, ), overflow: TextOverflow.ellipsis, - textScaleFactor: 1, + textScaler: const TextScaler.linear(1), ), ), trailing: Column( diff --git a/lib/widgets/wallet/wallet_balance_header.dart b/lib/widgets/wallet/wallet_balance_header.dart index 26e5f8a6..5e045cf5 100644 --- a/lib/widgets/wallet/wallet_balance_header.dart +++ b/lib/widgets/wallet/wallet_balance_header.dart @@ -15,8 +15,8 @@ class WalletBalanceHeader extends StatelessWidget { const WalletBalanceHeader( this._connectionState, this._wallet, { - Key? key, - }) : super(key: key); + super.key, + }); @override Widget build(BuildContext context) { diff --git a/lib/widgets/wallet/wallet_balance_price.dart b/lib/widgets/wallet/wallet_balance_price.dart index 45fc6668..6ba86c31 100644 --- a/lib/widgets/wallet/wallet_balance_price.dart +++ b/lib/widgets/wallet/wallet_balance_price.dart @@ -6,10 +6,10 @@ class WalletBalancePrice extends StatefulWidget { final Text valueInFiat; final Text fiatCoinValue; const WalletBalancePrice({ - Key? key, + super.key, required this.valueInFiat, required this.fiatCoinValue, - }) : super(key: key); + }); @override State createState() => _WalletBalancePriceState(); diff --git a/lib/widgets/wallet/wallet_home_connection.dart b/lib/widgets/wallet/wallet_home_connection.dart index d06833fe..caf7e99b 100644 --- a/lib/widgets/wallet/wallet_home_connection.dart +++ b/lib/widgets/wallet/wallet_home_connection.dart @@ -6,8 +6,7 @@ import '/../widgets/loading_indicator.dart'; class WalletHomeConnection extends StatelessWidget { final BackendConnectionState _connectionState; - const WalletHomeConnection(this._connectionState, {Key? key}) - : super(key: key); + const WalletHomeConnection(this._connectionState, {super.key}); @override Widget build(BuildContext context) { diff --git a/lib/widgets/wallet/wallet_home_qr.dart b/lib/widgets/wallet/wallet_home_qr.dart index 3442df8f..46cc9bdb 100644 --- a/lib/widgets/wallet/wallet_home_qr.dart +++ b/lib/widgets/wallet/wallet_home_qr.dart @@ -8,7 +8,7 @@ import '../double_tab_to_clipboard.dart'; class WalletHomeQr extends StatelessWidget { final String _unusedAddress; - const WalletHomeQr(this._unusedAddress, {Key? key}) : super(key: key); + const WalletHomeQr(this._unusedAddress, {super.key}); static void showQrDialog( BuildContext context, diff --git a/lib/widgets/wallet/wallet_reset_bottom_sheet.dart b/lib/widgets/wallet/wallet_reset_bottom_sheet.dart index 4072078d..0f7f535f 100644 --- a/lib/widgets/wallet/wallet_reset_bottom_sheet.dart +++ b/lib/widgets/wallet/wallet_reset_bottom_sheet.dart @@ -53,6 +53,15 @@ class WalletResetBottomSheet extends StatelessWidget { ), action: () => action(), ), + const SizedBox( + height: 10, + ), + PeerButtonBorder( + text: AppLocalizations.instance.translate( + 'server_settings_alert_cancel', + ), + action: () => Navigator.of(context).pop(), + ), ], ), ); diff --git a/pubspec.lock b/pubspec.lock index b1a2b01d..724c96aa 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -237,26 +237,26 @@ packages: dependency: transitive description: name: coinlib - sha256: "410993f49aef30e48b76bbad8a33fef33f6b90e103b15b6be0277683ffe15399" + sha256: b6813d812a3f4073b3c18f9810d9f2940c05f2b227d84b2ba317fb26af401310 url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "2.0.0-rc.4" coinlib_flutter: dependency: "direct main" description: name: coinlib_flutter - sha256: d0a6a3694fc8c851f6b912bb1c552e33998e3f453efab3d62f75c0b1773d1ab9 + sha256: "448df1b2ee58245e0b5bb82b2a99eaaa21ad340043136e58809e9721223de40c" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "2.0.0-rc.3" collection: dependency: "direct main" description: name: collection - sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a url: "https://pub.dev" source: hosted - version: "1.17.2" + version: "1.18.0" connectivity_plus: dependency: "direct main" description: @@ -463,10 +463,10 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 + sha256: e2a421b7e59244faef694ba7b30562e489c2b489866e505074eb005cd7060db7 url: "https://pub.dev" source: hosted - version: "2.0.3" + version: "3.0.1" flutter_local_notifications: dependency: "direct main" description: @@ -776,10 +776,10 @@ packages: dependency: transitive description: name: lints - sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" + sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "3.0.0" loader_overlay: dependency: "direct main" description: @@ -872,10 +872,10 @@ packages: dependency: transitive description: name: meta - sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.10.0" mime: dependency: transitive description: @@ -1008,10 +1008,10 @@ packages: dependency: transitive description: name: platform - sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" + sha256: ae68c7bfcd7383af3629daafb32fb4e8681c7154428da4febcff06200585f102 url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.2" plugin_platform_interface: dependency: transitive description: @@ -1277,10 +1277,10 @@ packages: dependency: transitive description: name: stack_trace - sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.11.1" step_progress_indicator: dependency: "direct main" description: @@ -1293,10 +1293,10 @@ packages: dependency: transitive description: name: stream_channel - sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" stream_transform: dependency: transitive description: @@ -1333,26 +1333,26 @@ packages: dependency: "direct dev" description: name: test - sha256: "13b41f318e2a5751c3169137103b60c584297353d4b1761b66029bae6411fe46" + sha256: a1f7595805820fcc05e5c52e3a231aedd0b72972cb333e8c738a8b1239448b6f url: "https://pub.dev" source: hosted - version: "1.24.3" + version: "1.24.9" test_api: dependency: transitive description: name: test_api - sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" url: "https://pub.dev" source: hosted - version: "0.6.0" + version: "0.6.1" test_core: dependency: transitive description: name: test_core - sha256: "99806e9e6d95c7b059b7a0fc08f07fc53fabe54a829497f0d9676299f1e8637e" + sha256: a757b14fc47507060a162cc2530d9a4a2f92f5100a952c7443b5cad5ef5b106a url: "https://pub.dev" source: hosted - version: "0.5.3" + version: "0.5.9" theme_mode_handler: dependency: "direct main" description: @@ -1477,10 +1477,10 @@ packages: dependency: transitive description: name: vm_service - sha256: c620a6f783fa22436da68e42db7ebbf18b8c44b9a46ab911f666ff09ffd9153f + sha256: c538be99af830f478718b51630ec1b6bee5e74e52c8a802d328d9e71d35d2583 url: "https://pub.dev" source: hosted - version: "11.7.1" + version: "11.10.0" wasm_interop: dependency: transitive description: @@ -1501,10 +1501,10 @@ packages: dependency: transitive description: name: web - sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 + sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 url: "https://pub.dev" source: hosted - version: "0.1.4-beta" + version: "0.3.0" web_socket_channel: dependency: "direct main" description: @@ -1562,5 +1562,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.1.0 <4.0.0" + dart: ">=3.2.0 <4.0.0" flutter: ">=3.13.0" diff --git a/pubspec.yaml b/pubspec.yaml index cb17e081..ae8add26 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,10 +1,10 @@ name: peercoin description: A new Peercoin wallet. -version: 1.2.2+134 +version: 1.2.3+135 environment: - sdk: '>=3.0.0 <4.0.0' + sdk: '>=3.2.0 <4.0.0' publish_to: none @@ -55,7 +55,7 @@ dependencies: app_bar_with_search_switch: ^1.5.4 decimal: ^2.3.3 characters: ^1.2.0 - coinlib_flutter: ^1.0.0 + coinlib_flutter: ^2.0.0-rc.3 dev_dependencies: integration_test: @@ -68,7 +68,7 @@ dev_dependencies: build_runner: ^2.0.4 flutter_launcher_icons: ^0.11.0 flutter_native_splash: ^2.2.7 - flutter_lints: ^2.0.1 + flutter_lints: ^3.0.0 flutter_icons: android: "launcher_icon" diff --git a/test/transaction_building_test.dart b/test/transaction_building_test.dart index 1ee7f5f0..43b04033 100644 --- a/test/transaction_building_test.dart +++ b/test/transaction_building_test.dart @@ -62,12 +62,14 @@ void main() async { title: walletName, letterCode: 'PPC', isImportedSeed: false, + watchOnly: false, ); wallet.addWallet( name: testnetWalletName, title: testnetWalletName, letterCode: 'tPPC', isImportedSeed: false, + watchOnly: false, ); }); diff --git a/test_driver/key_new.dart b/test_driver/key_new.dart index 92c1a060..0e77a93b 100644 --- a/test_driver/key_new.dart +++ b/test_driver/key_new.dart @@ -126,6 +126,7 @@ void main() { }); test('Change wallet title', () async { + await driver.tap(find.byTooltip('Transactions')); await driver.tap(find.byTooltip('Show menu')); await driver.tap(find.text('Change Title')); await driver.tap(find.byType('TextField'));