From 786831bcef80f10660f8f73d8708e1c57dffd497 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 7 Nov 2022 08:31:27 -0600 Subject: [PATCH 01/22] alphabetically sort contacts --- lib/services/address_book_service.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/services/address_book_service.dart b/lib/services/address_book_service.dart index 6f7d2b9bd..f51eefbba 100644 --- a/lib/services/address_book_service.dart +++ b/lib/services/address_book_service.dart @@ -20,10 +20,13 @@ class AddressBookService extends ChangeNotifier { List<Contact> get contacts { final keys = List<String>.from( DB.instance.keys<dynamic>(boxName: DB.boxNameAddressBook)); - return keys + final _contacts = keys .map((id) => Contact.fromJson(Map<String, dynamic>.from(DB.instance .get<dynamic>(boxName: DB.boxNameAddressBook, key: id) as Map))) .toList(growable: false); + _contacts + .sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase())); + return _contacts; } Future<List<Contact>>? _addressBookEntries; From f6bad974e6915338ce94c22ea356833b8cb8c8c8 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 7 Nov 2022 10:14:47 -0600 Subject: [PATCH 02/22] address book tests updated --- test/address_book_service_test.dart | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/test/address_book_service_test.dart b/test/address_book_service_test.dart index 1059f7fc3..c5effd223 100644 --- a/test/address_book_service_test.dart +++ b/test/address_book_service_test.dart @@ -94,19 +94,19 @@ void main() { test("get contacts", () { final service = AddressBookService(); expect(service.contacts.toString(), - [contactA, contactB, contactC].toString()); + [contactC, contactB, contactA].toString()); }); test("get addressBookEntries", () async { final service = AddressBookService(); expect((await service.addressBookEntries).toString(), - [contactA, contactB, contactC].toString()); + [contactC, contactB, contactA].toString()); }); test("search contacts", () async { final service = AddressBookService(); final results = await service.search("j"); - expect(results.toString(), [contactA, contactB].toString()); + expect(results.toString(), [contactB, contactA].toString()); final results2 = await service.search("ja"); expect(results2.toString(), [contactB].toString()); @@ -118,7 +118,7 @@ void main() { expect(results4.toString(), <Contact>[].toString()); final results5 = await service.search(""); - expect(results5.toString(), [contactA, contactB, contactC].toString()); + expect(results5.toString(), [contactC, contactB, contactA].toString()); final results6 = await service.search("epic address"); expect(results6.toString(), [contactC].toString()); @@ -140,7 +140,7 @@ void main() { expect(result, false); expect(service.contacts.length, 3); expect(service.contacts.toString(), - [contactA, contactB, contactC].toString()); + [contactC, contactB, contactA].toString()); }); test("edit contact", () async { @@ -149,14 +149,14 @@ void main() { expect(await service.editContact(editedContact), true); expect(service.contacts.length, 3); expect(service.contacts.toString(), - [contactA, editedContact, contactC].toString()); + [contactC, contactA, editedContact].toString()); }); test("remove existing contact", () async { final service = AddressBookService(); await service.removeContact(contactB.id); expect(service.contacts.length, 2); - expect(service.contacts.toString(), [contactA, contactC].toString()); + expect(service.contacts.toString(), [contactC, contactA].toString()); }); test("remove non existing contact", () async { @@ -164,7 +164,7 @@ void main() { await service.removeContact("some id"); expect(service.contacts.length, 3); expect(service.contacts.toString(), - [contactA, contactB, contactC].toString()); + [contactC, contactB, contactA].toString()); }); tearDown(() async { From bb260e3a23b7e0c1ddd4509d16a95db3a51305c0 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 7 Nov 2022 10:24:08 -0600 Subject: [PATCH 03/22] hacky fix (due to current persistence design) to get sent transactions showing up right away for electrumx coins --- .../send_view/confirm_transaction_view.dart | 4 +- .../coins/bitcoin/bitcoin_wallet.dart | 50 +++++++++++++++++++ .../coins/bitcoincash/bitcoincash_wallet.dart | 50 +++++++++++++++++++ lib/services/coins/coin_service.dart | 5 +- .../coins/dogecoin/dogecoin_wallet.dart | 50 +++++++++++++++++++ .../coins/epiccash/epiccash_wallet.dart | 8 +++ lib/services/coins/firo/firo_wallet.dart | 50 +++++++++++++++++++ .../coins/litecoin/litecoin_wallet.dart | 50 +++++++++++++++++++ lib/services/coins/manager.dart | 3 ++ lib/services/coins/monero/monero_wallet.dart | 8 +++ .../coins/namecoin/namecoin_wallet.dart | 50 +++++++++++++++++++ .../coins/wownero/wownero_wallet.dart | 8 +++ .../services/coins/fake_coin_service_api.dart | 6 +++ 13 files changed, 339 insertions(+), 3 deletions(-) diff --git a/lib/pages/send_view/confirm_transaction_view.dart b/lib/pages/send_view/confirm_transaction_view.dart index 81d5a3da2..26d1231f0 100644 --- a/lib/pages/send_view/confirm_transaction_view.dart +++ b/lib/pages/send_view/confirm_transaction_view.dart @@ -87,13 +87,13 @@ class _ConfirmTransactionViewState txid = await manager.confirmSend(txData: transactionInfo); } - unawaited(manager.refresh()); - // save note await ref .read(notesServiceChangeNotifierProvider(walletId)) .editOrAddNote(txid: txid, note: note); + unawaited(manager.refresh()); + // pop back to wallet if (mounted) { Navigator.of(context).popUntil(ModalRoute.withName(routeOnSuccessName)); diff --git a/lib/services/coins/bitcoin/bitcoin_wallet.dart b/lib/services/coins/bitcoin/bitcoin_wallet.dart index 391beb909..d0920075d 100644 --- a/lib/services/coins/bitcoin/bitcoin_wallet.dart +++ b/lib/services/coins/bitcoin/bitcoin_wallet.dart @@ -10,6 +10,7 @@ import 'package:bitcoindart/bitcoindart.dart'; import 'package:bs58check/bs58check.dart' as bs58check; import 'package:crypto/crypto.dart'; import 'package:decimal/decimal.dart'; +import 'package:devicelocale/devicelocale.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:http/http.dart'; @@ -1283,6 +1284,54 @@ class BitcoinWallet extends CoinServiceAPI { _transactionData ??= _fetchTransactionData(); Future<TransactionData>? _transactionData; + TransactionData? cachedTxData; + + // hack to add tx to txData before refresh completes + // required based on current app architecture where we don't properly store + // transactions locally in a good way + @override + Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async { + final priceData = + await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency); + Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero; + final locale = await Devicelocale.currentLocale; + final String worthNow = Format.localizedStringAsFixed( + value: + ((currentPrice * Decimal.fromInt(txData["recipientAmt"] as int)) / + Decimal.fromInt(Constants.satsPerCoin)) + .toDecimal(scaleOnInfinitePrecision: 2), + decimalPlaces: 2, + locale: locale!); + + final tx = models.Transaction( + txid: txData["txid"] as String, + confirmedStatus: false, + timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000, + txType: "Sent", + amount: txData["recipientAmt"] as int, + worthNow: worthNow, + worthAtBlockTimestamp: worthNow, + fees: txData["fee"] as int, + inputSize: 0, + outputSize: 0, + inputs: [], + outputs: [], + address: txData["address"] as String, + height: -1, + confirmations: 0, + ); + + if (cachedTxData == null) { + final data = await _fetchTransactionData(); + _transactionData = Future(() => data); + } + + final transactions = cachedTxData!.getAllTransactions(); + transactions[tx.txid] = tx; + cachedTxData = models.TransactionData.fromMap(transactions); + _transactionData = Future(() => cachedTxData!); + } + @override bool validateAddress(String address) { return Address.validateAddress(address, _network); @@ -2661,6 +2710,7 @@ class BitcoinWallet extends CoinServiceAPI { await DB.instance.put<dynamic>( boxName: walletId, key: 'latest_tx_model', value: txModel); + cachedTxData = txModel; return txModel; } diff --git a/lib/services/coins/bitcoincash/bitcoincash_wallet.dart b/lib/services/coins/bitcoincash/bitcoincash_wallet.dart index fa88b3f2f..98a31ee0c 100644 --- a/lib/services/coins/bitcoincash/bitcoincash_wallet.dart +++ b/lib/services/coins/bitcoincash/bitcoincash_wallet.dart @@ -11,6 +11,7 @@ import 'package:bitcoindart/bitcoindart.dart'; import 'package:bs58check/bs58check.dart' as bs58check; import 'package:crypto/crypto.dart'; import 'package:decimal/decimal.dart'; +import 'package:devicelocale/devicelocale.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:http/http.dart'; @@ -1154,6 +1155,54 @@ class BitcoinCashWallet extends CoinServiceAPI { _transactionData ??= _fetchTransactionData(); Future<TransactionData>? _transactionData; + TransactionData? cachedTxData; + + // hack to add tx to txData before refresh completes + // required based on current app architecture where we don't properly store + // transactions locally in a good way + @override + Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async { + final priceData = + await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency); + Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero; + final locale = await Devicelocale.currentLocale; + final String worthNow = Format.localizedStringAsFixed( + value: + ((currentPrice * Decimal.fromInt(txData["recipientAmt"] as int)) / + Decimal.fromInt(Constants.satsPerCoin)) + .toDecimal(scaleOnInfinitePrecision: 2), + decimalPlaces: 2, + locale: locale!); + + final tx = models.Transaction( + txid: txData["txid"] as String, + confirmedStatus: false, + timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000, + txType: "Sent", + amount: txData["recipientAmt"] as int, + worthNow: worthNow, + worthAtBlockTimestamp: worthNow, + fees: txData["fee"] as int, + inputSize: 0, + outputSize: 0, + inputs: [], + outputs: [], + address: txData["address"] as String, + height: -1, + confirmations: 0, + ); + + if (cachedTxData == null) { + final data = await _fetchTransactionData(); + _transactionData = Future(() => data); + } + + final transactions = cachedTxData!.getAllTransactions(); + transactions[tx.txid] = tx; + cachedTxData = models.TransactionData.fromMap(transactions); + _transactionData = Future(() => cachedTxData!); + } + @override bool validateAddress(String address) { try { @@ -2449,6 +2498,7 @@ class BitcoinCashWallet extends CoinServiceAPI { await DB.instance.put<dynamic>( boxName: walletId, key: 'latest_tx_model', value: txModel); + cachedTxData = txModel; return txModel; } diff --git a/lib/services/coins/coin_service.dart b/lib/services/coins/coin_service.dart index c36fa9eee..655865494 100644 --- a/lib/services/coins/coin_service.dart +++ b/lib/services/coins/coin_service.dart @@ -9,8 +9,8 @@ import 'package:stackwallet/services/coins/dogecoin/dogecoin_wallet.dart'; import 'package:stackwallet/services/coins/epiccash/epiccash_wallet.dart'; import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; import 'package:stackwallet/services/coins/monero/monero_wallet.dart'; -import 'package:stackwallet/services/coins/wownero/wownero_wallet.dart'; import 'package:stackwallet/services/coins/namecoin/namecoin_wallet.dart'; +import 'package:stackwallet/services/coins/wownero/wownero_wallet.dart'; import 'package:stackwallet/services/transaction_notification_tracker.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/prefs.dart'; @@ -277,4 +277,7 @@ abstract class CoinServiceAPI { Future<int> estimateFeeFor(int satoshiAmount, int feeRate); Future<bool> generateNewAddress(); + + // used for electrumx coins + Future<void> updateSentCachedTxData(Map<String, dynamic> txData); } diff --git a/lib/services/coins/dogecoin/dogecoin_wallet.dart b/lib/services/coins/dogecoin/dogecoin_wallet.dart index fbb551dcd..67be291a2 100644 --- a/lib/services/coins/dogecoin/dogecoin_wallet.dart +++ b/lib/services/coins/dogecoin/dogecoin_wallet.dart @@ -10,6 +10,7 @@ import 'package:bitcoindart/bitcoindart.dart'; import 'package:bs58check/bs58check.dart' as bs58check; import 'package:crypto/crypto.dart'; import 'package:decimal/decimal.dart'; +import 'package:devicelocale/devicelocale.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:http/http.dart'; @@ -1051,6 +1052,54 @@ class DogecoinWallet extends CoinServiceAPI { _transactionData ??= _fetchTransactionData(); Future<TransactionData>? _transactionData; + TransactionData? cachedTxData; + + // hack to add tx to txData before refresh completes + // required based on current app architecture where we don't properly store + // transactions locally in a good way + @override + Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async { + final priceData = + await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency); + Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero; + final locale = await Devicelocale.currentLocale; + final String worthNow = Format.localizedStringAsFixed( + value: + ((currentPrice * Decimal.fromInt(txData["recipientAmt"] as int)) / + Decimal.fromInt(Constants.satsPerCoin)) + .toDecimal(scaleOnInfinitePrecision: 2), + decimalPlaces: 2, + locale: locale!); + + final tx = models.Transaction( + txid: txData["txid"] as String, + confirmedStatus: false, + timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000, + txType: "Sent", + amount: txData["recipientAmt"] as int, + worthNow: worthNow, + worthAtBlockTimestamp: worthNow, + fees: txData["fee"] as int, + inputSize: 0, + outputSize: 0, + inputs: [], + outputs: [], + address: txData["address"] as String, + height: -1, + confirmations: 0, + ); + + if (cachedTxData == null) { + final data = await _fetchTransactionData(); + _transactionData = Future(() => data); + } + + final transactions = cachedTxData!.getAllTransactions(); + transactions[tx.txid] = tx; + cachedTxData = models.TransactionData.fromMap(transactions); + _transactionData = Future(() => cachedTxData!); + } + @override bool validateAddress(String address) { return Address.validateAddress(address, _network); @@ -2273,6 +2322,7 @@ class DogecoinWallet extends CoinServiceAPI { await DB.instance.put<dynamic>( boxName: walletId, key: 'latest_tx_model', value: txModel); + cachedTxData = txModel; return txModel; } diff --git a/lib/services/coins/epiccash/epiccash_wallet.dart b/lib/services/coins/epiccash/epiccash_wallet.dart index b98854e61..3702a9158 100644 --- a/lib/services/coins/epiccash/epiccash_wallet.dart +++ b/lib/services/coins/epiccash/epiccash_wallet.dart @@ -2259,6 +2259,14 @@ class EpicCashWallet extends CoinServiceAPI { _transactionData ??= _fetchTransactionData(); Future<TransactionData>? _transactionData; + // not used in epic + TransactionData? cachedTxData; + + @override + Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async { + // not used in epic + } + @override Future<List<UtxoObject>> get unspentOutputs => throw UnimplementedError(); diff --git a/lib/services/coins/firo/firo_wallet.dart b/lib/services/coins/firo/firo_wallet.dart index 2a2102e7c..d19c4f1ab 100644 --- a/lib/services/coins/firo/firo_wallet.dart +++ b/lib/services/coins/firo/firo_wallet.dart @@ -908,6 +908,52 @@ class FiroWallet extends CoinServiceAPI { Future<models.TransactionData> get _txnData => _transactionData ??= _fetchTransactionData(); + models.TransactionData? cachedTxData; + + // hack to add tx to txData before refresh completes + // required based on current app architecture where we don't properly store + // transactions locally in a good way + @override + Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async { + final currentPrice = await firoPrice; + final locale = await Devicelocale.currentLocale; + final String worthNow = Format.localizedStringAsFixed( + value: + ((currentPrice * Decimal.fromInt(txData["recipientAmt"] as int)) / + Decimal.fromInt(Constants.satsPerCoin)) + .toDecimal(scaleOnInfinitePrecision: 2), + decimalPlaces: 2, + locale: locale!); + + final tx = models.Transaction( + txid: txData["txid"] as String, + confirmedStatus: false, + timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000, + txType: "Sent", + amount: txData["recipientAmt"] as int, + worthNow: worthNow, + worthAtBlockTimestamp: worthNow, + fees: txData["fee"] as int, + inputSize: 0, + outputSize: 0, + inputs: [], + outputs: [], + address: txData["address"] as String, + height: -1, + confirmations: 0, + ); + + if (cachedTxData == null) { + final data = await _fetchTransactionData(); + _transactionData = Future(() => data); + } + + final transactions = cachedTxData!.getAllTransactions(); + transactions[tx.txid] = tx; + cachedTxData = models.TransactionData.fromMap(transactions); + _transactionData = Future(() => cachedTxData!); + } + /// Holds wallet lelantus transaction data Future<models.TransactionData>? _lelantusTransactionData; Future<models.TransactionData> get lelantusTransactionData => @@ -1110,6 +1156,9 @@ class FiroWallet extends CoinServiceAPI { final txHash = await _electrumXClient.broadcastTransaction( rawTx: txData["hex"] as String); Logging.instance.log("Sent txHash: $txHash", level: LogLevel.Info); + txData["txid"] = txHash; + // dirty ui update hack + await updateSentCachedTxData(txData as Map<String, dynamic>); return txHash; } catch (e, s) { Logging.instance.log("Exception rethrown from confirmSend(): $e\n$s", @@ -3465,6 +3514,7 @@ class FiroWallet extends CoinServiceAPI { await DB.instance.put<dynamic>( boxName: walletId, key: 'latest_tx_model', value: txModel); + cachedTxData = txModel; return txModel; } diff --git a/lib/services/coins/litecoin/litecoin_wallet.dart b/lib/services/coins/litecoin/litecoin_wallet.dart index c07cca1f3..4551325f7 100644 --- a/lib/services/coins/litecoin/litecoin_wallet.dart +++ b/lib/services/coins/litecoin/litecoin_wallet.dart @@ -10,6 +10,7 @@ import 'package:bitcoindart/bitcoindart.dart'; import 'package:bs58check/bs58check.dart' as bs58check; import 'package:crypto/crypto.dart'; import 'package:decimal/decimal.dart'; +import 'package:devicelocale/devicelocale.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:http/http.dart'; @@ -1285,6 +1286,54 @@ class LitecoinWallet extends CoinServiceAPI { _transactionData ??= _fetchTransactionData(); Future<TransactionData>? _transactionData; + TransactionData? cachedTxData; + + // hack to add tx to txData before refresh completes + // required based on current app architecture where we don't properly store + // transactions locally in a good way + @override + Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async { + final priceData = + await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency); + Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero; + final locale = await Devicelocale.currentLocale; + final String worthNow = Format.localizedStringAsFixed( + value: + ((currentPrice * Decimal.fromInt(txData["recipientAmt"] as int)) / + Decimal.fromInt(Constants.satsPerCoin)) + .toDecimal(scaleOnInfinitePrecision: 2), + decimalPlaces: 2, + locale: locale!); + + final tx = models.Transaction( + txid: txData["txid"] as String, + confirmedStatus: false, + timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000, + txType: "Sent", + amount: txData["recipientAmt"] as int, + worthNow: worthNow, + worthAtBlockTimestamp: worthNow, + fees: txData["fee"] as int, + inputSize: 0, + outputSize: 0, + inputs: [], + outputs: [], + address: txData["address"] as String, + height: -1, + confirmations: 0, + ); + + if (cachedTxData == null) { + final data = await _fetchTransactionData(); + _transactionData = Future(() => data); + } + + final transactions = cachedTxData!.getAllTransactions(); + transactions[tx.txid] = tx; + cachedTxData = models.TransactionData.fromMap(transactions); + _transactionData = Future(() => cachedTxData!); + } + @override bool validateAddress(String address) { return Address.validateAddress(address, _network, _network.bech32!); @@ -2673,6 +2722,7 @@ class LitecoinWallet extends CoinServiceAPI { await DB.instance.put<dynamic>( boxName: walletId, key: 'latest_tx_model', value: txModel); + cachedTxData = txModel; return txModel; } diff --git a/lib/services/coins/manager.dart b/lib/services/coins/manager.dart index c8329ec28..8054fe168 100644 --- a/lib/services/coins/manager.dart +++ b/lib/services/coins/manager.dart @@ -108,6 +108,9 @@ class Manager with ChangeNotifier { try { final txid = await _currentWallet.confirmSend(txData: txData); + txData["txid"] = txid; + await _currentWallet.updateSentCachedTxData(txData); + notifyListeners(); return txid; } catch (e) { diff --git a/lib/services/coins/monero/monero_wallet.dart b/lib/services/coins/monero/monero_wallet.dart index 9bcc3515f..662d4077b 100644 --- a/lib/services/coins/monero/monero_wallet.dart +++ b/lib/services/coins/monero/monero_wallet.dart @@ -1190,6 +1190,14 @@ class MoneroWallet extends CoinServiceAPI { _transactionData ??= _fetchTransactionData(); Future<TransactionData>? _transactionData; + // not used in monero + TransactionData? cachedTxData; + + @override + Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async { + // not used in monero + } + Future<TransactionData> _fetchTransactionData() async { final transactions = walletBase?.transactionHistory!.transactions; diff --git a/lib/services/coins/namecoin/namecoin_wallet.dart b/lib/services/coins/namecoin/namecoin_wallet.dart index 893db69e0..8a4b26012 100644 --- a/lib/services/coins/namecoin/namecoin_wallet.dart +++ b/lib/services/coins/namecoin/namecoin_wallet.dart @@ -10,6 +10,7 @@ import 'package:bitcoindart/bitcoindart.dart'; import 'package:bs58check/bs58check.dart' as bs58check; import 'package:crypto/crypto.dart'; import 'package:decimal/decimal.dart'; +import 'package:devicelocale/devicelocale.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:http/http.dart'; @@ -1276,6 +1277,54 @@ class NamecoinWallet extends CoinServiceAPI { _transactionData ??= _fetchTransactionData(); Future<TransactionData>? _transactionData; + TransactionData? cachedTxData; + + // hack to add tx to txData before refresh completes + // required based on current app architecture where we don't properly store + // transactions locally in a good way + @override + Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async { + final priceData = + await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency); + Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero; + final locale = await Devicelocale.currentLocale; + final String worthNow = Format.localizedStringAsFixed( + value: + ((currentPrice * Decimal.fromInt(txData["recipientAmt"] as int)) / + Decimal.fromInt(Constants.satsPerCoin)) + .toDecimal(scaleOnInfinitePrecision: 2), + decimalPlaces: 2, + locale: locale!); + + final tx = models.Transaction( + txid: txData["txid"] as String, + confirmedStatus: false, + timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000, + txType: "Sent", + amount: txData["recipientAmt"] as int, + worthNow: worthNow, + worthAtBlockTimestamp: worthNow, + fees: txData["fee"] as int, + inputSize: 0, + outputSize: 0, + inputs: [], + outputs: [], + address: txData["address"] as String, + height: -1, + confirmations: 0, + ); + + if (cachedTxData == null) { + final data = await _fetchTransactionData(); + _transactionData = Future(() => data); + } + + final transactions = cachedTxData!.getAllTransactions(); + transactions[tx.txid] = tx; + cachedTxData = models.TransactionData.fromMap(transactions); + _transactionData = Future(() => cachedTxData!); + } + @override bool validateAddress(String address) { return Address.validateAddress(address, _network, namecoin.bech32!); @@ -2673,6 +2722,7 @@ class NamecoinWallet extends CoinServiceAPI { await DB.instance.put<dynamic>( boxName: walletId, key: 'latest_tx_model', value: txModel); + cachedTxData = txModel; return txModel; } diff --git a/lib/services/coins/wownero/wownero_wallet.dart b/lib/services/coins/wownero/wownero_wallet.dart index 342e5d84a..72f43eac8 100644 --- a/lib/services/coins/wownero/wownero_wallet.dart +++ b/lib/services/coins/wownero/wownero_wallet.dart @@ -1195,6 +1195,14 @@ class WowneroWallet extends CoinServiceAPI { _transactionData ??= _fetchTransactionData(); Future<TransactionData>? _transactionData; + // not used in wownero + TransactionData? cachedTxData; + + @override + Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async { + // not used in wownero + } + Future<TransactionData> _fetchTransactionData() async { final transactions = walletBase?.transactionHistory!.transactions; diff --git a/test/services/coins/fake_coin_service_api.dart b/test/services/coins/fake_coin_service_api.dart index a3ae28a4b..c5f300c16 100644 --- a/test/services/coins/fake_coin_service_api.dart +++ b/test/services/coins/fake_coin_service_api.dart @@ -182,4 +182,10 @@ class FakeCoinServiceAPI extends CoinServiceAPI { // TODO: implement generateNewAddress throw UnimplementedError(); } + + @override + Future<void> updateSentCachedTxData(Map<String, dynamic> txData) { + // TODO: implement updateSentCachedTxData + throw UnimplementedError(); + } } From c88971ebd6eaf7e278591eef2925c1fe6b8a2081 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 7 Nov 2022 11:46:17 -0600 Subject: [PATCH 04/22] firo pub/priv balance send from choice on exchange flow --- .../confirm_change_now_send.dart | 13 +- lib/pages/exchange_view/send_from_view.dart | 559 ++++++++++++------ 2 files changed, 404 insertions(+), 168 deletions(-) diff --git a/lib/pages/exchange_view/confirm_change_now_send.dart b/lib/pages/exchange_view/confirm_change_now_send.dart index d77ad6b8c..e99cf2df4 100644 --- a/lib/pages/exchange_view/confirm_change_now_send.dart +++ b/lib/pages/exchange_view/confirm_change_now_send.dart @@ -10,6 +10,7 @@ import 'package:stackwallet/pages/wallet_view/wallet_view.dart'; import 'package:stackwallet/providers/exchange/trade_sent_from_stack_lookup_provider.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/route_generator.dart'; +import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; @@ -27,6 +28,7 @@ class ConfirmChangeNowSendView extends ConsumerStatefulWidget { required this.walletId, this.routeOnSuccessName = WalletView.routeName, required this.trade, + this.shouldSendPublicFiroFunds, }) : super(key: key); static const String routeName = "/confirmChangeNowSend"; @@ -35,6 +37,7 @@ class ConfirmChangeNowSendView extends ConsumerStatefulWidget { final String walletId; final String routeOnSuccessName; final Trade trade; + final bool? shouldSendPublicFiroFunds; @override ConsumerState<ConfirmChangeNowSendView> createState() => @@ -63,7 +66,15 @@ class _ConfirmChangeNowSendViewState ref.read(walletsChangeNotifierProvider).getManager(walletId); try { - final txid = await manager.confirmSend(txData: transactionInfo); + late final String txid; + + if (widget.shouldSendPublicFiroFunds == true) { + txid = await (manager.wallet as FiroWallet) + .confirmSendPublic(txData: transactionInfo); + } else { + txid = await manager.confirmSend(txData: transactionInfo); + } + unawaited(manager.refresh()); // save note diff --git a/lib/pages/exchange_view/send_from_view.dart b/lib/pages/exchange_view/send_from_view.dart index 20fc81903..c87175955 100644 --- a/lib/pages/exchange_view/send_from_view.dart +++ b/lib/pages/exchange_view/send_from_view.dart @@ -10,6 +10,8 @@ import 'package:stackwallet/pages/home_view/home_view.dart'; import 'package:stackwallet/pages/send_view/sub_widgets/building_transaction_dialog.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/route_generator.dart'; +import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; +import 'package:stackwallet/services/coins/manager.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; @@ -18,7 +20,9 @@ import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/animated_text.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/expandable.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; @@ -162,6 +166,130 @@ class _SendFromCardState extends ConsumerState<SendFromCard> { late final String address; late final Trade trade; + Future<void> _send(Manager manager, {bool? shouldSendPublicFiroFunds}) async { + final _amount = Format.decimalAmountToSatoshis(amount); + + try { + bool wasCancelled = false; + + unawaited( + showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: false, + builder: (context) { + return BuildingTransactionDialog( + onCancel: () { + wasCancelled = true; + + Navigator.of(context).pop(); + }, + ); + }, + ), + ); + + late Map<String, dynamic> txData; + + // if not firo then do normal send + if (shouldSendPublicFiroFunds == null) { + txData = await manager.prepareSend( + address: address, + satoshiAmount: _amount, + args: { + "feeRate": FeeRateType.average, + // ref.read(feeRateTypeStateProvider) + }, + ); + } else { + final firoWallet = manager.wallet as FiroWallet; + // otherwise do firo send based on balance selected + if (shouldSendPublicFiroFunds) { + txData = await firoWallet.prepareSendPublic( + address: address, + satoshiAmount: _amount, + args: { + "feeRate": FeeRateType.average, + // ref.read(feeRateTypeStateProvider) + }, + ); + } else { + txData = await firoWallet.prepareSend( + address: address, + satoshiAmount: _amount, + args: { + "feeRate": FeeRateType.average, + // ref.read(feeRateTypeStateProvider) + }, + ); + } + } + + if (!wasCancelled) { + // pop building dialog + + if (mounted) { + Navigator.of(context).pop(); + } + + txData["note"] = + "${trade.payInCurrency.toUpperCase()}/${trade.payOutCurrency.toUpperCase()} exchange"; + txData["address"] = address; + + if (mounted) { + await Navigator.of(context).push( + RouteGenerator.getRoute( + shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, + builder: (_) => ConfirmChangeNowSendView( + transactionInfo: txData, + walletId: walletId, + routeOnSuccessName: HomeView.routeName, + trade: trade, + shouldSendPublicFiroFunds: shouldSendPublicFiroFunds, + ), + settings: const RouteSettings( + name: ConfirmChangeNowSendView.routeName, + ), + ), + ); + } + } + } catch (e) { + // if (mounted) { + // pop building dialog + Navigator.of(context).pop(); + + await showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return StackDialog( + title: "Transaction failed", + message: e.toString(), + rightButton: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getSecondaryEnabledButtonColor(context), + child: Text( + "Ok", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondary, + ), + ), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ); + }, + ); + // } + } + } + @override void initState() { walletId = widget.walletId; @@ -182,181 +310,278 @@ class _SendFromCardState extends ConsumerState<SendFromCard> { final coin = manager.coin; + final isFiro = coin == Coin.firoTestNet || coin == Coin.firo; + return RoundedWhiteContainer( padding: const EdgeInsets.all(0), - child: MaterialButton( - splashColor: Theme.of(context).extension<StackColors>()!.highlight, - key: Key("walletsSheetItemButtonKey_$walletId"), - padding: const EdgeInsets.all(8), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + child: ConditionalParent( + condition: isFiro, + builder: (child) => Expandable( + header: Container( + color: Colors.transparent, + child: Padding( + padding: const EdgeInsets.all(12), + child: child, + ), + ), + body: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MaterialButton( + splashColor: + Theme.of(context).extension<StackColors>()!.highlight, + key: Key("walletsSheetItemButtonFiroPrivateKey_$walletId"), + padding: const EdgeInsets.all(0), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () => _send( + manager, + shouldSendPublicFiroFunds: false, + ), + child: Container( + color: Colors.transparent, + child: Padding( + padding: const EdgeInsets.only( + top: 6, + left: 16, + right: 16, + bottom: 6, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Use private balance", + style: STextStyles.itemSubtitle(context), + ), + FutureBuilder( + future: (manager.wallet as FiroWallet) + .availablePrivateBalance(), + builder: (builderContext, + AsyncSnapshot<Decimal> snapshot) { + if (snapshot.connectionState == + ConnectionState.done && + snapshot.hasData) { + return Text( + "${Format.localizedStringAsFixed( + value: snapshot.data!, + locale: locale, + decimalPlaces: Constants.decimalPlaces, + )} ${coin.ticker}", + style: STextStyles.itemSubtitle(context), + ); + } else { + return AnimatedText( + stringsToLoopThrough: const [ + "Loading balance", + "Loading balance.", + "Loading balance..", + "Loading balance..." + ], + style: STextStyles.itemSubtitle(context), + ); + } + }, + ), + ], + ), + SvgPicture.asset( + Assets.svg.chevronRight, + height: 14, + width: 7, + color: Theme.of(context) + .extension<StackColors>()! + .infoItemLabel, + ), + ], + ), + ), + ), + ), + MaterialButton( + splashColor: + Theme.of(context).extension<StackColors>()!.highlight, + key: Key("walletsSheetItemButtonFiroPublicKey_$walletId"), + padding: const EdgeInsets.all(0), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () => _send( + manager, + shouldSendPublicFiroFunds: true, + ), + child: Container( + color: Colors.transparent, + child: Padding( + padding: const EdgeInsets.only( + top: 6, + left: 16, + right: 16, + bottom: 6, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Use public balance", + style: STextStyles.itemSubtitle(context), + ), + FutureBuilder( + future: (manager.wallet as FiroWallet) + .availablePublicBalance(), + builder: (builderContext, + AsyncSnapshot<Decimal> snapshot) { + if (snapshot.connectionState == + ConnectionState.done && + snapshot.hasData) { + return Text( + "${Format.localizedStringAsFixed( + value: snapshot.data!, + locale: locale, + decimalPlaces: Constants.decimalPlaces, + )} ${coin.ticker}", + style: STextStyles.itemSubtitle(context), + ); + } else { + return AnimatedText( + stringsToLoopThrough: const [ + "Loading balance", + "Loading balance.", + "Loading balance..", + "Loading balance..." + ], + style: STextStyles.itemSubtitle(context), + ); + } + }, + ), + ], + ), + SvgPicture.asset( + Assets.svg.chevronRight, + height: 14, + width: 7, + color: Theme.of(context) + .extension<StackColors>()! + .infoItemLabel, + ), + ], + ), + ), + ), + ), + const SizedBox( + height: 6, + ), + ], ), ), - onPressed: () async { - final _amount = Format.decimalAmountToSatoshis(amount); - - try { - bool wasCancelled = false; - - unawaited(showDialog<dynamic>( - context: context, - useSafeArea: false, - barrierDismissible: false, - builder: (context) { - return BuildingTransactionDialog( - onCancel: () { - wasCancelled = true; - - Navigator.of(context).pop(); - }, - ); - }, - )); - - final txData = await manager.prepareSend( - address: address, - satoshiAmount: _amount, - args: { - "feeRate": FeeRateType.average, - // ref.read(feeRateTypeStateProvider) - }, - ); - - if (!wasCancelled) { - // pop building dialog - - if (mounted) { - Navigator.of(context).pop(); - } - - txData["note"] = - "${trade.payInCurrency.toUpperCase()}/${trade.payOutCurrency.toUpperCase()} exchange"; - txData["address"] = address; - - if (mounted) { - await Navigator.of(context).push( - RouteGenerator.getRoute( - shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, - builder: (_) => ConfirmChangeNowSendView( - transactionInfo: txData, - walletId: walletId, - routeOnSuccessName: HomeView.routeName, - trade: trade, - ), - settings: const RouteSettings( - name: ConfirmChangeNowSendView.routeName, - ), + child: ConditionalParent( + condition: !isFiro, + builder: (child) => MaterialButton( + splashColor: Theme.of(context).extension<StackColors>()!.highlight, + key: Key("walletsSheetItemButtonKey_$walletId"), + padding: const EdgeInsets.all(8), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () => _send(manager), + child: child, + ), + child: Row( + children: [ + Container( + decoration: BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .colorForCoin(manager.coin) + .withOpacity(0.5), + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), - ); - } - } - } catch (e) { - // if (mounted) { - // pop building dialog - Navigator.of(context).pop(); - - await showDialog<dynamic>( - context: context, - useSafeArea: false, - barrierDismissible: true, - builder: (context) { - return StackDialog( - title: "Transaction failed", - message: e.toString(), - rightButton: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor(context), - child: Text( - "Ok", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .buttonTextSecondary, + ), + child: Padding( + padding: const EdgeInsets.all(6), + child: SvgPicture.asset( + Assets.svg.iconFor(coin: coin), + width: 24, + height: 24, + ), + ), + ), + const SizedBox( + width: 12, + ), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + manager.walletName, + style: STextStyles.titleBold12(context), + ), + if (!isFiro) + const SizedBox( + height: 2, ), - ), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - ); - }, - ); - // } - } - }, - child: Row( - children: [ - Container( - decoration: BoxDecoration( - color: Theme.of(context) - .extension<StackColors>()! - .colorForCoin(manager.coin) - .withOpacity(0.5), - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + if (!isFiro) + FutureBuilder( + future: manager.totalBalance, + builder: + (builderContext, AsyncSnapshot<Decimal> snapshot) { + if (snapshot.connectionState == + ConnectionState.done && + snapshot.hasData) { + return Text( + "${Format.localizedStringAsFixed( + value: snapshot.data!, + locale: locale, + decimalPlaces: coin == Coin.monero + ? Constants.decimalPlacesMonero + : coin == Coin.wownero + ? Constants.decimalPlacesWownero + : Constants.decimalPlaces, + )} ${coin.ticker}", + style: STextStyles.itemSubtitle(context), + ); + } else { + return AnimatedText( + stringsToLoopThrough: const [ + "Loading balance", + "Loading balance.", + "Loading balance..", + "Loading balance..." + ], + style: STextStyles.itemSubtitle(context), + ); + } + }, + ), + ], ), ), - child: Padding( - padding: const EdgeInsets.all(6), - child: SvgPicture.asset( - Assets.svg.iconFor(coin: coin), - width: 24, - height: 24, - ), - ), - ), - const SizedBox( - width: 12, - ), - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - manager.walletName, - style: STextStyles.titleBold12(context), - ), - const SizedBox( - height: 2, - ), - FutureBuilder( - future: manager.totalBalance, - builder: (builderContext, AsyncSnapshot<Decimal> snapshot) { - if (snapshot.connectionState == ConnectionState.done && - snapshot.hasData) { - return Text( - "${Format.localizedStringAsFixed( - value: snapshot.data!, - locale: locale, - decimalPlaces: coin == Coin.monero - ? Constants.decimalPlacesMonero - : coin == Coin.wownero - ? Constants.decimalPlacesWownero - : Constants.decimalPlaces, - )} ${coin.ticker}", - style: STextStyles.itemSubtitle(context), - ); - } else { - return AnimatedText( - stringsToLoopThrough: const [ - "Loading balance", - "Loading balance.", - "Loading balance..", - "Loading balance..." - ], - style: STextStyles.itemSubtitle(context), - ); - } - }, - ), - ], - ), - ), - ], + ], + ), ), ), ); From 9fe9ee3a1236011d2e6c96147af82b1aa8874390 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Mon, 7 Nov 2022 10:42:55 -0700 Subject: [PATCH 05/22] desktop about view route added --- lib/pages_desktop_specific/home/desktop_home_view.dart | 7 +++++-- .../home/support_and_about_view/desktop_about_view.dart | 0 lib/route_generator.dart | 7 +++++++ 3 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 lib/pages_desktop_specific/home/support_and_about_view/desktop_about_view.dart diff --git a/lib/pages_desktop_specific/home/desktop_home_view.dart b/lib/pages_desktop_specific/home/desktop_home_view.dart index e9f6f2b4b..14d2dae03 100644 --- a/lib/pages_desktop_specific/home/desktop_home_view.dart +++ b/lib/pages_desktop_specific/home/desktop_home_view.dart @@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/pages_desktop_specific/home/desktop_menu.dart'; import 'package:stackwallet/pages_desktop_specific/home/desktop_settings_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/my_stack_view.dart'; +import 'package:stackwallet/pages_desktop_specific/home/support_and_about_view/desktop_about_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/support_and_about_view/desktop_support_view.dart'; import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; @@ -43,8 +44,10 @@ class _DesktopHomeViewState extends ConsumerState<DesktopHomeView> { onGenerateRoute: RouteGenerator.generateRoute, initialRoute: DesktopSupportView.routeName, ), - Container( - color: Colors.pink, + const Navigator( + key: Key("desktopAboutHomeKey"), + onGenerateRoute: RouteGenerator.generateRoute, + initialRoute: DesktopAboutView.routeName, ), ]; diff --git a/lib/pages_desktop_specific/home/support_and_about_view/desktop_about_view.dart b/lib/pages_desktop_specific/home/support_and_about_view/desktop_about_view.dart new file mode 100644 index 000000000..e69de29bb diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 47a84f07c..40f11dc57 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -99,6 +99,7 @@ import 'package:stackwallet/pages_desktop_specific/home/settings_menu/nodes_sett import 'package:stackwallet/pages_desktop_specific/home/settings_menu/security_settings.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/settings_menu.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/syncing_preferences_settings.dart'; +import 'package:stackwallet/pages_desktop_specific/home/support_and_about_view/desktop_about_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/support_and_about_view/desktop_support_view.dart'; import 'package:stackwallet/services/coins/manager.dart'; import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart'; @@ -1091,6 +1092,12 @@ class RouteGenerator { builder: (_) => const DesktopSupportView(), settings: RouteSettings(name: settings.name)); + case DesktopAboutView.routeName: + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const DesktopAboutView(), + settings: RouteSettings(name: settings.name)); + case WalletKeysDesktopPopup.routeName: if (args is List<String>) { return FadePageRoute( From 48a0e3a5ca89528a1bdca69e8c027967c7957e1d Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Mon, 7 Nov 2022 12:31:49 -0700 Subject: [PATCH 06/22] desktop about view added --- .../desktop_about_view.dart | 655 ++++++++++++++++++ 1 file changed, 655 insertions(+) diff --git a/lib/pages_desktop_specific/home/support_and_about_view/desktop_about_view.dart b/lib/pages_desktop_specific/home/support_and_about_view/desktop_about_view.dart index e69de29bb..86fcf78e0 100644 --- a/lib/pages_desktop_specific/home/support_and_about_view/desktop_about_view.dart +++ b/lib/pages_desktop_specific/home/support_and_about_view/desktop_about_view.dart @@ -0,0 +1,655 @@ +import 'dart:convert'; + +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_libepiccash/git_versions.dart' as EPIC_VERSIONS; +import 'package:flutter_libmonero/git_versions.dart' as MONERO_VERSIONS; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:http/http.dart'; +import 'package:lelantus/git_versions.dart' as FIRO_VERSIONS; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; +import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; +import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; +import 'package:url_launcher/url_launcher.dart'; + +const kGithubAPI = "https://api.github.com"; +const kGithubSearch = "/search/commits"; +const kGithubHead = "/repos"; + +enum CommitStatus { isHead, isOldCommit, notACommit, notLoaded } + +Future<bool> doesCommitExist( + String organization, + String project, + String commit, +) async { + Logging.instance.log("doesCommitExist", level: LogLevel.Info); + final Client client = Client(); + try { + final uri = Uri.parse( + "$kGithubAPI$kGithubHead/$organization/$project/commits/$commit"); + + final commitQuery = await client.get( + uri, + headers: {'Content-Type': 'application/json'}, + ); + + final response = jsonDecode(commitQuery.body.toString()); + Logging.instance.log("doesCommitExist $project $commit $response", + level: LogLevel.Info); + bool isThereCommit; + try { + isThereCommit = response['sha'] == commit; + Logging.instance + .log("isThereCommit $isThereCommit", level: LogLevel.Info); + return isThereCommit; + } catch (e, s) { + return false; + } + } catch (e, s) { + Logging.instance.log("$e $s", level: LogLevel.Error); + return false; + } +} + +Future<bool> isHeadCommit( + String organization, + String project, + String branch, + String commit, +) async { + Logging.instance.log("doesCommitExist", level: LogLevel.Info); + final Client client = Client(); + try { + final uri = Uri.parse( + "$kGithubAPI$kGithubHead/$organization/$project/commits/$branch"); + + final commitQuery = await client.get( + uri, + headers: {'Content-Type': 'application/json'}, + ); + + final response = jsonDecode(commitQuery.body.toString()); + Logging.instance.log("isHeadCommit $project $commit $branch $response", + level: LogLevel.Info); + bool isHead; + try { + isHead = response['sha'] == commit; + Logging.instance.log("isHead $isHead", level: LogLevel.Info); + return isHead; + } catch (e, s) { + return false; + } + } catch (e, s) { + Logging.instance.log("$e $s", level: LogLevel.Error); + return false; + } +} + +class DesktopAboutView extends ConsumerWidget { + const DesktopAboutView({Key? key}) : super(key: key); + + static const String routeName = "/desktopAboutView"; + + @override + Widget build(BuildContext context, WidgetRef ref) { + String firoCommit = FIRO_VERSIONS.getPluginVersion(); + String epicCashCommit = EPIC_VERSIONS.getPluginVersion(); + String moneroCommit = MONERO_VERSIONS.getPluginVersion(); + List<Future> futureFiroList = [ + doesCommitExist("cypherstack", "flutter_liblelantus", firoCommit), + isHeadCommit("cypherstack", "flutter_liblelantus", "main", firoCommit), + ]; + Future commitFiroFuture = Future.wait(futureFiroList); + List<Future> futureEpicList = [ + doesCommitExist("cypherstack", "flutter_libepiccash", epicCashCommit), + isHeadCommit( + "cypherstack", "flutter_libepiccash", "main", epicCashCommit), + ]; + Future commitEpicFuture = Future.wait(futureEpicList); + List<Future> futureMoneroList = [ + doesCommitExist("cypherstack", "flutter_libmonero", moneroCommit), + isHeadCommit("cypherstack", "flutter_libmonero", "main", moneroCommit), + ]; + Future commitMoneroFuture = Future.wait(futureMoneroList); + + debugPrint("BUILD: $runtimeType"); + return DesktopScaffold( + background: Theme.of(context).extension<StackColors>()!.background, + appBar: DesktopAppBar( + isCompactHeight: true, + leading: Row( + children: [ + const SizedBox( + width: 24, + height: 24, + ), + Text( + "About", + style: STextStyles.desktopH3(context), + ) + ], + ), + ), + body: Column( + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(24, 10, 24, 35), + child: RoundedWhiteContainer( + width: 929, + height: 411, + child: Padding( + padding: const EdgeInsets.only(left: 10, top: 10), + child: Column( + // mainAxisAlignment: MainAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text( + "Stack Wallet", + style: STextStyles.desktopH3(context), + textAlign: TextAlign.start, + ), + ], + ), + const SizedBox(height: 16), + Row( + children: [ + RichText( + textAlign: TextAlign.start, + text: TextSpan( + style: STextStyles.label(context), + children: [ + TextSpan( + text: + "By using Stack Wallet, you agree to the ", + style: STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark3), + ), + TextSpan( + text: "Terms of service", + style: STextStyles.richLink(context) + .copyWith(fontSize: 14), + recognizer: TapGestureRecognizer() + ..onTap = () { + launchUrl( + Uri.parse( + "https://stackwallet.com/terms-of-service.html"), + mode: LaunchMode.externalApplication, + ); + }, + ), + TextSpan( + text: " and ", + style: STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark3), + ), + TextSpan( + text: "Privacy policy", + style: STextStyles.richLink(context) + .copyWith(fontSize: 14), + recognizer: TapGestureRecognizer() + ..onTap = () { + launchUrl( + Uri.parse( + "https://stackwallet.com/privacy-policy.html"), + mode: LaunchMode.externalApplication, + ); + }, + ), + ], + ), + ), + ], + ), + const SizedBox(height: 32), + Padding( + padding: const EdgeInsets.only(right: 10, bottom: 10), + child: Column( + children: [ + FutureBuilder( + future: PackageInfo.fromPlatform(), + builder: + (context, AsyncSnapshot<PackageInfo> snapshot) { + String version = ""; + String signature = ""; + String build = ""; + + if (snapshot.connectionState == + ConnectionState.done && + snapshot.hasData) { + version = snapshot.data!.version; + build = snapshot.data!.buildNumber; + signature = snapshot.data!.buildSignature; + } + + return Column( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Version", + style: STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension< + StackColors>()! + .textDark), + ), + const SizedBox( + height: 2, + ), + SelectableText( + version, + style: STextStyles.itemSubtitle( + context), + ), + ], + ), + const SizedBox( + width: 400, + ), + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Build number", + style: STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension< + StackColors>()! + .textDark), + ), + const SizedBox( + height: 2, + ), + SelectableText( + build, + style: STextStyles.itemSubtitle( + context), + ), + ], + ), + ], + ), + const SizedBox(height: 32), + Row( + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Build signature", + style: STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension< + StackColors>()! + .textDark), + ), + const SizedBox( + height: 2, + ), + SelectableText( + signature, + style: STextStyles.itemSubtitle( + context), + ), + ], + ), + const SizedBox( + width: 350, + ), + FutureBuilder( + future: commitFiroFuture, + builder: (context, + AsyncSnapshot<dynamic> snapshot) { + bool commitExists = false; + bool isHead = false; + CommitStatus stateOfCommit = + CommitStatus.notLoaded; + + if (snapshot.connectionState == + ConnectionState.done && + snapshot.hasData) { + commitExists = + snapshot.data![0] as bool; + isHead = + snapshot.data![1] as bool; + if (commitExists && isHead) { + stateOfCommit = + CommitStatus.isHead; + } else if (commitExists) { + stateOfCommit = + CommitStatus.isOldCommit; + } else { + stateOfCommit = + CommitStatus.notACommit; + } + } + TextStyle indicationStyle = + STextStyles.itemSubtitle( + context); + switch (stateOfCommit) { + case CommitStatus.isHead: + indicationStyle = STextStyles + .itemSubtitle(context) + .copyWith( + color: Theme.of(context) + .extension< + StackColors>()! + .accentColorGreen); + break; + case CommitStatus.isOldCommit: + indicationStyle = STextStyles + .itemSubtitle(context) + .copyWith( + color: Theme.of(context) + .extension< + StackColors>()! + .accentColorYellow); + break; + case CommitStatus.notACommit: + indicationStyle = STextStyles + .itemSubtitle(context) + .copyWith( + color: Theme.of(context) + .extension< + StackColors>()! + .accentColorRed); + break; + default: + break; + } + return Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Firo Build Commit", + style: STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of( + context) + .extension< + StackColors>()! + .textDark), + ), + const SizedBox( + height: 2, + ), + SelectableText( + firoCommit, + style: indicationStyle, + ), + ], + ); + }), + ], + ), + const SizedBox(height: 35), + Row( + children: [ + FutureBuilder( + future: commitEpicFuture, + builder: (context, + AsyncSnapshot<dynamic> snapshot) { + bool commitExists = false; + bool isHead = false; + CommitStatus stateOfCommit = + CommitStatus.notLoaded; + + if (snapshot.connectionState == + ConnectionState.done && + snapshot.hasData) { + commitExists = + snapshot.data![0] as bool; + isHead = + snapshot.data![1] as bool; + if (commitExists && isHead) { + stateOfCommit = + CommitStatus.isHead; + } else if (commitExists) { + stateOfCommit = + CommitStatus.isOldCommit; + } else { + stateOfCommit = + CommitStatus.notACommit; + } + } + TextStyle indicationStyle = + STextStyles.itemSubtitle( + context); + switch (stateOfCommit) { + case CommitStatus.isHead: + indicationStyle = STextStyles + .itemSubtitle(context) + .copyWith( + color: Theme.of(context) + .extension< + StackColors>()! + .accentColorGreen); + break; + case CommitStatus.isOldCommit: + indicationStyle = STextStyles + .itemSubtitle(context) + .copyWith( + color: Theme.of(context) + .extension< + StackColors>()! + .accentColorYellow); + break; + case CommitStatus.notACommit: + indicationStyle = STextStyles + .itemSubtitle(context) + .copyWith( + color: Theme.of(context) + .extension< + StackColors>()! + .accentColorRed); + break; + default: + break; + } + return Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Epic Cash Build Commit", + style: STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of( + context) + .extension< + StackColors>()! + .textDark), + ), + const SizedBox( + height: 2, + ), + SelectableText( + epicCashCommit, + style: indicationStyle, + ), + ], + ); + }), + const SizedBox( + width: 105, + ), + FutureBuilder( + future: commitMoneroFuture, + builder: (context, + AsyncSnapshot<dynamic> snapshot) { + bool commitExists = false; + bool isHead = false; + CommitStatus stateOfCommit = + CommitStatus.notLoaded; + + if (snapshot.connectionState == + ConnectionState.done && + snapshot.hasData) { + commitExists = + snapshot.data![0] as bool; + isHead = + snapshot.data![1] as bool; + if (commitExists && isHead) { + stateOfCommit = + CommitStatus.isHead; + } else if (commitExists) { + stateOfCommit = + CommitStatus.isOldCommit; + } else { + stateOfCommit = + CommitStatus.notACommit; + } + } + TextStyle indicationStyle = + STextStyles.itemSubtitle( + context); + switch (stateOfCommit) { + case CommitStatus.isHead: + indicationStyle = STextStyles + .itemSubtitle(context) + .copyWith( + color: Theme.of(context) + .extension< + StackColors>()! + .accentColorGreen); + break; + case CommitStatus.isOldCommit: + indicationStyle = STextStyles + .itemSubtitle(context) + .copyWith( + color: Theme.of(context) + .extension< + StackColors>()! + .accentColorYellow); + break; + case CommitStatus.notACommit: + indicationStyle = STextStyles + .itemSubtitle(context) + .copyWith( + color: Theme.of(context) + .extension< + StackColors>()! + .accentColorRed); + break; + default: + break; + } + return Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Monero Build Commit", + style: STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of( + context) + .extension< + StackColors>()! + .textDark), + ), + const SizedBox( + height: 2, + ), + SelectableText( + moneroCommit, + style: indicationStyle, + ), + ], + ); + }), + ], + ), + const SizedBox(height: 35), + Row( + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Website", + style: STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension< + StackColors>()! + .textDark), + ), + const SizedBox( + height: 2, + ), + BlueTextButton( + text: "https://stackwallet.com", + onTap: () { + launchUrl( + Uri.parse( + "https://stackwallet.com"), + mode: LaunchMode + .externalApplication, + ); + }, + ), + ], + ) + ], + ) + ], + ); + }, + ) + ], + ), + ) + ], + ), + ), + ), + ), + ], + ), + ); + } +} From c962f597fdc9c696edbbdac655172ab9f5e217e7 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 7 Nov 2022 14:46:36 -0600 Subject: [PATCH 07/22] added extra checks to BCH as well as test cases --- .../coins/bitcoincash/bitcoincash_wallet.dart | 58 ++++--- .../bitcoincash/bitcoincash_wallet_test.dart | 164 +++++++++++++++++- 2 files changed, 199 insertions(+), 23 deletions(-) diff --git a/lib/services/coins/bitcoincash/bitcoincash_wallet.dart b/lib/services/coins/bitcoincash/bitcoincash_wallet.dart index 98a31ee0c..5b3b54663 100644 --- a/lib/services/coins/bitcoincash/bitcoincash_wallet.dart +++ b/lib/services/coins/bitcoincash/bitcoincash_wallet.dart @@ -208,9 +208,9 @@ class BitcoinCashWallet extends CoinServiceAPI { _getCurrentAddressForChain(0, DerivePathType.bip44); Future<String>? _currentReceivingAddressP2PKH; - Future<String> get currentReceivingAddressP2SH => - _currentReceivingAddressP2SH ??= - _getCurrentAddressForChain(0, DerivePathType.bip49); + // Future<String> get currentReceivingAddressP2SH => + // _currentReceivingAddressP2SH ??= + // _getCurrentAddressForChain(0, DerivePathType.bip49); Future<String>? _currentReceivingAddressP2SH; @override @@ -269,7 +269,11 @@ class BitcoinCashWallet extends CoinServiceAPI { try { if (bitbox.Address.detectFormat(address) == bitbox.Address.formatCashAddr) { - address = bitbox.Address.toLegacyAddress(address); + if (validateCashAddr(address)) { + address = bitbox.Address.toLegacyAddress(address); + } else { + throw ArgumentError('$address is not currently supported'); + } } } catch (e, s) {} try { @@ -294,11 +298,14 @@ class BitcoinCashWallet extends CoinServiceAPI { } catch (err) { // Bech32 decode fail } - if (_network.bech32 != decodeBech32!.hrp) { - throw ArgumentError('Invalid prefix or Network mismatch'); - } - if (decodeBech32.version != 0) { - throw ArgumentError('Invalid address version'); + + if (decodeBech32 != null) { + if (_network.bech32 != decodeBech32.hrp) { + throw ArgumentError('Invalid prefix or Network mismatch'); + } + if (decodeBech32.version != 0) { + throw ArgumentError('Invalid address version'); + } } } throw ArgumentError('$address has no matching Script'); @@ -1203,6 +1210,15 @@ class BitcoinCashWallet extends CoinServiceAPI { _transactionData = Future(() => cachedTxData!); } + bool validateCashAddr(String cashAddr) { + String addr = cashAddr; + if (cashAddr.contains(":")) { + addr = cashAddr.split(":").last; + } + + return addr.startsWith("q"); + } + @override bool validateAddress(String address) { try { @@ -1217,12 +1233,7 @@ class BitcoinCashWallet extends CoinServiceAPI { } if (format == bitbox.Address.formatCashAddr) { - String addr = address; - if (address.contains(":")) { - addr = address.split(":").last; - } - - return addr.startsWith("q"); + return validateCashAddr(address); } else { return address.startsWith("1"); } @@ -2085,7 +2096,8 @@ class BitcoinCashWallet extends CoinServiceAPI { String _convertToScriptHash(String bchAddress, NetworkType network) { try { if (bitbox.Address.detectFormat(bchAddress) == - bitbox.Address.formatCashAddr) { + bitbox.Address.formatCashAddr && + validateCashAddr(bchAddress)) { bchAddress = bitbox.Address.toLegacyAddress(bchAddress); } final output = Address.addressToOutputScript(bchAddress, network); @@ -2163,7 +2175,8 @@ class BitcoinCashWallet extends CoinServiceAPI { List<String> allAddressesOld = await _fetchAllOwnAddresses(); List<String> allAddresses = []; for (String address in allAddressesOld) { - if (bitbox.Address.detectFormat(address) == bitbox.Address.formatLegacy) { + if (bitbox.Address.detectFormat(address) == bitbox.Address.formatLegacy && + addressType(address: address) == DerivePathType.bip44) { allAddresses.add(bitbox.Address.toCashAddress(address)); } else { allAddresses.add(address); @@ -2882,7 +2895,12 @@ class BitcoinCashWallet extends CoinServiceAPI { String address = output["scriptPubKey"]["addresses"][0] as String; if (bitbox.Address.detectFormat(address) == bitbox.Address.formatCashAddr) { - address = bitbox.Address.toLegacyAddress(address); + if (validateCashAddr(address)) { + address = bitbox.Address.toLegacyAddress(address); + } else { + throw Exception( + "Unsupported address found during fetchBuildTxData(): $address"); + } } if (!addressTxid.containsKey(address)) { addressTxid[address] = <String>[]; @@ -2913,10 +2931,6 @@ class BitcoinCashWallet extends CoinServiceAPI { ); for (int i = 0; i < p2pkhLength; i++) { String address = addressesP2PKH[i]; - if (bitbox.Address.detectFormat(address) == - bitbox.Address.formatCashAddr) { - address = bitbox.Address.toLegacyAddress(address); - } // receives final receiveDerivation = receiveDerivations[address]; diff --git a/test/services/coins/bitcoincash/bitcoincash_wallet_test.dart b/test/services/coins/bitcoincash/bitcoincash_wallet_test.dart index 50ff8f741..077163809 100644 --- a/test/services/coins/bitcoincash/bitcoincash_wallet_test.dart +++ b/test/services/coins/bitcoincash/bitcoincash_wallet_test.dart @@ -60,7 +60,7 @@ void main() { }); }); - group("validate mainnet bitcoincash addresses", () { + group("mainnet bitcoincash addressType", () { MockElectrumX? client; MockCachedElectrumX? cachedClient; MockPriceAPI? priceAPI; @@ -136,6 +136,168 @@ void main() { verifyNoMoreInteractions(priceAPI); }); + test("P2PKH cashaddr with prefix", () { + expect( + mainnetWallet?.addressType( + address: + "bitcoincash:qrwjyc4pewj9utzrtnh0whkzkuvy5q8wg52n254x6k"), + DerivePathType.bip44); + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + + test("P2PKH cashaddr without prefix", () { + expect( + mainnetWallet?.addressType( + address: "qrwjyc4pewj9utzrtnh0whkzkuvy5q8wg52n254x6k"), + DerivePathType.bip44); + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + + test("Multisig cashaddr with prefix", () { + expect( + () => mainnetWallet?.addressType( + address: + "bitcoincash:pzpp3nchmzzf0gr69lj82ymurg5u3ds6kcwr5m07np"), + throwsArgumentError); + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + + test("Multisig cashaddr without prefix", () { + expect( + () => mainnetWallet?.addressType( + address: "pzpp3nchmzzf0gr69lj82ymurg5u3ds6kcwr5m07np"), + throwsArgumentError); + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + + test("Multisig/P2SH address", () { + expect( + mainnetWallet?.addressType( + address: "3DYuVEmuKWQFxJcF7jDPhwPiXLTiNnyMFb"), + DerivePathType.bip49); + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + }); + + group("validate mainnet bitcoincash addresses", () { + MockElectrumX? client; + MockCachedElectrumX? cachedClient; + MockPriceAPI? priceAPI; + FakeSecureStorage? secureStore; + MockTransactionNotificationTracker? tracker; + + BitcoinCashWallet? mainnetWallet; + + setUp(() { + client = MockElectrumX(); + cachedClient = MockCachedElectrumX(); + priceAPI = MockPriceAPI(); + secureStore = FakeSecureStorage(); + tracker = MockTransactionNotificationTracker(); + + mainnetWallet = BitcoinCashWallet( + walletId: "validateAddressMainNet", + walletName: "validateAddressMainNet", + coin: Coin.bitcoincash, + client: client!, + cachedClient: cachedClient!, + tracker: tracker!, + priceAPI: priceAPI, + secureStore: secureStore, + ); + }); + + test("valid mainnet legacy/p2pkh address type", () { + expect( + mainnetWallet?.validateAddress("1DP3PUePwMa5CoZwzjznVKhzdLsZftjcAT"), + true); + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + + test("valid mainnet legacy/p2pkh cashaddr with prefix address type", () { + expect( + mainnetWallet?.validateAddress( + "bitcoincash:qrwjyc4pewj9utzrtnh0whkzkuvy5q8wg52n254x6k"), + true); + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + + test("valid mainnet legacy/p2pkh cashaddr without prefix address type", () { + expect( + mainnetWallet + ?.validateAddress("qrwjyc4pewj9utzrtnh0whkzkuvy5q8wg52n254x6k"), + true); + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + + test("invalid legacy/p2pkh address type", () { + expect( + mainnetWallet?.validateAddress("mhqpGtwhcR6gFuuRjLTpHo41919QfuGy8Y"), + false); + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + + test( + "invalid cashaddr (is valid multisig but bitbox is broken for multisig)", + () { + expect( + mainnetWallet + ?.validateAddress("pzpp3nchmzzf0gr69lj82ymurg5u3ds6kcwr5m07np"), + false); + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + + test("multisig address should fail for bitbox", () { + expect( + mainnetWallet?.validateAddress("3DYuVEmuKWQFxJcF7jDPhwPiXLTiNnyMFb"), + false); + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + test("invalid mainnet bitcoincash legacy/p2pkh address", () { expect( mainnetWallet?.validateAddress("mhqpGtwhcR6gFuuRjLTpHo41919QfuGy8Y"), From b3a343d28a29c6cc60e3f71d6d851b2aff27ebcc Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 7 Nov 2022 15:48:16 -0600 Subject: [PATCH 08/22] desktop theme toggle --- .../settings_menu/appearance_settings.dart | 391 ++++++++++-------- 1 file changed, 218 insertions(+), 173 deletions(-) diff --git a/lib/pages_desktop_specific/home/settings_menu/appearance_settings.dart b/lib/pages_desktop_specific/home/settings_menu/appearance_settings.dart index b5f239ab1..bf6fc81e2 100644 --- a/lib/pages_desktop_specific/home/settings_menu/appearance_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/appearance_settings.dart @@ -1,16 +1,19 @@ import 'package:flutter/material.dart'; -import 'package:flutter/src/widgets/framework.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/providers/global/prefs_provider.dart'; +import 'package:stackwallet/providers/ui/color_theme_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/color_theme.dart'; +import 'package:stackwallet/utilities/theme/dark_colors.dart'; +import 'package:stackwallet/utilities/theme/light_colors.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/custom_buttons/draggable_switch_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; -import '../../../providers/global/prefs_provider.dart'; -import '../../../utilities/constants.dart'; -import '../../../widgets/custom_buttons/draggable_switch_button.dart'; - class AppearanceOptionSettings extends ConsumerStatefulWidget { const AppearanceOptionSettings({Key? key}) : super(key: key); @@ -140,7 +143,10 @@ class _AppearanceOptionSettings ], ), ), - ThemeToggle(), + const Padding( + padding: EdgeInsets.all(10), + child: ThemeToggle(), + ), ], ), ), @@ -150,7 +156,7 @@ class _AppearanceOptionSettings } } -class ThemeToggle extends StatefulWidget { +class ThemeToggle extends ConsumerStatefulWidget { const ThemeToggle({ Key? key, }) : super(key: key); @@ -159,187 +165,226 @@ class ThemeToggle extends StatefulWidget { // final void Function(bool)? onChanged; @override - State<StatefulWidget> createState() => _ThemeToggle(); + ConsumerState<ThemeToggle> createState() => _ThemeToggle(); } -class _ThemeToggle extends State<ThemeToggle> { +class _ThemeToggle extends ConsumerState<ThemeToggle> { // late bool externalCallsEnabled; + late String _selectedTheme; + + @override + void initState() { + _selectedTheme = + DB.instance.get<dynamic>(boxName: DB.boxNameTheme, key: "colorScheme") + as String? ?? + "light"; + + super.initState(); + } + @override Widget build(BuildContext context) { return Row( - // mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: RawMaterialButton( - elevation: 0, - hoverColor: Colors.transparent, - shape: RoundedRectangleBorder( - side: BorderSide( - color: - Theme.of(context).extension<StackColors>()!.infoItemIcons, - width: 2, - ), - // side: !externalCallsEnabled - // ? BorderSide.none - // : BorderSide( - // color: Theme.of(context) - // .extension<StackColors>()! - // .infoItemIcons, - // width: 2, - // ), - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius * 2, - ), - ), - onPressed: () {}, //onPressed - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Stack( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only( - left: 24, - ), - child: SvgPicture.asset( - Assets.svg.themeLight, - ), - ), - Padding( - padding: const EdgeInsets.only( - left: 50, - top: 12, - ), - child: Text( - "Light", - style: STextStyles.desktopTextExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark, - ), - ), - ) - ], - ), - // if (externalCallsEnabled) - Positioned( - bottom: 0, - left: 6, - child: SvgPicture.asset( - Assets.svg.checkCircle, - width: 20, - height: 20, - color: Theme.of(context) - .extension<StackColors>()! - .infoItemIcons, - ), - ), - // if (!externalCallsEnabled) - // Positioned( - // bottom: 0, - // left: 6, - // child: Container( - // width: 20, - // height: 20, - // decoration: BoxDecoration( - // borderRadius: BorderRadius.circular(1000), - // color: Theme.of(context) - // .extension<StackColors>()! - // .textFieldDefaultBG, - // ), - // ), - // ), - ], - ), + MaterialButton( + splashColor: Colors.transparent, + hoverColor: Colors.transparent, + padding: const EdgeInsets.all(0), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), ), - ), - const SizedBox( - width: 1, - ), - Expanded( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: RawMaterialButton( - elevation: 0, - hoverColor: Colors.transparent, - shape: RoundedRectangleBorder( - // side: !externalCallsEnabled - // ? BorderSide.none - // : BorderSide( - // color: Theme.of(context) - // .extension<StackColors>()! - // .infoItemIcons, - // width: 2, - // ), - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius * 2, - ), - ), - onPressed: () {}, //onPressed - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Stack( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SvgPicture.asset( - Assets.svg.themeDark, - ), - Padding( - padding: const EdgeInsets.only( - left: 45, - top: 12, - ), - child: Text( - "Dark", - style: STextStyles.desktopTextExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark, - ), - ), - ), - ], - ), - // if (externalCallsEnabled) - // Positioned( - // bottom: 0, - // left: 0, - // child: SvgPicture.asset( - // Assets.svg.checkCircle, - // width: 20, - // height: 20, - // color: Theme.of(context) - // .extension<StackColors>()! - // .infoItemIcons, - // ), - // ), - // if (!externalCallsEnabled) - Positioned( - bottom: 0, - left: 0, - child: Container( - width: 20, - height: 20, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(1000), - color: Theme.of(context) + onPressed: () { + DB.instance.put<dynamic>( + boxName: DB.boxNameTheme, + key: "colorScheme", + value: ThemeType.light.name, + ); + ref.read(colorThemeProvider.state).state = + StackColors.fromStackColorTheme( + LightColors(), + ); + + setState(() { + _selectedTheme = "light"; + }); + }, + child: SizedBox( + width: 200, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + decoration: BoxDecoration( + border: Border.all( + width: 2.5, + color: _selectedTheme == "light" + ? Theme.of(context) .extension<StackColors>()! - .textFieldDefaultBG, - ), + .infoItemIcons + : Theme.of(context).extension<StackColors>()!.popupBG, + ), + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + child: SvgPicture.asset( + Assets.svg.themeLight, + ), + ), + const SizedBox( + height: 12, + ), + Row( + children: [ + SizedBox( + width: 20, + height: 20, + child: Radio( + activeColor: Theme.of(context) + .extension<StackColors>()! + .radioButtonIconEnabled, + value: "light", + groupValue: _selectedTheme, + onChanged: (newValue) { + if (newValue is String && newValue == "light") { + DB.instance.put<dynamic>( + boxName: DB.boxNameTheme, + key: "colorScheme", + value: ThemeType.light.name, + ); + ref.read(colorThemeProvider.state).state = + StackColors.fromStackColorTheme( + LightColors(), + ); + + setState(() { + _selectedTheme = "light"; + }); + } + }, + ), + ), + const SizedBox( + width: 14, + ), + Text( + "Light", + style: + STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, ), ), ], ), - ), + ], + ), + ), + ), + const SizedBox( + width: 20, + ), + MaterialButton( + splashColor: Colors.transparent, + hoverColor: Colors.transparent, + padding: const EdgeInsets.all(0), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () { + DB.instance.put<dynamic>( + boxName: DB.boxNameTheme, + key: "colorScheme", + value: ThemeType.dark.name, + ); + ref.read(colorThemeProvider.state).state = + StackColors.fromStackColorTheme( + DarkColors(), + ); + + setState(() { + _selectedTheme = "dark"; + }); + }, + child: SizedBox( + width: 200, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + decoration: BoxDecoration( + border: Border.all( + width: 2.5, + color: _selectedTheme == "dark" + ? Theme.of(context) + .extension<StackColors>()! + .infoItemIcons + : Theme.of(context).extension<StackColors>()!.popupBG, + ), + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + child: SvgPicture.asset( + Assets.svg.themeDark, + ), + ), + const SizedBox( + height: 12, + ), + Row( + children: [ + SizedBox( + width: 20, + height: 20, + child: Radio( + activeColor: Theme.of(context) + .extension<StackColors>()! + .radioButtonIconEnabled, + value: "dark", + groupValue: _selectedTheme, + onChanged: (newValue) { + if (newValue is String && newValue == "dark") { + DB.instance.put<dynamic>( + boxName: DB.boxNameTheme, + key: "colorScheme", + value: ThemeType.dark.name, + ); + ref.read(colorThemeProvider.state).state = + StackColors.fromStackColorTheme( + DarkColors(), + ); + + setState(() { + _selectedTheme = "dark"; + }); + } + }, + ), + ), + const SizedBox( + width: 14, + ), + Text( + "Dark", + style: + STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ), + ], + ), + ], ), ), ), From 3c627a5ddb36e93c229f9e96a9829df1664d700d Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 7 Nov 2022 15:57:04 -0600 Subject: [PATCH 09/22] support tweak --- .../desktop_support_view.dart | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/lib/pages_desktop_specific/home/support_and_about_view/desktop_support_view.dart b/lib/pages_desktop_specific/home/support_and_about_view/desktop_support_view.dart index 8e9d709d1..ce3e3f3cc 100644 --- a/lib/pages_desktop_specific/home/support_and_about_view/desktop_support_view.dart +++ b/lib/pages_desktop_specific/home/support_and_about_view/desktop_support_view.dart @@ -1,12 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/pages/settings_views/global_settings_view/support_view.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; -import '../../../pages/settings_views/global_settings_view/support_view.dart'; - class DesktopSupportView extends ConsumerStatefulWidget { const DesktopSupportView({Key? key}) : super(key: key); @@ -38,10 +37,18 @@ class _DesktopSupportView extends ConsumerState<DesktopSupportView> { ), ), body: Column( - children: const [ + crossAxisAlignment: CrossAxisAlignment.start, + children: [ Padding( - padding: EdgeInsets.fromLTRB(24, 10, 377, 270), - child: SupportView(), + padding: const EdgeInsets.fromLTRB(24, 10, 0, 0), + child: Row( + children: const [ + SizedBox( + width: 576, + child: SupportView(), + ), + ], + ), ), ], ), From 2f6b1278fe59971855e9772de7e60961abf3a29e Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 7 Nov 2022 16:30:17 -0600 Subject: [PATCH 10/22] swb desktop layout tweaks --- .../create_backup_view.dart | 4 +- .../restore_from_file_view.dart | 4 +- .../backup_and_restore_settings.dart | 192 ++++++++++-------- .../enable_backup_dialog.dart | 3 +- 4 files changed, 115 insertions(+), 88 deletions(-) diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart index 2d2ed4960..30fcb7962 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart @@ -157,7 +157,7 @@ class _RestoreFromFileViewState extends State<CreateBackupView> { .textDark3), ), ), - // child, + child, const SizedBox(height: 20), Row( children: [ @@ -442,7 +442,7 @@ class _RestoreFromFileViewState extends State<CreateBackupView> { const SizedBox( height: 16, ), - const Spacer(), + if (!isDesktop) const Spacer(), TextButton( style: shouldEnableCreate ? Theme.of(context) diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_file_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_file_view.dart index 9f2796415..0c101d0b3 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_file_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_file_view.dart @@ -142,7 +142,7 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> { textAlign: TextAlign.left, ), ), - // child, + child, const SizedBox(height: 20), Row( children: [ @@ -285,7 +285,7 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> { const SizedBox( height: 16, ), - const Spacer(), + if (!isDesktop) const Spacer(), TextButton( style: passwordController.text.isEmpty || fileLocationController.text.isEmpty diff --git a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart index b59206f17..49debf22f 100644 --- a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart @@ -64,48 +64,56 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> { height: 48, ), Center( - child: Padding( - padding: const EdgeInsets.all(10), - child: RichText( - textAlign: TextAlign.start, - text: TextSpan( - children: [ - TextSpan( - text: "Auto Backup", - style: - STextStyles.desktopTextSmall(context), + child: Row( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.all(10), + child: RichText( + textAlign: TextAlign.start, + text: TextSpan( + children: [ + TextSpan( + text: "Auto Backup", + style: STextStyles.desktopTextSmall( + context), + ), + TextSpan( + text: + "\n\nAuto backup is a custom Stack Wallet feature that offers a convenient backup of your data." + "To ensure maximum security, we recommend using a unique password that you haven't used anywhere " + "else on the internet before. Your password is not stored.", + style: STextStyles + .desktopTextExtraExtraSmall( + context), + ), + TextSpan( + text: + "\n\nFor more information, please see our website ", + style: STextStyles + .desktopTextExtraExtraSmall( + context), + ), + TextSpan( + text: "stackwallet.com", + style: STextStyles.richLink(context) + .copyWith(fontSize: 14), + recognizer: TapGestureRecognizer() + ..onTap = () { + launchUrl( + Uri.parse( + "https://stackwallet.com/"), + mode: LaunchMode + .externalApplication, + ); + }, + ), + ], + ), ), - TextSpan( - text: - "\n\nAuto backup is a custom Stack Wallet feature that offers a convenient backup of your data." - "To ensure maximum security, we recommend using a unique password that you haven't used anywhere " - "else on the internet before. Your password is not stored.", - style: STextStyles - .desktopTextExtraExtraSmall(context), - ), - TextSpan( - text: - "\n\nFor more information, please see our website ", - style: STextStyles - .desktopTextExtraExtraSmall(context), - ), - TextSpan( - text: "stackwallet.com", - style: STextStyles.richLink(context) - .copyWith(fontSize: 14), - recognizer: TapGestureRecognizer() - ..onTap = () { - launchUrl( - Uri.parse( - "https://stackwallet.com/"), - mode: - LaunchMode.externalApplication, - ); - }, - ), - ], + ), ), - ), + ], ), ), Column( @@ -148,39 +156,49 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> { alignment: Alignment.topLeft, ), Center( - child: Padding( - padding: const EdgeInsets.all(10), - child: RichText( - textAlign: TextAlign.start, - text: TextSpan( - children: [ - TextSpan( - text: "Manual Backup", - style: - STextStyles.desktopTextSmall(context), + child: Row( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.all(10), + child: RichText( + textAlign: TextAlign.start, + text: TextSpan( + children: [ + TextSpan( + text: "Manual Backup", + style: STextStyles.desktopTextSmall( + context), + ), + TextSpan( + text: + "\n\nCreate manual backup to easily transfer your data between devices. " + "You will create a backup file that can be later used in the Restore option. " + "Use a strong password to encrypt your data.", + style: STextStyles + .desktopTextExtraExtraSmall( + context), + ), + ], + ), ), - TextSpan( - text: - "\n\nCreate manual backup to easily transfer your data between devices. " - "You will create a backup file that can be later used in the Restore option. " - "Use a strong password to encrypt your data.", - style: STextStyles - .desktopTextExtraExtraSmall(context), - ), - ], + ), ), - ), + ], ), ), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( - padding: EdgeInsets.all( + padding: const EdgeInsets.all( 10, ), child: createBackup - ? const CreateBackupView() + ? const SizedBox( + width: 512, + child: CreateBackupView(), + ) : PrimaryButton( desktopMed: true, width: 200, @@ -217,27 +235,34 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> { alignment: Alignment.topLeft, ), Center( - child: Padding( - padding: const EdgeInsets.all(10), - child: RichText( - textAlign: TextAlign.start, - text: TextSpan( - children: [ - TextSpan( - text: "Restore Backup", - style: - STextStyles.desktopTextSmall(context), + child: Row( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.all(10), + child: RichText( + textAlign: TextAlign.start, + text: TextSpan( + children: [ + TextSpan( + text: "Restore Backup", + style: STextStyles.desktopTextSmall( + context), + ), + TextSpan( + text: + "\n\nUse your Stack Wallet backup file to restore your wallets, address book " + "and wallet preferences.", + style: STextStyles + .desktopTextExtraExtraSmall( + context), + ), + ], + ), ), - TextSpan( - text: - "\n\nUse your Stack Wallet backup file to restore your wallets, address book " - "and wallet preferences.", - style: STextStyles - .desktopTextExtraExtraSmall(context), - ), - ], + ), ), - ), + ], ), ), Column( @@ -248,7 +273,10 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> { 10, ), child: restoreBackup - ? RestoreFromFileView() + ? const SizedBox( + width: 512, + child: RestoreFromFileView(), + ) : PrimaryButton( desktopMed: true, width: 200, diff --git a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/enable_backup_dialog.dart b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/enable_backup_dialog.dart index 046d136a8..963fb4441 100644 --- a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/enable_backup_dialog.dart +++ b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/enable_backup_dialog.dart @@ -61,8 +61,7 @@ class EnableBackupDialog extends StatelessWidget { child: SecondaryButton( label: "Cancel", onPressed: () { - int count = 0; - Navigator.of(context).popUntil((_) => count++ >= 2); + Navigator.of(context).pop(); }, ), ), From fa0c982274159d3d7be179ad48a88cf50faef62a Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Tue, 8 Nov 2022 07:35:28 -0600 Subject: [PATCH 11/22] Return what we internally consider the "txid" for epic transactions from the epic confirmSend to be consistent with all other coins confirmSend return value. This should fix the epic notes issue. --- .../coins/epiccash/epiccash_wallet.dart | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/lib/services/coins/epiccash/epiccash_wallet.dart b/lib/services/coins/epiccash/epiccash_wallet.dart index 3702a9158..6c71f39e4 100644 --- a/lib/services/coins/epiccash/epiccash_wallet.dart +++ b/lib/services/coins/epiccash/epiccash_wallet.dart @@ -833,10 +833,16 @@ class EpicCashWallet extends CoinServiceAPI { final txLogEntryFirst = txLogEntry[0]; Logger.print("TX_LOG_ENTRY_IS $txLogEntryFirst"); final wallet = await Hive.openBox<dynamic>(_walletId); - final slateToAddresses = (await wallet.get("slate_to_address")) as Map?; - slateToAddresses?[txLogEntryFirst['tx_slate_id']] = txData['addresss']; + final slateToAddresses = + (await wallet.get("slate_to_address")) as Map? ?? {}; + final slateId = txLogEntryFirst['tx_slate_id'] as String; + slateToAddresses[slateId] = txData['addresss']; await wallet.put('slate_to_address', slateToAddresses); - return txLogEntryFirst['tx_slate_id'] as String; + final slatesToCommits = await getSlatesToCommits(); + String? commitId = slatesToCommits[slateId]?['commitId'] as String?; + Logging.instance.log("sent commitId: $commitId", level: LogLevel.Info); + return commitId!; + // return txLogEntryFirst['tx_slate_id'] as String; } } catch (e, s) { Logging.instance.log("Error sending $e - $s", level: LogLevel.Error); @@ -2155,8 +2161,9 @@ class EpicCashWallet extends CoinServiceAPI { as String? ?? ""; String? commitId = slatesToCommits[slateId]?['commitId'] as String?; - Logging.instance - .log("commitId: $commitId $slateId", level: LogLevel.Info); + Logging.instance.log( + "commitId: $commitId, slateId: $slateId, id: ${tx["id"]}", + level: LogLevel.Info); bool isCancelled = tx["tx_type"] == "TxSentCancelled" || tx["tx_type"] == "TxReceivedCancelled"; From f17785ffc7e81f187239881eed0e72da6d7fb8b2 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Tue, 8 Nov 2022 10:18:48 -0600 Subject: [PATCH 12/22] monero/wownero untrusted cert popup --- .../add_edit_node_view.dart | 24 +++- .../manage_nodes_views/node_details_view.dart | 24 +++- .../test_monero_node_connection.dart | 115 +++++++++++++++--- lib/widgets/node_card.dart | 24 +++- lib/widgets/node_options_sheet.dart | 24 +++- 5 files changed, 192 insertions(+), 19 deletions(-) diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart index 87aee413e..382e3f09e 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart @@ -110,7 +110,29 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> { ref.read(nodeFormDataProvider).useSSL = false; } - testPassed = await testMoneroNodeConnection(Uri.parse(uriString)); + final response = await testMoneroNodeConnection( + Uri.parse(uriString), + false, + ); + + if (response.cert != null) { + if (mounted) { + final shouldAllowBadCert = await showBadX509CertificateDialog( + response.cert!, + response.url!, + response.port!, + context, + ); + + if (shouldAllowBadCert) { + final response = await testMoneroNodeConnection( + Uri.parse(uriString), true); + testPassed = response.success; + } + } + } else { + testPassed = response.success; + } } } catch (e, s) { Logging.instance.log("$e\n$s", level: LogLevel.Warning); diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart index c5e666ce2..f9b64c460 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart @@ -97,7 +97,29 @@ class _NodeDetailsViewState extends ConsumerState<NodeDetailsView> { String uriString = "${uri.scheme}://${uri.host}:${node.port}$path"; - testPassed = await testMoneroNodeConnection(Uri.parse(uriString)); + final response = await testMoneroNodeConnection( + Uri.parse(uriString), + false, + ); + + if (response.cert != null) { + if (mounted) { + final shouldAllowBadCert = await showBadX509CertificateDialog( + response.cert!, + response.url!, + response.port!, + context, + ); + + if (shouldAllowBadCert) { + final response = await testMoneroNodeConnection( + Uri.parse(uriString), true); + testPassed = response.success; + } + } + } else { + testPassed = response.success; + } } } catch (e, s) { Logging.instance.log("$e\n$s", level: LogLevel.Warning); diff --git a/lib/utilities/test_monero_node_connection.dart b/lib/utilities/test_monero_node_connection.dart index 7cb01e8b1..92b645141 100644 --- a/lib/utilities/test_monero_node_connection.dart +++ b/lib/utilities/test_monero_node_connection.dart @@ -1,26 +1,111 @@ import 'dart:convert'; +import 'dart:io'; -import 'package:http/http.dart' as http; +import 'package:flutter/material.dart'; +import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/stack_dialog.dart'; -Future<bool> testMoneroNodeConnection(Uri uri) async { +class MoneroNodeConnectionResponse { + final X509Certificate? cert; + final String? url; + final int? port; + final bool success; + + MoneroNodeConnectionResponse(this.cert, this.url, this.port, this.success); +} + +Future<MoneroNodeConnectionResponse> testMoneroNodeConnection( + Uri uri, + bool allowBadX509Certificate, +) async { + final client = HttpClient(); + MoneroNodeConnectionResponse? badCertResponse; try { - final client = http.Client(); - final response = await client - .post( - uri, - headers: {'Content-Type': 'application/json'}, - body: jsonEncode({"jsonrpc": "2.0", "id": "0", "method": "get_info"}), - ) - .timeout(const Duration(milliseconds: 1200), - onTimeout: () async => http.Response('Error', 408)); + client.badCertificateCallback = (cert, url, port) { + if (allowBadX509Certificate) { + return true; + } - final result = jsonDecode(response.body); + if (badCertResponse == null) { + badCertResponse = MoneroNodeConnectionResponse(cert, url, port, false); + } else { + return false; + } + + return false; + }; + + final request = await client.postUrl(uri); + + final body = utf8.encode( + jsonEncode({ + "jsonrpc": "2.0", + "id": "0", + "method": "get_info", + }), + ); + + request.headers.add( + 'Content-Length', + body.length.toString(), + preserveHeaderCase: true, + ); + request.headers.set( + 'Content-Type', + 'application/json', + preserveHeaderCase: true, + ); + + request.add(body); + + final response = await request.close(); + final result = await response.transform(utf8.decoder).join(); // TODO: json decoded without error so assume connection exists? // or we can check for certain values in the response to decide - return true; + return MoneroNodeConnectionResponse(null, null, null, true); } catch (e, s) { - Logging.instance.log("$e\n$s", level: LogLevel.Warning); - return false; + if (badCertResponse != null) { + return badCertResponse!; + } else { + Logging.instance.log("$e\n$s", level: LogLevel.Warning); + return MoneroNodeConnectionResponse(null, null, null, false); + } + } finally { + client.close(force: true); } } + +Future<bool> showBadX509CertificateDialog( + X509Certificate cert, + String url, + int port, + BuildContext context, +) async { + final result = await showDialog<bool>( + context: context, + barrierDismissible: false, + builder: (context) { + return StackDialog( + title: "Untrusted X509Certificate", + message: "SHA1: ${Format.uint8listToString(cert.sha1)}", + leftButton: SecondaryButton( + label: "Cancel", + onPressed: () { + Navigator.of(context).pop(false); + }, + ), + rightButton: PrimaryButton( + label: "Trust", + onPressed: () { + Navigator.of(context).pop(true); + }, + ), + ); + }, + ); + + return result ?? false; +} diff --git a/lib/widgets/node_card.dart b/lib/widgets/node_card.dart index bf9d2746e..1da7e9012 100644 --- a/lib/widgets/node_card.dart +++ b/lib/widgets/node_card.dart @@ -110,7 +110,29 @@ class _NodeCardState extends ConsumerState<NodeCard> { String uriString = "${uri.scheme}://${uri.host}:${node.port}$path"; - testPassed = await testMoneroNodeConnection(Uri.parse(uriString)); + final response = await testMoneroNodeConnection( + Uri.parse(uriString), + false, + ); + + if (response.cert != null) { + if (mounted) { + final shouldAllowBadCert = await showBadX509CertificateDialog( + response.cert!, + response.url!, + response.port!, + context, + ); + + if (shouldAllowBadCert) { + final response = await testMoneroNodeConnection( + Uri.parse(uriString), true); + testPassed = response.success; + } + } + } else { + testPassed = response.success; + } } } catch (e, s) { Logging.instance.log("$e\n$s", level: LogLevel.Warning); diff --git a/lib/widgets/node_options_sheet.dart b/lib/widgets/node_options_sheet.dart index a5345161c..7ffd290f3 100644 --- a/lib/widgets/node_options_sheet.dart +++ b/lib/widgets/node_options_sheet.dart @@ -93,7 +93,29 @@ class NodeOptionsSheet extends ConsumerWidget { String uriString = "${uri.scheme}://${uri.host}:${node.port}$path"; - testPassed = await testMoneroNodeConnection(Uri.parse(uriString)); + final response = await testMoneroNodeConnection( + Uri.parse(uriString), + false, + ); + + if (response.cert != null) { + // if (mounted) { + final shouldAllowBadCert = await showBadX509CertificateDialog( + response.cert!, + response.url!, + response.port!, + context, + ); + + if (shouldAllowBadCert) { + final response = + await testMoneroNodeConnection(Uri.parse(uriString), true); + testPassed = response.success; + } + // } + } else { + testPassed = response.success; + } } } catch (e, s) { Logging.instance.log("$e\n$s", level: LogLevel.Warning); From a8c3d5f1042b6b70888acca8db0020928faa4684 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Tue, 8 Nov 2022 11:41:12 -0600 Subject: [PATCH 13/22] format sha1 string --- lib/utilities/test_monero_node_connection.dart | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/utilities/test_monero_node_connection.dart b/lib/utilities/test_monero_node_connection.dart index 92b645141..5e35f9a03 100644 --- a/lib/utilities/test_monero_node_connection.dart +++ b/lib/utilities/test_monero_node_connection.dart @@ -84,13 +84,23 @@ Future<bool> showBadX509CertificateDialog( int port, BuildContext context, ) async { + final chars = Format.uint8listToString(cert.sha1) + .toUpperCase() + .characters + .toList(growable: false); + + String sha1 = chars.sublist(0, 2).join(); + for (int i = 2; i < chars.length; i += 2) { + sha1 += ":${chars.sublist(i, i + 2).join()}"; + } + final result = await showDialog<bool>( context: context, barrierDismissible: false, builder: (context) { return StackDialog( title: "Untrusted X509Certificate", - message: "SHA1: ${Format.uint8listToString(cert.sha1)}", + message: "SHA1:\n$sha1", leftButton: SecondaryButton( label: "Cancel", onPressed: () { From e41f8088b02c8aeaac8caaebac27dcbe7cc1d893 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Tue, 8 Nov 2022 12:00:10 -0600 Subject: [PATCH 14/22] WIP: wownero 25 word seed option ui --- .../restore_options_view.dart | 36 +++++++++++++++---- .../restore_wallet_view.dart | 5 +++ .../coins/wownero/wownero_wallet.dart | 10 +++++- lib/utilities/constants.dart | 6 +--- 4 files changed, 45 insertions(+), 12 deletions(-) diff --git a/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart b/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart index 76e74fa14..1ce5d713a 100644 --- a/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart +++ b/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart @@ -252,7 +252,11 @@ class _RestoreOptionsViewState extends ConsumerState<RestoreOptionsView> { SizedBox( height: isDesktop ? 40 : 24, ), - if (coin == Coin.monero || coin == Coin.epicCash) + if (coin == Coin.monero || + coin == Coin.epicCash || + (coin == Coin.wownero && + ref.watch(mnemonicWordCountStateProvider.state).state == + 25)) Text( "Choose start date", style: isDesktop @@ -264,11 +268,19 @@ class _RestoreOptionsViewState extends ConsumerState<RestoreOptionsView> { : STextStyles.smallMed12(context), textAlign: TextAlign.left, ), - if (coin == Coin.monero || coin == Coin.epicCash) + if (coin == Coin.monero || + coin == Coin.epicCash || + (coin == Coin.wownero && + ref.watch(mnemonicWordCountStateProvider.state).state == + 25)) SizedBox( height: isDesktop ? 16 : 8, ), - if (coin == Coin.monero || coin == Coin.epicCash) + if (coin == Coin.monero || + coin == Coin.epicCash || + (coin == Coin.wownero && + ref.watch(mnemonicWordCountStateProvider.state).state == + 25)) // if (!isDesktop) RestoreFromDatePicker( @@ -278,11 +290,19 @@ class _RestoreOptionsViewState extends ConsumerState<RestoreOptionsView> { // if (isDesktop) // // TODO desktop date picker - if (coin == Coin.monero || coin == Coin.epicCash) + if (coin == Coin.monero || + coin == Coin.epicCash || + (coin == Coin.wownero && + ref.watch(mnemonicWordCountStateProvider.state).state == + 25)) const SizedBox( height: 8, ), - if (coin == Coin.monero || coin == Coin.epicCash) + if (coin == Coin.monero || + coin == Coin.epicCash || + (coin == Coin.wownero && + ref.watch(mnemonicWordCountStateProvider.state).state == + 25)) RoundedWhiteContainer( child: Center( child: Text( @@ -299,7 +319,11 @@ class _RestoreOptionsViewState extends ConsumerState<RestoreOptionsView> { ), ), ), - if (coin == Coin.monero || coin == Coin.epicCash) + if (coin == Coin.monero || + coin == Coin.epicCash || + (coin == Coin.wownero && + ref.watch(mnemonicWordCountStateProvider.state).state == + 25)) SizedBox( height: isDesktop ? 24 : 16, ), diff --git a/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart b/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart index def0724b5..a6b7e7e77 100644 --- a/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart +++ b/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart @@ -149,6 +149,7 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> { super.dispose(); } + // TODO: check for wownero wordlist? bool _isValidMnemonicWord(String word) { // TODO: get the actual language if (widget.coin == Coin.monero) { @@ -181,6 +182,10 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> { if (widget.coin == Coin.monero) { height = monero.getHeigthByDate(date: widget.restoreFromDate); } + // todo: wait until this implemented + // else if (widget.coin == Coin.wownero) { + // height = wownero.getHeightByDate(date: widget.restoreFromDate); + // } // TODO: make more robust estimate of date maybe using https://explorer.epic.tech/api-index if (widget.coin == Coin.epicCash) { diff --git a/lib/services/coins/wownero/wownero_wallet.dart b/lib/services/coins/wownero/wownero_wallet.dart index 72f43eac8..788f2f9d8 100644 --- a/lib/services/coins/wownero/wownero_wallet.dart +++ b/lib/services/coins/wownero/wownero_wallet.dart @@ -942,6 +942,11 @@ class WowneroWallet extends CoinServiceAPI { required int maxNumberOfIndexesToCheck, required int height, }) async { + final int seedLength = mnemonic.trim().split(" ").length; + if (!(seedLength == 14 || seedLength == 25)) { + throw Exception("Invalid wownero mnemonic length found: $seedLength"); + } + await _prefs.init(); longMutex = true; final start = DateTime.now(); @@ -969,7 +974,10 @@ class WowneroWallet extends CoinServiceAPI { await _secureStore.write( key: '${_walletId}_mnemonic', value: mnemonic.trim()); - height = getSeedHeightSync(mnemonic.trim()); + // extract seed height from 14 word seed + if (seedLength == 14) { + height = getSeedHeightSync(mnemonic.trim()); + } await DB.instance .put<dynamic>(boxName: walletId, key: "restoreHeight", value: height); diff --git a/lib/utilities/constants.dart b/lib/utilities/constants.dart index 4fb3fb54b..e27fbaa3d 100644 --- a/lib/utilities/constants.dart +++ b/lib/utilities/constants.dart @@ -35,10 +35,6 @@ abstract class Constants { static const int pinLength = 4; - // enable testnet - // TODO: currently unused - static const bool allowTestnets = true; - // Enable Logger.print statements static const bool disableLogger = false; @@ -66,7 +62,7 @@ abstract class Constants { values.addAll([25]); break; case Coin.wownero: - values.addAll([14]); + values.addAll([14, 25]); break; } return values; From 43deb9f81fa99d65318a116c1c835a0dc9f96f9e Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Mon, 7 Nov 2022 16:17:54 -0700 Subject: [PATCH 15/22] desktop about ui fix --- .../desktop_about_view.dart | 933 ++++++++++-------- 1 file changed, 497 insertions(+), 436 deletions(-) diff --git a/lib/pages_desktop_specific/home/support_and_about_view/desktop_about_view.dart b/lib/pages_desktop_specific/home/support_and_about_view/desktop_about_view.dart index 86fcf78e0..18988cb68 100644 --- a/lib/pages_desktop_specific/home/support_and_about_view/desktop_about_view.dart +++ b/lib/pages_desktop_specific/home/support_and_about_view/desktop_about_view.dart @@ -140,262 +140,123 @@ class DesktopAboutView extends ConsumerWidget { children: [ Padding( padding: const EdgeInsets.fromLTRB(24, 10, 24, 35), - child: RoundedWhiteContainer( - width: 929, - height: 411, - child: Padding( - padding: const EdgeInsets.only(left: 10, top: 10), - child: Column( - // mainAxisAlignment: MainAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Text( - "Stack Wallet", - style: STextStyles.desktopH3(context), - textAlign: TextAlign.start, - ), - ], - ), - const SizedBox(height: 16), - Row( - children: [ - RichText( - textAlign: TextAlign.start, - text: TextSpan( - style: STextStyles.label(context), + child: Row( + children: [ + Expanded( + child: RoundedWhiteContainer( + width: 929, + height: 411, + child: Padding( + padding: const EdgeInsets.only(left: 10, top: 10), + child: Column( + // mainAxisAlignment: MainAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, children: [ - TextSpan( - text: - "By using Stack Wallet, you agree to the ", - style: STextStyles.desktopTextExtraExtraSmall( - context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark3), - ), - TextSpan( - text: "Terms of service", - style: STextStyles.richLink(context) - .copyWith(fontSize: 14), - recognizer: TapGestureRecognizer() - ..onTap = () { - launchUrl( - Uri.parse( - "https://stackwallet.com/terms-of-service.html"), - mode: LaunchMode.externalApplication, - ); - }, - ), - TextSpan( - text: " and ", - style: STextStyles.desktopTextExtraExtraSmall( - context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark3), - ), - TextSpan( - text: "Privacy policy", - style: STextStyles.richLink(context) - .copyWith(fontSize: 14), - recognizer: TapGestureRecognizer() - ..onTap = () { - launchUrl( - Uri.parse( - "https://stackwallet.com/privacy-policy.html"), - mode: LaunchMode.externalApplication, - ); - }, + Text( + "Stack Wallet", + style: STextStyles.desktopH3(context), + textAlign: TextAlign.start, ), ], ), - ), - ], - ), - const SizedBox(height: 32), - Padding( - padding: const EdgeInsets.only(right: 10, bottom: 10), - child: Column( - children: [ - FutureBuilder( - future: PackageInfo.fromPlatform(), - builder: - (context, AsyncSnapshot<PackageInfo> snapshot) { - String version = ""; - String signature = ""; - String build = ""; + const SizedBox(height: 16), + Row( + children: [ + RichText( + textAlign: TextAlign.start, + text: TextSpan( + style: STextStyles.label(context), + children: [ + TextSpan( + text: + "By using Stack Wallet, you agree to the ", + style: STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark3), + ), + TextSpan( + text: "Terms of service", + style: STextStyles.richLink(context) + .copyWith(fontSize: 14), + recognizer: TapGestureRecognizer() + ..onTap = () { + launchUrl( + Uri.parse( + "https://stackwallet.com/terms-of-service.html"), + mode: + LaunchMode.externalApplication, + ); + }, + ), + TextSpan( + text: " and ", + style: STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark3), + ), + TextSpan( + text: "Privacy policy", + style: STextStyles.richLink(context) + .copyWith(fontSize: 14), + recognizer: TapGestureRecognizer() + ..onTap = () { + launchUrl( + Uri.parse( + "https://stackwallet.com/privacy-policy.html"), + mode: + LaunchMode.externalApplication, + ); + }, + ), + ], + ), + ), + ], + ), + const SizedBox(height: 32), + Padding( + padding: + const EdgeInsets.only(right: 10, bottom: 10), + child: Column( + children: [ + FutureBuilder( + future: PackageInfo.fromPlatform(), + builder: (context, + AsyncSnapshot<PackageInfo> snapshot) { + String version = ""; + String signature = ""; + String build = ""; - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData) { - version = snapshot.data!.version; - build = snapshot.data!.buildNumber; - signature = snapshot.data!.buildSignature; - } + if (snapshot.connectionState == + ConnectionState.done && + snapshot.hasData) { + version = snapshot.data!.version; + build = snapshot.data!.buildNumber; + signature = snapshot.data!.buildSignature; + } - return Column( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - "Version", - style: STextStyles - .desktopTextExtraExtraSmall( - context) - .copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .textDark), - ), - const SizedBox( - height: 2, - ), - SelectableText( - version, - style: STextStyles.itemSubtitle( - context), - ), - ], - ), - const SizedBox( - width: 400, - ), - Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - "Build number", - style: STextStyles - .desktopTextExtraExtraSmall( - context) - .copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .textDark), - ), - const SizedBox( - height: 2, - ), - SelectableText( - build, - style: STextStyles.itemSubtitle( - context), - ), - ], - ), - ], - ), - const SizedBox(height: 32), - Row( - children: [ - Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - "Build signature", - style: STextStyles - .desktopTextExtraExtraSmall( - context) - .copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .textDark), - ), - const SizedBox( - height: 2, - ), - SelectableText( - signature, - style: STextStyles.itemSubtitle( - context), - ), - ], - ), - const SizedBox( - width: 350, - ), - FutureBuilder( - future: commitFiroFuture, - builder: (context, - AsyncSnapshot<dynamic> snapshot) { - bool commitExists = false; - bool isHead = false; - CommitStatus stateOfCommit = - CommitStatus.notLoaded; - - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData) { - commitExists = - snapshot.data![0] as bool; - isHead = - snapshot.data![1] as bool; - if (commitExists && isHead) { - stateOfCommit = - CommitStatus.isHead; - } else if (commitExists) { - stateOfCommit = - CommitStatus.isOldCommit; - } else { - stateOfCommit = - CommitStatus.notACommit; - } - } - TextStyle indicationStyle = - STextStyles.itemSubtitle( - context); - switch (stateOfCommit) { - case CommitStatus.isHead: - indicationStyle = STextStyles - .itemSubtitle(context) - .copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .accentColorGreen); - break; - case CommitStatus.isOldCommit: - indicationStyle = STextStyles - .itemSubtitle(context) - .copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .accentColorYellow); - break; - case CommitStatus.notACommit: - indicationStyle = STextStyles - .itemSubtitle(context) - .copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .accentColorRed); - break; - default: - break; - } - return Column( + return Column( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "Firo Build Commit", + "Version", style: STextStyles .desktopTextExtraExtraSmall( context) @@ -410,84 +271,22 @@ class DesktopAboutView extends ConsumerWidget { height: 2, ), SelectableText( - firoCommit, - style: indicationStyle, + version, + style: + STextStyles.itemSubtitle( + context), ), ], - ); - }), - ], - ), - const SizedBox(height: 35), - Row( - children: [ - FutureBuilder( - future: commitEpicFuture, - builder: (context, - AsyncSnapshot<dynamic> snapshot) { - bool commitExists = false; - bool isHead = false; - CommitStatus stateOfCommit = - CommitStatus.notLoaded; - - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData) { - commitExists = - snapshot.data![0] as bool; - isHead = - snapshot.data![1] as bool; - if (commitExists && isHead) { - stateOfCommit = - CommitStatus.isHead; - } else if (commitExists) { - stateOfCommit = - CommitStatus.isOldCommit; - } else { - stateOfCommit = - CommitStatus.notACommit; - } - } - TextStyle indicationStyle = - STextStyles.itemSubtitle( - context); - switch (stateOfCommit) { - case CommitStatus.isHead: - indicationStyle = STextStyles - .itemSubtitle(context) - .copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .accentColorGreen); - break; - case CommitStatus.isOldCommit: - indicationStyle = STextStyles - .itemSubtitle(context) - .copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .accentColorYellow); - break; - case CommitStatus.notACommit: - indicationStyle = STextStyles - .itemSubtitle(context) - .copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .accentColorRed); - break; - default: - break; - } - return Column( + ), + const SizedBox( + width: 400, + ), + Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "Epic Cash Build Commit", + "Build number", style: STextStyles .desktopTextExtraExtraSmall( context) @@ -502,82 +301,24 @@ class DesktopAboutView extends ConsumerWidget { height: 2, ), SelectableText( - epicCashCommit, - style: indicationStyle, + build, + style: + STextStyles.itemSubtitle( + context), ), ], - ); - }), - const SizedBox( - width: 105, - ), - FutureBuilder( - future: commitMoneroFuture, - builder: (context, - AsyncSnapshot<dynamic> snapshot) { - bool commitExists = false; - bool isHead = false; - CommitStatus stateOfCommit = - CommitStatus.notLoaded; - - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData) { - commitExists = - snapshot.data![0] as bool; - isHead = - snapshot.data![1] as bool; - if (commitExists && isHead) { - stateOfCommit = - CommitStatus.isHead; - } else if (commitExists) { - stateOfCommit = - CommitStatus.isOldCommit; - } else { - stateOfCommit = - CommitStatus.notACommit; - } - } - TextStyle indicationStyle = - STextStyles.itemSubtitle( - context); - switch (stateOfCommit) { - case CommitStatus.isHead: - indicationStyle = STextStyles - .itemSubtitle(context) - .copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .accentColorGreen); - break; - case CommitStatus.isOldCommit: - indicationStyle = STextStyles - .itemSubtitle(context) - .copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .accentColorYellow); - break; - case CommitStatus.notACommit: - indicationStyle = STextStyles - .itemSubtitle(context) - .copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .accentColorRed); - break; - default: - break; - } - return Column( + ), + ], + ), + const SizedBox(height: 32), + Row( + children: [ + Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "Monero Build Commit", + "Build signature", style: STextStyles .desktopTextExtraExtraSmall( context) @@ -592,60 +333,380 @@ class DesktopAboutView extends ConsumerWidget { height: 2, ), SelectableText( - moneroCommit, - style: indicationStyle, + signature, + style: + STextStyles.itemSubtitle( + context), ), ], - ); - }), - ], - ), - const SizedBox(height: 35), - Row( - children: [ - Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - "Website", - style: STextStyles - .desktopTextExtraExtraSmall( - context) - .copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .textDark), - ), - const SizedBox( - height: 2, - ), - BlueTextButton( - text: "https://stackwallet.com", - onTap: () { - launchUrl( - Uri.parse( - "https://stackwallet.com"), - mode: LaunchMode - .externalApplication, - ); - }, - ), - ], - ) - ], - ) - ], - ); - }, + ), + const SizedBox( + width: 350, + ), + FutureBuilder( + future: commitFiroFuture, + builder: (context, + AsyncSnapshot<dynamic> + snapshot) { + bool commitExists = false; + bool isHead = false; + CommitStatus stateOfCommit = + CommitStatus.notLoaded; + + if (snapshot.connectionState == + ConnectionState + .done && + snapshot.hasData) { + commitExists = snapshot + .data![0] as bool; + isHead = snapshot.data![1] + as bool; + if (commitExists && + isHead) { + stateOfCommit = + CommitStatus.isHead; + } else if (commitExists) { + stateOfCommit = + CommitStatus + .isOldCommit; + } else { + stateOfCommit = + CommitStatus + .notACommit; + } + } + TextStyle indicationStyle = + STextStyles.itemSubtitle( + context); + switch (stateOfCommit) { + case CommitStatus.isHead: + indicationStyle = STextStyles + .itemSubtitle( + context) + .copyWith( + color: Theme.of( + context) + .extension< + StackColors>()! + .accentColorGreen); + break; + case CommitStatus + .isOldCommit: + indicationStyle = STextStyles + .itemSubtitle( + context) + .copyWith( + color: Theme.of( + context) + .extension< + StackColors>()! + .accentColorYellow); + break; + case CommitStatus + .notACommit: + indicationStyle = STextStyles + .itemSubtitle( + context) + .copyWith( + color: Theme.of( + context) + .extension< + StackColors>()! + .accentColorRed); + break; + default: + break; + } + return Column( + crossAxisAlignment: + CrossAxisAlignment + .start, + children: [ + Text( + "Firo Build Commit", + style: STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of( + context) + .extension< + StackColors>()! + .textDark), + ), + const SizedBox( + height: 2, + ), + SelectableText( + firoCommit, + style: indicationStyle, + ), + ], + ); + }), + ], + ), + const SizedBox(height: 35), + Row( + children: [ + FutureBuilder( + future: commitEpicFuture, + builder: (context, + AsyncSnapshot<dynamic> + snapshot) { + bool commitExists = false; + bool isHead = false; + CommitStatus stateOfCommit = + CommitStatus.notLoaded; + + if (snapshot.connectionState == + ConnectionState + .done && + snapshot.hasData) { + commitExists = snapshot + .data![0] as bool; + isHead = snapshot.data![1] + as bool; + if (commitExists && + isHead) { + stateOfCommit = + CommitStatus.isHead; + } else if (commitExists) { + stateOfCommit = + CommitStatus + .isOldCommit; + } else { + stateOfCommit = + CommitStatus + .notACommit; + } + } + TextStyle indicationStyle = + STextStyles.itemSubtitle( + context); + switch (stateOfCommit) { + case CommitStatus.isHead: + indicationStyle = STextStyles + .itemSubtitle( + context) + .copyWith( + color: Theme.of( + context) + .extension< + StackColors>()! + .accentColorGreen); + break; + case CommitStatus + .isOldCommit: + indicationStyle = STextStyles + .itemSubtitle( + context) + .copyWith( + color: Theme.of( + context) + .extension< + StackColors>()! + .accentColorYellow); + break; + case CommitStatus + .notACommit: + indicationStyle = STextStyles + .itemSubtitle( + context) + .copyWith( + color: Theme.of( + context) + .extension< + StackColors>()! + .accentColorRed); + break; + default: + break; + } + return Column( + crossAxisAlignment: + CrossAxisAlignment + .start, + children: [ + Text( + "Epic Cash Build Commit", + style: STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of( + context) + .extension< + StackColors>()! + .textDark), + ), + const SizedBox( + height: 2, + ), + SelectableText( + epicCashCommit, + style: indicationStyle, + ), + ], + ); + }), + const SizedBox( + width: 105, + ), + FutureBuilder( + future: commitMoneroFuture, + builder: (context, + AsyncSnapshot<dynamic> + snapshot) { + bool commitExists = false; + bool isHead = false; + CommitStatus stateOfCommit = + CommitStatus.notLoaded; + + if (snapshot.connectionState == + ConnectionState + .done && + snapshot.hasData) { + commitExists = snapshot + .data![0] as bool; + isHead = snapshot.data![1] + as bool; + if (commitExists && + isHead) { + stateOfCommit = + CommitStatus.isHead; + } else if (commitExists) { + stateOfCommit = + CommitStatus + .isOldCommit; + } else { + stateOfCommit = + CommitStatus + .notACommit; + } + } + TextStyle indicationStyle = + STextStyles.itemSubtitle( + context); + switch (stateOfCommit) { + case CommitStatus.isHead: + indicationStyle = STextStyles + .itemSubtitle( + context) + .copyWith( + color: Theme.of( + context) + .extension< + StackColors>()! + .accentColorGreen); + break; + case CommitStatus + .isOldCommit: + indicationStyle = STextStyles + .itemSubtitle( + context) + .copyWith( + color: Theme.of( + context) + .extension< + StackColors>()! + .accentColorYellow); + break; + case CommitStatus + .notACommit: + indicationStyle = STextStyles + .itemSubtitle( + context) + .copyWith( + color: Theme.of( + context) + .extension< + StackColors>()! + .accentColorRed); + break; + default: + break; + } + return Column( + crossAxisAlignment: + CrossAxisAlignment + .start, + children: [ + Text( + "Monero Build Commit", + style: STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of( + context) + .extension< + StackColors>()! + .textDark), + ), + const SizedBox( + height: 2, + ), + SelectableText( + moneroCommit, + style: indicationStyle, + ), + ], + ); + }), + ], + ), + const SizedBox(height: 35), + Row( + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Website", + style: STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of( + context) + .extension< + StackColors>()! + .textDark), + ), + const SizedBox( + height: 2, + ), + BlueTextButton( + text: + "https://stackwallet.com", + onTap: () { + launchUrl( + Uri.parse( + "https://stackwallet.com"), + mode: LaunchMode + .externalApplication, + ); + }, + ), + ], + ) + ], + ) + ], + ); + }, + ) + ], + ), ) ], ), - ) - ], + ), + ), ), - ), + ], ), ), ], From 543f9631d845838c1cab2e39fc38e9f077e7d4a9 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Tue, 8 Nov 2022 09:34:47 -0700 Subject: [PATCH 16/22] changed desktop textbox fontsize --- .../stack_backup_views/create_backup_view.dart | 4 ++++ .../stack_backup_views/restore_from_file_view.dart | 2 ++ .../backup_and_restore/backup_and_restore_settings.dart | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart index 30fcb7962..48f1a1b7f 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart @@ -272,6 +272,8 @@ class _RestoreFromFileViewState extends State<CreateBackupView> { passwordFocusNode, context, ).copyWith( + labelStyle: + isDesktop ? STextStyles.fieldLabel(context) : null, suffixIcon: UnconstrainedBox( child: Row( children: [ @@ -403,6 +405,8 @@ class _RestoreFromFileViewState extends State<CreateBackupView> { passwordRepeatFocusNode, context, ).copyWith( + labelStyle: + isDesktop ? STextStyles.fieldLabel(context) : null, suffixIcon: UnconstrainedBox( child: Row( children: [ diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_file_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_file_view.dart index 0c101d0b3..c73d596f0 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_file_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_file_view.dart @@ -245,6 +245,8 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> { passwordFocusNode, context, ).copyWith( + labelStyle: + isDesktop ? STextStyles.fieldLabel(context) : null, suffixIcon: UnconstrainedBox( child: Row( children: [ diff --git a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart index 49debf22f..8928a268d 100644 --- a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart @@ -269,7 +269,7 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> { crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( - padding: EdgeInsets.all( + padding: const EdgeInsets.all( 10, ), child: restoreBackup From eea5225ba5c05a4c0cae4607cb8522b85a1cdc56 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Tue, 8 Nov 2022 10:07:38 -0700 Subject: [PATCH 17/22] button correction for desktop manual backup --- .../create_backup_view.dart | 356 ++++++++++++------ 1 file changed, 239 insertions(+), 117 deletions(-) diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart index 48f1a1b7f..51a1d7218 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart @@ -158,24 +158,24 @@ class _RestoreFromFileViewState extends State<CreateBackupView> { ), ), child, - const SizedBox(height: 20), - Row( - children: [ - PrimaryButton( - desktopMed: true, - width: 200, - label: "Create backup", - onPressed: () {}, - ), - const SizedBox(width: 16), - SecondaryButton( - desktopMed: true, - width: 200, - label: "Cancel", - onPressed: () {}, - ), - ], - ), + // const SizedBox(height: 20), + // Row( + // children: [ + // PrimaryButton( + // desktopMed: true, + // width: 200, + // label: "Create backup", + // onPressed: () {}, + // ), + // const SizedBox(width: 16), + // SecondaryButton( + // desktopMed: true, + // width: 200, + // label: "Cancel", + // onPressed: () {}, + // ), + // ], + // ), ], ); }, @@ -447,112 +447,234 @@ class _RestoreFromFileViewState extends State<CreateBackupView> { height: 16, ), if (!isDesktop) const Spacer(), - TextButton( - style: shouldEnableCreate - ? Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context) - : Theme.of(context) - .extension<StackColors>()! - .getPrimaryDisabledButtonColor(context), - onPressed: !shouldEnableCreate - ? null - : () async { - final String pathToSave = fileLocationController.text; - final String passphrase = passwordController.text; - final String repeatPassphrase = - passwordRepeatController.text; + !isDesktop + ? TextButton( + style: shouldEnableCreate + ? Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context) + : Theme.of(context) + .extension<StackColors>()! + .getPrimaryDisabledButtonColor(context), + onPressed: !shouldEnableCreate + ? null + : () async { + final String pathToSave = + fileLocationController.text; + final String passphrase = passwordController.text; + final String repeatPassphrase = + passwordRepeatController.text; - if (pathToSave.isEmpty) { - unawaited(showFloatingFlushBar( - type: FlushBarType.warning, - message: "Directory not chosen", - context: context, - )); - return; - } - if (!(await Directory(pathToSave).exists())) { - unawaited(showFloatingFlushBar( - type: FlushBarType.warning, - message: "Directory does not exist", - context: context, - )); - return; - } - if (passphrase.isEmpty) { - unawaited(showFloatingFlushBar( - type: FlushBarType.warning, - message: "A passphrase is required", - context: context, - )); - return; - } - if (passphrase != repeatPassphrase) { - unawaited(showFloatingFlushBar( - type: FlushBarType.warning, - message: "Passphrase does not match", - context: context, - )); - return; - } + if (pathToSave.isEmpty) { + unawaited(showFloatingFlushBar( + type: FlushBarType.warning, + message: "Directory not chosen", + context: context, + )); + return; + } + if (!(await Directory(pathToSave).exists())) { + unawaited(showFloatingFlushBar( + type: FlushBarType.warning, + message: "Directory does not exist", + context: context, + )); + return; + } + if (passphrase.isEmpty) { + unawaited(showFloatingFlushBar( + type: FlushBarType.warning, + message: "A passphrase is required", + context: context, + )); + return; + } + if (passphrase != repeatPassphrase) { + unawaited(showFloatingFlushBar( + type: FlushBarType.warning, + message: "Passphrase does not match", + context: context, + )); + return; + } - unawaited(showDialog<dynamic>( - context: context, - barrierDismissible: false, - builder: (_) => const StackDialog( - title: "Encrypting backup", - message: "This shouldn't take long", - ), - )); - // make sure the dialog is able to be displayed for at least 1 second - await Future<void>.delayed(const Duration(seconds: 1)); + unawaited(showDialog<dynamic>( + context: context, + barrierDismissible: false, + builder: (_) => const StackDialog( + title: "Encrypting backup", + message: "This shouldn't take long", + ), + )); + // make sure the dialog is able to be displayed for at least 1 second + await Future<void>.delayed( + const Duration(seconds: 1)); - final DateTime now = DateTime.now(); - final String fileToSave = - "$pathToSave/stackbackup_${now.year}_${now.month}_${now.day}_${now.hour}_${now.minute}_${now.second}.swb"; + final DateTime now = DateTime.now(); + final String fileToSave = + "$pathToSave/stackbackup_${now.year}_${now.month}_${now.day}_${now.hour}_${now.minute}_${now.second}.swb"; - final backup = await SWB.createStackWalletJSON(); + final backup = await SWB.createStackWalletJSON(); - bool result = await SWB.encryptStackWalletWithPassphrase( - fileToSave, - passphrase, - jsonEncode(backup), - ); + bool result = + await SWB.encryptStackWalletWithPassphrase( + fileToSave, + passphrase, + jsonEncode(backup), + ); - if (mounted) { - // pop encryption progress dialog - Navigator.of(context).pop(); + if (mounted) { + // pop encryption progress dialog + Navigator.of(context).pop(); - if (result) { - await showDialog<dynamic>( - context: context, - barrierDismissible: false, - builder: (_) => Platform.isAndroid - ? StackOkDialog( - title: "Backup saved to:", - message: fileToSave, - ) - : const StackOkDialog( - title: "Backup creation succeeded"), - ); - passwordController.text = ""; - passwordRepeatController.text = ""; - setState(() {}); - } else { - await showDialog<dynamic>( - context: context, - barrierDismissible: false, - builder: (_) => const StackOkDialog( - title: "Backup creation failed"), - ); - } - } - }, - child: Text( - "Create backup", - style: STextStyles.button(context), - ), - ), + if (result) { + await showDialog<dynamic>( + context: context, + barrierDismissible: false, + builder: (_) => Platform.isAndroid + ? StackOkDialog( + title: "Backup saved to:", + message: fileToSave, + ) + : const StackOkDialog( + title: "Backup creation succeeded"), + ); + passwordController.text = ""; + passwordRepeatController.text = ""; + setState(() {}); + } else { + await showDialog<dynamic>( + context: context, + barrierDismissible: false, + builder: (_) => const StackOkDialog( + title: "Backup creation failed"), + ); + } + } + }, + child: Text( + "Create backup", + style: STextStyles.button(context), + ), + ) + : Row( + children: [ + PrimaryButton( + width: 183, + desktopMed: true, + label: "Create backup", + enabled: shouldEnableCreate, + onPressed: !shouldEnableCreate + ? null + : () async { + final String pathToSave = + fileLocationController.text; + final String passphrase = + passwordController.text; + final String repeatPassphrase = + passwordRepeatController.text; + + if (pathToSave.isEmpty) { + unawaited(showFloatingFlushBar( + type: FlushBarType.warning, + message: "Directory not chosen", + context: context, + )); + return; + } + if (!(await Directory(pathToSave).exists())) { + unawaited(showFloatingFlushBar( + type: FlushBarType.warning, + message: "Directory does not exist", + context: context, + )); + return; + } + if (passphrase.isEmpty) { + unawaited(showFloatingFlushBar( + type: FlushBarType.warning, + message: "A passphrase is required", + context: context, + )); + return; + } + if (passphrase != repeatPassphrase) { + unawaited(showFloatingFlushBar( + type: FlushBarType.warning, + message: "Passphrase does not match", + context: context, + )); + return; + } + + unawaited(showDialog<dynamic>( + context: context, + barrierDismissible: false, + builder: (_) => const StackDialog( + title: "Encrypting backup", + message: "This shouldn't take long", + ), + )); + // make sure the dialog is able to be displayed for at least 1 second + await Future<void>.delayed( + const Duration(seconds: 1)); + + final DateTime now = DateTime.now(); + final String fileToSave = + "$pathToSave/stackbackup_${now.year}_${now.month}_${now.day}_${now.hour}_${now.minute}_${now.second}.swb"; + + final backup = + await SWB.createStackWalletJSON(); + + bool result = + await SWB.encryptStackWalletWithPassphrase( + fileToSave, + passphrase, + jsonEncode(backup), + ); + + if (mounted) { + // pop encryption progress dialog + Navigator.of(context).pop(); + + if (result) { + await showDialog<dynamic>( + context: context, + barrierDismissible: false, + builder: (_) => Platform.isAndroid + ? StackOkDialog( + title: "Backup saved to:", + message: fileToSave, + ) + : const StackOkDialog( + title: + "Backup creation succeeded"), + ); + passwordController.text = ""; + passwordRepeatController.text = ""; + setState(() {}); + } else { + await showDialog<dynamic>( + context: context, + barrierDismissible: false, + builder: (_) => const StackOkDialog( + title: "Backup creation failed"), + ); + } + } + }, + ), + const SizedBox( + width: 16, + ), + SecondaryButton( + width: 183, + desktopMed: true, + label: "Cancel", + onPressed: () {}, + ), + ], + ), ], ), ), From 8af1350b95acc35ffb0e83e3ea67a32453b53a2f Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Tue, 8 Nov 2022 10:50:12 -0700 Subject: [PATCH 18/22] button correction for desktop restore backup and other ui fixes --- .../create_backup_view.dart | 37 +- .../restore_from_file_view.dart | 354 ++++++++++++------ 2 files changed, 251 insertions(+), 140 deletions(-) diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart index 51a1d7218..eacdda66a 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart @@ -147,7 +147,7 @@ class _RestoreFromFileViewState extends State<CreateBackupView> { crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( - padding: const EdgeInsets.all(10.0), + padding: const EdgeInsets.only(bottom: 10), child: Text( "Choose file location", style: STextStyles.desktopTextExtraExtraSmall(context) @@ -158,24 +158,6 @@ class _RestoreFromFileViewState extends State<CreateBackupView> { ), ), child, - // const SizedBox(height: 20), - // Row( - // children: [ - // PrimaryButton( - // desktopMed: true, - // width: 200, - // label: "Create backup", - // onPressed: () {}, - // ), - // const SizedBox(width: 16), - // SecondaryButton( - // desktopMed: true, - // width: 200, - // label: "Cancel", - // onPressed: () {}, - // ), - // ], - // ), ], ); }, @@ -252,8 +234,21 @@ class _RestoreFromFileViewState extends State<CreateBackupView> { ); }), if (!Platform.isAndroid) - const SizedBox( - height: 8, + SizedBox( + height: !isDesktop ? 8 : 24, + ), + if (isDesktop) + Padding( + padding: const EdgeInsets.only(bottom: 10.0), + child: Text( + "Create a passphrase", + style: STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark3), + textAlign: TextAlign.left, + ), ), ClipRRect( borderRadius: BorderRadius.circular( diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_file_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_file_view.dart index c73d596f0..f7a9883de 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_file_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_file_view.dart @@ -131,7 +131,7 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> { crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( - padding: const EdgeInsets.all(10.0), + padding: const EdgeInsets.only(bottom: 10.0), child: Text( "Choose file location", style: STextStyles.desktopTextExtraExtraSmall(context) @@ -143,26 +143,6 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> { ), ), child, - const SizedBox(height: 20), - Row( - children: [ - PrimaryButton( - desktopMed: true, - width: 200, - label: "Restore", - onPressed: () { - restoreBackupPopup(context); - }, - ), - const SizedBox(width: 16), - SecondaryButton( - desktopMed: true, - width: 200, - label: "Cancel", - onPressed: () {}, - ), - ], - ), ], ); }, @@ -225,9 +205,22 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> { ), onChanged: (newValue) {}, ), - const SizedBox( - height: 8, + SizedBox( + height: !isDesktop ? 8 : 24, ), + if (isDesktop) + Padding( + padding: const EdgeInsets.only(bottom: 10.0), + child: Text( + "Enter passphrase", + style: STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark3), + textAlign: TextAlign.left, + ), + ), ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -288,113 +281,236 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> { height: 16, ), if (!isDesktop) const Spacer(), - TextButton( - style: passwordController.text.isEmpty || - fileLocationController.text.isEmpty - ? Theme.of(context) - .extension<StackColors>()! - .getPrimaryDisabledButtonColor(context) - : Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - onPressed: passwordController.text.isEmpty || - fileLocationController.text.isEmpty - ? null - : () async { - final String fileToRestore = - fileLocationController.text; - final String passphrase = passwordController.text; + !isDesktop + ? TextButton( + style: passwordController.text.isEmpty || + fileLocationController.text.isEmpty + ? Theme.of(context) + .extension<StackColors>()! + .getPrimaryDisabledButtonColor(context) + : Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context), + onPressed: passwordController.text.isEmpty || + fileLocationController.text.isEmpty + ? null + : () async { + final String fileToRestore = + fileLocationController.text; + final String passphrase = passwordController.text; - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed( - const Duration(milliseconds: 75)); - } + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed( + const Duration(milliseconds: 75)); + } - if (!(await File(fileToRestore).exists())) { - showFloatingFlushBar( - type: FlushBarType.warning, - message: "Backup file does not exist", - context: context, - ); - return; - } + if (!(await File(fileToRestore).exists())) { + await showFloatingFlushBar( + type: FlushBarType.warning, + message: "Backup file does not exist", + context: context, + ); + return; + } - bool shouldPop = false; - showDialog<dynamic>( - barrierDismissible: false, - context: context, - builder: (_) => WillPopScope( - onWillPop: () async { - return shouldPop; - }, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Material( - color: Colors.transparent, - child: Center( - child: Text( - "Decrypting Stack backup file", - style: STextStyles.pageTitleH2(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textWhite, + bool shouldPop = false; + await showDialog<dynamic>( + barrierDismissible: false, + context: context, + builder: (_) => WillPopScope( + onWillPop: () async { + return shouldPop; + }, + child: Column( + crossAxisAlignment: + CrossAxisAlignment.stretch, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Material( + color: Colors.transparent, + child: Center( + child: Text( + "Decrypting Stack backup file", + style: + STextStyles.pageTitleH2(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textWhite, + ), + ), + ), ), + const SizedBox( + height: 64, + ), + const Center( + child: LoadingIndicator( + width: 100, + ), + ), + ], + ), + ), + ); + + final String? jsonString = await compute( + SWB.decryptStackWalletWithPassphrase, + Tuple2(fileToRestore, passphrase), + debugLabel: "stack wallet decryption compute", + ); + + if (mounted) { + // pop LoadingIndicator + shouldPop = true; + Navigator.of(context).pop(); + + passwordController.text = ""; + + if (jsonString == null) { + await showFloatingFlushBar( + type: FlushBarType.warning, + message: "Failed to decrypt backup file", + context: context, + ); + return; + } + + await Navigator.of(context).push( + RouteGenerator.getRoute( + builder: (_) => StackRestoreProgressView( + jsonString: jsonString, ), ), - ), - const SizedBox( - height: 64, - ), - const Center( - child: LoadingIndicator( - width: 100, - ), - ), - ], - ), - ), - ); + ); + } + }, + child: Text( + "Restore", + style: STextStyles.button(context), + ), + ) + : Row( + children: [ + PrimaryButton( + width: 183, + desktopMed: true, + label: "Restore", + enabled: !(passwordController.text.isEmpty || + fileLocationController.text.isEmpty), + onPressed: passwordController.text.isEmpty || + fileLocationController.text.isEmpty + ? null + : () async { + final String fileToRestore = + fileLocationController.text; + final String passphrase = + passwordController.text; - final String? jsonString = await compute( - SWB.decryptStackWalletWithPassphrase, - Tuple2(fileToRestore, passphrase), - debugLabel: "stack wallet decryption compute", - ); + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed( + const Duration(milliseconds: 75)); + } - if (mounted) { - // pop LoadingIndicator - shouldPop = true; - Navigator.of(context).pop(); + if (!(await File(fileToRestore).exists())) { + await showFloatingFlushBar( + type: FlushBarType.warning, + message: "Backup file does not exist", + context: context, + ); + return; + } - passwordController.text = ""; + bool shouldPop = false; + await showDialog<dynamic>( + barrierDismissible: false, + context: context, + builder: (_) => WillPopScope( + onWillPop: () async { + return shouldPop; + }, + child: Column( + crossAxisAlignment: + CrossAxisAlignment.stretch, + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Material( + color: Colors.transparent, + child: Center( + child: Text( + "Decrypting Stack backup file", + style: STextStyles.pageTitleH2( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textWhite, + ), + ), + ), + ), + const SizedBox( + height: 64, + ), + const Center( + child: LoadingIndicator( + width: 100, + ), + ), + ], + ), + ), + ); - if (jsonString == null) { - showFloatingFlushBar( - type: FlushBarType.warning, - message: "Failed to decrypt backup file", - context: context, - ); - return; - } + final String? jsonString = await compute( + SWB.decryptStackWalletWithPassphrase, + Tuple2(fileToRestore, passphrase), + debugLabel: + "stack wallet decryption compute", + ); - Navigator.of(context).push( - RouteGenerator.getRoute( - builder: (_) => StackRestoreProgressView( - jsonString: jsonString, - ), - ), - ); - } - }, - child: Text( - "Restore", - style: STextStyles.button(context), - ), - ), + if (mounted) { + // pop LoadingIndicator + shouldPop = true; + Navigator.of(context).pop(); + + passwordController.text = ""; + + if (jsonString == null) { + await showFloatingFlushBar( + type: FlushBarType.warning, + message: + "Failed to decrypt backup file", + context: context, + ); + return; + } + + await Navigator.of(context).push( + RouteGenerator.getRoute( + builder: (_) => + StackRestoreProgressView( + jsonString: jsonString, + ), + ), + ); + } + }, + ), + const SizedBox( + width: 16, + ), + SecondaryButton( + width: 183, + desktopMed: true, + label: "Cancel", + onPressed: () {}, + ), + ], + ), ], ), )); From 95716bd0f6be7f5b7c6dcbb08b03efa6a5dfb545 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Tue, 8 Nov 2022 12:15:31 -0700 Subject: [PATCH 19/22] added textfield functionality to desktop create auto backup --- .../create_auto_backup.dart | 489 +++++++++++------- 1 file changed, 306 insertions(+), 183 deletions(-) diff --git a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart index f3e502bcb..57a8d7a64 100644 --- a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart +++ b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart @@ -1,14 +1,23 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/helpers/stack_file_system.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/enums/log_level_enum.dart'; +import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/progress_bar.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; +import 'package:zxcvbn/zxcvbn.dart'; class CreateAutoBackup extends StatefulWidget { const CreateAutoBackup({Key? key}) : super(key: key); @@ -22,13 +31,24 @@ class _CreateAutoBackup extends State<CreateAutoBackup> { late final TextEditingController passphraseController; late final TextEditingController passphraseRepeatController; - late final FocusNode chooseFileLocation; + late final StackFileSystem stackFileSystem; late final FocusNode passphraseFocusNode; late final FocusNode passphraseRepeatFocusNode; + final zxcvbn = Zxcvbn(); bool shouldShowPasswordHint = true; bool hidePassword = true; + String passwordFeedback = + "Add another word or two. Uncommon words are better. Use a few words, avoid common phrases. No need for symbols, digits, or uppercase letters."; + double passwordStrength = 0.0; + + bool get shouldEnableCreate { + return fileLocationController.text.isNotEmpty && + passphraseController.text.isNotEmpty && + passphraseRepeatController.text.isNotEmpty; + } + bool get fieldsMatch => passphraseController.text == passphraseRepeatController.text; @@ -42,14 +62,26 @@ class _CreateAutoBackup extends State<CreateAutoBackup> { @override void initState() { + stackFileSystem = StackFileSystem(); + fileLocationController = TextEditingController(); passphraseController = TextEditingController(); passphraseRepeatController = TextEditingController(); - chooseFileLocation = FocusNode(); passphraseFocusNode = FocusNode(); passphraseRepeatFocusNode = FocusNode(); + if (Platform.isAndroid) { + WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { + final dir = await stackFileSystem.prepareStorage(); + if (mounted) { + setState(() { + fileLocationController.text = dir.path; + }); + } + }); + } + super.initState(); } @@ -59,7 +91,6 @@ class _CreateAutoBackup extends State<CreateAutoBackup> { passphraseController.dispose(); passphraseRepeatController.dispose(); - chooseFileLocation.dispose(); passphraseFocusNode.dispose(); passphraseRepeatFocusNode.dispose(); @@ -71,9 +102,9 @@ class _CreateAutoBackup extends State<CreateAutoBackup> { debugPrint("BUILD: $runtimeType "); String? selectedItem = "Every 10 minutes"; - + final isDesktop = Util.isDesktop; return DesktopDialog( - maxHeight: 650, + maxHeight: 680, maxWidth: 600, child: Column( children: [ @@ -127,198 +158,289 @@ class _CreateAutoBackup extends State<CreateAutoBackup> { height: 10, ), Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - ), - child: ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: const Key("backupChooseFileLocation"), - focusNode: chooseFileLocation, - controller: fileLocationController, - style: STextStyles.desktopTextMedium(context).copyWith( - height: 2, - ), - textAlign: TextAlign.left, - enableSuggestions: false, - autocorrect: false, - decoration: standardInputDecoration( - "Save to...", - chooseFileLocation, - context, - ).copyWith( - labelStyle: - STextStyles.desktopTextExtraExtraSmall(context).copyWith( - color: - Theme.of(context).extension<StackColors>()!.textDark3, - ), - suffixIcon: Container( - decoration: BoxDecoration( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (!Platform.isAndroid) + Consumer(builder: (context, ref, __) { + return Container( color: Colors.transparent, - borderRadius: BorderRadius.circular(1000), - ), - height: 32, - width: 32, - child: Center( - child: SvgPicture.asset( - Assets.svg.folder, - color: Theme.of(context) - .extension<StackColors>()! - .textDark3, - width: 20, - height: 17.5, - ), - ), - ), - ), - ), - ), - ), - const SizedBox( - height: 24, - ), - Container( - alignment: Alignment.centerLeft, - padding: const EdgeInsets.only(left: 32), - child: Text( - "Create a passphrase", - style: STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context).extension<StackColors>()!.textDark3, - ), - textAlign: TextAlign.left, - ), - ), - const SizedBox( - height: 10, - ), - Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - ), - child: ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: const Key("createBackupPassphrase"), - focusNode: passphraseFocusNode, - controller: passphraseController, - style: STextStyles.desktopTextMedium(context).copyWith( - height: 2, - ), - obscureText: hidePassword, - enableSuggestions: false, - autocorrect: false, - decoration: standardInputDecoration( - "Create passphrase", - passphraseFocusNode, - context, - ).copyWith( - labelStyle: - STextStyles.desktopTextExtraExtraSmall(context).copyWith( - color: - Theme.of(context).extension<StackColors>()!.textDark3, - ), - suffixIcon: UnconstrainedBox( - child: GestureDetector( - key: const Key( - "createDesktopAutoBackupShowPassphraseButton1"), - onTap: () async { - setState(() { - hidePassword = !hidePassword; - }); - }, - child: Container( - decoration: BoxDecoration( - color: Colors.transparent, - borderRadius: BorderRadius.circular(1000), - ), - height: 32, - width: 32, - child: Center( - child: SvgPicture.asset( - hidePassword ? Assets.svg.eye : Assets.svg.eyeSlash, - color: Theme.of(context) - .extension<StackColors>()! - .textDark3, - width: 20, - height: 17.5, + child: TextField( + autocorrect: false, + enableSuggestions: false, + onTap: Platform.isAndroid + ? null + : () async { + try { + await stackFileSystem.prepareStorage(); + + if (mounted) { + await stackFileSystem.pickDir(context); + } + + if (mounted) { + setState(() { + fileLocationController.text = + stackFileSystem.dirPath ?? ""; + }); + } + } catch (e, s) { + Logging.instance + .log("$e\n$s", level: LogLevel.Error); + } + }, + controller: fileLocationController, + style: STextStyles.field(context), + decoration: InputDecoration( + hintText: "Save to...", + hintStyle: STextStyles.fieldLabel(context), + suffixIcon: UnconstrainedBox( + child: Row( + children: [ + const SizedBox( + width: 16, + ), + SvgPicture.asset( + Assets.svg.folder, + color: Theme.of(context) + .extension<StackColors>()! + .textDark3, + width: 16, + height: 16, + ), + const SizedBox( + width: 12, + ), + ], + ), ), ), + key: const Key( + "createBackupSaveToFileLocationTextFieldKey"), + readOnly: true, + toolbarOptions: const ToolbarOptions( + copy: true, + cut: false, + paste: false, + selectAll: false, + ), + onChanged: (newValue) { + // ref.read(addressEntryDataProvider(widget.id)).address = newValue; + }, ), + ); + }), + if (!Platform.isAndroid) + const SizedBox( + height: 24, + ), + if (isDesktop) + Padding( + padding: const EdgeInsets.only(bottom: 10.0), + child: Text( + "Create a passphrase", + style: STextStyles.desktopTextExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark3), + textAlign: TextAlign.left, ), ), - ), - ), - ), - ), - const SizedBox( - height: 16, - ), - Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - ), - child: ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: const Key("createBackupPassphrase"), - focusNode: passphraseRepeatFocusNode, - controller: passphraseRepeatController, - style: STextStyles.desktopTextMedium(context).copyWith( - height: 2, - ), - obscureText: hidePassword, - enableSuggestions: false, - autocorrect: false, - decoration: standardInputDecoration( - "Confirm passphrase", - passphraseRepeatFocusNode, - context, - ).copyWith( - labelStyle: - STextStyles.desktopTextExtraExtraSmall(context).copyWith( - color: - Theme.of(context).extension<StackColors>()!.textDark3, + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), - suffixIcon: UnconstrainedBox( - child: GestureDetector( - key: const Key( - "createDesktopAutoBackupShowPassphraseButton2"), - onTap: () async { + child: TextField( + key: const Key("createBackupPasswordFieldKey1"), + focusNode: passphraseFocusNode, + controller: passphraseController, + style: STextStyles.field(context), + obscureText: hidePassword, + enableSuggestions: false, + autocorrect: false, + decoration: standardInputDecoration( + "Create passphrase", + passphraseFocusNode, + context, + ).copyWith( + labelStyle: + isDesktop ? STextStyles.fieldLabel(context) : null, + suffixIcon: UnconstrainedBox( + child: Row( + children: [ + const SizedBox( + width: 16, + ), + GestureDetector( + key: const Key( + "createBackupPasswordFieldShowPasswordButtonKey"), + onTap: () async { + setState(() { + hidePassword = !hidePassword; + }); + }, + child: SvgPicture.asset( + hidePassword + ? Assets.svg.eye + : Assets.svg.eyeSlash, + color: Theme.of(context) + .extension<StackColors>()! + .textDark3, + width: 16, + height: 16, + ), + ), + const SizedBox( + width: 12, + ), + ], + ), + ), + ), + onChanged: (newValue) { + if (newValue.isEmpty) { setState(() { - hidePassword = !hidePassword; + passwordFeedback = ""; }); - }, - child: Container( - decoration: BoxDecoration( - color: Colors.transparent, - borderRadius: BorderRadius.circular(1000), - ), - height: 32, - width: 32, - child: Center( - child: SvgPicture.asset( - hidePassword ? Assets.svg.eye : Assets.svg.eyeSlash, - color: Theme.of(context) - .extension<StackColors>()! - .textDark3, - width: 20, - height: 17.5, - ), + return; + } + final result = zxcvbn.evaluate(newValue); + String suggestionsAndTips = ""; + for (var sug in result.feedback.suggestions!.toSet()) { + suggestionsAndTips += "$sug\n"; + } + suggestionsAndTips += result.feedback.warning!; + String feedback = + // "Password Strength: ${((result.score! / 4.0) * 100).toInt()}%\n" + suggestionsAndTips; + + passwordStrength = result.score! / 4; + + // hack fix to format back string returned from zxcvbn + if (feedback.contains("phrasesNo need")) { + feedback = feedback.replaceFirst( + "phrasesNo need", "phrases\nNo need"); + } + + if (feedback.endsWith("\n")) { + feedback = feedback.substring(0, feedback.length - 2); + } + + setState(() { + passwordFeedback = feedback; + }); + }, + ), + ), + if (passphraseFocusNode.hasFocus || + passphraseRepeatFocusNode.hasFocus || + passphraseController.text.isNotEmpty) + Padding( + padding: EdgeInsets.only( + left: 12, + right: 12, + top: passwordFeedback.isNotEmpty ? 4 : 0, + ), + child: passwordFeedback.isNotEmpty + ? Text( + passwordFeedback, + style: STextStyles.infoSmall(context), + ) + : null, + ), + if (passphraseFocusNode.hasFocus || + passphraseRepeatFocusNode.hasFocus || + passphraseController.text.isNotEmpty) + Padding( + padding: const EdgeInsets.only( + left: 12, + right: 12, + top: 10, + ), + child: ProgressBar( + key: const Key("createStackBackUpProgressBar"), + width: 510, + height: 5, + fillColor: passwordStrength < 0.51 + ? Theme.of(context) + .extension<StackColors>()! + .accentColorRed + : passwordStrength < 1 + ? Theme.of(context) + .extension<StackColors>()! + .accentColorYellow + : Theme.of(context) + .extension<StackColors>()! + .accentColorGreen, + backgroundColor: Theme.of(context) + .extension<StackColors>()! + .buttonBackSecondary, + percent: + passwordStrength < 0.25 ? 0.03 : passwordStrength, + ), + ), + const SizedBox( + height: 10, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key("createBackupPasswordFieldKey2"), + focusNode: passphraseRepeatFocusNode, + controller: passphraseRepeatController, + style: STextStyles.field(context), + obscureText: hidePassword, + enableSuggestions: false, + autocorrect: false, + decoration: standardInputDecoration( + "Confirm passphrase", + passphraseRepeatFocusNode, + context, + ).copyWith( + labelStyle: STextStyles.fieldLabel(context), + suffixIcon: UnconstrainedBox( + child: Row( + children: [ + const SizedBox( + width: 16, + ), + GestureDetector( + key: const Key( + "createBackupPasswordFieldShowPasswordButtonKey"), + onTap: () async { + setState(() { + hidePassword = !hidePassword; + }); + }, + child: SvgPicture.asset( + hidePassword + ? Assets.svg.eye + : Assets.svg.eyeSlash, + color: Theme.of(context) + .extension<StackColors>()! + .textDark3, + width: 16, + height: 16, + ), + ), + const SizedBox( + width: 12, + ), + ], ), ), ), + onChanged: (newValue) { + setState(() {}); + // TODO: ? check if passwords match? + }, ), ), - ), + ], ), ), const SizedBox( @@ -376,6 +498,7 @@ class _CreateAutoBackup extends State<CreateAutoBackup> { }, ), ), + const Spacer(), Padding( padding: const EdgeInsets.all(32), child: Row( From cede571350db905f4f70ba532f758c8869a9e15c Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Tue, 8 Nov 2022 13:50:53 -0600 Subject: [PATCH 20/22] desktop login/password screen --- .../desktop_login_view.dart | 166 ++++++++++++++++-- lib/utilities/text_styles.dart | 19 ++ .../custom_buttons/blue_text_button.dart | 11 +- 3 files changed, 179 insertions(+), 17 deletions(-) diff --git a/lib/pages_desktop_specific/desktop_login_view.dart b/lib/pages_desktop_specific/desktop_login_view.dart index c986bffde..1c70a5d98 100644 --- a/lib/pages_desktop_specific/desktop_login_view.dart +++ b/lib/pages_desktop_specific/desktop_login_view.dart @@ -1,7 +1,14 @@ import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/pages_desktop_specific/home/desktop_home_view.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; +import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/stack_text_field.dart'; class DesktopLoginView extends StatefulWidget { const DesktopLoginView({ @@ -18,28 +25,155 @@ class DesktopLoginView extends StatefulWidget { } class _DesktopLoginViewState extends State<DesktopLoginView> { + late final TextEditingController passwordController; + + late final FocusNode passwordFocusNode; + + bool hidePassword = true; + bool _continueEnabled = false; + + @override + void initState() { + passwordController = TextEditingController(); + passwordFocusNode = FocusNode(); + + super.initState(); + } + + @override + void dispose() { + passwordController.dispose(); + passwordFocusNode.dispose(); + + super.dispose(); + } + @override Widget build(BuildContext context) { - return Material( - child: Column( + return DesktopScaffold( + body: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ - Text( - "Login", - style: STextStyles.desktopH3(context), - ), - PrimaryButton( - label: "Login", - onPressed: () { - // todo auth + SizedBox( + width: 480, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SvgPicture.asset( + Assets.svg.stackIcon(context), + width: 100, + ), + const SizedBox( + height: 42, + ), + Text( + "Stack Wallet", + style: STextStyles.desktopH1(context), + ), + const SizedBox( + height: 24, + ), + SizedBox( + width: 350, + child: Text( + "Open source multicoin wallet for everyone", + textAlign: TextAlign.center, + style: STextStyles.desktopSubtitleH1(context), + ), + ), + const SizedBox( + height: 24, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key("desktopLoginPasswordFieldKey"), + focusNode: passwordFocusNode, + controller: passwordController, + style: STextStyles.desktopTextMedium(context).copyWith( + height: 2, + ), + obscureText: hidePassword, + enableSuggestions: false, + autocorrect: false, + decoration: standardInputDecoration( + "Enter password", + passwordFocusNode, + context, + ).copyWith( + suffixIcon: UnconstrainedBox( + child: SizedBox( + height: 70, + child: Row( + children: [ + const SizedBox( + width: 24, + ), + GestureDetector( + key: const Key( + "restoreFromFilePasswordFieldShowPasswordButtonKey"), + onTap: () async { + setState(() { + hidePassword = !hidePassword; + }); + }, + child: SvgPicture.asset( + hidePassword + ? Assets.svg.eye + : Assets.svg.eyeSlash, + color: Theme.of(context) + .extension<StackColors>()! + .textDark3, + width: 24, + height: 24, + ), + ), + const SizedBox( + width: 12, + ), + ], + ), + ), + ), + ), + onChanged: (newValue) { + setState(() { + _continueEnabled = passwordController.text.isNotEmpty; + }); + }, + ), + ), + const SizedBox( + height: 24, + ), + PrimaryButton( + label: "Continue", + enabled: _continueEnabled, + onPressed: () { + // todo auth - Navigator.of(context).pushNamedAndRemoveUntil( - DesktopHomeView.routeName, - (route) => false, - ); - }, - ) + Navigator.of(context).pushNamedAndRemoveUntil( + DesktopHomeView.routeName, + (route) => false, + ); + }, + ), + const SizedBox( + height: 60, + ), + BlueTextButton( + text: "Forgot password?", + textSize: 20, + onTap: () { + // todo: new screen + }, + ), + ], + ), + ), ], ), ); diff --git a/lib/utilities/text_styles.dart b/lib/utilities/text_styles.dart index 299ba5bec..63aa19afb 100644 --- a/lib/utilities/text_styles.dart +++ b/lib/utilities/text_styles.dart @@ -508,6 +508,25 @@ class STextStyles { // Desktop + static TextStyle desktopH1(BuildContext context) { + switch (_theme(context).themeType) { + case ThemeType.light: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w600, + fontSize: 40, + height: 40 / 40, + ); + case ThemeType.dark: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w600, + fontSize: 40, + height: 40 / 40, + ); + } + } + static TextStyle desktopH2(BuildContext context) { switch (_theme(context).themeType) { case ThemeType.light: diff --git a/lib/widgets/custom_buttons/blue_text_button.dart b/lib/widgets/custom_buttons/blue_text_button.dart index aa7f75b1f..a87d1e6b2 100644 --- a/lib/widgets/custom_buttons/blue_text_button.dart +++ b/lib/widgets/custom_buttons/blue_text_button.dart @@ -10,11 +10,13 @@ class BlueTextButton extends ConsumerStatefulWidget { required this.text, this.onTap, this.enabled = true, + this.textSize, }) : super(key: key); final String text; final VoidCallback? onTap; final bool enabled; + final double? textSize; @override ConsumerState<BlueTextButton> createState() => _BlueTextButtonState(); @@ -67,7 +69,14 @@ class _BlueTextButtonState extends ConsumerState<BlueTextButton> textAlign: TextAlign.center, text: TextSpan( text: widget.text, - style: STextStyles.link2(context).copyWith(color: color), + style: widget.textSize == null + ? STextStyles.link2(context).copyWith( + color: color, + ) + : STextStyles.link2(context).copyWith( + color: color, + fontSize: widget.textSize, + ), recognizer: widget.enabled ? (TapGestureRecognizer() ..onTap = () { From 97b4407957639d50a1cc9a52c710b6ef9a500baa Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Tue, 8 Nov 2022 14:04:00 -0600 Subject: [PATCH 21/22] desktop forgot password ui --- .../desktop_login_view.dart | 5 +- .../forgot_password_desktop_view.dart | 101 ++++++++++++++++++ lib/route_generator.dart | 7 ++ 3 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 lib/pages_desktop_specific/forgot_password_desktop_view.dart diff --git a/lib/pages_desktop_specific/desktop_login_view.dart b/lib/pages_desktop_specific/desktop_login_view.dart index 1c70a5d98..fe05d719f 100644 --- a/lib/pages_desktop_specific/desktop_login_view.dart +++ b/lib/pages_desktop_specific/desktop_login_view.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/pages_desktop_specific/forgot_password_desktop_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/desktop_home_view.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; @@ -168,7 +169,9 @@ class _DesktopLoginViewState extends State<DesktopLoginView> { text: "Forgot password?", textSize: 20, onTap: () { - // todo: new screen + Navigator.of(context).pushNamed( + ForgotPasswordDesktopView.routeName, + ); }, ), ], diff --git a/lib/pages_desktop_specific/forgot_password_desktop_view.dart b/lib/pages_desktop_specific/forgot_password_desktop_view.dart new file mode 100644 index 000000000..d501cbd38 --- /dev/null +++ b/lib/pages_desktop_specific/forgot_password_desktop_view.dart @@ -0,0 +1,101 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; +import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; + +class ForgotPasswordDesktopView extends StatefulWidget { + const ForgotPasswordDesktopView({ + Key? key, + }) : super(key: key); + + static const String routeName = "/forgotPasswordDesktop"; + + @override + State<ForgotPasswordDesktopView> createState() => + _ForgotPasswordDesktopViewState(); +} + +class _ForgotPasswordDesktopViewState extends State<ForgotPasswordDesktopView> { + @override + Widget build(BuildContext context) { + return DesktopScaffold( + appBar: DesktopAppBar( + leading: AppBarBackButton( + onPressed: () async { + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + isCompactHeight: false, + ), + body: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + width: 480, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SvgPicture.asset( + Assets.svg.stackIcon(context), + width: 100, + ), + const SizedBox( + height: 42, + ), + Text( + "Stack Wallet", + style: STextStyles.desktopH1(context), + ), + const SizedBox( + height: 24, + ), + SizedBox( + width: 400, + child: Text( + "Stack Wallet does not store your password. Create new wallet or use a Stack backup file to restore your wallet.", + textAlign: TextAlign.center, + style: STextStyles.desktopTextSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, + ), + ), + ), + const SizedBox( + height: 48, + ), + PrimaryButton( + label: "Create new wallet", + onPressed: () { + // // todo delete everything and start fresh? + }, + ), + const SizedBox( + height: 24, + ), + SecondaryButton( + label: "Restore from backup", + onPressed: () { + // todo SWB restore + }, + ), + const SizedBox( + height: kDesktopAppBarHeight, + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 40f11dc57..30963781b 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -85,6 +85,7 @@ import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_sear import 'package:stackwallet/pages/wallet_view/wallet_view.dart'; import 'package:stackwallet/pages/wallets_view/wallets_view.dart'; import 'package:stackwallet/pages_desktop_specific/create_password/create_password_view.dart'; +import 'package:stackwallet/pages_desktop_specific/forgot_password_desktop_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/desktop_home_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/desktop_settings_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/my_stack_view.dart'; @@ -998,6 +999,12 @@ class RouteGenerator { builder: (_) => const CreatePasswordView(), settings: RouteSettings(name: settings.name)); + case ForgotPasswordDesktopView.routeName: + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const ForgotPasswordDesktopView(), + settings: RouteSettings(name: settings.name)); + case DesktopHomeView.routeName: return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, From a94e66da9eec4ea809cc8b4a09e717c4fc852e62 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Tue, 8 Nov 2022 19:07:18 -0600 Subject: [PATCH 22/22] temp disable wow 25 word option in ui --- lib/utilities/constants.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/utilities/constants.dart b/lib/utilities/constants.dart index e27fbaa3d..e170dad2a 100644 --- a/lib/utilities/constants.dart +++ b/lib/utilities/constants.dart @@ -62,7 +62,9 @@ abstract class Constants { values.addAll([25]); break; case Coin.wownero: - values.addAll([14, 25]); + values.addAll([14]); + // todo: uncomment when wownero 25 word seeds implemented + // values.addAll([14, 25]); break; } return values;